@maka/maka-cli 5.67.0 → 5.68.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/archetypes.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.d.ts +39 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +85 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.d.ts +37 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +53 -115
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.d.ts +55 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js +203 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +42 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +28 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +69 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/register.d.ts +15 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +32 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/register.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.d.ts +35 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +108 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.d.ts +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.d.ts +57 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.js +162 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.js.map +1 -0
- 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/game.d.ts +50 -11
- package/bundle/typescript/src/commands/game/sideQuest/game.js +148 -28
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +24 -12
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +53 -13
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +31 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +10 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.d.ts +108 -0
- package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.js +186 -0
- package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.d.ts +88 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +137 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +32 -1
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +13 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +36 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +83 -0
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command, ICommand } from './command.js';
|
|
2
|
+
/**
|
|
3
|
+
* KILL COMPLEX FORM (SR5e p.250-251) -- "unravel <name>".
|
|
4
|
+
*
|
|
5
|
+
* The last of the seven Resonance actions on p.250 that this game did
|
|
6
|
+
* not have (see resonance-actions.ts for the audit that found it).
|
|
7
|
+
*
|
|
8
|
+
* THE GAME ALREADY KNEW THIS HOLE WAS HERE, which is the best argument
|
|
9
|
+
* for filling it. dispel.ts refuses counterspelling against Resonance
|
|
10
|
+
* with a deliberate, canon-correct line -- "What's woven around X isn't
|
|
11
|
+
* mana -- it's RESONANCE. Your counterspelling finds nothing to grip."
|
|
12
|
+
* Somebody stood exactly on the edge of this gap, described it
|
|
13
|
+
* precisely, and had no verb to point at. Now that refusal names a real
|
|
14
|
+
* route instead of an absence.
|
|
15
|
+
*
|
|
16
|
+
* RAW: Software + Resonance [Level] v. Complex Form Level + Resonance
|
|
17
|
+
* (the threader's). Every net hit reduces the hits from the form's
|
|
18
|
+
* ORIGINAL threading test; drive that to zero and the form ends. Then
|
|
19
|
+
* you "resist Fading as if you had threaded the complex form you
|
|
20
|
+
* targeted" -- the toll is the form's, not yours, which is what makes
|
|
21
|
+
* killing a big weave genuinely dangerous rather than free.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IT CAN REACH TODAY, said plainly because it is a real limit and
|
|
24
|
+
* not a small one: NOTHING IN A SOLO GAME -- not because the route is
|
|
25
|
+
* closed but because it is GATED. An NPC CAN reach ThreadCommand:
|
|
26
|
+
* `order` runs a command as the companion through NPC.runCommand, which
|
|
27
|
+
* parses the same registry. What stops it is ThreadCommand's own
|
|
28
|
+
* isTechnomancer() check ("The Resonance doesn't know your voice"),
|
|
29
|
+
* and no NPC is a technomancer today. So no NPC holds a form, and a
|
|
30
|
+
* solo runner meets no weave to cut. The reachable target today is
|
|
31
|
+
* another PLAYER on a shared run.
|
|
32
|
+
*
|
|
33
|
+
* That distinction is the load-bearing half: "structurally impossible"
|
|
34
|
+
* and "open but gated" imply very different costs for the NPC-
|
|
35
|
+
* technomancer feature. The plumbing already exists, and an NPC
|
|
36
|
+
* technomancer inherits this through `order` for free.
|
|
37
|
+
*
|
|
38
|
+
* THE WRITERS OF sustainedForms, enumerated -- and a warning about how
|
|
39
|
+
* to enumerate them. `grep sustainedForms` near a mutator finds
|
|
40
|
+
* thread.ts's push and this file's filter, and MISSES thread.ts's drop
|
|
41
|
+
* path, which aliases first (`const held = actor.sustainedForms`) and
|
|
42
|
+
* splices the alias on a later line. An alias defeats a name-grep, so
|
|
43
|
+
* the search that looks thorough returns an incomplete answer. The full
|
|
44
|
+
* set is: thread.ts push (creates), thread.ts splice-via-alias
|
|
45
|
+
* (releases), unravel.ts filter (kills). All three run as a command
|
|
46
|
+
* actor; none can be an NPC while the gate above stands.
|
|
47
|
+
*
|
|
48
|
+
* It refuses your OWN forms on purpose: "thread drop <form>" already
|
|
49
|
+
* releases those, for free, and routing a free action through an
|
|
50
|
+
* opposed test that can hurt you would be a worse answer to the same
|
|
51
|
+
* question.
|
|
52
|
+
*/
|
|
53
|
+
export declare class UnravelCommand extends Command implements ICommand {
|
|
54
|
+
protected static verb: string;
|
|
55
|
+
protected static description: string;
|
|
56
|
+
execute(args?: string[]): Promise<string>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
3
|
+
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
|
+
import { Player } from '../models/player.js';
|
|
6
|
+
import { FORMS } from './thread.js';
|
|
7
|
+
/**
|
|
8
|
+
* KILL COMPLEX FORM (SR5e p.250-251) -- "unravel <name>".
|
|
9
|
+
*
|
|
10
|
+
* The last of the seven Resonance actions on p.250 that this game did
|
|
11
|
+
* not have (see resonance-actions.ts for the audit that found it).
|
|
12
|
+
*
|
|
13
|
+
* THE GAME ALREADY KNEW THIS HOLE WAS HERE, which is the best argument
|
|
14
|
+
* for filling it. dispel.ts refuses counterspelling against Resonance
|
|
15
|
+
* with a deliberate, canon-correct line -- "What's woven around X isn't
|
|
16
|
+
* mana -- it's RESONANCE. Your counterspelling finds nothing to grip."
|
|
17
|
+
* Somebody stood exactly on the edge of this gap, described it
|
|
18
|
+
* precisely, and had no verb to point at. Now that refusal names a real
|
|
19
|
+
* route instead of an absence.
|
|
20
|
+
*
|
|
21
|
+
* RAW: Software + Resonance [Level] v. Complex Form Level + Resonance
|
|
22
|
+
* (the threader's). Every net hit reduces the hits from the form's
|
|
23
|
+
* ORIGINAL threading test; drive that to zero and the form ends. Then
|
|
24
|
+
* you "resist Fading as if you had threaded the complex form you
|
|
25
|
+
* targeted" -- the toll is the form's, not yours, which is what makes
|
|
26
|
+
* killing a big weave genuinely dangerous rather than free.
|
|
27
|
+
*
|
|
28
|
+
* WHAT IT CAN REACH TODAY, said plainly because it is a real limit and
|
|
29
|
+
* not a small one: NOTHING IN A SOLO GAME -- not because the route is
|
|
30
|
+
* closed but because it is GATED. An NPC CAN reach ThreadCommand:
|
|
31
|
+
* `order` runs a command as the companion through NPC.runCommand, which
|
|
32
|
+
* parses the same registry. What stops it is ThreadCommand's own
|
|
33
|
+
* isTechnomancer() check ("The Resonance doesn't know your voice"),
|
|
34
|
+
* and no NPC is a technomancer today. So no NPC holds a form, and a
|
|
35
|
+
* solo runner meets no weave to cut. The reachable target today is
|
|
36
|
+
* another PLAYER on a shared run.
|
|
37
|
+
*
|
|
38
|
+
* That distinction is the load-bearing half: "structurally impossible"
|
|
39
|
+
* and "open but gated" imply very different costs for the NPC-
|
|
40
|
+
* technomancer feature. The plumbing already exists, and an NPC
|
|
41
|
+
* technomancer inherits this through `order` for free.
|
|
42
|
+
*
|
|
43
|
+
* THE WRITERS OF sustainedForms, enumerated -- and a warning about how
|
|
44
|
+
* to enumerate them. `grep sustainedForms` near a mutator finds
|
|
45
|
+
* thread.ts's push and this file's filter, and MISSES thread.ts's drop
|
|
46
|
+
* path, which aliases first (`const held = actor.sustainedForms`) and
|
|
47
|
+
* splices the alias on a later line. An alias defeats a name-grep, so
|
|
48
|
+
* the search that looks thorough returns an incomplete answer. The full
|
|
49
|
+
* set is: thread.ts push (creates), thread.ts splice-via-alias
|
|
50
|
+
* (releases), unravel.ts filter (kills). All three run as a command
|
|
51
|
+
* actor; none can be an NPC while the gate above stands.
|
|
52
|
+
*
|
|
53
|
+
* It refuses your OWN forms on purpose: "thread drop <form>" already
|
|
54
|
+
* releases those, for free, and routing a free action through an
|
|
55
|
+
* opposed test that can hurt you would be a worse answer to the same
|
|
56
|
+
* question.
|
|
57
|
+
*/
|
|
58
|
+
export class UnravelCommand extends Command {
|
|
59
|
+
static verb = 'unravel';
|
|
60
|
+
static description = 'Kill a complex form somebody else is sustaining (technomancers): "unravel <name>" -- Software + Resonance vs their form. Net hits eat the hits that wove it; drive them to zero and it collapses. You resist the fading as though you had threaded it yourself. Your own forms come off free with "thread drop".';
|
|
61
|
+
async execute(args = []) {
|
|
62
|
+
const actor = this.actor;
|
|
63
|
+
if (!actor.isTechnomancer()) {
|
|
64
|
+
return `Complex forms are Resonance, and the Resonance doesn't answer to you. Only a technomancer can unravel one.`;
|
|
65
|
+
}
|
|
66
|
+
// Everyone standing here who is holding something, minus you.
|
|
67
|
+
const room = actor.currentLocation;
|
|
68
|
+
const holders = (room?.getActors() ?? [])
|
|
69
|
+
.filter((a) => a instanceof Player && a !== actor && (a.sustainedForms?.length ?? 0) > 0);
|
|
70
|
+
const targetName = args.join(' ').trim();
|
|
71
|
+
if (holders.length === 0) {
|
|
72
|
+
// Distinguish "nobody is weaving" from "you asked for a name that
|
|
73
|
+
// isn't here" -- the two have different fixes and one message for
|
|
74
|
+
// both sends players hunting for a typo that isn't there.
|
|
75
|
+
const ownForms = actor.sustainedForms.length > 0;
|
|
76
|
+
return `Nothing woven here but your own${ownForms ? ` -- and yours come off free.${hint(` ("thread drop <form>")`)}` : `, and you hold nothing either.`}`;
|
|
77
|
+
}
|
|
78
|
+
let target = holders[0];
|
|
79
|
+
if (targetName) {
|
|
80
|
+
const picked = fuzzyPickName(targetName, holders.map(h => h.name));
|
|
81
|
+
if (!picked) {
|
|
82
|
+
// Naming your own forms is a common and reasonable mistake.
|
|
83
|
+
if (fuzzyPickName(targetName, [actor.name])) {
|
|
84
|
+
return `Your own weave doesn't fight you.${hint(` ("thread drop <form>" releases it, free.)`)}`;
|
|
85
|
+
}
|
|
86
|
+
return `Nobody here by that name is sustaining anything. Holding a form: ${holders.map(h => h.name).join(', ')}.`;
|
|
87
|
+
}
|
|
88
|
+
target = holders.find(h => h.name === picked) ?? holders[0];
|
|
89
|
+
}
|
|
90
|
+
else if (holders.length > 1) {
|
|
91
|
+
return `Unravel WHOSE weave? Holding a form: ${holders.map(h => h.name).join(', ')}.`;
|
|
92
|
+
}
|
|
93
|
+
// The oldest form they hold -- the one that has been shaping the
|
|
94
|
+
// fight longest, and the one a player means by "unravel her".
|
|
95
|
+
const held = target.sustainedForms[0];
|
|
96
|
+
const form = FORMS.find(f => f.key === held.key);
|
|
97
|
+
const formName = form?.name ?? held.key;
|
|
98
|
+
const level = held.level;
|
|
99
|
+
// Fall back to the Level for a form woven before hits were
|
|
100
|
+
// recorded; see Player.sustainedForms.
|
|
101
|
+
const standing = held.hits ?? level;
|
|
102
|
+
// OPPOSED: Software + Resonance [Level] v. Level + their Resonance.
|
|
103
|
+
const software = actor.skillRating('software');
|
|
104
|
+
const roll = rollPool(Math.max(1, actor.resonance + (software > 0 ? software : -1) + actor.woundModifier - actor.sustainingPenalty));
|
|
105
|
+
// Hits capped by the Level, the same limit threading works under.
|
|
106
|
+
const hits = Math.min(roll.hits, level);
|
|
107
|
+
const defense = rollPool(Math.max(1, level + target.resonance));
|
|
108
|
+
const net = Math.max(0, hits - defense.hits);
|
|
109
|
+
const poolParts = [`Resonance ${actor.resonance}`, software > 0 ? `software ${software}` : `no software -1`];
|
|
110
|
+
if (actor.woundModifier < 0)
|
|
111
|
+
poolParts.push(`wounds ${actor.woundModifier}`);
|
|
112
|
+
if (actor.sustainingPenalty > 0)
|
|
113
|
+
poolParts.push(`sustaining -${actor.sustainingPenalty}`);
|
|
114
|
+
this.logger.meta(`Kill Complex Form (${poolParts.join(', ')}) [Level ${level}]: ${formatRoll(roll)} v. ${formatRoll(defense)}`);
|
|
115
|
+
actor.performAction('unravels a complex form', formName);
|
|
116
|
+
const remaining = Math.max(0, standing - net);
|
|
117
|
+
const collapsed = remaining === 0;
|
|
118
|
+
const lines = [
|
|
119
|
+
`You take hold of the ${formName} riding ${target.name} and PULL:`,
|
|
120
|
+
];
|
|
121
|
+
if (collapsed) {
|
|
122
|
+
target.sustainedForms = target.sustainedForms.filter(f => f !== held);
|
|
123
|
+
lines.push(` The weave comes apart in your hands -- ${formName} collapses off ${target.name}.`);
|
|
124
|
+
this.scene.addWorldEvent(`${actor.name} unravelled the ${formName} ${target.name} was sustaining in ${room?.name ?? 'the room'}.`);
|
|
125
|
+
}
|
|
126
|
+
else if (net > 0) {
|
|
127
|
+
held.hits = remaining;
|
|
128
|
+
lines.push(` Threads part and the weave SAGS -- ${net} of its ${standing} hold${standing === 1 ? '' : 's'} torn out, ${remaining} still gripping.`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lines.push(` It holds. ${target.name}'s weave slides out from under you untouched.`);
|
|
132
|
+
}
|
|
133
|
+
// FADING AS THOUGH YOU HAD THREADED IT (p.251) -- their form's
|
|
134
|
+
// value, your Resonance and Willpower to resist. Physical past your
|
|
135
|
+
// own Resonance, the same overreach law threading works under: a
|
|
136
|
+
// technomancer who reaches for a weave bigger than they are pays in
|
|
137
|
+
// body, and that is the whole risk of the action.
|
|
138
|
+
const overreach = level > actor.resonance;
|
|
139
|
+
const fadingValue = Math.max(2, level + (form?.fvDelta ?? 0));
|
|
140
|
+
const resist = rollPool(Math.max(1, actor.willpower + actor.resonance));
|
|
141
|
+
const fadingTaken = Math.max(0, fadingValue - resist.hits);
|
|
142
|
+
this.logger.meta(`Fading (${fadingValue}${overreach ? ', OVERREACH -- physical' : ''}): resist ${formatRoll(resist)}`);
|
|
143
|
+
if (fadingTaken === 0) {
|
|
144
|
+
lines.push(` The fading washes past you.`);
|
|
145
|
+
}
|
|
146
|
+
else if (overreach) {
|
|
147
|
+
actor.takeDamage(fadingTaken);
|
|
148
|
+
lines.push(` You reached past your own Resonance and it burns coming back -- ${fadingTaken} PHYSICAL: ${actor.conditionSummary()}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const { stun, overflow } = actor.takeStun(fadingTaken);
|
|
152
|
+
lines.push(` The fading bites -- ${stun} stun: ${actor.stunSummary()}`);
|
|
153
|
+
if (overflow > 0) {
|
|
154
|
+
lines.push(` It's more than your mind can hold -- ${overflow} box${overflow === 1 ? '' : 'es'} spill into your body: ${actor.conditionSummary()}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.logger.write(`UnravelCommand: ${actor.name} v. ${target.name}'s ${held.key} L${level} -- ${hits} v ${defense.hits} (net ${net}), ${standing} -> ${remaining}, fading ${fadingValue} (took ${fadingTaken}${overreach ? ' physical' : ''}).`);
|
|
158
|
+
this.scene.updateStatus();
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=unravel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unravel.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/commands/unravel.ts"],"names":[],"mappings":"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"}
|
|
@@ -69,5 +69,21 @@
|
|
|
69
69
|
// told they ARE the barrier. They were told about every barrier
|
|
70
70
|
// around them and never about that, so a picket beside an open
|
|
71
71
|
// door offered to let people past while the geometry refused.
|
|
72
|
-
|
|
72
|
+
// 1.16.0 (2026-08-28): SPRITES BECOME REAL COMPANIONS. Three shared-
|
|
73
|
+
// session surfaces move:
|
|
74
|
+
// - SAVE-SHAPE FIELDS the server writes back: a technomancer's
|
|
75
|
+
// registered sprites are a LIST (registeredSprites[]) rather than
|
|
76
|
+
// one slot, because canon caps them at LOGIC and a Logic-many cap
|
|
77
|
+
// over a single field is the same vanishing one step downstream.
|
|
78
|
+
// The old singular field is still READ for existing saves and no
|
|
79
|
+
// longer written, so no SAVE_VERSION bump is owed (the `bodies?`
|
|
80
|
+
// precedent at save-file.ts).
|
|
81
|
+
// - COMMAND SEMANTICS in shared scenes: new verbs `sprites`,
|
|
82
|
+
// `unravel` (Kill Complex Form) and `decompile`; `recall` gained
|
|
83
|
+
// Call/Dismiss Sprite; `compile` no longer implicitly releases;
|
|
84
|
+
// `order <sprite> stand down` no longer destroys the registration.
|
|
85
|
+
// - COMPANION MOVEMENT: a sprite now heels on its own plane, so a
|
|
86
|
+
// jacked-in technomancer keeps it and a walking one does not drag
|
|
87
|
+
// it through the meat world. It was exactly backwards.
|
|
88
|
+
export const ENGINE_VERSION = '1.16.0';
|
|
73
89
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../../../../../src/commands/game/sideQuest/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,sEAAsE;AACtE,6CAA6C;AAC7C,iEAAiE;AACjE,qCAAqC;AACrC,oEAAoE;AACpE,6DAA6D;AAC7D,uEAAuE;AACvE,kDAAkD;AAClD,iEAAiE;AACjE,mEAAmE;AACnE,gEAAgE;AAChE,8DAA8D;AAC9D,kEAAkE;AAClE,kDAAkD;AAClD,kEAAkE;AAClE,4DAA4D;AAC5D,qEAAqE;AACrE,yDAAyD;AACzD,sEAAsE;AACtE,2CAA2C;AAC3C,qEAAqE;AACrE,yDAAyD;AACzD,iEAAiE;AACjE,qEAAqE;AACrE,qEAAqE;AACrE,uEAAuE;AACvE,wEAAwE;AACxE,oEAAoE;AACpE,wEAAwE;AACxE,6DAA6D;AAC7D,uEAAuE;AACvE,sEAAsE;AACtE,0DAA0D;AAC1D,2BAA2B;AAC3B,uEAAuE;AACvE,8DAA8D;AAC9D,mEAAmE;AACnE,4CAA4C;AAC5C,kEAAkE;AAClE,oEAAoE;AACpE,uEAAuE;AACvE,mEAAmE;AACnE,sEAAsE;AACtE,mEAAmE;AACnE,+DAA+D;AAC/D,yCAAyC;AACzC,qEAAqE;AACrE,oEAAoE;AACpE,mEAAmE;AACnE,kEAAkE;AAClE,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../../../../../src/commands/game/sideQuest/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,sEAAsE;AACtE,6CAA6C;AAC7C,iEAAiE;AACjE,qCAAqC;AACrC,oEAAoE;AACpE,6DAA6D;AAC7D,uEAAuE;AACvE,kDAAkD;AAClD,iEAAiE;AACjE,mEAAmE;AACnE,gEAAgE;AAChE,8DAA8D;AAC9D,kEAAkE;AAClE,kDAAkD;AAClD,kEAAkE;AAClE,4DAA4D;AAC5D,qEAAqE;AACrE,yDAAyD;AACzD,sEAAsE;AACtE,2CAA2C;AAC3C,qEAAqE;AACrE,yDAAyD;AACzD,iEAAiE;AACjE,qEAAqE;AACrE,qEAAqE;AACrE,uEAAuE;AACvE,wEAAwE;AACxE,oEAAoE;AACpE,wEAAwE;AACxE,6DAA6D;AAC7D,uEAAuE;AACvE,sEAAsE;AACtE,0DAA0D;AAC1D,2BAA2B;AAC3B,uEAAuE;AACvE,8DAA8D;AAC9D,mEAAmE;AACnE,4CAA4C;AAC5C,kEAAkE;AAClE,oEAAoE;AACpE,uEAAuE;AACvE,mEAAmE;AACnE,sEAAsE;AACtE,mEAAmE;AACnE,+DAA+D;AAC/D,yCAAyC;AACzC,qEAAqE;AACrE,oEAAoE;AACpE,mEAAmE;AACnE,kEAAkE;AAClE,qEAAqE;AACrE,yBAAyB;AACzB,iEAAiE;AACjE,sEAAsE;AACtE,sEAAsE;AACtE,qEAAqE;AACrE,qEAAqE;AACrE,qEAAqE;AACrE,kCAAkC;AAClC,+DAA+D;AAC/D,qEAAqE;AACrE,oEAAoE;AACpE,uEAAuE;AACvE,oEAAoE;AACpE,sEAAsE;AACtE,2DAA2D;AAC3D,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC"}
|
|
@@ -2,7 +2,7 @@ import blessed from 'blessed';
|
|
|
2
2
|
import { ISceneSeedJson } from './types/seed/scene-seed.js';
|
|
3
3
|
import { Item } from './models/item.js';
|
|
4
4
|
import { Player, Inventory, Room, Scene } from './models/_index.js';
|
|
5
|
-
import { ISaveFileV1, ISavedItem, SaveReason } from './types/save-file.js';
|
|
5
|
+
import { ISaveFileV1, ISavedItem, SaveReason, IRegisteredSprite } from './types/save-file.js';
|
|
6
6
|
/**
|
|
7
7
|
* One crew slot (see THE CREW block below): the persistent record a
|
|
8
8
|
* live 'ally' shell is rebuilt from each run. `ghost` marks a slot held
|
|
@@ -33,6 +33,7 @@ import { IDocwagonContract } from './utilities/docwagon.js';
|
|
|
33
33
|
import { ILifestyleTier } from './utilities/commerce.js';
|
|
34
34
|
import { CommandQueue } from './utilities/command-queue.js';
|
|
35
35
|
import { NPC } from './models/npc.js';
|
|
36
|
+
import type { CompanionKind } from './models/npc.js';
|
|
36
37
|
import { AutoFight } from './utilities/auto-fight.js';
|
|
37
38
|
import { IMainGameConfig } from './ui.js';
|
|
38
39
|
import { PlayerEquipment } from './types/player-equipment.js';
|
|
@@ -329,7 +330,7 @@ export default class Game {
|
|
|
329
330
|
private seedHomeTurfContacts;
|
|
330
331
|
companions: {
|
|
331
332
|
npc: NPC;
|
|
332
|
-
kind:
|
|
333
|
+
kind: CompanionKind;
|
|
333
334
|
services?: number;
|
|
334
335
|
force: number;
|
|
335
336
|
boundDevice?: Item;
|
|
@@ -340,9 +341,51 @@ export default class Game {
|
|
|
340
341
|
* survives travel and rides the save file. */
|
|
341
342
|
registered?: boolean;
|
|
342
343
|
}[];
|
|
343
|
-
/**
|
|
344
|
-
*
|
|
345
|
-
|
|
344
|
+
/** REGISTERED SPRITES WAITING IN THE RESONANCE (SR5 p.254-256):
|
|
345
|
+
* folded for travel, or staged from a resumed save, waiting to
|
|
346
|
+
* re-shell (the drone _pendingRedeploys school).
|
|
347
|
+
*
|
|
348
|
+
* An ARRAY since 2026-08-28. It was a single slot back when one
|
|
349
|
+
* sprite was the hard maximum; the canon cap is one COMPILED sprite
|
|
350
|
+
* with registered ones exempt, up to LOGIC of them (register.ts), so
|
|
351
|
+
* a single slot would have quietly dropped every registered sprite
|
|
352
|
+
* but one at the next save or journey -- the same "your permanent
|
|
353
|
+
* companion vanished" the fix is for, moved one step downstream. */
|
|
354
|
+
private _pendingSpriteRedeploy;
|
|
355
|
+
/** How many registrations are spent: the ones standing here plus the
|
|
356
|
+
* ones waiting in the Resonance. register.ts bills this against
|
|
357
|
+
* Logic. */
|
|
358
|
+
registeredSpriteCount(): number;
|
|
359
|
+
/** Registered sprites waiting in the Resonance, for the surfaces that
|
|
360
|
+
* have to tell a player what they still have. A copy: the stash is
|
|
361
|
+
* maintained by the movers above and nowhere else. */
|
|
362
|
+
registeredSpritesWaiting(): IRegisteredSprite[];
|
|
363
|
+
/**
|
|
364
|
+
* DISMISS half of Call/Dismiss Sprite (SR5 p.250, Simple Action):
|
|
365
|
+
* "send a sprite back to the Resonance, releasing it from any tasks it
|
|
366
|
+
* still owes you."
|
|
367
|
+
*
|
|
368
|
+
* The task release is canon and it is the expensive part -- p.256
|
|
369
|
+
* offers STANDBY (costs 1 task, keeps the rest) precisely because
|
|
370
|
+
* dismissing burns them all. So tasks go to 0 here rather than being
|
|
371
|
+
* carried, and the command that calls this says so out loud before a
|
|
372
|
+
* player spends a companion's usefulness to tidy the grid.
|
|
373
|
+
*
|
|
374
|
+
* Only a REGISTERED sprite has anywhere to go. A compiled one that is
|
|
375
|
+
* sent away is simply gone, which is decompile's job, not this one --
|
|
376
|
+
* so this refuses it rather than quietly destroying it.
|
|
377
|
+
*/
|
|
378
|
+
dismissSpriteToResonance(entry: (typeof this.companions)[number]): boolean;
|
|
379
|
+
/**
|
|
380
|
+
* CALL half of Call/Dismiss Sprite (p.250): "You call a registered
|
|
381
|
+
* sprite you have waiting in the Resonance."
|
|
382
|
+
*
|
|
383
|
+
* `name` picks one when several wait; without it, the first. Returns
|
|
384
|
+
* the arrival line, or undefined when nothing answered -- the caller
|
|
385
|
+
* owns the refusal wording, because "you have none" and "you have
|
|
386
|
+
* some but not that one" are different things to be told.
|
|
387
|
+
*/
|
|
388
|
+
callSpriteFromResonance(name?: string): string | undefined;
|
|
346
389
|
private _pendingRedeploys;
|
|
347
390
|
private _saveFilePath?;
|
|
348
391
|
private _dead;
|
|
@@ -390,7 +433,7 @@ export default class Game {
|
|
|
390
433
|
spawnCompanionShell(opts: {
|
|
391
434
|
name: string;
|
|
392
435
|
description: string;
|
|
393
|
-
kind:
|
|
436
|
+
kind: CompanionKind;
|
|
394
437
|
combat: Partial<import('./models/player.js').ICombatAttributes>;
|
|
395
438
|
room: Room;
|
|
396
439
|
plane: 'meat' | 'matrix' | 'astral';
|
|
@@ -433,11 +476,7 @@ export default class Game {
|
|
|
433
476
|
/** Re-shell a registered sprite (travel arrival, or a resumed save).
|
|
434
477
|
* Returns the arrival line, or undefined when the shell can't land
|
|
435
478
|
* (name collision -- vanishingly rare). */
|
|
436
|
-
respawnRegisteredSprite(spec:
|
|
437
|
-
spriteType: string;
|
|
438
|
-
level: number;
|
|
439
|
-
tasks: number;
|
|
440
|
-
}): string | undefined;
|
|
479
|
+
respawnRegisteredSprite(spec: IRegisteredSprite): string | undefined;
|
|
441
480
|
/**
|
|
442
481
|
* Records a real transaction with a hub NPC: first contact opens the
|
|
443
482
|
* relationship at Loyalty 1 (a safeguard -- the hub cast is pre-seeded
|
|
@@ -96,9 +96,12 @@ import { ContactsCommand } from './commands/contacts.js';
|
|
|
96
96
|
import { JumpCommand } from './commands/jump.js';
|
|
97
97
|
import { InitiateCommand } from './commands/initiate.js';
|
|
98
98
|
import { SummonCommand } from './commands/summon.js';
|
|
99
|
-
import { CompileCommand, SPRITE_TYPES } from './commands/compile.js';
|
|
99
|
+
import { CompileCommand, SPRITE_TYPES, uniqueSpriteName } from './commands/compile.js';
|
|
100
|
+
import { DecompileCommand } from './commands/decompile.js';
|
|
100
101
|
import { ThreadCommand } from './commands/thread.js';
|
|
101
102
|
import { RegisterCommand } from './commands/register.js';
|
|
103
|
+
import { SpritesCommand } from './commands/sprites.js';
|
|
104
|
+
import { UnravelCommand } from './commands/unravel.js';
|
|
102
105
|
import { OrderCommand } from './commands/order.js';
|
|
103
106
|
import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
|
|
104
107
|
import { DeckCommand, LoadProgramCommand, UnloadProgramCommand } from './commands/deck.js';
|
|
@@ -947,9 +950,98 @@ export default class Game {
|
|
|
947
950
|
// death both remove the entry; persistent state (the frame, the deck)
|
|
948
951
|
// lives on Items in the player's possession, never here.
|
|
949
952
|
companions = [];
|
|
950
|
-
/**
|
|
951
|
-
*
|
|
952
|
-
|
|
953
|
+
/** REGISTERED SPRITES WAITING IN THE RESONANCE (SR5 p.254-256):
|
|
954
|
+
* folded for travel, or staged from a resumed save, waiting to
|
|
955
|
+
* re-shell (the drone _pendingRedeploys school).
|
|
956
|
+
*
|
|
957
|
+
* An ARRAY since 2026-08-28. It was a single slot back when one
|
|
958
|
+
* sprite was the hard maximum; the canon cap is one COMPILED sprite
|
|
959
|
+
* with registered ones exempt, up to LOGIC of them (register.ts), so
|
|
960
|
+
* a single slot would have quietly dropped every registered sprite
|
|
961
|
+
* but one at the next save or journey -- the same "your permanent
|
|
962
|
+
* companion vanished" the fix is for, moved one step downstream. */
|
|
963
|
+
_pendingSpriteRedeploy = [];
|
|
964
|
+
/** How many registrations are spent: the ones standing here plus the
|
|
965
|
+
* ones waiting in the Resonance. register.ts bills this against
|
|
966
|
+
* Logic. */
|
|
967
|
+
registeredSpriteCount() {
|
|
968
|
+
return this.companions.filter(c => c.kind === 'sprite' && c.registered).length
|
|
969
|
+
+ this._pendingSpriteRedeploy.length;
|
|
970
|
+
}
|
|
971
|
+
// THE STASH AND THE LIVE ROSTER ARE DISJOINT, and every mover below
|
|
972
|
+
// must keep them that way: a registered sprite is EITHER standing in
|
|
973
|
+
// `companions` OR waiting here, never both and never neither.
|
|
974
|
+
// foldCompanionsForTravel pushes here and dismisses the shell in the
|
|
975
|
+
// same pass; both drains splice(0); dismissSpriteToResonance and
|
|
976
|
+
// callSpriteFromResonance are the two halves of Call/Dismiss and move
|
|
977
|
+
// a sprite across in one step each.
|
|
978
|
+
//
|
|
979
|
+
// This is why decompile.ts needs no "forget this registration" call,
|
|
980
|
+
// and why one written there could not be made to fail under mutation:
|
|
981
|
+
// decompile only reaches actors in the room, and a sprite waiting
|
|
982
|
+
// HERE is not in the room. That stays true now that Call/Dismiss can
|
|
983
|
+
// put one here mid-run -- dismissing does not make it decompilable,
|
|
984
|
+
// it makes it absent.
|
|
985
|
+
/** Registered sprites waiting in the Resonance, for the surfaces that
|
|
986
|
+
* have to tell a player what they still have. A copy: the stash is
|
|
987
|
+
* maintained by the movers above and nowhere else. */
|
|
988
|
+
registeredSpritesWaiting() {
|
|
989
|
+
return this._pendingSpriteRedeploy.map(s => ({ ...s }));
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* DISMISS half of Call/Dismiss Sprite (SR5 p.250, Simple Action):
|
|
993
|
+
* "send a sprite back to the Resonance, releasing it from any tasks it
|
|
994
|
+
* still owes you."
|
|
995
|
+
*
|
|
996
|
+
* The task release is canon and it is the expensive part -- p.256
|
|
997
|
+
* offers STANDBY (costs 1 task, keeps the rest) precisely because
|
|
998
|
+
* dismissing burns them all. So tasks go to 0 here rather than being
|
|
999
|
+
* carried, and the command that calls this says so out loud before a
|
|
1000
|
+
* player spends a companion's usefulness to tidy the grid.
|
|
1001
|
+
*
|
|
1002
|
+
* Only a REGISTERED sprite has anywhere to go. A compiled one that is
|
|
1003
|
+
* sent away is simply gone, which is decompile's job, not this one --
|
|
1004
|
+
* so this refuses it rather than quietly destroying it.
|
|
1005
|
+
*/
|
|
1006
|
+
dismissSpriteToResonance(entry) {
|
|
1007
|
+
if (entry.kind !== 'sprite' || !entry.registered || !entry.spriteType)
|
|
1008
|
+
return false;
|
|
1009
|
+
this._pendingSpriteRedeploy.push({
|
|
1010
|
+
spriteType: entry.spriteType,
|
|
1011
|
+
level: entry.force,
|
|
1012
|
+
tasks: 0,
|
|
1013
|
+
name: entry.npc.name,
|
|
1014
|
+
});
|
|
1015
|
+
this.dismissCompanion(entry, 'dismissed to the Resonance');
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* CALL half of Call/Dismiss Sprite (p.250): "You call a registered
|
|
1020
|
+
* sprite you have waiting in the Resonance."
|
|
1021
|
+
*
|
|
1022
|
+
* `name` picks one when several wait; without it, the first. Returns
|
|
1023
|
+
* the arrival line, or undefined when nothing answered -- the caller
|
|
1024
|
+
* owns the refusal wording, because "you have none" and "you have
|
|
1025
|
+
* some but not that one" are different things to be told.
|
|
1026
|
+
*/
|
|
1027
|
+
callSpriteFromResonance(name) {
|
|
1028
|
+
const idx = name === undefined
|
|
1029
|
+
? (this._pendingSpriteRedeploy.length > 0 ? 0 : -1)
|
|
1030
|
+
: this._pendingSpriteRedeploy.findIndex(s => s.name === name);
|
|
1031
|
+
if (idx < 0)
|
|
1032
|
+
return undefined;
|
|
1033
|
+
// Removed BEFORE the re-shell, so a failed spawn cannot leave the
|
|
1034
|
+
// same sprite both waiting and standing.
|
|
1035
|
+
const [spec] = this._pendingSpriteRedeploy.splice(idx, 1);
|
|
1036
|
+
const line = this.respawnRegisteredSprite(spec);
|
|
1037
|
+
if (line === undefined) {
|
|
1038
|
+
// It could not land. Put it back rather than losing it: the
|
|
1039
|
+
// registration is the player's property, not this call's.
|
|
1040
|
+
this._pendingSpriteRedeploy.splice(idx, 0, spec);
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
return line;
|
|
1044
|
+
}
|
|
953
1045
|
// Drones deployed at travel time re-shell on arrival ("the rotors spin
|
|
954
1046
|
// back up at the curb") -- tracked as their items across the swap.
|
|
955
1047
|
_pendingRedeploys = [];
|
|
@@ -1228,7 +1320,7 @@ export default class Game {
|
|
|
1228
1320
|
// A REGISTERED sprite persists (register.ts): it folds into
|
|
1229
1321
|
// your PAN for the ride and re-shells at the curb.
|
|
1230
1322
|
if (entry.registered && entry.spriteType) {
|
|
1231
|
-
this._pendingSpriteRedeploy
|
|
1323
|
+
this._pendingSpriteRedeploy.push({ spriteType: entry.spriteType, level: entry.force, tasks: entry.services ?? 0, name: entry.npc.name });
|
|
1232
1324
|
lines.push(`${entry.npc.name} folds into your PAN for the ride -- registered code travels with you.`);
|
|
1233
1325
|
}
|
|
1234
1326
|
else {
|
|
@@ -1260,10 +1352,11 @@ export default class Game {
|
|
|
1260
1352
|
if (shell)
|
|
1261
1353
|
lines.push(`The ${item.name}'s rotors spin back up at the curb.`);
|
|
1262
1354
|
}
|
|
1263
|
-
//
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1355
|
+
// EVERY registered sprite re-shells from the PAN (register.ts).
|
|
1356
|
+
// splice(0) drains the stash exactly once, the way the drone loop
|
|
1357
|
+
// above does -- a sprite that fails to land is not left staged to
|
|
1358
|
+
// try again at the next curb.
|
|
1359
|
+
for (const spec of this._pendingSpriteRedeploy.splice(0)) {
|
|
1267
1360
|
const line = this.respawnRegisteredSprite(spec);
|
|
1268
1361
|
if (line)
|
|
1269
1362
|
lines.push(line);
|
|
@@ -1278,7 +1371,11 @@ export default class Game {
|
|
|
1278
1371
|
if (!type)
|
|
1279
1372
|
return undefined;
|
|
1280
1373
|
const shell = this.spawnCompanionShell({
|
|
1281
|
-
name
|
|
1374
|
+
// The name it left with, so "order <name>" still works after the
|
|
1375
|
+
// ride -- but never on top of a live actor, which is how two
|
|
1376
|
+
// registered sprites of the same type and Level used to collide
|
|
1377
|
+
// and silently lose the second one.
|
|
1378
|
+
name: this.scene.getActor(spec.name) ? uniqueSpriteName(this.scene, type.name, spec.level) : spec.name,
|
|
1282
1379
|
description: `A registered ${type.name.toLowerCase()} sprite riding ${this.player.name}'s PAN -- ${type.blurb}. It serves until its ${spec.tasks} task${spec.tasks === 1 ? '' : 's'} are spent.`,
|
|
1283
1380
|
kind: 'sprite',
|
|
1284
1381
|
combat: type.combat(spec.level),
|
|
@@ -1976,9 +2073,13 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
1976
2073
|
CommandFactory.registerCommand('summon', SummonCommand);
|
|
1977
2074
|
CommandFactory.registerCommand('conjure', SummonCommand);
|
|
1978
2075
|
CommandFactory.registerCommand('compile', CompileCommand);
|
|
1979
|
-
CommandFactory.registerCommand('decompile',
|
|
2076
|
+
CommandFactory.registerCommand('decompile', DecompileCommand);
|
|
1980
2077
|
CommandFactory.registerCommand('thread', ThreadCommand);
|
|
1981
2078
|
CommandFactory.registerCommand('register', RegisterCommand);
|
|
2079
|
+
// The sprite database (p.256-259) -- reference, the way "spells" is.
|
|
2080
|
+
CommandFactory.registerCommand('sprites', SpritesCommand);
|
|
2081
|
+
// Kill Complex Form (p.250) -- the Resonance counterpart to dispel.
|
|
2082
|
+
CommandFactory.registerCommand('unravel', UnravelCommand);
|
|
1982
2083
|
// "At" spots (utilities/spots.ts): walk/move/approach are one
|
|
1983
2084
|
// crossing; run and sprint are the fast modes (combat semantics in
|
|
1984
2085
|
// the spot-move phase).
|
|
@@ -2314,12 +2415,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2314
2415
|
}
|
|
2315
2416
|
// A REGISTERED sprite from the save re-shells at the cot (the
|
|
2316
2417
|
// technomancer's persistent companion -- register.ts).
|
|
2317
|
-
if (this._pendingSpriteRedeploy && this.scene === this._hubScene) {
|
|
2318
|
-
const spec
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2418
|
+
if (this._pendingSpriteRedeploy.length > 0 && this.scene === this._hubScene) {
|
|
2419
|
+
for (const spec of this._pendingSpriteRedeploy.splice(0)) {
|
|
2420
|
+
const line = this.respawnRegisteredSprite(spec);
|
|
2421
|
+
if (line)
|
|
2422
|
+
Logger.getInstance().logWithColor(line, 'cyan');
|
|
2423
|
+
}
|
|
2323
2424
|
}
|
|
2324
2425
|
if (this._resume) {
|
|
2325
2426
|
const pendingNote = this._pendingRunSeed
|
|
@@ -2403,9 +2504,22 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2403
2504
|
if (this.crew.length > 0 && this.crewForHire.some(c => this.crew.some(m => m.name === c.name))) {
|
|
2404
2505
|
this.rollHireCandidates();
|
|
2405
2506
|
}
|
|
2406
|
-
//
|
|
2407
|
-
|
|
2408
|
-
|
|
2507
|
+
// Registered sprites stage for their re-shell at hub arrival. The
|
|
2508
|
+
// array is the live field; the singular `registeredSprite` is only
|
|
2509
|
+
// read when it is absent, which is exactly the pre-2026-08-28 saves
|
|
2510
|
+
// written before a technomancer could hold more than one. Reading
|
|
2511
|
+
// BOTH would double a sprite on the first save that carries each.
|
|
2512
|
+
if (save.game.registeredSprites && save.game.registeredSprites.length > 0) {
|
|
2513
|
+
this._pendingSpriteRedeploy = save.game.registeredSprites.map(s => ({ ...s }));
|
|
2514
|
+
}
|
|
2515
|
+
else if (save.game.registeredSprite) {
|
|
2516
|
+
const legacy = save.game.registeredSprite;
|
|
2517
|
+
const type = SPRITE_TYPES[legacy.spriteType];
|
|
2518
|
+
this._pendingSpriteRedeploy = [{
|
|
2519
|
+
...legacy,
|
|
2520
|
+
// Old saves carry no name; rebuild the one it would have had.
|
|
2521
|
+
name: `${type?.name ?? 'Courier'} Sprite (Level ${legacy.level})`,
|
|
2522
|
+
}];
|
|
2409
2523
|
}
|
|
2410
2524
|
if (this.crew.length === 0 && save.game.enlistedAlly) {
|
|
2411
2525
|
const legacy = save.game.enlistedAlly;
|
|
@@ -2514,13 +2628,19 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2514
2628
|
contacts: [...this.contacts.entries()],
|
|
2515
2629
|
procurements: [...this.pendingProcurements],
|
|
2516
2630
|
docwagon: this.docwagon ? { ...this.docwagon } : undefined,
|
|
2517
|
-
|
|
2518
|
-
//
|
|
2519
|
-
//
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2631
|
+
registeredSprites: (() => {
|
|
2632
|
+
// Registered sprites ride the save from wherever they are:
|
|
2633
|
+
// STANDING here (a live companion, whose services are the
|
|
2634
|
+
// truth for its task count) or WAITING in the redeploy stash
|
|
2635
|
+
// after a fold. Both sources, because a technomancer can have
|
|
2636
|
+
// some of each -- taking only one of the two lists is how a
|
|
2637
|
+
// sprite that happened to be manifest at save time would
|
|
2638
|
+
// survive while its folded sibling silently did not.
|
|
2639
|
+
const live = this.companions
|
|
2640
|
+
.filter(c => c.kind === 'sprite' && c.registered && c.spriteType)
|
|
2641
|
+
.map(c => ({ spriteType: c.spriteType, level: c.force, tasks: c.services ?? 0, name: c.npc.name }));
|
|
2642
|
+
const all = [...live, ...this._pendingSpriteRedeploy.map(s => ({ ...s }))];
|
|
2643
|
+
return all.length > 0 ? all : undefined;
|
|
2524
2644
|
})(),
|
|
2525
2645
|
crew: this.crew.map(m => ({ ...m, gear: [...m.gear] })),
|
|
2526
2646
|
commandRefusals: { ...this.commandRefusals },
|
|
@@ -4247,7 +4367,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
4247
4367
|
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
4248
4368
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
4249
4369
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
4250
|
-
{ title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
|
|
4370
|
+
{ title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['unravel'], ['register'], ['sprites']] },
|
|
4251
4371
|
{ title: 'social', entries: [['say'], ['emote', 'me'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
4252
4372
|
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
4253
4373
|
{ title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|