@maka/maka-cli 5.5.0 → 5.7.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 (212) hide show
  1. package/bundle/typescript/package.json +2 -2
  2. package/bundle/typescript/src/commands/_index.d.ts +4 -0
  3. package/bundle/typescript/src/commands/_index.js +4 -0
  4. package/bundle/typescript/src/commands/_index.js.map +1 -1
  5. package/bundle/typescript/src/commands/debug.js +1 -7
  6. package/bundle/typescript/src/commands/debug.js.map +1 -1
  7. package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js +17 -1
  8. package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js.map +1 -1
  9. package/bundle/typescript/src/commands/env/_index.d.ts +3 -0
  10. package/bundle/typescript/src/commands/env/_index.js +4 -0
  11. package/bundle/typescript/src/commands/env/_index.js.map +1 -0
  12. package/bundle/typescript/src/commands/env/env.command.d.ts +3 -0
  13. package/bundle/typescript/src/commands/env/env.command.js +22 -0
  14. package/bundle/typescript/src/commands/env/env.command.js.map +1 -0
  15. package/bundle/typescript/src/commands/env/get.sub.cmd.d.ts +1 -0
  16. package/bundle/typescript/src/commands/env/get.sub.cmd.js +46 -0
  17. package/bundle/typescript/src/commands/env/get.sub.cmd.js.map +1 -0
  18. package/bundle/typescript/src/commands/env/prompt-for-env.d.ts +10 -0
  19. package/bundle/typescript/src/commands/env/prompt-for-env.js +27 -0
  20. package/bundle/typescript/src/commands/env/prompt-for-env.js.map +1 -0
  21. package/bundle/typescript/src/commands/env/set.sub.cmd.d.ts +1 -0
  22. package/bundle/typescript/src/commands/env/set.sub.cmd.js +59 -0
  23. package/bundle/typescript/src/commands/env/set.sub.cmd.js.map +1 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +16 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +79 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +31 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +125 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.d.ts +17 -7
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +113 -15
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.d.ts +37 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +193 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +3 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +10 -3
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.d.ts +3 -2
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +12 -4
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js +7 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js.map +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.d.ts +16 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +97 -0
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -0
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +21 -10
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +41 -27
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +12 -0
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +63 -7
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.d.ts +35 -1
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +277 -44
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.d.ts +13 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +37 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -0
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/drink.d.ts +14 -0
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/drink.js +15 -0
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/drink.js.map +1 -0
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +1 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +7 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/eat.d.ts +14 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/eat.js +15 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/eat.js.map +1 -0
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js +10 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +3 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +101 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +4 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +15 -11
  78. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +3 -3
  80. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +58 -2
  82. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  83. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.d.ts +26 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +125 -0
  85. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -0
  86. package/bundle/typescript/src/commands/game/sideQuest/commands/install.d.ts +21 -0
  87. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +125 -0
  88. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js.map +1 -0
  89. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.d.ts +16 -0
  90. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +62 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js.map +1 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.d.ts +16 -0
  93. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js +76 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js.map +1 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/commands/lock.js +4 -0
  96. package/bundle/typescript/src/commands/game/sideQuest/commands/lock.js.map +1 -1
  97. package/bundle/typescript/src/commands/game/sideQuest/commands/pay.d.ts +13 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/commands/pay.js +39 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/commands/pay.js.map +1 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +5 -1
  101. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  102. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +5 -2
  103. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.d.ts +21 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +121 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js.map +1 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +60 -12
  108. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
  109. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +27 -10
  110. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  111. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +6 -2
  112. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  113. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.d.ts +17 -0
  114. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +125 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -0
  116. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +36 -3
  117. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  118. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +9 -1
  119. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js.map +1 -1
  120. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +27 -4
  121. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -1
  122. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +1 -1
  123. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -1
  124. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +8 -0
  125. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  126. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.d.ts +14 -0
  127. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js +67 -0
  128. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js.map +1 -0
  129. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +7 -0
  130. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  131. package/bundle/typescript/src/commands/game/sideQuest/commands/use.d.ts +29 -0
  132. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +118 -11
  133. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  134. package/bundle/typescript/src/commands/game/sideQuest/commands/work.d.ts +15 -0
  135. package/bundle/typescript/src/commands/game/sideQuest/commands/work.js +68 -0
  136. package/bundle/typescript/src/commands/game/sideQuest/commands/work.js.map +1 -0
  137. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js +2 -1
  138. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js.map +1 -1
  139. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +1 -1
  140. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +131 -4
  141. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  142. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +6 -2
  143. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +10 -6
  144. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
  145. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +115 -0
  146. package/bundle/typescript/src/commands/game/sideQuest/game.js +464 -25
  147. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  148. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +28 -0
  149. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +111 -0
  150. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  151. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +70 -7
  152. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  153. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +109 -4
  154. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +423 -39
  155. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  156. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +6 -6
  157. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +38 -14
  158. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  159. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +9 -0
  160. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +41 -8
  161. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  162. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +44 -0
  163. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +155 -0
  164. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -0
  165. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +274 -5
  166. package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
  167. package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +17 -0
  168. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.d.ts +2 -0
  169. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js +4 -0
  170. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js.map +1 -1
  171. package/bundle/typescript/src/commands/game/sideQuest/ui.js +50 -3
  172. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  173. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.d.ts +13 -6
  174. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +25 -14
  175. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
  176. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +100 -10
  177. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  178. package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.d.ts +56 -0
  179. package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js +144 -0
  180. package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js.map +1 -0
  181. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.d.ts +10 -0
  182. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.js +16 -0
  183. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.js.map +1 -0
  184. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +21 -0
  185. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +84 -0
  186. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  187. package/bundle/typescript/src/commands/game/sideQuest/utilities/terminal-reset.d.ts +17 -0
  188. package/bundle/typescript/src/commands/game/sideQuest/utilities/terminal-reset.js +23 -0
  189. package/bundle/typescript/src/commands/game/sideQuest/utilities/terminal-reset.js.map +1 -0
  190. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +51 -15
  191. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  192. package/bundle/typescript/src/commands/remote/reconfig.sub.cmd.js +27 -1
  193. package/bundle/typescript/src/commands/remote/reconfig.sub.cmd.js.map +1 -1
  194. package/bundle/typescript/src/commands/run/run.command.js +1 -7
  195. package/bundle/typescript/src/commands/run/run.command.js.map +1 -1
  196. package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js +1 -6
  197. package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js.map +1 -1
  198. package/bundle/typescript/src/generators/config.js +11 -10
  199. package/bundle/typescript/src/generators/config.js.map +1 -1
  200. package/bundle/typescript/src/templates/meteor/config/pm2.config.js +3 -3
  201. package/bundle/typescript/src/templates/meteor/project/gitignore +2 -1
  202. package/bundle/typescript/src/tool.d.ts +2 -0
  203. package/bundle/typescript/src/tool.js +3 -0
  204. package/bundle/typescript/src/tool.js.map +1 -1
  205. package/bundle/typescript/src/tools/aws/aws.class.js +6 -4
  206. package/bundle/typescript/src/tools/aws/aws.class.js.map +1 -1
  207. package/bundle/typescript/src/tools/dotenvx/dotenvx.class.d.ts +24 -0
  208. package/bundle/typescript/src/tools/dotenvx/dotenvx.class.js +66 -0
  209. package/bundle/typescript/src/tools/dotenvx/dotenvx.class.js.map +1 -0
  210. package/bundle/typescript/src/tools/ssh/ssh.class.js +1 -1
  211. package/bundle/typescript/src/tools/ssh/ssh.class.js.map +1 -1
  212. package/package.json +2 -2
