@maka/maka-cli 5.6.0 → 5.8.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 (243) 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 +33 -32
  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 +81 -2
  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 +197 -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 +21 -11
  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 +13 -5
  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 +6 -7
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +54 -20
  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 +306 -55
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.d.ts +33 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +137 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -0
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.d.ts +13 -0
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +37 -0
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -0
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/drink.d.ts +14 -0
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/drink.js +15 -0
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/drink.js.map +1 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +7 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/eat.d.ts +14 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/eat.js +15 -0
  72. package/bundle/typescript/src/commands/game/sideQuest/commands/eat.js.map +1 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js +10 -0
  74. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +3 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +101 -1
  78. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +6 -1
  80. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +67 -22
  81. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  82. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +15 -7
  83. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  84. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +58 -2
  85. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  86. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.d.ts +26 -0
  87. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +125 -0
  88. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -0
  89. package/bundle/typescript/src/commands/game/sideQuest/commands/install.d.ts +21 -0
  90. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +125 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js.map +1 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.d.ts +16 -0
  93. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +83 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js.map +1 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.d.ts +16 -0
  96. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js +76 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js.map +1 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/commands/lock.js +4 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/commands/lock.js.map +1 -1
  100. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +7 -6
  101. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  102. package/bundle/typescript/src/commands/game/sideQuest/commands/order.d.ts +19 -0
  103. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +151 -0
  104. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/commands/pay.d.ts +13 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/commands/pay.js +39 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/commands/pay.js.map +1 -0
  108. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +5 -1
  109. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  110. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +6 -3
  111. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
  112. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.d.ts +21 -0
  113. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +121 -0
  114. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js.map +1 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +60 -12
  116. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
  117. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +27 -10
  118. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  119. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +15 -4
  120. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  121. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.d.ts +17 -0
  122. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +125 -0
  123. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -0
  124. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +36 -3
  125. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  126. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +9 -1
  127. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js.map +1 -1
  128. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +27 -4
  129. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -1
  130. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +1 -1
  131. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -1
  132. package/bundle/typescript/src/commands/game/sideQuest/commands/summon.d.ts +19 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +112 -0
  134. package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js.map +1 -0
  135. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +8 -0
  136. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  137. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.d.ts +14 -0
  138. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js +67 -0
  139. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js.map +1 -0
  140. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +7 -0
  141. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  142. package/bundle/typescript/src/commands/game/sideQuest/commands/use.d.ts +29 -0
  143. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +118 -11
  144. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  145. package/bundle/typescript/src/commands/game/sideQuest/commands/work.d.ts +15 -0
  146. package/bundle/typescript/src/commands/game/sideQuest/commands/work.js +68 -0
  147. package/bundle/typescript/src/commands/game/sideQuest/commands/work.js.map +1 -0
  148. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js +2 -1
  149. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js.map +1 -1
  150. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +1 -1
  151. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +132 -5
  152. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  153. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +6 -2
  154. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +10 -6
  155. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
  156. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +196 -0
  157. package/bundle/typescript/src/commands/game/sideQuest/game.js +828 -31
  158. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  159. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +28 -0
  160. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +111 -0
  161. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  162. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +33 -1
  163. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +232 -10
  164. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  165. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +111 -5
  166. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +432 -41
  167. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  168. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +6 -6
  169. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +38 -14
  170. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  171. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +1 -0
  172. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
  173. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  174. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +9 -0
  175. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +48 -8
  176. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  177. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +44 -0
  178. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +155 -0
  179. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -0
  180. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +274 -5
  181. package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
  182. package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +17 -0
  183. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.d.ts +2 -0
  184. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js +4 -0
  185. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js.map +1 -1
  186. package/bundle/typescript/src/commands/game/sideQuest/ui.js +82 -8
  187. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  188. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.d.ts +13 -6
  189. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +28 -15
  190. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
  191. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +15 -3
  192. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +207 -34
  193. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  194. package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.d.ts +56 -0
  195. package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js +144 -0
  196. package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js.map +1 -0
  197. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +7 -4
  198. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +8 -5
  199. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -1
  200. package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.d.ts +14 -0
  201. package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js +15 -0
  202. package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js.map +1 -0
  203. package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.d.ts +16 -0
  204. package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js +49 -4
  205. package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js.map +1 -1
  206. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.d.ts +10 -0
  207. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.js +16 -0
  208. package/bundle/typescript/src/commands/game/sideQuest/utilities/outdoors.js.map +1 -0
  209. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +21 -0
  210. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +107 -0
  211. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  212. package/bundle/typescript/src/commands/game/sideQuest/utilities/terminal-reset.d.ts +17 -0
  213. package/bundle/typescript/src/commands/game/sideQuest/utilities/terminal-reset.js +23 -0
  214. package/bundle/typescript/src/commands/game/sideQuest/utilities/terminal-reset.js.map +1 -0
  215. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +51 -15
  216. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  217. package/bundle/typescript/src/commands/remote/reconfig.sub.cmd.js +27 -1
  218. package/bundle/typescript/src/commands/remote/reconfig.sub.cmd.js.map +1 -1
  219. package/bundle/typescript/src/commands/run/run.command.js +1 -7
  220. package/bundle/typescript/src/commands/run/run.command.js.map +1 -1
  221. package/bundle/typescript/src/commands/ssl.js +10 -15
  222. package/bundle/typescript/src/commands/ssl.js.map +1 -1
  223. package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js +1 -6
  224. package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js.map +1 -1
  225. package/bundle/typescript/src/generators/config.js +11 -10
  226. package/bundle/typescript/src/generators/config.js.map +1 -1
  227. package/bundle/typescript/src/templates/meteor/config/pm2.config.js +3 -3
  228. package/bundle/typescript/src/templates/meteor/project/gitignore +2 -1
  229. package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.tsx +3 -4
  230. package/bundle/typescript/src/tool.d.ts +2 -0
  231. package/bundle/typescript/src/tool.js +3 -0
  232. package/bundle/typescript/src/tool.js.map +1 -1
  233. package/bundle/typescript/src/tools/aws/aws.class.js +6 -4
  234. package/bundle/typescript/src/tools/aws/aws.class.js.map +1 -1
  235. package/bundle/typescript/src/tools/dotenvx/dotenvx.class.d.ts +24 -0
  236. package/bundle/typescript/src/tools/dotenvx/dotenvx.class.js +66 -0
  237. package/bundle/typescript/src/tools/dotenvx/dotenvx.class.js.map +1 -0
  238. package/bundle/typescript/src/tools/ssh/ssh.class.js +1 -1
  239. package/bundle/typescript/src/tools/ssh/ssh.class.js.map +1 -1
  240. package/bundle/typescript/src/tools/ssl/ssl.class.d.ts +8 -0
  241. package/bundle/typescript/src/tools/ssl/ssl.class.js +53 -0
  242. package/bundle/typescript/src/tools/ssl/ssl.class.js.map +1 -1
  243. package/package.json +2 -2
@@ -1,6 +1,10 @@
1
1
  import blessed from 'blessed';
2
2
  import { capitalCase } from 'change-case';
3
+ import { Category } from './types/shared/item-enum.js';
4
+ import { Item } from './models/item.js';
3
5
  import { Room } from './models/_index.js';
6
+ import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
7
+ import { rollPool, formatRoll } from './utilities/dice.js';
4
8
  import { Direction } from './types/shared/direction-enum.js';
5
9
  import { Logger } from './utilities/logger.js';
6
10
  import { LookCommand } from './commands/look.js';
@@ -47,8 +51,26 @@ import { PanCommand } from './commands/pan.js';
47
51
  import { RebootCommand } from './commands/reboot.js';
48
52
  import { BrandishCommand, HolsterCommand } from './commands/brandish.js';
49
53
  import { AdvanceCommand } from './commands/advance.js';
54
+ import { QualityTradeCommand } from './commands/quality-trade.js';
55
+ import { AskCommand } from './commands/ask.js';
56
+ import { ContactsCommand } from './commands/contacts.js';
57
+ import { JumpCommand } from './commands/jump.js';
58
+ import { InitiateCommand } from './commands/initiate.js';
59
+ import { SummonCommand } from './commands/summon.js';
60
+ import { OrderCommand } from './commands/order.js';
61
+ import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
62
+ import { InstallCommand } from './commands/install.js';
63
+ import { BuyCommand } from './commands/buy.js';
64
+ import { SellCommand } from './commands/sell.js';
65
+ import { PayCommand } from './commands/pay.js';
66
+ import { LifestyleCommand } from './commands/lifestyle.js';
67
+ import { TreatCommand } from './commands/treat.js';
68
+ import { WorkCommand } from './commands/work.js';
69
+ import { EatCommand } from './commands/eat.js';
70
+ import { DrinkCommand } from './commands/drink.js';
50
71
  import { equipIntoEmptySlots } from './archetypes.js';
