@maka/maka-cli 5.3.16 → 5.4.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 (220) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/create.js +1 -1
  3. package/bundle/typescript/src/commands/create.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +43 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +131 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.d.ts +14 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +79 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js.map +1 -0
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.d.ts +42 -0
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +224 -0
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -0
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +28 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +101 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +19 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +149 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.d.ts +58 -0
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +378 -0
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.d.ts +11 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js +8 -3
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/command.d.ts +6 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js +4 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js.map +1 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.d.ts +15 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +60 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -0
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.d.ts +13 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js +31 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js.map +1 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.d.ts +6 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +27 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js.map +1 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.d.ts +4 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +40 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.d.ts +13 -0
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +43 -0
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -0
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +7 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +11 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +115 -4
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +25 -0
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +176 -0
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -0
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.d.ts +12 -0
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +39 -0
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -0
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.d.ts +17 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +64 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js.map +1 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/kill.d.ts +10 -3
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/kill.js +11 -33
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/kill.js.map +1 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +35 -4
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/map.d.ts +12 -0
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +46 -0
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -0
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.d.ts +12 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +55 -0
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +9 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +18 -0
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/project.d.ts +19 -0
  72. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +54 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -0
  74. package/bundle/typescript/src/commands/game/sideQuest/commands/read.js +6 -5
  75. package/bundle/typescript/src/commands/game/sideQuest/commands/read.js.map +1 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.d.ts +12 -0
  77. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js +52 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js.map +1 -0
  79. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.d.ts +14 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +40 -0
  81. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js.map +1 -0
  82. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.d.ts +15 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +59 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -0
  85. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +23 -2
  86. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +20 -3
  88. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.d.ts +12 -0
  90. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +49 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.d.ts +13 -0
  93. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +50 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js +32 -11
  96. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js.map +1 -1
  97. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.d.ts +14 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +65 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.d.ts +12 -0
  101. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +41 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -0
  103. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.d.ts +21 -0
  104. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +43 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js.map +1 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.d.ts +20 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +45 -0
  108. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js.map +1 -0
  109. package/bundle/typescript/src/commands/game/sideQuest/commands/take.d.ts +8 -0
  110. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +80 -1
  111. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  112. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +1 -0
  113. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  114. package/bundle/typescript/src/commands/game/sideQuest/commands/use.d.ts +24 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +101 -1
  116. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  117. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js +2 -1
  118. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js.map +1 -1
  119. package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js +7 -2
  120. package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js.map +1 -1
  121. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js +2 -1
  122. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js.map +1 -1
  123. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +2 -0
  124. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
  125. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.d.ts +8 -1
  126. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +138 -21
  127. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  128. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +119 -0
  129. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +816 -0
  130. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -0
  131. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +70 -9
  132. package/bundle/typescript/src/commands/game/sideQuest/game.js +403 -164
  133. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  134. package/bundle/typescript/src/commands/game/sideQuest/minigames/hack-minigame.d.ts +16 -0
  135. package/bundle/typescript/src/commands/game/sideQuest/minigames/hack-minigame.js +101 -0
  136. package/bundle/typescript/src/commands/game/sideQuest/minigames/hack-minigame.js.map +1 -0
  137. package/bundle/typescript/src/commands/game/sideQuest/minigames/lockpick-minigame.d.ts +15 -0
  138. package/bundle/typescript/src/commands/game/sideQuest/minigames/lockpick-minigame.js +95 -0
  139. package/bundle/typescript/src/commands/game/sideQuest/minigames/lockpick-minigame.js.map +1 -0
  140. package/bundle/typescript/src/commands/game/sideQuest/models/door.d.ts +10 -0
  141. package/bundle/typescript/src/commands/game/sideQuest/models/door.js +15 -2
  142. package/bundle/typescript/src/commands/game/sideQuest/models/door.js.map +1 -1
  143. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.d.ts +10 -0
  144. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js +54 -4
  145. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js.map +1 -1
  146. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +41 -0
  147. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +106 -3
  148. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  149. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +25 -0
  150. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +220 -47
  151. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  152. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +213 -0
  153. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +525 -9
  154. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  155. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +75 -1
  156. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +174 -6
  157. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  158. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +20 -0
  159. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +39 -18
  160. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  161. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +69 -0
  162. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +175 -2
  163. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  164. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +80 -1
  165. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +7 -2
  166. package/bundle/typescript/src/commands/game/sideQuest/types/player-equipment.d.ts +1 -0
  167. package/bundle/typescript/src/commands/game/sideQuest/types/seed/exit-seed.d.ts +1 -0
  168. package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
  169. package/bundle/typescript/src/commands/game/sideQuest/types/seed/npc-seed.d.ts +1 -0
  170. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +34 -0
  171. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +15 -1
  172. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +3 -0
  173. package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +5 -0
  174. package/bundle/typescript/src/commands/game/sideQuest/types/seed/win-condition-seed.d.ts +15 -0
  175. package/bundle/typescript/src/commands/game/sideQuest/types/seed/win-condition-seed.js +2 -0
  176. package/bundle/typescript/src/commands/game/sideQuest/types/seed/win-condition-seed.js.map +1 -0
  177. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.d.ts +1 -0
  178. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.js +5 -0
  179. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.js.map +1 -1
  180. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.d.ts +1 -0
  181. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js +1 -0
  182. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js.map +1 -1
  183. package/bundle/typescript/src/commands/game/sideQuest/ui.d.ts +3 -0
  184. package/bundle/typescript/src/commands/game/sideQuest/ui.js +95 -5
  185. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  186. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.d.ts +60 -0
  187. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +138 -0
  188. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -0
  189. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +143 -0
  190. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +328 -0
  191. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -0
  192. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.d.ts +2 -0
  193. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +12 -0
  194. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js.map +1 -0
  195. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +22 -0
  196. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +34 -0
  197. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -0
  198. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.d.ts +36 -0
  199. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js +92 -0
  200. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js.map +1 -0
  201. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +44 -0
  202. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +193 -0
  203. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -0
  204. package/bundle/typescript/src/commands/game/sideQuest/utilities/pursuit.d.ts +16 -0
  205. package/bundle/typescript/src/commands/game/sideQuest/utilities/pursuit.js +36 -0
  206. package/bundle/typescript/src/commands/game/sideQuest/utilities/pursuit.js.map +1 -0
  207. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +54 -7
  208. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  209. package/bundle/typescript/src/commands/set/global.sub.cmd.js +59 -5
  210. package/bundle/typescript/src/commands/set/global.sub.cmd.js.map +1 -1
  211. package/bundle/typescript/src/tools/ai/ai.class.d.ts +35 -0
  212. package/bundle/typescript/src/tools/ai/ai.class.js +112 -0
  213. package/bundle/typescript/src/tools/ai/ai.class.js.map +1 -0
  214. package/bundle/typescript/src/tools/fsi/fsi.interfaces.d.ts +1 -0
  215. package/bundle/typescript/src/tools/fsi/fsi.interfaces.js.map +1 -1
  216. package/bundle/typescript/src/tools/https/https.class.d.ts +2 -1
  217. package/bundle/typescript/src/tools/https/https.class.js +43 -0
  218. package/bundle/typescript/src/tools/https/https.class.js.map +1 -1
  219. package/bundle/typescript/src/tools/https/https.interface.d.ts +8 -0
  220. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { Category } from '../types/shared/item-enum.js';
