@maka/maka-cli 5.3.17 → 5.5.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 (245) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +62 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +196 -0
  4. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.d.ts +16 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +65 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.d.ts +16 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +122 -0
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js.map +1 -0
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.d.ts +42 -0
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +243 -0
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.d.ts +16 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +39 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.d.ts +21 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js +82 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js.map +1 -0
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +28 -0
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +101 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +19 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +219 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.d.ts +58 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +397 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.d.ts +11 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js +8 -3
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/command.d.ts +6 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js +4 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.d.ts +15 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +72 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +3 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.d.ts +13 -0
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js +31 -0
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/edge.js.map +1 -0
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.d.ts +6 -0
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +27 -0
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js.map +1 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.d.ts +4 -0
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +59 -2
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.d.ts +13 -0
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +50 -0
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -0
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +7 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +13 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +147 -4
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +25 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +199 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -0
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.d.ts +15 -0
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +75 -0
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -0
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.d.ts +12 -0
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +43 -0
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.d.ts +17 -0
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +64 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js.map +1 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/kill.d.ts +10 -3
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/kill.js +11 -33
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/kill.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +47 -4
  73. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/commands/map.d.ts +12 -0
  75. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +46 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -0
  77. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.d.ts +12 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +55 -0
  79. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.d.ts +18 -0
  81. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +100 -0
  82. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +10 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +33 -0
  85. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -0
  86. package/bundle/typescript/src/commands/game/sideQuest/commands/project.d.ts +19 -0
  87. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +54 -0
  88. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -0
  89. package/bundle/typescript/src/commands/game/sideQuest/commands/read.js +6 -5
  90. package/bundle/typescript/src/commands/game/sideQuest/commands/read.js.map +1 -1
  91. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.d.ts +12 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js +52 -0
  93. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js.map +1 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.d.ts +14 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +40 -0
  96. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js.map +1 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.d.ts +15 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +59 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +31 -2
  101. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  102. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +71 -7
  103. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.d.ts +12 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +58 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.d.ts +13 -0
  108. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +50 -0
  109. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -0
  110. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.d.ts +15 -0
  111. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +35 -0
  112. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js.map +1 -0
  113. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js +32 -11
  114. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js.map +1 -1
  115. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.d.ts +14 -0
  116. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +86 -0
  117. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -0
  118. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.d.ts +12 -0
  119. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +41 -0
  120. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -0
  121. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.d.ts +21 -0
  122. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +43 -0
  123. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js.map +1 -0
  124. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.d.ts +20 -0
  125. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +45 -0
  126. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js.map +1 -0
  127. package/bundle/typescript/src/commands/game/sideQuest/commands/take.d.ts +8 -0
  128. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +138 -2
  129. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  130. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +8 -0
  131. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
  132. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +6 -3
  133. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js.map +1 -1
  134. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +1 -0
  135. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  136. package/bundle/typescript/src/commands/game/sideQuest/commands/use.d.ts +24 -0
  137. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +102 -1
  138. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  139. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js +2 -1
  140. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js.map +1 -1
  141. package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js +7 -2
  142. package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js.map +1 -1
  143. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js +2 -1
  144. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js.map +1 -1
  145. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +3 -0
  146. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
  147. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +175 -0
  148. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +673 -0
  149. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -0
  150. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.d.ts +8 -1
  151. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +139 -21
  152. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  153. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +174 -0
  154. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +647 -0
  155. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -0
  156. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +138 -9
  157. package/bundle/typescript/src/commands/game/sideQuest/game.js +757 -197
  158. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  159. package/bundle/typescript/src/commands/game/sideQuest/minigames/hack-minigame.d.ts +16 -0
  160. package/bundle/typescript/src/commands/game/sideQuest/minigames/hack-minigame.js +101 -0
  161. package/bundle/typescript/src/commands/game/sideQuest/minigames/hack-minigame.js.map +1 -0
  162. package/bundle/typescript/src/commands/game/sideQuest/minigames/lockpick-minigame.d.ts +15 -0
  163. package/bundle/typescript/src/commands/game/sideQuest/minigames/lockpick-minigame.js +95 -0
  164. package/bundle/typescript/src/commands/game/sideQuest/minigames/lockpick-minigame.js.map +1 -0
  165. package/bundle/typescript/src/commands/game/sideQuest/models/door.d.ts +10 -0
  166. package/bundle/typescript/src/commands/game/sideQuest/models/door.js +15 -2
  167. package/bundle/typescript/src/commands/game/sideQuest/models/door.js.map +1 -1
  168. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.d.ts +10 -0
  169. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js +54 -4
  170. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js.map +1 -1
  171. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +41 -0
  172. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +106 -3
  173. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  174. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +27 -0
  175. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +312 -50
  176. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  177. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +278 -0
  178. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +661 -9
  179. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  180. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +75 -1
  181. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +199 -6
  182. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  183. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +24 -0
  184. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +53 -18
  185. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  186. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +73 -0
  187. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +208 -3
  188. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  189. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +319 -70
  190. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +661 -227
  191. package/bundle/typescript/src/commands/game/sideQuest/types/player-equipment.d.ts +1 -0
  192. package/bundle/typescript/src/commands/game/sideQuest/types/seed/exit-seed.d.ts +1 -0
  193. package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
  194. package/bundle/typescript/src/commands/game/sideQuest/types/seed/npc-seed.d.ts +1 -0
  195. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +42 -0
  196. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +15 -1
  197. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +4 -0
  198. package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +8 -0
  199. package/bundle/typescript/src/commands/game/sideQuest/types/seed/win-condition-seed.d.ts +15 -0
  200. package/bundle/typescript/src/commands/game/sideQuest/types/seed/win-condition-seed.js +2 -0
  201. package/bundle/typescript/src/commands/game/sideQuest/types/seed/win-condition-seed.js.map +1 -0
  202. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.d.ts +1 -0
  203. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.js +5 -0
  204. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.js.map +1 -1
  205. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.d.ts +1 -0
  206. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js +1 -0
  207. package/bundle/typescript/src/commands/game/sideQuest/types/shared/item-enum.js.map +1 -1
  208. package/bundle/typescript/src/commands/game/sideQuest/ui.d.ts +3 -0
  209. package/bundle/typescript/src/commands/game/sideQuest/ui.js +95 -6
  210. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  211. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.d.ts +60 -0
  212. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +138 -0
  213. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -0
  214. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +145 -0
  215. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +354 -0
  216. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -0
  217. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.d.ts +2 -0
  218. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +12 -0
  219. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js.map +1 -0
  220. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +22 -0
  221. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +34 -0
  222. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -0
  223. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.d.ts +36 -0
  224. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js +92 -0
  225. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js.map +1 -0
  226. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +77 -0
  227. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +265 -0
  228. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -0
  229. package/bundle/typescript/src/commands/game/sideQuest/utilities/pursuit.d.ts +16 -0
  230. package/bundle/typescript/src/commands/game/sideQuest/utilities/pursuit.js +36 -0
  231. package/bundle/typescript/src/commands/game/sideQuest/utilities/pursuit.js.map +1 -0
  232. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +78 -7
  233. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  234. package/bundle/typescript/src/commands/set/global.sub.cmd.js +59 -5
  235. package/bundle/typescript/src/commands/set/global.sub.cmd.js.map +1 -1
  236. package/bundle/typescript/src/tools/ai/ai.class.d.ts +42 -0
  237. package/bundle/typescript/src/tools/ai/ai.class.js +117 -0
  238. package/bundle/typescript/src/tools/ai/ai.class.js.map +1 -0
  239. package/bundle/typescript/src/tools/fsi/fsi.interfaces.d.ts +1 -0
  240. package/bundle/typescript/src/tools/fsi/fsi.interfaces.js.map +1 -1
  241. package/bundle/typescript/src/tools/https/https.class.d.ts +3 -2
  242. package/bundle/typescript/src/tools/https/https.class.js +51 -2
  243. package/bundle/typescript/src/tools/https/https.class.js.map +1 -1
  244. package/bundle/typescript/src/tools/https/https.interface.d.ts +10 -0
  245. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import blessed from 'blessed';
