@maka/maka-cli 5.67.0 → 5.69.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 (137) 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/abandon.d.ts +30 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/abandon.js +54 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/abandon.js.map +1 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +4 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js.map +1 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +5 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +12 -4
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +28 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +105 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/climb.d.ts +48 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/climb.js +107 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/climb.js.map +1 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.d.ts +39 -3
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +89 -4
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.d.ts +37 -17
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +53 -115
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.d.ts +55 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js +203 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js.map +1 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +23 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +44 -9
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +24 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +70 -12
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/lock.js +17 -4
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/lock.js.map +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +12 -2
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +12 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +8 -6
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/move.d.ts +11 -0
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +107 -14
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +69 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +5 -4
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/register.d.ts +15 -2
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +32 -6
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js.map +1 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +33 -2
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.d.ts +35 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +108 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js.map +1 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/standby.d.ts +40 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/standby.js +91 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/standby.js.map +1 -0
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +10 -11
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.d.ts +27 -0
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +7 -2
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +4 -2
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.d.ts +57 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.js +162 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/unravel.js.map +1 -0
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +12 -5
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  73. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +17 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +41 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +4 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +2 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +91 -6
  81. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  82. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +6 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  84. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +135 -11
  85. package/bundle/typescript/src/commands/game/sideQuest/game.js +361 -30
  86. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +24 -12
  88. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +53 -13
  89. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
  90. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +25 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +31 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +98 -1
  94. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +98 -4
  95. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +95 -4
  97. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +179 -20
  98. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  99. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +90 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +114 -5
  101. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  102. package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.d.ts +108 -0
  103. package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.js +186 -0
  104. package/bundle/typescript/src/commands/game/sideQuest/resonance-actions.js.map +1 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +1 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +2 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.d.ts +88 -0
  108. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +137 -0
  109. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js.map +1 -0
  110. package/bundle/typescript/src/commands/game/sideQuest/types/repro.d.ts +42 -0
  111. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +32 -1
  112. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +12 -0
  113. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +1 -0
  114. package/bundle/typescript/src/commands/game/sideQuest/ui.js +3 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  116. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +5 -1
  117. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  118. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +41 -1
  119. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +52 -4
  120. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -1
  121. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +14 -2
  122. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +78 -7
  123. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -1
  124. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.d.ts +7 -0
  125. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.js +24 -0
  126. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.js.map +1 -1
  127. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +14 -0
  128. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  129. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.d.ts +46 -0
  130. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +90 -5
  131. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js.map +1 -1
  132. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +70 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +168 -22
  134. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  135. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +83 -0
  136. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  137. package/package.json +1 -1
@@ -96,14 +96,20 @@ 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 { StandbyCommand } from './commands/standby.js';
105
+ import { AbandonCommand } from './commands/abandon.js';
106
+ import { UnravelCommand } from './commands/unravel.js';
102
107
  import { OrderCommand } from './commands/order.js';
103
108
  import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
104
109
  import { DeckCommand, LoadProgramCommand, UnloadProgramCommand } from './commands/deck.js';
105
110
  import { getProgram } from './utilities/programs.js';
106
111
  import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
112
+ import { ClimbCommand, DescendCommand } from './commands/climb.js';
107
113
  import { HintsCommand } from './commands/hints.js';
108
114
  import { hint } from './utilities/hints.js';
109
115
  import { camerasLive } from './utilities/surveillance.js';
