@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.
Files changed (69) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +6 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +4 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js.map +1 -1
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.d.ts +39 -3
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +85 -4
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.d.ts +37 -17
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +53 -115
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js.map +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.d.ts +55 -0
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js +203 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js.map +1 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +23 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +42 -7
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +27 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +28 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +69 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/register.d.ts +15 -2
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +32 -6
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js.map +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.d.ts +35 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +108 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js.map +1 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.d.ts +27 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +7 -2
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.d.ts +57 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.js +162 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.js.map +1 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +17 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +50 -11
  39. package/bundle/typescript/src/commands/game/sideQuest/game.js +148 -28
  40. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +24 -12
  42. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +53 -13
  43. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +9 -0
  45. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +9 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +31 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -0
  49. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +10 -3
  51. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +5 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.d.ts +108 -0
  54. package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.js +186 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.js.map +1 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.d.ts +88 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +137 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js.map +1 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +32 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/ui.js +3 -0
  61. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +13 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +36 -3
  64. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +14 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +83 -0
  68. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  69. 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":"AAAA,OAAO,EAAE,OAAO,EAAY,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,OAAO,cAAe,SAAQ,OAAO;IAC/B,MAAM,CAAC,IAAI,GAAG,SAAS,CAAC;IACxB,MAAM,CAAC,WAAW,GAAG,kTAAkT,CAAC;IAElV,KAAK,CAAC,OAAO,CAAC,OAAiB,EAAE;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAEzB,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YAC5B,OAAO,4GAA4G,CAAC;QACtH,CAAC;QAED,8DAA8D;QAC9D,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,CAAC;QACnC,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;aACtC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,YAAY,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAEzG,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAEzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,kEAAkE;YAClE,kEAAkE;YAClE,0DAA0D;YAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;YACjD,OAAO,kCAAkC,QAAQ,CAAC,CAAC,CAAC,+BAA+B,IAAI,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC,CAAC,gCAAgC,EAAE,CAAC;QAC5J,CAAC;QAED,IAAI,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACnE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,4DAA4D;gBAC5D,IAAI,aAAa,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;oBAC5C,OAAO,oCAAoC,IAAI,CAAC,4CAA4C,CAAC,EAAE,CAAC;gBAClG,CAAC;gBACD,OAAO,oEAAoE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YACpH,CAAC;YACD,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;QAC9D,CAAC;aAAM,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,OAAO,wCAAwC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QACxF,CAAC;QAED,iEAAiE;QACjE,8DAA8D;QAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,2DAA2D;QAC3D,uCAAuC;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC;QAEpC,oEAAoE;QACpE,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC;QACrI,kEAAkE;QAClE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAE7C,MAAM,SAAS,GAAG,CAAC,aAAa,KAAK,CAAC,SAAS,EAAE,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,QAAQ,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;QAC7G,IAAI,KAAK,CAAC,aAAa,GAAG,CAAC;YAAE,SAAS,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC;QAC7E,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC;YAAE,SAAS,CAAC,IAAI,CAAC,eAAe,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAC1F,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,KAAK,MAAM,UAAU,CAAC,IAAI,CAAC,OAAO,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAEhI,KAAK,CAAC,aAAa,CAAC,yBAAyB,EAAE,QAAQ,CAAC,CAAC;QAEzD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,GAAG,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,SAAS,KAAK,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG;YACZ,wBAAwB,QAAQ,WAAW,MAAM,CAAC,IAAI,YAAY;SACnE,CAAC;QAEF,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YACtE,KAAK,CAAC,IAAI,CAAC,4CAA4C,QAAQ,kBAAkB,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;YACjG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC,IAAI,mBAAmB,QAAQ,IAAI,MAAM,CAAC,IAAI,sBAAsB,IAAI,EAAE,IAAI,IAAI,UAAU,GAAG,CAAC,CAAC;QACrI,CAAC;aAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,wCAAwC,GAAG,WAAW,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,cAAc,SAAS,kBAAkB,CAAC,CAAC;QACvJ,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,eAAe,MAAM,CAAC,IAAI,+CAA+C,CAAC,CAAC;QACxF,CAAC;QAED,+DAA+D;QAC/D,oEAAoE;QACpE,iEAAiE;QACjE,oEAAoE;QACpE,kDAAkD;QAClD,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC;QAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE,aAAa,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAEvH,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;QAC9C,CAAC;aAAM,IAAI,SAAS,EAAE,CAAC;YACrB,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;YAC9B,KAAK,CAAC,IAAI,CAAC,qEAAqE,WAAW,cAAc,KAAK,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;QACvI,CAAC;aAAM,CAAC;YACN,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACvD,KAAK,CAAC,IAAI,CAAC,yBAAyB,IAAI,UAAU,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YACzE,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,0CAA0C,QAAQ,OAAO,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,0BAA0B,KAAK,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;YACtJ,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,KAAK,CAAC,IAAI,OAAO,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,GAAG,KAAK,KAAK,OAAO,IAAI,MAAM,OAAO,CAAC,IAAI,SAAS,GAAG,MAAM,QAAQ,OAAO,SAAS,YAAY,WAAW,UAAU,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACjP,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC"}
@@ -16,4 +16,4 @@
16
16
  * No import.meta here (engine-graph law) -- this file is loaded by the
17
17
  * server.
18
18
  */
19
- export declare const ENGINE_VERSION = "1.15.0";
19
+ export declare const ENGINE_VERSION = "1.16.0";
@@ -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
- export const ENGINE_VERSION = '1.15.0';
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: 'spirit' | 'sprite' | 'agent' | 'drone' | 'ally';
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
- /** A registered sprite folded for travel, waiting to re-shell at the
344
- * destination (the drone _pendingRedeploys school). */
345
- private _pendingSpriteRedeploy?;
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: 'spirit' | 'sprite' | 'agent' | 'drone' | 'ally';
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
- /** A registered sprite folded for travel, waiting to re-shell at the
951
- * destination (the drone _pendingRedeploys school). */
952
- _pendingSpriteRedeploy;
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 = { spriteType: entry.spriteType, level: entry.force, tasks: entry.services ?? 0 };
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
- // A REGISTERED sprite re-shells from the PAN (register.ts).
1264
- if (this._pendingSpriteRedeploy) {
1265
- const spec = this._pendingSpriteRedeploy;
1266
- this._pendingSpriteRedeploy = undefined;
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: `${type.name} Sprite (Level ${spec.level})`,
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', CompileCommand);
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 = this._pendingSpriteRedeploy;
2319
- this._pendingSpriteRedeploy = undefined;
2320
- const line = this.respawnRegisteredSprite(spec);
2321
- if (line)
2322
- Logger.getInstance().logWithColor(line, 'cyan');
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
- // The registered sprite stages for its re-shell at hub arrival.
2407
- if (save.game.registeredSprite) {
2408
- this._pendingSpriteRedeploy = { ...save.game.registeredSprite };
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
- registeredSprite: (() => {
2518
- // A live registered sprite rides the save; a travel-folded one
2519
- // is in the redeploy stash. Either way it survives the session.
2520
- const live = this.companions.find(c => c.kind === 'sprite' && c.registered && c.spriteType);
2521
- if (live)
2522
- return { spriteType: live.spriteType, level: live.force, tasks: live.services ?? 0 };
2523
- return this._pendingSpriteRedeploy ? { ...this._pendingSpriteRedeploy } : undefined;
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']] },