2
2
  import { capitalCase } from 'change-case';
3
- import { Door } from './models/_index.js';
3
+ import { Room } from './models/_index.js';
4
+ import { Direction } from './types/shared/direction-enum.js';
5
+ import { Logger } from './utilities/logger.js';
4
6
  import { LookCommand } from './commands/look.js';
5
7
  import { TakeCommand } from './commands/take.js';
6
8
  import { GoCommand } from './commands/go.js';
@@ -18,20 +20,166 @@ import { StoreCommand } from './commands/store.js';
18
20
  import { GiveCommand } from './commands/give.js';
19
21
  import { ReadCommand } from './commands/read.js';
20
22
  import { KillCommand } from './commands/kill.js';
23
+ import { AttackCommand } from './commands/attack.js';
24
+ import { SpellsCommand } from './commands/spells.js';
25
+ import { InventoryCommand } from './commands/inv.js';
26
+ import { EquipmentCommand } from './commands/equipment.js';
27
+ import { MapCommand } from './commands/map.js';
28
+ import { SilentCommand } from './commands/silent.js';
29
+ import { RestCommand } from './commands/rest.js';
30
+ import { ReloadCommand } from './commands/reload.js';
31
+ import { SubdueCommand } from './commands/subdue.js';
32
+ import { EdgeCommand } from './commands/edge.js';
33
+ import { SheetCommand } from './commands/sheet.js';
34
+ import { CallCommand } from './commands/call.js';
35
+ import { EndCallCommand } from './commands/end-call.js';
36
+ import { fuzzyPickName, levenshteinDistance } from './utilities/fuzzy-match.js';
37
+ import { PickCommand } from './commands/pick.js';
38
+ import { HackCommand } from './commands/hack.js';
39
+ import { CastCommand } from './commands/cast.js';
40
+ import { StanceCommand } from './commands/stance.js';
41
+ import { SurrenderCommand } from './commands/surrender.js';
42
+ import { JackCommand } from './commands/jack.js';
43
+ import { ProjectCommand, ReturnCommand } from './commands/project.js';
44
+ import { AssenseCommand } from './commands/assense.js';
45
+ import { DroneCommand } from './commands/drone.js';
46
+ import { PanCommand } from './commands/pan.js';
47
+ import { RebootCommand } from './commands/reboot.js';
48
+ import { BrandishCommand, HolsterCommand } from './commands/brandish.js';
49
+ import { AdvanceCommand } from './commands/advance.js';
50
+ import { equipIntoEmptySlots } from './archetypes.js';
51
+ import { NPC } from './models/npc.js';
52
+ import { HaggleCommand } from './commands/haggle.js';
53
+ import { SneakCommand } from './commands/sneak.js';
54
+ import { PersuadeCommand } from './commands/persuade.js';
55
+ import { BluffCommand } from './commands/bluff.js';
56
+ import { AutoFight } from './utilities/auto-fight.js';
21
57
  import { CommandFactory } from './factories/command-factory.js';
22
58
  import { SceneSynthesizer } from './factories/scene-factory.js';