51
72
  import { NPC } from './models/npc.js';
73
+ import { CommandRegistry } from './commands/command-registry.js';
52
74
  import { HaggleCommand } from './commands/haggle.js';
53
75
  import { SneakCommand } from './commands/sneak.js';
54
76
  import { PersuadeCommand } from './commands/persuade.js';
@@ -58,6 +80,8 @@ import { CommandFactory } from './factories/command-factory.js';
58
80
  import { SceneSynthesizer } from './factories/scene-factory.js';
59
81
  import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator.js';
60
82
  import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
83
+ import { AI } from '../../../tools/ai/ai.class.js';
84
+ import { BULLET } from './utilities/log-style.js';
61
85
  export default class Game {
62
86
  initialized;
63
87
  static instance;
@@ -73,6 +97,7 @@ export default class Game {
73
97
  // give the log the whole screen.
74
98
  _screen;
75
99
  _logBox;
100
+ _metaBox;
76
101
  _statusBox;
77
102
  _inputBox;
78
103
  _useAi = false;
@@ -99,6 +124,476 @@ export default class Game {
99
124
  get hasHub() {
100
125
  return this._hubScene !== undefined;
101
126
  }
127
+ /** The active job's name (seed name) -- notoriety incidents cite it. */
128
+ get currentJobName() {
129
+ return this.sceneSeed?.name;
130
+ }
131
+ // ==================== CONTACTS (SR5e p.386-389) ====================
132
+ // Earned, never bought: a hub NPC becomes a contact through a real
133
+ // transaction (a sale, a bowl served, stitches, a fenced score), and
134
+ // the relationship deepens through gigs (to Loyalty 6) or patronage
135
+ // (money alone caps at Loyalty 3 -- cred buys familiarity, not
136
+ // friendship). CONNECTION is the contact's reach (role heuristic,
137
+ // same duck-typing school as commerce.isFence); LOYALTY is yours to
138
+ // grow. "ask" (commands/ask.ts) is what the roster is FOR: legwork
139
+ // against the real pending-run seed. One pull per contact per
140
+ // homecoming (asked resets in returnToHub).
141
+ contacts = new Map();
142
+ /** How much juice the name carries, read off the role. */
143
+ static connectionFor(npc) {
144
+ const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
145
+ if (npc.name === FIXER_NAME || /fixer|johnson/.test(hay))
146
+ return 5;
147
+ if (/fence|pawn|launder|smuggl|no questions/.test(hay))
148
+ return 4;
149
+ if (/talismonger|street doc|clinic|surgeon|decker|information/.test(hay))
150
+ return 3;
151
+ if (/bartender|barkeep|vendor|noodle|shop|stall|merchant|dealer|market/.test(hay))
152
+ return 2;
153
+ return 1;
154
+ }
155
+ /**
156
+ * HOME TURF: the hub is where the runner LIVES -- everyone in the
157
+ * district already knows them (player request: "all NPCs there should
158
+ * already know them"). Called at hub capture: the whole meat-plane
159
+ * cast opens at Loyalty 1 ("knows your name"), so legwork works from
160
+ * day one and only run-scene faces are ever strangers. Depth is still
161
+ * earned -- trade, gigs, patronage -- exactly as before. Ice and
162
+ * spirits don't do noodles: non-meat actors stay off the roster.
163
+ */
164
+ seedHomeTurfContacts() {
165
+ for (const actor of this._hubScene?.allActors ?? []) {
166
+ if (actor === this.player || actor.plane !== 'meat')
167
+ continue;
168
+ if (this.contacts.has(actor.name))
169
+ continue;
170
+ this.contacts.set(actor.name, { connection: Game.connectionFor(actor), loyalty: 1, spent: 0, gigs: 0, asked: false });
171
+ }
172
+ Logger.getInstance().write(`Home turf: ${this.contacts.size} contacts seeded from the hub cast.`);
173
+ }
174
+ // ==================== COMPANIONS: the party layer ====================
175
+ // Every companion is a per-scene NPC SHELL plus persistent STATE, and
176
+ // scene transitions are covered by narration, never instance rebinding
177
+ // (design decision with the player): spirits fade (RAW p.300: the
178
+ // summons doesn't outlast the ride), drones fold to the pack, agents
179
+ // into the deck -- damage lives on the bound Item and redeploys free.
180
+ // Tenants: summoned spirits (summon.ts), matrix agents (agent-cmd.ts),
181
+ // autonomous drones (deploy in drone-deploy.ts). Future tenants
182
+ // (enlisted allies, anthroform frames) ride the same rail with
183
+ // different words ("takes their own ride and meets you there").
184
+ // Roster invariant: entries are LIVE SHELLS only -- dismissal and
185
+ // death both remove the entry; persistent state (the frame, the deck)
186
+ // lives on Items in the player's possession, never here.
187
+ companions = [];
188
+ // Drones deployed at travel time re-shell on arrival ("the rotors spin
189
+ // back up at the curb") -- tracked as their items across the swap.
190
+ _pendingRedeploys = [];
191
+ /**
192
+ * The one correct runtime NPC spawn (mirrors scene-factory's seed
193
+ * path): construct with a scene-bound registry, add to the scene
194
+ * (names must be unique -- addActor throws), place via
195
+ * setCharacterLocation (the ONLY path that binds room observers), and
196
+ * hand it the scene for its AI context.
197
+ */
198
+ spawnCompanionShell(opts) {
199
+ if (this.scene.getActor(opts.name)) {
200
+ Logger.getInstance().write(`spawnCompanionShell: name "${opts.name}" already in scene -- refusing.`);
201
+ return null;
202
+ }
203
+ const npc = new NPC({
204
+ dialog: [],
205
+ description: opts.description,
206
+ playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat },
207
+ }, new CommandRegistry(this.scene));
208
+ npc.plane = opts.plane;
209
+ npc.allyOf = this.player.name;
210
+ npc.companionKind = opts.kind;
211
+ npc.boundDevice = opts.boundDevice;
212
+ this.scene.addActor(npc);
213
+ this.scene.setCharacterLocation(npc, opts.room.name);
214
+ npc.scene = this.scene;
215
+ this.companions.push({ npc, kind: opts.kind, services: opts.services, force: opts.force, boundDevice: opts.boundDevice });
216
+ Logger.getInstance().write(`Companion spawned: ${opts.name} (${opts.kind}, force ${opts.force}) in ${opts.room.name}.`);
217
+ return npc;
218
+ }
219
+ /**
220
+ * The autonomous-drone shell, shared by the deploy command and
221
+ * arrival re-shelling: the frame's dog-brain (Pilot = 2 + quality)
222
+ * stands in for every attribute it lacks (RAW p.269-271).
223
+ */
224
+ // Cargo sling by frame class (player request: capacity enforced -- a
225
+ // palm-sized Fly-Spy is not a mule). Keyed by the frame Item's size.
226
+ static DRONE_CARGO_KG = {
227
+ tiny: 0.5, small: 2, medium: 8, large: 20, huge: 40,
228
+ };
229
+ deployDroneShell(item, room) {
230
+ const pilot = 2 + item.qualityBonus;
231
+ const armed = /armed|combat|gun|roto|assault|sentry|weapon/i.test(`${item.name} ${item.description ?? ''}`);
232
+ const shell = this.spawnCompanionShell({
233
+ name: `${item.name} (autopilot)`,
234
+ description: `${this.player.name}'s drone running its dog-brain autopilot: it follows them, watches, and ${armed ? 'shoots what threatens them' : 'has cameras where guns would go -- it never attacks'}.`,
235
+ kind: 'drone',
236
+ combat: { body: 2 + item.qualityBonus, agility: pilot, reaction: pilot, logic: pilot, intuition: pilot, willpower: pilot, strength: 1, charisma: 1, skills: { perception: pilot, ...(armed ? { firearms: pilot } : {}) } },
237
+ room, plane: 'meat', force: pilot, boundDevice: item,
238
+ });
239
+ // Take-command weight checks (Player.addInventory) enforce this cap
240
+ // for the shell exactly as they do for a runner.
241
+ if (shell)
242
+ shell.maxCarryingWeight = Game.DRONE_CARGO_KG[String(item.size)] ?? 2;
243
+ return shell;
244
+ }
245
+ /** Drops a companion's shell and its roster entry. */
246
+ dismissCompanion(entry, reason) {
247
+ if (this.scene.getActor(entry.npc.name) === entry.npc) {
248
+ // Cargo comes home with the shell (player request: fetch-and-carry
249
+ // runs -- a real session's drone took a credstick and had no way
250
+ // to hand it over). A deliberate dismissal passes whatever the
251
+ // shell carried to its master; without this, scene.removeActor
252
+ // dumps it on the floor wherever the shell happened to be.
253
+ const carried = entry.npc.inventory.getAllItems().map(i => i.name);
254
+ const delivered = carried.filter(n => entry.npc.inventory.transferItemTo(n, this.player.inventory));
255
+ if (delivered.length > 0) {
256
+ Logger.getInstance().logWithColor(`👥 From ${entry.npc.name}'s cargo, now yours: ${delivered.join(', ')}.`, 'green');
257
+ }
258
+ // Mark deliberate before removeActor fires onActorRemoved, so no
259
+ // wreckage is charged to a clean dismissal.
260
+ entry.npc.deathClaimed = true;
261
+ this.scene.removeActor(entry.npc.name);
262
+ }
263
+ const idx = this.companions.indexOf(entry);
264
+ if (idx >= 0)
265
+ this.companions.splice(idx, 1);
266
+ Logger.getInstance().write(`Companion dismissed: ${entry.npc.name} -- ${reason}`);
267
+ }
268
+ /**
269
+ * An actor left the world (Scene.removeActor). For a companion shell
270
+ * that DIED (not a deliberate dismissal -- those set deathClaimed
271
+ * first and prune themselves), settle the wreckage: a drone's frame is
272
+ * wrecked where it fell, an agent derezzes (the deck already ate the
273
+ * damage), a spirit is banished outright.
274
+ */
275
+ onActorRemoved(name) {
276
+ const entry = this.companions.find(c => c.npc.name === name);
277
+ if (!entry)
278
+ return;
279
+ const idx = this.companions.indexOf(entry);
280
+ if (idx >= 0)
281
+ this.companions.splice(idx, 1);
282
+ if (entry.kind === 'drone' && entry.boundDevice && !entry.npc.deathClaimed) {
283
+ entry.boundDevice.takeDroneDamage(entry.boundDevice.maxDroneBoxes);
284
+ Logger.getInstance().logWithColor(`👥 The ${entry.boundDevice.name} is WRECKED where it fell -- rest to repair the frame.`, 'yellow');
285
+ }
286
+ this.updateStatus();
287
+ }
288
+ /**
289
+ * Spirits with no services left depart once no exchange holds them
290
+ * (called from CombatExchange.finishExchange): the bargain is spent.
291
+ */
292
+ settleSpiritServices() {
293
+ for (const entry of [...this.companions]) {
294
+ if (entry.kind !== 'spirit' || (entry.services ?? 0) > 0)
295
+ continue;
296
+ if (entry.npc.inExchange)
297
+ continue;
298
+ // The paid fight is fought to the END. Between exchanges of the
299
+ // same fight inExchange is briefly false -- a real session's
300
+ // spirit spent its last service to join a fight and was dismissed
301
+ // 3ms later, before a single blow. Same freshness window as the
302
+ // per-fight billing in combat-exchange.
303
+ if (entry.npc.combatOpponent && Date.now() - entry.npc.lastExchangeAt < 60_000)
304
+ continue;
305
+ Logger.getInstance().logWithColor(`👥 ${entry.npc.name}'s bargain is spent -- it bows once and thins into nothing.`, 'green');
306
+ this.dismissCompanion(entry, 'services exhausted');
307
+ this.updateStatus();
308
+ }
309
+ }
310
+ // ==================== Pursuit heartbeat ====================
311
+ // Companions are stimulus-driven like every NPC: a directive burst ends
312
+ // at the reflect cap ("Max recursion reached") and a shell alone in a
313
+ // distant room then NEVER acts again -- a real session's drone was
314
+ // ordered to "find Mr. Krow", walked three rooms, and froze mid-hunt.
315
+ // The heartbeat re-spurs every shell with an open objective on a slow
316
+ // tick until it reports DONE/FAILED (which clears the objective -- see
317
+ // NPC.relayToMaster) or is dismissed.
318
+ _pursuitTimer;
319
+ _pursuitInFlight = new Set();
320
+ static PURSUIT_TICK_MS = 30_000;
321
+ startPursuitHeartbeat() {
322
+ if (this._pursuitTimer)
323
+ return;
324
+ this._pursuitTimer = setInterval(() => this.spurCompanionPursuits(), Game.PURSUIT_TICK_MS);
325
+ // Never hold the process open on our account.
326
+ this._pursuitTimer.unref?.();
327
+ }
328
+ stopPursuitHeartbeat() {
329
+ if (this._pursuitTimer)
330
+ clearInterval(this._pursuitTimer);
331
+ this._pursuitTimer = undefined;
332
+ }
333
+ spurCompanionPursuits() {
334
+ // The tick doubles as the spent-bargain sweep: finishExchange's own
335
+ // settle call now skips spirits whose fight is still warm (see
336
+ // settleSpiritServices), so without this a victorious spirit would
337
+ // linger at zero services forever.
338
+ this.settleSpiritServices();
339
+ if (!AI.isConfigured())
340
+ return;
341
+ for (const entry of this.companions) {
342
+ const npc = entry.npc;
343
+ if (!npc.objective)
344
+ continue;
345
+ if (npc.isIncapacitated() || npc.inExchange || npc.waitingForLull)
346
+ continue;
347
+ // A stale shell (already removed from the world) gets no spur.
348
+ if (this.scene.getActor(npc.name) !== npc)
349
+ continue;
350
+ // One spur in flight per shell -- a slow AI burst must not stack
351
+ // with the next tick.
352
+ if (this._pursuitInFlight.has(npc))
353
+ continue;
354
+ this._pursuitInFlight.add(npc);
355
+ Logger.getInstance().write(`Pursuit heartbeat: spurring ${npc.name} -- ${npc.objective}`);
356
+ void Promise.resolve(npc.hear(this.player, `[System note: HEARTBEAT -- your standing directive is still open: "${npc.objective}". Continue it NOW with [COMMAND] actions -- several steps if needed. If it is complete, say a line starting "DONE: <result>"; if it cannot be done, say a line starting "FAILED: <why>". Do not go quiet.]`, { viaLink: true }))
357
+ .catch(err => Logger.getInstance().error(`Pursuit heartbeat failed for ${npc.name}: ${err}`))
358
+ .finally(() => this._pursuitInFlight.delete(npc));
359
+ }
360
+ }
361
+ /** Pre-travel: every shell folds, each with its own goodbye. */
362
+ foldCompanionsForTravel() {
363
+ const lines = [];
364
+ for (const entry of [...this.companions]) {
365
+ if (entry.kind === 'spirit') {
366
+ lines.push(`Your bound spirit thins into the rain -- the summons doesn't outlast the ride.`);
367
+ }
368
+ else if (entry.kind === 'drone') {
369
+ lines.push(`The ${entry.boundDevice?.name ?? 'drone'} autopilots home to your hand for the ride.`);
370
+ if (entry.boundDevice)
371
+ this._pendingRedeploys.push(entry.boundDevice);
372
+ }
373
+ else if (entry.kind === 'agent') {
374
+ lines.push(`Your agent folds back into the deck.`);
375
+ }
376
+ else {
377
+ lines.push(`${entry.npc.name} takes their own ride -- they'll meet you there.`);
378
+ }
379
+ this.dismissCompanion(entry, `travel: ${entry.kind} folds`);
380
+ }
381
+ return lines;
382
+ }
383
+ /** Post-arrival: re-shell what was deployed when you left (drones). */
384
+ unfoldCompanionsOnArrival() {
385
+ const lines = [];
386
+ for (const item of this._pendingRedeploys.splice(0)) {
387
+ if (item.isWrecked)
388
+ continue;
389
+ const shell = this.deployDroneShell(item, this.player.currentLocation);
390
+ if (shell)
391
+ lines.push(`The ${item.name}'s rotors spin back up at the curb.`);
392
+ }
393
+ return lines;
394
+ }
395
+ /**
396
+ * Records a real transaction with a hub NPC: first contact opens the
397
+ * relationship at Loyalty 1 (a safeguard -- the hub cast is pre-seeded
398
+ * by seedHomeTurfContacts); gigs raise it a point each (cap 6);
399
+ * cumulative spend raises it at each full 1,000 nuyen (cap 3).
400
+ * Returns a player-facing line when something changed, else null.
401
+ */
402
+ touchContact(name, kind, spent = 0) {
403
+ if (!this.hasHub)
404
+ return null;
405
+ const npc = this._hubScene?.allActors?.find(a => a.name === name && a !== this.player);
406
+ if (!npc)
407
+ return null;
408
+ let c = this.contacts.get(name);
409
+ if (!c) {
410
+ c = { connection: Game.connectionFor(npc), loyalty: 1, spent: 0, gigs: 0, asked: false };
411
+ this.contacts.set(name, c);
412
+ if (kind === 'trade')
413
+ c.spent += spent;
414
+ if (kind === 'gig')
415
+ c.gigs += 1;
416
+ Logger.getInstance().write(`Contact opened: ${name} (connection ${c.connection}).`);
417
+ return `${name} knows your name now -- a CONTACT (Connection ${c.connection}, Loyalty 1). ("contacts" for the roster, "ask ${name} about ..." for legwork.)`;
418
+ }
419
+ if (kind === 'gig') {
420
+ c.gigs += 1;
421
+ if (c.loyalty < 6) {
422
+ c.loyalty += 1;
423
+ return `${name} remembers who made that hand-off happen. (Loyalty ${c.loyalty}.)`;
424
+ }
425
+ return null;
426
+ }
427
+ const thousandsBefore = Math.floor(c.spent / 1000);
428
+ c.spent += spent;
429
+ if (Math.floor(c.spent / 1000) > thousandsBefore && c.loyalty < 3) {
430
+ c.loyalty += 1;
431
+ return `${name} has started keeping your usual ready. (Loyalty ${c.loyalty} -- patronage tops out at 3; run their errands to go deeper.)`;
432
+ }
433
+ return null;
434
+ }
435
+ // ==================== Downtime economy (hub life) ====================
436
+ // A fence's standing quote (see commands/sell.ts / haggle.ts): one at a
437
+ // time, replaced by any new quote, dead the moment the fence isn't at
438
+ // the counter. Lives here because sell and haggle are separate commands
439
+ // negotiating the same number.
440
+ pendingSale;
441
+ // LIFESTYLE (SR5e p.369, monthly costs scaled to per-job-cycle): the
442
+ // hideout has a tier, upkeep is charged on every homecoming
443
+ // (returnToHub), and the tier buys real mechanics -- rest quality at
444
+ // home (rest.ts) and, higher up, a bonus edge every job. Can't cover
445
+ // the upkeep? You slide DOWN a tier and the shortfall becomes debt --
446
+ // and while the tab stands, rest at the hideout recovers half (rest.ts;
447
+ // "pay" squares it, pay.ts; "lifestyle" views and moves tiers).
448
+ lifestyleTier = 'Squatter';
449
+ rentDebt = 0;
450
+ /** The active lifestyle tier's row (falls back to Squatter). */
451
+ get lifestyle() {
452
+ return LIFESTYLE_TIERS.find(t => t.name === this.lifestyleTier) ?? LIFESTYLE_TIERS[1];
453
+ }
454
+ /**
455
+ * Makes the hideout LOOK like what the rent buys: the room's description
456
+ * follows the active tier (player request -- an upgraded lifestyle read
457
+ * as the same windowless squat). Called wherever the tier moves: the
458
+ * lifestyle command, and the shortfall slide in returnToHub.
459
+ */
460
+ syncHomeToLifestyle() {
461
+ if (this._homeRoom)
462
+ this._homeRoom.description = this.lifestyle.homeDescription;
463
+ }
464
+ // RESTOCK (see returnToHub): consumable shop stock -- food, drink,
465
+ // ammo, patches -- snapshotted at hub capture and restored to any shelf
466
+ // found empty on each homecoming. Durable one-offs are deliberately NOT
467
+ // in the category set: the pawnshop's lockpicks, once sold, stay sold.
468
+ static RESTOCK_CATEGORIES = new Set([
469
+ Category.Food, Category.Drink, Category.Ammunition, Category.StimPatch, Category.Medical, Category.SlapPatch,
470
+ ]);
471
+ _restockSnapshot = [];
472
+ /** Captures every restockable shelf item's blueprint (hub capture time). */
473
+ snapshotVendorStock() {
474
+ this._restockSnapshot = [];
475
+ for (const room of Object.values(this.scene.getRooms())) {
476
+ for (const item of room.inventory.getAllItems()) {
477
+ if (!item.owner || !Game.RESTOCK_CATEGORIES.has(item.category))
478
+ continue;
479
+ this._restockSnapshot.push({
480
+ room: room.name,
481
+ owner: item.owner,
482
+ config: {
483
+ name: item.name, description: item.description, size: item.size,
484
+ category: item.category, shape: item.shape, color: item.color,
485
+ texture: item.texture, rating: item.rating, weight: item.weight,
486
+ details: item.details, price: item.price,
487
+ },
488
+ });
489
+ }
490
+ }
491
+ }
492
+ /** Refills empty shelves from the snapshot; returns how many came back. */
493
+ restockVendorShelves() {
494
+ let restocked = 0;
495
+ const rooms = this.scene.getRooms();
496
+ for (const entry of this._restockSnapshot) {
497
+ const room = Object.values(rooms).find(r => r.name === entry.room);
498
+ if (!room || room.inventory.hasItem(entry.config.name))
499
+ continue;
500
+ const fresh = new Item(entry.config);
501
+ fresh.owner = entry.owner;
502
+ room.inventory.addItem(fresh);
503
+ restocked += 1;
504
+ }
505
+ return restocked;
506
+ }
507
+ /** The fence moves merchandise: anything sold to a fence is sold ON. */
508
+ fenceSellsOn() {
509
+ for (const actor of this.scene.allActors) {
510
+ if (actor === this.player || !isFence(actor, actor.currentLocation))
511
+ continue;
512
+ for (const item of [...actor.inventory.getAllItems()]) {
513
+ if (item.transferable && item.category !== Category.Commlink) {
514
+ actor.inventory.removeItem(item.name);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ // SIDE GIGS (see commands/work.ts / give.ts): one small favor on offer
520
+ // at a time, rotating each homecoming like the rumor. Take it with
521
+ // "work" in front of the giver; deliver the package with "give"; the
522
+ // pay lands on delivery.
523
+ gigAvailable;
524
+ activeGig;
525
+ _gigPool = [];
526
+ _gigIndex = 0;
527
+ /** Rotates the gig on offer (skipped while one is already in hand). */
528
+ advanceGig() {
529
+ if (this._gigPool.length === 0 || this.activeGig)
530
+ return;
531
+ this.gigAvailable = this._gigPool[this._gigIndex % this._gigPool.length];
532
+ this._gigIndex += 1;
533
+ }
534
+ /**
535
+ * WORK FROM A RELATIONSHIP (see commands/ask.ts): a contact puts THEIR
536
+ * errand on the table. Their seeded gigs get first claim -- spliced
537
+ * out of the rotating pool, so work handed to you personally never
538
+ * reappears on the ambient board -- and when the pool holds nothing of
539
+ * theirs, the relationship synthesizes one: a sealed parcel to another
540
+ * hub local, priced off Connection (their reach IS the stakes) with a
541
+ * Loyalty kicker. Loyalty 5+ pays the family rate (+25%). The offer
542
+ * lands as gigAvailable: the existing "work" pickup and "give"
543
+ * delivery handle the rest unchanged.
544
+ */
545
+ offerGigFrom(contactName) {
546
+ const contact = this.contacts.get(contactName);
547
+ if (!contact || !this.hasHub || this.activeGig)
548
+ return null;
549
+ let gig = undefined;
550
+ const seeded = this._gigPool.findIndex(g => g.from === contactName);
551
+ if (seeded >= 0) {
552
+ gig = this._gigPool.splice(seeded, 1)[0];
553
+ }
554
+ else {
555
+ const locals = (this._hubScene?.allActors ?? [])
556
+ .filter(a => a !== this.player && a.name !== contactName && a.plane === 'meat' && !a.isIncapacitated())
557
+ .map(a => a.name);
558
+ if (locals.length === 0)
559
+ return null;
560
+ const to = locals[Math.floor(Math.random() * locals.length)];
561
+ const pay = 60 + contact.connection * 40 + contact.loyalty * 10;
562
+ gig = {
563
+ from: contactName,
564
+ to,
565
+ itemName: `${contactName}'s Sealed Parcel`,
566
+ itemDescription: `A tight-wrapped parcel, taped past curiosity. Whatever it is, ${contactName} trusts you to not ask.`,
567
+ pay,
568
+ brief: `Quiet job, no questions asked: this needs to reach ${to}, today, unopened. ${pay} nuyen on delivery.`,
569
+ };
570
+ }
571
+ const familyRate = contact.loyalty >= 5;
572
+ if (familyRate)
573
+ gig = { ...gig, pay: Math.round(gig.pay * 1.25) };
574
+ this.gigAvailable = gig;
575
+ Logger.getInstance().write(`Contact gig offered by ${contactName}: "${gig.itemName}" -> ${gig.to}, ${gig.pay}${familyRate ? ' (family rate)' : ''}.`);
576
+ return { ...gig, familyRate };
577
+ }
578
+ // FIRST-OF-ITS-KIND bounty (see commands/sell.ts): the fence pays a
579
+ // 50% premium on the first item of each category to cross her counter.
580
+ fencedCategories = new Set();
581
+ // Street rumors: the hub seed ships a pool (scene2.json "rumors"); one
582
+ // is ACTIVE at a time, rotating each time a run wraps. The active rumor
583
+ // is planted in the world-event mesh (every hub NPC's prompt reads it,
584
+ // so Mama Wu can actually gossip) and handed to the next job's skeleton
585
+ // prompt as its backdrop -- the street foreshadows the work.
586
+ _rumorPool = [];
587
+ _rumorIndex = 0;
588
+ activeRumor;
589
+ /** Rotates to the next rumor and plants it where NPCs will hear it. */
590
+ advanceRumor() {
591
+ if (this._rumorPool.length === 0)
592
+ return;
593
+ this.activeRumor = this._rumorPool[this._rumorIndex % this._rumorPool.length];
594
+ this._rumorIndex += 1;
595
+ this._hubScene?.addWorldEvent(`Word around the district: ${this.activeRumor}`);
596
+ }
102
597
  /** The persistent hideout room -- go.ts's walk-home trigger checks it. */
103
598
  get homeRoom() {
104
599
  return this._homeRoom;
@@ -242,6 +737,32 @@ export default class Game {
242
737
  CommandFactory.registerCommand('hangup', EndCallCommand);
243
738
  CommandFactory.registerCommand('advance', AdvanceCommand);
244
739
  CommandFactory.registerCommand('train', AdvanceCommand);
740
+ CommandFactory.registerCommand('qualities', QualityTradeCommand);
741
+ CommandFactory.registerCommand('quality', QualityTradeCommand);
742
+ CommandFactory.registerCommand('ask', AskCommand);
743
+ CommandFactory.registerCommand('contacts', ContactsCommand);
744
+ CommandFactory.registerCommand('jump', JumpCommand);
745
+ CommandFactory.registerCommand('rig', JumpCommand);
746
+ CommandFactory.registerCommand('initiate', InitiateCommand);
747
+ CommandFactory.registerCommand('submerge', InitiateCommand);
748
+ CommandFactory.registerCommand('summon', SummonCommand);
749
+ CommandFactory.registerCommand('conjure', SummonCommand);
750
+ CommandFactory.registerCommand('order', OrderCommand);
751
+ CommandFactory.registerCommand('deploy', DeployCommand);
752
+ CommandFactory.registerCommand('recall', RecallCommand);
753
+ CommandFactory.registerCommand('agent', AgentCommand);
754
+ CommandFactory.registerCommand('install', InstallCommand);
755
+ CommandFactory.registerCommand('chrome', InstallCommand);
756
+ CommandFactory.registerCommand('buy', BuyCommand);
757
+ CommandFactory.registerCommand('browse', BuyCommand);
758
+ CommandFactory.registerCommand('sell', SellCommand);
759
+ CommandFactory.registerCommand('pay', PayCommand);
760
+ CommandFactory.registerCommand('eat', EatCommand);
761
+ CommandFactory.registerCommand('drink', DrinkCommand);
762
+ CommandFactory.registerCommand('lifestyle', LifestyleCommand);
763
+ CommandFactory.registerCommand('treat', TreatCommand);
764
+ CommandFactory.registerCommand('work', WorkCommand);
765
+ CommandFactory.registerCommand('gig', WorkCommand);
245
766
  CommandFactory.registerCommand('haggle', HaggleCommand);
246
767
  CommandFactory.registerCommand('negotiate', HaggleCommand);
247
768
  CommandFactory.registerCommand('sneak', SneakCommand);
@@ -281,6 +802,9 @@ export default class Game {
281
802
  get logBox() {
282
803
  return this._logBox;
283
804
  }
805
+ get metaBox() {
806
+ return this._metaBox;
807
+ }
284
808
  get inputBox() {
285
809
  return this._inputBox;
286
810
  }
@@ -289,6 +813,7 @@ export default class Game {
289
813
  this._logFilePath = options.logFilePath;
290
814
  this.initInputBox(screen);
291
815
  this.initLogBox(screen);
816
+ this.initMetaBox(screen);
292
817
  this.initStatusBox(screen);
293
818
  // No "Generating Scene... Please wait." box anymore: every path now
294
819
  // arrives with REAL prose (hand-authored static seeds, chunk-authored
@@ -307,9 +832,12 @@ export default class Game {
307
832
  this.player.currentLocation = this.determineStartRoom();
308
833
  this.registerCommands();
309
834
  this._screen = screen;
835
+ // The description follows the rent from day one (see
836
+ // syncHomeToLifestyle) -- a fresh session at the default Squatter tier
837
+ // reads exactly as the hideout always has.
310
838
  this._homeRoom = new Room({
311
839
  name: 'Your Hideout',
312
- description: `A cramped, windowless room you can actually call your own -- a cot, a locker, and a door that locks from the inside. Whatever you leave here stays here, no matter where the next job takes you.`,
840
+ description: this.lifestyle.homeDescription,
313
841
  roomType: 'Residence',
314
842
  });
315
843
  this.scene.addRoom(this._homeRoom);
@@ -325,6 +853,17 @@ export default class Game {
325
853
  if (this.scene.isHub) {
326
854
  this._hubScene = this.scene;
327
855
  this._hubName = this.sceneSeed?.name ?? 'home';
856
+ // The street starts talking from minute one.
857
+ this._rumorPool = this.sceneSeed?.rumors ?? [];
858
+ this.advanceRumor();
859
+ // The economy wakes up with it: shelf blueprints for restocking,
860
+ // the first odd job on offer, and the lifestyle's edge perk.
861
+ this.snapshotVendorStock();
862
+ this._gigPool = this.sceneSeed?.gigs ?? [];
863
+ this.advanceGig();
864
+ this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
865
+ // Home turf knows its own (see seedHomeTurfContacts).
866
+ this.seedHomeTurfContacts();
328
867
  }
329
868
  // Gear up automatically: wearables (and the commlink) slot into any
330
869
  // EMPTY slots at load -- no "equip all" ritual to start a session
@@ -332,6 +871,7 @@ export default class Game {
332
871
  // never swapped out.
333
872
  equipIntoEmptySlots(this.player);
334
873
  this.warmNpcAppearancesInBackground();
874
+ this.startPursuitHeartbeat();
335
875
  this.initialized = true;
336
876
  // Nudge the player toward "call" up front -- the fixer convention means
337
877
  // the story deliberately doesn't narrate a completed briefing, so
@@ -394,11 +934,13 @@ export default class Game {
394
934
  * instant feedback, but this stays safe to call directly too).
395
935
  */
396
936
  async continueToNextScene() {
397
- // Hub sessions: a finished RUN doesn't chain into the next draft --
398
- // "continue" takes you home instead, and the next run is commissioned
399
- // from there (call the fixer). Classic sessions skip this and keep
400
- // the old continue->draft loop.
401
- if (this._hubScene && this.scene !== this._hubScene && this.scene.isCompleted()) {
937
+ // Hub sessions: a RUN doesn't chain into the next draft -- going
938
+ // home is going home, and the next run is commissioned from there
939
+ // (call the fixer). Deliberately NOT gated on completion: the taxi
940
+ // will carry a runner away from a job gone sideways (see call.ts) --
941
+ // walking away just means the payout never landed. Classic sessions
942
+ // skip this and keep the old continue->draft loop.
943
+ if (this._hubScene && this.scene !== this._hubScene) {
402
944
  return this.returnToHub();
403
945
  }
404
946
  const gate = this.canDraftNextScene();
@@ -425,6 +967,11 @@ export default class Game {
425
967
  const hub = this._hubScene;
426
968
  if (!hub)
427
969
  return `There's no home to go back to.`;
970
+ // Read the run's fate BEFORE the scene swap below discards it -- a
971
+ // walked-away job gets a different homecoming line than a wrapped one.
972
+ const wrappedRun = this.scene !== hub && this.scene.isCompleted();
973
+ // Companions fold for the ride home, same as the trip out.
974
+ const companionFoldLines = this.foldCompanionsForTravel();
428
975
  // Settle cross-plane and combat state that must not leak into the hub
429
976
  // (the run's rooms -- including a bodyRoom the player left behind --
430
977
  // are about to be orphaned wholesale).
@@ -433,6 +980,7 @@ export default class Game {
433
980
  this.player.simMode = undefined;
434
981
  this.player.astralTicks = 0;
435
982
  this.player.activeDeck = undefined;
983
+ this.player.riggedDrone = undefined;
436
984
  this.player.astralPerceiving = false;
437
985
  this.player.sneaking = false;
438
986
  this.player.inExchange = false;
@@ -444,12 +992,63 @@ export default class Game {
444
992
  this.connectHomeRoom(hub.determineStartRoom());
445
993
  this.player.currentLocation = this._homeRoom;
446
994
  this.player.cameFrom = undefined;
995
+ this.pendingSale = undefined;
996
+ // Contacts have had time to work their networks again -- one legwork
997
+ // pull each per homecoming (see commands/ask.ts).
998
+ for (const contact of this.contacts.values())
999
+ contact.asked = false;
1000
+ // LIFESTYLE upkeep comes due with the homecoming: this tier's share
1001
+ // plus any standing debt, paid from carried cred as far as it goes.
1002
+ // Coming up short SLIDES YOU DOWN a tier (the district notices) and
1003
+ // the shortfall becomes the tab -- rest.ts halves recovery while you
1004
+ // owe; "pay" squares it; "lifestyle" climbs back up.
1005
+ const tier = this.lifestyle;
1006
+ const due = tier.upkeep + this.rentDebt;
1007
+ const paid = Math.min(this.player.currency, due);
1008
+ if (paid > 0)
1009
+ this.player.adjustCurrency(-paid);
1010
+ this.rentDebt = due - paid;
1011
+ let rentLine;
1012
+ if (this.rentDebt > 0) {
1013
+ const tierIndex = LIFESTYLE_TIERS.findIndex(t => t.name === tier.name);
1014
+ if (tierIndex > 0) {
1015
+ this.lifestyleTier = LIFESTYLE_TIERS[tierIndex - 1].name;
1016
+ // The downgrade is visible the moment you look around.
1017
+ this.syncHomeToLifestyle();
1018
+ rentLine = `The landlord's drone pings before the door even shuts: ${paid > 0 ? `${paid} paid, ` : ''}${this.rentDebt} nuyen still owed. Your ${tier.name} arrangement lapses -- you're living ${this.lifestyleTier} now. ("pay" the tab, "lifestyle" to climb back.)`;
1019
+ }
1020
+ else {
1021
+ rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab. ("pay" when you're flush.)`;
1022
+ }
1023
+ }
1024
+ else {
1025
+ rentLine = `The landlord's drone takes its ${paid} nuyen (${tier.name} upkeep) at the door and buzzes off satisfied.`;
1026
+ }
1027
+ this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
1028
+ // A High lifestyle erases the run: hot shower, real bed, full reset.
1029
+ let comfortLine = '';
1030
+ if (this.lifestyle.clearStunOnReturn && this.player.stunTaken > 0) {
1031
+ const cleared = this.player.healStun(this.player.stunTaken);
1032
+ comfortLine = `\nThe shower runs hot until the run is a rumor -- ${cleared} stun gone before your head hits the pillow.`;
1033
+ }
1034
+ // Fresh streets, fresh gossip, restocked shelves, new work.
1035
+ this.advanceRumor();
1036
+ this.fenceSellsOn();
1037
+ const restocked = this.restockVendorShelves();
1038
+ const restockLine = restocked > 0 ? `\nThe district has restocked around you -- the wok steams, the bar's shelf is full, the market stalls are loud again.` : '';
1039
+ this.advanceGig();
447
1040
  this.updateEquipment(this.player.equipment);
448
1041
  this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
449
1042
  this.updateExits(this.player.currentLocation);
450
1043
  this.updateStatus();
451
- Logger.getInstance().write(`Returned to hub "${this._hubName}".`);
452
- return `\n=== ${this._hubName.toUpperCase()} ===\nThe run's wrapped and the street swallows you back up. You're home -- your hideout, one door off ${hub.determineStartRoom().name}. Call ${FIXER_NAME} when you're hungry for the next job.`;
1044
+ Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
1045
+ // Drones spin back up beside the cot.
1046
+ const companionArrivalLines = this.unfoldCompanionsOnArrival();
1047
+ const companionBlock = [...companionFoldLines, ...companionArrivalLines];
1048
+ const fateLine = wrappedRun
1049
+ ? `The run's wrapped and the street swallows you back up.`
1050
+ : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
1051
+ return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}. Call ${FIXER_NAME} when you're hungry for the next job.\n${rentLine}${comfortLine}${restockLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
453
1052
  }
454
1053
  /**
455
1054
  * The actual drafting work, split out from continueToNextScene() purely
@@ -481,7 +1080,7 @@ export default class Game {
481
1080
  //
482
1081
  // The player object rides along so the job is drafted FOR this runner
483
1082
  // -- their archetype, stats, and toolkit (see buildPlayerDossier).
484
- const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, previousFixerName, this.player);
1083
+ const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, previousFixerName, this.player, this.activeRumor);
485
1084
  if (!nextSeed) {
486
1085
  return `Incoming call -- ${FIXER_NAME}: "Job fell through, chummer. Client got cold feet before the ink dried. Give me another call in a bit -- I'll shake something loose."`;
487
1086
  }
@@ -513,20 +1112,30 @@ export default class Game {
513
1112
  this.player.activeCallPartner.activeCallPartner = undefined;
514
1113
  this.player.activeCallPartner = undefined;
515
1114
  }
1115
+ // Companions fold for the ride (spirits depart, drones to the pack,
1116
+ // agents into the deck) -- their lines join the JOB banner below.
1117
+ const companionLines = this.foldCompanionsForTravel();
516
1118
  // false, false: no starter kit (the player carries their own gear
517
1119
  // forward), and no blocking AI-redressing pass either -- the chunked
518
1120
  // rooms pass already wrote this scene's prose.
519
1121
  this.scene = await SceneSynthesizer.synthesizeFromJson(seed, this.player, false, false);
520
1122
  this.scene.initialize(this);
521
1123
  this.player.currentLocation = this.determineStartRoom();
522
- // New job, fresh luck (see commands/edge.ts).
523
- this.player.refreshEdge();
524
- // The home base is owned by Game, not any one Scene -- re-inject it and
525
- // rewire its one exit to point at this job's start room.
526
- this.scene.addRoom(this._homeRoom);
527
- this.connectHomeRoom(this.player.currentLocation);
528
- if (opts.startInHideout) {
529
- this.player.currentLocation = this._homeRoom;
1124
+ // New job -- and the needs come due at the door (see
1125
+ // applyDepartureNeeds): edge only refills fed-and-slept, and ignored
1126
+ // needs land as locked fatigue. Lines ride the JOB banner below.
1127
+ const needsLines = this.applyDepartureNeeds();
1128
+ // The home base is owned by Game, not any one Scene. HUB sessions
1129
+ // leave it OUT of run scenes (player request): your cot is across
1130
+ // town, not stapled to the jobsite -- the taxi ("call taxi", fare on
1131
+ // the tab if you're broke) is the road home. Classic sessions keep
1132
+ // the hideout adjacent as always: it's the only home they have.
1133
+ if (!this.hasHub) {
1134
+ this.scene.addRoom(this._homeRoom);
1135
+ this.connectHomeRoom(this.player.currentLocation);
1136
+ if (opts.startInHideout) {
1137
+ this.player.currentLocation = this._homeRoom;
1138
+ }
530
1139
  }
531
1140
  this.updateEquipment(this.player.equipment);
532
1141
  this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
@@ -534,8 +1143,72 @@ export default class Game {
534
1143
  this.updateStatus();
535
1144
  // Fresh streets, no trail yet (see Player.cameFrom / the HUD crumb).
536
1145
  this.player.cameFrom = undefined;
1146
+ // Deployed drones spin back up at the curb -- BEFORE appearance
1147
+ // warming so re-shelled companions get warmed with the cast.
1148
+ const arrivalLines = this.unfoldCompanionsOnArrival();
537
1149
  this.warmNpcAppearancesInBackground();
538
- return `\n=== JOB #${this._tier}: ${seed.name} ===\n${this.scene.story}`;
1150
+ const companionBlock = [...companionLines, ...arrivalLines];
1151
+ return `\n=== JOB #${this._tier}: ${seed.name} ===\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
1152
+ }
1153
+ /**
1154
+ * The needs check at every job departure (SR5e p.56 / p.172, scaled to
1155
+ * job-cycle "days"). Fed AND slept since the last departure: edge
1156
+ * refills and you head out sharp. Skip either: edge stays at whatever's
1157
+ * left. Keep skipping: escalating fatigue (1S, 2S, 3S... per cycle),
1158
+ * resisted Body + Willpower, landing as LOCKED stun no recovery touches
1159
+ * until the need is met (Player.healStun / markFed / markSlept). A
1160
+ * Sleep Regulator (augmentations.ts) halves the effective sleep debt.
1161
+ * Fatigue is capped one box short of a knockout -- RAW would drop you,
1162
+ * but collapsing INSIDE the departure transition would strand the
1163
+ * session between scenes; "running on fumes" carries the spirit.
1164
+ */
1165
+ applyDepartureNeeds() {
1166
+ const p = this.player;
1167
+ const fed = p.ateThisCycle;
1168
+ const slept = p.sleptThisCycle;
1169
+ const lines = [];
1170
+ p.hungerStreak = fed ? 0 : p.hungerStreak + 1;
1171
+ p.sleepStreak = slept ? 0 : p.sleepStreak + 1;
1172
+ p.refreshEdge(fed && slept);
1173
+ if (fed && slept) {
1174
+ lines.push(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edge + p.lifestyleEdgeBonus}.)`);
1175
+ }
1176
+ else {
1177
+ const missing = !fed && !slept ? 'meal and no sleep' : !fed ? 'meal' : 'sleep';
1178
+ lines.push(`No ${missing} since the last job -- your edge doesn't come back (${p.edgeRemaining} left). A hot meal and a night in your own bed reset it.`);
1179
+ }
1180
+ // Escalating, resisted, LOCKED fatigue per ignored need.
1181
+ const events = [];
1182
+ if (p.hungerStreak > 0) {
1183
+ events.push({ kind: 'hunger', label: 'Hunger', dmg: p.hungerStreak });
1184
+ }
1185
+ const effectiveSleepDebt = p.hasAug('sleep-regulator') ? Math.floor(p.sleepStreak / 2) : p.sleepStreak;
1186
+ if (effectiveSleepDebt > 0) {
1187
+ events.push({ kind: 'sleep', label: 'Sleep deprivation', dmg: effectiveSleepDebt });
1188
+ }
1189
+ for (const event of events) {
1190
+ const resist = rollPool(Math.max(1, p.body + p.willpower));
1191
+ const taken = Math.max(0, event.dmg - resist.hits);
1192
+ const headroom = Math.max(0, p.maxStunBoxes - p.stunTaken - 1);
1193
+ const applied = Math.min(taken, headroom);
1194
+ if (applied > 0) {
1195
+ p.takeStun(applied);
1196
+ p.addLockedFatigue(event.kind, applied);
1197
+ lines.push(`${event.label} (${event.dmg}S): resist ${formatRoll(resist)} -- ${applied} stun, and it will NOT heal until you ${event.kind === 'hunger' ? 'eat' : 'sleep'}: ${p.stunSummary()}`);
1198
+ }
1199
+ else if (taken > 0) {
1200
+ lines.push(`${event.label} (${event.dmg}S): resist ${formatRoll(resist)} -- you're running on fumes; there's barely anything left to take.`);
1201
+ }
1202
+ else {
1203
+ lines.push(`${event.label} (${event.dmg}S): resist ${formatRoll(resist)} -- you shake it off. For now.`);
1204
+ }
1205
+ Logger.getInstance().write(`Departure needs: ${event.label} ${event.dmg}S, resisted ${resist.hits}, applied ${applied} (locked).`);
1206
+ }
1207
+ // The new cycle starts unfed and unslept -- Wu's counter and your own
1208
+ // cot are how these flip back (use.ts eat / rest.ts at a Residence).
1209
+ p.ateThisCycle = false;
1210
+ p.sleptThisCycle = false;
1211
+ return lines;
539
1212
  }
540
1213
  /**
541
1214
  * "travel": takes the arranged transport to the pending job (see
@@ -569,6 +1242,7 @@ export default class Game {
569
1242
  }
570
1243
  }
571
1244
  async close() {
1245
+ this.stopPursuitHeartbeat();
572
1246
  this._inputBox.removeAllListeners(); // removes all event listeners
573
1247
  this._inputBox.cancel(); // cancels any active input session
574
1248
  this._inputBox.detach(); // optionally remove from screen
@@ -588,8 +1262,13 @@ export default class Game {
588
1262
  border: { type: 'line' },
589
1263
  style: { fg: 'white', border: { fg: 'cyan' } },
590
1264
  keys: true, // Enable keyboard input
591
- mouse: true, // Enable mouse input
592
- inputOnFocus: true, // Focus the text input when clicked,
1265
+ // NO mouse -- deliberately. Any widget with mouse:true makes
1266
+ // blessed arm xterm mouse tracking (?1000h/?1002h/?1003h/?1005h),
1267
+ // and three real sessions ended with the whole process tree dying
1268
+ // hard (no JS cleanup possible) and the shell flooded by raw mouse
1269
+ // reports on every cursor move. The input box is auto-focused and
1270
+ // keyboard-driven; click-to-focus bought nothing worth that risk.
1271
+ inputOnFocus: true,
593
1272
  vi: true
594
1273
  });
595
1274
  this._inputBox = inputBox;
@@ -620,13 +1299,17 @@ export default class Game {
620
1299
  id: 'logBox',
621
1300
  top: '0%',
622
1301
  left: '0%',
623
- width: '100%',
1302
+ // COLUMNS, not rows (playtest note: a 3-row Mechanics strip made
1303
+ // combat rolls "near impossible to read" -- they scrolled out as
1304
+ // fast as they landed). The world log takes the left ~two-thirds;
1305
+ // the Mechanics column stands full-height beside it.
1306
+ width: '65%',
624
1307
  // Fixed-row footer: 8 rows of HUD (status box) + 3 rows of input.
625
1308
  height: '100%-11',
626
1309
  border: {
627
1310
  type: 'line'
628
1311
  },
629
- label: ' Game Log ',
1312
+ label: ' Game Log (PgUp/PgDn to scroll) ',
630
1313
  scrollable: true, // Enable scrolling
631
1314
  alwaysScroll: true,
632
1315
  scrollbar: {
@@ -639,13 +1322,90 @@ export default class Game {
639
1322
  }
640
1323
  },
641
1324
  keys: true, // Enable keyboard input
642
- mouse: true, // Enable mouse input
1325
+ // NO mouse (see initInputBox): never arm mouse tracking. The log
1326
+ // auto-scrolls (alwaysScroll) and keys/vi still scroll it when
1327
+ // focused -- wheel-scroll wasn't worth shells full of coordinate
1328
+ // bytes every time a session died hard.
643
1329
  tags: true // Intercept tags
644
1330
  });
645
1331
  this._logBox = logBox;
646
- logBox.removeAllListeners('mousemove');
1332
+ // Scrolling without focus (player report: "can't scroll the game log
1333
+ // anymore"): focus lives in the input box on every path, so the
1334
+ // box's own keys never fire, and the wheel died with mouse tracking.
1335
+ // SCREEN-level PgUp/PgDn scroll the log while you keep typing; while
1336
+ // you're up in history, followBottom stops every new line from
1337
+ // yanking the view back down (Logger.log and ui.ts honor it).
1338
+ // Paging back to the bottom -- or End -- re-engages the follow.
1339
+ logBox.followBottom = true;
1340
+ const page = () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : 20) - 3);
1341
+ const setFollow = (on) => {
1342
+ logBox.followBottom = on;
1343
+ logBox.setLabel(on ? ' Game Log (PgUp/PgDn to scroll) ' : ' Game Log -- SCROLLED (End to follow) ');
1344
+ };
1345
+ const atBottom = () => logBox.getScrollPerc() >= 100 || logBox.getScrollHeight() <= page() + 3;
1346
+ screen.key(['pageup'], () => {
1347
+ logBox.scroll(-page());
1348
+ setFollow(false);
1349
+ screen.render();
1350
+ });
1351
+ screen.key(['pagedown'], () => {
1352
+ logBox.scroll(page());
1353
+ if (atBottom())
1354
+ setFollow(true);
1355
+ screen.render();
1356
+ });
1357
+ screen.key(['end'], () => {
1358
+ logBox.scrollTo(logBox.getScrollHeight());
1359
+ setFollow(true);
1360
+ screen.render();
1361
+ });
647
1362
  screen.append(logBox);
648
1363
  }
1364
+ /**
1365
+ * Scroll the log to its tail -- unless the player has paged up to read
1366
+ * history (followBottom off), in which case their place is left alone.
1367
+ * Every "new line arrived" site goes through here.
1368
+ */
1369
+ followLog() {
1370
+ const box = this._logBox;
1371
+ if (!box || box.followBottom === false)
1372
+ return;
1373
+ box.scrollTo(box.getScrollHeight());
1374
+ }
1375
+ /**
1376
+ * The MECHANICS column (readability pass, player request: the log was
1377
+ * a "wall of text"): dice anatomy and resource arithmetic land here
1378
+ * (via Logger.meta) instead of the world log, so the narrative stays
1379
+ * prose. A full-height COLUMN beside the world log, not a bottom
1380
+ * strip -- the first playtest's 3-row ticker scrolled combat rolls
1381
+ * away faster than they could be read. De-emphasis is the BLUE border
1382
+ * -- gray is banned (ANSI bright-black, illegible on stock dark
1383
+ * palettes). Follow-only (no keys); the full interleaved record lives
1384
+ * in session.log. Roll lines wrap at the column width -- fine in a
1385
+ * tall column. NO mouse (see initInputBox): never arm mouse tracking.
1386
+ */
1387
+ initMetaBox(screen) {
1388
+ const metaBox = blessed.box({
1389
+ id: 'metaBox',
1390
+ top: '0%',
1391
+ left: '65%',
1392
+ width: '35%',
1393
+ // Same height as the world log: HUD (8) + input (3) below both.
1394
+ height: '100%-11',
1395
+ border: {
1396
+ type: 'line'
1397
+ },
1398
+ style: {
1399
+ border: { fg: 'blue' }
1400
+ },
1401
+ label: ' Mechanics ',
1402
+ scrollable: true,
1403
+ alwaysScroll: true,
1404
+ tags: true,
1405
+ });
1406
+ this._metaBox = metaBox;
1407
+ screen.append(metaBox);
1408
+ }
649
1409
  /**
650
1410
  * The HUD: a fixed strip between the log and the input box for the
651
1411
  * important real-time state -- condition tracks, armed/link status,
@@ -762,6 +1522,30 @@ export default class Game {
762
1522
  if (player.activeCallPartner) {
763
1523
  lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.activeCallPartner.name}{/${CALL_COLOR}-fg}`);
764
1524
  }
1525
+ // The party: one line per live companion shell, with the tracks that
1526
+ // matter for its kind. A spirit shows its BODY too (it's a full
1527
+ // Force-statted combatant and hits land on it -- player request:
1528
+ // the track was real but invisible); drones show hull, agents deck.
1529
+ for (const c of this.companions) {
1530
+ const track = c.kind === 'spirit'
1531
+ ? `${c.services ?? 0} service${(c.services ?? 0) === 1 ? '' : 's'} ${c.npc.conditionSummary()}${c.npc.stunTaken > 0 ? ` stun ${c.npc.stunSummary()}` : ''}`
1532
+ : c.kind === 'drone' && c.boundDevice ? `hull ${c.boundDevice.droneSummary()}`
1533
+ : c.kind === 'agent' && c.boundDevice ? `deck ${c.boundDevice.deckSummary()}`
1534
+ : c.npc.conditionSummary();
1535
+ // Each kind speaks its own language (a real rigger session read a
1536
+ // generic "(F3)" as spell Force on a Fly-Spy): drones fly on a
1537
+ // PILOT rating, agents run at a RATING, and a spirit's Force is
1538
+ // already in its name.
1539
+ const grade = c.kind === 'drone' ? `Pilot ${c.force} -- ` : c.kind === 'agent' ? `Rating ${c.force} -- ` : '';
1540
+ lines.push(`{green-fg}👥 ${c.npc.name} -- ${grade}${track}{/green-fg}`);
1541
+ }
1542
+ // DECK-AR INDICATOR (player request): "Link online" is the
1543
+ // COMMLINK's AR overlay; this is the DECK's -- lit when an agent
1544
+ // works the grid while you're back in the meat (the AR-drop exit).
1545
+ const remoteAgent = this.companions.find(c => c.kind === 'agent');
1546
+ if (remoteAgent && player.plane !== 'matrix') {
1547
+ lines.push(`{cyan-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''} ("jack in" to rejoin it, "recall" to fold it home){/cyan-fg}`);
1548
+ }
765
1549
  // Off-plane context: which reality you're in, your persona's Matrix
766
1550
  // track (cold-sim damage lands there), and where your empty body lies.
767
1551
  if (player.plane === 'matrix') {
@@ -776,6 +1560,15 @@ export default class Game {
776
1560
  else if (player.plane === 'astral') {
777
1561
  lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
778
1562
  }
1563
+ else if (player.plane === 'drone') {
1564
+ // Jumped-in: the HULL is the track that matters (attacks land there,
1565
+ // half bleeding through the link), so it lives on the HUD alongside
1566
+ // where the empty body was left.
1567
+ const drone = player.riggedDrone;
1568
+ const hull = drone ? ` Hull ${drone.droneSummary()}` : '';
1569
+ const mount = player.droneArmed ? ' ⌖ armed' : ' (recon -- no mounts)';
1570
+ lines.push(`{yellow-fg}✈ JUMPED IN: ${drone?.name ?? 'drone'} ${(player.simMode ?? 'cold').toUpperCase()}-SIM${mount}${hull} -- body at ${player.bodyRoom?.name ?? '???'}{/yellow-fg}`);
1571
+ }
779
1572
  this._statusBox.setContent(lines.join('\n'));
780
1573
  this._screen.render();
781
1574
  }
@@ -860,7 +1653,10 @@ export default class Game {
860
1653
  return `I don't understand "${verb}" -- did you mean "${guess}"?`;
861
1654
  }
862
1655
  const corrected = await this.handleInput([guess, ...args].join(' '));
863
- return corrected === null ? null : `(read "${verb}" as "${guess}")\n${corrected ?? ''}`;
1656
+ // The correction note is bookkeeping -- Mechanics ticker, not the
1657
+ // narrative (readability pass).
1658
+ Logger.getInstance().meta(`(read "${verb}" as "${guess}")`);
1659
+ return corrected;
864
1660
  }
865
1661
  return `I don't understand the command "${verb}".`;
866
1662
  }
@@ -876,12 +1672,13 @@ export default class Game {
876
1672
  static HELP_CATEGORIES = [
877
1673
  { title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
878
1674
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
879
- { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['stance'], ['surrender'], ['edge'], ['rest']] },
880
- { title: 'magic', entries: [['spells'], ['cast'], ['project'], ['return'], ['assense']] },
881
- { title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['drone']] },
882
- { title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['haggle', 'negotiate'], ['persuade', 'convince']] },
1675
+ { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['order'], ['stance'], ['surrender'], ['edge'], ['rest']] },
1676
+ { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
1677
+ { title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['deploy'], ['recall'], ['jump', 'rig']] },
1678
+ { title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
1679
+ { title: 'street', entries: [['browse', 'buy'], ['sell'], ['work', 'gig'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
883
1680
  { title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
884
- { title: 'character', entries: [['sheet', 'stats'], ['advance', 'train']] },
1681
+ { title: 'character', entries: [['sheet', 'stats'], ['advance', 'train'], ['qualities', 'quality'], ['initiate', 'submerge'], ['install', 'chrome']] },
885
1682
  { title: 'session', entries: [['help'], ['clear'], ['quit', 'exit']] },
886
1683
  ];
887
1684
  /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
@@ -910,7 +1707,7 @@ export default class Game {
910
1707
  const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
911
1708
  const lines = [`=== ${category.title.toUpperCase()} ===`];
912
1709
  for (const [primary, ...aliases] of category.entries) {
913
- lines.push(`- ${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
1710
+ lines.push(`${BULLET}${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
914
1711
  }
915
1712
  return lines.join('\n');
916
1713
  }