2
2
  import { Logger } from '../utilities/logger.js';
3
3
  import { Inventory } from './inventory.js';
4
4
  import { AbstractPlayer } from '../types/shared/abstracts.js';
5
+ export const STANCE_CYCLE = ['manual', 'guarded', 'aggressive'];
5
6
  export class Player extends AbstractPlayer {
6
7
  _playerName;
7
8
  _currentLocation;
@@ -11,6 +12,167 @@ export class Player extends AbstractPlayer {
11
12
  _lifestyleChoices = [];
12
13
  logger;
13
14
  _currency = 0; // for seeding/debug only
15
+ // Whoever this actor most recently placed/received a commlink call with.
16
+ // No explicit "hang up" -- placing a new call just replaces it. Lets a
17
+ // reply via the ordinary "say" command reach a call partner in a
18
+ // different room, the same way a real call would, without the NPC (or
19
+ // player) needing to learn a separate reply verb. See say.ts and call.ts.
20
+ activeCallPartner;
21
+ // Running silent (see commands/silent.ts): the commlink stays equipped
22
+ // but stops broadcasting -- no calls out, and nobody can raise this
23
+ // actor (call.ts checks both ends). Toggling it drops any active call.
24
+ runningSilent = false;
25
+ // Whoever this actor most recently traded combat exchanges with, and
26
+ // when -- set for BOTH sides by CombatExchange.beginExchange(). Walking
27
+ // out on a live, recent opponent invites a parting shot (see go.ts);
28
+ // firing one (or a death) clears it.
29
+ combatOpponent;
30
+ lastExchangeAt = 0;
31
+ // True while this actor is a combatant in an exchange that's still
32
+ // resolving. Serializes combat per actor: a second attack involving a
33
+ // busy combatant is refused instead of interleaving -- found via a real
34
+ // session where an NPC's overlapping AI loops fired two exchanges at
35
+ // once and the target died TWICE (two kill events, two narrations).
36
+ inExchange = false;
37
+ // Fight-or-flight autopilot (see utilities/auto-fight.ts): the FIGHT
38
+ // half runs automatically at the set temperature, the flight stays
39
+ // manual -- always. Tab cycles it in the UI; "stance" sets it by name.
40
+ // manual -- today's behavior, you type every swing
41
+ // guarded -- auto-fights, but finishes non-lethally (switches to
42
+ // subdue when the target is nearly down) and stands down
43
+ // if they surrender
44
+ // aggressive -- fights until they're dead; surrender means nothing
45
+ autoStance = 'manual';
46
+ // Set by the "surrender" command: this actor has thrown their hands up.
47
+ // Cleared the moment they initiate violence again (see
48
+ // CombatExchange.beginExchange). Guarded auto-fight honors it;
49
+ // aggressive doesn't have to.
50
+ surrendered = false;
51
+ // ==================== Planes (see utilities/planes.ts) ====================
52
+ // Which plane this actor currently exists on. Ice NPCs live on 'matrix',
53
+ // spirits on 'astral', natively and permanently; a player shifts planes
54
+ // via jack in / project and back.
55
+ plane = 'meat';
56
+ // While off the meat plane, the empty body slumps HERE -- visible,
57
+ // attackable (see planes.ts resolveBodyAttack), and where you snap back
58
+ // to. undefined while on the meat plane, or for native off-plane actors
59
+ // (ice and spirits have no body to leave behind).
60
+ bodyRoom;
61
+ // Matrix only: cold or hot sim, chosen at jack-in. undefined off-Matrix
62
+ // and for native ice.
63
+ simMode;
64
+ // Astral only: room-moves made while projecting. The longer you're out,
65
+ // the harder the snap-back -- drained as stun on return (planes.ts).
66
+ astralTicks = 0;
67
+ // The DECK you're jacked in on (set by planes.ts enterMatrix, cleared on
68
+ // leaving). Matrix damage is HARDWARE damage: it lands on this item's
69
+ // device condition monitor (see Item's cyberdeck section), persists
70
+ // across jack-outs, and a full track BRICKS the deck -- forced dump,
71
+ // and no re-entry on that deck until it's repaired.
72
+ activeDeck;
73
+ /** The best working cyberdeck carried (bricked ones don't count). */
74
+ getCyberdeck() {
75
+ return this._inventory.getAllItems()
76
+ .filter(i => i.isCyberdeck() && !i.isBricked)
77
+ .sort((a, b) => b.qualityBonus - a.qualityBonus)[0];
78
+ }
79
+ /** Any deck at all, bricked included -- for status displays and repair. */
80
+ getAnyCyberdeck() {
81
+ return this.activeDeck ?? this._inventory.getAllItems()
82
+ .filter(i => i.isCyberdeck())
83
+ .sort((a, b) => b.qualityBonus - a.qualityBonus)[0];
84
+ }
85
+ // ==================== PAN (see commands/pan.ts, hack.ts) ====================
86
+ // Hacked dark: an enemy cracked your PAN and cut the AR overlay. Costs
87
+ // the commlink initiative bonus and drops calls, until "reboot". Distinct
88
+ // from runningSilent (chosen dark) -- this one was done TO you.
89
+ arOffline = false;
90
+ /**
91
+ * The PAN's master device: your slaved gear defends behind its
92
+ * firewall (SR slaving, simplified). Best working deck beats the
93
+ * equipped commlink; no devices = no PAN (and nothing to hack).
94
+ */
95
+ getPanMaster() {
96
+ const deck = this.getCyberdeck();
97
+ const link = this._equipment.head?.commlink ?? undefined;
98
+ if (deck && link)
99
+ return deck.qualityBonus >= link.qualityBonus ? deck : link;
100
+ return deck ?? link;
101
+ }
102
+ /**
103
+ * Defense against a PAN intrusion: Logic + the master's firewall
104
+ * (+2 while running silent -- a hidden PAN must be FOUND first).
105
+ */
106
+ getPanDefensePool() {
107
+ const master = this.getPanMaster();
108
+ const silentBonus = this.runningSilent ? 2 : 0;
109
+ return Math.max(0, this.logic + (master?.qualityBonus ?? 0) + silentBonus);
110
+ }
111
+ get matrixTaken() {
112
+ return this.getAnyCyberdeck()?.deckDamage ?? 0;
113
+ }
114
+ get maxMatrixBoxes() {
115
+ return this.getAnyCyberdeck()?.maxDeckBoxes ?? 0;
116
+ }
117
+ /** Routes Matrix combat damage into the jacked-in deck's hardware track. */
118
+ takeMatrixDamage(boxes) {
119
+ const deck = this.getAnyCyberdeck();
120
+ return deck ? deck.takeDeckDamage(boxes) : 0;
121
+ }
122
+ get matrixCrashed() {
123
+ return this.getAnyCyberdeck()?.isBricked ?? false;
124
+ }
125
+ /** The deck track, same rendering as conditionSummary(). */
126
+ matrixSummary() {
127
+ return this.getAnyCyberdeck()?.deckSummary() ?? '(no deck)';
128
+ }
129
+ /**
130
+ * Cross-plane perception, the single rule everything filters through:
131
+ * same plane sees same plane; the astral perceives the meat world
132
+ * (shifted, not gone -- auras and movement, dimly); meat and Matrix see
133
+ * nothing beyond their own plane. Used by see/hear guards, targeting,
134
+ * and room listings.
135
+ */
136
+ canPerceive(other) {
137
+ if (this.plane === other.plane)
138
+ return true;
139
+ return this.plane === 'astral' && other.plane === 'meat';
140
+ }
141
+ // True while an AI-driven action is parked waiting for an in-flight
142
+ // exchange to settle (CombatExchange.waitForLull). One queued action per
143
+ // actor: a second colliding attempt is refused outright instead of
144
+ // stacking a burst of delayed attacks behind the same lull.
145
+ waitingForLull = false;
146
+ // Combat attributes (see ICombatAttributes). Defaults make an unseeded
147
+ // actor a competent-but-mortal street-level runner; seeds can override
148
+ // per-NPC to make a hardened enforcer or a soft civilian.
149
+ body;
150
+ agility;
151
+ reaction;
152
+ combatSkill;
153
+ magic;
154
+ strength;
155
+ willpower;
156
+ logic;
157
+ intuition;
158
+ charisma;
159
+ edge;
160
+ resonance;
161
+ adept;
162
+ // Edge is a per-job pool: spend a point (see commands/edge.ts) to arm a
163
+ // boost that supercharges your next strike. Refreshed to `edge` on every
164
+ // new job (see Game.continueToNextScene).
165
+ edgeRemaining = 0;
166
+ edgeBoostArmed = false;
167
+ // Physical condition monitor, straight from SR5: 8 + (Body / 2, rounded
168
+ // up) boxes. Damage fills boxes; a full track is death.
169
+ _damageTaken = 0;
170
+ // Stun condition monitor -- the second track, filled by spell drain (see
171
+ // cast.ts). A full stun track is a knockout, not death, but overflow
172
+ // past it spills into the physical track. Sized off Reaction as the
173
+ // stand-in for SR's Willpower (the closest thing to a mental attribute
174
+ // here without adding one): 8 + (Reaction / 2, rounded up).
175
+ _stunTaken = 0;
14
176
  constructor(config) {
15
177
  super();
16
178
  this._playerName = config.playerName;
@@ -18,6 +180,20 @@ export class Player extends AbstractPlayer {
18
180
  this._inventory = new Inventory();
19
181
  this.logger = Logger.getInstance();
20
182
  this._lifestyleChoices = config.lifestyleChoices ?? [];
183
+ this.body = config.combat?.body ?? 4;
184
+ this.agility = config.combat?.agility ?? 4;
185
+ this.reaction = config.combat?.reaction ?? 4;
186
+ this.combatSkill = config.combat?.combatSkill ?? 3;
187
+ this.magic = config.combat?.magic ?? 0;
188
+ this.strength = config.combat?.strength ?? 3;
189
+ this.willpower = config.combat?.willpower ?? 3;
190
+ this.logic = config.combat?.logic ?? 3;
191
+ this.intuition = config.combat?.intuition ?? 3;
192
+ this.charisma = config.combat?.charisma ?? 3;
193
+ this.edge = config.combat?.edge ?? 2;
194
+ this.edgeRemaining = this.edge;
195
+ this.resonance = config.combat?.resonance ?? 0;
196
+ this.adept = config.combat?.adept ?? false;
21
197
  this._equipment = this.initializeEquipment();
22
198
  }
23
199
  initializeEquipment() {
@@ -31,6 +207,7 @@ export class Player extends AbstractPlayer {
31
207
  commlink: null
32
208
  },
33
209
  torso: {
210
+ clothing: null,
34
211
  armorVest: null,
35
212
  armorCoat: null,
36
213
  backpack: null,
@@ -97,7 +274,9 @@ export class Player extends AbstractPlayer {
97
274
  this._equipment = newEquipment;
98
275
  }
99
276
  get maxCarryingWeight() {
100
- return this._maxCarryingWeight;
277
+ // Strength-derived: 20 + 10/point. The old flat default (50kg) is
278
+ // exactly what a strength-3 runner gets, so pre-sheet balance holds.
279
+ return 20 + this.strength * 10;
101
280
  }
102
281
  set maxCarryingWeight(weight) {
103
282
  if (weight > 0) {
@@ -109,10 +288,19 @@ export class Player extends AbstractPlayer {
109
288
  }
110
289
  get currentWeight() {
111
290
  let totalWeight = this._inventory.getWeight();
112
- for (const slot in this._equipment) {
113
- const equippedItem = this._equipment[slot];
114
- if (equippedItem) {
115
- totalWeight += equippedItem.weight;
291
+ // Equipment is nested one level deeper than inventory: groups (head,
292
+ // torso...) containing slots. This used to add `group.weight` --
293
+ // undefined -- which turned the total into NaN, so the carrying cap
294
+ // NEVER triggered (NaN compares false against everything). Found when
295
+ // a "take all" verification watched a 100kg engine block walk into a
296
+ // 50kg pack.
297
+ for (const group of Object.values(this._equipment)) {
298
+ if (!group)
299
+ continue;
300
+ for (const item of Object.values(group)) {
301
+ if (item) {
302
+ totalWeight += item.weight;
303
+ }
116
304
  }
117
305
  }
118
306
  return totalWeight;
@@ -149,12 +337,18 @@ export class Player extends AbstractPlayer {
149
337
  if (!groupRef) {
150
338
  return `You have no ${group} equipment group initialized.`;
151
339
  }
152
- if (groupRef[slot]) {
153
- return `You already have something equipped in your ${group}.${slot} slot.`;
340
+ const previousItem = groupRef[slot];
341
+ let swapNotice = '';
342
+ if (previousItem) {
343
+ if (previousItem.name === item.name) {
344
+ return `You already have ${item.name} equipped in your ${group}.${slot} slot.`;
345
+ }
346
+ this.unequipBySlot(group, slot);
347
+ swapNotice = ` (unequipped ${previousItem.name})`;
154
348
  }
155
349
  groupRef[slot] = item;
156
350
  this.removeInventory(item);
157
- return `You equipped ${item.name} in your ${group}.${slot} slot.`;
351
+ return `You equipped ${item.name} in your ${group}.${slot} slot.${swapNotice}`;
158
352
  }
159
353
  unequip(input) {
160
354
  const slotInfo = this.determineEquipmentSlot(input);
@@ -197,8 +391,14 @@ export class Player extends AbstractPlayer {
197
391
  case Category.Commlink:
198
392
  return { group: 'head', slot: 'commlink' };
199
393
  // TORSO
394
+ case Category.Clothing:
395
+ case 'shirt':
396
+ case 'hoodie':
397
+ case 'top':
398
+ return { group: 'torso', slot: 'clothing' };
200
399
  case Category.Armor:
201
400
  case Category.BodyArmor:
401
+ case Category.ArmorClothing:
202
402
  case 'armor vest':
203
403
  return { group: 'torso', slot: 'armorVest' };
204
404
  case 'armor coat':
@@ -295,11 +495,15 @@ export class Player extends AbstractPlayer {
295
495
  return `You unequipped ${itemToUnequip.name} from your ${group}.${slot} slot.`;
296
496
  }
297
497
  hear(actor, message) {
498
+ // Cross-plane silence: a Matrix persona's chatter never reaches meat
499
+ // ears and vice versa; the astral hears the meat world it overlays.
500
+ if (!this.canPerceive(actor))
501
+ return;
298
502
  this.logger.write(`${this.name} heard ${actor.name}`);
299
503
  this.logger.logWithColor(`{white-fg}${actor.name}:{/white-fg} ${message}`, (actor === this) ? 'yellow' : 'green');
300
504
  }
301
505
  see(actor, verb, details) {
302
- if (actor !== this) {
506
+ if (actor !== this && this.canPerceive(actor)) {
303
507
  this.logger.write(`You see ${actor.name} ${verb} ${details}`);
304
508
  this.logger.logWithColor(`${actor.name} ${verb} ${details}`, 'cyan');
305
509
  }
@@ -323,6 +527,318 @@ export class Player extends AbstractPlayer {
323
527
  this.logger.write(`${this.name} has left ${this._currentLocation.name}`);
324
528
  }
325
529
  }
530
+ // ======================== Combat (see commands/attack.ts) ========================
531
+ static ARMOR_CATEGORIES = new Set([
532
+ Category.Armor, Category.BodyArmor, Category.ArmorClothing, Category.Shield,
533
+ Category.Helmet, Category.ArmArmor, Category.LegArmor, Category.Boots, Category.Gloves,
534
+ ]);
535
+ get maxConditionBoxes() {
536
+ return 8 + Math.ceil(this.body / 2);
537
+ }
538
+ get damageTaken() {
539
+ return this._damageTaken;
540
+ }
541
+ get maxStunBoxes() {
542
+ // Willpower-based, as SR intends (Reaction stood in before Willpower
543
+ // existed on the sheet). Default willpower 3 keeps the same 10 boxes
544
+ // the old reaction-4 default produced.
545
+ return 8 + Math.ceil(this.willpower / 2);
546
+ }
547
+ get stunTaken() {
548
+ return this._stunTaken;
549
+ }
550
+ /**
551
+ * SR5 wound modifier: -1 die on every pool per 3 full boxes of damage,
552
+ * and the two tracks STACK -- being shot and drained both make you worse
553
+ * at everything, including not getting shot.
554
+ */
555
+ get woundModifier() {
556
+ return -(Math.floor(this._damageTaken / 3) + Math.floor(this._stunTaken / 3));
557
+ }
558
+ /** Dead: the physical track is full. */
559
+ isDown() {
560
+ return this._damageTaken >= this.maxConditionBoxes;
561
+ }
562
+ /** Out cold: the stun track is full (but not necessarily dead). */
563
+ isUnconscious() {
564
+ return this._stunTaken >= this.maxStunBoxes;
565
+ }
566
+ /** Out of the fight either way -- the gate every action should check. */
567
+ isIncapacitated() {
568
+ return this.isDown() || this.isUnconscious();
569
+ }
570
+ /** Applies damage boxes; returns the number actually taken. */
571
+ takeDamage(boxes) {
572
+ const taken = Math.max(0, Math.floor(boxes));
573
+ this._damageTaken = Math.min(this.maxConditionBoxes, this._damageTaken + taken);
574
+ return taken;
575
+ }
576
+ /**
577
+ * Clears damage boxes; returns how many were actually healed (can be
578
+ * less than asked when the track is nearly clean). Healing also restores
579
+ * wound-modifier dice, since that's derived from damageTaken.
580
+ */
581
+ healDamage(boxes) {
582
+ const before = this._damageTaken;
583
+ this._damageTaken = Math.max(0, before - Math.max(0, Math.floor(boxes)));
584
+ return before - this._damageTaken;
585
+ }
586
+ /**
587
+ * Applies stun boxes; anything past a full stun track OVERFLOWS into the
588
+ * physical track (SR's rule -- enough drain doesn't just knock you out,
589
+ * it starts killing you). Returns what landed where.
590
+ */
591
+ takeStun(boxes) {
592
+ const asked = Math.max(0, Math.floor(boxes));
593
+ const space = this.maxStunBoxes - this._stunTaken;
594
+ const stun = Math.min(asked, space);
595
+ this._stunTaken += stun;
596
+ const overflow = asked - stun;
597
+ if (overflow > 0) {
598
+ this.takeDamage(overflow);
599
+ }
600
+ return { stun, overflow };
601
+ }
602
+ /** e.g. "■■■□□□□□□□ (3/10)" */
603
+ conditionSummary() {
604
+ const max = this.maxConditionBoxes;
605
+ const filled = Math.min(max, this._damageTaken);
606
+ return `${'■'.repeat(filled)}${'□'.repeat(max - filled)} (${filled}/${max})`;
607
+ }
608
+ /** Clears stun boxes; returns how many were actually recovered. */
609
+ healStun(boxes) {
610
+ const before = this._stunTaken;
611
+ this._stunTaken = Math.max(0, before - Math.max(0, Math.floor(boxes)));
612
+ return before - this._stunTaken;
613
+ }
614
+ /** The stun track, same rendering as conditionSummary(). */
615
+ stunSummary() {
616
+ const max = this.maxStunBoxes;
617
+ const filled = Math.min(max, this._stunTaken);
618
+ return `${'■'.repeat(filled)}${'□'.repeat(max - filled)} (${filled}/${max})`;
619
+ }
620
+ /**
621
+ * The weapon actually in hand (right hand first, then left), or bare
622
+ * fists. Damage value and the attack-pool bonus both come off the item's
623
+ * quality rating -- gear is the whole progression system in this game,
624
+ * so it has to be what combat power flows from.
625
+ */
626
+ getWeaponProfile() {
627
+ // Off the meat plane your gun is just a memory: Matrix combat runs on
628
+ // attack programs (Logic-scaled), astral combat on raw will given
629
+ // form (Magic-scaled). Hot-sim's deeper immersion sharpens the
630
+ // programs' bite.
631
+ if (this.plane === 'matrix') {
632
+ const hot = this.simMode === 'hot' ? 1 : 0;
633
+ // The deck IS the arsenal: its quality feeds program damage the way
634
+ // a better gun feeds DV. A technomancer's living persona weaves
635
+ // resonance instead of running programs.
636
+ if (this.isTechnomancer()) {
637
+ return { name: 'resonance surge', damage: 2 + Math.floor(this.logic / 2) + Math.floor(this.resonance / 2) + hot, poolBonus: 0 };
638
+ }
639
+ const deck = this.activeDeck ?? this.getCyberdeck();
640
+ return { name: 'attack programs', damage: 2 + Math.floor(this.logic / 2) + (deck?.qualityBonus ?? 0) + hot, poolBonus: 0 };
641
+ }
642
+ if (this.plane === 'astral') {
643
+ return { name: 'astral strike', damage: 2 + Math.floor(Math.max(1, this.magic) / 2), poolBonus: 0 };
644
+ }
645
+ // Muscle drives melee: unarmed 1 + STR/2, melee weapons 2 + STR/2 +
646
+ // quality (a strength-3 actor lands exactly the old flat numbers).
647
+ // Firearms don't care how strong you are.
648
+ const strengthBase = Math.floor(this.strength / 2);
649
+ const weapon = this._equipment.rightHand?.weapon ?? this._equipment.leftHand?.weapon;
650
+ if (!weapon) {
651
+ return { name: 'bare fists', damage: 1 + strengthBase, poolBonus: 0 };
652
+ }
653
+ const bonus = weapon.qualityBonus;
654
+ const base = weapon.category === Category.MeleeWeapon ? 2 + strengthBase : 4;
655
+ return { name: weapon.name, damage: base + bonus, poolBonus: bonus };
656
+ }
657
+ /** Total soak dice from every equipped armor-category item. */
658
+ getArmorValue() {
659
+ let total = 0;
660
+ for (const group of Object.values(this._equipment)) {
661
+ if (!group)
662
+ continue;
663
+ for (const item of Object.values(group)) {
664
+ if (item && Player.ARMOR_CATEGORIES.has(item.category)) {
665
+ total += 1 + item.qualityBonus;
666
+ }
667
+ }
668
+ }
669
+ return total;
670
+ }
671
+ getAttackPool() {
672
+ // Plane decides which attribute drives the fight: meat is muscle
673
+ // (Agility), the Matrix is mind (Logic, hot-sim +2), the astral is
674
+ // will (Willpower + half Magic). Combat skill is trained instinct and
675
+ // carries everywhere; wounds and sustaining drag everywhere too.
676
+ if (this.plane === 'matrix') {
677
+ const hot = this.simMode === 'hot' ? 2 : 0;
678
+ return Math.max(0, this.logic + this.combatSkill + Math.floor(this.resonance / 2) + hot + this.woundModifier - this.sustainingPenalty);
679
+ }
680
+ if (this.plane === 'astral') {
681
+ return Math.max(0, this.willpower + Math.floor(this.magic / 2) + this.combatSkill + this.woundModifier - this.sustainingPenalty);
682
+ }
683
+ // An adept's magic lives in their hands: half of it (rounded down)
684
+ // rides every meat-plane strike -- killing hands, improved reflexes.
685
+ const adeptBonus = this.adept ? Math.floor(this.magic / 2) : 0;
686
+ return Math.max(0, this.agility + this.combatSkill + adeptBonus + this.getWeaponProfile().poolBonus + this.woundModifier - this.sustainingPenalty);
687
+ }
688
+ getDefensePool() {
689
+ // Reaction plus HALF intuition (rounded down) -- full SR
690
+ // Reaction+Intuition turned text-paced exchanges into whiff-fests, so
691
+ // gut feel contributes at half weight. Intuition 3 (the default) adds
692
+ // +1 over the old reaction-only pool. Off-plane variants swap the
693
+ // reflex attribute but keep the same shape.
694
+ if (this.plane === 'matrix') {
695
+ const hot = this.simMode === 'hot' ? 1 : 0;
696
+ return Math.max(0, this.intuition + Math.floor(this.logic / 2) + Math.floor(this.resonance / 2) + hot + this.woundModifier - this.sustainingPenalty);
697
+ }
698
+ if (this.plane === 'astral') {
699
+ return Math.max(0, this.intuition + Math.floor(this.magic / 2) + this.woundModifier - this.sustainingPenalty);
700
+ }
701
+ // Combat sense: the adept's magic reads the fight before it happens.
702
+ const adeptBonus = this.adept ? Math.floor(this.magic / 2) : 0;
703
+ return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + adeptBonus + this.woundModifier - this.sustainingPenalty);
704
+ }
705
+ getSoakPool() {
706
+ // Matrix: firewall -- Logic plus the DECK's quality (your deck is your
707
+ // armor); a technomancer's firewall is Willpower plus half resonance
708
+ // (their mind guards itself). Astral: pure Willpower; worn armor is
709
+ // meat left behind.
710
+ if (this.plane === 'matrix') {
711
+ if (this.isTechnomancer()) {
712
+ return Math.max(0, this.willpower + Math.floor(this.resonance / 2));
713
+ }
714
+ const deck = this.activeDeck ?? this.getCyberdeck();
715
+ return Math.max(0, this.logic + (deck?.qualityBonus ?? 0));
716
+ }
717
+ if (this.plane === 'astral') {
718
+ return Math.max(0, this.willpower);
719
+ }
720
+ return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor);
721
+ }
722
+ /**
723
+ * Initiative: Reaction + Intuition (straight SR), plus the AR edge from
724
+ * an equipped, ONLINE commlink -- the tactical overlay feeding you
725
+ * angles. Running silent kills the overlay along with the broadcast, so
726
+ * going dark costs you speed on the draw: the silent tradeoff grows
727
+ * teeth. Wounds and sustained spells slow you like everything else.
728
+ */
729
+ getInitiativePool() {
730
+ // Matrix speed is thought-speed: Logic + Intuition, hot-sim +2 (the
731
+ // whole point of running hot). Astral forms move at the speed of
732
+ // intention: Intuition doubled.
733
+ if (this.plane === 'matrix') {
734
+ const hot = this.simMode === 'hot' ? 2 : 0;
735
+ return Math.max(0, this.logic + this.intuition + Math.floor(this.resonance / 2) + hot + this.woundModifier - this.sustainingPenalty);
736
+ }
737
+ if (this.plane === 'astral') {
738
+ return Math.max(0, this.intuition * 2 + this.woundModifier - this.sustainingPenalty);
739
+ }
740
+ const link = this._equipment.head?.commlink;
741
+ // AR needs a link that's on you, broadcasting, and not hacked dark.
742
+ const arBonus = link && !this.runningSilent && !this.arOffline ? link.qualityBonus : 0;
743
+ return Math.max(0, this.reaction + this.intuition + arBonus + this.woundModifier - this.sustainingPenalty);
744
+ }
745
+ /**
746
+ * If an edge boost is armed (see commands/edge.ts), consume it: returns
747
+ * the bonus dice for the strike being rolled right now and disarms.
748
+ * Called by the strike-profile builders, so the boost lands on the next
749
+ * attack/subdue/manabolt this actor initiates.
750
+ */
751
+ consumeEdgeBoost() {
752
+ if (!this.edgeBoostArmed)
753
+ return 0;
754
+ this.edgeBoostArmed = false;
755
+ return this.edge;
756
+ }
757
+ /** New job, fresh luck. */
758
+ refreshEdge() {
759
+ this.edgeRemaining = this.edge;
760
+ this.edgeBoostArmed = false;
761
+ }
762
+ // ======================== Magic (see commands/cast.ts) ========================
763
+ // Two roads to casting. GEAR-DRIVEN: what a mundane can cast comes from
764
+ // the spell formulas/grimoires they carry, at a flat scholar's base pool
765
+ // -- consistent with the rest of the game, where gear IS the progression
766
+ // system. INNATE: an Awakened actor (magic attribute > 0) knows every
767
+ // spell without any gear -- the talent is the knowledge; formulas are
768
+ // how mundanes fake it -- and casts from magic + 2 instead of the flat
769
+ // base, so a strong mage out-throws any book-taught scholar. A Focus
770
+ // stacks on top of either road.
771
+ /** Base casting dice for a mundane casting from a formula/grimoire. */
772
+ static BASE_MAGIC_POOL = 5;
773
+ /** Awakened: innate talent, no gear required. Adepts count -- their
774
+ * aura burns like any mage's, and assensing works -- but they channel
775
+ * magic into the body, not spells (see knowsSpell/isCaster). */
776
+ isAwakened() {
777
+ return this.magic > 0;
778
+ }
779
+ /** Technomancer: living persona -- the Matrix without a deck (see ICombatAttributes.resonance). */
780
+ isTechnomancer() {
781
+ return this.resonance > 0;
782
+ }
783
+ // A sustained spell (currently only the armor spell) grants its bonus
784
+ // continuously but costs concentration: -2 dice on every active pool
785
+ // while it's up. See cast.ts.
786
+ _sustainedArmor = 0;
787
+ get sustainedArmor() {
788
+ return this._sustainedArmor;
789
+ }
790
+ setSustainedArmor(bonus) {
791
+ this._sustainedArmor = Math.max(0, Math.floor(bonus));
792
+ }
793
+ get sustainingPenalty() {
794
+ return this._sustainedArmor > 0 ? 2 : 0;
795
+ }
796
+ /** The best Focus item on this actor, if any -- scales casting power. */
797
+ getBestFocus() {
798
+ return this._inventory.getAllItems()
799
+ .filter(i => i.category === Category.Focus)
800
+ .sort((a, b) => b.qualityBonus - a.qualityBonus)[0];
801
+ }
802
+ getMagicPool() {
803
+ const focus = this.getBestFocus();
804
+ const focusBonus = focus ? 1 + focus.qualityBonus : 0;
805
+ // An Awakened caster's raw talent replaces the scholar's flat base --
806
+ // but never undercuts it (a barely-Awakened magic 1 still casts at
807
+ // least as well as someone who studied the book).
808
+ const base = this.isAwakened()
809
+ ? Math.max(Player.BASE_MAGIC_POOL, this.magic + 2)
810
+ : Player.BASE_MAGIC_POOL;
811
+ return Math.max(0, base + focusBonus + this.woundModifier - this.sustainingPenalty);
812
+ }
813
+ /**
814
+ * Awakened actors know every spell innately. A mundane knows a spell if
815
+ * they carry its formula (item name must contain the spell's name) or
816
+ * any grimoire (a spellbook teaches everything).
817
+ */
818
+ knowsSpell(spellName) {
819
+ // An adept's magic is muscle memory, not incantation -- no spells,
820
+ // not even from a formula in hand. Their magic already lives in their
821
+ // meat-plane combat pools.
822
+ if (this.adept)
823
+ return false;
824
+ if (this.isAwakened())
825
+ return true;
826
+ const wanted = spellName.toLowerCase();
827
+ return this._inventory.getAllItems().some(i => i.category === Category.Grimoire ||
828
+ (i.category === Category.SpellFormula && i.name.toLowerCase().includes(wanted)));
829
+ }
830
+ /**
831
+ * The gate for attempting magic at all (ward bypass included): innate
832
+ * talent, or any magic gear.
833
+ */
834
+ isCaster() {
835
+ if (this.adept)
836
+ return false; // body-magic only -- see knowsSpell
837
+ return this.isAwakened() || this._inventory.getAllItems().some(i => i.category === Category.SpellFormula ||
838
+ i.category === Category.Grimoire ||
839
+ i.category === Category.Focus);
840
+ }
841
+ // ================================================================================
326
842
  get lifestyleChoices() {
327
843
  return this._lifestyleChoices;
328
844
  }