@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
@@ -0,0 +1,647 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { AI } from '../../../../tools/ai/ai.class.js';
4
+ import { Player } from '../models/player.js';
5
+ import { Room } from '../models/room.js';
6
+ import { Logger } from '../utilities/logger.js';
7
+ import { SceneSynthesizer } from './scene-factory.js';
8
+ import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildPuzzlesPrompt, buildItemsPrompt, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, canBypassUnaided, } from './scene-chunks.js';
9
+ // The fixer name convention and the player dossier live with the prompt
10
+ // builders now (see scene-chunks.ts); re-exported so the existing
11
+ // import sites (call.ts, game.ts) keep working unchanged.
12
+ export { FIXER_NAME } from './scene-chunks.js';
13
+ export class SceneSeedGenerator {
14
+ /** Fresh-skeleton pipeline attempts (structural faults promote to a new one). */
15
+ static maxPipelineAttempts = 2;
16
+ /** Per-pipeline chunk repairs before demanding a fresh skeleton. */
17
+ static maxRepairRounds = 3;
18
+ /** Attempts per individual AI call (skeleton or one detail chunk). */
19
+ static maxCallAttempts = 2;
20
+ // Per-call response budgets. Each is a fraction of a whole scene -- the
21
+ // point of chunking: no single completion carries everything, so scene
22
+ // scale is no longer bounded by the provider's output ceiling (a real
23
+ // session lost four of six whole-scene attempts to JSON truncated
24
+ // mid-string at the old fixed cap).
25
+ static SKELETON_TOKENS = 4000;
26
+ static ROOMS_TOKENS = 3000;
27
+ static NPCS_TOKENS = 6000;
28
+ static PUZZLES_TOKENS = 3000;
29
+ static ITEMS_TOKENS = 6000;
30
+ /**
31
+ * CHUNKED generation (see scene-chunks.ts for the shapes and prompts):
32
+ * one compact SKELETON pass fixes the scene's whole structure -- every
33
+ * cross-reference the validators check -- then small detail passes flesh
34
+ * out rooms, NPCs, puzzles, and items (items last: clue items bake in
35
+ * the puzzle passphrases). The assembled scene runs through the same
36
+ * validators as ever; a failure regenerates only the chunk responsible
37
+ * (classifyRepair) instead of throwing the whole scene away.
38
+ *
39
+ * `tier` scales scope and stakes; `previousFixerName` keeps the same
40
+ * job-giver identity across "continue"; `player` becomes the dossier
41
+ * that tailors the job to the runner's archetype and toolkit.
42
+ */
43
+ static async generate(logFilePath, tier = 1, previousFixerName, player) {
44
+ if (!AI.isConfigured())
45
+ return null;
46
+ const logger = Logger.getInstance(logFilePath, { children: [], render: () => { } });
47
+ const playerName = player?.name ?? 'Maka';
48
+ // Two budgets, deliberately separate: PIPELINE attempts (a fresh
49
+ // skeleton + full chunk set -- structural faults land here) and, per
50
+ // pipeline, chunk REPAIR rounds. A real generation burned its whole
51
+ // repair budget on two skeleton restarts and died with one round left
52
+ // for an items fault; skeleton restarts no longer eat repair rounds.
53
+ let carriedError;
54
+ for (let pipeline = 1; pipeline <= this.maxPipelineAttempts; pipeline++) {
55
+ try {
56
+ const skeleton = await this.requestSkeleton(tier, previousFixerName, player, logger, carriedError);
57
+ const details = await this.requestAllDetails(skeleton, logger);
58
+ let structuralFault;
59
+ for (let round = 0; round <= this.maxRepairRounds; round++) {
60
+ const candidate = assembleScene(skeleton, details, playerName);
61
+ try {
62
+ await this.dryRunValidate(candidate, player);
63
+ logger.write(`SceneSeedGenerator: assembled valid scene "${candidate.name}" (pipeline ${pipeline}, ${round} repair round(s))`);
64
+ this.saveSeedToDisk(candidate, logFilePath, tier, skeleton);
65
+ return candidate;
66
+ }
67
+ catch (err) {
68
+ const message = err instanceof Error ? err.message : String(err);
69
+ const target = classifyRepair(message);
70
+ if (target === 'skeleton' || round === this.maxRepairRounds) {
71
+ structuralFault = message;
72
+ logger.write(`SceneSeedGenerator: pipeline ${pipeline} needs a fresh skeleton (${target === 'skeleton' ? 'structural fault' : 'repair budget exhausted'}): ${message}`);
73
+ break;
74
+ }
75
+ logger.write(`SceneSeedGenerator: validation failed -> repairing ${target}: ${message}`);
76
+ if (target === 'rooms') {
77
+ details.rooms = await this.requestChunk('rooms', buildRoomsPrompt(skeleton, message), this.ROOMS_TOKENS, logger);
78
+ }
79
+ else if (target === 'npcs') {
80
+ details.npcs = await this.requestChunk('npcs', buildNpcsPrompt(skeleton, message), this.NPCS_TOKENS, logger);
81
+ }
82
+ else if (target === 'puzzles') {
83
+ details.puzzles = await this.requestChunk('puzzles', buildPuzzlesPrompt(skeleton, message), this.PUZZLES_TOKENS, logger);
84
+ // New passphrases invalidate the baked-in clue text.
85
+ details.items = await this.requestChunk('items', buildItemsPrompt(skeleton, details.puzzles), this.ITEMS_TOKENS, logger);
86
+ }
87
+ else {
88
+ details.items = await this.requestChunk('items', buildItemsPrompt(skeleton, details.puzzles, message), this.ITEMS_TOKENS, logger);
89
+ }
90
+ }
91
+ }
92
+ carriedError = structuralFault;
93
+ }
94
+ catch (err) {
95
+ carriedError = err instanceof Error ? err.message : String(err);
96
+ logger.write(`SceneSeedGenerator: pipeline ${pipeline} failed: ${carriedError}`);
97
+ }
98
+ }
99
+ logger.write(`SceneSeedGenerator: giving up (${carriedError ?? 'unknown'}) -- falling back to a static scene`);
100
+ return null;
101
+ }
102
+ /** The skeleton pass: structure only, fast-validated before any detail pass builds on it. */
103
+ static async requestSkeleton(tier, previousFixerName, player, logger, previousError) {
104
+ let lastError = previousError;
105
+ for (let attempt = 1; attempt <= this.maxCallAttempts; attempt++) {
106
+ const reply = await AI.ask(buildSkeletonPrompt(tier, previousFixerName, player, lastError), { maxTokens: this.SKELETON_TOKENS });
107
+ try {
108
+ const skeleton = parseJsonReply(reply);
109
+ skeleton.puzzles = skeleton.puzzles ?? [];
110
+ for (const note of normalizeSkeleton(skeleton)) {
111
+ logger.write(`SceneSeedGenerator: ${note}`);
112
+ }
113
+ validateSkeleton(skeleton, player);
114
+ logger.write(`SceneSeedGenerator: skeleton "${skeleton.name}" -- ${skeleton.rooms.length} rooms, ${skeleton.npcs.length} npcs, ${skeleton.puzzles.length} puzzles, ${skeleton.items.length} items`);
115
+ return skeleton;
116
+ }
117
+ catch (err) {
118
+ lastError = err instanceof Error ? err.message : String(err);
119
+ logger.write(`SceneSeedGenerator: skeleton attempt ${attempt} failed: ${lastError}`);
120
+ if (attempt === this.maxCallAttempts)
121
+ throw err;
122
+ }
123
+ }
124
+ throw new Error('unreachable');
125
+ }
126
+ /** One detail chunk: prompt -> JSON, with one retry feeding the parse error back. */
127
+ static async requestChunk(label, prompt, maxTokens, logger) {
128
+ let lastError;
129
+ for (let attempt = 1; attempt <= this.maxCallAttempts; attempt++) {
130
+ const fullPrompt = lastError
131
+ ? `${prompt}\n\nYour previous attempt failed with this error -- fix it and try again: ${lastError}`
132
+ : prompt;
133
+ const reply = await AI.ask(fullPrompt, { maxTokens });
134
+ try {
135
+ const parsed = parseJsonReply(reply);
136
+ logger.write(`SceneSeedGenerator: chunk "${label}" ok (attempt ${attempt})`);
137
+ return parsed;
138
+ }
139
+ catch (err) {
140
+ lastError = err instanceof Error ? err.message : String(err);
141
+ logger.write(`SceneSeedGenerator: chunk "${label}" attempt ${attempt} failed: ${lastError}`);
142
+ if (attempt === this.maxCallAttempts)
143
+ throw err;
144
+ }
145
+ }
146
+ throw new Error('unreachable');
147
+ }
148
+ /**
149
+ * Rooms, NPCs, and puzzles are independent given the skeleton -- they
150
+ * run in parallel. Items runs after puzzles: each clue item's "details"
151
+ * must contain its puzzle's exact passphrase.
152
+ */
153
+ static async requestAllDetails(skeleton, logger) {
154
+ const [rooms, npcs, puzzles] = await Promise.all([
155
+ this.requestChunk('rooms', buildRoomsPrompt(skeleton), this.ROOMS_TOKENS, logger),
156
+ this.requestChunk('npcs', buildNpcsPrompt(skeleton), this.NPCS_TOKENS, logger),
157
+ skeleton.puzzles.length > 0
158
+ ? this.requestChunk('puzzles', buildPuzzlesPrompt(skeleton), this.PUZZLES_TOKENS, logger)
159
+ : Promise.resolve([]),
160
+ ]);
161
+ const items = await this.requestChunk('items', buildItemsPrompt(skeleton, puzzles), this.ITEMS_TOKENS, logger);
162
+ return { rooms, npcs, puzzles, items };
163
+ }
164
+ /**
165
+ * Saves a validated scene alongside that session's game log (same
166
+ * directory as `logFilePath`) so a run can be inspected or replayed later
167
+ * -- AI-generated scenes previously only existed in memory for the
168
+ * process that generated them.
169
+ */
170
+ static saveSeedToDisk(candidate, logFilePath, tier, skeleton) {
171
+ const logger = Logger.getInstance();
172
+ const sessionDir = path.dirname(logFilePath);
173
+ const seedPath = path.join(sessionDir, `scene-job-${tier}.json`);
174
+ try {
175
+ fs.mkdirSync(sessionDir, { recursive: true });
176
+ fs.writeFileSync(seedPath, JSON.stringify(candidate, null, 2));
177
+ // The skeleton rides along for post-mortems: it shows what the
178
+ // structure pass planned, separate from what the detail passes made
179
+ // of it.
180
+ if (skeleton) {
181
+ fs.writeFileSync(path.join(sessionDir, `scene-job-${tier}-skeleton.json`), JSON.stringify(skeleton, null, 2));
182
+ }
183
+ logger.write(`SceneSeedGenerator: saved generated scene to ${seedPath}`);
184
+ }
185
+ catch (err) {
186
+ logger.error(`SceneSeedGenerator: failed to save generated scene to ${seedPath}: ${err}`);
187
+ }
188
+ }
189
+ /**
190
+ * Runs the candidate through the exact same synthesis pipeline a real
191
+ * session would use, purely to surface referential-integrity errors
192
+ * (missing room/item/npc names) before committing to it. Discards the
193
+ * resulting Scene either way -- the real session re-synthesizes fresh
194
+ * (with AI flavor enabled) once this seed is handed off.
195
+ */
196
+ static async dryRunValidate(candidate, player) {
197
+ // Must run BEFORE synthesis: a room/npc/item with a missing or blank
198
+ // "name" doesn't fail synthesis with a useful error -- it crashes deep
199
+ // inside it with a raw JS TypeError ("Cannot read properties of
200
+ // undefined (reading 'normalize')"), found via a real generation where
201
+ // that opaque message became the "previous attempt failed" feedback fed
202
+ // back into the retry, giving the model nothing to actually act on.
203
+ this.validateBasicStructure(candidate);
204
+ const nexus = new Room({ name: 'Nexus', description: 'A featureless room where lost souls drift.', roomType: 'void' });
205
+ const throwawayPlayer = new Player({ playerName: candidate?.player?.name || 'Maka', startLocation: nexus });
206
+ // Referential-integrity errors in winCondition (bad item/toNpc/rewardItem
207
+ // names) are already caught by synthesis itself -- scene-factory.ts
208
+ // throws on those. This only checks the one semantic rule synthesis
209
+ // can't: the item must actually be findable, not already delivered.
210
+ await SceneSynthesizer.synthesizeFromJson(candidate, throwawayPlayer, false);
211
+ this.validateClueItems(candidate, player);
212
+ this.validateWinCondition(candidate, player);
213
+ this.validateMatrixDefense(candidate);
214
+ this.validateStartingItems(candidate, player);
215
+ this.validateBypassTypes(candidate);
216
+ this.validatePuzzleChain(candidate);
217
+ this.validateDoorKeys(candidate);
218
+ this.validateRewardItemPlacement(candidate);
219
+ this.validatePuzzlePlacement(candidate);
220
+ }
221
+ /**
222
+ * The one structural guarantee every other check assumes: every room,
223
+ * NPC, and item actually has a real, non-blank "name". Nothing about the
224
+ * JSON shape stops a model from emitting `"name": ""` or omitting the
225
+ * field outright, and synthesis doesn't validate it either -- it just
226
+ * uses whatever's there as a map key and crashes with an opaque error the
227
+ * moment something tries to look it up.
228
+ */
229
+ static validateBasicStructure(candidate) {
230
+ const isBlank = (name) => typeof name !== 'string' || name.trim().length === 0;
231
+ (candidate.rooms ?? []).forEach((room, i) => {
232
+ if (isBlank(room?.name)) {
233
+ throw new Error(`"rooms[${i}]" is missing a "name" (or it's blank) -- every room needs a real, non-empty name.`);
234
+ }
235
+ });
236
+ (candidate.npcs ?? []).forEach((npc, i) => {
237
+ if (isBlank(npc?.player?.name)) {
238
+ throw new Error(`"npcs[${i}].player.name" is missing (or it's blank) -- every NPC needs a real, non-empty name.`);
239
+ }
240
+ });
241
+ (candidate.items ?? []).forEach((item, i) => {
242
+ if (isBlank(item?.name)) {
243
+ throw new Error(`"items[${i}]" is missing a "name" (or it's blank) -- every item needs a real, non-empty name.`);
244
+ }
245
+ });
246
+ (candidate.puzzles ?? []).forEach((puzzle, i) => {
247
+ if (isBlank(puzzle?.name)) {
248
+ throw new Error(`"puzzles[${i}]" is missing a "name" (or it's blank) -- every puzzle needs a real, non-empty name.`);
249
+ }
250
+ });
251
+ }
252
+ /**
253
+ * A puzzle must belong to exactly one place: a room (via "puzzles[].room")
254
+ * or a door (via some "exits[].puzzle" naming it) -- see DOORS AS PUZZLES.
255
+ * Synthesis itself doesn't catch either failure mode: an orphaned puzzle
256
+ * (neither) silently never appears anywhere in the world, and a
257
+ * double-placed one (both) would exist as two disconnected instances of
258
+ * "the same" barrier -- solvable in the room without ever needing to open
259
+ * the door, or vice versa.
260
+ */
261
+ static validatePuzzlePlacement(candidate) {
262
+ const rooms = candidate.rooms ?? [];
263
+ const puzzles = candidate.puzzles ?? [];
264
+ const puzzleNames = new Set(puzzles.map(p => p.name));
265
+ const exitReferences = new Map();
266
+ for (const room of rooms) {
267
+ for (const exit of room.exits ?? []) {
268
+ if (!exit.puzzle)
269
+ continue;
270
+ if (!puzzleNames.has(exit.puzzle)) {
271
+ throw new Error(`Exit from "${room.name}" to "${exit.targetRoom}" references puzzle "${exit.puzzle}", which doesn't match any "puzzles[].name".`);
272
+ }
273
+ exitReferences.set(exit.puzzle, (exitReferences.get(exit.puzzle) ?? 0) + 1);
274
+ }
275
+ }
276
+ for (const puzzle of puzzles) {
277
+ const doorCount = exitReferences.get(puzzle.name) ?? 0;
278
+ if (puzzle.room && doorCount > 0) {
279
+ throw new Error(`Puzzle "${puzzle.name}" is placed both in room "${puzzle.room}" AND on a door (via "exits[].puzzle") -- it must be exactly one or the other. Remove one placement.`);
280
+ }
281
+ if (!puzzle.room && doorCount === 0) {
282
+ throw new Error(`Puzzle "${puzzle.name}" isn't placed anywhere -- set its "room", or reference it from an "exits[].puzzle".`);
283
+ }
284
+ if (doorCount > 1) {
285
+ throw new Error(`Puzzle "${puzzle.name}" is referenced by more than one "exits[].puzzle" -- a single puzzle can only guard one door.`);
286
+ }
287
+ }
288
+ }
289
+ /**
290
+ * The schema says a reward item must have neither "room" nor "heldBy" --
291
+ * solving the puzzle (or completing the win condition) is what's supposed
292
+ * to bring it into existence for the player -- but nothing ever actually
293
+ * enforced that. Found via a real session where a puzzle's rewardItem was
294
+ * ALSO "heldBy" the very NPC guarding it: solving the puzzle granted a
295
+ * second, phantom copy of the item while the NPC's own real one stayed
296
+ * put (and later dropped normally when killed) -- so the player ended up
297
+ * with two disconnected copies of "the same" item, and the NPC's own
298
+ * dialogue reflected the same confused premise (accusing the player of
299
+ * theft for solving a puzzle it had just dared them to solve).
300
+ */
301
+ static validateRewardItemPlacement(candidate) {
302
+ const itemByName = new Map((candidate.items ?? []).map(i => [i.name, i]));
303
+ for (const puzzle of candidate.puzzles ?? []) {
304
+ if (!puzzle.rewardItem)
305
+ continue;
306
+ const reward = itemByName.get(puzzle.rewardItem);
307
+ if (reward && (reward.room || reward.heldBy)) {
308
+ throw new Error(`Puzzle "${puzzle.name}"'s rewardItem "${puzzle.rewardItem}" already has "${reward.room ? 'room' : 'heldBy'}" set -- a reward item can't already exist somewhere in the world, since solving the puzzle is what's supposed to bring it into the player's possession. Remove "${reward.room ? 'room' : 'heldBy'}" from that item.`);
309
+ }
310
+ // A puzzle reward and the winCondition.item are opposite by definition
311
+ // (one MUST have no room/heldBy, the other MUST have one), so reusing
312
+ // the same item name for both is unsatisfiable no matter which way it's
313
+ // fixed -- caught via a real session where the model bounced between
314
+ // "remove room" and "not placed anywhere" across three straight
315
+ // attempts on the exact same item, because neither error alone said
316
+ // the two roles can't share an item. Named here explicitly so a retry
317
+ // fixes the actual problem instead of re-tripping the other rule.
318
+ if (candidate.winCondition && puzzle.rewardItem === candidate.winCondition.item) {
319
+ throw new Error(`Puzzle "${puzzle.name}"'s rewardItem and "winCondition.item" are the same item ("${puzzle.rewardItem}") -- they can't be, since a puzzle reward must have no "room"/"heldBy" while the winCondition.item must have one. Use two different items: the winCondition.item pre-placed somewhere in the world, and a separate rewardItem this puzzle grants.`);
320
+ }
321
+ }
322
+ const wcRewardName = candidate.winCondition?.rewardItem;
323
+ if (wcRewardName) {
324
+ const reward = itemByName.get(wcRewardName);
325
+ if (reward && (reward.room || reward.heldBy)) {
326
+ throw new Error(`Win condition rewardItem "${wcRewardName}" already has "${reward.room ? 'room' : 'heldBy'}" set -- same rule as a puzzle's rewardItem. Remove it.`);
327
+ }
328
+ }
329
+ }
330
+ /**
331
+ * Maps every item that has a concrete location (room, or heldBy an NPC
332
+ * whose startLocation gives it one) to that room's name. Shared by the
333
+ * reachability BFS and the win-condition checks below.
334
+ */
335
+ static buildItemLocationMap(candidate) {
336
+ const npcStartByName = new Map((candidate.npcs ?? []).map(n => [n.player.name, n.player.startLocation]));
337
+ const itemLocation = new Map();
338
+ for (const item of candidate.items ?? []) {
339
+ if (item.room) {
340
+ itemLocation.set(item.name, item.room);
341
+ }
342
+ else if (item.heldBy) {
343
+ const loc = npcStartByName.get(item.heldBy);
344
+ if (loc)
345
+ itemLocation.set(item.name, loc);
346
+ }
347
+ }
348
+ return itemLocation;
349
+ }
350
+ /**
351
+ * Lock-and-key reachability BFS from the start room: a room becomes
352
+ * reachable once some already-reachable room has an unlocked exit to it,
353
+ * or a locked exit whose key has already been found in a reachable
354
+ * location. Rooms marked "reachable: false" are excluded from this
355
+ * traversal on purpose -- see validateDoorKeys.
356
+ */
357
+ static computeReachableRooms(candidate) {
358
+ const rooms = candidate.rooms ?? [];
359
+ const itemLocation = this.buildItemLocationMap(candidate);
360
+ const startRoom = rooms.find(r => r.isStartRoom)?.name;
361
+ const reachable = new Set();
362
+ if (!startRoom || !rooms.some(r => r.name === startRoom))
363
+ return reachable;
364
+ // Edges are BIDIRECTIONAL: the engine mirrors every declared exit
365
+ // into a shared two-way Door (Room.addExit writes both sides), so a
366
+ // one-sided declaration -- the chunked skeleton's house style --
367
+ // still connects both rooms in play. The old directed-only walk
368
+ // falsely stranded rooms reachable via the mirrored direction.
369
+ const edges = [];
370
+ for (const room of rooms) {
371
+ for (const exit of room.exits ?? []) {
372
+ edges.push({ a: room.name, b: exit.targetRoom, keyRequired: exit.keyRequired, puzzle: exit.puzzle });
373
+ }
374
+ }
375
+ reachable.add(startRoom);
376
+ let grew = true;
377
+ while (grew) {
378
+ grew = false;
379
+ for (const edge of edges) {
380
+ const from = reachable.has(edge.a) ? edge.a : reachable.has(edge.b) ? edge.b : undefined;
381
+ const to = from === edge.a ? edge.b : edge.a;
382
+ if (!from || reachable.has(to))
383
+ continue;
384
+ const keyLocation = edge.keyRequired ? itemLocation.get(edge.keyRequired) : undefined;
385
+ // A puzzle-guarded exit (see DOORS AS PUZZLES) is always treated
386
+ // as eventually passable: the clue/competence rules guarantee a
387
+ // route, and pick/hack/cast/bluff is a real (if risky) shortcut.
388
+ // Only a plain "keyRequired" door needs its key traced to a
389
+ // reachable location first.
390
+ const passable = !!edge.puzzle || !edge.keyRequired || (keyLocation !== undefined && reachable.has(keyLocation));
391
+ if (passable) {
392
+ reachable.add(to);
393
+ grew = true;
394
+ }
395
+ }
396
+ }
397
+ return reachable;
398
+ }
399
+ /**
400
+ * Synthesis itself doesn't catch a locked door whose "keyRequired" names
401
+ * an item that doesn't exist, or one that's placed somewhere only
402
+ * reachable by first passing through that same door -- both leave the
403
+ * player permanently stuck with no way to unlock it (the real bug this
404
+ * guards against, found via a real session where a player was stonewalled
405
+ * behind a locked door for an entire play session). Also enforces the
406
+ * "reachable: false" contract (used for a fixer's inaccessible hideout,
407
+ * say): such a room must never connect to a normally-reachable one, in
408
+ * either direction, and nothing the player needs to retrieve may live
409
+ * inside one.
410
+ */
411
+ static validateDoorKeys(candidate) {
412
+ const rooms = candidate.rooms ?? [];
413
+ const items = candidate.items ?? [];
414
+ const roomByName = new Map(rooms.map(r => [r.name, r]));
415
+ for (const room of rooms) {
416
+ for (const exit of room.exits ?? []) {
417
+ if (!exit.keyRequired)
418
+ continue;
419
+ const item = items.find(i => i.name === exit.keyRequired);
420
+ if (!item) {
421
+ throw new Error(`Door from "${room.name}" to "${exit.targetRoom}" requires key "${exit.keyRequired}", which doesn't match any item in "items".`);
422
+ }
423
+ if (!item.room && !item.heldBy) {
424
+ throw new Error(`Key item "${exit.keyRequired}" (for the door from "${room.name}" to "${exit.targetRoom}") has neither "room" nor "heldBy" set -- it isn't placed anywhere in the world, so it can never actually be found.`);
425
+ }
426
+ if (item.room && roomByName.get(item.room)?.reachable === false) {
427
+ throw new Error(`Key item "${exit.keyRequired}" is placed in room "${item.room}", which is marked "reachable: false" -- it could never actually be retrieved.`);
428
+ }
429
+ }
430
+ }
431
+ // A "reachable: false" room (a fixer's hideout, say) must never connect
432
+ // to a normally-reachable one -- the engine wires doors bidirectionally
433
+ // regardless of which side declares the exit, so this has to check
434
+ // every declared exit's endpoints, not just exits declared from the
435
+ // unreachable room's own side.
436
+ for (const room of rooms) {
437
+ for (const exit of room.exits ?? []) {
438
+ const targetRoom = roomByName.get(exit.targetRoom);
439
+ if (!targetRoom)
440
+ continue; // caught by synthesis itself
441
+ if ((room.reachable === false) !== (targetRoom.reachable === false)) {
442
+ throw new Error(`Exit from "${room.name}" to "${exit.targetRoom}" connects a "reachable: false" room to a normally-reachable one -- this would let the player walk into (or the NPC walk out of) a room meant to be inaccessible. A "reachable: false" room must only connect to other "reachable: false" rooms, or have no exits at all.`);
443
+ }
444
+ }
445
+ }
446
+ const startRoom = rooms.find(r => r.isStartRoom)?.name;
447
+ if (!startRoom || !roomByName.has(startRoom))
448
+ return; // caught elsewhere
449
+ const reachable = this.computeReachableRooms(candidate);
450
+ const unreachable = rooms.filter(r => r.reachable !== false && !reachable.has(r.name));
451
+ if (unreachable.length > 0) {
452
+ throw new Error(`These rooms are unreachable from the start room "${startRoom}" given the locked doors and key placements: ${unreachable.map(r => r.name).join(', ')}. Every door's key must be reachable before the door that needs it (or mark the room "reachable": false if it's meant to be inaccessible, like a fixer's hideout).`);
453
+ }
454
+ }
455
+ /**
456
+ * requiresPuzzle references are structurally valid JSON either way (a
457
+ * bad name or a cycle won't make synthesis itself throw), so this walks
458
+ * each puzzle's prerequisite chain looking for a reference to a puzzle
459
+ * that doesn't exist or a cycle. Chains of any length are fine -- only
460
+ * missing references and cycles are rejected.
461
+ */
462
+ static validatePuzzleChain(candidate) {
463
+ const puzzles = candidate.puzzles ?? [];
464
+ const byName = new Map(puzzles.map(p => [p.name, p]));
465
+ for (const start of puzzles) {
466
+ const visited = new Set([start.name]);
467
+ let current = start;
468
+ while (current.requiresPuzzle) {
469
+ const next = byName.get(current.requiresPuzzle);
470
+ if (!next) {
471
+ throw new Error(`Puzzle "${current.name}" requires puzzle "${current.requiresPuzzle}", which doesn't exist.`);
472
+ }
473
+ if (visited.has(next.name)) {
474
+ throw new Error(`Puzzle chain starting at "${start.name}" cycles back to "${next.name}".`);
475
+ }
476
+ visited.add(next.name);
477
+ current = next;
478
+ }
479
+ }
480
+ }
481
+ /**
482
+ * Every AI-generated scene must have a completable win condition -- this
483
+ * is what the "continue into a new job" loop hooks into. Also guards
484
+ * against a win condition item that's already sitting in the target
485
+ * NPC's own inventory, which would make the "find and deliver it"
486
+ * premise nonsensical.
487
+ */
488
+ static validateWinCondition(candidate, player) {
489
+ const wc = candidate.winCondition;
490
+ if (!wc) {
491
+ throw new Error('Scene has no "winCondition". Every generated run needs one.');
492
+ }
493
+ const item = (candidate.items ?? []).find(i => i.name === wc.item);
494
+ if (!item) {
495
+ throw new Error(`Win condition item "${wc.item}" is not in "items".`);
496
+ }
497
+ if (!item.room && !item.heldBy) {
498
+ throw new Error(`Win condition item "${wc.item}" has neither "room" nor "heldBy" set -- it isn't placed anywhere in the world, so it can never actually be found.`);
499
+ }
500
+ // The payout is what the game actually pays AND what every NPC quotes
501
+ // -- a missing or nonsense figure breaks the promise/payment linkage
502
+ // this field exists for.
503
+ if (typeof wc.payout !== 'number' || !isFinite(wc.payout) || wc.payout <= 0) {
504
+ throw new Error(`"winCondition.payout" must be a positive number of nuyen (the job's agreed pay -- the engine transfers it on completion and the fixer quotes it). Got: ${JSON.stringify(wc.payout)}.`);
505
+ }
506
+ if (item.heldBy && item.heldBy === wc.toNpc) {
507
+ throw new Error(`Win condition item "${wc.item}" is already heldBy "${wc.toNpc}" -- it must be findable elsewhere, not already delivered.`);
508
+ }
509
+ const rooms = candidate.rooms ?? [];
510
+ if (item.room && rooms.find(r => r.name === item.room)?.reachable === false) {
511
+ throw new Error(`Win condition item "${wc.item}" is placed in room "${item.room}", which is marked "reachable: false" -- the player could never retrieve it.`);
512
+ }
513
+ // If the job-giver lives somewhere the player can never physically walk
514
+ // into (the fixer convention -- see buildSchemaGuide), "call" is the
515
+ // only way to complete a "give" condition, and "call" requires a
516
+ // commlink on BOTH ends.
517
+ if (wc.type === 'give' && wc.toNpc) {
518
+ const toNpcData = (candidate.npcs ?? []).find(n => n.player.name === wc.toNpc);
519
+ const toNpcRoom = rooms.find(r => r.name === toNpcData?.player.startLocation);
520
+ if (toNpcRoom?.reachable === false) {
521
+ const reachable = this.computeReachableRooms(candidate);
522
+ // Continuation drafts author no startingItems -- the dossier
523
+ // player's OWN kit (carried or equipped commlink) counts.
524
+ const ownsCommlink = !!player && (player.inventory.getAllItems().some(i => String(i.category).toLowerCase() === 'commlink') ||
525
+ player.equipment.head?.commlink != null);
526
+ const playerHasCommlink = ownsCommlink ||
527
+ (candidate.player?.startingItems ?? []).some(i => i.category === 'Commlink') ||
528
+ (candidate.items ?? []).some(i => i.category === 'Commlink' && i.room && reachable.has(i.room));
529
+ if (!playerHasCommlink) {
530
+ throw new Error(`Win condition target "${wc.toNpc}" is in a "reachable: false" room ("${toNpcRoom.name}"), so the player can only contact them via "call" -- but no Commlink-category item is available (not in "player.startingItems", and none findable in a reachable room). Give the player a way to get a commlink.`);
531
+ }
532
+ const fixerHasCommlink = (candidate.items ?? []).some(i => i.category === 'Commlink' && i.heldBy === wc.toNpc);
533
+ if (!fixerHasCommlink) {
534
+ throw new Error(`Win condition target "${wc.toNpc}" is in a "reachable: false" room, reachable only via "call" -- but they don't hold a Commlink-category item themselves. Add one to "items" with "heldBy": "${wc.toNpc}".`);
535
+ }
536
+ }
537
+ }
538
+ }
539
+ /**
540
+ * bypassType is a closed enum the engine indexes into (puzzle.ts
541
+ * BYPASS_FLAVOR) -- an unvalidated value used to reach a live session
542
+ * and crash every "search" in that puzzle's room (the model emitted
543
+ * "con" for a social barrier before that type existed). The lookups are
544
+ * defensive now, but a bad value would still mean a barrier whose
545
+ * bypass route silently doesn't exist -- reject it with the exact
546
+ * allowed list instead.
547
+ */
548
+ static validateBypassTypes(candidate) {
549
+ const allowed = new Set(['lockpick', 'hack', 'spell', 'con']);
550
+ for (const puzzle of candidate.puzzles ?? []) {
551
+ if (puzzle.bypassType && !allowed.has(puzzle.bypassType)) {
552
+ throw new Error(`Puzzle "${puzzle.name}" has bypassType "${puzzle.bypassType}", which isn't valid -- use exactly one of "lockpick", "hack", "spell", "con" (lowercase), or omit the field for a barrier with no bypass route.`);
553
+ }
554
+ }
555
+ }
556
+ /**
557
+ * Mechanical teeth behind the dossier's "never duplicate their gear"
558
+ * rule: the prompt guidance alone was violated on its very first outing
559
+ * (an Adept's startingItems included "Weapon Focus Katana" verbatim, and
560
+ * the player watched the duplicate bounce off their occupied weapon
561
+ * slot). Rejects any startingItem whose name matches something the
562
+ * dossier player already carries or wears.
563
+ */
564
+ static validateStartingItems(candidate, player) {
565
+ if (!player)
566
+ return;
567
+ const owned = new Set(player.inventory.getAllItems().map(i => i.name.toLowerCase()));
568
+ for (const group of Object.values(player.equipment)) {
569
+ if (!group)
570
+ continue;
571
+ for (const item of Object.values(group)) {
572
+ if (item)
573
+ owned.add(item.name.toLowerCase());
574
+ }
575
+ }
576
+ for (const starting of candidate.player?.startingItems ?? []) {
577
+ if (starting?.name && owned.has(starting.name.toLowerCase())) {
578
+ throw new Error(`"player.startingItems" includes "${starting.name}", which the runner ALREADY OWNS (see THE RUNNER dossier) -- remove it. Substitute a complementary basic they don't have (ammunition, a medkit, a tool), or simply grant fewer items.`);
579
+ }
580
+ }
581
+ }
582
+ /**
583
+ * A matrix-plane win-condition item must sit in a hasNode room with an
584
+ * ice NPC guarding it -- otherwise the whole run is a free walk: jack
585
+ * in, ride the grid (physical locks don't gate personas), take the
586
+ * file, done. Found via a real session that completed exactly that way
587
+ * with zero resistance. Hand-in-hand with the host-vault mechanic
588
+ * (matrix items stay sealed until the host is cracked -- see hack.ts /
589
+ * take.ts), this guarantees a datarun always has teeth.
590
+ */
591
+ static validateMatrixDefense(candidate) {
592
+ const wc = candidate.winCondition;
593
+ if (!wc)
594
+ return;
595
+ const item = (candidate.items ?? []).find(i => i.name === wc.item);
596
+ if (!item || item.plane !== 'matrix')
597
+ return;
598
+ if (!item.room) {
599
+ throw new Error(`Matrix-plane win condition item "${wc.item}" must be placed in a room (a hasNode room), not heldBy an NPC -- data lives on a host.`);
600
+ }
601
+ const room = (candidate.rooms ?? []).find(r => r.name === item.room);
602
+ if (!room?.hasNode) {
603
+ throw new Error(`Matrix-plane win condition item "${wc.item}" is in room "${item.room}", which has no "hasNode": true -- matrix files can only exist on a host. Mark that room hasNode.`);
604
+ }
605
+ const iceGuard = (candidate.npcs ?? []).some(n => n.plane === 'matrix' && n.player.startLocation === item.room);
606
+ if (!iceGuard) {
607
+ throw new Error(`Matrix-plane win condition item "${wc.item}" sits in hasNode room "${item.room}" with NO ice guarding it -- add an npc with "plane": "matrix", "startLocation": "${item.room}", and a combat block (logic 4-7). An undefended data vault makes the run a free win.`);
608
+ }
609
+ }
610
+ /**
611
+ * Enforces the "always some way to find the solution" guarantee: every
612
+ * puzzle must have a discoverable clue, placed in a different room. None
613
+ * of this is caught by synthesis (a puzzle with no clue, or an empty one,
614
+ * is still structurally valid), so it's checked separately and treated
615
+ * the same as any other validation failure -- it triggers a retry.
616
+ *
617
+ * Two patterns both count. Found via a real generation that kept getting
618
+ * rejected despite being genuinely solvable: a riddle whose answer is a
619
+ * real item's own name (e.g. "obsidian keycard", matching an actual item
620
+ * called "Obsidian Keycard" sitting in another room) needs no separate
621
+ * documentation -- finding it and seeing its name on screen already
622
+ * reveals the answer. A Book clue is still required for anything else.
623
+ */
624
+ static validateClueItems(candidate, player) {
625
+ const items = candidate.items ?? [];
626
+ for (const puzzle of candidate.puzzles ?? []) {
627
+ const requiredPhrase = (puzzle.requiredPhrase ?? '').toLowerCase();
628
+ const hasBookClue = items.some(item => item.category === 'Book' &&
629
+ item.room &&
630
+ item.room !== puzzle.room &&
631
+ item.details &&
632
+ item.details.trim().length > 0 &&
633
+ (!requiredPhrase || item.details.toLowerCase().includes(requiredPhrase)));
634
+ const hasNamedItemClue = requiredPhrase.length > 0 && items.some(item => item.room &&
635
+ item.room !== puzzle.room &&
636
+ item.name.toLowerCase() === requiredPhrase);
637
+ // Loosened against the dossier: a barrier whose bypassType matches
638
+ // the runner's own trained means (a lockpick door for a master
639
+ // locksmith) doesn't strictly need its credential written down --
640
+ // the skill IS the way past. Strict rule stays without a dossier.
641
+ if (!hasBookClue && !hasNamedItemClue && !canBypassUnaided(player, puzzle.bypassType)) {
642
+ throw new Error(`Puzzle "${puzzle.name}" has no discoverable clue: no Book-category item in a different room whose "details" contains the answer "${puzzle.requiredPhrase}", no item elsewhere literally named "${puzzle.requiredPhrase}", and its bypassType (${puzzle.bypassType ?? 'none'}) is not a means this runner reliably has. Every puzzle needs one of the three.`);
643
+ }
644
+ }
645
+ }
646
+ }
647
+ //# sourceMappingURL=scene-seed-generator.js.map