59
+ import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator.js';
60
+ import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
23
61
  export default class Game {
24
62
  initialized;
25
63
  static instance;
26
64
  sceneSeed;
27
65
  scene;
28
66
  player;
67
+ // The fight autopilot (Tab / "stance" command) -- created once, reads
68
+ // live game state at fire time so it survives scene transitions.
69
+ autoFight = new AutoFight(this);
70
+ // Just three surfaces: a full-width log, a HUD strip, and the input box.
71
+ // The old always-on Inventory/Equipment/Map sidebar boxes became the
72
+ // on-demand inv/equipment/map commands (see commands/inv.ts et al.) to
73
+ // give the log the whole screen.
29
74
  _screen;
30
- _exitsBox;
31
- _inventoryBox;
32
75
  _logBox;
76
+ _statusBox;
33
77
  _inputBox;
34
- _equipmentBox;
78
+ _useAi = false;
79
+ _logFilePath = '';
80
+ _tier = 1;
81
+ // Guards against two overlapping continueToNextScene() calls -- found via
82
+ // a real session where calling the fixer twice in a row (nothing stopped
83
+ // it, and a 20-40+ second draft invites a second, impatient call) fired
84
+ // two independent drafts at once. Both read the same starting _tier
85
+ // before either finished, both generated a "tier 2" job, and whichever
86
+ // one finished last silently overwrote the first one's scene, room, and
87
+ // player-location state out from under whatever the player was already
88
+ // doing in it.
89
+ _draftInProgress = false;
90
+ // The HUB scene, when this session started in one (a seed with
91
+ // "hub": true -- the persistent home base, e.g. Tacoma Purple Haze).
92
+ // Kept as the live Scene INSTANCE, same pattern as _homeRoom, so
93
+ // everything in it -- NPC state, shop stock, dropped loot -- persists
94
+ // untouched while runs play out, and returnToHub() just re-points the
95
+ // world at it. undefined for classic sessions (test scene, launch-
96
+ // generated runs), which keep the old continue->draft loop.
97
+ _hubScene;
98
+ _hubName = 'home';
99
+ get hasHub() {
100
+ return this._hubScene !== undefined;
101
+ }
102
+ /** The persistent hideout room -- go.ts's walk-home trigger checks it. */
103
+ get homeRoom() {
104
+ return this._homeRoom;
105
+ }
106
+ // A drafted run waiting for the player to take it (hub sessions only):
107
+ // generation finishing sends a MESSAGE instead of teleporting anyone --
108
+ // "call Mr. Johnson" hears the offer, "travel" goes.
109
+ _pendingRunSeed;
110
+ _pendingRunTier;
111
+ get pendingRun() {
112
+ return this._pendingRunSeed;
113
+ }
114
+ get pendingRunTier() {
115
+ return this._pendingRunTier;
116
+ }
117
+ /**
118
+ * The cab that answers "call taxi" varies per job (player request: not
119
+ * always a van) -- picked once when the pending run lands, so the
120
+ * arrival and the ride read as ONE vehicle.
121
+ */
122
+ static RIDE_POOL = [
123
+ {
124
+ arrive: `A matte-black step-van splashes to the curb in front of you, side door already sliding open.`,
125
+ board: `The door rattles shut and the Haze smears past the grimy windows.`,
126
+ },
127
+ {
128
+ arrive: `A pearl-gray Westwind glides up out of the rain, gullwing lifting -- somebody's billing this to the client.`,
129
+ board: `The gullwing sighs closed and the launch pins you into leather that costs more than your rent.`,
130
+ },
131
+ {
132
+ arrive: `A patchwork gypsy cab rattles around the corner and stops dead in front of you, meter torn out, driver watching the mirrors.`,
133
+ board: `The cabbie pulls off before the door's even latched, humming something older than the district.`,
134
+ },
135
+ {
136
+ arrive: `A rust-eaten GMC Bulldog bulls through traffic and mounts the curb beside you, panel doors ajar.`,
137
+ board: `The Bulldog coughs, catches, and shoves into traffic like it's owed something.`,
138
+ },
139
+ {
140
+ arrive: `An unmarked ambulance rolls up silent, lights dead, back doors swinging open -- nobody looks twice at it.`,
141
+ board: `You ride out flat on the gurney, ceiling lights strobing past the little windows.`,
142
+ },
143
+ {
144
+ arrive: `A growling Harley with a reinforced sidecar pulls a U-turn and stops at your boots, rider jerking a thumb at the car.`,
145
+ board: `You drop into the sidecar; the pipes drown the whole district out.`,
146
+ },
147
+ ];
148
+ _pendingRide;
149
+ /** The pending job's cab flavor -- "call taxi" narrates with it (see call.ts). */
150
+ get pendingRide() {
151
+ return this._pendingRide;
152
+ }
153
+ /**
154
+ * Generates NPC first-impression appearances ("look at <npc>") in the
155
+ * background, one at a time -- the only AI dressing still worth doing
156
+ * now that every scene arrives with real prose. Fire-and-forget: until
157
+ * an appearance lands, look-at just shows a bit less flavor.
158
+ */
159
+ warmNpcAppearancesInBackground() {
160
+ if (!this._useAi)
161
+ return;
162
+ const npcs = this.scene.allActors.filter((a) => a instanceof NPC);
163
+ const context = `${this.sceneSeed?.name ?? ''} -- ${this.scene.story}`;
164
+ void (async () => {
165
+ for (const npc of npcs) {
166
+ try {
167
+ await npc.ensureAIAppearance(context);
168
+ }
169
+ catch {
170
+ // Non-fatal: the next look-at simply has less flavor.
171
+ }
172
+ }
173
+ })();
174
+ }
175
+ // A single fixed room, created once and never torn down across job
176
+ // transitions -- the player's own place, unlike every job's generated
177
+ // rooms which get discarded wholesale when the next job replaces them.
178
+ _homeRoom;
179
+ _homeRoomExitDirection;
180
+ static HOME_DIRECTION_PRIORITY = [
181
+ Direction.DOWN, Direction.UP, Direction.SOUTH, Direction.NORTH, Direction.EAST, Direction.WEST,
182
+ ];
35
183
  constructor(player, sceneSeed) {
36
184
  this.player = player;
37
185
  this.sceneSeed = sceneSeed;
@@ -55,10 +203,81 @@ export default class Game {
55
203
  CommandFactory.registerCommand('give', GiveCommand);
56
204
  CommandFactory.registerCommand('read', ReadCommand);
57
205
  CommandFactory.registerCommand('kill', KillCommand);
206
+ CommandFactory.registerCommand('attack', AttackCommand);
207
+ CommandFactory.registerCommand('spells', SpellsCommand);
208
+ CommandFactory.registerCommand('inv', InventoryCommand);
209
+ CommandFactory.registerCommand('inventory', InventoryCommand);
210
+ CommandFactory.registerCommand('equipment', EquipmentCommand);
211
+ CommandFactory.registerCommand('eq', EquipmentCommand);
212
+ CommandFactory.registerCommand('map', MapCommand);
213
+ CommandFactory.registerCommand('exits', MapCommand);
214
+ CommandFactory.registerCommand('silent', SilentCommand);
215
+ CommandFactory.registerCommand('rest', RestCommand);
216
+ CommandFactory.registerCommand('reload', ReloadCommand);
217
+ CommandFactory.registerCommand('subdue', SubdueCommand);
218
+ CommandFactory.registerCommand('edge', EdgeCommand);
219
+ CommandFactory.registerCommand('sheet', SheetCommand);
220
+ CommandFactory.registerCommand('stats', SheetCommand);
221
+ CommandFactory.registerCommand('call', CallCommand);
222
+ CommandFactory.registerCommand('end-call', EndCallCommand);
223
+ CommandFactory.registerCommand('pick', PickCommand);
224
+ CommandFactory.registerCommand('hack', HackCommand);
225
+ CommandFactory.registerCommand('cast', CastCommand);
226
+ CommandFactory.registerCommand('stance', StanceCommand);
227
+ CommandFactory.registerCommand('surrender', SurrenderCommand);
228
+ CommandFactory.registerCommand('jack', JackCommand);
229
+ CommandFactory.registerCommand('project', ProjectCommand);
230
+ CommandFactory.registerCommand('return', ReturnCommand);
231
+ CommandFactory.registerCommand('assense', AssenseCommand);
232
+ CommandFactory.registerCommand('drone', DroneCommand);
233
+ CommandFactory.registerCommand('pan', PanCommand);
234
+ CommandFactory.registerCommand('reboot', RebootCommand);
235
+ CommandFactory.registerCommand('brandish', BrandishCommand);
236
+ CommandFactory.registerCommand('draw', BrandishCommand);
237
+ CommandFactory.registerCommand('holster', HolsterCommand);
238
+ CommandFactory.registerCommand('stow', HolsterCommand);
239
+ // "end" and "hangup" both bounced in real sessions when players
240
+ // wanted to end a call -- the intent is unambiguous mid-call.
241
+ CommandFactory.registerCommand('end', EndCallCommand);
242
+ CommandFactory.registerCommand('hangup', EndCallCommand);
243
+ CommandFactory.registerCommand('advance', AdvanceCommand);
244
+ CommandFactory.registerCommand('train', AdvanceCommand);
245
+ CommandFactory.registerCommand('haggle', HaggleCommand);
246
+ CommandFactory.registerCommand('negotiate', HaggleCommand);
247
+ CommandFactory.registerCommand('sneak', SneakCommand);
248
+ CommandFactory.registerCommand('persuade', PersuadeCommand);
249
+ CommandFactory.registerCommand('convince', PersuadeCommand);
250
+ CommandFactory.registerCommand('bluff', BluffCommand);
58
251
  }
59
252
  determineStartRoom() {
60
253
  return this.scene.determineStartRoom();
61
254
  }
255
+ /**
256
+ * Wires a fresh, always-unlocked path between the persistent home base
257
+ * and wherever the player is starting the current job from. Clears the
258
+ * previous job's connection first (if any) so the home base never keeps
259
+ * a stale door into an abandoned scene's now-orphaned rooms -- only ever
260
+ * one way out at a time, pointing at whichever job is actually current.
261
+ */
262
+ connectHomeRoom(jobStartRoom) {
263
+ if (this._homeRoomExitDirection) {
264
+ this._homeRoom.exits.delete(this._homeRoomExitDirection);
265
+ }
266
+ const direction = Game.HOME_DIRECTION_PRIORITY.find(d => !jobStartRoom.exits.has(d));
267
+ if (!direction) {
268
+ // Every direction is already spoken for on this job's start room --
269
+ // rare (most generated start rooms use 1-2 exits), but leave the
270
+ // player without a home-base link this job rather than clobbering
271
+ // one of the job's own exits.
272
+ this._homeRoomExitDirection = undefined;
273
+ return;
274
+ }
275
+ jobStartRoom.addExit(direction, this._homeRoom);
276
+ // Tracked from the HOME room's own side (the opposite of `direction`,
277
+ // which is the job room's side) -- that's the key actually stored on
278
+ // this._homeRoom.exits, and what gets cleared above next time around.
279
+ this._homeRoomExitDirection = jobStartRoom.getOppositeDirection(direction);
280
+ }
62
281
  get logBox() {
63
282
  return this._logBox;
64
283
  }
@@ -66,43 +285,288 @@ export default class Game {
66
285
  return this._inputBox;
67
286
  }
68
287
  async init(options, screen) {
69
- this.initMap(screen);
288
+ this._useAi = options.useAi;
289
+ this._logFilePath = options.logFilePath;
70
290
  this.initInputBox(screen);
71
291
  this.initLogBox(screen);
72
- this.initEquipmentBox(screen);
73
- this.initInventoryBox(screen);
74
- // Create a loading box
75
- const loadingBox = blessed.box({
76
- parent: screen,
77
- top: 'center',
78
- left: 'center',
79
- width: '50%',
80
- height: 'shrink',
81
- content: 'Generating Scene... Please wait.',
82
- border: { type: 'line' },
83
- style: {
84
- fg: 'white',
85
- bg: 'black',
86
- border: { fg: 'cyan' },
87
- }
88
- });
89
- screen.append(loadingBox);
90
- screen.render();
292
+ this.initStatusBox(screen);
293
+ // No "Generating Scene... Please wait." box anymore: every path now
294
+ // arrives with REAL prose (hand-authored static seeds, chunk-authored
295
+ // generated scenes), so the old blocking AI-redressing pass -- 30-60
296
+ // seconds rewriting text that was already good -- is skipped and the
297
+ // game opens instantly. The one thing still worth generating (NPC
298
+ // first-impression appearances for "look at") warms in the background
299
+ // below.
91
300
  try {
92
- this.scene = await SceneSynthesizer.synthesizeFromJson(this.sceneSeed, this.player, options.useAi);
301
+ this.scene = await SceneSynthesizer.synthesizeFromJson(this.sceneSeed, this.player, false);
93
302
  }
94
303
  catch (err) {
95
304
  throw new Error(`Scene generation failed: ${err.message}`);
96
305
  }
97
- // Remove loading box
98
- screen.remove(loadingBox);
99
- screen.render();
100
306
  this.scene.initialize(this);
101
307
  this.player.currentLocation = this.determineStartRoom();
102
308
  this.registerCommands();
103
309
  this._screen = screen;
310
+ this._homeRoom = new Room({
311
+ 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.`,
313
+ roomType: 'Residence',
314
+ });
315
+ this.scene.addRoom(this._homeRoom);
316
+ this.connectHomeRoom(this.player.currentLocation);
317
+ // Every session starts AT HOME -- the hideout, one door from the job's
318
+ // start room (player request, left as an in-game note: "I'd like to
319
+ // always start in my hideout"). The scene's isStartRoom still anchors
320
+ // where the hideout's door leads; the runner just wakes up on their
321
+ // own cot instead of already standing in the job.
322
+ this.player.currentLocation = this._homeRoom;
323
+ // Starting in a hub makes this a hub SESSION: runs are commissioned
324
+ // by calling the fixer from here, and completing one returns here.
325
+ if (this.scene.isHub) {
326
+ this._hubScene = this.scene;
327
+ this._hubName = this.sceneSeed?.name ?? 'home';
328
+ }
329
+ // Gear up automatically: wearables (and the commlink) slot into any
330
+ // EMPTY slots at load -- no "equip all" ritual to start a session
331
+ // (player request). Occupied slots (archetype signature gear) are
332
+ // never swapped out.
333
+ equipIntoEmptySlots(this.player);
334
+ this.warmNpcAppearancesInBackground();
104
335
  this.initialized = true;
105
- return `Welcome, ${this.player.name}\n ${this.scene.story}`;
336
+ // Nudge the player toward "call" up front -- the fixer convention means
337
+ // the story deliberately doesn't narrate a completed briefing, so
338
+ // without this hint there'd be no in-game indication that "call" is how
339
+ // you actually get your orders.
340
+ const wc = this.scene.getWinCondition();
341
+ const callHint = (wc?.type === 'give' && wc.toNpc)
342
+ ? `\n\nYour commlink is quiet for now, but ${wc.toNpc} is expecting to hear from you -- try "call ${wc.toNpc}" when you're ready to check in.`
343
+ : '';
344
+ return `Welcome, ${this.player.name}\n ${this.scene.story}${callHint}`;
345
+ }
346
+ /**
347
+ * Synchronous readiness check for continueToNextScene() -- lets callers
348
+ * give instant feedback for the "not ready yet" cases (job not done, no
349
+ * AI configured) without waiting on anything async.
350
+ */
351
+ canDraftNextScene() {
352
+ if (this._draftInProgress) {
353
+ return { allowed: false, message: `Still lining it up -- I'll ping you.` };
354
+ }
355
+ if (this._pendingRunSeed) {
356
+ return { allowed: false, message: `You've got my message already. Call a taxi when you're ready to roll.` };
357
+ }
358
+ // A hub is ALWAYS ready to commission the next run -- it has no win
359
+ // condition to complete; that's the whole point of home.
360
+ if (!this.scene.isHub && !this.scene.isCompleted()) {
361
+ return { allowed: false, message: `This job isn't finished yet.` };
362
+ }
363
+ if (!this._useAi) {
364
+ return { allowed: false, message: `Continuing needs an AI-generated job, which requires an AI provider to be configured (OPENAI_API_KEY or ANTHROPIC_API_KEY).` };
365
+ }
366
+ return { allowed: true };
367
+ }
368
+ /**
369
+ * Fires continueToNextScene() without waiting on it -- job generation can
370
+ * take 20-40+ seconds, and neither "continue" nor calling the fixer
371
+ * should freeze the rest of the session behind that. Self-announces the
372
+ * result once it resolves, styled the same as any other comms exchange,
373
+ * since by design nothing else is holding a reference to display it.
374
+ * Public: called directly by call.ts when the player calls the fixer
375
+ * after a job's done, not just from "continue" here in game.ts.
376
+ */
377
+ draftNextSceneInBackground() {
378
+ void this.continueToNextScene()
379
+ .then(nextJobText => {
380
+ if (nextJobText) {
381
+ Logger.getInstance().logWithColor(`${CALL_ICON} ${nextJobText}`, CALL_COLOR);
382
+ }
383
+ })
384
+ .catch(err => {
385
+ Logger.getInstance().error(`Failed to draft the next job: ${err}`);
386
+ });
387
+ }
388
+ /**
389
+ * Drafts and loads the next job once the current scene's win condition
390
+ * has been met. Reuses the same long-lived Player (and its inventory),
391
+ * so gear compounds across the session -- nothing is persisted to disk,
392
+ * it just carries forward for the life of this process. Re-checks
393
+ * canDraftNextScene() itself (callers are expected to check it first for
394
+ * instant feedback, but this stays safe to call directly too).
395
+ */
396
+ 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()) {
402
+ return this.returnToHub();
403
+ }
404
+ const gate = this.canDraftNextScene();
405
+ if (!gate.allowed) {
406
+ return gate.message ?? `This job isn't finished yet.`;
407
+ }
408
+ this._draftInProgress = true;
409
+ try {
410
+ return await this.draftNextScene();
411
+ }
412
+ finally {
413
+ this._draftInProgress = false;
414
+ }
415
+ }
416
+ /**
417
+ * Re-points the world at the preserved hub scene after a run: the hub's
418
+ * whole state (NPCs, shop stock, anything dropped there) survived
419
+ * untouched because the INSTANCE survived. The player walks back in
420
+ * through their own hideout, firmly in the meat -- any lingering run
421
+ * state (a persona still jacked in, an astral projection, a sneak,
422
+ * open combat bookkeeping) is settled on the way home.
423
+ */
424
+ returnToHub() {
425
+ const hub = this._hubScene;
426
+ if (!hub)
427
+ return `There's no home to go back to.`;
428
+ // Settle cross-plane and combat state that must not leak into the hub
429
+ // (the run's rooms -- including a bodyRoom the player left behind --
430
+ // are about to be orphaned wholesale).
431
+ this.player.plane = 'meat';
432
+ this.player.bodyRoom = undefined;
433
+ this.player.simMode = undefined;
434
+ this.player.astralTicks = 0;
435
+ this.player.activeDeck = undefined;
436
+ this.player.astralPerceiving = false;
437
+ this.player.sneaking = false;
438
+ this.player.inExchange = false;
439
+ this.player.combatOpponent = undefined;
440
+ this.player.activeCallPartner = undefined;
441
+ this.autoFight.cancelPending();
442
+ this.scene = hub;
443
+ this.scene.addRoom(this._homeRoom);
444
+ this.connectHomeRoom(hub.determineStartRoom());
445
+ this.player.currentLocation = this._homeRoom;
446
+ this.player.cameFrom = undefined;
447
+ this.updateEquipment(this.player.equipment);
448
+ this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
449
+ this.updateExits(this.player.currentLocation);
450
+ 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.`;
453
+ }
454
+ /**
455
+ * The actual drafting work, split out from continueToNextScene() purely
456
+ * so that method can wrap it in the try/finally that clears
457
+ * _draftInProgress -- every return path below (including the early
458
+ * "couldn't draft" one) needs that cleanup to run.
459
+ */
460
+ async draftNextScene() {
461
+ const nextTier = this._tier + 1;
462
+ // Carry the current fixer's identity forward -- without this, each
463
+ // "continue" generates a scene with zero knowledge of the last one, so
464
+ // the AI just invents a brand new job-giver every time. Looked up by
465
+ // identity (is there an actor actually named FIXER_NAME?), not via the
466
+ // win condition's toNpc -- that's often a one-off contact the fixer
467
+ // pointed the runner toward, not the fixer himself (see the "broker,
468
+ // not always the destination" convention).
469
+ const fixerStillInScene = this.scene.allActors.some(a => a.name === FIXER_NAME);
470
+ const previousFixerName = fixerStillInScene ? FIXER_NAME : undefined;
471
+ // Append, don't replace -- this used to be setContent(), which wiped
472
+ // the whole log box (including the JOB COMPLETE banner and everything
473
+ // before it) for the entire generation wait, which can run 20-40+
474
+ // seconds for a tiered scene with AI flavor enabled. Framed as the
475
+ // fixer's own line when he's actually in this scene, rather than a
476
+ // bare system message -- he's the one drumming up the next gig.
477
+ // No system "drafting..." line -- the fixer's own one-ring commission
478
+ // (call.ts) already told the player he'll call back; anything more
479
+ // reads as machinery (real playtest note). The wait resolves as HIS
480
+ // callback either way, success or failure.
481
+ //
482
+ // The player object rides along so the job is drafted FOR this runner
483
+ // -- their archetype, stats, and toolkit (see buildPlayerDossier).
484
+ const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, previousFixerName, this.player);
485
+ if (!nextSeed) {
486
+ 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
+ }
488
+ // HUB sessions: the draft lands as a MESSAGE, never a teleport (real
489
+ // playtest note: being yanked into the run the instant generation
490
+ // finished was jarring). The player invokes it on their own time:
491
+ // "call Mr. Johnson" delivers the offer (haggle window included --
492
+ // see call.ts), and "travel" actually goes (launchPendingRun).
493
+ if (this.hasHub) {
494
+ this._pendingRunSeed = nextSeed;
495
+ this._pendingRunTier = nextTier;
496
+ this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
497
+ return `Your link pings -- 1 new message: ${FIXER_NAME}. ("call ${FIXER_NAME}" to hear the job.)`;
498
+ }
499
+ // Classic sessions transition immediately, hideout-first, as always.
500
+ return await this.activateRunScene(nextSeed, nextTier, { startInHideout: true });
501
+ }
502
+ /**
503
+ * Installs a drafted run as the live scene. Shared by the classic
504
+ * immediate path (hideout start) and the hub's "travel" (start-room
505
+ * start -- you rode there, you arrive AT the job).
506
+ */
507
+ async activateRunScene(seed, tier, opts) {
508
+ this._tier = tier;
509
+ this.sceneSeed = seed;
510
+ // A call partner from the old scene wouldn't exist in the new one --
511
+ // hang up before moving on, rather than leave a dangling reference.
512
+ if (this.player.activeCallPartner) {
513
+ this.player.activeCallPartner.activeCallPartner = undefined;
514
+ this.player.activeCallPartner = undefined;
515
+ }
516
+ // false, false: no starter kit (the player carries their own gear
517
+ // forward), and no blocking AI-redressing pass either -- the chunked
518
+ // rooms pass already wrote this scene's prose.
519
+ this.scene = await SceneSynthesizer.synthesizeFromJson(seed, this.player, false, false);
520
+ this.scene.initialize(this);
521
+ 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;
530
+ }
531
+ this.updateEquipment(this.player.equipment);
532
+ this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
533
+ this.updateExits(this.player.currentLocation);
534
+ this.updateStatus();
535
+ // Fresh streets, no trail yet (see Player.cameFrom / the HUD crumb).
536
+ this.player.cameFrom = undefined;
537
+ this.warmNpcAppearancesInBackground();
538
+ return `\n=== JOB #${this._tier}: ${seed.name} ===\n${this.scene.story}`;
539
+ }
540
+ /**
541
+ * "travel": takes the arranged transport to the pending job (see
542
+ * commands/travel.ts). The player arrives AT the run's start room --
543
+ * "You travel to <location>" -- not teleported onto their own cot.
544
+ * A haggle struck during the offer call already mutated the pending
545
+ * seed's payout, and stays spent across the transition (refreshEdge
546
+ * would otherwise re-arm it).
547
+ */
548
+ async launchPendingRun() {
549
+ const seed = this._pendingRunSeed;
550
+ const tier = this._pendingRunTier;
551
+ if (!seed || tier === undefined) {
552
+ return `There's no job waiting. "call ${FIXER_NAME}" to drum one up.`;
553
+ }
554
+ const haggledAtOffer = this.player.haggledThisJob;
555
+ try {
556
+ const banner = await this.activateRunScene(seed, tier, { startInHideout: false });
557
+ this._pendingRunSeed = undefined;
558
+ this._pendingRunTier = undefined;
559
+ this._pendingRide = undefined;
560
+ this.player.haggledThisJob = haggledAtOffer;
561
+ return `You travel to ${this.player.currentLocation.name}.\n${banner}`;
562
+ }
563
+ catch (err) {
564
+ this._pendingRunSeed = undefined;
565
+ this._pendingRunTier = undefined;
566
+ this._pendingRide = undefined;
567
+ Logger.getInstance().error(`launchPendingRun failed: ${err}`);
568
+ return `The ride never shows and the job address goes dead. ${FIXER_NAME} will not enjoy hearing this -- call him for another.`;
569
+ }
106
570
  }
107
571
  async close() {
108
572
  this._inputBox.removeAllListeners(); // removes all event listeners
@@ -113,42 +577,13 @@ export default class Game {
113
577
  process.exit(2);
114
578
  });
115
579
  }
116
- initMap(screen) {
117
- // Map
118
- const exitsBox = blessed.box({
119
- top: '70%',
120
- left: '75%',
121
- width: '25%',
122
- height: '20%',
123
- border: {
124
- type: 'line'
125
- },
126
- label: ' Map ',
127
- content: 'Available exits will be shown here...',
128
- tags: true,
129
- scrollable: true,
130
- scrollbar: {
131
- ch: ' ',
132
- track: {
133
- bg: 'yellow'
134
- },
135
- style: {
136
- inverse: true
137
- }
138
- },
139
- keys: true,
140
- mouse: true,
141
- });
142
- screen.append(exitsBox);
143
- this._exitsBox = exitsBox;
144
- }
145
580
  initInputBox(screen) {
146
581
  // Create an input box for user commands
147
582
  const inputBox = blessed.textbox({
148
583
  bottom: 0,
149
584
  left: 0,
150
585
  width: '100%',
151
- height: '10%',
586
+ height: 3,
152
587
  tags: true,
153
588
  border: { type: 'line' },
154
589
  style: { fg: 'white', border: { fg: 'cyan' } },
@@ -161,8 +596,21 @@ export default class Game {
161
596
  screen.append(inputBox);
162
597
  // Key bindings
163
598
  screen.key(['escape', 'C-c'], () => process.exit(0));
164
- // Focus on the input box by default
165
- inputBox.focus();
599
+ // Focus on the input box by default -- but ONLY if it isn't already
600
+ // focused. screen.append() auto-focuses the first keyable element
601
+ // (Node.insert), and since the sidebar boxes were removed this textbox
602
+ // IS the first one, so it arrives here already focused with its input
603
+ // reader mid-setup. Blessed's _focus() has no same-element guard:
604
+ // focusing an already-focused textbox fires a self-blur that cancels
605
+ // that reader BEFORE its keypress listener finishes attaching (the
606
+ // attach rides a delayed tick), so the cancel removes nothing, a
607
+ // second reader starts, both listeners land -- and every keystroke
608
+ // doubles, from startup ("exit" arrived as "eexxiitt" in a real
609
+ // session). Previously the Map box was appended first and soaked up
610
+ // the auto-focus, which is the only reason this never fired before.
611
+ if (screen.focused !== inputBox) {
612
+ inputBox.focus();
613
+ }
166
614
  // Render the screen
167
615
  screen.render();
168
616
  }
@@ -172,8 +620,9 @@ export default class Game {
172
620
  id: 'logBox',
173
621
  top: '0%',
174
622
  left: '0%',
175
- width: '75%',
176
- height: '90%',
623
+ width: '100%',
624
+ // Fixed-row footer: 8 rows of HUD (status box) + 3 rows of input.
625
+ height: '100%-11',
177
626
  border: {
178
627
  type: 'line'
179
628
  },
@@ -197,139 +646,156 @@ export default class Game {
197
646
  logBox.removeAllListeners('mousemove');
198
647
  screen.append(logBox);
199
648
  }
200
- initEquipmentBox(screen) {
201
- const equipmentBox = blessed.box({
202
- top: '45%',
203
- left: '75%',
204
- width: '25%',
205
- height: '25%',
649
+ /**
650
+ * The HUD: a fixed strip between the log and the input box for the
651
+ * important real-time state -- condition tracks, armed/link status,
652
+ * sustained spells, active call. Anything that changes moment to moment
653
+ * and shouldn't need a command to check lives here; everything slower
654
+ * (full inventory, slot-by-slot equipment, the map) is an on-demand
655
+ * command instead. Future contexts (astral projection, VR hot-sim) get
656
+ * their own line here the same way the call line did: small explicit
657
+ * pieces, not a status framework built ahead of need.
658
+ */
659
+ initStatusBox(screen) {
660
+ const statusBox = blessed.box({
661
+ id: 'statusBox',
662
+ bottom: 3, // sits directly on the input box
663
+ left: '0%',
664
+ width: '100%',
665
+ height: 8, // 6 content rows inside the border: location, tracks,
666
+ // readiness, and up to two transient lines (sustained, call)
206
667
  border: {
207
668
  type: 'line'
208
669
  },
209
- label: ' Equipment ',
210
- content: 'Your equipped items will be listed here...',
211
670
  tags: true,
212
- scrollable: true,
213
- scrollbar: {
214
- ch: ' ',
215
- track: {
216
- bg: 'yellow'
217
- },
218
- style: {
219
- inverse: true
220
- }
221
- },
222
- keys: true,
223
- mouse: true,
671
+ content: '',
224
672
  });
225
- this._equipmentBox = equipmentBox;
226
- // Append the equipment box to the screen
227
- screen.append(equipmentBox);
673
+ this._statusBox = statusBox;
674
+ screen.append(statusBox);
228
675
  }
229
- initInventoryBox(screen) {
230
- const inventoryBox = blessed.box({
231
- top: '0%',
232
- left: '75%',
233
- width: '25%',
234
- height: '45%',
235
- border: {
236
- type: 'line'
237
- },
238
- label: ' Inventory ',
239
- content: 'Your items will be listed here...',
240
- tags: true, // Enable formatting tags in content
241
- scrollable: true, // Enable scrolling if the content overflows
242
- scrollbar: {
243
- ch: ' ',
244
- track: {
245
- bg: 'yellow'
246
- },
247
- style: {
248
- inverse: true
249
- }
250
- },
251
- keys: true,
252
- mouse: true,
253
- });
254
- // Append the inventory box to the screen
255
- screen.append(inventoryBox);
256
- this._inventoryBox = inventoryBox;
257
- }
258
- updateEquipment(playerEquipment) {
259
- const sections = [];
260
- const renderSlotGroup = (label, group) => {
261
- if (!group) {
262
- sections.push(`{bold}${label}:{/bold} None`);
263
- return;
264
- }
265
- const equipped = Object.entries(group)
266
- .filter(([_, item]) => item !== null)
267
- .map(([slot, item]) => ` ${slot}: ${item.name}`);
268
- if (equipped.length > 0) {
269
- sections.push(`{bold}${label}:{/bold}`);
270
- sections.push(...equipped);
271
- }
272
- else {
273
- sections.push(`{bold}${label}:{/bold} None`);
274
- }
275
- };
276
- renderSlotGroup("Head", playerEquipment.head);
277
- renderSlotGroup("Torso", playerEquipment.torso);
278
- renderSlotGroup("Legs", playerEquipment.legs);
279
- renderSlotGroup("Feet", playerEquipment.feet);
280
- renderSlotGroup("Arms", playerEquipment.arms);
281
- renderSlotGroup("Gloves", playerEquipment.gloves);
282
- renderSlotGroup("Left Hand", playerEquipment.leftHand);
283
- renderSlotGroup("Right Hand", playerEquipment.rightHand);
284
- this._equipmentBox.setContent(sections.join('\n'));
285
- this._screen.render();
286
- }
287
- ;
288
- updateExits(room) {
289
- let description = `Currently in: ${room.name}\n\n`;
290
- description += `----- Exits ------\n`;
291
- if (room.exits.size > 0) {
292
- room.exits.forEach((exit, key) => {
293
- if (exit instanceof Door) {
294
- const nextRoom = exit.getOtherSideToName(room);
295
- description += `${capitalCase(key)}: ${nextRoom}`;
296
- if (exit.checkIfLocked()) {
297
- description += " (Locked)";
298
- }
299
- else if (exit.checkIfLockable()) {
300
- description += ' (Unlocked)';
301
- }
302
- description += '\n';
303
- }
304
- else {
305
- description += `${capitalCase(key)}\n`; // Example: "NORTH"
306
- }
307
- });
676
+ /**
677
+ * Refreshes the status box from current player state. Called after any
678
+ * command that can change what belongs there (currently "call"/"end-call")
679
+ * and once on load/job transition.
680
+ */
681
+ updateStatus() {
682
+ // updateInventory/updateEquipment route here now, and those hooks can
683
+ // fire from synthesis paths before the blessed UI exists.
684
+ if (!this._statusBox)
685
+ return;
686
+ const player = this.player;
687
+ const lines = [];
688
+ // Line 1: where you are, and the fight autopilot mode. The stance
689
+ // lives HERE -- the shortest HUD line -- because appended to the long
690
+ // readiness line it wrapped out of the box on narrower terminals and
691
+ // effectively only ever showed in the Tab announcement. Always shown,
692
+ // even MANUAL: the HUD is how players discover Tab cycles it at all
693
+ // (see utilities/auto-fight.ts). MANUAL is white, not gray -- blessed's
694
+ // gray is ANSI "bright black", which on a stock dark terminal palette
695
+ // is illegible enough that the stance looked absent entirely.
696
+ const stanceColor = player.autoStance === 'aggressive' ? 'red' : player.autoStance === 'guarded' ? 'yellow' : 'white';
697
+ const stance = `{${stanceColor}-fg}⚔ ${player.autoStance.toUpperCase()}{/${stanceColor}-fg}`;
698
+ // The breadcrumb, reading chronologically into the current location:
699
+ // "Neon Strip (west) > Night Market" -- origin first, the direction
700
+ // BACK to it in parens. An 18-room hub made "which way did I come in"
701
+ // a real question.
702
+ const crumb = player.cameFrom ? `${player.cameFrom.room} (${player.cameFrom.back}) > ` : '';
703
+ if (player.currentLocation) {
704
+ lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}{/bold} | ${stance}`);
308
705
  }
309
706
  else {
310
- description += '\nThere are no exits here.';
707
+ lines.push(stance);
311
708
  }
312
- this._exitsBox.setContent(description);
313
- this._screen.render();
314
- }
315
- // Function to update the inventory
316
- updateInventory(playerInventory, currencyName = 'currency') {
317
- const items = playerInventory.getAllItems();
318
- let currencyTotal = 0;
319
- const itemLines = [];
320
- for (const item of items) {
321
- let line = item.name;
322
- if (item.isCurrencyCarrier?.() && item.currencyAmount > 0) {
323
- line += ` [${item.currencyAmount}]`;
324
- currencyTotal += item.currencyAmount;
325
- }
326
- itemLines.push(line);
709
+ // Line 2: both condition monitors -- in a game where a fight can kill
710
+ // you and casting can knock you out, current damage on either track is
711
+ // never something a player should have to ask for. Colors shift as
712
+ // each track fills; the stacked wound modifier from both tracks shows
713
+ // once.
714
+ const trackColor = (taken, max) => taken === 0 ? 'green' : taken >= max - 3 ? 'red' : 'yellow';
715
+ const wound = player.woundModifier;
716
+ const woundNote = wound < 0 ? ` {red-fg}(${wound} dice){/red-fg}` : '';
717
+ const physColor = trackColor(player.damageTaken, player.maxConditionBoxes);
718
+ const stunColor = trackColor(player.stunTaken, player.maxStunBoxes);
719
+ lines.push(`{${physColor}-fg}Physical ${player.conditionSummary()}{/${physColor}-fg} ` +
720
+ `{${stunColor}-fg}Stun ${player.stunSummary()}{/${stunColor}-fg}${woundNote}`);
721
+ // Line 3: readiness -- armed state, armor, and whether the commlink is
722
+ // actually live (equipped), the recurring "why can't I call anyone"
723
+ // friction surfaced at a glance.
724
+ const weapon = player.getWeaponProfile();
725
+ const weaponItem = player.getCarriedWeapon();
726
+ const ammoNote = weaponItem?.isFirearm()
727
+ ? (weaponItem.jammed
728
+ ? ` {red-fg}[JAMMED -- reboot]{/red-fg}`
729
+ : weaponItem.ammo === 0 ? ` {red-fg}[DRY]{/red-fg}` : ` [${weaponItem.ammo} rds]`)
730
+ : '';
731
+ // Three armed states on the meat plane: drawn, holstered (weapon
732
+ // carried but riding leather -- "brandish" puts it in hand; combat
733
+ // auto-draws so this is posture, not readiness), or nothing at all.
734
+ // Off-plane the profile is attack programs / astral will and reads as
735
+ // Armed. White, not gray, for the same legibility reason as the stance.
736
+ const armed = player.plane === 'meat' && weaponItem && !player.weaponDrawn
737
+ ? `{white-fg}Holstered: ${weaponItem.name}${ammoNote} -- "brandish"{/white-fg}`
738
+ : weapon.name === 'bare fists'
739
+ ? `{gray-fg}Unarmed{/gray-fg}`
740
+ : `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
741
+ const armorNote = `Armor +${player.getArmorValue()}`;
742
+ const hasLink = player.equipment.head?.commlink != null;
743
+ const link = !hasLink
744
+ ? `{gray-fg}Link offline{/gray-fg}`
745
+ : player.runningSilent
746
+ ? `{magenta-fg}Link SILENT{/magenta-fg}`
747
+ : player.arOffline
748
+ ? `{red-fg}AR CUT -- reboot{/red-fg}`
749
+ : `{${CALL_COLOR}-fg}Link online{/${CALL_COLOR}-fg}`;
750
+ lines.push(`${armed} | ${armorNote} | ${link}`);
751
+ // Lines 4+: transient states, present only while active. Future
752
+ // contexts (astral projection, VR hot-sim) join here the same way.
753
+ if (player.sustainedArmor > 0) {
754
+ lines.push(`{magenta-fg}Sustained: armor +${player.sustainedArmor} soak (-2 dice to everything else){/magenta-fg}`);
755
+ }
756
+ if (player.astralPerceiving) {
757
+ lines.push(`{magenta-fg}✧ ASTRAL SENSES OPEN (-2 dice sustained) -- "assense" to close{/magenta-fg}`);
758
+ }
759
+ if (player.sneaking) {
760
+ lines.push(`{white-fg}🐾 SNEAKING -- entries are opposed vs their perception; loud acts break it{/white-fg}`);
761
+ }
762
+ if (player.activeCallPartner) {
763
+ lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.activeCallPartner.name}{/${CALL_COLOR}-fg}`);
327
764
  }
328
- itemLines.unshift(`{bold}Total ${currencyName}:{/bold} ${currencyTotal}\n`);
329
- this._inventoryBox.setContent(itemLines.join('\n'));
765
+ // Off-plane context: which reality you're in, your persona's Matrix
766
+ // track (cold-sim damage lands there), and where your empty body lies.
767
+ if (player.plane === 'matrix') {
768
+ // Matrix damage is hardware damage: show the DECK's track. A
769
+ // technomancer has no deck -- their stun track (already on line 2)
770
+ // is what the ice chews on.
771
+ const track = player.activeDeck
772
+ ? ` Deck ${player.activeDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}`
773
+ : ` (living persona -- damage is REAL stun)`;
774
+ lines.push(`{cyan-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/cyan-fg}`);
775
+ }
776
+ else if (player.plane === 'astral') {
777
+ lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
778
+ }
779
+ this._statusBox.setContent(lines.join('\n'));
330
780
  this._screen.render();
331
781
  }
332
- ;
782
+ // The three methods below were the render paths for the old always-on
783
+ // sidebar boxes. Those boxes are gone (the log owns the whole screen; see
784
+ // commands/inv.ts, equipment.ts, map.ts for the on-demand replacements),
785
+ // but the methods stay as no-ops: dozens of call sites across commands,
786
+ // scene proxies, and ui.ts still ping them after inventory/equipment/exit
787
+ // changes, and keeping the hooks alive means a future HUD element can tap
788
+ // those same signals without re-plumbing. Each refreshes the HUD, since
789
+ // several HUD fields (armed, armor, link) derive from equipment state.
790
+ updateEquipment(_playerEquipment) {
791
+ this.updateStatus();
792
+ }
793
+ updateExits(_room) {
794
+ // Nothing to render -- the "map" command reads live room state.
795
+ }
796
+ updateInventory(_playerInventory, _currencyName = 'currency') {
797
+ this.updateStatus();
798
+ }
333
799
  // Public method to get the instance of the Game class
334
800
  static async getInstance(player, sceneSeed) {
335
801
  if (!Game.instance) {
@@ -338,17 +804,26 @@ export default class Game {
338
804
  return Game.instance;
339
805
  }
340
806
  async handleInput(input) {
341
- const parts = input.trim().split(' ');
807
+ // "jack-in"/"jack-out" read naturally but parse as unknown verbs (a
808
+ // real session typed "jack-in" and bounced) -- normalize the hyphen.
809
+ const normalized = input.replace(/^\s*jack-(in|out)\b/i, 'jack $1');
810
+ const parts = normalized.trim().split(' ');
342
811
  const verb = parts[0].toLowerCase();
343
812
  const args = parts.slice(1);
344
- if (verb === 'quit') {
813
+ if (verb === 'quit' || verb === 'exit') {
345
814
  return null;
346
815
  }
347
816
  if (verb === 'maka') {
348
817
  return 'You are still in the game, type "quit" to exit';
349
818
  }
819
+ if (verb === 'continue') {
820
+ // RETIRED verb, kept only as a redirect: jobs flow through the
821
+ // fixer ("call Mr. Johnson"), and a finished run ends by walking
822
+ // home to your hideout -- no system command in the loop.
823
+ return `That's not how the street works anymore, chummer: "call ${FIXER_NAME}" when you want work. When a run's wrapped, head home to your hideout -- the door knows the way.`;
824
+ }
350
825
  if (verb === 'help') {
351
- return this.getHelpMessage();
826
+ return this.getHelpMessage(args);
352
827
  }
353
828
  if (verb === 'clear') {
354
829
  return '';
@@ -357,26 +832,111 @@ export default class Game {
357
832
  actor: this.player,
358
833
  rooms: this.scene.getRooms(),
359
834
  scene: this.scene,
835
+ screen: this._screen,
836
+ game: this,
360
837
  };
361
838
  const command = CommandFactory.createCommand(verb, context);
362
839
  if (command) {
363
840
  const result = await command.execute(args);
364
841
  return result;
365
842
  }
843
+ // Typo tolerance: two straight sessions bounced on slips like "searh"
844
+ // and "maop". Reuse the same fuzzy matcher item/NPC names go through
845
+ // (ambiguous guesses deliberately miss). A unique match auto-runs with
846
+ // a note -- EXCEPT anything that ends the session, which only ever
847
+ // gets a suggestion: a typo must never quit the game.
848
+ const knownVerbs = [...Object.keys(CommandFactory.getAllRegisteredCommands()), 'help', 'clear', 'quit', 'exit'];
849
+ let guess = fuzzyPickName(verb, knownVerbs);
850
+ // The shared matcher refuses typo tolerance on words <= 3 chars (too
851
+ // risky for item/NPC names) -- but the verb list is small and
852
+ // distinct, so a unique edit-distance-1 hit is safe ("maop" -> "map").
853
+ if (!guess && verb.length >= 3) {
854
+ const close = knownVerbs.filter(v => levenshteinDistance(verb, v) <= 1);
855
+ if (close.length === 1)
856
+ guess = close[0];
857
+ }
858
+ if (guess && guess !== verb) {
859
+ if (guess === 'quit' || guess === 'exit') {
860
+ return `I don't understand "${verb}" -- did you mean "${guess}"?`;
861
+ }
862
+ const corrected = await this.handleInput([guess, ...args].join(' '));
863
+ return corrected === null ? null : `(read "${verb}" as "${guess}")\n${corrected ?? ''}`;
864
+ }
366
865
  return `I don't understand the command "${verb}".`;
367
866
  }
368
- getHelpMessage() {
369
- const commands = CommandFactory.getAllRegisteredCommands();
370
- let helpText = "Available Commands:\n";
371
- for (const verb in commands) {
372
- if (Object.hasOwn(commands, verb)) {
373
- const CommandClass = commands[verb];
374
- helpText += `- ${verb}: ${CommandClass.getDescription()}\n`;
867
+ /**
868
+ * The help taxonomy: every entry is [primary, ...aliases], grouped the
869
+ * way a player thinks, not the order commands were registered. The
870
+ * overview shows only verbs per category (the flat all-descriptions
871
+ * dump had grown past 45 lines); "help <category>" and "help <command>"
872
+ * drill into the real descriptions. Verbs registered but missing from
873
+ * this table surface in an "other" bucket rather than vanishing -- a
874
+ * future command can't silently fall out of help.
875
+ */
876
+ static HELP_CATEGORIES = [
877
+ { title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
878
+ { 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']] },
883
+ { title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
884
+ { title: 'character', entries: [['sheet', 'stats'], ['advance', 'train']] },
885
+ { title: 'session', entries: [['help'], ['clear'], ['quit', 'exit']] },
886
+ ];
887
+ /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
888
+ static BUILTIN_DESCRIPTIONS = {
889
+ help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
890
+ clear: 'Clear the screen.',
891
+ quit: 'Exit the game.',
892
+ };
893
+ describeVerb(verb) {
894
+ const builtin = Game.BUILTIN_DESCRIPTIONS[verb];
895
+ if (builtin)
896
+ return builtin;
897
+ const CommandClass = CommandFactory.getAllRegisteredCommands()[verb];
898
+ return CommandClass?.getDescription() ?? 'No description available.';
899
+ }
900
+ getHelpMessage(args = []) {
901
+ const registered = Object.keys(CommandFactory.getAllRegisteredCommands());
902
+ const covered = new Set(Game.HELP_CATEGORIES.flatMap(c => c.entries.flat()));
903
+ const uncategorized = registered.filter(v => !covered.has(v));
904
+ const query = args.join(' ').trim().toLowerCase();
905
+ // "help <category>": that group, with full descriptions.
906
+ if (query) {
907
+ const categoryNames = Game.HELP_CATEGORIES.map(c => c.title);
908
+ const pickedCategory = fuzzyPickName(query, categoryNames);
909
+ if (pickedCategory) {
910
+ const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
911
+ const lines = [`=== ${category.title.toUpperCase()} ===`];
912
+ for (const [primary, ...aliases] of category.entries) {
913
+ lines.push(`- ${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
914
+ }
915
+ return lines.join('\n');
375
916
  }
917
+ // "help <command>": one description, aliases resolved to their primary.
918
+ const allVerbs = [...covered, ...uncategorized];
919
+ const pickedVerb = fuzzyPickName(query, allVerbs);
920
+ if (pickedVerb) {
921
+ const entry = Game.HELP_CATEGORIES.flatMap(c => c.entries).find(e => e.includes(pickedVerb));
922
+ const primary = entry?.[0] ?? pickedVerb;
923
+ const aliases = entry?.slice(1) ?? [];
924
+ return `${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`;
925
+ }
926
+ return `No command or category called "${query}". Try plain "help".`;
927
+ }
928
+ // The overview: compact category lines, primaries only.
929
+ const lines = ['=== COMMANDS ==='];
930
+ const pad = Math.max(...Game.HELP_CATEGORIES.map(c => c.title.length));
931
+ for (const category of Game.HELP_CATEGORIES) {
932
+ lines.push(`${category.title.padEnd(pad)} ${category.entries.map(e => e[0]).join(', ')}`);
933
+ }
934
+ if (uncategorized.length > 0) {
935
+ lines.push(`${'other'.padEnd(pad)} ${uncategorized.join(', ')}`);
376
936
  }
377
- helpText += `- clear: Clear the screen.\n`;
378
- helpText += `- quit: Exit the game.\n`;
379
- return helpText;
937
+ lines.push('');
938
+ lines.push(`"help <category>" for a group with descriptions ("help combat"); "help <command>" for one ("help haggle").`);
939
+ return lines.join('\n');
380
940
  }
381
941
  }
382
942
  //# sourceMappingURL=game.js.map