@@ -3,6 +3,10 @@ import { Logger } from '../utilities/logger.js';
3
3
  import { Inventory } from './inventory.js';
4
4
  import { AbstractPlayer } from '../types/shared/abstracts.js';
5
5
  export const STANCE_CYCLE = ['manual', 'guarded', 'aggressive'];
6
+ /** Meat and a jumped-in drone share the physical world. */
7
+ export function isPhysicalPlane(plane) {
8
+ return plane === 'meat' || plane === 'drone';
9
+ }
6
10
  export const SKILL_NAMES = [
7
11
  'firearms', 'blades', 'unarmed',
8
12
  'sneaking', 'perception', 'athletics',
@@ -166,13 +170,28 @@ export class Player extends AbstractPlayer {
166
170
  canPerceive(other) {
167
171
  if (this.plane === other.plane)
168
172
  return true;
169
- return this.plane === 'astral' && other.plane === 'meat';
173
+ // A drone is a physical machine in the meat world: meat sees it, its
174
+ // sensors see meat -- and combat between them is ordinary physics.
175
+ if (isPhysicalPlane(this.plane) && isPhysicalPlane(other.plane))
176
+ return true;
177
+ return this.plane === 'astral' && isPhysicalPlane(other.plane);
178
+ }
179
+ /** Whether these two can trade physical blows (see attack.ts targeting). */
180
+ sharesCombatPlane(other) {
181
+ return this.plane === other.plane
182
+ || (isPhysicalPlane(this.plane) && isPhysicalPlane(other.plane));
170
183
  }
171
184
  // True while an AI-driven action is parked waiting for an in-flight
172
185
  // exchange to settle (CombatExchange.waitForLull). One queued action per
173
186
  // actor: a second colliding attempt is refused outright instead of
174
187
  // stacking a burst of delayed attacks behind the same lull.
175
188
  waitingForLull = false;
189
+ // Death-resolution claim (CombatExchange.resolveDeath): set
190
+ // SYNCHRONOUSLY by the first resolution to reach this actor, so a
191
+ // concurrent kill (autopilot + typed action racing -- a real session
192
+ // crashed on the second removeActor) skips cleanly instead of
193
+ // narrating a second death during the first one's narration beats.
194
+ deathClaimed = false;
176
195
  // Combat attributes (see ICombatAttributes). Defaults make an unseeded
177
196
  // actor a competent-but-mortal street-level runner; seeds can override
178
197
  // per-NPC to make a hardened enforcer or a soft civilian.
@@ -225,8 +244,192 @@ export class Player extends AbstractPlayer {
225
244
  this.resonance = config.combat?.resonance ?? 0;
226
245
  this.adept = config.combat?.adept ?? false;
227
246
  this.skills = { ...(config.combat?.skills ?? {}) };
247
+ // Essence bookkeeping needs the UNAUGMENTED talent to burn down from
248
+ // (see installAug) -- capture it before any 'ware lands.
249
+ this.baseMagic = this.magic;
250
+ this.baseResonance = this.resonance;
228
251
  this._equipment = this.initializeEquipment();
229
252
  }
253
+ // ==================== Augmentations (see augmentations.ts) ====================
254
+ /** Everyone is born whole. */
255
+ static ESSENCE_MAX = 6;
256
+ // What's left of the meat. Burned permanently by installAug; hitting 0
257
+ // is refused at the clinic -- there'd be nothing left to cut away.
258
+ essence = Player.ESSENCE_MAX;
259
+ // The unaugmented Magic/Resonance, from which essence loss burns down
260
+ // (SR5e: each full point of essence lost -- ROUNDED UP -- costs one).
261
+ // `magic`/`resonance` above stay the EFFECTIVE values every pool reads.
262
+ baseMagic = 0;
263
+ baseResonance = 0;
264
+ /** Installed 'ware, by catalog key (see AUGMENTATIONS). */
265
+ augmentations = new Set();
266
+ hasAug(key) {
267
+ return this.augmentations.has(key);
268
+ }
269
+ /** Full essence points lost, rounded up -- the toll Magic/Resonance pay. */
270
+ get essenceBurn() {
271
+ return Math.ceil(Player.ESSENCE_MAX - this.essence - 1e-9);
272
+ }
273
+ /**
274
+ * Burns the essence and records the 'ware. No gating here -- the clinic
275
+ * (commands/install.ts) owns the refusals (essence floor, conflicts,
276
+ * payment); archetype starting chrome installs through this too.
277
+ */
278
+ installAug(aug) {
279
+ this.augmentations.add(aug.key);
280
+ // One-decimal arithmetic: 6 - 0.1 - 0.2 must read 5.7, not 5.6999....
281
+ this.essence = Math.round((this.essence - aug.essence) * 10) / 10;
282
+ const burn = this.essenceBurn;
283
+ this.magic = Math.max(0, this.baseMagic - burn);
284
+ this.resonance = Math.max(0, this.baseResonance - burn);
285
+ }
286
+ /**
287
+ * The single tap every pool reads chrome through. Wired Reflexes tiers
288
+ * don't stack -- the higher tier wins (II supersedes I) -- but the
289
+ * synaptic-vs-wired conflict is the clinic's job, not this getter's.
290
+ */
291
+ augBonus(kind) {
292
+ if (this.augmentations.size === 0)
293
+ return 0;
294
+ const has = (k) => this.augmentations.has(k);
295
+ const reflexes = (has('wired-reflexes-2') ? 2 : has('wired-reflexes-1') ? 1 : 0) + (has('synaptic-booster') ? 1 : 0);
296
+ switch (kind) {
297
+ case 'initiative': return reflexes;
298
+ case 'defense': return reflexes;
299
+ case 'attack': return has('muscle-toner') ? 1 : 0;
300
+ case 'firearms': return has('smartlink') ? 2 : 0;
301
+ case 'soak': return (has('bone-lacing') ? 1 : 0) + (has('dermal-plating') ? 2 : 0);
302
+ case 'unarmed-damage': return has('bone-lacing') ? 2 : 0;
303
+ case 'perception': return has('cybereyes') ? 2 : 0;
304
+ case 'matrix': return has('datajack') ? 1 : 0;
305
+ case 'piloting': return has('control-rig') ? 2 : 0;
306
+ }
307
+ }
308
+ // ==================== Qualities (see qualities.ts) ====================
309
+ // SR5e's traits (p.71-85), baked into the archetype at creation
310
+ // (outfitArchetype). Every entry in the catalog has a real mechanical
311
+ // hook; the ones that read as flat pool nudges all tap through
312
+ // qualityBonus below, the same single-switch pattern as augBonus.
313
+ /** Installed qualities, by catalog key (see QUALITIES). */
314
+ qualities = new Set();
315
+ hasQuality(key) {
316
+ return this.qualities.has(key);
317
+ }
318
+ /**
319
+ * Records a quality and applies its one-time creation effect. Lucky is
320
+ * the only quality that edits an attribute (RAW raises the Edge ceiling;
321
+ * with no attribute point-buy here, the point lands directly).
322
+ */
323
+ addQuality(key) {
324
+ if (this.qualities.has(key))
325
+ return;
326
+ this.qualities.add(key);
327
+ if (key === 'lucky') {
328
+ this.edge += 1;
329
+ this.edgeRemaining += 1;
330
+ }
331
+ }
332
+ /** The single tap every pool reads qualities through (cf. augBonus). */
333
+ qualityBonus(kind) {
334
+ if (this.qualities.size === 0)
335
+ return 0;
336
+ const has = (k) => this.qualities.has(k);
337
+ switch (kind) {
338
+ case 'sneak': return (has('catlike') ? 2 : 0) + (has('distinctive-style') ? -2 : 0);
339
+ case 'athletics': return has('natural-athlete') ? 2 : 0;
340
+ case 'matrix': return has('simsense-vertigo') ? -2 : 0;
341
+ case 'hacking': return (has('analytical-mind') ? 2 : 0) + this.qualityBonus('matrix');
342
+ case 'piloting': return has('juryrigger') ? 2 : 0;
343
+ case 'soak': return has('toughness') ? 1 : 0;
344
+ case 'healing': return has('quick-healer') ? 2 : 0;
345
+ }
346
+ }
347
+ // First Impression (p.74): the first social play of each job rolls +2.
348
+ // Consumed like the edge boost and the liquid-courage die; re-armed
349
+ // with the per-job resets in refreshEdge.
350
+ _firstImpressionUsed = false;
351
+ /** Spends the first impression: +2 for the social roll happening NOW. */
352
+ consumeFirstImpression() {
353
+ if (!this.qualities.has('first-impression') || this._firstImpressionUsed)
354
+ return 0;
355
+ this._firstImpressionUsed = true;
356
+ return 2;
357
+ }
358
+ /** SINner tax (p.84-85): the cut every payout loses to having a file. */
359
+ get sinTaxRate() {
360
+ if (this.qualities.has('sinner-corp'))
361
+ return 0.20;
362
+ if (this.qualities.has('sinner-criminal') || this.qualities.has('sinner-national'))
363
+ return 0.15;
364
+ return 0;
365
+ }
366
+ // Combat Paralysis (p.80): armed by beginExchange when a FRESH fight
367
+ // opens, consumed by the initiative roll-off -- pool halved, then the
368
+ // freeze passes (subsequent exchanges of the same fight roll normally).
369
+ paralysisFreeze = false;
370
+ // ==================== Initiation & Submersion (SR5e p.324-326 / p.257-258) ====================
371
+ // The Awakened (and technomancer) endgame: karma-priced grades (10 +
372
+ // grade x3, see commands/initiate.ts) that lift the talent ceiling to
373
+ // 6 + grade and grant one METAMAGIC (or echo) each. Grade can never
374
+ // exceed the BASE talent (RAW). The picks live here as a Set so every
375
+ // pool can read them the same way it reads qualities and chrome.
376
+ initiationGrade = 0;
377
+ /** Metamagics (casters/adepts) and echoes (technomancers), by key. */
378
+ metamagics = new Set();
379
+ hasMetamagic(key) {
380
+ return this.metamagics.has(key);
381
+ }
382
+ // ==================== Reputation (SR5e p.367-368) ====================
383
+ // Three tracked scores. STREET CRED is earned karma made visible: one
384
+ // point per full 10 career karma (earned on the street -- creation
385
+ // karma doesn't count; the name starts unknown). NOTORIETY starts from
386
+ // the qualities (the book's own list) and grows from incidents --
387
+ // walking out on a job, cutting down someone mid-surrender -- each
388
+ // counted ONCE (RAW: never twice for the same incident). PUBLIC
389
+ // AWARENESS derives from both, plus a Distinctive Style everyone can
390
+ // describe to a Knight Errant sketch artist.
391
+ /** Total karma EARNED in play -- spending it doesn't touch this. */
392
+ careerKarma = 0;
393
+ // Each stain on the record, in the player's own fiction ("walked out
394
+ // on 'Tacoma Rain'"). The strings ARE the dedupe keys.
395
+ _notorietyReasons = [];
396
+ /** Records an incident once; false means this exact stain already stands. */
397
+ addNotoriety(reason) {
398
+ if (this._notorietyReasons.includes(reason))
399
+ return false;
400
+ this._notorietyReasons.push(reason);
401
+ return true;
402
+ }
403
+ get notorietyReasons() {
404
+ return this._notorietyReasons;
405
+ }
406
+ get streetCred() {
407
+ return Math.floor(this.careerKarma / 10);
408
+ }
409
+ /** The p.368 quality list, restricted to the qualities this game ships. */
410
+ get qualityNotoriety() {
411
+ const plus = ['bad-luck', 'combat-paralysis', 'sinner-criminal']
412
+ .filter(k => this.qualities.has(k)).length
413
+ + [...this.qualities].filter(k => k.startsWith('incompetent-')).length;
414
+ const minus = ['first-impression', 'lucky'].filter(k => this.qualities.has(k)).length;
415
+ return plus - minus;
416
+ }
417
+ get notoriety() {
418
+ return Math.max(0, this.qualityNotoriety + this._notorietyReasons.length);
419
+ }
420
+ get publicAwareness() {
421
+ return Math.floor((this.streetCred + this.notoriety) / 3)
422
+ + (this.qualities.has('distinctive-style') ? 1 : 0);
423
+ }
424
+ /**
425
+ * The dice a NAME is worth in shadow-side social work (haggle,
426
+ * persuade): cred minus the stories that undercut it. RAW makes cred a
427
+ * social LIMIT bonus; with no limit system here, net-rep dice carry
428
+ * the same idea -- and notoriety eating cred keeps it self-balancing.
429
+ */
430
+ get socialRep() {
431
+ return Math.max(0, this.streetCred - this.notoriety);
432
+ }
230
433
  // ======================== Skills (see SkillName) ========================
231
434
  /** Explicit per-skill ratings; anything absent resolves via skillRating(). */
232
435
  skills = {};
@@ -241,6 +444,10 @@ export class Player extends AbstractPlayer {
241
444
  * everything else is 0 (unskilled).
242
445
  */
243
446
  skillRating(name) {
447
+ // Incompetent (p.81): the skill isn't low, it's ABSENT -- pinned to 0
448
+ // no matter what the sheet or the fallback would say.
449
+ if (this.qualities.has(`incompetent-${name}`))
450
+ return 0;
244
451
  const explicit = this.skills[name];
245
452
  if (typeof explicit === 'number')
246
453
  return Math.max(0, explicit);
@@ -628,7 +835,22 @@ export class Player extends AbstractPlayer {
628
835
  * at everything, including not getting shot.
629
836
  */
630
837
  get woundModifier() {
631
- return -(Math.floor(this._damageTaken / 3) + Math.floor(this._stunTaken / 3));
838
+ const painEditor = this.augmentations.has('pain-editor');
839
+ // High Pain Tolerance (p.74): one box simply doesn't count toward
840
+ // modifiers -- taken off the physical track first, spilling to stun if
841
+ // there's nothing physical to ignore. RAW forbids stacking it with a
842
+ // pain editor, so the chrome wins when both are present.
843
+ const ignore = !painEditor && this.qualities.has('high-pain-tolerance') ? 1 : 0;
844
+ const dmg = Math.max(0, this._damageTaken - ignore);
845
+ const spill = Math.max(0, ignore - this._damageTaken);
846
+ const stun = Math.max(0, this._stunTaken - spill);
847
+ // Low Pain Tolerance (p.82): the -1 arrives every 2 boxes, not 3.
848
+ const step = this.qualities.has('low-pain-tolerance') ? 2 : 3;
849
+ const raw = Math.floor(dmg / step) + Math.floor(stun / step);
850
+ // A pain editor files the first wound tier under "later": the -1 never
851
+ // arrives, though everything past it still does.
852
+ const eased = painEditor ? Math.max(0, raw - 1) : raw;
853
+ return -eased;
632
854
  }
633
855
  /** Dead: the physical track is full. */
634
856
  isDown() {
@@ -680,10 +902,17 @@ export class Player extends AbstractPlayer {
680
902
  const filled = Math.min(max, this._damageTaken);
681
903
  return `${'■'.repeat(filled)}${'□'.repeat(max - filled)} (${filled}/${max})`;
682
904
  }
683
- /** Clears stun boxes; returns how many were actually recovered. */
905
+ /**
906
+ * Clears stun boxes; returns how many were actually recovered. FATIGUE
907
+ * boxes (see lockedFatigue) are untouchable until their need is met --
908
+ * SR5e p.172: fatigue damage cannot be healed while the condition
909
+ * causing it still exists. Eating/sleeping unlocks them (markFed /
910
+ * markSlept), after which they heal like any stun.
911
+ */
684
912
  healStun(boxes) {
685
913
  const before = this._stunTaken;
686
- this._stunTaken = Math.max(0, before - Math.max(0, Math.floor(boxes)));
914
+ const floor = Math.min(before, this.lockedFatigue);
915
+ this._stunTaken = Math.max(floor, before - Math.max(0, Math.floor(boxes)));
687
916
  return before - this._stunTaken;
688
917
  }
689
918
  /** The stun track, same rendering as conditionSummary(). */
@@ -702,7 +931,14 @@ export class Player extends AbstractPlayer {
702
931
  // Off the meat plane your gun is just a memory: Matrix combat runs on
703
932
  // attack programs (Logic-scaled), astral combat on raw will given
704
933
  // form (Magic-scaled). Hot-sim's deeper immersion sharpens the
705
- // programs' bite.
934
+ // programs' bite. A jumped-in drone fights with its MOUNT (armed
935
+ // airframes only -- attack.ts refuses an unarmed recon frame).
936
+ if (this.plane === 'drone') {
937
+ const quality = this.riggedDrone?.qualityBonus ?? 0;
938
+ return this.droneArmed
939
+ ? { name: `${this.riggedDrone?.name ?? 'drone'} weapon mount`, damage: 2 + quality, poolBonus: 0 }
940
+ : { name: 'no weapon mounts', damage: 0, poolBonus: 0 };
941
+ }
706
942
  if (this.plane === 'matrix') {
707
943
  const hot = this.simMode === 'hot' ? 1 : 0;
708
944
  // The deck IS the arsenal: its quality feeds program damage the way
@@ -717,19 +953,21 @@ export class Player extends AbstractPlayer {
717
953
  if (this.plane === 'astral') {
718
954
  return { name: 'astral strike', damage: 2 + Math.floor(Math.max(1, this.magic) / 2), poolBonus: 0 };
719
955
  }
720
- // Muscle drives melee: unarmed 1 + STR/2, melee weapons 2 + STR/2 +
721
- // quality (a strength-3 actor lands exactly the old flat numbers).
722
- // Firearms don't care how strong you are. A HOLSTERED weapon counts
723
- // for nothing -- until it's drawn (brandish, or the auto-draw when an
724
- // exchange opens), your hands are empty.
956
+ // Muscle drives melee, and the weapon CLASS drives everything else
957
+ // now (RAW gear pass, see Item.rangedBase/meleeBase/armorPen): a
958
+ // holdout no longer hits like a shotgun, and a knife's bite comes
959
+ // partly from ignoring armor. Quality still rides on top. A HOLSTERED
960
+ // weapon counts for nothing -- until it's drawn (brandish, or the
961
+ // auto-draw when an exchange opens), your hands are empty.
725
962
  const strengthBase = Math.floor(this.strength / 2);
726
963
  const weapon = this.weaponDrawn ? this.getCarriedWeapon() : null;
727
964
  if (!weapon) {
728
- return { name: 'bare fists', damage: 1 + strengthBase, poolBonus: 0 };
965
+ // Bone lacing turns the fists themselves into weapons.
966
+ return { name: 'bare fists', damage: 1 + strengthBase + this.augBonus('unarmed-damage'), poolBonus: 0, armorPen: 0 };
729
967
  }
730
968
  const bonus = weapon.qualityBonus;
731
- const base = weapon.category === Category.MeleeWeapon ? 2 + strengthBase : 4;
732
- return { name: weapon.name, damage: base + bonus, poolBonus: bonus };
969
+ const base = weapon.category === Category.MeleeWeapon ? weapon.meleeBase + strengthBase : weapon.rangedBase;
970
+ return { name: weapon.name, damage: base + bonus, poolBonus: bonus, armorPen: weapon.armorPen };
733
971
  }
734
972
  /** The weapon riding in a hand slot, drawn or holstered. */
735
973
  getCarriedWeapon() {
@@ -754,14 +992,28 @@ export class Player extends AbstractPlayer {
754
992
  }
755
993
  /** Total soak dice from every equipped armor-category item. */
756
994
  getArmorValue() {
995
+ // RAW armor pass (p.436-437, compressed): the BODY piece carries the
996
+ // real protection (1 + quality, +1 for jacket/plate-class weight --
997
+ // RAW clothing 6 vs jacket 12), a helmet or shield adds its flat
998
+ // point, and trim (boots, gloves, arm/leg pieces) is flavor, not
999
+ // dice -- stacking four rated accessories used to out-armor the
1000
+ // jacket they hung off.
757
1001
  let total = 0;
758
1002
  for (const group of Object.values(this._equipment)) {
759
1003
  if (!group)
760
1004
  continue;
761
1005
  for (const item of Object.values(group)) {
762
- if (item && Player.ARMOR_CATEGORIES.has(item.category)) {
763
- total += 1 + item.qualityBonus;
764
- }
1006
+ if (!item || !Player.ARMOR_CATEGORIES.has(item.category))
1007
+ continue;
1008
+ if (item.category === Category.Helmet)
1009
+ total += 1;
1010
+ else if (item.category === Category.Shield)
1011
+ total += 2;
1012
+ else if (item.category === Category.Boots || item.category === Category.Gloves
1013
+ || item.category === Category.ArmArmor || item.category === Category.LegArmor)
1014
+ total += 0;
1015
+ else
1016
+ total += 1 + item.qualityBonus + (item.isHeavyArmor ? 1 : 0);
765
1017
  }
766
1018
  }
767
1019
  return total;
@@ -769,21 +1021,33 @@ export class Player extends AbstractPlayer {
769
1021
  getAttackPool() {
770
1022
  // Plane decides which attribute drives the fight: meat is muscle
771
1023
  // (Agility), the Matrix is mind (Logic, hot-sim +2), the astral is
772
- // will (Willpower + half Magic). The SKILL is per-armament now (see
1024
+ // will (Willpower + half Magic), and a jumped-in drone is GUNNERY --
1025
+ // Logic + piloting through the control rig (its +2 rides
1026
+ // augBonus('piloting')), hot-sim +1 (p.266: matrix bonuses apply to
1027
+ // vehicle actions). The SKILL is per-armament now (see
773
1028
  // activeCombatSkill / skillRating): firearms, blades, or unarmed in
774
1029
  // the meat; hacking is cybercombat; astral strikes ride unarmed (will
775
1030
  // given form). Wounds and sustaining drag everywhere.
1031
+ if (this.plane === 'drone') {
1032
+ const hot = this.simMode === 'hot' ? 1 : 0;
1033
+ return Math.max(0, this.logic + this.skillRating('piloting') + this.augBonus('piloting') + this.qualityBonus('piloting') + hot + this.woundModifier - this.sustainingPenalty);
1034
+ }
776
1035
  if (this.plane === 'matrix') {
777
1036
  const hot = this.simMode === 'hot' ? 2 : 0;
778
- return Math.max(0, this.logic + this.skillRating('hacking') + Math.floor(this.resonance / 2) + hot + this.woundModifier - this.sustainingPenalty);
1037
+ // Attack Upgrade echo (p.258): +1 on the offensive matrix pool.
1038
+ return Math.max(0, this.logic + this.skillRating('hacking') + Math.floor(this.resonance / 2) + hot + this.augBonus('matrix') + this.qualityBonus('hacking') + (this.hasMetamagic('attack-upgrade') ? 1 : 0) + this.woundModifier - this.sustainingPenalty);
779
1039
  }
780
1040
  if (this.plane === 'astral') {
781
1041
  return Math.max(0, this.willpower + Math.floor(this.magic / 2) + this.skillRating('unarmed') + this.woundModifier - this.sustainingPenalty);
782
1042
  }
783
1043
  // An adept's magic lives in their hands: half of it (rounded down)
784
1044
  // rides every meat-plane strike -- killing hands, improved reflexes.
785
- const adeptBonus = this.adept ? Math.floor(this.magic / 2) : 0;
786
- return Math.max(0, this.agility + this.skillRating(this.activeCombatSkill()) + adeptBonus + this.getWeaponProfile().poolBonus + this.woundModifier - this.sustainingPenalty);
1045
+ // Chrome rides here too: muscle toner on every swing, the smartlink
1046
+ // only when the drawn armament is actually a firearm.
1047
+ // Power Point (initiation, adepts): one more point of magic-as-muscle.
1048
+ const adeptBonus = this.adept ? Math.floor(this.magic / 2) + (this.hasMetamagic('power-point') ? 1 : 0) : 0;
1049
+ const smartlink = this.activeCombatSkill() === 'firearms' && this.weaponDrawn ? this.augBonus('firearms') : 0;
1050
+ return Math.max(0, this.agility + this.skillRating(this.activeCombatSkill()) + adeptBonus + this.getWeaponProfile().poolBonus + this.augBonus('attack') + smartlink + this.woundModifier - this.sustainingPenalty);
787
1051
  }
788
1052
  /**
789
1053
  * The subdual pool: bare-hands work regardless of what's in the other
@@ -791,34 +1055,44 @@ export class Player extends AbstractPlayer {
791
1055
  * See subdue.ts.
792
1056
  */
793
1057
  getUnarmedAttackPool() {
794
- const adeptBonus = this.adept ? Math.floor(this.magic / 2) : 0;
795
- return Math.max(0, this.agility + this.skillRating('unarmed') + adeptBonus + this.woundModifier - this.sustainingPenalty);
1058
+ const adeptBonus = this.adept ? Math.floor(this.magic / 2) + (this.hasMetamagic('power-point') ? 1 : 0) : 0;
1059
+ return Math.max(0, this.agility + this.skillRating('unarmed') + adeptBonus + this.augBonus('attack') + this.woundModifier - this.sustainingPenalty);
796
1060
  }
797
1061
  getDefensePool() {
798
1062
  // Reaction plus HALF intuition (rounded down) -- full SR
799
1063
  // Reaction+Intuition turned text-paced exchanges into whiff-fests, so
800
1064
  // gut feel contributes at half weight. Intuition 3 (the default) adds
801
1065
  // +1 over the old reaction-only pool. Off-plane variants swap the
802
- // reflex attribute but keep the same shape.
1066
+ // reflex attribute but keep the same shape. Jumped-in: the rigger's
1067
+ // own reflexes drive the airframe, control rig sharpening handling.
1068
+ if (this.plane === 'drone') {
1069
+ return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + this.augBonus('piloting') + this.qualityBonus('piloting') + this.woundModifier - this.sustainingPenalty);
1070
+ }
803
1071
  if (this.plane === 'matrix') {
804
1072
  const hot = this.simMode === 'hot' ? 1 : 0;
805
- return Math.max(0, this.intuition + Math.floor(this.logic / 2) + Math.floor(this.resonance / 2) + hot + this.woundModifier - this.sustainingPenalty);
1073
+ return Math.max(0, this.intuition + Math.floor(this.logic / 2) + Math.floor(this.resonance / 2) + hot + this.augBonus('matrix') + this.qualityBonus('matrix') + this.woundModifier - this.sustainingPenalty);
806
1074
  }
807
1075
  if (this.plane === 'astral') {
808
1076
  return Math.max(0, this.intuition + Math.floor(this.magic / 2) + this.woundModifier - this.sustainingPenalty);
809
1077
  }
810
1078
  // Combat sense: the adept's magic reads the fight before it happens.
1079
+ // Wired reflexes read it faster still.
811
1080
  const adeptBonus = this.adept ? Math.floor(this.magic / 2) : 0;
812
- return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + adeptBonus + this.woundModifier - this.sustainingPenalty);
1081
+ return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + adeptBonus + this.augBonus('defense') + this.woundModifier - this.sustainingPenalty);
813
1082
  }
814
1083
  getSoakPool() {
815
1084
  // Matrix: firewall -- Logic plus the DECK's quality (your deck is your
816
1085
  // armor); a technomancer's firewall is Willpower plus half resonance
817
1086
  // (their mind guards itself). Astral: pure Willpower; worn armor is
818
- // meat left behind.
1087
+ // meat left behind. Drone: the HULL soaks -- airframe body scaled by
1088
+ // build quality; the rider's armor is streets away with their body.
1089
+ if (this.plane === 'drone') {
1090
+ return Math.max(0, 4 + 2 * (this.riggedDrone?.qualityBonus ?? 0));
1091
+ }
819
1092
  if (this.plane === 'matrix') {
820
1093
  if (this.isTechnomancer()) {
821
- return Math.max(0, this.willpower + Math.floor(this.resonance / 2));
1094
+ // Firewall Upgrade echo (p.258): the living persona hardens.
1095
+ return Math.max(0, this.willpower + Math.floor(this.resonance / 2) + (this.hasMetamagic('firewall-upgrade') ? 1 : 0));
822
1096
  }
823
1097
  const deck = this.activeDeck ?? this.getCyberdeck();
824
1098
  return Math.max(0, this.logic + (deck?.qualityBonus ?? 0));
@@ -826,7 +1100,7 @@ export class Player extends AbstractPlayer {
826
1100
  if (this.plane === 'astral') {
827
1101
  return Math.max(0, this.willpower);
828
1102
  }
829
- return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor);
1103
+ return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor + this.augBonus('soak') + this.qualityBonus('soak'));
830
1104
  }
831
1105
  /**
832
1106
  * Initiative: Reaction + Intuition (straight SR), plus the AR edge from
@@ -838,18 +1112,34 @@ export class Player extends AbstractPlayer {
838
1112
  getInitiativePool() {
839
1113
  // Matrix speed is thought-speed: Logic + Intuition, hot-sim +2 (the
840
1114
  // whole point of running hot). Astral forms move at the speed of
841
- // intention: Intuition doubled.
1115
+ // intention: Intuition doubled. Jumped-in rides VR reflexes (p.266:
1116
+ // cold +2, hot +3 -- the engine's flattened take on the extra dice).
1117
+ //
1118
+ // NO sustaining penalty here (RAW spell pass): initiative is a score,
1119
+ // not a TEST -- holding a spell slows what you DO, never when you get
1120
+ // to do it. (This also lets Increase Reflexes actually work: its +2
1121
+ // used to be eaten whole by its own -2 sustaining burden.) Wounds
1122
+ // still slow the draw -- pain is not a spell.
1123
+ if (this.plane === 'drone') {
1124
+ const vr = this.simMode === 'hot' ? 3 : 2;
1125
+ return Math.max(0, this.reaction + this.intuition + vr + this.woundModifier);
1126
+ }
842
1127
  if (this.plane === 'matrix') {
843
- const hot = this.simMode === 'hot' ? 2 : 0;
844
- return Math.max(0, this.logic + this.intuition + Math.floor(this.resonance / 2) + hot + this.woundModifier - this.sustainingPenalty);
1128
+ // Overclocking echo (p.258): +1 while running hot -- the living
1129
+ // persona accelerated past spec.
1130
+ const hot = this.simMode === 'hot' ? 2 + (this.hasMetamagic('overclocking') ? 1 : 0) : 0;
1131
+ return Math.max(0, this.logic + this.intuition + Math.floor(this.resonance / 2) + hot + this.augBonus('matrix') + this.qualityBonus('matrix') + this.woundModifier);
845
1132
  }
846
1133
  if (this.plane === 'astral') {
847
- return Math.max(0, this.intuition * 2 + this.woundModifier - this.sustainingPenalty);
1134
+ return Math.max(0, this.intuition * 2 + this.woundModifier);
848
1135
  }
849
1136
  const link = this._equipment.head?.commlink;
850
1137
  // AR needs a link that's on you, broadcasting, and not hacked dark.
851
1138
  const arBonus = link && !this.runningSilent && !this.arOffline ? link.qualityBonus : 0;
852
- return Math.max(0, this.reaction + this.intuition + arBonus + this.woundModifier - this.sustainingPenalty);
1139
+ // Increase Reflexes (sustained, cast.ts): the spellwork equivalent of
1140
+ // wired reflexes, for as long as the caster holds it.
1141
+ const spellReflexes = this.sustainedReflexes ? 2 : 0;
1142
+ return Math.max(0, this.reaction + this.intuition + arBonus + this.augBonus('initiative') + spellReflexes + this.woundModifier);
853
1143
  }
854
1144
  /**
855
1145
  * If an edge boost is armed (see commands/edge.ts), consume it: returns
@@ -863,11 +1153,60 @@ export class Player extends AbstractPlayer {
863
1153
  this.edgeBoostArmed = false;
864
1154
  return this.edge;
865
1155
  }
866
- /** New job, fresh luck -- and a fresh seat at the negotiating table. */
867
- refreshEdge() {
868
- this.edgeRemaining = this.edge;
1156
+ // A comfortable home sharpens the luck: Middle+ lifestyles bank a bonus
1157
+ // edge each job (set by Game from the active tier; 0 for NPCs).
1158
+ lifestyleEdgeBonus = 0;
1159
+ // ==================== Needs (SR5e p.56 / p.172) ====================
1160
+ // Edge comes back from "a fulfilling meal and a good night's sleep" --
1161
+ // so eating (use.ts) and sleeping (rest.ts, at a Residence) are tracked
1162
+ // per JOB-CYCLE and cashed in at departure (Game.applyDepartureNeeds).
1163
+ // Ignore a need across multiple cycles and RAW fatigue stacks: 1S, 2S,
1164
+ // 3S... resisted Body + Willpower -- and fatigue stun is LOCKED (won't
1165
+ // heal by rest, stim, food's own recovery, or the doc) until the need
1166
+ // that caused it is finally met.
1167
+ // Sessions start fresh off your own cot: fed, slept, sharp.
1168
+ ateThisCycle = true;
1169
+ sleptThisCycle = true;
1170
+ hungerStreak = 0;
1171
+ sleepStreak = 0;
1172
+ _hungerFatigue = 0;
1173
+ _sleepFatigue = 0;
1174
+ /** Stun boxes no recovery can touch until their need is met. */
1175
+ get lockedFatigue() {
1176
+ return this._hungerFatigue + this._sleepFatigue;
1177
+ }
1178
+ /** A real meal: hunger resets and hunger-fatigue unlocks (heals normally now). */
1179
+ markFed() {
1180
+ this.ateThisCycle = true;
1181
+ this.hungerStreak = 0;
1182
+ this._hungerFatigue = 0;
1183
+ }
1184
+ /** A real sleep (a bed, not a jobsite corner): sleep debt resets and unlocks. */
1185
+ markSlept() {
1186
+ this.sleptThisCycle = true;
1187
+ this.sleepStreak = 0;
1188
+ this._sleepFatigue = 0;
1189
+ }
1190
+ /** Records freshly-taken stun as need-locked fatigue. */
1191
+ addLockedFatigue(kind, boxes) {
1192
+ if (kind === 'hunger')
1193
+ this._hungerFatigue += Math.max(0, boxes);
1194
+ else
1195
+ this._sleepFatigue += Math.max(0, boxes);
1196
+ }
1197
+ /**
1198
+ * New job bookkeeping -- the boost and the once-per-job haggle always
1199
+ * reset, but the edge POOL only refills when the runner left fed and
1200
+ * slept (SR5e p.56: a fulfilling meal and a good night's sleep).
1201
+ */
1202
+ refreshEdge(wellRested = true) {
1203
+ if (wellRested) {
1204
+ this.edgeRemaining = this.edge + this.lifestyleEdgeBonus;
1205
+ }
869
1206
  this.edgeBoostArmed = false;
870
1207
  this.haggledThisJob = false;
1208
+ // A new job is a new room to read: First Impression re-arms (p.74).
1209
+ this._firstImpressionUsed = false;
871
1210
  }
872
1211
  // ======================== Magic (see commands/cast.ts) ========================
873
1212
  // Two roads to casting. GEAR-DRIVEN: what a mundane can cast comes from
@@ -900,11 +1239,25 @@ export class Player extends AbstractPlayer {
900
1239
  setSustainedArmor(bonus) {
901
1240
  this._sustainedArmor = Math.max(0, Math.floor(bonus));
902
1241
  }
1242
+ // The other sustained spells (RAW gear/spell pass): invisibility bends
1243
+ // light around the bearer (+3 on sneak contests, npc.ts), increase
1244
+ // reflexes runs the nerves hot (+2 initiative). Each holds the standard
1245
+ // -2 sustaining burden; cast again to release (cast.ts).
1246
+ sustainedInvisibility = false;
1247
+ sustainedReflexes = false;
903
1248
  get sustainingPenalty() {
904
1249
  // Each sustained effect costs -2 dice across the board: the armor
905
- // spell, and open astral senses (see commands/assense.ts). They stack
906
- // -- holding both is walking around half-blind.
907
- return (this._sustainedArmor > 0 ? 2 : 0) + (this.astralPerceiving ? 2 : 0);
1250
+ // spell, invisibility, hot reflexes, and open astral senses (see
1251
+ // commands/assense.ts). They stack -- holding two is walking around
1252
+ // half-blind. Dumpshock disorientation (p.229) rides the same rail.
1253
+ // QUICKENING (metamagic, p.326 adapted): the armor spell anchors into
1254
+ // the world and sustains itself -- its -2 simply never bills.
1255
+ const armorCost = this._sustainedArmor > 0 && !this.hasMetamagic('quickening') ? 2 : 0;
1256
+ return armorCost
1257
+ + (this.sustainedInvisibility ? 2 : 0)
1258
+ + (this.sustainedReflexes ? 2 : 0)
1259
+ + (this.astralPerceiving ? 2 : 0)
1260
+ + (this.dumpshocked ? 2 : 0);
908
1261
  }
909
1262
  // Astral perception (see commands/assense.ts): senses held OPEN as a
910
1263
  // sustained state -- passive aura reads, at -2 dice to everything else.
@@ -915,9 +1268,40 @@ export class Player extends AbstractPlayer {
915
1268
  // Sneaking-vs-Perception rolls per witness (NPC.see). Broken by any
916
1269
  // loud act -- attacking, casting, speaking, brandishing, calling.
917
1270
  sneaking = false;
1271
+ // ==================== Jumped-in rigging (commands/jump.ts) ====================
1272
+ // The drone currently being ridden while plane === 'drone': the item
1273
+ // whose hull track (Item.droneDamage) eats what would hit the rider,
1274
+ // whose quality feeds the drone pools, and whose destruction is a
1275
+ // DUMPSHOCK (see planes.ts leaveDrone).
1276
+ riggedDrone;
1277
+ /** Whether the ridden drone carries a weapon mount (name/description duck-typing). */
1278
+ get droneArmed() {
1279
+ const d = this.riggedDrone;
1280
+ if (!d)
1281
+ return false;
1282
+ return /armed|combat|gun|roto|assault|sentry|weapon/i.test(`${d.name} ${d.description ?? ''}`);
1283
+ }
1284
+ // Dumpshock disorientation (SR5e p.229): -2 on everything for
1285
+ // (10 - Willpower) minutes after a forced dump, wall-clock -- the
1286
+ // penalty rides sustainingPenalty so every pool already feels it.
1287
+ dumpshockedUntil = 0;
1288
+ get dumpshocked() {
1289
+ return Date.now() < this.dumpshockedUntil;
1290
+ }
918
1291
  // One haggle per job (see commands/haggle.ts); reset with the edge pool
919
1292
  // on each new job (refreshEdge).
920
1293
  haggledThisJob = false;
1294
+ // A drink in you (see use.ts): +1 die on the next social roll --
1295
+ // persuade, haggle, bluff -- then it's gone. A second drink before it's
1296
+ // spent doesn't stack, it just costs a stun box ("one too many").
1297
+ liquidCourage = false;
1298
+ /** Spends the drink: returns its bonus die for the roll happening NOW. */
1299
+ consumeLiquidCourage() {
1300
+ if (!this.liquidCourage)
1301
+ return 0;
1302
+ this.liquidCourage = false;
1303
+ return 1;
1304
+ }
921
1305
  // Where this actor just came from and the direction BACK -- the HUD's
922
1306
  // breadcrumb (see game.ts updateStatus / go.ts). Big scenes (the hub's
923
1307
  // 18 rooms) made "which way did I come in" a real question. Cleared on