@@ -947,9 +953,133 @@ export default class Game {
947
953
  // death both remove the entry; persistent state (the frame, the deck)
948
954
  // lives on Items in the player's possession, never here.
949
955
  companions = [];
950
- /** A registered sprite folded for travel, waiting to re-shell at the
951
- * destination (the drone _pendingRedeploys school). */
952
- _pendingSpriteRedeploy;
956
+ /** REGISTERED SPRITES WAITING IN THE RESONANCE (SR5 p.254-256):
957
+ * folded for travel, or staged from a resumed save, waiting to
958
+ * re-shell (the drone _pendingRedeploys school).
959
+ *
960
+ * An ARRAY since 2026-08-28. It was a single slot back when one
961
+ * sprite was the hard maximum; the canon cap is one COMPILED sprite
962
+ * with registered ones exempt, up to LOGIC of them (register.ts), so
963
+ * a single slot would have quietly dropped every registered sprite
964
+ * but one at the next save or journey -- the same "your permanent
965
+ * companion vanished" the fix is for, moved one step downstream. */
966
+ _pendingSpriteRedeploy = [];
967
+ /** How many registrations are spent: the ones standing here plus the
968
+ * ones waiting in the Resonance. register.ts bills this against
969
+ * Logic. */
970
+ registeredSpriteCount() {
971
+ return this.companions.filter(c => c.kind === 'sprite' && c.registered).length
972
+ + this._pendingSpriteRedeploy.length;
973
+ }
974
+ // THE STASH AND THE LIVE ROSTER ARE DISJOINT, and every mover below
975
+ // must keep them that way: a registered sprite is EITHER standing in
976
+ // `companions` OR waiting here, never both and never neither.
977
+ // foldCompanionsForTravel pushes here and dismisses the shell in the
978
+ // same pass; both drains splice(0); dismissSpriteToResonance and
979
+ // callSpriteFromResonance are the two halves of Call/Dismiss and move
980
+ // a sprite across in one step each.
981
+ //
982
+ // This is why decompile.ts needs no "forget this registration" call,
983
+ // and why one written there could not be made to fail under mutation:
984
+ // decompile only reaches actors in the room, and a sprite waiting
985
+ // HERE is not in the room. That stays true now that Call/Dismiss can
986
+ // put one here mid-run -- dismissing does not make it decompilable,
987
+ // it makes it absent.
988
+ /** Registered sprites waiting in the Resonance, for the surfaces that
989
+ * have to tell a player what they still have. A copy: the stash is
990
+ * maintained by the movers above and nowhere else. */
991
+ registeredSpritesWaiting() {
992
+ return this._pendingSpriteRedeploy.map(s => ({ ...s }));
993
+ }
994
+ /**
995
+ * DISMISS half of Call/Dismiss Sprite (SR5 p.250, Simple Action):
996
+ * "send a sprite back to the Resonance, releasing it from any tasks it
997
+ * still owes you."
998
+ *
999
+ * The task release is canon and it is the expensive part -- p.256
1000
+ * offers STANDBY (costs 1 task, keeps the rest) precisely because
1001
+ * dismissing burns them all. So tasks go to 0 here rather than being
1002
+ * carried, and the command that calls this says so out loud before a
1003
+ * player spends a companion's usefulness to tidy the grid.
1004
+ *
1005
+ * Only a REGISTERED sprite has anywhere to go. A compiled one that is
1006
+ * sent away is simply gone, which is decompile's job, not this one --
1007
+ * so this refuses it rather than quietly destroying it.
1008
+ */
1009
+ dismissSpriteToResonance(entry) {
1010
+ if (entry.kind !== 'sprite' || !entry.registered || !entry.spriteType)
1011
+ return false;
1012
+ this._pendingSpriteRedeploy.push({
1013
+ spriteType: entry.spriteType,
1014
+ level: entry.force,
1015
+ tasks: 0,
1016
+ name: entry.npc.name,
1017
+ });
1018
+ this.dismissCompanion(entry, 'dismissed to the Resonance');
1019
+ return true;
1020
+ }
1021
+ /**
1022
+ * STANDBY (SR5 p.256, a registered sprite TASK): "tells the sprite to
1023
+ * return to the Resonance but come when you call it. This keeps the
1024
+ * sprite from giving away your presence or getting in harm's way."
1025
+ * Costs ONE task.
1026
+ *
1027
+ * The middle option dismissal made visible. Dismissing is free at the
1028
+ * door and costs everything the sprite owes you (p.250); standby
1029
+ * costs one and keeps the rest, which is the whole reason the book
1030
+ * sells it. Without this the only way to park a sprite was to spend
1031
+ * its entire usefulness, and a player who wanted it out of sight for
1032
+ * one room paid for that with every favour it had left.
1033
+ *
1034
+ * Returns false when there is no task to spend with. A registered
1035
+ * sprite that owes you nothing cannot pay for standby -- and does not
1036
+ * need to, because dismissing it releases nothing it still had. The
1037
+ * caller says that out loud rather than silently doing the other one.
1038
+ */
1039
+ standbySpriteToResonance(entry) {
1040
+ if (entry.kind !== 'sprite' || !entry.registered || !entry.spriteType)
1041
+ return false;
1042
+ const owed = entry.services ?? 0;
1043
+ if (owed < 1)
1044
+ return false;
1045
+ this._pendingSpriteRedeploy.push({
1046
+ spriteType: entry.spriteType,
1047
+ level: entry.force,
1048
+ // The task it spends going quiet. The REST survive, which is the
1049
+ // entire difference between this and dismissal.
1050
+ tasks: owed - 1,
1051
+ name: entry.npc.name,
1052
+ });
1053
+ this.dismissCompanion(entry, 'sent to standby');
1054
+ return true;
1055
+ }
1056
+ /**
1057
+ * CALL half of Call/Dismiss Sprite (p.250): "You call a registered
1058
+ * sprite you have waiting in the Resonance."
1059
+ *
1060
+ * `name` picks one when several wait; without it, the first. Returns
1061
+ * the arrival line, or undefined when nothing answered -- the caller
1062
+ * owns the refusal wording, because "you have none" and "you have
1063
+ * some but not that one" are different things to be told.
1064
+ */
1065
+ callSpriteFromResonance(name) {
1066
+ const idx = name === undefined
1067
+ ? (this._pendingSpriteRedeploy.length > 0 ? 0 : -1)
1068
+ : this._pendingSpriteRedeploy.findIndex(s => s.name === name);
1069
+ if (idx < 0)
1070
+ return undefined;
1071
+ // Removed BEFORE the re-shell, so a failed spawn cannot leave the
1072
+ // same sprite both waiting and standing.
1073
+ const [spec] = this._pendingSpriteRedeploy.splice(idx, 1);
1074
+ const line = this.respawnRegisteredSprite(spec);
1075
+ if (line === undefined) {
1076
+ // It could not land. Put it back rather than losing it: the
1077
+ // registration is the player's property, not this call's.
1078
+ this._pendingSpriteRedeploy.splice(idx, 0, spec);
1079
+ return undefined;
1080
+ }
1081
+ return line;
1082
+ }
953
1083
  // Drones deployed at travel time re-shell on arrival ("the rotors spin
954
1084
  // back up at the curb") -- tracked as their items across the swap.
955
1085
  _pendingRedeploys = [];
@@ -1228,7 +1358,7 @@ export default class Game {
1228
1358
  // A REGISTERED sprite persists (register.ts): it folds into
1229
1359
  // your PAN for the ride and re-shells at the curb.
1230
1360
  if (entry.registered && entry.spriteType) {
1231
- this._pendingSpriteRedeploy = { spriteType: entry.spriteType, level: entry.force, tasks: entry.services ?? 0 };
1361
+ this._pendingSpriteRedeploy.push({ spriteType: entry.spriteType, level: entry.force, tasks: entry.services ?? 0, name: entry.npc.name });
1232
1362
  lines.push(`${entry.npc.name} folds into your PAN for the ride -- registered code travels with you.`);
1233
1363
  }
1234
1364
  else {
@@ -1260,10 +1390,11 @@ export default class Game {
1260
1390
  if (shell)
1261
1391
  lines.push(`The ${item.name}'s rotors spin back up at the curb.`);
1262
1392
  }
1263
- // A REGISTERED sprite re-shells from the PAN (register.ts).
1264
- if (this._pendingSpriteRedeploy) {
1265
- const spec = this._pendingSpriteRedeploy;
1266
- this._pendingSpriteRedeploy = undefined;
1393
+ // EVERY registered sprite re-shells from the PAN (register.ts).
1394
+ // splice(0) drains the stash exactly once, the way the drone loop
1395
+ // above does -- a sprite that fails to land is not left staged to
1396
+ // try again at the next curb.
1397
+ for (const spec of this._pendingSpriteRedeploy.splice(0)) {
1267
1398
  const line = this.respawnRegisteredSprite(spec);
1268
1399
  if (line)
1269
1400
  lines.push(line);
@@ -1278,7 +1409,11 @@ export default class Game {
1278
1409
  if (!type)
1279
1410
  return undefined;
1280
1411
  const shell = this.spawnCompanionShell({
1281
- name: `${type.name} Sprite (Level ${spec.level})`,
1412
+ // The name it left with, so "order <name>" still works after the
1413
+ // ride -- but never on top of a live actor, which is how two
1414
+ // registered sprites of the same type and Level used to collide
1415
+ // and silently lose the second one.
1416
+ name: this.scene.getActor(spec.name) ? uniqueSpriteName(this.scene, type.name, spec.level) : spec.name,
1282
1417
  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
1418
  kind: 'sprite',
1284
1419
  combat: type.combat(spec.level),
@@ -1673,6 +1808,12 @@ export default class Game {
1673
1808
  // Taxi warn-once (player ruling): leaving with the pay unclaimed
1674
1809
  // arms this; the second "call taxi" forfeits. Mirrors _quitArmed.
1675
1810
  forfeitArmed = false;
1811
+ /** Set by "abandon": the next taxi WALKS OUT instead of running an
1812
+ * errand (mYTCzJbsdZxYsTsZo). The reversible trip is the default and
1813
+ * the irreversible one is asked for -- Notoriety, a forfeited payout
1814
+ * and a destroyed scene should require the deliberate word, and a
1815
+ * round trip you can undo by getting back in the cab should not. */
1816
+ walkOutArmed = false;
1676
1817
  /**
1677
1818
  * The meet and the wrap-up, triggered by the player's first/next real
1678
1819
  * interaction with the client (talk-to, a connected call, a give).
@@ -1976,15 +2117,27 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1976
2117
  CommandFactory.registerCommand('summon', SummonCommand);
1977
2118
  CommandFactory.registerCommand('conjure', SummonCommand);
1978
2119
  CommandFactory.registerCommand('compile', CompileCommand);
1979
- CommandFactory.registerCommand('decompile', CompileCommand);
2120
+ CommandFactory.registerCommand('decompile', DecompileCommand);
1980
2121
  CommandFactory.registerCommand('thread', ThreadCommand);
1981
2122
  CommandFactory.registerCommand('register', RegisterCommand);
2123
+ // The sprite database (p.256-259) -- reference, the way "spells" is.
2124
+ CommandFactory.registerCommand('sprites', SpritesCommand);
2125
+ // STANDBY (p.256): the registered-sprite task that parks one for 1.
2126
+ CommandFactory.registerCommand('standby', StandbyCommand);
2127
+ // Walk out for good -- the taxi's irreversible half (mYTCzJbs...).
2128
+ CommandFactory.registerCommand('abandon', AbandonCommand);
2129
+ // Kill Complex Form (p.250) -- the Resonance counterpart to dispel.
2130
+ CommandFactory.registerCommand('unravel', UnravelCommand);
1982
2131
  // "At" spots (utilities/spots.ts): walk/move/approach are one
1983
2132
  // crossing; run and sprint are the fast modes (combat semantics in
1984
2133
  // the spot-move phase).
1985
2134
  CommandFactory.registerCommand('walk', MoveCommand);
1986
2135
  CommandFactory.registerCommand('move', MoveCommand);
1987
2136
  CommandFactory.registerCommand('approach', MoveCommand);
2137
+ // Vertical travel by name (commands/climb.ts) -- shortcuts over
2138
+ // move, which already crosses levels.
2139
+ CommandFactory.registerCommand('climb', ClimbCommand);
2140
+ CommandFactory.registerCommand('descend', DescendCommand);
1988
2141
  CommandFactory.registerCommand('run', RunCommand);
1989
2142
  CommandFactory.registerCommand('sprint', SprintCommand);
1990
2143
  CommandFactory.registerCommand('hints', HintsCommand);
@@ -2314,12 +2467,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2314
2467
  }
2315
2468
  // A REGISTERED sprite from the save re-shells at the cot (the
2316
2469
  // 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');
2470
+ if (this._pendingSpriteRedeploy.length > 0 && this.scene === this._hubScene) {
2471
+ for (const spec of this._pendingSpriteRedeploy.splice(0)) {
2472
+ const line = this.respawnRegisteredSprite(spec);
2473
+ if (line)
2474
+ Logger.getInstance().logWithColor(line, 'cyan');
2475
+ }
2323
2476
  }
2324
2477
  if (this._resume) {
2325
2478
  const pendingNote = this._pendingRunSeed
@@ -2403,9 +2556,22 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2403
2556
  if (this.crew.length > 0 && this.crewForHire.some(c => this.crew.some(m => m.name === c.name))) {
2404
2557
  this.rollHireCandidates();
2405
2558
  }
2406
- // The registered sprite stages for its re-shell at hub arrival.
2407
- if (save.game.registeredSprite) {
2408
- this._pendingSpriteRedeploy = { ...save.game.registeredSprite };
2559
+ // Registered sprites stage for their re-shell at hub arrival. The
2560
+ // array is the live field; the singular `registeredSprite` is only
2561
+ // read when it is absent, which is exactly the pre-2026-08-28 saves
2562
+ // written before a technomancer could hold more than one. Reading
2563
+ // BOTH would double a sprite on the first save that carries each.
2564
+ if (save.game.registeredSprites && save.game.registeredSprites.length > 0) {
2565
+ this._pendingSpriteRedeploy = save.game.registeredSprites.map(s => ({ ...s }));
2566
+ }
2567
+ else if (save.game.registeredSprite) {
2568
+ const legacy = save.game.registeredSprite;
2569
+ const type = SPRITE_TYPES[legacy.spriteType];
2570
+ this._pendingSpriteRedeploy = [{
2571
+ ...legacy,
2572
+ // Old saves carry no name; rebuild the one it would have had.
2573
+ name: `${type?.name ?? 'Courier'} Sprite (Level ${legacy.level})`,
2574
+ }];
2409
2575
  }
2410
2576
  if (this.crew.length === 0 && save.game.enlistedAlly) {
2411
2577
  const legacy = save.game.enlistedAlly;
@@ -2514,13 +2680,19 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2514
2680
  contacts: [...this.contacts.entries()],
2515
2681
  procurements: [...this.pendingProcurements],
2516
2682
  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;
2683
+ registeredSprites: (() => {
2684
+ // Registered sprites ride the save from wherever they are:
2685
+ // STANDING here (a live companion, whose services are the
2686
+ // truth for its task count) or WAITING in the redeploy stash
2687
+ // after a fold. Both sources, because a technomancer can have
2688
+ // some of each -- taking only one of the two lists is how a
2689
+ // sprite that happened to be manifest at save time would
2690
+ // survive while its folded sibling silently did not.
2691
+ const live = this.companions
2692
+ .filter(c => c.kind === 'sprite' && c.registered && c.spriteType)
2693
+ .map(c => ({ spriteType: c.spriteType, level: c.force, tasks: c.services ?? 0, name: c.npc.name }));
2694
+ const all = [...live, ...this._pendingSpriteRedeploy.map(s => ({ ...s }))];
2695
+ return all.length > 0 ? all : undefined;
2524
2696
  })(),
2525
2697
  crew: this.crew.map(m => ({ ...m, gear: [...m.gear] })),
2526
2698
  commandRefusals: { ...this.commandRefusals },
@@ -2634,6 +2806,124 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2634
2806
  * state (a persona still jacked in, an astral projection, a sneak,
2635
2807
  * open combat bookkeeping) is settled on the way home.
2636
2808
  */
2809
+ /**
2810
+ * THE JOB YOU LEFT STANDING (mYTCzJbsdZxYsTsZo: "I'd like it to not
2811
+ * fail the run -- I should be able to come and go to the job as I
2812
+ * need (to get new gear)").
2813
+ *
2814
+ * Held in memory only, and deliberately NOT saved. Mid-run state
2815
+ * never is -- "runs are disposable, a quit resumes to the hub with
2816
+ * the job back on offer" -- so an errand is SESSION-SCOPED and needs
2817
+ * no save-schema change. Quit mid-errand and you get exactly today's
2818
+ * behaviour rather than a half-restored scene from a save that was
2819
+ * never designed to hold one.
2820
+ *
2821
+ * The rooms survive because something finally holds them: returnToHub
2822
+ * orphans a run "wholesale" only because nothing kept a reference.
2823
+ */
2824
+ _openRun;
2825
+ /** Is there a job waiting for the runner to come back to it? */
2826
+ get hasOpenRun() {
2827
+ return this._openRun !== undefined;
2828
+ }
2829
+ /** The room an open run is waiting in, for the surfaces that say so. */
2830
+ get openRunSceneName() {
2831
+ return this._openRun?.room.name;
2832
+ }
2833
+ /**
2834
+ * AN ERRAND IS NOT A HOMECOMING, and that is the whole design.
2835
+ *
2836
+ * returnToHub is a RITUAL, not a journey. Walking its full length
2837
+ * twice per gear run would hand a player, free and on demand: a full
2838
+ * stun heal at High lifestyle ("the shower runs hot until the run is
2839
+ * a rumor"), every vendor's shelves restocked, a fresh legwork pull
2840
+ * from every contact, the busking cycle reset, and a rerolled
2841
+ * for-hire board -- while ALSO charging rent again and burning a
2842
+ * twelfth of the DocWagon contract, whose year "ticks in homecomings".
2843
+ * Wrong in both directions at once: the exploit and the punishment
2844
+ * are the same line of code.
2845
+ *
2846
+ * So this is its own path rather than a flag on that one. Written as
2847
+ * a separate method ON PURPOSE: parameterising the ritual would mean
2848
+ * every future line added to it silently joins the errand too, and
2849
+ * the failure would be invisible -- a restock nobody notices is not a
2850
+ * crash. Here, anything an errand should do has to be written here.
2851
+ *
2852
+ * WHAT IT DOES: state hygiene the cab demands (a persona cannot ride
2853
+ * in a taxi), companions fold and unfold as on any trip, and the
2854
+ * scene swaps. Nothing else.
2855
+ *
2856
+ * WHAT IT DOES NOT DO, all of it deliberate: no client settlement, no
2857
+ * crew settlement or disband, no jobsRun tally, no lifestyle upkeep,
2858
+ * no restock, no rumor, no gig advance, no hire reroll, no DocWagon
2859
+ * tick, no legwork refresh -- and NO per-job reset. A RETURN LEG IS
2860
+ * NOT A NEW JOB: Edge does not refill, haggledThisJob does not clear,
2861
+ * First Impression does not re-arm. You come back as you left, which
2862
+ * is the difference between fetching a gun and starting over.
2863
+ */
2864
+ leaveRunForErrand() {
2865
+ const hub = this._hubScene;
2866
+ if (!hub || this.scene === hub)
2867
+ return undefined;
2868
+ this._openRun = {
2869
+ scene: this.scene,
2870
+ room: this.player.currentLocation,
2871
+ spot: this.player.atSpot,
2872
+ };
2873
+ const foldLines = this.foldCompanionsForTravel();
2874
+ this.settleTravelState();
2875
+ this.scene = hub;
2876
+ this.scene.addRoom(this._homeRoom);
2877
+ this.connectHomeRoom(hub.determineStartRoom());
2878
+ this.player.currentLocation = this._homeRoom;
2879
+ this.player.cameFrom = undefined;
2880
+ const arrivalLines = this.unfoldCompanionsOnArrival();
2881
+ this.refreshHubUi();
2882
+ return [...foldLines, ...arrivalLines].join('\n');
2883
+ }
2884
+ /** Back to the job you left standing. */
2885
+ resumeOpenRun() {
2886
+ const open = this._openRun;
2887
+ if (!open)
2888
+ return undefined;
2889
+ const foldLines = this.foldCompanionsForTravel();
2890
+ this.settleTravelState();
2891
+ this.scene = open.scene;
2892
+ this.player.currentLocation = open.room;
2893
+ // AFTER currentLocation: its setter clears atSpot on every room
2894
+ // change, so the order here is load-bearing (same trap as
2895
+ // scene-factory's stationing).
2896
+ this.player.atSpot = open.spot;
2897
+ this.player.cameFrom = undefined;
2898
+ this._openRun = undefined;
2899
+ const arrivalLines = this.unfoldCompanionsOnArrival();
2900
+ this.refreshHubUi();
2901
+ return [...foldLines, ...arrivalLines].join('\n');
2902
+ }
2903
+ /** Cross-plane and combat state that must not ride in a cab, shared
2904
+ * by both legs of an errand. A persona has no body to put in a taxi
2905
+ * and a fight does not travel. */
2906
+ settleTravelState() {
2907
+ this.player.plane = 'meat';
2908
+ this.player.bodyRoom = undefined;
2909
+ this.player.simMode = undefined;
2910
+ this.player.astralTicks = 0;
2911
+ this.player.activeDeck = undefined;
2912
+ this.player.riggedDrone = undefined;
2913
+ this.player.astralPerceiving = false;
2914
+ this.player.sneaking = false;
2915
+ this.player.inExchange = false;
2916
+ this.player.combatOpponent = undefined;
2917
+ this.player.activeCallPartner = undefined;
2918
+ this.autoFight.cancelPending();
2919
+ }
2920
+ /** The panel refresh both errand legs need. */
2921
+ refreshHubUi() {
2922
+ this.updateEquipment(this.player.equipment);
2923
+ this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
2924
+ this.updateExits(this.player.currentLocation);
2925
+ this.updateStatus();
2926
+ }
2637
2927
  returnToHub() {
2638
2928
  const hub = this._hubScene;
2639
2929
  if (!hub)
@@ -2665,6 +2955,25 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2665
2955
  this.player.astralTicks = 0;
2666
2956
  this.player.activeDeck = undefined;
2667
2957
  this.player.riggedDrone = undefined;
2958
+ // THE JOB'S NEGOTIATION ENDS WITH THE JOB (d4yoYPcRAtWpLtm9y).
2959
+ //
2960
+ // haggledThisJob is per-JOB and was only ever cleared on DEPARTURE
2961
+ // (applyDepartureNeeds -> refreshEdge). Coming home cleared nothing,
2962
+ // so a runner who pushed on the number at this job's meet carried
2963
+ // the flag back to the hub -- and launchPendingRun's offer-window
2964
+ // capture then restored that stale true over the next job's fresh
2965
+ // clear, refusing their first real haggle with "you already pushed
2966
+ // on the number once this job."
2967
+ //
2968
+ // Cleared HERE rather than papering over it at the capture, because
2969
+ // the flag's meaning is "this job", and the moment the job ends is
2970
+ // when that stops being true. Anything set afterwards belongs to
2971
+ // the next offer, which is exactly what the capture wants to keep.
2972
+ //
2973
+ // Deliberately NOT refreshEdge(): Edge refills on departure, fed
2974
+ // and slept (SR5e p.56), and refilling it on arrival home would
2975
+ // hand back a resource the next run is supposed to start without.
2976
+ this.player.haggledThisJob = false;
2668
2977
  this.player.astralPerceiving = false;
2669
2978
  this.player.sneaking = false;
2670
2979
  this.player.inExchange = false;
@@ -3215,6 +3524,28 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3215
3524
  // The departure save: hub state with the job still on offer -- what
3216
3525
  // a mid-run quit resumes to (the "runs are disposable" ruling).
3217
3526
  this.requestSave('job-accept');
3527
+ // CARRY THE OFFER'S HAGGLE ACROSS THE TRAVEL, BUT ONLY THIS
3528
+ // OFFER'S (d4yoYPcRAtWpLtm9y: "I got a notice that I've already
3529
+ // pushed the number once, however I haven't").
3530
+ //
3531
+ // activateRunScene below refreshes per-job state, which clears
3532
+ // haggledThisJob -- correct, a new job is a new negotiation. This
3533
+ // capture/restore exists because the offer itself has a haggle
3534
+ // window at the hub ("call <fixer>" delivers it), and a push made
3535
+ // there must not be forgotten by the drive over.
3536
+ //
3537
+ // THE BUG WAS THAT THE CAPTURE COULD NOT TELL WHOSE HAGGLE IT WAS.
3538
+ // haggledThisJob survives a finished run -- returnToHub clears
3539
+ // neither it nor Edge -- so a runner who haggled at LAST job's meet
3540
+ // came back to the hub still flagged, accepted a new job, and this
3541
+ // line captured that stale true and faithfully restored it over the
3542
+ // fresh clear. Their first haggle of the new job was then refused
3543
+ // with "you already pushed on the number once this job", which was
3544
+ // false, and the meet had just told them the rate was negotiable.
3545
+ //
3546
+ // Fixed at the source rather than here: returnToHub now ends the
3547
+ // job's negotiation with the job, so anything still set when a ride
3548
+ // is called genuinely belongs to the offer in hand.
3218
3549
  const haggledAtOffer = this.player.haggledThisJob;
3219
3550
  try {
3220
3551
  const banner = await this.activateRunScene(seed, tier, { startInHideout: false });
@@ -4235,7 +4566,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4235
4566
  * future command can't silently fall out of help.
4236
4567
  */
4237
4568
  static HELP_CATEGORIES = [
4238
- { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow', 'unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
4569
+ { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['climb', 'descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow', 'unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
4239
4570
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
4240
4571
  { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['stance'], ['surrender'], ['edge'], ['rest']] },
4241
4572
  // The party layer: everyone who walks (or flies, or manifests) at
@@ -4247,9 +4578,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4247
4578
  { title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
4248
4579
  // The Emerged get their own shelf (player request: "there MUST be
4249
4580
  // resonance" -- matrix is the place, Resonance is the talent).
4250
- { title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
4581
+ { title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sprites']] },
4251
4582
  { title: 'social', entries: [['say'], ['emote', 'me'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
4252
- { title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
4583
+ { title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
4253
4584
  { title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
4254
4585
  { title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
4255
4586
  { title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['roles'], ['clear'], ['quit', 'exit']] },