@rpgm-tools/neo-angband-core 0.10.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 (1183) hide show
  1. package/LICENSE.md +43 -0
  2. package/README.md +80 -0
  3. package/dist/agent/act.d.ts +22 -0
  4. package/dist/agent/act.d.ts.map +1 -0
  5. package/dist/agent/act.js +88 -0
  6. package/dist/agent/act.js.map +1 -0
  7. package/dist/agent/controller.d.ts +34 -0
  8. package/dist/agent/controller.d.ts.map +1 -0
  9. package/dist/agent/controller.js +90 -0
  10. package/dist/agent/controller.js.map +1 -0
  11. package/dist/agent/events.d.ts +27 -0
  12. package/dist/agent/events.d.ts.map +1 -0
  13. package/dist/agent/events.js +31 -0
  14. package/dist/agent/events.js.map +1 -0
  15. package/dist/agent/index.d.ts +17 -0
  16. package/dist/agent/index.d.ts.map +1 -0
  17. package/dist/agent/index.js +15 -0
  18. package/dist/agent/index.js.map +1 -0
  19. package/dist/agent/perceive.d.ts +21 -0
  20. package/dist/agent/perceive.d.ts.map +1 -0
  21. package/dist/agent/perceive.js +386 -0
  22. package/dist/agent/perceive.js.map +1 -0
  23. package/dist/agent/types.d.ts +436 -0
  24. package/dist/agent/types.d.ts.map +1 -0
  25. package/dist/agent/types.js +63 -0
  26. package/dist/agent/types.js.map +1 -0
  27. package/dist/bitflag.d.ts +199 -0
  28. package/dist/bitflag.d.ts.map +1 -0
  29. package/dist/bitflag.js +478 -0
  30. package/dist/bitflag.js.map +1 -0
  31. package/dist/cmd.d.ts +164 -0
  32. package/dist/cmd.d.ts.map +1 -0
  33. package/dist/cmd.js +350 -0
  34. package/dist/cmd.js.map +1 -0
  35. package/dist/color.d.ts +104 -0
  36. package/dist/color.d.ts.map +1 -0
  37. package/dist/color.js +240 -0
  38. package/dist/color.js.map +1 -0
  39. package/dist/combat/brand-slay.d.ts +119 -0
  40. package/dist/combat/brand-slay.d.ts.map +1 -0
  41. package/dist/combat/brand-slay.js +264 -0
  42. package/dist/combat/brand-slay.js.map +1 -0
  43. package/dist/combat/hit.d.ts +198 -0
  44. package/dist/combat/hit.d.ts.map +1 -0
  45. package/dist/combat/hit.js +310 -0
  46. package/dist/combat/hit.js.map +1 -0
  47. package/dist/combat/index.d.ts +15 -0
  48. package/dist/combat/index.d.ts.map +1 -0
  49. package/dist/combat/index.js +15 -0
  50. package/dist/combat/index.js.map +1 -0
  51. package/dist/combat/melee.d.ts +261 -0
  52. package/dist/combat/melee.d.ts.map +1 -0
  53. package/dist/combat/melee.js +402 -0
  54. package/dist/combat/melee.js.map +1 -0
  55. package/dist/combat/mon-melee.d.ts +233 -0
  56. package/dist/combat/mon-melee.d.ts.map +1 -0
  57. package/dist/combat/mon-melee.js +883 -0
  58. package/dist/combat/mon-melee.js.map +1 -0
  59. package/dist/combat/ranged.d.ts +89 -0
  60. package/dist/combat/ranged.d.ts.map +1 -0
  61. package/dist/combat/ranged.js +253 -0
  62. package/dist/combat/ranged.js.map +1 -0
  63. package/dist/constants.d.ts +134 -0
  64. package/dist/constants.d.ts.map +1 -0
  65. package/dist/constants.js +171 -0
  66. package/dist/constants.js.map +1 -0
  67. package/dist/dice.d.ts +91 -0
  68. package/dist/dice.d.ts.map +1 -0
  69. package/dist/dice.js +424 -0
  70. package/dist/dice.js.map +1 -0
  71. package/dist/effects/effect-choice.d.ts +15 -0
  72. package/dist/effects/effect-choice.d.ts.map +1 -0
  73. package/dist/effects/effect-choice.js +18 -0
  74. package/dist/effects/effect-choice.js.map +1 -0
  75. package/dist/effects/effect-info.d.ts +124 -0
  76. package/dist/effects/effect-info.d.ts.map +1 -0
  77. package/dist/effects/effect-info.js +706 -0
  78. package/dist/effects/effect-info.js.map +1 -0
  79. package/dist/effects/effect.d.ts +165 -0
  80. package/dist/effects/effect.d.ts.map +1 -0
  81. package/dist/effects/effect.js +452 -0
  82. package/dist/effects/effect.js.map +1 -0
  83. package/dist/effects/handlers.d.ts +70 -0
  84. package/dist/effects/handlers.d.ts.map +1 -0
  85. package/dist/effects/handlers.js +394 -0
  86. package/dist/effects/handlers.js.map +1 -0
  87. package/dist/effects/interpreter.d.ts +291 -0
  88. package/dist/effects/interpreter.d.ts.map +1 -0
  89. package/dist/effects/interpreter.js +325 -0
  90. package/dist/effects/interpreter.js.map +1 -0
  91. package/dist/events.d.ts +188 -0
  92. package/dist/events.d.ts.map +1 -0
  93. package/dist/events.js +138 -0
  94. package/dist/events.js.map +1 -0
  95. package/dist/expression.d.ts +83 -0
  96. package/dist/expression.d.ts.map +1 -0
  97. package/dist/expression.js +333 -0
  98. package/dist/expression.js.map +1 -0
  99. package/dist/game/cave-cmd.d.ts +195 -0
  100. package/dist/game/cave-cmd.d.ts.map +1 -0
  101. package/dist/game/cave-cmd.js +968 -0
  102. package/dist/game/cave-cmd.js.map +1 -0
  103. package/dist/game/char-sheet.d.ts +189 -0
  104. package/dist/game/char-sheet.d.ts.map +1 -0
  105. package/dist/game/char-sheet.js +381 -0
  106. package/dist/game/char-sheet.js.map +1 -0
  107. package/dist/game/chest.d.ts +94 -0
  108. package/dist/game/chest.d.ts.map +1 -0
  109. package/dist/game/chest.js +267 -0
  110. package/dist/game/chest.js.map +1 -0
  111. package/dist/game/context.d.ts +970 -0
  112. package/dist/game/context.d.ts.map +1 -0
  113. package/dist/game/context.js +353 -0
  114. package/dist/game/context.js.map +1 -0
  115. package/dist/game/curse-tick.d.ts +35 -0
  116. package/dist/game/curse-tick.d.ts.map +1 -0
  117. package/dist/game/curse-tick.js +100 -0
  118. package/dist/game/curse-tick.js.map +1 -0
  119. package/dist/game/describe.d.ts +20 -0
  120. package/dist/game/describe.d.ts.map +1 -0
  121. package/dist/game/describe.js +45 -0
  122. package/dist/game/describe.js.map +1 -0
  123. package/dist/game/display.d.ts +196 -0
  124. package/dist/game/display.d.ts.map +1 -0
  125. package/dist/game/display.js +684 -0
  126. package/dist/game/display.js.map +1 -0
  127. package/dist/game/dump-level.d.ts +37 -0
  128. package/dist/game/dump-level.d.ts.map +1 -0
  129. package/dist/game/dump-level.js +110 -0
  130. package/dist/game/dump-level.js.map +1 -0
  131. package/dist/game/effect-attack.d.ts +36 -0
  132. package/dist/game/effect-attack.d.ts.map +1 -0
  133. package/dist/game/effect-attack.js +694 -0
  134. package/dist/game/effect-attack.js.map +1 -0
  135. package/dist/game/effect-detect.d.ts +34 -0
  136. package/dist/game/effect-detect.d.ts.map +1 -0
  137. package/dist/game/effect-detect.js +456 -0
  138. package/dist/game/effect-detect.js.map +1 -0
  139. package/dist/game/effect-env.d.ts +77 -0
  140. package/dist/game/effect-env.d.ts.map +1 -0
  141. package/dist/game/effect-env.js +166 -0
  142. package/dist/game/effect-env.js.map +1 -0
  143. package/dist/game/effect-game-env.d.ts +76 -0
  144. package/dist/game/effect-game-env.d.ts.map +1 -0
  145. package/dist/game/effect-game-env.js +23 -0
  146. package/dist/game/effect-game-env.js.map +1 -0
  147. package/dist/game/effect-general.d.ts +98 -0
  148. package/dist/game/effect-general.d.ts.map +1 -0
  149. package/dist/game/effect-general.js +914 -0
  150. package/dist/game/effect-general.js.map +1 -0
  151. package/dist/game/effect-item.d.ts +124 -0
  152. package/dist/game/effect-item.d.ts.map +1 -0
  153. package/dist/game/effect-item.js +789 -0
  154. package/dist/game/effect-item.js.map +1 -0
  155. package/dist/game/effect-melee.d.ts +43 -0
  156. package/dist/game/effect-melee.d.ts.map +1 -0
  157. package/dist/game/effect-melee.js +477 -0
  158. package/dist/game/effect-melee.js.map +1 -0
  159. package/dist/game/effect-mon-origin.d.ts +67 -0
  160. package/dist/game/effect-mon-origin.d.ts.map +1 -0
  161. package/dist/game/effect-mon-origin.js +164 -0
  162. package/dist/game/effect-mon-origin.js.map +1 -0
  163. package/dist/game/effect-monster.d.ts +29 -0
  164. package/dist/game/effect-monster.d.ts.map +1 -0
  165. package/dist/game/effect-monster.js +281 -0
  166. package/dist/game/effect-monster.js.map +1 -0
  167. package/dist/game/effect-summon.d.ts +37 -0
  168. package/dist/game/effect-summon.d.ts.map +1 -0
  169. package/dist/game/effect-summon.js +113 -0
  170. package/dist/game/effect-summon.js.map +1 -0
  171. package/dist/game/effect-teleport.d.ts +130 -0
  172. package/dist/game/effect-teleport.d.ts.map +1 -0
  173. package/dist/game/effect-teleport.js +529 -0
  174. package/dist/game/effect-teleport.js.map +1 -0
  175. package/dist/game/effect-terrain.d.ts +91 -0
  176. package/dist/game/effect-terrain.d.ts.map +1 -0
  177. package/dist/game/effect-terrain.js +812 -0
  178. package/dist/game/effect-terrain.js.map +1 -0
  179. package/dist/game/energy.d.ts +45 -0
  180. package/dist/game/energy.d.ts.map +1 -0
  181. package/dist/game/energy.js +51 -0
  182. package/dist/game/energy.js.map +1 -0
  183. package/dist/game/equip-cmp.d.ts +138 -0
  184. package/dist/game/equip-cmp.d.ts.map +1 -0
  185. package/dist/game/equip-cmp.js +363 -0
  186. package/dist/game/equip-cmp.js.map +1 -0
  187. package/dist/game/floor.d.ts +182 -0
  188. package/dist/game/floor.d.ts.map +1 -0
  189. package/dist/game/floor.js +435 -0
  190. package/dist/game/floor.js.map +1 -0
  191. package/dist/game/gear.d.ts +408 -0
  192. package/dist/game/gear.d.ts.map +1 -0
  193. package/dist/game/gear.js +1037 -0
  194. package/dist/game/gear.js.map +1 -0
  195. package/dist/game/harness.d.ts +61 -0
  196. package/dist/game/harness.d.ts.map +1 -0
  197. package/dist/game/harness.js +198 -0
  198. package/dist/game/harness.js.map +1 -0
  199. package/dist/game/history.d.ts +49 -0
  200. package/dist/game/history.d.ts.map +1 -0
  201. package/dist/game/history.js +55 -0
  202. package/dist/game/history.js.map +1 -0
  203. package/dist/game/ignore-cmd.d.ts +42 -0
  204. package/dist/game/ignore-cmd.d.ts.map +1 -0
  205. package/dist/game/ignore-cmd.js +59 -0
  206. package/dist/game/ignore-cmd.js.map +1 -0
  207. package/dist/game/inscription-confirm.d.ts +80 -0
  208. package/dist/game/inscription-confirm.d.ts.map +1 -0
  209. package/dist/game/inscription-confirm.js +95 -0
  210. package/dist/game/inscription-confirm.js.map +1 -0
  211. package/dist/game/known.d.ts +297 -0
  212. package/dist/game/known.d.ts.map +1 -0
  213. package/dist/game/known.js +849 -0
  214. package/dist/game/known.js.map +1 -0
  215. package/dist/game/loop.d.ts +139 -0
  216. package/dist/game/loop.d.ts.map +1 -0
  217. package/dist/game/loop.js +631 -0
  218. package/dist/game/loop.js.map +1 -0
  219. package/dist/game/lore-color.d.ts +25 -0
  220. package/dist/game/lore-color.d.ts.map +1 -0
  221. package/dist/game/lore-color.js +95 -0
  222. package/dist/game/lore-color.js.map +1 -0
  223. package/dist/game/mon-cast.d.ts +150 -0
  224. package/dist/game/mon-cast.d.ts.map +1 -0
  225. package/dist/game/mon-cast.js +301 -0
  226. package/dist/game/mon-cast.js.map +1 -0
  227. package/dist/game/mon-cmd.d.ts +51 -0
  228. package/dist/game/mon-cmd.d.ts.map +1 -0
  229. package/dist/game/mon-cmd.js +670 -0
  230. package/dist/game/mon-cmd.js.map +1 -0
  231. package/dist/game/mon-death.d.ts +143 -0
  232. package/dist/game/mon-death.d.ts.map +1 -0
  233. package/dist/game/mon-death.js +328 -0
  234. package/dist/game/mon-death.js.map +1 -0
  235. package/dist/game/mon-group.d.ts +81 -0
  236. package/dist/game/mon-group.d.ts.map +1 -0
  237. package/dist/game/mon-group.js +323 -0
  238. package/dist/game/mon-group.js.map +1 -0
  239. package/dist/game/mon-list.d.ts +85 -0
  240. package/dist/game/mon-list.d.ts.map +1 -0
  241. package/dist/game/mon-list.js +154 -0
  242. package/dist/game/mon-list.js.map +1 -0
  243. package/dist/game/mon-message.d.ts +100 -0
  244. package/dist/game/mon-message.d.ts.map +1 -0
  245. package/dist/game/mon-message.js +336 -0
  246. package/dist/game/mon-message.js.map +1 -0
  247. package/dist/game/mon-place.d.ts +226 -0
  248. package/dist/game/mon-place.d.ts.map +1 -0
  249. package/dist/game/mon-place.js +642 -0
  250. package/dist/game/mon-place.js.map +1 -0
  251. package/dist/game/mon-ranged.d.ts +87 -0
  252. package/dist/game/mon-ranged.d.ts.map +1 -0
  253. package/dist/game/mon-ranged.js +353 -0
  254. package/dist/game/mon-ranged.js.map +1 -0
  255. package/dist/game/mon-shape.d.ts +43 -0
  256. package/dist/game/mon-shape.d.ts.map +1 -0
  257. package/dist/game/mon-shape.js +115 -0
  258. package/dist/game/mon-shape.js.map +1 -0
  259. package/dist/game/mon-side.d.ts +63 -0
  260. package/dist/game/mon-side.d.ts.map +1 -0
  261. package/dist/game/mon-side.js +403 -0
  262. package/dist/game/mon-side.js.map +1 -0
  263. package/dist/game/monster-turn.d.ts +195 -0
  264. package/dist/game/monster-turn.d.ts.map +1 -0
  265. package/dist/game/monster-turn.js +1560 -0
  266. package/dist/game/monster-turn.js.map +1 -0
  267. package/dist/game/obj-cmd.d.ts +453 -0
  268. package/dist/game/obj-cmd.d.ts.map +1 -0
  269. package/dist/game/obj-cmd.js +1530 -0
  270. package/dist/game/obj-cmd.js.map +1 -0
  271. package/dist/game/obj-list.d.ts +92 -0
  272. package/dist/game/obj-list.d.ts.map +1 -0
  273. package/dist/game/obj-list.js +251 -0
  274. package/dist/game/obj-list.js.map +1 -0
  275. package/dist/game/object-inspect.d.ts +37 -0
  276. package/dist/game/object-inspect.d.ts.map +1 -0
  277. package/dist/game/object-inspect.js +106 -0
  278. package/dist/game/object-inspect.js.map +1 -0
  279. package/dist/game/pickup.d.ts +126 -0
  280. package/dist/game/pickup.d.ts.map +1 -0
  281. package/dist/game/pickup.js +367 -0
  282. package/dist/game/pickup.js.map +1 -0
  283. package/dist/game/player-path.d.ts +133 -0
  284. package/dist/game/player-path.d.ts.map +1 -0
  285. package/dist/game/player-path.js +1268 -0
  286. package/dist/game/player-path.js.map +1 -0
  287. package/dist/game/player-side.d.ts +59 -0
  288. package/dist/game/player-side.d.ts.map +1 -0
  289. package/dist/game/player-side.js +506 -0
  290. package/dist/game/player-side.js.map +1 -0
  291. package/dist/game/player-turn.d.ts +161 -0
  292. package/dist/game/player-turn.d.ts.map +1 -0
  293. package/dist/game/player-turn.js +739 -0
  294. package/dist/game/player-turn.js.map +1 -0
  295. package/dist/game/project-cast.d.ts +268 -0
  296. package/dist/game/project-cast.d.ts.map +1 -0
  297. package/dist/game/project-cast.js +496 -0
  298. package/dist/game/project-cast.js.map +1 -0
  299. package/dist/game/project-feat.d.ts +47 -0
  300. package/dist/game/project-feat.d.ts.map +1 -0
  301. package/dist/game/project-feat.js +350 -0
  302. package/dist/game/project-feat.js.map +1 -0
  303. package/dist/game/project-monster.d.ts +106 -0
  304. package/dist/game/project-monster.d.ts.map +1 -0
  305. package/dist/game/project-monster.js +272 -0
  306. package/dist/game/project-monster.js.map +1 -0
  307. package/dist/game/project-obj.d.ts +60 -0
  308. package/dist/game/project-obj.d.ts.map +1 -0
  309. package/dist/game/project-obj.js +245 -0
  310. package/dist/game/project-obj.js.map +1 -0
  311. package/dist/game/project-player.d.ts +120 -0
  312. package/dist/game/project-player.d.ts.map +1 -0
  313. package/dist/game/project-player.js +108 -0
  314. package/dist/game/project-player.js.map +1 -0
  315. package/dist/game/quest.d.ts +101 -0
  316. package/dist/game/quest.d.ts.map +1 -0
  317. package/dist/game/quest.js +192 -0
  318. package/dist/game/quest.js.map +1 -0
  319. package/dist/game/ranged-cmd.d.ts +32 -0
  320. package/dist/game/ranged-cmd.d.ts.map +1 -0
  321. package/dist/game/ranged-cmd.js +372 -0
  322. package/dist/game/ranged-cmd.js.map +1 -0
  323. package/dist/game/scheduler.d.ts +47 -0
  324. package/dist/game/scheduler.d.ts.map +1 -0
  325. package/dist/game/scheduler.js +141 -0
  326. package/dist/game/scheduler.js.map +1 -0
  327. package/dist/game/spell-cmd.d.ts +104 -0
  328. package/dist/game/spell-cmd.d.ts.map +1 -0
  329. package/dist/game/spell-cmd.js +320 -0
  330. package/dist/game/spell-cmd.js.map +1 -0
  331. package/dist/game/spoil.d.ts +28 -0
  332. package/dist/game/spoil.d.ts.map +1 -0
  333. package/dist/game/spoil.js +435 -0
  334. package/dist/game/spoil.js.map +1 -0
  335. package/dist/game/steal.d.ts +35 -0
  336. package/dist/game/steal.d.ts.map +1 -0
  337. package/dist/game/steal.js +145 -0
  338. package/dist/game/steal.js.map +1 -0
  339. package/dist/game/take-hit-hooks.d.ts +52 -0
  340. package/dist/game/take-hit-hooks.d.ts.map +1 -0
  341. package/dist/game/take-hit-hooks.js +127 -0
  342. package/dist/game/take-hit-hooks.js.map +1 -0
  343. package/dist/game/target-loop.d.ts +119 -0
  344. package/dist/game/target-loop.d.ts.map +1 -0
  345. package/dist/game/target-loop.js +408 -0
  346. package/dist/game/target-loop.js.map +1 -0
  347. package/dist/game/target.d.ts +133 -0
  348. package/dist/game/target.d.ts.map +1 -0
  349. package/dist/game/target.js +353 -0
  350. package/dist/game/target.js.map +1 -0
  351. package/dist/game/thrust.d.ts +37 -0
  352. package/dist/game/thrust.d.ts.map +1 -0
  353. package/dist/game/thrust.js +155 -0
  354. package/dist/game/thrust.js.map +1 -0
  355. package/dist/game/trap.d.ts +218 -0
  356. package/dist/game/trap.d.ts.map +1 -0
  357. package/dist/game/trap.js +618 -0
  358. package/dist/game/trap.js.map +1 -0
  359. package/dist/game/ui-entry.d.ts +282 -0
  360. package/dist/game/ui-entry.d.ts.map +1 -0
  361. package/dist/game/ui-entry.js +1838 -0
  362. package/dist/game/ui-entry.js.map +1 -0
  363. package/dist/game/wizard.d.ts +654 -0
  364. package/dist/game/wizard.d.ts.map +1 -0
  365. package/dist/game/wizard.js +1323 -0
  366. package/dist/game/wizard.js.map +1 -0
  367. package/dist/game/world.d.ts +183 -0
  368. package/dist/game/world.d.ts.map +1 -0
  369. package/dist/game/world.js +685 -0
  370. package/dist/game/world.js.map +1 -0
  371. package/dist/gen/cave.d.ts +247 -0
  372. package/dist/gen/cave.d.ts.map +1 -0
  373. package/dist/gen/cave.js +2406 -0
  374. package/dist/gen/cave.js.map +1 -0
  375. package/dist/gen/gen-monster.d.ts +151 -0
  376. package/dist/gen/gen-monster.d.ts.map +1 -0
  377. package/dist/gen/gen-monster.js +473 -0
  378. package/dist/gen/gen-monster.js.map +1 -0
  379. package/dist/gen/generate.d.ts +240 -0
  380. package/dist/gen/generate.d.ts.map +1 -0
  381. package/dist/gen/generate.js +358 -0
  382. package/dist/gen/generate.js.map +1 -0
  383. package/dist/gen/room.d.ts +160 -0
  384. package/dist/gen/room.d.ts.map +1 -0
  385. package/dist/gen/room.js +1910 -0
  386. package/dist/gen/room.js.map +1 -0
  387. package/dist/gen/util.d.ts +466 -0
  388. package/dist/gen/util.d.ts.map +1 -0
  389. package/dist/gen/util.js +1553 -0
  390. package/dist/gen/util.js.map +1 -0
  391. package/dist/generated/dun-profiles.d.ts +44 -0
  392. package/dist/generated/dun-profiles.d.ts.map +1 -0
  393. package/dist/generated/dun-profiles.js +28 -0
  394. package/dist/generated/dun-profiles.js.map +1 -0
  395. package/dist/generated/effects.d.ts +1021 -0
  396. package/dist/generated/effects.d.ts.map +1 -0
  397. package/dist/generated/effects.js +239 -0
  398. package/dist/generated/effects.js.map +1 -0
  399. package/dist/generated/elements.d.ts +83 -0
  400. package/dist/generated/elements.d.ts.map +1 -0
  401. package/dist/generated/elements.js +60 -0
  402. package/dist/generated/elements.js.map +1 -0
  403. package/dist/generated/equip-slots.d.ts +104 -0
  404. package/dist/generated/equip-slots.d.ts.map +1 -0
  405. package/dist/generated/equip-slots.js +34 -0
  406. package/dist/generated/equip-slots.js.map +1 -0
  407. package/dist/generated/history-types.d.ts +52 -0
  408. package/dist/generated/history-types.d.ts.map +1 -0
  409. package/dist/generated/history-types.js +32 -0
  410. package/dist/generated/history-types.js.map +1 -0
  411. package/dist/generated/ignore-types.d.ts +116 -0
  412. package/dist/generated/ignore-types.d.ts.map +1 -0
  413. package/dist/generated/ignore-types.js +64 -0
  414. package/dist/generated/ignore-types.js.map +1 -0
  415. package/dist/generated/index.d.ts +32 -0
  416. package/dist/generated/index.d.ts.map +1 -0
  417. package/dist/generated/index.js +33 -0
  418. package/dist/generated/index.js.map +1 -0
  419. package/dist/generated/kind-flags.d.ts +72 -0
  420. package/dist/generated/kind-flags.d.ts.map +1 -0
  421. package/dist/generated/kind-flags.js +42 -0
  422. package/dist/generated/kind-flags.js.map +1 -0
  423. package/dist/generated/message.d.ts +624 -0
  424. package/dist/generated/message.d.ts.map +1 -0
  425. package/dist/generated/message.js +318 -0
  426. package/dist/generated/message.js.map +1 -0
  427. package/dist/generated/mon-message.d.ts +386 -0
  428. package/dist/generated/mon-message.d.ts.map +1 -0
  429. package/dist/generated/mon-message.js +136 -0
  430. package/dist/generated/mon-message.js.map +1 -0
  431. package/dist/generated/mon-race-flags.d.ts +433 -0
  432. package/dist/generated/mon-race-flags.d.ts.map +1 -0
  433. package/dist/generated/mon-race-flags.js +180 -0
  434. package/dist/generated/mon-race-flags.js.map +1 -0
  435. package/dist/generated/mon-spells.d.ts +380 -0
  436. package/dist/generated/mon-spells.d.ts.map +1 -0
  437. package/dist/generated/mon-spells.js +196 -0
  438. package/dist/generated/mon-spells.js.map +1 -0
  439. package/dist/generated/mon-temp-flags.d.ts +52 -0
  440. package/dist/generated/mon-temp-flags.d.ts.map +1 -0
  441. package/dist/generated/mon-temp-flags.js +32 -0
  442. package/dist/generated/mon-temp-flags.js.map +1 -0
  443. package/dist/generated/mon-timed.d.ts +118 -0
  444. package/dist/generated/mon-timed.d.ts.map +1 -0
  445. package/dist/generated/mon-timed.js +32 -0
  446. package/dist/generated/mon-timed.js.map +1 -0
  447. package/dist/generated/object-flags.d.ts +169 -0
  448. package/dist/generated/object-flags.d.ts.map +1 -0
  449. package/dist/generated/object-flags.js +93 -0
  450. package/dist/generated/object-flags.js.map +1 -0
  451. package/dist/generated/object-modifiers.d.ts +50 -0
  452. package/dist/generated/object-modifiers.d.ts.map +1 -0
  453. package/dist/generated/object-modifiers.js +41 -0
  454. package/dist/generated/object-modifiers.js.map +1 -0
  455. package/dist/generated/options.d.ts +284 -0
  456. package/dist/generated/options.d.ts.map +1 -0
  457. package/dist/generated/options.js +102 -0
  458. package/dist/generated/options.js.map +1 -0
  459. package/dist/generated/origins.d.ts +138 -0
  460. package/dist/generated/origins.d.ts.map +1 -0
  461. package/dist/generated/origins.js +62 -0
  462. package/dist/generated/origins.js.map +1 -0
  463. package/dist/generated/parser-errors.d.ts +264 -0
  464. package/dist/generated/parser-errors.d.ts.map +1 -0
  465. package/dist/generated/parser-errors.js +138 -0
  466. package/dist/generated/parser-errors.js.map +1 -0
  467. package/dist/generated/player-flags.d.ts +62 -0
  468. package/dist/generated/player-flags.d.ts.map +1 -0
  469. package/dist/generated/player-flags.js +46 -0
  470. package/dist/generated/player-flags.js.map +1 -0
  471. package/dist/generated/player-timed.d.ts +273 -0
  472. package/dist/generated/player-timed.d.ts.map +1 -0
  473. package/dist/generated/player-timed.js +116 -0
  474. package/dist/generated/player-timed.js.map +1 -0
  475. package/dist/generated/projections.d.ts +130 -0
  476. package/dist/generated/projections.d.ts.map +1 -0
  477. package/dist/generated/projections.js +101 -0
  478. package/dist/generated/projections.js.map +1 -0
  479. package/dist/generated/randart-properties.d.ts +388 -0
  480. package/dist/generated/randart-properties.d.ts.map +1 -0
  481. package/dist/generated/randart-properties.js +200 -0
  482. package/dist/generated/randart-properties.js.map +1 -0
  483. package/dist/generated/room-flags.d.ts +21 -0
  484. package/dist/generated/room-flags.d.ts.map +1 -0
  485. package/dist/generated/room-flags.js +19 -0
  486. package/dist/generated/room-flags.js.map +1 -0
  487. package/dist/generated/rooms.d.ts +122 -0
  488. package/dist/generated/rooms.d.ts.map +1 -0
  489. package/dist/generated/rooms.js +48 -0
  490. package/dist/generated/rooms.js.map +1 -0
  491. package/dist/generated/square-flags.d.ts +96 -0
  492. package/dist/generated/square-flags.d.ts.map +1 -0
  493. package/dist/generated/square-flags.js +54 -0
  494. package/dist/generated/square-flags.js.map +1 -0
  495. package/dist/generated/stats.d.ts +23 -0
  496. package/dist/generated/stats.d.ts.map +1 -0
  497. package/dist/generated/stats.js +20 -0
  498. package/dist/generated/stats.js.map +1 -0
  499. package/dist/generated/terrain-flags.d.ts +136 -0
  500. package/dist/generated/terrain-flags.d.ts.map +1 -0
  501. package/dist/generated/terrain-flags.js +74 -0
  502. package/dist/generated/terrain-flags.js.map +1 -0
  503. package/dist/generated/terrain.d.ts +83 -0
  504. package/dist/generated/terrain.d.ts.map +1 -0
  505. package/dist/generated/terrain.js +60 -0
  506. package/dist/generated/terrain.js.map +1 -0
  507. package/dist/generated/trap-flags.d.ts +68 -0
  508. package/dist/generated/trap-flags.d.ts.map +1 -0
  509. package/dist/generated/trap-flags.js +40 -0
  510. package/dist/generated/trap-flags.js.map +1 -0
  511. package/dist/generated/tvals.d.ts +152 -0
  512. package/dist/generated/tvals.d.ts.map +1 -0
  513. package/dist/generated/tvals.js +82 -0
  514. package/dist/generated/tvals.js.map +1 -0
  515. package/dist/generated/ui-entry-renderers.d.ts +62 -0
  516. package/dist/generated/ui-entry-renderers.d.ts.map +1 -0
  517. package/dist/generated/ui-entry-renderers.js +22 -0
  518. package/dist/generated/ui-entry-renderers.js.map +1 -0
  519. package/dist/guard.d.ts +43 -0
  520. package/dist/guard.d.ts.map +1 -0
  521. package/dist/guard.js +65 -0
  522. package/dist/guard.js.map +1 -0
  523. package/dist/host/args.d.ts +129 -0
  524. package/dist/host/args.d.ts.map +1 -0
  525. package/dist/host/args.js +251 -0
  526. package/dist/host/args.js.map +1 -0
  527. package/dist/host/bridge.d.ts +61 -0
  528. package/dist/host/bridge.d.ts.map +1 -0
  529. package/dist/host/bridge.js +176 -0
  530. package/dist/host/bridge.js.map +1 -0
  531. package/dist/host/index.d.ts +18 -0
  532. package/dist/host/index.d.ts.map +1 -0
  533. package/dist/host/index.js +18 -0
  534. package/dist/host/index.js.map +1 -0
  535. package/dist/host/io.d.ts +169 -0
  536. package/dist/host/io.d.ts.map +1 -0
  537. package/dist/host/io.js +137 -0
  538. package/dist/host/io.js.map +1 -0
  539. package/dist/host/memory.d.ts +56 -0
  540. package/dist/host/memory.d.ts.map +1 -0
  541. package/dist/host/memory.js +105 -0
  542. package/dist/host/memory.js.map +1 -0
  543. package/dist/host/raw.d.ts +114 -0
  544. package/dist/host/raw.d.ts.map +1 -0
  545. package/dist/host/raw.js +107 -0
  546. package/dist/host/raw.js.map +1 -0
  547. package/dist/index.d.ts +180 -0
  548. package/dist/index.d.ts.map +1 -0
  549. package/dist/index.js +222 -0
  550. package/dist/index.js.map +1 -0
  551. package/dist/loc.d.ts +63 -0
  552. package/dist/loc.d.ts.map +1 -0
  553. package/dist/loc.js +128 -0
  554. package/dist/loc.js.map +1 -0
  555. package/dist/mod/hooks.d.ts +204 -0
  556. package/dist/mod/hooks.d.ts.map +1 -0
  557. package/dist/mod/hooks.js +161 -0
  558. package/dist/mod/hooks.js.map +1 -0
  559. package/dist/mod/ids.d.ts +160 -0
  560. package/dist/mod/ids.d.ts.map +1 -0
  561. package/dist/mod/ids.js +343 -0
  562. package/dist/mod/ids.js.map +1 -0
  563. package/dist/mod/registry-host.d.ts +156 -0
  564. package/dist/mod/registry-host.d.ts.map +1 -0
  565. package/dist/mod/registry-host.js +142 -0
  566. package/dist/mod/registry-host.js.map +1 -0
  567. package/dist/mod/save-blocks.d.ts +201 -0
  568. package/dist/mod/save-blocks.d.ts.map +1 -0
  569. package/dist/mod/save-blocks.js +640 -0
  570. package/dist/mod/save-blocks.js.map +1 -0
  571. package/dist/mod/vocabulary.d.ts +104 -0
  572. package/dist/mod/vocabulary.d.ts.map +1 -0
  573. package/dist/mod/vocabulary.js +0 -0
  574. package/dist/mod/vocabulary.js.map +1 -0
  575. package/dist/mon/bind.d.ts +266 -0
  576. package/dist/mon/bind.d.ts.map +1 -0
  577. package/dist/mon/bind.js +610 -0
  578. package/dist/mon/bind.js.map +1 -0
  579. package/dist/mon/desc.d.ts +72 -0
  580. package/dist/mon/desc.d.ts.map +1 -0
  581. package/dist/mon/desc.js +237 -0
  582. package/dist/mon/desc.js.map +1 -0
  583. package/dist/mon/knowledge-groups.d.ts +61 -0
  584. package/dist/mon/knowledge-groups.d.ts.map +1 -0
  585. package/dist/mon/knowledge-groups.js +121 -0
  586. package/dist/mon/knowledge-groups.js.map +1 -0
  587. package/dist/mon/lore-describe.d.ts +216 -0
  588. package/dist/mon/lore-describe.d.ts.map +1 -0
  589. package/dist/mon/lore-describe.js +1105 -0
  590. package/dist/mon/lore-describe.js.map +1 -0
  591. package/dist/mon/lore.d.ts +117 -0
  592. package/dist/mon/lore.d.ts.map +1 -0
  593. package/dist/mon/lore.js +221 -0
  594. package/dist/mon/lore.js.map +1 -0
  595. package/dist/mon/make.d.ts +147 -0
  596. package/dist/mon/make.d.ts.map +1 -0
  597. package/dist/mon/make.js +353 -0
  598. package/dist/mon/make.js.map +1 -0
  599. package/dist/mon/monster.d.ts +94 -0
  600. package/dist/mon/monster.d.ts.map +1 -0
  601. package/dist/mon/monster.js +90 -0
  602. package/dist/mon/monster.js.map +1 -0
  603. package/dist/mon/predicate.d.ts +87 -0
  604. package/dist/mon/predicate.d.ts.map +1 -0
  605. package/dist/mon/predicate.js +190 -0
  606. package/dist/mon/predicate.js.map +1 -0
  607. package/dist/mon/project-mon.d.ts +129 -0
  608. package/dist/mon/project-mon.d.ts.map +1 -0
  609. package/dist/mon/project-mon.js +652 -0
  610. package/dist/mon/project-mon.js.map +1 -0
  611. package/dist/mon/spell.d.ts +155 -0
  612. package/dist/mon/spell.d.ts.map +1 -0
  613. package/dist/mon/spell.js +355 -0
  614. package/dist/mon/spell.js.map +1 -0
  615. package/dist/mon/steal.d.ts +112 -0
  616. package/dist/mon/steal.d.ts.map +1 -0
  617. package/dist/mon/steal.js +163 -0
  618. package/dist/mon/steal.js.map +1 -0
  619. package/dist/mon/summon.d.ts +62 -0
  620. package/dist/mon/summon.d.ts.map +1 -0
  621. package/dist/mon/summon.js +111 -0
  622. package/dist/mon/summon.js.map +1 -0
  623. package/dist/mon/take-hit.d.ts +75 -0
  624. package/dist/mon/take-hit.d.ts.map +1 -0
  625. package/dist/mon/take-hit.js +122 -0
  626. package/dist/mon/take-hit.js.map +1 -0
  627. package/dist/mon/timed.d.ts +74 -0
  628. package/dist/mon/timed.d.ts.map +1 -0
  629. package/dist/mon/timed.js +219 -0
  630. package/dist/mon/timed.js.map +1 -0
  631. package/dist/mon/types.d.ts +252 -0
  632. package/dist/mon/types.d.ts.map +1 -0
  633. package/dist/mon/types.js +46 -0
  634. package/dist/mon/types.js.map +1 -0
  635. package/dist/msg.d.ts +71 -0
  636. package/dist/msg.d.ts.map +1 -0
  637. package/dist/msg.js +126 -0
  638. package/dist/msg.js.map +1 -0
  639. package/dist/obj/artifact-fake.d.ts +39 -0
  640. package/dist/obj/artifact-fake.d.ts.map +1 -0
  641. package/dist/obj/artifact-fake.js +52 -0
  642. package/dist/obj/artifact-fake.js.map +1 -0
  643. package/dist/obj/artifact-known.d.ts +50 -0
  644. package/dist/obj/artifact-known.d.ts.map +1 -0
  645. package/dist/obj/artifact-known.js +55 -0
  646. package/dist/obj/artifact-known.js.map +1 -0
  647. package/dist/obj/bind.d.ts +219 -0
  648. package/dist/obj/bind.d.ts.map +1 -0
  649. package/dist/obj/bind.js +1239 -0
  650. package/dist/obj/bind.js.map +1 -0
  651. package/dist/obj/chest.d.ts +91 -0
  652. package/dist/obj/chest.d.ts.map +1 -0
  653. package/dist/obj/chest.js +220 -0
  654. package/dist/obj/chest.js.map +1 -0
  655. package/dist/obj/desc.d.ts +79 -0
  656. package/dist/obj/desc.d.ts.map +1 -0
  657. package/dist/obj/desc.js +602 -0
  658. package/dist/obj/desc.js.map +1 -0
  659. package/dist/obj/effects-info.d.ts +78 -0
  660. package/dist/obj/effects-info.d.ts.map +1 -0
  661. package/dist/obj/effects-info.js +240 -0
  662. package/dist/obj/effects-info.js.map +1 -0
  663. package/dist/obj/flavor.d.ts +68 -0
  664. package/dist/obj/flavor.d.ts.map +1 -0
  665. package/dist/obj/flavor.js +201 -0
  666. package/dist/obj/flavor.js.map +1 -0
  667. package/dist/obj/ignore.d.ts +143 -0
  668. package/dist/obj/ignore.d.ts.map +1 -0
  669. package/dist/obj/ignore.js +345 -0
  670. package/dist/obj/ignore.js.map +1 -0
  671. package/dist/obj/knowledge.d.ts +520 -0
  672. package/dist/obj/knowledge.d.ts.map +1 -0
  673. package/dist/obj/knowledge.js +1364 -0
  674. package/dist/obj/knowledge.js.map +1 -0
  675. package/dist/obj/known-object.d.ts +293 -0
  676. package/dist/obj/known-object.d.ts.map +1 -0
  677. package/dist/obj/known-object.js +532 -0
  678. package/dist/obj/known-object.js.map +1 -0
  679. package/dist/obj/make.d.ts +292 -0
  680. package/dist/obj/make.d.ts.map +1 -0
  681. package/dist/obj/make.js +1055 -0
  682. package/dist/obj/make.js.map +1 -0
  683. package/dist/obj/object-info.d.ts +188 -0
  684. package/dist/obj/object-info.d.ts.map +1 -0
  685. package/dist/obj/object-info.js +1407 -0
  686. package/dist/obj/object-info.js.map +1 -0
  687. package/dist/obj/object.d.ts +335 -0
  688. package/dist/obj/object.d.ts.map +1 -0
  689. package/dist/obj/object.js +1051 -0
  690. package/dist/obj/object.js.map +1 -0
  691. package/dist/obj/power.d.ts +65 -0
  692. package/dist/obj/power.d.ts.map +1 -0
  693. package/dist/obj/power.js +580 -0
  694. package/dist/obj/power.js.map +1 -0
  695. package/dist/obj/randart-build.d.ts +223 -0
  696. package/dist/obj/randart-build.d.ts.map +1 -0
  697. package/dist/obj/randart-build.js +1444 -0
  698. package/dist/obj/randart-build.js.map +1 -0
  699. package/dist/obj/randart-data.d.ts +179 -0
  700. package/dist/obj/randart-data.d.ts.map +1 -0
  701. package/dist/obj/randart-data.js +1153 -0
  702. package/dist/obj/randart-data.js.map +1 -0
  703. package/dist/obj/randart.d.ts +128 -0
  704. package/dist/obj/randart.d.ts.map +1 -0
  705. package/dist/obj/randart.js +458 -0
  706. package/dist/obj/randart.js.map +1 -0
  707. package/dist/obj/randname.d.ts +33 -0
  708. package/dist/obj/randname.d.ts.map +1 -0
  709. package/dist/obj/randname.js +116 -0
  710. package/dist/obj/randname.js.map +1 -0
  711. package/dist/obj/recharge.d.ts +28 -0
  712. package/dist/obj/recharge.d.ts.map +1 -0
  713. package/dist/obj/recharge.js +52 -0
  714. package/dist/obj/recharge.js.map +1 -0
  715. package/dist/obj/types.d.ts +584 -0
  716. package/dist/obj/types.d.ts.map +1 -0
  717. package/dist/obj/types.js +104 -0
  718. package/dist/obj/types.js.map +1 -0
  719. package/dist/obj/value.d.ts +64 -0
  720. package/dist/obj/value.d.ts.map +1 -0
  721. package/dist/obj/value.js +176 -0
  722. package/dist/obj/value.js.map +1 -0
  723. package/dist/player/abilities.d.ts +57 -0
  724. package/dist/player/abilities.d.ts.map +1 -0
  725. package/dist/player/abilities.js +118 -0
  726. package/dist/player/abilities.js.map +1 -0
  727. package/dist/player/best-digger.d.ts +42 -0
  728. package/dist/player/best-digger.d.ts.map +1 -0
  729. package/dist/player/best-digger.js +89 -0
  730. package/dist/player/best-digger.js.map +1 -0
  731. package/dist/player/bind.d.ts +270 -0
  732. package/dist/player/bind.d.ts.map +1 -0
  733. package/dist/player/bind.js +698 -0
  734. package/dist/player/bind.js.map +1 -0
  735. package/dist/player/birth.d.ts +258 -0
  736. package/dist/player/birth.d.ts.map +1 -0
  737. package/dist/player/birth.js +639 -0
  738. package/dist/player/birth.js.map +1 -0
  739. package/dist/player/calcs.d.ts +403 -0
  740. package/dist/player/calcs.d.ts.map +1 -0
  741. package/dist/player/calcs.js +1189 -0
  742. package/dist/player/calcs.js.map +1 -0
  743. package/dist/player/combat-regen.d.ts +14 -0
  744. package/dist/player/combat-regen.d.ts.map +1 -0
  745. package/dist/player/combat-regen.js +29 -0
  746. package/dist/player/combat-regen.js.map +1 -0
  747. package/dist/player/exp.d.ts +96 -0
  748. package/dist/player/exp.d.ts.map +1 -0
  749. package/dist/player/exp.js +229 -0
  750. package/dist/player/exp.js.map +1 -0
  751. package/dist/player/history.d.ts +91 -0
  752. package/dist/player/history.d.ts.map +1 -0
  753. package/dist/player/history.js +145 -0
  754. package/dist/player/history.js.map +1 -0
  755. package/dist/player/options.d.ts +123 -0
  756. package/dist/player/options.d.ts.map +1 -0
  757. package/dist/player/options.js +199 -0
  758. package/dist/player/options.js.map +1 -0
  759. package/dist/player/player.d.ts +242 -0
  760. package/dist/player/player.d.ts.map +1 -0
  761. package/dist/player/player.js +119 -0
  762. package/dist/player/player.js.map +1 -0
  763. package/dist/player/shape-lore.d.ts +56 -0
  764. package/dist/player/shape-lore.d.ts.map +1 -0
  765. package/dist/player/shape-lore.js +221 -0
  766. package/dist/player/shape-lore.js.map +1 -0
  767. package/dist/player/spell.d.ts +146 -0
  768. package/dist/player/spell.d.ts.map +1 -0
  769. package/dist/player/spell.js +519 -0
  770. package/dist/player/spell.js.map +1 -0
  771. package/dist/player/take-hit.d.ts +87 -0
  772. package/dist/player/take-hit.d.ts.map +1 -0
  773. package/dist/player/take-hit.js +99 -0
  774. package/dist/player/take-hit.js.map +1 -0
  775. package/dist/player/timed.d.ts +205 -0
  776. package/dist/player/timed.d.ts.map +1 -0
  777. package/dist/player/timed.js +310 -0
  778. package/dist/player/timed.js.map +1 -0
  779. package/dist/player/types.d.ts +390 -0
  780. package/dist/player/types.d.ts.map +1 -0
  781. package/dist/player/types.js +51 -0
  782. package/dist/player/types.js.map +1 -0
  783. package/dist/rational.d.ts +99 -0
  784. package/dist/rational.d.ts.map +1 -0
  785. package/dist/rational.js +345 -0
  786. package/dist/rational.js.map +1 -0
  787. package/dist/rng.d.ts +168 -0
  788. package/dist/rng.d.ts.map +1 -0
  789. package/dist/rng.js +404 -0
  790. package/dist/rng.js.map +1 -0
  791. package/dist/save/buffer.d.ts +120 -0
  792. package/dist/save/buffer.d.ts.map +1 -0
  793. package/dist/save/buffer.js +238 -0
  794. package/dist/save/buffer.js.map +1 -0
  795. package/dist/save/compress.d.ts +79 -0
  796. package/dist/save/compress.d.ts.map +1 -0
  797. package/dist/save/compress.js +97 -0
  798. package/dist/save/compress.js.map +1 -0
  799. package/dist/save/description.d.ts +61 -0
  800. package/dist/save/description.d.ts.map +1 -0
  801. package/dist/save/description.js +51 -0
  802. package/dist/save/description.js.map +1 -0
  803. package/dist/save/integrity.d.ts +59 -0
  804. package/dist/save/integrity.d.ts.map +1 -0
  805. package/dist/save/integrity.js +103 -0
  806. package/dist/save/integrity.js.map +1 -0
  807. package/dist/score/display.d.ts +40 -0
  808. package/dist/score/display.d.ts.map +1 -0
  809. package/dist/score/display.js +86 -0
  810. package/dist/score/display.js.map +1 -0
  811. package/dist/score/score.d.ts +166 -0
  812. package/dist/score/score.d.ts.map +1 -0
  813. package/dist/score/score.js +244 -0
  814. package/dist/score/score.js.map +1 -0
  815. package/dist/score/types.d.ts +98 -0
  816. package/dist/score/types.d.ts.map +1 -0
  817. package/dist/score/types.js +27 -0
  818. package/dist/score/types.js.map +1 -0
  819. package/dist/session/boot.d.ts +207 -0
  820. package/dist/session/boot.d.ts.map +1 -0
  821. package/dist/session/boot.js +151 -0
  822. package/dist/session/boot.js.map +1 -0
  823. package/dist/session/game.d.ts +257 -0
  824. package/dist/session/game.d.ts.map +1 -0
  825. package/dist/session/game.js +2880 -0
  826. package/dist/session/game.js.map +1 -0
  827. package/dist/session/save.d.ts +847 -0
  828. package/dist/session/save.d.ts.map +1 -0
  829. package/dist/session/save.js +1306 -0
  830. package/dist/session/save.js.map +1 -0
  831. package/dist/sound/engine.d.ts +146 -0
  832. package/dist/sound/engine.d.ts.map +1 -0
  833. package/dist/sound/engine.js +332 -0
  834. package/dist/sound/engine.js.map +1 -0
  835. package/dist/sound/index.d.ts +13 -0
  836. package/dist/sound/index.d.ts.map +1 -0
  837. package/dist/sound/index.js +13 -0
  838. package/dist/sound/index.js.map +1 -0
  839. package/dist/sound/sound-prefs-data.d.ts +10 -0
  840. package/dist/sound/sound-prefs-data.d.ts.map +1 -0
  841. package/dist/sound/sound-prefs-data.js +165 -0
  842. package/dist/sound/sound-prefs-data.js.map +1 -0
  843. package/dist/sound/types.d.ts +64 -0
  844. package/dist/sound/types.d.ts.map +1 -0
  845. package/dist/sound/types.js +23 -0
  846. package/dist/sound/types.js.map +1 -0
  847. package/dist/store/bind.d.ts +43 -0
  848. package/dist/store/bind.d.ts.map +1 -0
  849. package/dist/store/bind.js +130 -0
  850. package/dist/store/bind.js.map +1 -0
  851. package/dist/store/price.d.ts +27 -0
  852. package/dist/store/price.d.ts.map +1 -0
  853. package/dist/store/price.js +81 -0
  854. package/dist/store/price.js.map +1 -0
  855. package/dist/store/store-cmd.d.ts +51 -0
  856. package/dist/store/store-cmd.d.ts.map +1 -0
  857. package/dist/store/store-cmd.js +73 -0
  858. package/dist/store/store-cmd.js.map +1 -0
  859. package/dist/store/store.d.ts +156 -0
  860. package/dist/store/store.d.ts.map +1 -0
  861. package/dist/store/store.js +616 -0
  862. package/dist/store/store.js.map +1 -0
  863. package/dist/store/transact.d.ts +198 -0
  864. package/dist/store/transact.d.ts.map +1 -0
  865. package/dist/store/transact.js +383 -0
  866. package/dist/store/transact.js.map +1 -0
  867. package/dist/store/types.d.ts +101 -0
  868. package/dist/store/types.d.ts.map +1 -0
  869. package/dist/store/types.js +14 -0
  870. package/dist/store/types.js.map +1 -0
  871. package/dist/version.d.ts +25 -0
  872. package/dist/version.d.ts.map +1 -0
  873. package/dist/version.js +25 -0
  874. package/dist/version.js.map +1 -0
  875. package/dist/visuals/engine.d.ts +223 -0
  876. package/dist/visuals/engine.d.ts.map +1 -0
  877. package/dist/visuals/engine.js +357 -0
  878. package/dist/visuals/engine.js.map +1 -0
  879. package/dist/visuals/glyph-table.d.ts +112 -0
  880. package/dist/visuals/glyph-table.d.ts.map +1 -0
  881. package/dist/visuals/glyph-table.js +156 -0
  882. package/dist/visuals/glyph-table.js.map +1 -0
  883. package/dist/visuals/grafmode-data.d.ts +4 -0
  884. package/dist/visuals/grafmode-data.d.ts.map +1 -0
  885. package/dist/visuals/grafmode-data.js +88 -0
  886. package/dist/visuals/grafmode-data.js.map +1 -0
  887. package/dist/visuals/grafmode.d.ts +83 -0
  888. package/dist/visuals/grafmode.d.ts.map +1 -0
  889. package/dist/visuals/grafmode.js +82 -0
  890. package/dist/visuals/grafmode.js.map +1 -0
  891. package/dist/visuals/index.d.ts +19 -0
  892. package/dist/visuals/index.d.ts.map +1 -0
  893. package/dist/visuals/index.js +19 -0
  894. package/dist/visuals/index.js.map +1 -0
  895. package/dist/visuals/map-text.d.ts +79 -0
  896. package/dist/visuals/map-text.d.ts.map +1 -0
  897. package/dist/visuals/map-text.js +122 -0
  898. package/dist/visuals/map-text.js.map +1 -0
  899. package/dist/visuals/pref-expr.d.ts +56 -0
  900. package/dist/visuals/pref-expr.d.ts.map +1 -0
  901. package/dist/visuals/pref-expr.js +173 -0
  902. package/dist/visuals/pref-expr.js.map +1 -0
  903. package/dist/visuals/prefs.d.ts +280 -0
  904. package/dist/visuals/prefs.d.ts.map +1 -0
  905. package/dist/visuals/prefs.js +835 -0
  906. package/dist/visuals/prefs.js.map +1 -0
  907. package/dist/visuals/tile-prefs.d.ts +132 -0
  908. package/dist/visuals/tile-prefs.d.ts.map +1 -0
  909. package/dist/visuals/tile-prefs.js +105 -0
  910. package/dist/visuals/tile-prefs.js.map +1 -0
  911. package/dist/world/chunk.d.ts +195 -0
  912. package/dist/world/chunk.d.ts.map +1 -0
  913. package/dist/world/chunk.js +422 -0
  914. package/dist/world/chunk.js.map +1 -0
  915. package/dist/world/feature.d.ts +86 -0
  916. package/dist/world/feature.d.ts.map +1 -0
  917. package/dist/world/feature.js +153 -0
  918. package/dist/world/feature.js.map +1 -0
  919. package/dist/world/flow.d.ts +33 -0
  920. package/dist/world/flow.d.ts.map +1 -0
  921. package/dist/world/flow.js +115 -0
  922. package/dist/world/flow.js.map +1 -0
  923. package/dist/world/project.d.ts +176 -0
  924. package/dist/world/project.d.ts.map +1 -0
  925. package/dist/world/project.js +543 -0
  926. package/dist/world/project.js.map +1 -0
  927. package/dist/world/projection.d.ts +100 -0
  928. package/dist/world/projection.d.ts.map +1 -0
  929. package/dist/world/projection.js +152 -0
  930. package/dist/world/projection.js.map +1 -0
  931. package/dist/world/scatter.d.ts +25 -0
  932. package/dist/world/scatter.d.ts.map +1 -0
  933. package/dist/world/scatter.js +54 -0
  934. package/dist/world/scatter.js.map +1 -0
  935. package/dist/world/trap.d.ts +84 -0
  936. package/dist/world/trap.d.ts.map +1 -0
  937. package/dist/world/trap.js +106 -0
  938. package/dist/world/trap.js.map +1 -0
  939. package/dist/world/view.d.ts +60 -0
  940. package/dist/world/view.d.ts.map +1 -0
  941. package/dist/world/view.js +436 -0
  942. package/dist/world/view.js.map +1 -0
  943. package/dist/z-queue.d.ts +133 -0
  944. package/dist/z-queue.d.ts.map +1 -0
  945. package/dist/z-queue.js +283 -0
  946. package/dist/z-queue.js.map +1 -0
  947. package/package.json +75 -0
  948. package/src/agent/act.ts +114 -0
  949. package/src/agent/controller.ts +109 -0
  950. package/src/agent/events.ts +47 -0
  951. package/src/agent/index.ts +36 -0
  952. package/src/agent/perceive.ts +440 -0
  953. package/src/agent/types.ts +464 -0
  954. package/src/bitflag.ts +536 -0
  955. package/src/cmd.ts +547 -0
  956. package/src/color.ts +267 -0
  957. package/src/combat/brand-slay.ts +360 -0
  958. package/src/combat/hit.ts +471 -0
  959. package/src/combat/index.ts +15 -0
  960. package/src/combat/melee.ts +728 -0
  961. package/src/combat/mon-melee.ts +1153 -0
  962. package/src/combat/ranged.ts +397 -0
  963. package/src/constants.ts +324 -0
  964. package/src/dice.ts +467 -0
  965. package/src/effects/effect-choice.ts +32 -0
  966. package/src/effects/effect-info.ts +859 -0
  967. package/src/effects/effect.ts +560 -0
  968. package/src/effects/handlers.ts +452 -0
  969. package/src/effects/interpreter.ts +603 -0
  970. package/src/events.ts +317 -0
  971. package/src/expression.ts +360 -0
  972. package/src/game/cave-cmd.ts +1147 -0
  973. package/src/game/char-sheet.ts +557 -0
  974. package/src/game/chest.ts +348 -0
  975. package/src/game/context.ts +1218 -0
  976. package/src/game/curse-tick.ts +130 -0
  977. package/src/game/describe.ts +62 -0
  978. package/src/game/display.ts +838 -0
  979. package/src/game/dump-level.ts +114 -0
  980. package/src/game/effect-attack.ts +746 -0
  981. package/src/game/effect-detect.ts +542 -0
  982. package/src/game/effect-env.ts +252 -0
  983. package/src/game/effect-game-env.ts +86 -0
  984. package/src/game/effect-general.ts +1122 -0
  985. package/src/game/effect-item.ts +999 -0
  986. package/src/game/effect-melee.ts +557 -0
  987. package/src/game/effect-mon-origin.ts +196 -0
  988. package/src/game/effect-monster.ts +310 -0
  989. package/src/game/effect-summon.ts +161 -0
  990. package/src/game/effect-teleport.ts +645 -0
  991. package/src/game/effect-terrain.ts +881 -0
  992. package/src/game/energy.ts +61 -0
  993. package/src/game/equip-cmp.ts +476 -0
  994. package/src/game/floor.ts +524 -0
  995. package/src/game/gear.ts +1352 -0
  996. package/src/game/harness.ts +270 -0
  997. package/src/game/history.ts +78 -0
  998. package/src/game/ignore-cmd.ts +68 -0
  999. package/src/game/inscription-confirm.ts +115 -0
  1000. package/src/game/known.ts +960 -0
  1001. package/src/game/loop.ts +671 -0
  1002. package/src/game/lore-color.ts +108 -0
  1003. package/src/game/mon-cast.ts +409 -0
  1004. package/src/game/mon-cmd.ts +767 -0
  1005. package/src/game/mon-death.ts +459 -0
  1006. package/src/game/mon-group.ts +368 -0
  1007. package/src/game/mon-list.ts +213 -0
  1008. package/src/game/mon-message.ts +359 -0
  1009. package/src/game/mon-place.ts +1012 -0
  1010. package/src/game/mon-ranged.ts +405 -0
  1011. package/src/game/mon-shape.ts +161 -0
  1012. package/src/game/mon-side.ts +511 -0
  1013. package/src/game/monster-turn.ts +1750 -0
  1014. package/src/game/obj-cmd.ts +1970 -0
  1015. package/src/game/obj-list.ts +302 -0
  1016. package/src/game/object-inspect.ts +162 -0
  1017. package/src/game/pickup.ts +483 -0
  1018. package/src/game/player-path.ts +1434 -0
  1019. package/src/game/player-side.ts +555 -0
  1020. package/src/game/player-turn.ts +852 -0
  1021. package/src/game/project-cast.ts +766 -0
  1022. package/src/game/project-feat.ts +386 -0
  1023. package/src/game/project-monster.ts +407 -0
  1024. package/src/game/project-obj.ts +298 -0
  1025. package/src/game/project-player.ts +236 -0
  1026. package/src/game/quest.ts +248 -0
  1027. package/src/game/ranged-cmd.ts +417 -0
  1028. package/src/game/scheduler.ts +150 -0
  1029. package/src/game/spell-cmd.ts +404 -0
  1030. package/src/game/spoil.ts +562 -0
  1031. package/src/game/steal.ts +169 -0
  1032. package/src/game/take-hit-hooks.ts +152 -0
  1033. package/src/game/target-loop.ts +501 -0
  1034. package/src/game/target.ts +424 -0
  1035. package/src/game/thrust.ts +174 -0
  1036. package/src/game/trap.ts +791 -0
  1037. package/src/game/ui-entry.ts +2075 -0
  1038. package/src/game/wizard.ts +1760 -0
  1039. package/src/game/world.ts +786 -0
  1040. package/src/gen/cave.ts +2858 -0
  1041. package/src/gen/gen-monster.ts +592 -0
  1042. package/src/gen/generate.ts +523 -0
  1043. package/src/gen/room.ts +2213 -0
  1044. package/src/gen/util.ts +1958 -0
  1045. package/src/generated/dun-profiles.ts +30 -0
  1046. package/src/generated/effects.ts +241 -0
  1047. package/src/generated/elements.ts +62 -0
  1048. package/src/generated/equip-slots.ts +36 -0
  1049. package/src/generated/history-types.ts +34 -0
  1050. package/src/generated/ignore-types.ts +66 -0
  1051. package/src/generated/index.ts +33 -0
  1052. package/src/generated/kind-flags.ts +44 -0
  1053. package/src/generated/message.ts +320 -0
  1054. package/src/generated/mon-message.ts +138 -0
  1055. package/src/generated/mon-race-flags.ts +182 -0
  1056. package/src/generated/mon-spells.ts +198 -0
  1057. package/src/generated/mon-temp-flags.ts +34 -0
  1058. package/src/generated/mon-timed.ts +34 -0
  1059. package/src/generated/object-flags.ts +95 -0
  1060. package/src/generated/object-modifiers.ts +43 -0
  1061. package/src/generated/options.ts +104 -0
  1062. package/src/generated/origins.ts +64 -0
  1063. package/src/generated/parser-errors.ts +140 -0
  1064. package/src/generated/player-flags.ts +48 -0
  1065. package/src/generated/player-timed.ts +118 -0
  1066. package/src/generated/projections.ts +103 -0
  1067. package/src/generated/randart-properties.ts +202 -0
  1068. package/src/generated/room-flags.ts +21 -0
  1069. package/src/generated/rooms.ts +50 -0
  1070. package/src/generated/square-flags.ts +56 -0
  1071. package/src/generated/stats.ts +22 -0
  1072. package/src/generated/terrain-flags.ts +76 -0
  1073. package/src/generated/terrain.ts +62 -0
  1074. package/src/generated/trap-flags.ts +42 -0
  1075. package/src/generated/tvals.ts +84 -0
  1076. package/src/generated/ui-entry-renderers.ts +24 -0
  1077. package/src/guard.ts +70 -0
  1078. package/src/host/args.ts +334 -0
  1079. package/src/host/bridge.ts +193 -0
  1080. package/src/host/index.ts +18 -0
  1081. package/src/host/io.ts +239 -0
  1082. package/src/host/memory.ts +134 -0
  1083. package/src/host/raw.ts +190 -0
  1084. package/src/index.ts +250 -0
  1085. package/src/loc.ts +157 -0
  1086. package/src/mod/hooks.ts +278 -0
  1087. package/src/mod/ids.ts +390 -0
  1088. package/src/mod/registry-host.ts +258 -0
  1089. package/src/mod/save-blocks.ts +787 -0
  1090. package/src/mod/vocabulary.ts +0 -0
  1091. package/src/mon/bind.ts +899 -0
  1092. package/src/mon/desc.ts +212 -0
  1093. package/src/mon/knowledge-groups.ts +164 -0
  1094. package/src/mon/lore-describe.ts +1414 -0
  1095. package/src/mon/lore.ts +306 -0
  1096. package/src/mon/make.ts +422 -0
  1097. package/src/mon/monster.ts +161 -0
  1098. package/src/mon/predicate.ts +220 -0
  1099. package/src/mon/project-mon.ts +855 -0
  1100. package/src/mon/spell.ts +418 -0
  1101. package/src/mon/steal.ts +246 -0
  1102. package/src/mon/summon.ts +145 -0
  1103. package/src/mon/take-hit.ts +195 -0
  1104. package/src/mon/timed.ts +300 -0
  1105. package/src/mon/types.ts +286 -0
  1106. package/src/msg.ts +146 -0
  1107. package/src/obj/artifact-fake.ts +63 -0
  1108. package/src/obj/artifact-known.ts +69 -0
  1109. package/src/obj/bind.ts +1345 -0
  1110. package/src/obj/chest.ts +249 -0
  1111. package/src/obj/desc.ts +699 -0
  1112. package/src/obj/effects-info.ts +349 -0
  1113. package/src/obj/flavor.ts +256 -0
  1114. package/src/obj/ignore.ts +387 -0
  1115. package/src/obj/knowledge.ts +1577 -0
  1116. package/src/obj/known-object.ts +718 -0
  1117. package/src/obj/make.ts +1367 -0
  1118. package/src/obj/object-info.ts +1737 -0
  1119. package/src/obj/object.ts +1296 -0
  1120. package/src/obj/power.ts +674 -0
  1121. package/src/obj/randart-build.ts +1705 -0
  1122. package/src/obj/randart-data.ts +1344 -0
  1123. package/src/obj/randart.ts +552 -0
  1124. package/src/obj/randname.ts +128 -0
  1125. package/src/obj/recharge.ts +62 -0
  1126. package/src/obj/types.ts +559 -0
  1127. package/src/obj/value.ts +215 -0
  1128. package/src/player/abilities.ts +142 -0
  1129. package/src/player/best-digger.ts +118 -0
  1130. package/src/player/bind.ts +1021 -0
  1131. package/src/player/birth.ts +794 -0
  1132. package/src/player/calcs.ts +1522 -0
  1133. package/src/player/combat-regen.ts +33 -0
  1134. package/src/player/exp.ts +275 -0
  1135. package/src/player/history.ts +211 -0
  1136. package/src/player/options.ts +244 -0
  1137. package/src/player/player.ts +356 -0
  1138. package/src/player/shape-lore.ts +250 -0
  1139. package/src/player/spell.ts +627 -0
  1140. package/src/player/take-hit.ts +173 -0
  1141. package/src/player/timed.ts +482 -0
  1142. package/src/player/types.ts +415 -0
  1143. package/src/rational.ts +388 -0
  1144. package/src/rng.ts +476 -0
  1145. package/src/save/buffer.ts +287 -0
  1146. package/src/save/compress.ts +132 -0
  1147. package/src/save/description.ts +70 -0
  1148. package/src/save/integrity.ts +133 -0
  1149. package/src/score/display.ts +125 -0
  1150. package/src/score/score.ts +341 -0
  1151. package/src/score/types.ts +104 -0
  1152. package/src/session/boot.ts +338 -0
  1153. package/src/session/game.ts +3712 -0
  1154. package/src/session/save.ts +2074 -0
  1155. package/src/sound/engine.ts +375 -0
  1156. package/src/sound/index.ts +13 -0
  1157. package/src/sound/sound-prefs-data.ts +173 -0
  1158. package/src/sound/types.ts +68 -0
  1159. package/src/store/bind.ts +145 -0
  1160. package/src/store/price.ts +97 -0
  1161. package/src/store/store-cmd.ts +73 -0
  1162. package/src/store/store.ts +723 -0
  1163. package/src/store/transact.ts +644 -0
  1164. package/src/store/types.ts +110 -0
  1165. package/src/version.ts +26 -0
  1166. package/src/visuals/engine.ts +444 -0
  1167. package/src/visuals/glyph-table.ts +191 -0
  1168. package/src/visuals/grafmode-data.ts +90 -0
  1169. package/src/visuals/grafmode.ts +123 -0
  1170. package/src/visuals/index.ts +19 -0
  1171. package/src/visuals/map-text.ts +152 -0
  1172. package/src/visuals/pref-expr.ts +182 -0
  1173. package/src/visuals/prefs.ts +981 -0
  1174. package/src/visuals/tile-prefs.ts +183 -0
  1175. package/src/world/chunk.ts +531 -0
  1176. package/src/world/feature.ts +215 -0
  1177. package/src/world/flow.ts +128 -0
  1178. package/src/world/project.ts +687 -0
  1179. package/src/world/projection.ts +228 -0
  1180. package/src/world/scatter.ts +72 -0
  1181. package/src/world/trap.ts +155 -0
  1182. package/src/world/view.ts +513 -0
  1183. package/src/z-queue.ts +300 -0
@@ -0,0 +1,3712 @@
1
+ /**
2
+ * New-game assembly: pack in, a live GameState the turn loop can run.
3
+ *
4
+ * bootLevel (boot.ts) produces the world and the spot the player would
5
+ * occupy; this adds the missing half - birthing a player character and
6
+ * wiring it, the generated monsters, and the action registry into the
7
+ * GameState the loop operates on. It is the smallest "start a playable
8
+ * game" entry point and, like the rest of the boot seam, is headless and
9
+ * takes already-parsed pack JSON so it serves tests and any front end.
10
+ *
11
+ * It composes public domain APIs only and adds no rules: generatePlayer
12
+ * (birth), calcBonuses (derived combat/defence), and the context helpers
13
+ * that place the player and register the monsters.
14
+ *
15
+ * Race and class default to Human Warrior; pass raceName/className to
16
+ * override. Decision 6.2 stream order matches C player-birth.c:1269-1292 +
17
+ * ui-game.c:757-760: store_reset, seed_randart, seed_flavor (back-to-back),
18
+ * then prepare_next_level generation.
19
+ */
20
+
21
+ import { loc } from "../loc.js";
22
+ import type { Loc } from "../loc.js";
23
+ import { MessageLog } from "../msg.js";
24
+ import { SKILL, STAT_MAX } from "../player/types.js";
25
+ import { EF, ELEM, HIST, OF, PF, RF, STAT, TMD } from "../generated/index.js";
26
+ import { bindPlayer } from "../player/bind.js";
27
+ import type { PlayerPackRecords, PlayerRegistry } from "../player/bind.js";
28
+ import {
29
+ generatePlayer,
30
+ optionsInitCheat,
31
+ flavorSetAllAware,
32
+ } from "../player/birth.js";
33
+ import {
34
+ bonusChangeMessages,
35
+ calcBonuses,
36
+ calcHitpoints,
37
+ toCombatState,
38
+ toDefenderState,
39
+ } from "../player/calcs.js";
40
+ import type { PlayerState } from "../player/calcs.js";
41
+ import {
42
+ playerBestDiggerDigging,
43
+ playerBestDiggerWithClause,
44
+ } from "../player/best-digger.js";
45
+ import { playerExpGain, playerKillExp } from "../player/exp.js";
46
+ import type { ExpDeps } from "../player/exp.js";
47
+ import { historyAdd, historyFindArtifact, historyLoseArtifact } from "../player/history.js";
48
+ import { artifactHistoryName, historyStamp } from "../game/history.js";
49
+ import { makePlayerSideEffects, makeIncCheckQueries } from "../game/player-side.js";
50
+ import { makeTakeHitHooks } from "../game/take-hit-hooks.js";
51
+ import { makeMonBlowEnv } from "../game/mon-side.js";
52
+ import { adj_dex_safe } from "../player/calcs.js";
53
+ import { processCurseTimeouts } from "../game/curse-tick.js";
54
+ import { buildEffectContext } from "../game/effect-env.js";
55
+ import { attachGameEnv } from "../game/effect-game-env.js";
56
+ import { sourceMonster, sourceNone, sourcePlayer } from "../effects/interpreter.js";
57
+ import {
58
+ DEFAULT_GAME_CONSTANTS,
59
+ addMonster,
60
+ deleteMonster,
61
+ placePlayer,
62
+ squareMonster,
63
+ updateMonsterDistances,
64
+ } from "../game/context.js";
65
+ import { Chunk } from "../world/chunk.js";
66
+ import { chunkWriteTerrain } from "../gen/cave.js";
67
+ import { FEAT, SQUARE } from "../generated/index.js";
68
+ import { blankMonster } from "../mon/monster.js";
69
+ import { MON_GROUP } from "../mon/types.js";
70
+ import type {
71
+ GameState,
72
+ ItemTargetRef,
73
+ PlayerActor,
74
+ PlayerCommand,
75
+ } from "../game/context.js";
76
+ import { monsterGroupAssign, monsterGroupsVerify } from "../game/mon-group.js";
77
+ import { floorCarry, floorObjectForUse, floorPile } from "../game/floor.js";
78
+ import { installPickup } from "../game/pickup.js";
79
+ import { IgnoreSettings, ignoreItemOk } from "../obj/ignore.js";
80
+ import { EffectRegistry } from "../effects/interpreter.js";
81
+ import { registerCoreHandlers } from "../effects/handlers.js";
82
+ import { registerAttackHandlers } from "../game/effect-attack.js";
83
+ import { registerGeneralHandlers } from "../game/effect-general.js";
84
+ import type { GeneralEffectEnv } from "../game/effect-general.js";
85
+ import { registerMonsterHandlers } from "../game/effect-monster.js";
86
+ import { registerTeleportHandlers, teleportMonster } from "../game/effect-teleport.js";
87
+ import { registerTerrainHandlers, wizLightLevel } from "../game/effect-terrain.js";
88
+ import { registerItemHandlers } from "../game/effect-item.js";
89
+ import type { ItemEffectEnv, ItemRequest } from "../game/effect-item.js";
90
+ import { registerMeleeHandlers } from "../game/effect-melee.js";
91
+ import { registerSummonHandlers } from "../game/effect-summon.js";
92
+ import type { SummonEffectEnv } from "../game/effect-summon.js";
93
+ import { registerDetectHandlers } from "../game/effect-detect.js";
94
+ import {
95
+ becomeAware,
96
+ caveIlluminateKnown,
97
+ caveKnown,
98
+ newKnownMap,
99
+ noteSpots,
100
+ viewerStateOf,
101
+ } from "../game/known.js";
102
+ import { updateView } from "../world/view.js";
103
+ import { PY_EXERT, compactMonsters, isDaytime, playerOverExert } from "../game/world.js";
104
+ import { restoreMonsters } from "../game/scheduler.js";
105
+ import { newTargetState, targetSetMonster } from "../game/target.js";
106
+ import {
107
+ getLore,
108
+ loreCountU16,
109
+ loreLearnFlagIfVisible,
110
+ loreLearnSpellIfVisible,
111
+ loreUpdate,
112
+ } from "../mon/lore.js";
113
+ import { monsterIsVisible } from "../mon/predicate.js";
114
+ import {
115
+ countMonsterRaces,
116
+ multiplyMonster,
117
+ pickAndPlaceDistantMonster,
118
+ squareIsEmptyLive,
119
+ wipeMonsterCounts,
120
+ } from "../game/mon-place.js";
121
+ import type { MonPlaceDeps } from "../game/mon-place.js";
122
+ import { SummonTable } from "../mon/summon.js";
123
+ import { MonAllocTable } from "../mon/make.js";
124
+ import type { EffectBuilderInjections } from "../effects/effect.js";
125
+ import { thrustAway } from "../game/thrust.js";
126
+ import { basicPlayerActor } from "../game/project-cast.js";
127
+ import type { CastContext } from "../game/project-cast.js";
128
+ import type { EffectEnvDeps } from "../game/effect-env.js";
129
+ import { installMonsterCasting } from "../game/mon-ranged.js";
130
+ import { buildMonSpellHooks, buildFailRuneEnv } from "../game/mon-cast.js";
131
+ import { installMonCommand } from "../game/mon-cmd.js";
132
+ import { monsterChangeShape, monsterRevertShape } from "../game/mon-shape.js";
133
+ import type { MonShapeHooks } from "../mon/timed.js";
134
+ import { installMonTimedLore } from "../mon/timed.js";
135
+ import {
136
+ applyAutoinscription,
137
+ autoinscribeGround,
138
+ autoinscribePack,
139
+ installObjCommands,
140
+ packOverflow,
141
+ } from "../game/obj-cmd.js";
142
+ import type { ObjCmdDeps } from "../game/obj-cmd.js";
143
+ import { displayFeeling, installCaveCommands, movementAutoDig } from "../game/cave-cmd.js";
144
+ import type { CaveCmdDeps } from "../game/cave-cmd.js";
145
+ import { installSteal } from "../game/steal.js";
146
+ import type { ChestCmdDeps } from "../game/chest.js";
147
+ import {
148
+ calcUnlockingChance,
149
+ installChunkFeatHook,
150
+ installTrap,
151
+ installTraps,
152
+ playerIsTrapsafe,
153
+ squareDoorPower,
154
+ squareIsPlayerTrap,
155
+ squareIsWarded,
156
+ squareIsWebbed,
157
+ squareRemoveAllTraps,
158
+ squareSetDoorLock,
159
+ trapPredicates,
160
+ } from "../game/trap.js";
161
+ import type { TrapDeps } from "../game/trap.js";
162
+ import { lookupTrap } from "../world/trap.js";
163
+ import {
164
+ calcMana,
165
+ calcSpells,
166
+ cumberArmorFrom,
167
+ playerSpellsInit,
168
+ registerBookKinds,
169
+ } from "../player/spell.js";
170
+ import { installSpellCommands, makeSpellChanceEnv } from "../game/spell-cmd.js";
171
+ import { installRangedCommands } from "../game/ranged-cmd.js";
172
+ import { markNoscore, NOSCORE } from "../game/wizard.js";
173
+ import type { WizardDeps } from "../game/wizard.js";
174
+ import { createTownStores, storeUpdate, storeWillBuy } from "../store/store.js";
175
+ import type { Store, StoreMaintContext } from "../store/store.js";
176
+ import {
177
+ homeCarry,
178
+ homeRetrieve,
179
+ homeStash,
180
+ storeBuy,
181
+ storeSell,
182
+ storeSellFloor,
183
+ } from "../store/transact.js";
184
+ import type { BuyResult, SellResult, TxnKnowledge } from "../store/transact.js";
185
+ import { storeCheckNum } from "../store/store.js";
186
+ import { priceItem } from "../store/price.js";
187
+ import {
188
+ formatMonsterMessage,
189
+ formatMonsterMessageByName,
190
+ formatMonsterMessageShowDamage,
191
+ formatPainMessage,
192
+ formatPainMessageShowDamage,
193
+ monMessageSoundType,
194
+ painMessageCode,
195
+ } from "../game/mon-message.js";
196
+ import {
197
+ AutoinscriptionRegistry,
198
+ RuneNoteRegistry,
199
+ buildRuneList,
200
+ runeKey,
201
+ FlavorKnowledge,
202
+ EverseenKnowledge,
203
+ equipLearnElement,
204
+ makeRuneEnv,
205
+ NOOP_FLAVOR_AWARE_DEPS,
206
+ OBJ_NOTICE,
207
+ objectLearnOnWield,
208
+ playerLearnInnate,
209
+ playerLearnAllRunes,
210
+ } from "../obj/knowledge.js";
211
+ import type { FlavorAwareDeps } from "../obj/knowledge.js";
212
+ import { flavorInit } from "../obj/flavor.js";
213
+ import { ELEM_MAX } from "../obj/types.js";
214
+ import type { ObjectKind } from "../obj/types.js";
215
+ import { ArtifactState, ObjAllocState } from "../obj/make.js";
216
+ import type { MakeDeps } from "../obj/make.js";
217
+ import { monsterDeath, installNonplayerHitDeps } from "../game/mon-death.js";
218
+ import type { MonsterDeathDeps } from "../game/mon-death.js";
219
+ import type { ProjectFeatEnv } from "../game/project-feat.js";
220
+ import {
221
+ newGear,
222
+ outfitPlayer,
223
+ gearGet,
224
+ calcInventory,
225
+ minusAc as applyMinusAc,
226
+ objectCopyAmt,
227
+ } from "../game/gear.js";
228
+ import { objectValue as computeObjectValue } from "../obj/value.js";
229
+ import type { GameObject, CurseTimedFoil } from "../obj/object.js";
230
+ import { buildCurseTimedFoil } from "../obj/object.js";
231
+ import { createDefaultRegistry, installMeleeSideEffects, search } from "../game/player-turn.js";
232
+ import type { ActionRegistry } from "../game/player-turn.js";
233
+ import { buildTempBrandSlay, playerIncCheck } from "../player/timed.js";
234
+ import type {
235
+ TimedTempBrandSlayRecord,
236
+ PlayerIncCheckQueries,
237
+ TimedWeaponDesc,
238
+ } from "../player/timed.js";
239
+ import { disturb, installRunning } from "../game/player-path.js";
240
+ import { bindCore, bootLevel, genDeps } from "./boot.js";
241
+ import {
242
+ dungeonGetNextLevel,
243
+ isQuest,
244
+ playerQuestsReset,
245
+ questCheck,
246
+ } from "../game/quest.js";
247
+ import type {
248
+ BootedLevel,
249
+ BootLevelOptions,
250
+ CorePack,
251
+ CoreRegistries,
252
+ } from "./boot.js";
253
+ import { Rng } from "../rng.js";
254
+ import type { Player } from "../player/player.js";
255
+ import { OptionState } from "../player/options.js";
256
+ import type { OptionName } from "../player/options.js";
257
+ import { doRandart, RANDNAME_TOLKIEN } from "../obj/randart.js";
258
+ import { makeActivationSummarizer } from "../obj/effects-info.js";
259
+ import type { RawTimedRecord } from "../obj/effects-info.js";
260
+ import type { ActivationSummarizer } from "../obj/randart-build.js";
261
+ import { generateLevel, getJoinInfo, type QuestSpawn } from "../gen/generate.js";
262
+ import { iToGrid } from "../gen/util.js";
263
+ import {
264
+ SAVE_VERSION,
265
+ deserializeAutoinscriptions,
266
+ deserializeEverseen,
267
+ deserializeFlavor,
268
+ deserializeIgnore,
269
+ deserializeChunk,
270
+ deserializeFloor,
271
+ buildFeatRemap,
272
+ deserializeArtifactsCreated,
273
+ deserializeArtifactFlags,
274
+ deserializeGear,
275
+ deserializeKnown,
276
+ deserializeLevelCache,
277
+ deserializeLore,
278
+ deserializeMessages,
279
+ deserializeMonster,
280
+ deserializePlayer,
281
+ deserializeStores,
282
+ deserializeTraps,
283
+ serializeGame,
284
+ } from "./save.js";
285
+ import type { SavedGame } from "./save.js";
286
+ import { ContentIdResolver } from "../mod/ids.js";
287
+ import {
288
+ coreOnlyManifest,
289
+ quarantineSave,
290
+ rehydrateSave,
291
+ } from "../mod/save-blocks.js";
292
+ import type { ModBag, OrphanStore, SaveManifest } from "../mod/save-blocks.js";
293
+
294
+ /**
295
+ * The getItem seam body (cmd_get_item's "tgtitem" fast path, cmd-core.c L1060):
296
+ * resolve the shell's preset target (a gear handle or a floor-pile index) to a
297
+ * live object. If it resolves and passes the request's tester, consume the
298
+ * one-shot preset and return it; otherwise record the unfulfilled request and
299
+ * return null - the faithful cancel/abort (C falls through a filter-failing
300
+ * preset to the blocking prompt, which the port cannot do mid-turn). Draws no
301
+ * RNG.
302
+ */
303
+ export function resolveTargetItem(
304
+ state: GameState,
305
+ req: ItemRequest,
306
+ ): GameObject | null {
307
+ const ref = state.itemTarget;
308
+ let obj: GameObject | null = null;
309
+ if (ref) {
310
+ if ("handle" in ref) {
311
+ obj = gearGet(state.gear, ref.handle);
312
+ } else {
313
+ obj = floorPile(state, state.actor.grid)[ref.floor] ?? null;
314
+ }
315
+ }
316
+ if (obj && req.tester(obj)) {
317
+ /* One-shot: clear so a two-prompt effect cannot reuse the same object. */
318
+ state.itemTarget = null;
319
+ return obj;
320
+ }
321
+ state.itemRequest = req;
322
+ return null;
323
+ }
324
+
325
+ /**
326
+ * The chooseCurse seam body (get_curse, effect-handler-general.c): return the
327
+ * shell's preset curse index when it is one of the removable curses, else the
328
+ * first removable one (upstream get_curse's default highlight), else null. No
329
+ * RNG - get_curse is a pure menu.
330
+ */
331
+ export function resolveTargetCurse(
332
+ state: GameState,
333
+ removable: readonly number[],
334
+ ): number | null {
335
+ const preset = state.curseTarget;
336
+ if (preset !== null && preset !== undefined && removable.includes(preset)) {
337
+ return preset;
338
+ }
339
+ return removable[0] ?? null;
340
+ }
341
+
342
+ /** A pack that also carries the player-domain records (races, classes, ...). */
343
+ export interface GamePack extends CorePack {
344
+ player: PlayerPackRecords;
345
+ }
346
+
347
+ /** Options for starting a new game. */
348
+ export interface StartGameOptions extends BootLevelOptions {
349
+ /** Race name (case-insensitive). Default "Human". */
350
+ raceName?: string;
351
+ /** Class name (case-insensitive). Default "Warrior". */
352
+ className?: string;
353
+ /**
354
+ * Stat roller method (ui-birth.c BIRTH_ROLLER_CHOICE). "point" applies the
355
+ * point-based allocation in `birthStats` (no stat RNG); "roller" (the
356
+ * default when omitted) runs the classic get_stats roller.
357
+ */
358
+ roller?: "point" | "roller";
359
+ /**
360
+ * Point-based allocated base stats (STAT_MAX values), used only when
361
+ * `roller` is "point". Threaded into generatePlayer so the character is born
362
+ * with these stats and the stat stage draws no RNG.
363
+ */
364
+ birthStats?: readonly number[];
365
+ /**
366
+ * A standard-roller result the shell's roller UI rolled and the player
367
+ * accepted (do_cmd_roll_stats, player-birth.c:1159-1193): the STAT_MAX natural
368
+ * stat values, applied VERBATIM by generatePlayer (no point-buy clamp, no
369
+ * stat RNG). Used when `roller` is "roller"/omitted; wins over `birthStats`.
370
+ */
371
+ rolledStats?: readonly number[];
372
+ /**
373
+ * An edited character background the shell's history stage produced
374
+ * (do_cmd_choose_history, player-birth.c:1219-1230). Replaces the get_history
375
+ * text on the player; the history walk still runs so the RNG order is intact.
376
+ */
377
+ history?: string;
378
+ /**
379
+ * Birth / interface option choices, applied over the table defaults at
380
+ * character creation (option.c options_init_defaults). Birth options become
381
+ * the immutable birth snapshot; the rest seed the live option store.
382
+ */
383
+ optionOverrides?: Partial<Record<OptionName, boolean>>;
384
+ /** op_ptr->hitpoint_warn (0..9). Default 3 (DEFAULT_HITPOINT_WARN). */
385
+ hitpointWarn?: number;
386
+ /**
387
+ * The player's per-patch choices, resolved by the host from each enabled mod's
388
+ * manifest `rules`. Recorded on GameState.modRules and saved, so a save says
389
+ * which patches a character was played with. OPAQUE to core - see the field's
390
+ * note in game/context.ts.
391
+ */
392
+ modRules?: Readonly<Record<string, boolean>>;
393
+ /**
394
+ * The behaviour every enabled mod supplies, already folded into one object by
395
+ * the host (mod/hooks.ts composeModHooks). Absent - the case with no mod
396
+ * enabled - leaves core byte-identical to faithful 4.2.6.
397
+ */
398
+ modHooks?: import("../mod/hooks.js").ModHooks;
399
+ /**
400
+ * The host's own FOV refresh, installed BEFORE the initial level-entry flood.
401
+ *
402
+ * Core installs a default when this is absent, so a host needs this only when it
403
+ * wants its own - the web shell routes the view's events at its sound bus. It is
404
+ * an option rather than a post-boot assignment because the level-entry flood runs
405
+ * inside startGame: a seam installed afterwards misses it, which is how the web
406
+ * build came to do its first view build with `onlyPartial` already cleared.
407
+ */
408
+ updateFov?: (state: GameState) => void;
409
+ }
410
+
411
+ /** A started game: the loop's state and registry, plus what a renderer needs. */
412
+ export interface StartedGame {
413
+ state: GameState;
414
+ registry: ActionRegistry;
415
+ /**
416
+ * The effect interpreter (null on a worldless boot). Surfaced so a host can
417
+ * build the trusted-mod registry facade (mod/registry-host.ts, W2.2) for
418
+ * effect-handler overrides. The room registry lives on booted.registries.rooms
419
+ * and the command seam is `registry` above.
420
+ */
421
+ effects: EffectRegistry | null;
422
+ /** The generated world (features, placed objects, registries) for rendering. */
423
+ booted: BootedLevel;
424
+ players: PlayerRegistry;
425
+ /** Per-game flavor knowledge (aware/tried), for the save format. */
426
+ flavor: FlavorKnowledge;
427
+ /**
428
+ * Per-game everseen knowledge (kind/ego "ever seen"), for the object + ego
429
+ * knowledge browsers and the save format.
430
+ */
431
+ everseen: EverseenKnowledge;
432
+ /** seed_flavor: the seed flavor_init used, persisted so a reload matches. */
433
+ seedFlavor: number;
434
+ /**
435
+ * The mod manifest (save-blocks.ts, P7.2): the pack set + resolved load order
436
+ * + core-owned determinism mode. A core-only game carries coreOnlyManifest();
437
+ * a loaded game carries the manifest the save was written with.
438
+ */
439
+ manifest: SaveManifest;
440
+ /** Per-mod private save bags (mod:<id>), round-tripped verbatim. */
441
+ mods: Record<string, ModBag>;
442
+ /**
443
+ * Quarantined entities (missing/shadowed packs), preserved across save/load
444
+ * so reinstalling a pack rehydrates its content. Empty for a core-only game.
445
+ */
446
+ orphans: OrphanStore;
447
+ /** decision-8: whether the one-time orphan keep/purge prompt has been shown. */
448
+ orphansAcknowledged: boolean;
449
+ /** The player option store (option.c), persisted in the save. */
450
+ options: OptionState;
451
+ /**
452
+ * randart_seed (obj-randart.c): the seed do_randart used when birth_randarts
453
+ * is on, persisted so a reload reproduces the same random artifact set. 0
454
+ * when birth_randarts is off (no randart set was generated).
455
+ */
456
+ randartSeed: number;
457
+ /**
458
+ * dungeon_change_level + prepare_next_level: generate a fresh level at
459
+ * `depth` from the game's own RNG stream and repopulate the state in
460
+ * place (same GameState object, so installed commands keep working).
461
+ * The caller clears state.generateLevel and refreshes FOV/render.
462
+ */
463
+ changeLevel: (depth: number) => void;
464
+ /**
465
+ * The wizard/debug engine bundles (WP-14 / gap 15.2): effect interpreter,
466
+ * ExpDeps, TrapDeps, live MonPlaceDeps and MakeDeps for the interactive debug
467
+ * command menu. Assembled inside wireGame (single source of truth).
468
+ */
469
+ wizardBundles: WizardBundle;
470
+ /**
471
+ * do_cmd_buy: purchase `amt` of a store-stock object into the player's pack
472
+ * (store/transact.ts), with the deps and knowledge closed over. Town only.
473
+ */
474
+ buy: (store: Store, obj: GameObject, amt: number) => BuyResult;
475
+ /**
476
+ * do_cmd_sell: sell `amt` of the gear object at `handle` to the store. The
477
+ * handle may name a pack, equipped, or quiver object (ui-store.c L487 get_mode
478
+ * USE_INVEN|USE_EQUIP|USE_QUIVER); a stuck equipped item is refused.
479
+ */
480
+ sell: (store: Store, handle: number, amt: number) => SellResult;
481
+ /**
482
+ * do_cmd_sell for a FLOOR object (ui-store.c L487 USE_FLOOR): sell `amt` of the
483
+ * floor pile object `obj` (at the player's grid) to the store. Detached via
484
+ * floor_object_for_use rather than a gear handle.
485
+ */
486
+ sellFloor: (store: Store, obj: GameObject, amt: number) => SellResult;
487
+ /**
488
+ * price_item for display: the per-item price the player pays (storeBuying
489
+ * false) or is offered (storeBuying true).
490
+ */
491
+ price: (store: Store, obj: GameObject, storeBuying: boolean, qty: number) => number;
492
+ /**
493
+ * store_will_buy (store.c L524): whether `store` would purchase `obj`, for the
494
+ * sell picker's pre-filter (ui-store.c store_sell get_item tester). Uses the
495
+ * same knowledge the sell transaction does so the picker and the sale agree.
496
+ */
497
+ willBuy: (store: Store, obj: GameObject) => boolean;
498
+ }
499
+
500
+ /**
501
+ * The wizard/debug engine bundles (WP-14 / gap 15.2): the effect interpreter,
502
+ * ExpDeps, TrapDeps, the live MonPlaceDeps and MakeDeps that the interactive
503
+ * wizard commands (game/wizard.ts) dispatch through. Assembled once inside
504
+ * wireGame - the single source of truth for this wiring - and surfaced on
505
+ * StartedGame so the web debug menu (packages/web wizard.ts) never re-derives
506
+ * them. A subset of WizardDeps: the shell adds the wizard flag, msg, the
507
+ * markNoscore hook and the pure registry data (races/artifacts/curses).
508
+ */
509
+ export type WizardBundle = Pick<
510
+ WizardDeps,
511
+ "makeDeps" | "expDeps" | "effect" | "trapDeps" | "monPlace"
512
+ >;
513
+
514
+ /** What the shared command/effect wiring returns. */
515
+ interface WiredGame {
516
+ registry: ActionRegistry;
517
+ trapDeps: TrapDeps | null;
518
+ flavor: FlavorKnowledge;
519
+ everseen: EverseenKnowledge;
520
+ /**
521
+ * The effect interpreter, or null on a worldless boot (no projections). Kept
522
+ * on the wired result so the host can hand it to a trusted mod's registry
523
+ * facade (W2.2, mod/registry-host.ts) for effect-handler overrides.
524
+ */
525
+ effects: EffectRegistry | null;
526
+ /** The wizard/debug engine bundles (WP-14), for the debug command menu. */
527
+ wizardBundles: WizardBundle;
528
+ }
529
+
530
+ /**
531
+ * The real, in-play FlavorAwareDeps for object_flavor_aware's ignore fix
532
+ * (obj-knowledge.c L2276-2279, #89): reads the live ignore settings so a
533
+ * kind ignored while unaware keeps being ignored once identified, and flags
534
+ * player->upkeep->notice's PN_IGNORE (state.noticeIgnore) so a later
535
+ * ignore_drop() pass (#25 UI, not yet wired) can react. Shared by the two
536
+ * in-play becomes-aware sites: game/obj-cmd.ts's item-use knowledge gain
537
+ * (installObjCommands below) and store/transact.ts's buy/sell (makeStoreApi).
538
+ */
539
+ function flavorAwareDeps(state: GameState): FlavorAwareDeps {
540
+ return {
541
+ isIgnoredUnaware: (kidx) => state.ignore.kindIsIgnoredUnaware(kidx),
542
+ ignoreWhenAware: (kidx) => state.ignore.kindIgnoreWhenAware(kidx),
543
+ requestIgnoreNotice: () => {
544
+ state.noticeIgnore = true;
545
+ },
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Install every command and effect-stack seam on a constructed GameState:
551
+ * pickup, the effect interpreter (monster casting, item use, player
552
+ * spells), traps (disarm + the step hook) and the cave commands with the
553
+ * lock seams. Shared by startGame and loadGame; the same state object is
554
+ * captured by every closure, so a level change may swap the state's chunk
555
+ * and entity stores in place without rewiring.
556
+ */
557
+ function wireGame(
558
+ state: GameState,
559
+ reg: CoreRegistries,
560
+ players: PlayerRegistry,
561
+ pstate: PlayerState,
562
+ seedFlavor: number,
563
+ pack: GamePack,
564
+ ): WiredGame {
565
+ // Rolling message log (message.c file-statics; gap 12.8). Shared producer for
566
+ // both new-game and load paths: startGame arrives with no log so this creates
567
+ // it; loadGame already restored one via deserializeMessages, so preserve it
568
+ // (??=) rather than clobber. The shell's message sink (state.msg) appends here
569
+ // in addition to its existing routing.
570
+ state.messages ??= new MessageLog();
571
+
572
+ // Live commands over the floor piles: 'g'et + autopickup on stepping.
573
+ const registry = createDefaultRegistry();
574
+
575
+ const flavor = new FlavorKnowledge(reg.objects.ordinaryKindCount);
576
+
577
+ // kind/ego everseen (object_kind/ego_item everseen): one per-game store, read
578
+ // by the object + ego knowledge browsers and marked on live describes via
579
+ // knownDescOf (game/describe.ts) and for bought start items (startGame).
580
+ const everseen = new EverseenKnowledge();
581
+ state.everseen = everseen;
582
+
583
+ // flavor_init (obj-util.c): assign each flavoured kind a colour/adjective and
584
+ // mark the non-flavoured ordinary kinds aware. Deterministic in seedFlavor,
585
+ // so a save/reload reproduces identical flavours. The assignment feeds the
586
+ // object_desc name seams below (state.hasFlavor / state.flavorText).
587
+ const flavorAssignment = flavorInit(seedFlavor, flavor, {
588
+ kinds: reg.objects.kinds,
589
+ flavors: reg.objects.flavors,
590
+ ordinaryKindCount: reg.objects.ordinaryKindCount,
591
+ nameSections: reg.nameSections,
592
+ /* OPT(player, birth_randarts): scrub the fixed flavours so the randart
593
+ * set's items are not pre-identified by their standard colour/adjective. */
594
+ birthRandarts: state.options?.get("birth_randarts") ?? false,
595
+ });
596
+ state.hasFlavor = (kind) => flavorAssignment.hasFlavor(kind);
597
+ state.flavorText = (kind) => flavorAssignment.text(kind);
598
+ state.flavorGlyph = (kind) => flavorAssignment.get(kind);
599
+
600
+ // ignore_item_ok (obj-ignore.c): the player's ignore settings resolved with
601
+ // live flavor awareness. Everything reads it through state.isIgnored so the
602
+ // floor / pickup / running / projection paths need no flavor coupling.
603
+ state.isIgnored = (obj) =>
604
+ ignoreItemOk(obj, state.ignore, flavor.isAware(obj.kind));
605
+
606
+ // object_flavor_is_aware (obj-knowledge.c): the presentation view models
607
+ // (obj-list.c, #25) read kind awareness through this seam, keeping them
608
+ // decoupled from the flavor store just like isIgnored.
609
+ state.isAware = (kind) => flavor.isAware(kind);
610
+
611
+ // The flavor store + its object_flavor_aware side-channel, exposed so the
612
+ // game-layer knowledge sweep (game/known.ts updatePlayerObjectKnowledge, the
613
+ // port of update_player_object_knowledge) can flip a kind aware when a
614
+ // rune-learn completes a carried jewel's runes (KN-03).
615
+ state.flavorKnown = flavor;
616
+ state.flavorAwareDeps = flavorAwareDeps(state);
617
+
618
+ installPickup(state, registry, {
619
+ constants: reg.constants,
620
+ env: { isIgnored: (obj) => state.isIgnored!(obj) },
621
+ /* PU_INVEN after inven_carry (see PickupDeps.refreshInventory): route
622
+ * picked-up ammo into the quiver. Same calc_inventory opts as the store
623
+ * refreshQuiver and object-command paths. */
624
+ refreshInventory: (): void => {
625
+ calcInventory(state.gear, reg.constants, {
626
+ store: false,
627
+ /* earlier_object reads player->state.ammo_tval off the global player
628
+ * (player-calcs.c:954-959), so EVERY caller must supply the live value
629
+ * -- omitting it here silently sorted the quiver as if no launcher were
630
+ * wielded, so a picked-up usable arrow lost its precedence over
631
+ * unusable ammo. Same source calcInvOpts uses (obj-cmd.ts:168). */
632
+ ammoTval: state.playerState?.ammoTval ?? 0,
633
+ objectValue: (obj: GameObject): number =>
634
+ computeObjectValue(reg.objects, obj, 1, true),
635
+ rogueLike: state.options?.get("rogue_like_commands") ?? false,
636
+ characterDungeon: true,
637
+ msg: (text: string): void => state.msg?.(text),
638
+ });
639
+ },
640
+ });
641
+
642
+ // Rune learning (obj-knowledge.c learn-by-use): the registry tables plus
643
+ // live equipment access. Reads through the state object so level changes
644
+ // and gear swaps need no rewiring.
645
+ state.runeEnv = makeRuneEnv(
646
+ (slot) =>
647
+ state.gear.store.get(state.actor.player.equipment[slot] ?? 0) ?? null,
648
+ (v) => state.rng.randcalcVaries(v),
649
+ {
650
+ brands: reg.objects.brands,
651
+ slays: reg.objects.slays,
652
+ curses: reg.objects.curses,
653
+ properties: reg.objects.properties,
654
+ ...(reg.projections
655
+ ? {
656
+ elementNames: reg.projections
657
+ .slice(0, ELEM_MAX)
658
+ .map((p) => p.name),
659
+ }
660
+ : {}),
661
+ flavor,
662
+ },
663
+ );
664
+
665
+ // The live derived state (upstream p->state). refreshDerived is the
666
+ // port's PU_BONUS | PU_HP | PU_MANA: recompute from the current gear,
667
+ // refresh the actor (including the wielded weapon), re-derive hitpoints
668
+ // from the rolled hitdice and mana from the armor encumbrance. Installed
669
+ // as state.updateBonuses so equipment commands trigger it.
670
+ let derived: PlayerState = pstate;
671
+ // Expose the live derived state so update_mon reads the current OF flags
672
+ // (telepathy / see-invisible) and see_infra. refreshDerived reassigns it.
673
+ state.playerState = derived;
674
+ // A stable live copy of state->stat_ind: refreshDerived replaces the whole
675
+ // derived PlayerState (new statInd array), so anything that captured
676
+ // pstate.statInd would freeze at birth values. This array keeps the same
677
+ // reference and is refreshed in place, so the casting math and a shell's
678
+ // fail-chance display always read the current stats.
679
+ const liveStatInd: number[] = [...pstate.statInd];
680
+ state.statInd = liveStatInd;
681
+ const refreshDerived = (): void => {
682
+ const p = state.actor.player;
683
+ const equipment = p.equipment.map((h) =>
684
+ h ? gearGet(state.gear, h) : null,
685
+ );
686
+ const daytime = isDaytime(state.turn, state.z.dayLength);
687
+ /* p->state before the memcpy at the end of calc_bonuses: the encumbrance
688
+ * notices below diff against it (player-calcs.c:2412-2453). */
689
+ const before = derived;
690
+ derived = calcBonuses(p, {
691
+ equipment,
692
+ timedEffects: players.timed,
693
+ curses: reg.objects.curses,
694
+ update: true,
695
+ depth: state.chunk.depth,
696
+ isDaytime: daytime,
697
+ });
698
+ state.playerState = derived;
699
+ /* calc_light's town-daytime branch (player-calcs.c 1608-1611) flags
700
+ * PU_UPDATE_VIEW | PU_MONSTERS before returning; reinstate that refresh so
701
+ * ambient town light tracks the day/night cycle. */
702
+ if (state.chunk.depth === 0 && daytime) state.updateFov?.(state);
703
+ for (let i = 0; i < liveStatInd.length; i++) {
704
+ liveStatInd[i] = derived.statInd[i] ?? 0;
705
+ }
706
+ const combat = toCombatState(derived);
707
+ state.actor.combat = combat;
708
+ state.actor.defense = toDefenderState(derived);
709
+ state.actor.speed = derived.speed;
710
+ state.actor.light = derived.curLight;
711
+ state.actor.unlight = derived.pflags.has(PF.UNLIGHT);
712
+ state.actor.stealth = combat.skills[SKILL.STEALTH] ?? 0;
713
+ const weaponSlot = p.body.slots.findIndex((s) => s.type === "WEAPON");
714
+ state.actor.weapon =
715
+ weaponSlot >= 0 ? (equipment[weaponSlot] ?? null) : null;
716
+ /* calc_hitpoints from the rolled hitdice (CON may have changed). */
717
+ p.mhp = calcHitpoints(
718
+ p.playerHp[p.lev - 1] ?? p.hitdie,
719
+ p.lev,
720
+ derived.statInd[STAT.CON] ?? 0,
721
+ );
722
+ if (p.chp > p.mhp) p.chp = p.mhp;
723
+ /* calc_mana with the worn-armor encumbrance. It owns state->cumber_armor
724
+ * (player-calcs.c:1503, :1528), so record it before the diff below reads
725
+ * it - upstream's calc_mana runs inside calc_bonuses, ahead of the
726
+ * notices. */
727
+ derived.cumberArmor = calcMana(
728
+ p,
729
+ derived.statInd,
730
+ wornArmorWeight(p, equipment),
731
+ );
732
+ if (p.csp > p.msp) p.csp = p.msp;
733
+ /* The state-change notices at the end of calc_bonuses (2412-2453). Emitted
734
+ * last because cumber_armor is only known once calc_mana has run. */
735
+ const weaponSlotIdx = p.body.slots.findIndex((s) => s.type === "WEAPON");
736
+ const bowSlotIdx = p.body.slots.findIndex((s) => s.type === "BOW");
737
+ for (const text of bonusChangeMessages(before, derived, {
738
+ hasWeapon: weaponSlotIdx >= 0 && !!equipment[weaponSlotIdx],
739
+ hasLauncher: bowSlotIdx >= 0 && !!equipment[bowSlotIdx],
740
+ })) {
741
+ state.msg?.(text);
742
+ }
743
+ };
744
+ state.updateBonuses = refreshDerived;
745
+
746
+ /**
747
+ * update_stuff's PU_UPDATE_VIEW arm (player-calcs.c:2608), as the DEFAULT.
748
+ *
749
+ * `state.updateFov` is a host seam that core already calls from ~25 sites - the
750
+ * level-entry flood, the after-action refresh in player-turn.ts, every terrain
751
+ * and light effect. All of them are `?.`, and core supplied no default, so a
752
+ * host that did not install one got silence from every one of them. Measured on
753
+ * a fresh startGame boot with no host wiring: of 12740 cells, `known` was true
754
+ * for 0 and `inView` for 0, including the player's own square. The engine was
755
+ * complete and every caller was wired; the DEFAULT was the missing piece, and
756
+ * an agent driving the frozen agent API therefore had no map at all.
757
+ *
758
+ * Invisible until something drove it: the web shell installs its own (main.ts,
759
+ * routing view events at its sound bus) and the Borg's tests use a hand-built
760
+ * fake AgentView, so no test in the repository ever ran core's perceive path
761
+ * with nothing installed.
762
+ *
763
+ * `??=` rather than `=`: a host that wants its own events bus or light sources
764
+ * still replaces this, and the web does. Wired in wireGame so startGame and
765
+ * loadGame both get it - a resumed character has the same right to a map as a
766
+ * new one.
767
+ *
768
+ * The viewer fields come from `viewerStateOf` (game/known.ts) rather than being
769
+ * spelled out again here, because spelling them out twice is how both hosts came
770
+ * to pass `chunk.depth` where cave-view.c:778 reads `p->lev`.
771
+ */
772
+ state.updateFov ??= (s: GameState): void => {
773
+ updateView(
774
+ s.chunk,
775
+ viewerStateOf(s),
776
+ { maxSight: reg.constants.maxSight, feelingNeed: reg.constants.feelingNeed },
777
+ [],
778
+ s.events,
779
+ );
780
+ noteSpots(s);
781
+ };
782
+ /* game-world.c:941-947: the C refreshes upkeep->inven[] before its
783
+ * catch-all pack_overflow(NULL). This closure has the same live
784
+ * calc_inventory inputs used by pickup and command paths, preserving the
785
+ * earlier_object order of the derived gear.inven view. */
786
+ state.overflowPack = (): void => {
787
+ const calcInv = {
788
+ ammoTval: state.playerState?.ammoTval ?? 0,
789
+ objectValue: (obj: GameObject): number =>
790
+ computeObjectValue(reg.objects, obj, 1, true),
791
+ rogueLike: state.options?.get("rogue_like_commands") ?? false,
792
+ characterDungeon: true,
793
+ ...(state.msg ? { msg: state.msg } : {}),
794
+ };
795
+ /* notice_stuff()/handle_stuff() precede pack_overflow(NULL) at
796
+ * game-world.c:941-947; materialize the current upkeep->inven[] analogue
797
+ * before selecting its final entry. */
798
+ calcInventory(state.gear, reg.constants, calcInv);
799
+ packOverflow(state, 0, reg.constants, {
800
+ calcInv,
801
+ ...(state.msg ? { msg: state.msg } : {}),
802
+ });
803
+ };
804
+ /* Stash the class list so refreshTownStores can expand the bookseller's
805
+ * town-book always lines (object_kind_to_book, store.c:208-231). */
806
+ state.classes = players.classes;
807
+
808
+ // player_best_digger (player-util.c L744): digging temporarily wields the
809
+ // pack's best digger and recomputes calc_bonuses (update=false, no RNG) to
810
+ // read its DIGGING; this closes over the same calc_bonuses options as
811
+ // refreshDerived so the swapped derive matches upstream. Feeds the existing
812
+ // randint0(1600) dig roll (game/cave-cmd.ts tunnelAux, game/player-path.ts
813
+ // rubblePenalty) without adding or reordering any draw.
814
+ state.bestDiggerDigging = (): number => {
815
+ const p = state.actor.player;
816
+ const equipment = p.equipment.map((h) =>
817
+ h ? gearGet(state.gear, h) : null,
818
+ );
819
+ const weaponSlot = p.body.slots.findIndex((s) => s.type === "WEAPON");
820
+ const daytime = isDaytime(state.turn, state.z.dayLength);
821
+ return playerBestDiggerDigging(
822
+ equipment,
823
+ [...state.gear.store.values()],
824
+ weaponSlot,
825
+ (equip) =>
826
+ calcBonuses(p, {
827
+ equipment: equip,
828
+ timedEffects: players.timed,
829
+ curses: reg.objects.curses,
830
+ update: false,
831
+ depth: state.chunk.depth,
832
+ isDaytime: daytime,
833
+ }).skills[SKILL.DIGGING] ?? 0,
834
+ );
835
+ };
836
+
837
+ /* The same swap decision as a string, for tunnel_aux's with_clause
838
+ * (cmd-cave.c:541, :552) - "with your hands" / "with your weapon" / "with
839
+ * your swap digger". Shares bestDiggerSwap with the roll above so the
840
+ * message and the chance can never disagree. */
841
+ const digWithClause = (): string => {
842
+ const p = state.actor.player;
843
+ const equipment = p.equipment.map((h) =>
844
+ h ? gearGet(state.gear, h) : null,
845
+ );
846
+ const weaponSlot = p.body.slots.findIndex((s) => s.type === "WEAPON");
847
+ const daytime = isDaytime(state.turn, state.z.dayLength);
848
+ return playerBestDiggerWithClause(
849
+ equipment,
850
+ [...state.gear.store.values()],
851
+ weaponSlot,
852
+ (equip) =>
853
+ calcBonuses(p, {
854
+ equipment: equip,
855
+ timedEffects: players.timed,
856
+ curses: reg.objects.curses,
857
+ update: false,
858
+ depth: state.chunk.depth,
859
+ isDaytime: daytime,
860
+ }).skills[SKILL.DIGGING] ?? 0,
861
+ );
862
+ };
863
+
864
+ // Experience (player.c): a level change recomputes the derived state
865
+ // (upstream's PU_BONUS | PU_HP | PU_SPELLS), and a player kill rewards
866
+ // mexp * rlev / plev with the fractional carry.
867
+ const expDeps: ExpDeps = {
868
+ rng: state.rng,
869
+ /* msgt(MSG_LEVEL, "Welcome to level %d.") (player.c L250). This sink was
870
+ * missing, so every level-up was silent: the level and max HP simply
871
+ * changed on the status line with no message at all. */
872
+ msg: (text, type): void => state.msg?.(text, type),
873
+ onLevelChange: (p): void => {
874
+ refreshDerived();
875
+ /* Casters learn/forget spells at the new level; calcSpells announces the
876
+ * new allowance ("You can learn N more spells.") on a change, as C's
877
+ * calc_bonuses->calc_spells does (player-calcs.c:1465). */
878
+ calcSpells(p, derived.statInd, (text) => state.msg?.(text));
879
+ },
880
+ /* history_add(HIST_GAIN_LEVEL) (player.c L246-247), fired from inside
881
+ * adjustLevel's up-loop before the "Welcome to level" message. */
882
+ onGainLevel: (p, lev): void => {
883
+ const stamp = historyStamp(state);
884
+ historyAdd(p, `Reached level ${lev}`, HIST.GAIN_LEVEL, stamp.dlev, lev, stamp.turn);
885
+ },
886
+ };
887
+ // Monster-death loot deps (mon_create_drop + monster_death, game/mon-death.ts).
888
+ // Assigned inside the projections block below once makeDeps, the object
889
+ // registry, the shared floorEnv and the trap predicates are all available;
890
+ // onPlayerKill / onMonsterDeath run only after wireGame has finished, so the
891
+ // deferred assignment is always resolved by the time they fire.
892
+ let deathDeps: MonsterDeathDeps | undefined;
893
+ /* become_aware (mon-util.c L711, game/known.ts): reveal a camouflaged
894
+ * mimic. Installed once here and threaded into every hit / cast / melee
895
+ * site below so a camouflaged monster unmasks wherever upstream calls
896
+ * become_aware, instead of the flag never clearing. */
897
+ state.becomeAware = (mon): void => becomeAware(state, mon);
898
+ state.onPlayerKill = (mon): void => {
899
+ /* Experience comes from the killed form (player_kill_monster computes
900
+ * new_exp before monster_death's revert). */
901
+ const expRace = mon.race;
902
+ /* Shapechanged monsters revert on death (mon-util.c L1027). */
903
+ monsterRevertShape(state, mon);
904
+ /* player_kill_monster: dead uniques stay dead (max_num = 0). The flag
905
+ * is session-lifetime; persisting it rides the save format (ledgered). */
906
+ if (mon.race.flags.has(RF.UNIQUE)) {
907
+ /* bug-fixes #4245 ("Unique coming back to life?"): a unique can produce
908
+ * multiple "Killed X" kill-history entries via shape-change / projection
909
+ * death paths (the misleading death MESSAGE was fixed by PR #6245, in the
910
+ * 4.2.6 baseline, but its author states that does NOT fix the multiple-
911
+ * history-entries defect). With bugfix.uniqueKillHistory on, a second kill
912
+ * of an already-dead unique (race.maxNum already 0) does not log a duplicate
913
+ * entry; faithful 4.2.6 logs one per lethal blow. Read BEFORE max_num=0. */
914
+ const alreadyDead = mon.race.maxNum === 0;
915
+ mon.race.maxNum = 0;
916
+ /* history_add(HIST_SLAY_UNIQUE) (mon-util.c L1099-1101), read BEFORE
917
+ * playerKillExp below so p.lev is the pre-kill level, matching
918
+ * upstream's history_add-before-player_exp_gain order. MDESC_DIED_FROM
919
+ * for a unique is just the race name (no article/pronoun swap), so no
920
+ * MDESC subsystem is needed here. */
921
+ /* The history seam (mod/hooks.ts historyAdd). Faithful 4.2.6 logs one entry
922
+ * per lethal blow, duplicates included, so with no hook installed the
923
+ * `?? true` below IS the faithful answer. `duplicate` tells a mod what it
924
+ * needs to decide without core deciding anything. */
925
+ const what = `Killed ${mon.race.name}`;
926
+ const wanted =
927
+ state.modHooks?.historyAdd?.({
928
+ what,
929
+ type: HIST.SLAY_UNIQUE,
930
+ duplicate: alreadyDead,
931
+ }) ?? true;
932
+ if (wanted) {
933
+ const stamp = historyStamp(state);
934
+ historyAdd(
935
+ state.actor.player,
936
+ what,
937
+ HIST.SLAY_UNIQUE,
938
+ stamp.dlev,
939
+ stamp.clev,
940
+ stamp.turn,
941
+ );
942
+ }
943
+ }
944
+ /* Generate treasure (monster_death, mon-util.c L1108) BEFORE the pkills /
945
+ * tkills lore counting (L1118), so loreUpdate below sees any drop_gold /
946
+ * drop_item that loreTreasure records. */
947
+ if (deathDeps) monsterDeath(state, mon, deathDeps);
948
+ /* quest_check (player-quest.c L219, called at the end of monster_death,
949
+ * mon-util.c L1005): a slain guardian may finish a quest, build the
950
+ * escape stairs, and - when the last quest falls (Morgoth) - win the
951
+ * game. Placed here so every player-kill path (melee, ranged, spells,
952
+ * effects) triggers it exactly once through this single seam. */
953
+ questCheck(state, state.actor.player, mon);
954
+ /* Recall even invisible uniques (mon-util.c L1118): count the kill
955
+ * and refresh the derived lore (monster_race_track rides #25). */
956
+ if (monsterIsVisible(mon) || mon.race.flags.has(RF.UNIQUE)) {
957
+ const lore = getLore(state.lore, mon.race);
958
+ loreCountU16(lore, "pkills");
959
+ loreCountU16(lore, "tkills");
960
+ loreUpdate(mon.race, lore);
961
+ }
962
+ playerKillExp(state.actor.player, expRace, expDeps);
963
+ };
964
+ const expGain = (amount: number): void =>
965
+ playerExpGain(state.actor.player, amount, expDeps);
966
+
967
+ // object_touch's history_find_artifact (obj-knowledge.c L960-972): fires
968
+ // when an artifact enters the pack (pickup.ts's playerPickupAux). The
969
+ // name builder is RNG-free (game/history.ts artifactHistoryName).
970
+ state.onArtifactFound = (art): void => {
971
+ const stamp = historyStamp(state);
972
+ historyFindArtifact(
973
+ state.actor.player,
974
+ art,
975
+ stamp.dlev,
976
+ stamp.clev,
977
+ stamp.turn,
978
+ (a) => artifactHistoryName(state, reg.objects, reg.constants, a),
979
+ );
980
+ };
981
+
982
+ // history_lose_artifact (player-history.c L246): an artifact is destroyed,
983
+ // abandoned on a regenerated level, or discarded by a store. RNG-free name.
984
+ state.onArtifactLost = (art): void => {
985
+ const stamp = historyStamp(state);
986
+ historyLoseArtifact(
987
+ state.actor.player,
988
+ art,
989
+ stamp.dlev,
990
+ stamp.clev,
991
+ stamp.turn,
992
+ (a) => artifactHistoryName(state, reg.objects, reg.constants, a),
993
+ );
994
+ };
995
+
996
+ // The effect stack: with bound projections, monsters cast spells on
997
+ // their turns (make_ranged_attack), items are usable (cmd-obj.c), the
998
+ // player casts (player-spell.c) and traps fire (trap.c) - all through
999
+ // the same effect interpreter.
1000
+ let trapDeps: TrapDeps | null = null;
1001
+ let chestDeps: ChestCmdDeps | null = null;
1002
+ /* Hoisted so the wired result can surface it to the host (W2.2 mod facade);
1003
+ * assigned inside the projections block below, null on a worldless boot. */
1004
+ let effectRegistry: EffectRegistry | null = null;
1005
+ /* The wizard/debug effect bundle (WP-14): the same effect_simple stack items
1006
+ * and spells run through, assembled once the block's locals exist. undefined
1007
+ * on a worldless boot, where the wizard effect commands are inert anyway. */
1008
+ let wizardEffect: WizardDeps["effect"] = undefined;
1009
+ /* obj_kind_can_browse(kind) for the birthed class (obj-tval.c): the set of
1010
+ * this character's readable spellbook kinds (tval,sval), stamped by
1011
+ * registerBookKinds. make_object uses it to reject unreadable books (gap 3.5).
1012
+ * Empty for a non-caster class, so it rejects every book (faithful). */
1013
+ const classBookKeys = new Set(
1014
+ state.actor.player.cls.magic.books.map((b) => `${b.tvalIdx},${b.sval}`),
1015
+ );
1016
+ const makeDeps: MakeDeps = {
1017
+ reg: reg.objects,
1018
+ alloc: new ObjAllocState(reg.objects, reg.constants),
1019
+ constants: reg.constants,
1020
+ artifacts: state.artifacts ?? new ArtifactState(reg.objects.artifacts.length),
1021
+ noArtifacts: state.options?.get("birth_no_artifacts") ?? false,
1022
+ /* The mod behaviour seam: read state.modHooks LIVE, not captured, so a mod
1023
+ * that installs a hook at boot (after wireGame builds this) is still seen.
1024
+ * Absent => makeArtifact's faithful branch, which is the only branch. */
1025
+ get hooks() {
1026
+ return state.modHooks;
1027
+ },
1028
+ /* append_object_curse TIMED_INC foil (obj-curse.c L159-188, gap 3.2):
1029
+ * reject a curse an existing item property would foil, built from the bound
1030
+ * player-timed fail tables. */
1031
+ timedFoil: buildCurseTimedFoil(players.timed),
1032
+ /* obj_kind_can_browse book rejection (obj-make.c L1187-1194, gap 3.5). */
1033
+ canBrowseBook: (kind): boolean =>
1034
+ classBookKeys.has(`${kind.tval},${kind.sval}`),
1035
+ /* make_gold birth_no_selling 5x dungeon inflation (obj-make.c L1310-1312,
1036
+ * gap 3.7). */
1037
+ noSelling: state.options?.get("birth_no_selling") ?? false,
1038
+ };
1039
+ /* The one complete player take_hit consequences object, shared by every
1040
+ * damage site (projections, melee via mon-side, effects via envDeps, DoT /
1041
+ * terrain via state.world). Defined out here so both the projections block
1042
+ * and the state.world assignment below reference the same object. Wiring
1043
+ * onDeath is what finally records died_from + clears total_winner on death
1044
+ * (audit 01 P1 CRITICAL). wizardEffect.current is filled once the effect
1045
+ * stack exists so EVENT_CHEAT_DEATH can call wizCheatDeath (W2-009). */
1046
+ const wizardEffectHolder: { current: WizardDeps["effect"] | undefined } = {
1047
+ current: undefined,
1048
+ };
1049
+ const sharedTakeHitHooks = makeTakeHitHooks(state, {
1050
+ wizardEffect: wizardEffectHolder,
1051
+ });
1052
+ /* on_begin_effect / on_end_effect dispatch for timed transitions (audit 01
1053
+ * T2, player-timed.c:873-891). Assigned inside the projections block where the
1054
+ * effect registry + env exist, and read by the world-clock timedHooks
1055
+ * (function-body scope, like sharedTakeHitHooks). Undefined when no effect
1056
+ * stack is built (headless save inspection); then transitions run no chain. */
1057
+ let runTimedTransition:
1058
+ | ((idx: number, begin: boolean, canDisturb: boolean) => void)
1059
+ | undefined;
1060
+ if (reg.projections) {
1061
+ const effects = new EffectRegistry();
1062
+ effectRegistry = effects;
1063
+ registerCoreHandlers(effects);
1064
+ registerAttackHandlers(effects);
1065
+ registerMonsterHandlers(effects);
1066
+ registerTeleportHandlers(effects);
1067
+ registerGeneralHandlers(effects);
1068
+ registerTerrainHandlers(effects);
1069
+ registerItemHandlers(effects);
1070
+ registerMeleeHandlers(effects);
1071
+ registerSummonHandlers(effects);
1072
+ registerDetectHandlers(effects);
1073
+
1074
+ // The trap-backed square predicates feed every consumer that stubbed
1075
+ // them (teleport landing checks, drop placement) once traps exist.
1076
+ const preds = reg.traps ? trapPredicates(state) : null;
1077
+ // The shared floor drop environment (drop_near's ignore / trap rules),
1078
+ // used by both the object commands and monster-death loot so a kill's
1079
+ // drops land under the same placement rules as any other floor drop.
1080
+ const floorEnv = {
1081
+ isIgnored: (obj: GameObject): boolean => state.isIgnored!(obj),
1082
+ ...(preds ? { isTrap: preds.isTrap } : {}),
1083
+ };
1084
+ // Monster-death loot deps: makeDeps builds the objects, reg.objects looks
1085
+ // up specified drops, floorEnv places them, state.lore feeds the theft
1086
+ // reduction and loreTreasure.
1087
+ deathDeps = {
1088
+ makeDeps,
1089
+ reg: reg.objects,
1090
+ floorEnv,
1091
+ lore: state.lore,
1092
+ };
1093
+ const teleport = preds
1094
+ ? {
1095
+ isPlayerTrap: preds.isPlayerTrap,
1096
+ isWarded: preds.isWarded,
1097
+ isWebbed: preds.isWebbed,
1098
+ /* is_quest (player-quest.c L140): the real implementation behind the
1099
+ * force_descend / teleport-level guards (effect-general.ts,
1100
+ * effect-teleport.ts, effect-terrain.ts) - a quest level cannot be
1101
+ * skipped or recalled away from. */
1102
+ isQuest: (depth: number): boolean => isQuest(state.actor.player, depth),
1103
+ /* dungeon_get_next_level (player-util.c:1147). This seam existed but
1104
+ * nothing ever wired it, so every consumer silently degraded to a
1105
+ * bare `depth + dir` - no stair_skip, no max_depth clamp and, worst,
1106
+ * no quest scan. */
1107
+ getNextLevel: (from: number, dir: 1 | -1): number =>
1108
+ dungeonGetNextLevel(state.actor.player, from, dir, state.z),
1109
+ changeLevel: (targetDepth: number): void => {
1110
+ state.targetDepth = targetDepth;
1111
+ state.generateLevel = true;
1112
+ },
1113
+ /* player_handle_post_move eval_trap half (player-util.c:1627-1629):
1114
+ * immediate traps at the landing grid after teleport / thrust. */
1115
+ onPlayerPostMove: (_byMonster: boolean): void => {
1116
+ state.onPlayerMoved?.(state, state.actor.grid);
1117
+ },
1118
+ }
1119
+ : undefined;
1120
+ // Glyph / web creation needs the trap system; trapDeps joins below
1121
+ // once it is built (the mutual reference is deliberate). The stat
1122
+ // adjectives (desc_stat) come from the bound object properties, and
1123
+ // experience gains ripple level changes through expDeps.
1124
+ const general: GeneralEffectEnv = {
1125
+ properties: reg.objects.properties,
1126
+ expDeps,
1127
+ shapes: players.shapes,
1128
+ };
1129
+ // Item-targeting seams: the ego / curse tables, arrow generation, and the
1130
+ // get_item / get_curse choosers. The shell pre-resolves the target object
1131
+ // (async item menu) and rides it on the command as state.itemTarget; these
1132
+ // closures are the sync side of cmd_get_item's "tgtitem" fast path
1133
+ // (cmd-core.c L1060), turning the preset back into a live object without
1134
+ // blocking the turn loop. Absent a preset, the choosing effect aborts (the
1135
+ // upstream cancel path) and records the unfulfilled request for the shell.
1136
+ const item: ItemEffectEnv = {
1137
+ reg: reg.objects,
1138
+ makeDeps,
1139
+ getItem: (req) => resolveTargetItem(state, req),
1140
+ chooseCurse: (_obj, removable) => resolveTargetCurse(state, removable),
1141
+ };
1142
+ // Summoning: the bound summon table, the session's live allocation
1143
+ // table (get_mon_num over the full race registry) and the placement
1144
+ // deps. The summonNameToIdx injection lets effect chains resolve
1145
+ // "SUMMON:UNDEAD"-style subtypes at build time.
1146
+ const summons = new SummonTable(reg.monsters.summons, reg.monsters.bases);
1147
+ const summon: SummonEffectEnv = {
1148
+ summons,
1149
+ place: {
1150
+ table: new MonAllocTable(reg.monsters.races, {
1151
+ maxDepth: reg.constants.maxDepth,
1152
+ oodChance: reg.constants.oodMonsterChance,
1153
+ oodAmount: reg.constants.oodMonsterAmount,
1154
+ }),
1155
+ groupMax: reg.constants.monsterGroupMax,
1156
+ groupDist: reg.constants.monsterGroupDist,
1157
+ ...(preds ? { preds } : {}),
1158
+ },
1159
+ };
1160
+ const inject: EffectBuilderInjections = {
1161
+ summonNameToIdx: (name) => summons.nameToIdx(name),
1162
+ /* shape_name_to_idx (player-util.c L987) over lookup_player_shape (L971).
1163
+ * NOTE: upstream matches with streq (z-util.h:157 = strcmp), i.e. CASE
1164
+ * SENSITIVELY, unlike the my_stricmp used by stat_name_to_idx /
1165
+ * proj_name_to_idx. Core shape names ("Pukel-man") are cited with exact
1166
+ * case in class.txt so the two agree on stock data; this stays
1167
+ * case-insensitive because narrowing it would reject mod data the port has
1168
+ * always accepted, and the parity-visible behaviour is identical.
1169
+ * lookup_player_shape's msg("Could not find %s shape!") on failure is a
1170
+ * diagnostic the -1 return covers. */
1171
+ shapeNameToIdx: (name) =>
1172
+ players.shapes.findIndex(
1173
+ (s) => s.name.toLowerCase() === name.toLowerCase(),
1174
+ ),
1175
+ /* W2.3 vocabulary extension: resolve effect NAMES beyond the upstream EF
1176
+ * set so a mod can name its own effect codes in pack/effect text. Only
1177
+ * reached after effect_lookup fails (a non-core name), so core effect
1178
+ * text is byte-identical; a string EffectCode dispatches to itself via
1179
+ * the EffectRegistry (mods register string codes through the W2.2
1180
+ * ModRegistryHost.effects facade). Unknown names still return null ->
1181
+ * PARSE_ERROR_INVALID_EFFECT, exactly as before. */
1182
+ lookupEffect: (name) => (effects.isRegistered(name) ? name : null),
1183
+ };
1184
+ // project_o / project_f world access; trapDeps joins it below once the
1185
+ // trap system is wired (the mutual reference is deliberate).
1186
+ const worldEnv: ProjectFeatEnv = { makeDeps };
1187
+ /* The projection view reads the LIVE derived state, so worn resistance
1188
+ * gear reduces projection damage and equipment swaps take effect. */
1189
+ const playerActor = basicPlayerActor(state, {
1190
+ resistLevel: (t) => derived.elInfo[t]?.resLevel ?? 0,
1191
+ reduction: () => ({
1192
+ damRed: derived.damRed,
1193
+ percDamRed: derived.percDamRed,
1194
+ }),
1195
+ /* minus_ac (obj-gear.c L376-438 / project-player.c L69): a live acid hit
1196
+ * damages a random worn armour piece and halves the damage (gap 6.2/4.2).
1197
+ * Called by project_p only inside adjust_dam's PROJ_ACID branch, so the
1198
+ * armour-damage side effect and its RNG draws fire exactly as upstream. */
1199
+ minusAc: (): boolean =>
1200
+ applyMinusAc(state.actor.player, state.gear, state.rng, {
1201
+ msg: (text: string): void => state.msg?.(text),
1202
+ updateBonuses: (): void => state.updateBonuses?.(),
1203
+ }),
1204
+ });
1205
+ const cast: CastContext = {
1206
+ projections: reg.projections,
1207
+ maxRange: reg.constants.maxRange,
1208
+ playerActor,
1209
+ worldEnv,
1210
+ hooks: {
1211
+ monster: {
1212
+ /* Spell/device kills reward experience like melee kills. */
1213
+ onKill: (m): void => state.onPlayerKill?.(m),
1214
+ /* become_aware: reveal a camouflaged monster hit by a projection
1215
+ * (project_m) or that stopped an effect (PROJECT_STOP). */
1216
+ becomeAware: (m): void => state.becomeAware?.(m),
1217
+ /* monster_death for a monster-vs-monster kill: no player reward, just
1218
+ * drops (project-mon.c fires monster_death for these too). */
1219
+ onMonsterDeath: (m): void => {
1220
+ if (deathDeps) monsterDeath(state, m, deathDeps);
1221
+ },
1222
+ /* add_monster_message: "the kobold dies", "wakes up", "catches
1223
+ * fire" - the MON_MSG grammar, routed to the game's message sink so
1224
+ * a shell shows ranged/spell/status monster messages the same way
1225
+ * melee shows "You hit/slay the X". The projection already gates on
1226
+ * visibility before calling this. */
1227
+ message: (m, msgCode, _delay, damage): void => {
1228
+ /* add_monster_message_show_damage (mon-msg.c:288) when the driver
1229
+ * passed a damage total, else add_monster_message (L252). */
1230
+ const text =
1231
+ damage === undefined
1232
+ ? formatMonsterMessage(m, msgCode)
1233
+ : formatMonsterMessageShowDamage(m, msgCode, damage);
1234
+ const type = monMessageSoundType(msgCode, m.race);
1235
+ if (text) state.msg?.(text, type);
1236
+ state.sound?.(type);
1237
+ },
1238
+ /* OPT(player, show_damage): project_m_player_attack's display_dam
1239
+ * (project-mon.c:1111) picks the *_show_damage message variants. */
1240
+ showDamage: state.options?.get("show_damage") ?? false,
1241
+ /* message_pain: the graded "shrugs off the attack" / "cries out in
1242
+ * pain" line for a monster hurt but not killed. */
1243
+ messagePain: (m, dam, showDamage): void => {
1244
+ const text = showDamage
1245
+ ? formatPainMessageShowDamage(m, dam)
1246
+ : formatPainMessage(m, dam);
1247
+ const type = monMessageSoundType(painMessageCode(m, dam), m.race);
1248
+ if (text) state.msg?.(text, type);
1249
+ state.sound?.(type);
1250
+ },
1251
+ /* mon_set_timed's queued status messages (slowed, confused, held). */
1252
+ timedMessage: (m, note): void => {
1253
+ const text = formatMonsterMessageByName(m, note);
1254
+ if (text) state.msg?.(text);
1255
+ },
1256
+ /* Lore learning when a projection's outcome is seen. */
1257
+ learnRaceFlag: (m, flag): void =>
1258
+ loreLearnFlagIfVisible(getLore(state.lore, m.race), m, flag),
1259
+ learnSpellFlag: (m, flag): void =>
1260
+ loreLearnSpellIfVisible(getLore(state.lore, m.race), m, flag),
1261
+ /* monster_revert_shape on death / MON_DRAIN (mon-shape.ts). */
1262
+ revertShape: (m): void => {
1263
+ monsterRevertShape(state, m);
1264
+ },
1265
+ /* PROJ_AWAY_ALL teleports and PROJ_FORCE knockback for monsters. */
1266
+ teleport: (m, dist): void =>
1267
+ teleportMonster(state, m.midx, dist, teleport ?? {}),
1268
+ /* project-mon.c:183-185 / 208-212 + player thrust landing. */
1269
+ thrustAway: (centre, target, gridsAway): void =>
1270
+ thrustAway(state, centre, target, gridsAway, {
1271
+ onPlayerPostMove: (): void => {
1272
+ state.onPlayerMoved?.(state, state.actor.grid);
1273
+ },
1274
+ }),
1275
+ },
1276
+ /* The per-PROJ player side effects (project-player.c handlers). */
1277
+ player: {
1278
+ /* OPT(player, show_damage): the extra "you take N damage" lines. */
1279
+ showDamage: state.options?.get("show_damage") ?? false,
1280
+ /* project_p's own messages ("You are hit by ...!") and disturb, plus
1281
+ * the full take_hit consequences. Without these a breath / spell that
1282
+ * damaged or killed the player was silent and never recorded the
1283
+ * killer (audit 01 P1 CRITICAL). */
1284
+ message: (text: string): void => state.msg?.(text),
1285
+ onDisturb: (): void => disturb(state),
1286
+ takeHit: sharedTakeHitHooks,
1287
+ onSideEffects: makePlayerSideEffects(state, {
1288
+ timed: players.timed,
1289
+ actor: playerActor,
1290
+ projections: reg.projections,
1291
+ expDeps,
1292
+ lifeDrainPercent: reg.constants.lifeDrainPercent,
1293
+ ...(teleport ? { teleport } : {}),
1294
+ }),
1295
+ /* adjust_dam(actual=true) equip_learn_element (project-player.c
1296
+ * L60-62, audit 01 P5): being hit by an element teaches the resist
1297
+ * rune on worn gear. No RNG. */
1298
+ equipLearnElement: (resType: number): void =>
1299
+ equipLearnElement(state.actor.player, state.runeEnv, resType),
1300
+ },
1301
+ /* health_track (project.c:971-980): a player projection that affected
1302
+ * exactly one monster, without jumping, recalls and health-tracks it.
1303
+ *
1304
+ * This hook was DECLARED in world/project.ts, fired with the right gate,
1305
+ * and threaded through project-cast.ts - and no production caller ever
1306
+ * supplied it. Only two tests did, which is exactly why it read as live:
1307
+ * the call-site census counts references, so a hook referenced only by a
1308
+ * test is indistinguishable from a wired one. The visible symptom was that
1309
+ * no bolt or ball spell ever put a monster on the health bar.
1310
+ *
1311
+ * project.ts has already applied the one-monster / no-JUMP / player-source
1312
+ * gate and confirmed a monster is on the grid, so this must not re-check
1313
+ * them. The gate here is VISIBLE, not obvious - project.c uses
1314
+ * monster_is_visible. */
1315
+ onTrackMonster: (grid: Loc): void => {
1316
+ const mon = squareMonster(state, grid);
1317
+ if (mon && monsterIsVisible(mon)) state.healthWho = mon;
1318
+ },
1319
+ },
1320
+ };
1321
+ /* player_inc_check resolvers (player-timed.c:945-953, gaps 2.8/6.11): read
1322
+ * the LIVE derived state so EF_TIMED_INC on the player is foiled by a worn
1323
+ * resist ("You resist the effect!") and the resisted LIGHT/SOUND messages
1324
+ * are gated. Absent, every increase was allowed. Same shape as
1325
+ * buildFailRuneEnv / makePlayerSideEffects. */
1326
+ const incQueries: PlayerIncCheckQueries = makeIncCheckQueries(state);
1327
+ const envDeps: EffectEnvDeps = {
1328
+ timedTable: players.timed,
1329
+ // Effect status/damage messages ("You feel better", "You feel yourself
1330
+ // yanked upwards!") route to the game's message sink so a shell shows
1331
+ // them; absent, they would drop.
1332
+ onMessage: (text: string, msgt?: string): void => state.msg?.(text, msgt),
1333
+ incQueries,
1334
+ /* on_begin_effect / on_end_effect (audit 01 T2): the interpreter timed
1335
+ * path (a SCRAMBLE / SPRINT potion or spell) must run the chain too, not
1336
+ * just the world clock. The thunk reads runTimedTransition (assigned just
1337
+ * below) at call time, so SCRAMBLE_STATS fires when the potion lands. */
1338
+ timedHooks: {
1339
+ onTransition: (idx: number, begin: boolean, canDisturb: boolean): void =>
1340
+ runTimedTransition?.(idx, begin, canDisturb),
1341
+ },
1342
+ /* The shared take_hit consequences, so effect-driven player damage (traps,
1343
+ * EF_DAMAGE, activations, monster casts via mon-cast) shows the message
1344
+ * chain and records died_from on death, exactly like melee/projection. */
1345
+ takeHitHooks: sharedTakeHitHooks,
1346
+ };
1347
+
1348
+ /* on_begin_effect / on_end_effect chain runner (audit 01 T2): dispatch the
1349
+ * bound chain of the timed effect at `idx` through the live effect stack.
1350
+ * Upstream (player-timed.c:878-889) uses source_none() when can_disturb is
1351
+ * true and source_player() otherwise, so any TIMED_INC in the chain honors
1352
+ * disturbance. Runs SCRAMBLE's SCRAMBLE_STATS / UNSCRAMBLE_STATS and
1353
+ * SPRINT's ending TIMED_INC_NO_RES:SLOW; a no-op for effects with no chain. */
1354
+ runTimedTransition = (idx, begin, canDisturb): void => {
1355
+ const eff = players.timed[idx];
1356
+ const chain = begin ? eff?.onBeginEffect : eff?.onEndEffect;
1357
+ if (!chain) return;
1358
+ const origin = canDisturb ? sourceNone() : sourcePlayer();
1359
+ for (const step of chain) {
1360
+ /* The chain effects (SCRAMBLE_STATS etc.) read the full game env, so the
1361
+ * context must be attachGameEnv-wrapped exactly like the trap/obj-cmd
1362
+ * paths - a bare buildEffectContext leaves gameEnv() null and they no-op. */
1363
+ const ctx = attachGameEnv(buildEffectContext(state, envDeps), {
1364
+ state,
1365
+ cast,
1366
+ takeHitHooks: sharedTakeHitHooks,
1367
+ ...(teleport ? { teleport } : {}),
1368
+ general,
1369
+ item,
1370
+ summon,
1371
+ });
1372
+ effects.effectSimple(step.effect, ctx, {
1373
+ origin,
1374
+ subtype: step.subtype,
1375
+ ...(step.dice !== undefined ? { diceString: step.dice } : {}),
1376
+ });
1377
+ }
1378
+ };
1379
+
1380
+ /* The wizard/debug effect bundle (WP-14): identical to the object-command
1381
+ * and trap effect bundles - registry + the game-env pieces effect_simple
1382
+ * needs. The interactive debug commands (cure/detect/map/teleport/summon-
1383
+ * random/hit-los) run their effect_simple calls over exactly this stack. */
1384
+ wizardEffect = {
1385
+ registry: effects,
1386
+ cast,
1387
+ envDeps,
1388
+ inject,
1389
+ ...(teleport ? { teleport } : {}),
1390
+ general,
1391
+ item,
1392
+ summon,
1393
+ };
1394
+ /* Fill the take_hit cheat-death holder so wizCheatDeath can clear timers
1395
+ * (W2-009; ui-display.c EVENT_CHEAT_DEATH). */
1396
+ wizardEffectHolder.current = wizardEffect;
1397
+
1398
+ /* monster_change_shape / monster_revert_shape, driving the
1399
+ * MON_TMD_CHANGED timer (the SHAPECHANGE monster spell). */
1400
+ const monShape: MonShapeHooks = {
1401
+ change: (m) =>
1402
+ monsterChangeShape(state, m, {
1403
+ summon,
1404
+ spells: reg.monsters.spells,
1405
+ ...(teleport ? { teleport } : {}),
1406
+ }),
1407
+ revert: (m) => monsterRevertShape(state, m),
1408
+ };
1409
+
1410
+ /* spell_message {type}/{oftype} tags resolve the caster's lash projection
1411
+ * name to its lash_desc (mon-spell.c L47-274 tag substitution). */
1412
+ const projLashDesc = new Map<string, string | null>();
1413
+ for (const proj of reg.projections) projLashDesc.set(proj.name, proj.lashDesc);
1414
+
1415
+ const monSpellDeps = {
1416
+ registry: effects,
1417
+ cast,
1418
+ spells: reg.monsters.spells,
1419
+ envDeps,
1420
+ saveSkill: pstate.skills[SKILL.SAVE] ?? 0,
1421
+ inject,
1422
+ ...(teleport ? { teleport } : {}),
1423
+ general,
1424
+ summon,
1425
+ monShape,
1426
+ /* do_mon_spell UI/lore hooks (8.1/8.2/8.11): announce the cast
1427
+ * (spell_message, mon-spell.c L369), print the save message, and learn
1428
+ * the foil rune on a save (spell_check_for_fail_rune, L383). Without this
1429
+ * a monster's spells were silent. */
1430
+ hooks: buildMonSpellHooks(state, {
1431
+ lashDesc: (name: string): string | null => projLashDesc.get(name) ?? null,
1432
+ failRune: buildFailRuneEnv(state, players.timed),
1433
+ }),
1434
+ };
1435
+ installMonsterCasting(state, monSpellDeps, {
1436
+ /* become_aware: a hidden caster reveals itself (mon-attack.c L454). */
1437
+ becomeAware: (midx): void => {
1438
+ const caster = state.monsters[midx];
1439
+ if (caster) state.becomeAware?.(caster);
1440
+ },
1441
+ });
1442
+ /* do_cmd_mon_command: EF_COMMAND possession drives the monster. */
1443
+ installMonCommand(state, monSpellDeps);
1444
+
1445
+ /* mon_take_nonplayer_hit deps (mon-util.c L1193, gaps 5.1/7.2): monster-vs-
1446
+ * monster and terrain (lava) damage route through the full non-player hit
1447
+ * path - die/pain messages, loot via monster_death, fear rolls. The
1448
+ * scheduler reads these for monster_take_terrain_damage; without them
1449
+ * terrain damage was inert. */
1450
+ installNonplayerHitDeps(state, {
1451
+ ...deathDeps,
1452
+ message: (text: string): void => state.msg?.(text),
1453
+ });
1454
+
1455
+ /* does_resist lore (mon-timed.c L107-110, gap 8.6): when a monster resists a
1456
+ * timed effect via a race flag, learn that flag into visible lore. The
1457
+ * timed layer has no deps param (get_lore is a global upstream), so register
1458
+ * the session's lore store against its Rng. */
1459
+ installMonTimedLore(state.rng, state.lore);
1460
+
1461
+ /* make_attack_normal's blow-effect environment (game/mon-side.ts): the
1462
+ * monster-melee analog of the player onSideEffects hook, so a melee blow
1463
+ * applies its full elemental / status / stat / theft / terrain
1464
+ * consequences in upstream RNG order. EF_EARTHQUAKE (SHATTER) routes
1465
+ * through the effect interpreter so its internal draws are shared. */
1466
+ state.monBlowEnv = makeMonBlowEnv(state, {
1467
+ timed: players.timed,
1468
+ actor: playerActor,
1469
+ projections: reg.projections,
1470
+ expDeps,
1471
+ lifeDrainPercent: reg.constants.lifeDrainPercent,
1472
+ adjDexSafe: adj_dex_safe,
1473
+ packSize: reg.constants.packSize,
1474
+ makeDeps,
1475
+ ...(teleport ? { teleport } : {}),
1476
+ earthquake: (mon, radius): void => {
1477
+ effects.effectSimple(EF.EARTHQUAKE, buildEffectContext(state, envDeps), {
1478
+ origin: sourceMonster(mon.midx),
1479
+ subtype: 0,
1480
+ radius,
1481
+ });
1482
+ },
1483
+ msg: (text: string, msgt?: string): void => state.msg?.(text, msgt),
1484
+ });
1485
+
1486
+ const objCmdDeps: ObjCmdDeps = {
1487
+ constants: reg.constants,
1488
+ registry: effects,
1489
+ cast,
1490
+ envDeps,
1491
+ flavor,
1492
+ flavorDeps: flavorAwareDeps(state),
1493
+ /* get_autoinscription (obj-ignore.c L229): read the per-game per-kind
1494
+ * note registry so a note registered through the knowledge-menu manager
1495
+ * is applied by applyAutoinscription. */
1496
+ autoNote: (kind, aware): string | null =>
1497
+ state.autoinscribe?.get(kind.kidx, aware) ?? null,
1498
+ inject,
1499
+ ...(teleport ? { teleport } : {}),
1500
+ general,
1501
+ item,
1502
+ summon,
1503
+ floorEnv,
1504
+ /* object_learn_on_use XP (obj-knowledge.c L1925-1936, gap 4.3): a first
1505
+ * identify-by-use rewards experience via the same ExpDeps hook as
1506
+ * spell/trap/chest use. */
1507
+ expGain,
1508
+ /* calc_inventory quiver inputs (gap 4.1a): the earlier_object ammo value
1509
+ * tiebreak (object_value, ammo is always aware), the preferred_quiver_slot
1510
+ * keyset (rogue_like_commands) and the character_dungeon re-arrange
1511
+ * message gate (true in live play). ammoTval falls back to the live
1512
+ * derived state.playerState.ammoTval inside calcInvOpts. */
1513
+ objectValue: (obj: GameObject): number =>
1514
+ computeObjectValue(reg.objects, obj, 1, true),
1515
+ rogueLike: state.options?.get("rogue_like_commands") ?? false,
1516
+ characterDungeon: true,
1517
+ // Route object/effect messages (msg / msgt / activation_message) to the
1518
+ // game's message sink so a shell shows them; absent, they would drop.
1519
+ env: { msg: (text: string): void => state.msg?.(text) },
1520
+ };
1521
+ installObjCommands(registry, objCmdDeps);
1522
+
1523
+ /* apply_autoinscription / autoinscribe_ground + autoinscribe_pack as seams,
1524
+ * so the upstream call sites that have no ObjCmdDeps can reach them:
1525
+ * inven_carry (obj-gear.c:868), store selling (store.c:1977) and
1526
+ * update_player_object_knowledge's tail (obj-knowledge.c:1245-1247). */
1527
+ state.autoinscribeObject = (obj: GameObject): void => {
1528
+ applyAutoinscription(state, obj, objCmdDeps);
1529
+ };
1530
+ state.autoinscribeAll = (): void => {
1531
+ autoinscribeGround(state, objCmdDeps);
1532
+ autoinscribePack(state, objCmdDeps);
1533
+ };
1534
+
1535
+ /* Player melee blow side effects (player-attack.c:669-1012, gap 2.5/3.6):
1536
+ * the OF_IMPACT earthquake (effect_simple(EF_EARTHQUAKE, source_player, 10)
1537
+ * L688) and the temporary brand/slay predicate (player_has_temporary_brand/
1538
+ * slay over the live timed array, obj-slays.c:287-317). Without these the
1539
+ * quake never fired and brand/slay potions gave no melee multiplier. The
1540
+ * brand/slay directives live in the player_timed pack records (untyped on
1541
+ * PlayerTimedRecordJson but present in the data), in TMD index order. */
1542
+ installMeleeSideEffects(state, {
1543
+ earthquake: (): void => {
1544
+ effects.effectSimple(EF.EARTHQUAKE, buildEffectContext(state, envDeps), {
1545
+ origin: sourcePlayer(),
1546
+ subtype: 0,
1547
+ radius: 10,
1548
+ });
1549
+ },
1550
+ temp: buildTempBrandSlay(
1551
+ state.actor.player,
1552
+ pack.player.timed as unknown as readonly TimedTempBrandSlayRecord[],
1553
+ reg.objects.brands,
1554
+ reg.objects.slays,
1555
+ ),
1556
+ });
1557
+
1558
+ // Player spellcasting (cast / study) for casting classes.
1559
+ installSpellCommands(registry, {
1560
+ effects: {
1561
+ registry: effects,
1562
+ cast,
1563
+ envDeps,
1564
+ inject,
1565
+ ...(teleport ? { teleport } : {}),
1566
+ general,
1567
+ item,
1568
+ summon,
1569
+ },
1570
+ statInd: liveStatInd,
1571
+ env: {
1572
+ expGain,
1573
+ msg: (text: string): void => state.msg?.(text),
1574
+ // spell_chance fear + PF_UNLIGHT inputs (player-spell.c:417,424),
1575
+ // shared with the fail-chance DISPLAY path so the shown rate matches
1576
+ // the cast rate (see makeSpellChanceEnv).
1577
+ ...makeSpellChanceEnv(state),
1578
+ // spell_cast overcast (player-spell.c L552-553): once mana empties,
1579
+ // player_over_exert twice in the exact upstream order - FAINT then CON -
1580
+ // so the RNG stream draws faithfully (playerOverExert draws per flag).
1581
+ overExert: (oops: number): void => {
1582
+ playerOverExert(state, PY_EXERT.FAINT, 100, 5 * oops + 1);
1583
+ playerOverExert(state, PY_EXERT.CON, 50, 0);
1584
+ },
1585
+ },
1586
+ });
1587
+
1588
+ // Player ranged attacks (fire launcher + ammo, throw an object). The hit
1589
+ // math is combat/ranged.ts; the front-end walks the missile's path and
1590
+ // routes hit / death messages through state.msg like the other commands.
1591
+ installRangedCommands(registry);
1592
+
1593
+ // Traps: disarm + the step-onto-trap hook; a trapdoor drops a level.
1594
+ if (reg.traps) {
1595
+ trapDeps = {
1596
+ kinds: reg.traps,
1597
+ effects: {
1598
+ registry: effects,
1599
+ cast,
1600
+ envDeps,
1601
+ inject,
1602
+ ...(teleport ? { teleport } : {}),
1603
+ general,
1604
+ item,
1605
+ summon,
1606
+ },
1607
+ env: {
1608
+ expGain,
1609
+ msg: (text: string): void => state.msg?.(text),
1610
+ /* player_of_has for trap saves / OF_TRAP_IMMUNE (trap.c:515-539). */
1611
+ playerHasFlag: (flag: number): boolean =>
1612
+ state.playerState?.flags.has(flag) ?? false,
1613
+ changeLevel: (s: GameState): void => {
1614
+ /* trap.c:579-582: a TRF_DOWN trapdoor drops you through
1615
+ * dungeon_get_next_level, not blindly one level. */
1616
+ s.targetDepth = dungeonGetNextLevel(
1617
+ s.actor.player,
1618
+ s.chunk.depth,
1619
+ 1,
1620
+ s.z,
1621
+ );
1622
+ s.generateLevel = true;
1623
+ },
1624
+ /* disturb(player) before trap effects (trap.c:525-526). */
1625
+ disturb: (): void => disturb(state),
1626
+ },
1627
+ };
1628
+ installTraps(state, registry, trapDeps);
1629
+ installChunkFeatHook(state);
1630
+ worldEnv.trapDeps = trapDeps;
1631
+ general.trapDeps = trapDeps;
1632
+ }
1633
+
1634
+ // Curse periodic effects (DECISION E, do_curse_effect / decrease_timeouts):
1635
+ // once per game turn each equipped item's curse timeouts count down and fire
1636
+ // when they reach zero, running the curse's effect through the same bundle.
1637
+ state.curseTick = (): void => {
1638
+ processCurseTimeouts(state, {
1639
+ curses: reg.objects.curses,
1640
+ effects: {
1641
+ registry: effects,
1642
+ cast,
1643
+ envDeps,
1644
+ inject,
1645
+ ...(teleport ? { teleport } : {}),
1646
+ general,
1647
+ item,
1648
+ summon,
1649
+ },
1650
+ });
1651
+ };
1652
+
1653
+ // Chests (gap #49): reuse the exact effect bundle traps/objects use, so
1654
+ // chest_trap's dice draws (poison/paralysis/summon/explosion) share the
1655
+ // interpreter, RNG stream and summon wiring with every other effect
1656
+ // source, and floorEnv so a chest's loot lands under the same drop
1657
+ // rules as any other floor drop.
1658
+ chestDeps = {
1659
+ makeDeps,
1660
+ floorEnv,
1661
+ effects: {
1662
+ registry: effects,
1663
+ cast,
1664
+ envDeps,
1665
+ inject,
1666
+ ...(teleport ? { teleport } : {}),
1667
+ general,
1668
+ item,
1669
+ summon,
1670
+ },
1671
+ env: {
1672
+ expGain,
1673
+ msg: (text: string): void => state.msg?.(text),
1674
+ },
1675
+ };
1676
+ }
1677
+
1678
+ // Cave commands (open / close / tunnel / alter / stair checks); rubble
1679
+ // finds and gold veins pay out through the object generator, door locks
1680
+ // resolve through the trap system when it is live, and chests (gap #49)
1681
+ // open/disarm through game/chest.ts when the effect stack is live.
1682
+ const lockKind = trapDeps ? lookupTrap(trapDeps.kinds, "door lock") : null;
1683
+ const deps = trapDeps; // narrow for the closures
1684
+ const caveDeps: CaveCmdDeps = {
1685
+ makeDeps,
1686
+ ...(deps ? { trapDeps: deps } : {}),
1687
+ env: {
1688
+ // Route open/close/tunnel/chest messages to the game's message sink
1689
+ // (matching installObjCommands/installSpellCommands/installTraps);
1690
+ // absent, door/tunnel/chest messages would silently drop.
1691
+ msg: (text: string): void => state.msg?.(text),
1692
+ /* player_is_trapsafe (player-util.c:1073-1077) via trap.ts:86. */
1693
+ isTrapsafe: playerIsTrapsafe,
1694
+ digWithClause,
1695
+ /* get_check for do_cmd_go_down's force_descend quest warning
1696
+ * (cmd-cave.c:126). The shell has no synchronous prompt inside a turn,
1697
+ * so this takes the same default as the effect handlers' confirm seam:
1698
+ * an unprompted terminal auto-accepts. */
1699
+ confirm: (): boolean => true,
1700
+ ...(deps && lockKind
1701
+ ? {
1702
+ isLockedDoor: (grid: Loc): boolean =>
1703
+ squareDoorPower(state, grid, deps) > 0,
1704
+ pickLock: (grid: Loc): boolean => {
1705
+ const power = squareDoorPower(state, grid, deps);
1706
+ const chance = calcUnlockingChance(state, power);
1707
+ if (state.rng.randint0(100) < chance) {
1708
+ squareRemoveAllTraps(state, grid, lockKind.tidx);
1709
+ return true;
1710
+ }
1711
+ return false;
1712
+ },
1713
+ }
1714
+ : {}),
1715
+ },
1716
+ ...(chestDeps ? { chestDeps } : {}),
1717
+ };
1718
+ installCaveCommands(registry, caveDeps);
1719
+
1720
+ // The walk-into-a-wall seam (mod/hooks.ts walkBlockedByDiggable). walkAction
1721
+ // consults this before its no-energy bump; movementAutoDig returns null having
1722
+ // drawn no RNG when no mod installed a hook, so faithful play is byte-identical.
1723
+ // null, not 0: zero is a mod handling the walk for free, which is a different
1724
+ // answer from no mod handling it at all.
1725
+ state.autoDigStep = (s, grid): number | null => movementAutoDig(s, grid, caveDeps);
1726
+
1727
+ // steal (cmd-cave.c do_cmd_steal): the rogue / PF_STEAL lift-from-monster
1728
+ // command. The PF_STEAL gate reads the live derived state (state.playerState).
1729
+ installSteal(registry, {
1730
+ constants: reg.constants,
1731
+ msg: (text: string): void => state.msg?.(text),
1732
+ });
1733
+
1734
+ // Running (player-path.c): the corridor / open-area running engine. It
1735
+ // re-queues itself onto state.cmdQueue, which processPlayer drains.
1736
+ installRunning(registry);
1737
+
1738
+ // process_world upkeep environment (game/world.ts): the bound timed table so
1739
+ // decrease_timeouts / digestion route through the grade / message machinery,
1740
+ // the DoT take_hit hooks (rng is threaded in by worldTakeHit), and the
1741
+ // ambient-monster spawn using the same allocation-table placement path as
1742
+ // normal generation so its variable RNG draws stay faithful.
1743
+ const worldPreds = reg.traps ? trapPredicates(state) : null;
1744
+ const ambientPlaceDeps: MonPlaceDeps = {
1745
+ table: new MonAllocTable(reg.monsters.races, {
1746
+ maxDepth: reg.constants.maxDepth,
1747
+ oodChance: reg.constants.oodMonsterChance,
1748
+ oodAmount: reg.constants.oodMonsterAmount,
1749
+ }),
1750
+ groupMax: reg.constants.monsterGroupMax,
1751
+ groupDist: reg.constants.monsterGroupDist,
1752
+ ...(worldPreds ? { preds: worldPreds } : {}),
1753
+ /* mon_create_drop at live placement (summons, breeders, ambient spawns,
1754
+ * wizard): build the held pile so monster_death spills it. makeDeps makes
1755
+ * the objects; lore feeds the unique theft reduction. */
1756
+ makeDeps,
1757
+ lore: state.lore,
1758
+ };
1759
+ // monster_turn_multiply's multiply_monster (mon-move.c): a breeder spawns a
1760
+ // copy through the live placement path (reusing ambientPlaceDeps so the
1761
+ // scatter / createMonster draws stay faithful). monster_turn_multiply itself
1762
+ // (the cap / crowd / chance rolls) lives in game/monster-turn.ts.
1763
+ state.monsterMultiply = (m): boolean =>
1764
+ multiplyMonster(state, m, ambientPlaceDeps);
1765
+
1766
+ // Door-lock seams for monster_turn_can_move's locked-door branch: locks are
1767
+ // "door lock" traps (#21), so these route through the trap system when live.
1768
+ if (deps && lockKind) {
1769
+ state.doorLockPower = (grid: Loc): number =>
1770
+ squareDoorPower(state, grid, deps);
1771
+ state.setDoorLock = (grid: Loc, power: number): void =>
1772
+ squareSetDoorLock(state, grid, power, deps);
1773
+ state.removeDoorLock = (grid: Loc): void => {
1774
+ squareRemoveAllTraps(state, grid, lockKind.tidx);
1775
+ };
1776
+ }
1777
+
1778
+ /* player_inc_check for the world clock's over-exertion / DoT timed increases
1779
+ * (audit 01 T1, player-timed.c:1056): the game-turn callers (world.ts inc with
1780
+ * check=true - fainting SCRAMBLE, over-cast CONFUSED/IMAGE) previously passed
1781
+ * hooks lacking incCheck, so timed.ts defaulted to ALLOW and PROT_CONF /
1782
+ * RES_CHAOS / RES_NEXUS were ignored on that path. Shares makeIncCheckQueries
1783
+ * with the effect-interpreter env, reading the live derived state. */
1784
+ const worldIncQueries = makeIncCheckQueries(state);
1785
+ state.world = {
1786
+ timedTable: players.timed,
1787
+ timedHooks: {
1788
+ onMessage: (text: string, msgt?: string): void => state.msg?.(text, msgt),
1789
+ onNotify: (_idx: number, canDisturb: boolean): void => {
1790
+ if (canDisturb) disturb(state);
1791
+ },
1792
+ incCheck: (idx: number): boolean => {
1793
+ const eff = players.timed[idx];
1794
+ return eff ? playerIncCheck(eff, worldIncQueries) : true;
1795
+ },
1796
+ /* on_begin_effect / on_end_effect dispatch (audit 01 T2): when a timed
1797
+ * effect starts or lapses on the world clock (e.g. SPRINT ends -> SLOW,
1798
+ * SCRAMBLE ends -> UNSCRAMBLE_STATS), run its bound chain. */
1799
+ onTransition: (idx: number, begin: boolean, canDisturb: boolean): void => {
1800
+ runTimedTransition?.(idx, begin, canDisturb);
1801
+ },
1802
+ /* print_custom_message weapon substitution (obj-util.c:1118, gap 2.9):
1803
+ * the {name}/{kind}/{s}/{is} tags in weapon-related timed messages (the
1804
+ * temporary-brand on-begin/on-end lines) resolve against the CURRENTLY
1805
+ * wielded weapon, so a getter keeps it live across wield/takeoff. name /
1806
+ * kind use the ODESC_BASE approximation (kind name) already adopted across
1807
+ * the port (obj-knowledge.ts). No weapon => the obj == NULL tag forms. */
1808
+ get weapon(): TimedWeaponDesc {
1809
+ const w = state.actor.weapon;
1810
+ /* No weapon => the obj == NULL tag forms. A sentinel with name/kind
1811
+ * "hands" and number 2 reproduces substituteTimedMessage's obj == NULL
1812
+ * output byte-for-byte ({name}/{kind} -> "hands", {s} -> "", {is} ->
1813
+ * "are"), so exactOptionalPropertyTypes is satisfied without a getter
1814
+ * that returns undefined. */
1815
+ if (!w) return { name: "hands", kind: "hands", number: 2 };
1816
+ return { name: w.kind.name, kind: w.kind.name, number: w.number };
1817
+ },
1818
+ },
1819
+ /* The same shared consequences drive the world clock's DoT ticks, terrain
1820
+ * (lava) damage and over-exertion, so poison / a fatal wound / starvation
1821
+ * death records died_from and shows the full chain too. */
1822
+ takeHitHooks: sharedTakeHitHooks,
1823
+ expDeps,
1824
+ spawnAmbientMonster: (s: GameState): boolean =>
1825
+ pickAndPlaceDistantMonster(
1826
+ s,
1827
+ s.actor.grid,
1828
+ s.z.maxSight + 5,
1829
+ true,
1830
+ s.chunk.depth,
1831
+ ambientPlaceDeps,
1832
+ ),
1833
+ // cave_illuminate on the town dawn/nightfall boundary (game-world.c,
1834
+ // called from processWorld in game/loop.ts): relights SQUARE_GLOW and
1835
+ // updates player map knowledge (square_memorize/square_forget) to match.
1836
+ caveIlluminate: (s: GameState, dawn: boolean): void =>
1837
+ caveIlluminateKnown(s, dawn),
1838
+ /* player_take_terrain_damage's adjust_dam(PROJ_FIRE) needs the projection
1839
+ * table to scale lava damage by the player's fire resistance. */
1840
+ ...(reg.projections ? { projections: reg.projections } : {}),
1841
+ };
1842
+
1843
+ /* Surface the wizard/debug engine bundles (WP-14): all four locals are in
1844
+ * scope here (makeDeps L731, expDeps L603, trapDeps hoisted, wizardEffect
1845
+ * assigned in the projections block, ambientPlaceDeps the live MonPlaceDeps).
1846
+ * Null bundles are omitted so WizardDeps' optional fields stay absent. */
1847
+ const wizardBundles: WizardBundle = {
1848
+ makeDeps,
1849
+ expDeps,
1850
+ monPlace: ambientPlaceDeps,
1851
+ ...(wizardEffect ? { effect: wizardEffect } : {}),
1852
+ ...(trapDeps ? { trapDeps } : {}),
1853
+ };
1854
+
1855
+ return { registry, trapDeps, flavor, everseen, effects: effectRegistry, wizardBundles };
1856
+ }
1857
+
1858
+ /** The parts of a generated level that populate a GameState. */
1859
+ interface LevelContent {
1860
+ playerSpot: Loc | null;
1861
+ monsters: readonly { grid: Loc; mon: import("../mon/monster.js").Monster }[];
1862
+ objects: readonly { grid: Loc; obj: import("../obj/object.js").GameObject }[];
1863
+ trapGrids: readonly Loc[];
1864
+ /**
1865
+ * Kind+power chosen at generation (place_trap). Required for every trapGrid
1866
+ * when trapDeps is live; bare markers throw rather than re-draw (trap.c:356).
1867
+ */
1868
+ traps?: readonly { grid: Loc; tidx: number; power: number }[];
1869
+ lockedDoors: readonly { grid: Loc; power: number }[];
1870
+ depth: number;
1871
+ }
1872
+
1873
+ /**
1874
+ * Register a generated level's content on the live state: place the player,
1875
+ * the monsters (rebuilding groups from the generation group_info, exactly
1876
+ * as upstream rebuilds from a savefile), the floor piles, and instantiate
1877
+ * the marked traps and rolled door locks.
1878
+ */
1879
+ function populateFromLevel(
1880
+ state: GameState,
1881
+ level: LevelContent,
1882
+ trapDeps: TrapDeps | null,
1883
+ ): void {
1884
+ const spot: Loc = level.playerSpot ?? loc(1, 1);
1885
+ state.actor.grid = spot;
1886
+ placePlayer(state, spot);
1887
+
1888
+ /* Track the deepest level reached (player->max_depth). */
1889
+ if (level.depth > state.actor.player.maxDepth) {
1890
+ state.actor.player.maxDepth = level.depth;
1891
+ }
1892
+ /* A new level clears the decoy (glyph traps do not persist the swap). */
1893
+ state.decoy = null;
1894
+
1895
+ for (const pm of level.monsters) {
1896
+ pm.mon.grid = pm.grid;
1897
+ addMonster(state, pm.mon);
1898
+ }
1899
+ for (let i = 1; i < state.monsters.length; i++) {
1900
+ const mon = state.monsters[i];
1901
+ if (mon) monsterGroupAssign(state, mon, mon.groupInfo, true);
1902
+ }
1903
+ monsterGroupsVerify(state);
1904
+ updateMonsterDistances(state);
1905
+ /* Count racial occurrences (generation tracks uniques level-locally; the
1906
+ * live cur_num starts here and placement / deleteMonster maintain it). */
1907
+ countMonsterRaces(state);
1908
+
1909
+ // Register the generated floor objects as live piles (floor_carry), so
1910
+ // pickup / drop / projections operate on the same objects the level laid
1911
+ // down.
1912
+ for (const po of level.objects) {
1913
+ floorCarry(state, po.grid, po.obj);
1914
+ }
1915
+
1916
+ // Instantiate generation-marked traps on the live cave. Kind+power were
1917
+ // chosen at gen time (level.traps / place_trap); install without a second
1918
+ // pick/power draw (trap.c:356-394). Every C place_trap site must record a
1919
+ // GenTrap. Bare trapGrids markers are a gen bug - fail loudly rather than
1920
+ // re-drawing on the play stream or silently dropping them.
1921
+ if (trapDeps) {
1922
+ const descriptors = level.traps ?? [];
1923
+ const covered = new Set(
1924
+ descriptors.map((t) => t.grid.y * state.chunk.width + t.grid.x),
1925
+ );
1926
+ for (const grid of level.trapGrids) {
1927
+ const key = grid.y * state.chunk.width + grid.x;
1928
+ if (!covered.has(key)) {
1929
+ throw new Error(
1930
+ `populateFromLevel: bare trap marker at (${grid.x},${grid.y}) ` +
1931
+ `without kind+power; generation must call placeTrap with ` +
1932
+ `trapKinds so pick_trap + power are spent in the gen stream ` +
1933
+ `(trap.c:356-394)`,
1934
+ );
1935
+ }
1936
+ }
1937
+ for (const t of descriptors) {
1938
+ installTrap(state, t.grid, t.tidx, t.power, trapDeps);
1939
+ }
1940
+ for (const door of level.lockedDoors) {
1941
+ squareSetDoorLock(state, door.grid, door.power, trapDeps);
1942
+ }
1943
+ }
1944
+ }
1945
+
1946
+ /** square_isarrivable (cave-square.c L613) on the live cave: no occupant, no
1947
+ * player trap / web, and a floor or stair (a legal player arrival square). */
1948
+ function squareIsArrivable(state: GameState, grid: Loc): boolean {
1949
+ const c = state.chunk;
1950
+ if (c.mon(grid) !== 0) return false;
1951
+ if (squareIsPlayerTrap(state, grid)) return false;
1952
+ if (squareIsWebbed(state, grid)) return false;
1953
+ return c.isFloor(grid) || c.isStairs(grid);
1954
+ }
1955
+
1956
+ /**
1957
+ * sanitize_player_loc (generate.c L1265): keep the player's grid if it is a
1958
+ * legal arrival square (in bounds, arrivable, not a vault); otherwise pick a
1959
+ * random empty non-vault square, then fall back to a full linear scan (keeping
1960
+ * a vault square only as a last resort). A faithful port including the RNG
1961
+ * draws - reached only on the birth_levels_persist restore path. Mutates
1962
+ * state.actor.grid in place, as upstream mutates p->grid.
1963
+ */
1964
+ function sanitizePlayerLoc(state: GameState): void {
1965
+ const c = state.chunk;
1966
+ const preds = {
1967
+ isPlayerTrap: (g: Loc): boolean => squareIsPlayerTrap(state, g),
1968
+ isWebbed: (g: Loc): boolean => squareIsWebbed(state, g),
1969
+ isWarded: (g: Loc): boolean => squareIsWarded(state, g),
1970
+ };
1971
+ const isVault = (g: Loc): boolean => c.sqinfoHas(g, SQUARE.VAULT);
1972
+
1973
+ /* Allow direct transfer if the retained grid is teleportable. */
1974
+ const grid = state.actor.grid;
1975
+ if (
1976
+ c.inBoundsFully(grid) &&
1977
+ squareIsArrivable(state, grid) &&
1978
+ !isVault(grid)
1979
+ ) {
1980
+ return;
1981
+ }
1982
+
1983
+ /* A bunch of random locations. */
1984
+ for (let attempt = 1000; attempt > 0; attempt--) {
1985
+ const tx = state.rng.randint0(c.width - 1) + 1;
1986
+ const ty = state.rng.randint0(c.height - 1) + 1;
1987
+ const g = loc(tx, ty);
1988
+ if (squareIsEmptyLive(state, g, preds) && !isVault(g)) {
1989
+ state.actor.grid = g;
1990
+ return;
1991
+ }
1992
+ }
1993
+
1994
+ /* Whelp, that didn't work: scan the whole dungeon linearly from a random
1995
+ * start, remembering the last empty vault square as a fallback. */
1996
+ const ix = state.rng.randint0(c.width - 1) + 1;
1997
+ const iy = state.rng.randint0(c.height - 1) + 1;
1998
+ let tx = ix + 1;
1999
+ let ty = iy;
2000
+ if (tx >= c.width - 1) {
2001
+ tx = 1;
2002
+ ty = ty + 1;
2003
+ if (ty >= c.height - 1) ty = 1;
2004
+ }
2005
+ let vx = 1;
2006
+ let vy = 1;
2007
+ for (;;) {
2008
+ const g = loc(tx, ty);
2009
+ if (squareIsEmptyLive(state, g, preds)) {
2010
+ if (!isVault(g)) {
2011
+ state.actor.grid = g;
2012
+ return;
2013
+ }
2014
+ /* A vault, but remember it just in case. */
2015
+ vy = ty;
2016
+ vx = tx;
2017
+ }
2018
+ /* Oops, tried every tile. */
2019
+ if (tx === ix && ty === iy) break;
2020
+ tx = tx + 1;
2021
+ if (tx >= c.width - 1) {
2022
+ tx = 1;
2023
+ ty = ty + 1;
2024
+ if (ty >= c.height - 1) ty = 1;
2025
+ }
2026
+ }
2027
+
2028
+ /* Fallback vault location (or at least a non-crashy square). */
2029
+ state.actor.grid = loc(vx, vy);
2030
+ }
2031
+
2032
+ /**
2033
+ * dungeon_change_level + prepare_next_level: generate a fresh level at
2034
+ * `depth` from the state's own RNG stream and swap it into the state in
2035
+ * place. Installed commands keep working (they close over the state
2036
+ * object, whose chunk and entity stores are replaced).
2037
+ */
2038
+ function makeChangeLevel(
2039
+ state: GameState,
2040
+ reg: CoreRegistries,
2041
+ trapDeps: TrapDeps | null,
2042
+ opts: { inArena?: boolean } = {},
2043
+ ): (depth: number) => void {
2044
+ /* The level stashed when entering an arena: leaving a level FOR an
2045
+ * arena persists it even without birth_levels_persist (generate.c
2046
+ * L1349), and the arena exit restores it. */
2047
+ let arenaStash: {
2048
+ chunk: GameState["chunk"];
2049
+ monsters: GameState["monsters"];
2050
+ groups: GameState["groups"];
2051
+ floor: GameState["floor"];
2052
+ traps: GameState["traps"];
2053
+ known: GameState["known"];
2054
+ decoy: Loc | null;
2055
+ monMidx: number;
2056
+ } | null = null;
2057
+ let inArena = opts.inArena ?? false;
2058
+
2059
+ return (depth: number): void => {
2060
+ /* Consume the pending arrival-stair request (create_up_stair /
2061
+ * create_down_stair) exactly once, on every path - the equivalent of
2062
+ * player_place clearing the flags (player-util.c:1585-1586). Only a fresh
2063
+ * generation below acts on it; arena/recall/persist-restore paths clear it
2064
+ * without laying a stair, exactly as upstream leaves the flags unset. */
2065
+ const pendingStair = state.arrivalStair ?? null;
2066
+ delete state.arrivalStair;
2067
+
2068
+ /* --- Arena entry: EF_SINGLE_COMBAT fired the change. --- */
2069
+ if (state.arenaLevel && !inArena) {
2070
+ const mon = state.healthWho;
2071
+ if (mon) {
2072
+ arenaStash = {
2073
+ chunk: state.chunk,
2074
+ monsters: state.monsters,
2075
+ groups: state.groups,
2076
+ floor: state.floor,
2077
+ traps: state.traps,
2078
+ known: state.known,
2079
+ decoy: state.decoy ?? null,
2080
+ monMidx: mon.midx,
2081
+ };
2082
+ /* arena_gen (gen-cave.c L3984): 6x6 floor bounded by perm rock,
2083
+ * the player in one corner and the opponent in the other. */
2084
+ const arena = new Chunk(reg.features, 6, 6);
2085
+ arena.depth = state.chunk.depth;
2086
+ for (let y = 0; y < 6; y++) {
2087
+ for (let x = 0; x < 6; x++) {
2088
+ const edge = y === 0 || x === 0 || y === 5 || x === 5;
2089
+ arena.setFeat(loc(x, y), edge ? FEAT.PERM : FEAT.FLOOR);
2090
+ }
2091
+ }
2092
+ state.chunk = arena;
2093
+ installChunkFeatHook(state);
2094
+ state.monsters = [null];
2095
+ state.groups = [null];
2096
+ state.floor = new Map();
2097
+ state.traps = new Map();
2098
+ state.known = newKnownMap(6, 6);
2099
+ delete state.decoy;
2100
+
2101
+ /* wiz_light(chunk, p, false) (generate.c:1109): every arena level is
2102
+ * lit on generation. Upstream runs it while `chunk` is not yet `cave`,
2103
+ * so square_memorize / square_know_pile / square_forget all short-
2104
+ * circuit on their `c != cave` guard and the call is a pure
2105
+ * SQUARE_GLOW pass - hence isCurrentCave = false here. Without it the
2106
+ * arena is unlit and the opponent invisible without a light source. */
2107
+ wizLightLevel(state, true, false, false);
2108
+
2109
+ /* The monster is COPIED in (upstream memcpy); the original stays
2110
+ * in the stashed level and is finished on the way out. Held
2111
+ * objects are ignored, and it gets a fresh group. */
2112
+ const copy = blankMonster(mon.race);
2113
+ copy.originalRace = mon.originalRace;
2114
+ copy.hp = mon.hp;
2115
+ copy.maxhp = mon.maxhp;
2116
+ copy.mspeed = mon.mspeed;
2117
+ copy.energy = mon.energy;
2118
+ copy.mTimed.set(mon.mTimed);
2119
+ copy.mflag = mon.mflag.clone();
2120
+ copy.grid = loc(4, 1);
2121
+ addMonster(state, copy);
2122
+ state.groups[1] = { index: 1, leader: copy.midx, members: [copy.midx] };
2123
+ copy.groupInfo[0] = { index: 1, role: MON_GROUP.LEADER };
2124
+ state.healthWho = copy;
2125
+ targetSetMonster(state, copy);
2126
+
2127
+ placePlayer(state, loc(1, 4));
2128
+ inArena = true;
2129
+ delete state.targetDepth;
2130
+ state.updateBonuses?.(); /* on_new_level PU_BONUS -> calc_light */
2131
+ state.updateFov?.(state);
2132
+ return;
2133
+ }
2134
+ /* No tracked opponent: fall through to a normal change. */
2135
+ state.arenaLevel = false;
2136
+ }
2137
+
2138
+ /* --- Arena exit: the fight is over (or abandoned). --- */
2139
+ if (inArena) {
2140
+ inArena = false;
2141
+ state.arenaLevel = false;
2142
+ const stash = arenaStash;
2143
+ arenaStash = null;
2144
+ if (stash) {
2145
+ /* Restore the level left behind (the player marker stayed). */
2146
+ state.chunk = stash.chunk;
2147
+ installChunkFeatHook(state);
2148
+ state.monsters = stash.monsters;
2149
+ state.groups = stash.groups;
2150
+ state.floor = stash.floor;
2151
+ state.traps = stash.traps;
2152
+ state.known = stash.known;
2153
+ if (stash.decoy) state.decoy = stash.decoy;
2154
+ const back = state.oldGrid ?? state.actor.grid;
2155
+ state.actor.grid = back;
2156
+ state.chunk.setMon(back, -1);
2157
+ delete state.oldGrid;
2158
+
2159
+ /* Kill the arena monster's original (kill_arena_monster). */
2160
+ const orig = state.monsters[stash.monMidx];
2161
+ if (orig) {
2162
+ orig.hp = -1;
2163
+ state.msg?.(`${orig.race.name} is defeated!`);
2164
+ state.onPlayerKill?.(orig);
2165
+ deleteMonster(state, orig.midx);
2166
+ }
2167
+ state.healthWho = null;
2168
+ targetSetMonster(state, null);
2169
+ delete state.targetDepth;
2170
+ state.updateBonuses?.(); /* on_new_level PU_BONUS -> calc_light */
2171
+ state.updateFov?.(state);
2172
+ return;
2173
+ }
2174
+ /* The stash did not survive a save boundary: fall through to a
2175
+ * fresh level of the same depth (ledgered). */
2176
+ delete state.oldGrid;
2177
+ }
2178
+
2179
+ /* dungeon_change_level: track the deepest level reached. */
2180
+ if (depth > state.actor.player.maxDepth) {
2181
+ state.actor.player.maxDepth = depth;
2182
+ }
2183
+
2184
+ /* birth_levels_persist (#30, off by default): when on, the level being
2185
+ * left is frozen into a depth-keyed cache and a previously-frozen target
2186
+ * level is restored instead of regenerated (prepare_next_level's persist
2187
+ * branch, generate.c L1347-1556). The whole branch is gated on the option,
2188
+ * so default play runs the original fresh-level path below unchanged. The
2189
+ * key is `depth` - the faithful identity for upstream's level NAME
2190
+ * (chunk_find_name of level_by_depth(depth)->name); see StoredLevel. */
2191
+ const persist = state.options?.get("birth_levels_persist") ?? false;
2192
+ const currentDepth = state.chunk.depth;
2193
+
2194
+ /*
2195
+ * Non-persist town store (generate.c:1371-1373): leaving depth 0 always
2196
+ * keeps a terrain-only Town chunk so town_gen can reload the same layout
2197
+ * (shops, stairs, ruins) without birth_levels_persist.
2198
+ */
2199
+ if (!persist && currentDepth === 0) {
2200
+ state.townChunk = chunkWriteTerrain(state.chunk);
2201
+ }
2202
+
2203
+ /* wipe_mon_list: the old level's monsters forget their racial counts
2204
+ * before the new level allocates against them. Under persist the old level
2205
+ * (and its monsters) survives in the cache with its counts intact, exactly
2206
+ * as the persist branch skips cave_clear/wipe_mon_list. */
2207
+ if (!persist) {
2208
+ wipeMonsterCounts(state);
2209
+ }
2210
+ /* Forget the target and the tracked monster (game-world.c L1010),
2211
+ * and release any commanded monster (L1065). */
2212
+ targetSetMonster(state, null);
2213
+ state.healthWho = null;
2214
+ state.actor.player.timed[TMD.COMMAND] = 0;
2215
+
2216
+ if (persist) {
2217
+ const cache = (state.levelCache ??= new Map());
2218
+
2219
+ /* Freeze the level being left (cave_store, generate.c L1366).
2220
+ * compact_monsters(cave, 0) is the RNG-free "too many holes" pass; the
2221
+ * player marker is cleared (L1362, non-arena - arena freezing keeps it
2222
+ * and is handled by the arena stash above); the freeze turn is stamped on
2223
+ * the chunk (cave_store L1032) for restore_monsters. Persistent levels
2224
+ * keep their artifacts, so the non-persist artifact-loss loop below is
2225
+ * skipped on this path. */
2226
+ compactMonsters(state, 0);
2227
+ state.chunk.setMon(state.actor.grid, 0);
2228
+ state.chunk.turn = state.turn;
2229
+ /* chunk_list_add (gen-chunk.c L69) is this Map insert: the C appends the
2230
+ * stored chunk to a realloc'd chunk_list[] keyed by chunk->name (which is
2231
+ * level_by_depth(depth)->name), so a depth-keyed Map is the same lookup
2232
+ * without the name indirection or the manual growth. */
2233
+ cache.set(currentDepth, {
2234
+ chunk: state.chunk,
2235
+ monsters: state.monsters,
2236
+ groups: state.groups,
2237
+ floor: state.floor,
2238
+ traps: state.traps,
2239
+ known: state.known,
2240
+ decoy: state.decoy ?? null,
2241
+ turn: state.turn,
2242
+ /* chunk->join (generate.c L1203-1214): freeze the level's stair
2243
+ * connectors so a later first-visit neighbour can align its stairs. */
2244
+ join: state.currentJoins ?? [],
2245
+ });
2246
+
2247
+ /* Enter a previously-frozen target level: assign it back and let its
2248
+ * monsters recover over the elapsed turns (prepare_next_level
2249
+ * L1414-1506). chunk_find_name is the depth lookup here. */
2250
+ const stored = cache.get(depth);
2251
+ if (stored) {
2252
+ cache.delete(depth); // chunk_list_remove (L1505)
2253
+ state.chunk = stored.chunk;
2254
+ installChunkFeatHook(state);
2255
+ state.monsters = stored.monsters;
2256
+ state.groups = stored.groups;
2257
+ state.floor = stored.floor;
2258
+ state.traps = stored.traps;
2259
+ state.known = stored.known;
2260
+ state.decoy = stored.decoy;
2261
+ /* Restore the level's stair connectors so a subsequent departure
2262
+ * re-freezes them and adjacent first-visits keep aligning. */
2263
+ state.currentJoins = stored.join;
2264
+
2265
+ /* restore_monsters (mon-move.c L2007): HP regen + timed reduction over
2266
+ * the turns the level was frozen (turn - chunk->freeze-turn). */
2267
+ restoreMonsters(state, state.turn - stored.turn);
2268
+
2269
+ /* Place the player (prepare_next_level non-arena, L1497-1501): sanitize
2270
+ * the retained grid into a legal arrival square, then player_place.
2271
+ * DEFERRAL: exact stair-connector matching (dun->persist / one_off_*
2272
+ * join connectors, generate.c L1147-1152) is applied by upstream only
2273
+ * when GENERATING a persistent level, not when restoring one - a
2274
+ * restored level uses this same sanitize+place, so the round-trip is
2275
+ * faithful. The connector wiring for FIRST-visit generation stays
2276
+ * dormant (cave.ts): a first visit arrives at the fresh level's normal
2277
+ * playerSpot, which is a valid stair/empty grid. Level identity on
2278
+ * re-entry is exact; only the first-visit arrival stair is approximate. */
2279
+ sanitizePlayerLoc(state);
2280
+ placePlayer(state, state.actor.grid);
2281
+ installChunkFeatHook(state);
2282
+
2283
+ refreshTownStores(state, reg);
2284
+ delete state.targetDepth;
2285
+ state.updateBonuses?.(); /* on_new_level PU_BONUS -> calc_light */
2286
+ /* only_partial during level-entry FOV (ui-display.c:2522 / cave-view.c:851). */
2287
+ state.chunk.onlyPartial = true;
2288
+ state.updateFov?.(state);
2289
+ announceFeeling(state, reg);
2290
+ search(state); /* on_new_level (game-world.c:1052). */
2291
+ state.chunk.onlyPartial = false;
2292
+ return;
2293
+ }
2294
+ /* First visit to this depth: fall through to fresh generation. The old
2295
+ * level is already frozen; counts were not wiped and artifacts are not
2296
+ * lost (both skipped above / below under persist). */
2297
+ }
2298
+
2299
+ /* Consume stored town layout for non-persist re-entry (town_gen L2682). */
2300
+ const townLayout =
2301
+ !persist && depth === 0 && state.townChunk ? state.townChunk : null;
2302
+ if (townLayout) state.townChunk = null;
2303
+
2304
+ /* birth_levels_persist first-visit generation (generate.c L1147-1152): seed
2305
+ * this fresh level's stair connectors from the frozen adjacent levels so
2306
+ * up/down stairs line up, and mark the build persistent so dun.persist
2307
+ * branches run. Off by default: persist is false, joinInfo stays undefined,
2308
+ * and generateLevel builds exactly as before. */
2309
+ const persistCache = state.levelCache;
2310
+ let joinInfo: ReturnType<typeof getJoinInfo> | undefined;
2311
+ if (persist && persistCache) {
2312
+ const above = persistCache.get(depth - 1)?.join;
2313
+ const twoAbove = persistCache.get(depth - 2)?.join;
2314
+ const below = persistCache.get(depth + 1)?.join;
2315
+ const twoBelow = persistCache.get(depth + 2)?.join;
2316
+ joinInfo = getJoinInfo({
2317
+ ...(above ? { above } : {}),
2318
+ ...(twoAbove ? { twoAbove } : {}),
2319
+ ...(below ? { below } : {}),
2320
+ ...(twoBelow ? { twoBelow } : {}),
2321
+ });
2322
+ }
2323
+
2324
+ /* choose_profile's NOSCORE_JUMPING request (generate.c L824-836): consumed
2325
+ * once, cleared as it is passed on, exactly as the C clears the bit. */
2326
+ const jumpProfileName = state.jumpProfileName;
2327
+ state.jumpProfileName = undefined;
2328
+
2329
+ const g = generateLevel(
2330
+ state.rng,
2331
+ depth,
2332
+ {
2333
+ ...genDeps(
2334
+ reg,
2335
+ true,
2336
+ state.artifacts,
2337
+ state.options?.get("birth_no_artifacts") ?? false,
2338
+ /* mon_create_drop's unique theft reduction reads the live lore store. */
2339
+ state.lore,
2340
+ ),
2341
+ /* The mod behaviour seam, read at build time (this call is synchronous,
2342
+ * so a Fixes & tweaks toggle applies from the next level onward).
2343
+ * `levelGenerated` is the only hook cave_generate consults; absent =>
2344
+ * faithful. */
2345
+ ...(state.modHooks ? { hooks: state.modHooks } : {}),
2346
+ /* new_player_spot's placement failure (gen-util.c:422): always audible. */
2347
+ msg: (text: string): void => state.msg?.(text),
2348
+ /* cheat_room's restart narration (generate.c:1164, :1222): only when
2349
+ * the cheat option is on, which is what gates it upstream. */
2350
+ ...((state.options?.get("cheat_room") ?? false)
2351
+ ? { cheatMsg: (text: string): void => state.msg?.(text) }
2352
+ : {}),
2353
+ },
2354
+ /* is_daytime() only affects the town (depth 0) build; passed always so a
2355
+ * RECALL back to town honours the day/night clock. */
2356
+ {
2357
+ daytime: isDaytime(state.turn, state.z.dayLength),
2358
+ birthLoseArts: state.options?.get("birth_lose_arts") ?? false,
2359
+ questSpawns: questSpawnsForDepth(state, reg, depth),
2360
+ persist,
2361
+ ...(joinInfo ? { joinInfo } : {}),
2362
+ /* chunk_find_adjacent (gen-chunk.c:147): whether the neighbour levels
2363
+ * already exist in the frozen-level cache, so handle_level_stairs skips
2364
+ * the alloc_stairs for a direction the neighbour already seeded. Passed
2365
+ * unconditionally; only read under persist. */
2366
+ hasAdjacentAbove: persistCache?.has(depth - 1) ?? false,
2367
+ hasAdjacentBelow: persistCache?.has(depth + 1) ?? false,
2368
+ /* birth_connect_stairs (gen-util.c:427-433): lay the arrival stair the
2369
+ * stair command requested ("up" after a descent, "down" after an
2370
+ * ascent), unless the option is off. Recall/first-spawn leave
2371
+ * pendingStair null, so no arrival stair is laid. */
2372
+ ...(pendingStair &&
2373
+ (state.options?.get("birth_connect_stairs") ?? true)
2374
+ ? { createStair: pendingStair }
2375
+ : {}),
2376
+ ...(townLayout ? { townLayout } : {}),
2377
+ ...(jumpProfileName ? { profileName: jumpProfileName } : {}),
2378
+ },
2379
+ );
2380
+ /* chunk->join (generate.c L1203-1214): remember this level's stair
2381
+ * connectors for freezing and adjacent-level alignment. Gated on persist so
2382
+ * that with birth_levels_persist off the field stays unset and the savefile
2383
+ * is byte-identical to today (no currentJoins key emitted). */
2384
+ if (persist) state.currentJoins = g.joins;
2385
+ state.chunk = g.c;
2386
+ state.monsters = [null];
2387
+ state.groups = [null];
2388
+ /* Artifacts left on the abandoned level are lost (generate.c L1383-1394):
2389
+ * a known one (or any, under birth_lose_arts) is logged as missed. The
2390
+ * created-mark reset that lets an unknown one regenerate rides artifact
2391
+ * upkeep (#24). Runs before the floor is cleared below. This is the
2392
+ * NON-persist branch only: a frozen persistent level keeps its artifacts. */
2393
+ if (!persist) {
2394
+ const loseArts = state.options?.get("birth_lose_arts") ?? false;
2395
+ for (const pile of state.floor.values()) {
2396
+ for (const obj of pile) {
2397
+ if (
2398
+ obj.artifact &&
2399
+ (loseArts || (obj.notice & OBJ_NOTICE.ASSESSED) !== 0)
2400
+ ) {
2401
+ state.onArtifactLost?.(obj.artifact);
2402
+ }
2403
+ }
2404
+ }
2405
+ }
2406
+ state.floor = new Map();
2407
+ state.traps = new Map();
2408
+ state.known = newKnownMap(g.c.width, g.c.height);
2409
+ installChunkFeatHook(state);
2410
+ populateFromLevel(
2411
+ state,
2412
+ {
2413
+ playerSpot: g.playerSpot,
2414
+ monsters: g.monsters,
2415
+ objects: g.objects,
2416
+ trapGrids: [...g.trapGrids].map((i) => iToGrid(i, g.c.width)),
2417
+ traps: g.traps,
2418
+ lockedDoors: g.lockedDoors,
2419
+ depth,
2420
+ },
2421
+ trapDeps,
2422
+ );
2423
+ refreshTownStores(state, reg);
2424
+ /* town_gen -> cave_illuminate (gen-cave.c / cave-map.c L555): the town is
2425
+ * illuminated at generation. The chunk-flag half already ran in the town
2426
+ * builder (gen/cave.ts caveIlluminate); this applies the player-KNOWLEDGE
2427
+ * half against the freshly built known map so a daytime town is fully
2428
+ * memorized (visible from anywhere on entry, exactly like the C town) and a
2429
+ * night town forgets its boring floors. Town only (depth 0); dungeon levels
2430
+ * start dark. Runs on both birth and recall-to-town. */
2431
+ if (depth === 0) {
2432
+ caveKnown(state);
2433
+ caveIlluminateKnown(state, isDaytime(state.turn, state.z.dayLength));
2434
+ }
2435
+ /* cave_generate (generate.c:1255-1258): a builder that asked for the level
2436
+ * to be revealed (labyrinth_gen's "known" maze, gen-cave.c:1594) gets
2437
+ * `wiz_light(chunk, p, false)` and the flag is cleared. `chunk` is not yet
2438
+ * `cave` there, so square_memorize / square_know_pile / square_forget all
2439
+ * short-circuit on their `c != cave` guard and the call perma-LIGHTS the
2440
+ * level without memorizing anything - hence isCurrentCave = false. */
2441
+ if (g.lightLevel) {
2442
+ wizLightLevel(state, true, false, false);
2443
+ g.lightLevel = false;
2444
+ }
2445
+ delete state.targetDepth;
2446
+ /* on_new_level (game-world.c:1034-1037): PU_BONUS -> update_bonuses ->
2447
+ * calc_light recomputes cur_light for the NEW depth, then the view is
2448
+ * flooded. Without this the daytime-town cur_light (0) leaks into the
2449
+ * dungeon, so the torch radius is -1 and arrival renders dark until the
2450
+ * next bonus recompute. Run the bonus pass before the first view build. */
2451
+ state.updateBonuses?.();
2452
+ /* only_partial during level-entry FOV (ui-display.c:2522 / cave-view.c:851). */
2453
+ state.chunk.onlyPartial = true;
2454
+ state.updateFov?.(state);
2455
+ announceFeeling(state, reg);
2456
+ search(state); /* on_new_level (game-world.c:1052). */
2457
+ state.chunk.onlyPartial = false;
2458
+ };
2459
+ }
2460
+
2461
+ /**
2462
+ * Resolve the quest guardians to place on a freshly generated level
2463
+ * (generate.c L1170-1191). Returns one QuestSpawn per active quest whose
2464
+ * level == depth, mapping the stored race index to its MonsterRace. A
2465
+ * completed quest has level 0 (never a dungeon depth), so it is naturally
2466
+ * excluded. The unique-already-alive (cur_num > 0) skip is applied inside
2467
+ * generateLevel, matching upstream's placement-time check.
2468
+ */
2469
+ function questSpawnsForDepth(
2470
+ state: GameState,
2471
+ reg: CoreRegistries,
2472
+ depth: number,
2473
+ ): QuestSpawn[] {
2474
+ if (depth <= 0) return [];
2475
+ const quests = state.actor.player.quests;
2476
+ if (!quests || quests.length === 0) return [];
2477
+ const spawns: QuestSpawn[] = [];
2478
+ for (const q of quests) {
2479
+ if (q.level !== depth) continue;
2480
+ const race = reg.monsters.races[q.race];
2481
+ if (!race) continue;
2482
+ spawns.push({ race, maxNum: q.maxNum });
2483
+ }
2484
+ return spawns;
2485
+ }
2486
+
2487
+ /**
2488
+ * on_new_level's level-feeling announcement (game-world.c:1047-1049):
2489
+ *
2490
+ * if (player->depth)
2491
+ * display_feeling(false);
2492
+ *
2493
+ * The whole feeling system was ported - both message tables verbatim, the
2494
+ * obj/mon rating arithmetic, place_feeling's scatter, feeling_squares, the
2495
+ * birth_feelings gate, the LF: status row - and displayFeeling had exactly ONE
2496
+ * caller in the repo: the ^F command. Nothing announced it on arrival, so a
2497
+ * player who never pressed ^F never saw a feeling message in the whole game.
2498
+ * That is the shape a call-site census is built to catch: the function exists and
2499
+ * is correct, and no live path reaches it.
2500
+ *
2501
+ * The depth guard is the CALLER's upstream, not displayFeeling's own - the town
2502
+ * line ("Looks like a typical town.") is reachable only through ^F, so guarding
2503
+ * here rather than inside is what keeps the town silent on every arrival.
2504
+ *
2505
+ * Deliberately NOT called from the two arena paths, which mirror upstream's
2506
+ * `arena_level` early return at game-world.c:1044-1046.
2507
+ */
2508
+ function announceFeeling(state: GameState, reg: CoreRegistries): void {
2509
+ if (!state.chunk.depth) return;
2510
+ displayFeeling(state, { feelingNeed: reg.constants.feelingNeed });
2511
+ }
2512
+
2513
+ /**
2514
+ * store_init / store_reset / store_update lifecycle (store.c). The stores
2515
+ * (including FEAT_HOME) are PERSISTENT global state, created once and kept alive
2516
+ * for the whole game - they are NOT wiped on descent, so the home stash and
2517
+ * shop stock survive across levels and save/load (gap 12.1). First creation
2518
+ * (store_reset) fills every non-home shop; each subsequent return to town runs
2519
+ * store_update, catching the shops up on the days that elapsed in the dungeon
2520
+ * and occasionally shuffling a shopkeeper (gaps 12.2/12.3), consuming daycount.
2521
+ * No-op when the pack ships no stores; a loaded save's restored stores skip the
2522
+ * first-creation branch and (in town, daycount 0) leave the RNG stream untouched.
2523
+ */
2524
+ function refreshTownStores(state: GameState, reg: CoreRegistries): void {
2525
+ if (!reg.stores) return;
2526
+ const storeDeps: MakeDeps = {
2527
+ reg: reg.objects,
2528
+ alloc: new ObjAllocState(reg.objects, reg.constants),
2529
+ constants: reg.constants,
2530
+ /* Stores pass allowArtifacts=false, so these are inert here; shared
2531
+ * for consistency with the rest of the game's MakeDeps. */
2532
+ artifacts: state.artifacts ?? new ArtifactState(reg.objects.artifacts.length),
2533
+ noArtifacts: state.options?.get("birth_no_artifacts") ?? false,
2534
+ /* class book-rejection / curse-foil / no-selling generation foils. */
2535
+ ...genFoilFields(state),
2536
+ };
2537
+ if (!state.stores) {
2538
+ /* First time in town: store_init + store_reset. Build and stock every shop
2539
+ * once. Gated on depth 0 so a game that begins in the dungeon (tests, or a
2540
+ * future non-town start) draws no store RNG until town is actually reached -
2541
+ * preserving the exact stream and the pre-fix behaviour. */
2542
+ if (state.chunk.depth !== 0) return;
2543
+ state.stores = createTownStores(
2544
+ reg.stores.stores,
2545
+ storeDeps,
2546
+ state.rng,
2547
+ state.actor.player.maxDepth,
2548
+ /* parse_always book expansion (store.c:208-231): the bookseller's
2549
+ * no-sval always lines need class-book metadata to resolve which town
2550
+ * spellbooks to stock. Stashed on state by wireGame. */
2551
+ state.classes,
2552
+ );
2553
+ return;
2554
+ }
2555
+ /* Stores already exist (persisted across levels). On the return to town run
2556
+ * store_update over the accrued days, then zero daycount (store.c:1462). In
2557
+ * the dungeon the stores are simply left as-is (home stash preserved). */
2558
+ if (state.chunk.depth === 0) {
2559
+ const ctx: StoreMaintContext = {
2560
+ rng: state.rng,
2561
+ deps: storeDeps,
2562
+ maxDepth: state.actor.player.maxDepth,
2563
+ stores: state.stores,
2564
+ /* OPT(cheat_xtra) (store.c:1424, :1444). */
2565
+ ...(state.options?.get("cheat_xtra")
2566
+ ? { cheatMsg: (text: string): void => state.msg?.(text) }
2567
+ : {}),
2568
+ };
2569
+ storeUpdate(ctx, state.daycount ?? 0);
2570
+ state.daycount = 0;
2571
+ }
2572
+ }
2573
+
2574
+ /**
2575
+ * Materialise upkeep->inven[] and upkeep->quiver[] for a game that has just
2576
+ * come into existence, from birth (player_outfit -> inven_carry sets PU_INVEN,
2577
+ * which the first update_stuff turns into calc_inventory) or from a savefile
2578
+ * (rd_gear's explicit calc_inventory tail, load.c:1187).
2579
+ *
2580
+ * Both views are DERIVED and never persisted, and until this ran nothing built
2581
+ * them: the port only recomputed them as a side effect of an object command or
2582
+ * a game turn. Anything reading them before that - an inventory listing, an
2583
+ * item picker, the quiver screen - saw an empty pack and an empty quiver.
2584
+ *
2585
+ * `characterDungeon` is false here: upstream's "You re-arrange your quiver."
2586
+ * notice belongs to play, not to a character coming into being.
2587
+ */
2588
+ function buildGearViews(
2589
+ state: GameState,
2590
+ reg: CoreRegistries,
2591
+ ammoTval: number,
2592
+ ): void {
2593
+ calcInventory(state.gear, reg.constants, {
2594
+ store: false,
2595
+ ammoTval,
2596
+ objectValue: (obj: GameObject): number =>
2597
+ computeObjectValue(reg.objects, obj, 1, true),
2598
+ rogueLike: state.options?.get("rogue_like_commands") ?? false,
2599
+ characterDungeon: false,
2600
+ });
2601
+ }
2602
+
2603
+ /**
2604
+ * Assemble a runnable GameState from a pack: generate a level, birth a
2605
+ * character, derive its bonuses, and register the placed monsters. The
2606
+ * caller wires state.nextCommand (input) and state.updateFov (FOV) and then
2607
+ * drives runGameLoop; on LOOP_STATUS.LEVEL_CHANGE it calls
2608
+ * game.changeLevel(state.targetDepth) and clears state.generateLevel.
2609
+ */
2610
+ export function startGame(pack: GamePack, opts: StartGameOptions = {}): StartedGame {
2611
+ // Bind registries and the player domain first: spellbook object kinds
2612
+ // are created FROM the class book definitions (init.c write_book_kind),
2613
+ // and must exist before level generation builds the allocation tables
2614
+ // (so books spawn) and before the starting kit resolves.
2615
+ const reg = bindCore(pack);
2616
+ const players = bindPlayer(pack.player);
2617
+ registerBookKinds(reg.objects, players.classes);
2618
+
2619
+ // The player option store (option.c options_init_defaults): seeded from
2620
+ // OPTION_ENTRIES defaults, with the birth/interface choices applied. Built
2621
+ // before level generation so birth_randarts can swap the artifact set first.
2622
+ // Every Angband option (show_damage, center_player, auto_more, ...) ships in
2623
+ // core with its faithful upstream default (OPTION_ENTRIES.normal); the player
2624
+ // changes them in the options menu exactly as upstream. Only explicit birth /
2625
+ // interface CHOICES (birth screen, point-buy) are applied here as overrides,
2626
+ // so with none supplied the store is byte-identical to the table.
2627
+ const options = new OptionState({
2628
+ ...(opts.optionOverrides ? { overrides: opts.optionOverrides } : {}),
2629
+ ...(opts.hitpointWarn !== undefined ? { hitpointWarn: opts.hitpointWarn } : {}),
2630
+ });
2631
+
2632
+ // options_init_cheat (player-birth.c:1234, gap 1.12): at character accept,
2633
+ // clear every cheat option and its score twin, THEN re-apply the port-only
2634
+ // birth optionOverrides seam AFTER the clear. Maintainer-decided faithful
2635
+ // ordering: a NORMAL birth matches upstream exactly (cheat options default
2636
+ // off, so this clear is a no-op), while an EXPLICIT birth-time cheat override
2637
+ // (the port-only seam with no upstream equivalent) still survives because it
2638
+ // is re-applied last. Birth options were already frozen into the snapshot at
2639
+ // construction, so re-applying them here is a harmless no-op (set() is locked
2640
+ // for birth options); only the interface/cheat/score overrides take effect.
2641
+ optionsInitCheat(options);
2642
+ if (opts.optionOverrides) {
2643
+ for (const [name, value] of Object.entries(opts.optionOverrides)) {
2644
+ if (value !== undefined) options.set(name, value);
2645
+ }
2646
+ }
2647
+
2648
+ // Main game RNG (Decision 6.2): one continuous stream for birth UI
2649
+ // continuation (opts.rng / opts.rngState), store_reset, seed_randart,
2650
+ // seed_flavor, player outfit, and level gen. C player-birth.c:1269-1292
2651
+ // order is store_reset, then seed_randart immediately followed by
2652
+ // seed_flavor; prepare_next_level (ui-game.c:757-760) runs only later.
2653
+ const mainRng = opts.rng ?? new Rng(opts.seed ?? 1);
2654
+ if (!opts.rng && opts.rngState) mainRng.setState(opts.rngState);
2655
+
2656
+ const race =
2657
+ (opts.raceName ? players.raceByName(opts.raceName) : null) ??
2658
+ players.raceByName("Human") ??
2659
+ players.races[0]!;
2660
+ const cls =
2661
+ (opts.className ? players.classByName(opts.className) : null) ??
2662
+ players.classByName("Warrior") ??
2663
+ players.classes[0]!;
2664
+
2665
+ const body = players.bodies[race.body] ?? players.bodies[0]!;
2666
+ // Point-based birth: apply the chosen allocation (drawing no stat RNG) only
2667
+ // when the roller method is "point" AND a full stat array was supplied;
2668
+ // otherwise fall through to the classic roller (the unchanged default).
2669
+ const usePointBuy =
2670
+ opts.roller === "point" &&
2671
+ opts.birthStats !== undefined &&
2672
+ opts.birthStats.length === STAT_MAX;
2673
+ // The accepted standard-roller stats ride opts.rolledStats (gap, birth
2674
+ // thread-through): applied verbatim, they draw no stat RNG, so the character
2675
+ // is born with exactly the set the roller UI showed.
2676
+ const useRolledStats =
2677
+ opts.rolledStats !== undefined && opts.rolledStats.length === STAT_MAX;
2678
+ // Character birth draws (roll_hp / ahw / history / stats) run before
2679
+ // store_reset, matching C accept order where player_generate / roll_hp
2680
+ // precede store_reset (player-birth.c:1236-1270). When the shell already
2681
+ // advanced the stream for birth UI and supplies rolledStats/history, only
2682
+ // roll_hp / ahw / the history walk remain here.
2683
+ const birth = generatePlayer(
2684
+ race,
2685
+ cls,
2686
+ {
2687
+ body,
2688
+ historyChart: players.historyChart(race),
2689
+ ...(useRolledStats ? { rolledStats: opts.rolledStats } : {}),
2690
+ ...(usePointBuy ? { stats: opts.birthStats } : {}),
2691
+ ...(opts.history !== undefined ? { historyOverride: opts.history } : {}),
2692
+ },
2693
+ mainRng,
2694
+ );
2695
+
2696
+ // player_quests_reset (player-quest.c L157, called from player_birth): copy
2697
+ // the standard quest table into the fresh character's quest history so
2698
+ // is_quest and quest_check see the Sauron/Morgoth guardians from turn one.
2699
+ playerQuestsReset(birth.player, reg.quests);
2700
+
2701
+ // C player-birth.c:1269-1292 then ui-game.c:757-760:
2702
+ // store_reset -> seed_randart -> seed_flavor -> (later) prepare_next_level
2703
+ // Town store_reset draws on the main stream BEFORE both seeds; level gen
2704
+ // draws come AFTER both seeds. Gated on starting depth 0 so a game that
2705
+ // begins in the dungeon (tests) still draws no store RNG until town.
2706
+ const startDepth = opts.depth ?? 1;
2707
+ let earlyStores: Store[] | undefined;
2708
+ if (reg.stores && startDepth === 0) {
2709
+ const bookKeys = new Set(
2710
+ birth.player.cls.magic.books.map((b) => `${b.tvalIdx},${b.sval}`),
2711
+ );
2712
+ const storeDeps: MakeDeps = {
2713
+ reg: reg.objects,
2714
+ alloc: new ObjAllocState(reg.objects, reg.constants),
2715
+ constants: reg.constants,
2716
+ artifacts: new ArtifactState(reg.objects.artifacts.length),
2717
+ noArtifacts: options.get("birth_no_artifacts") ?? false,
2718
+ timedFoil: buildCurseTimedFoil(players.timed),
2719
+ canBrowseBook: (kind): boolean =>
2720
+ bookKeys.has(`${kind.tval},${kind.sval}`),
2721
+ noSelling: options.get("birth_no_selling") ?? false,
2722
+ };
2723
+ earlyStores = createTownStores(
2724
+ reg.stores.stores,
2725
+ storeDeps,
2726
+ mainRng,
2727
+ birth.player.maxDepth,
2728
+ players.classes,
2729
+ );
2730
+ }
2731
+
2732
+ // OPT(player, birth_randarts) (obj-randart.c do_randart): seed_randart =
2733
+ // randint0(0x10000000) on the main stream (player-birth.c:1285), then
2734
+ // seed_flavor immediately after (player-birth.c:1291).
2735
+ let randartSeed = 0;
2736
+ if (options.get("birth_randarts")) {
2737
+ randartSeed = mainRng.randint0(0x10000000);
2738
+ swapRandartSet(
2739
+ reg,
2740
+ randartSeed,
2741
+ buildCurseTimedFoil(players.timed),
2742
+ buildActivationSummarizer(pack, reg),
2743
+ );
2744
+ }
2745
+ const seedFlavor = mainRng.randint0(0x10000000);
2746
+
2747
+ // aup_info[] (obj-make.c): the game's shared artifact-created registry.
2748
+ // Built AFTER swapRandartSet so its length matches the (index-preserving)
2749
+ // final artifact set, and BEFORE bootLevel so the starting level shares it.
2750
+ const artifacts = new ArtifactState(reg.objects.artifacts.length);
2751
+ const noArtifacts = options.get("birth_no_artifacts") ?? false;
2752
+
2753
+ // player_outfit (player-birth.c L1299): after seed_flavor, before level gen.
2754
+ const gear = newGear();
2755
+ /* object_flavor_aware(p, obj) on each start item (player-birth.c:650) has to
2756
+ * wait for wireGame to build the awareness store, so collect the kinds here
2757
+ * and apply them below. Without this a Warrior's own starting Flask of Oil
2758
+ * read as "a Clear Flask" - named by flavour, as though they had found it on
2759
+ * the floor. */
2760
+ const startKinds: ObjectKind[] = [];
2761
+ outfitPlayer(gear, birth.player, reg.objects, mainRng, reg.constants, {
2762
+ opt: (name: string): boolean => options.get(name) ?? false,
2763
+ onStartKind: (kind): void => void startKinds.push(kind),
2764
+ });
2765
+
2766
+ // prepare_next_level (ui-game.c:757-760): level generation AFTER both seeds.
2767
+ const booted = bootLevel(pack, {
2768
+ ...opts,
2769
+ rng: mainRng,
2770
+ registries: reg,
2771
+ artifacts,
2772
+ noArtifacts,
2773
+ });
2774
+
2775
+ // Resolve the worn objects by body slot; calc_bonuses reads them for the
2776
+ // equipment analysis, and the wielded weapon drives melee (py_attack).
2777
+ const equipment = birth.player.equipment.map((h) =>
2778
+ h ? gearGet(gear, h) : null,
2779
+ );
2780
+ const weaponSlot = birth.player.body.slots.findIndex(
2781
+ (s) => s.type === "WEAPON",
2782
+ );
2783
+ const weapon = weaponSlot >= 0 ? (equipment[weaponSlot] ?? null) : null;
2784
+
2785
+ const pstate = calcBonuses(birth.player, {
2786
+ equipment,
2787
+ timedEffects: players.timed,
2788
+ curses: reg.objects.curses,
2789
+ update: true,
2790
+ });
2791
+ const combat = toCombatState(pstate);
2792
+
2793
+ // Spell bookkeeping for casting classes: size the spell arrays, compute
2794
+ // the learnable-spell allowance (calc_spells) and mana (calc_mana, with
2795
+ // the worn-armor weight over the class allowance as the penalty).
2796
+ playerSpellsInit(birth.player);
2797
+ calcSpells(birth.player, pstate.statInd);
2798
+ /* Record cumber_armor on the birth state too, so the first refreshDerived of
2799
+ * the game diffs against the true starting value and cannot announce an
2800
+ * armour change that never happened. */
2801
+ pstate.cumberArmor = calcMana(
2802
+ birth.player,
2803
+ pstate.statInd,
2804
+ wornArmorWeight(birth.player, equipment),
2805
+ );
2806
+ birth.player.csp = birth.player.msp; // born rested, full mana
2807
+
2808
+ const spot: Loc = booted.playerSpot ?? loc(1, 1);
2809
+ const actor: PlayerActor = {
2810
+ player: birth.player,
2811
+ grid: spot,
2812
+ energy: 0,
2813
+ speed: pstate.speed,
2814
+ totalEnergy: 0,
2815
+ combat,
2816
+ defense: toDefenderState(pstate),
2817
+ weapon,
2818
+ stealth: combat.skills[SKILL.STEALTH] ?? 0,
2819
+ light: pstate.curLight,
2820
+ unlight: pstate.pflags.has(PF.UNLIGHT),
2821
+ };
2822
+
2823
+ const state: GameState = {
2824
+ rng: booted.rng,
2825
+ chunk: booted.chunk,
2826
+ actor,
2827
+ gear,
2828
+ monsters: [null],
2829
+ groups: [null],
2830
+ floor: new Map(),
2831
+ traps: new Map(),
2832
+ known: newKnownMap(booted.chunk.width, booted.chunk.height),
2833
+ target: newTargetState(),
2834
+ ignore: new IgnoreSettings(),
2835
+ autoinscribe: new AutoinscriptionRegistry(),
2836
+ runeNotes: new RuneNoteRegistry(),
2837
+ options,
2838
+ artifacts,
2839
+ /* Effective mod-rule flags (declarative bundled-mod seam): absent/empty =
2840
+ * faithful 4.2.6. Seeded a copy so later menu toggles mutate this map, not
2841
+ * the caller's. */
2842
+ ...(opts.modRules ? { modRules: { ...opts.modRules } } : {}),
2843
+ /* The behaviour seam itself, already composed by the host. Not copied: it is
2844
+ * a fold of functions, not mutable player state, and the menu re-composes it
2845
+ * rather than editing it in place. */
2846
+ ...(opts.modHooks ? { modHooks: opts.modHooks } : {}),
2847
+ lore: new Map(),
2848
+ /* birth_levels_persist (#30) frozen-level cache; empty until a level is
2849
+ * left with the option on (the whole persist path is option-gated). */
2850
+ levelCache: new Map(),
2851
+ turn: 0,
2852
+ z: {
2853
+ ...DEFAULT_GAME_CONSTANTS,
2854
+ maxSight: reg.constants.maxSight,
2855
+ maxRange: reg.constants.maxRange,
2856
+ floorSize: reg.constants.floorSize,
2857
+ maxDepth: reg.constants.maxDepth,
2858
+ stairSkip: reg.constants.stairSkip,
2859
+ dayLength: reg.constants.dayLength,
2860
+ foodValue: reg.constants.foodValue,
2861
+ allocMonsterChance: reg.constants.allocMonsterChance,
2862
+ storeTurns: reg.constants.storeTurns,
2863
+ lifeDrainPercent: reg.constants.lifeDrainPercent,
2864
+ levelMonsterMax: reg.constants.levelMonsterMax,
2865
+ },
2866
+ brands: reg.objects.brands,
2867
+ slays: reg.objects.slays,
2868
+ /* Placeholder; wireGame installs the full registry-backed env. */
2869
+ runeEnv: makeRuneEnv(
2870
+ () => null,
2871
+ () => false,
2872
+ ),
2873
+ playing: true,
2874
+ isDead: false,
2875
+ generateLevel: false,
2876
+ nextCommand: (): PlayerCommand | null => null,
2877
+ // store_reset already ran on the main stream before seed_randart/flavor.
2878
+ ...(earlyStores ? { stores: earlyStores } : {}),
2879
+ };
2880
+
2881
+ // seed_flavor already drawn above; flavor_init runs inside wireGame.
2882
+ if (opts.updateFov) state.updateFov = opts.updateFov;
2883
+ const wired = wireGame(state, reg, players, pstate, seedFlavor, pack);
2884
+ /* PU_INVEN from the starting kit's inven_carry (player-birth.c). */
2885
+ buildGearViews(state, reg, pstate.ammoTval);
2886
+
2887
+ // kind->everseen = true for each bought start item (player-birth.c L658). At
2888
+ // birth the gear holds only the starting kit, so marking every carried kind
2889
+ // is exactly the start-item set. Pure Set insert, no RNG.
2890
+ for (const obj of state.gear.store.values()) {
2891
+ wired.everseen.markKind(obj.kind);
2892
+ }
2893
+
2894
+ // object_flavor_aware(p, obj) per start item (player-birth.c:650), applied now
2895
+ // that flavor_init has run and the awareness store exists. NOOP deps, not the
2896
+ // live ones: upstream's L2276-79 ignore side effects re-check a pack that does
2897
+ // not exist yet at this point in birth, and the first real notice_stuff pass
2898
+ // covers it. No RNG.
2899
+ for (const kind of startKinds) {
2900
+ wired.flavor.objectFlavorAware(kind, NOOP_FLAVOR_AWARE_DEPS);
2901
+ }
2902
+
2903
+ // birth_know_runes (player-birth.c L1261-1262): a birth_know_runes character
2904
+ // knows every rune for ID-on-walkover (gap 1.5). No RNG. Before
2905
+ // player_learn_innate, matching the C acceptance order.
2906
+ if (options.get("birth_know_runes")) {
2907
+ playerLearnAllRunes(birth.player, state.runeEnv);
2908
+ }
2909
+
2910
+ // birth_know_flavors (player-birth.c L1295-1296): a birth_know_flavors
2911
+ // character is aware of every flavoured kind for auto-ID of consumables (gap
2912
+ // 1.5). flavor_init already ran inside wireGame. No RNG.
2913
+ if (options.get("birth_know_flavors")) {
2914
+ flavorSetAllAware(wired.flavor, reg.objects.kinds, (k) =>
2915
+ state.hasFlavor ? state.hasFlavor(k) : false,
2916
+ );
2917
+ }
2918
+
2919
+ // Racial rune knowledge (player-birth.c L1274 player_learn_innate) and the
2920
+ // starting kit's obvious runes (L495 object_learn_on_wield): the outfit
2921
+ // wield ran before the rune env existed and learned only the modifier
2922
+ // runes, so run the full wield learning over the worn items now (their
2923
+ // WORN notice bit is still clear).
2924
+ playerLearnInnate(birth.player, state.runeEnv);
2925
+ for (let i = 0; i < birth.player.body.count; i++) {
2926
+ const worn = state.runeEnv.slotObject(i);
2927
+ if (worn) objectLearnOnWield(birth.player, worn, state.runeEnv);
2928
+ }
2929
+
2930
+ populateFromLevel(
2931
+ state,
2932
+ {
2933
+ playerSpot: booted.playerSpot,
2934
+ monsters: booted.monsters,
2935
+ objects: booted.objects,
2936
+ trapGrids: booted.trapGrids,
2937
+ traps: booted.traps,
2938
+ lockedDoors: booted.lockedDoors,
2939
+ depth: booted.depth,
2940
+ },
2941
+ wired.trapDeps,
2942
+ );
2943
+ // Stores already created pre-seed when startDepth === 0; refresh is then a
2944
+ // daycount-0 no-op. Depth > 0 still no-ops until the first town entry.
2945
+ refreshTownStores(state, reg);
2946
+ /* town_gen -> cave_illuminate (gen-cave.c / cave-map.c L555): the birth town
2947
+ * is illuminated at generation. The chunk-flag half ran in the town builder;
2948
+ * this applies the player-KNOWLEDGE half so a daytime town is fully memorized
2949
+ * (visible from the spawn corner on entry, exactly like the C town) and a
2950
+ * night town forgets its boring floors. Town only (booted depth 0); the level
2951
+ * changer (changeLevel) does the same for recall-to-town. */
2952
+ if (booted.depth === 0) {
2953
+ caveKnown(state);
2954
+ caveIlluminateKnown(state, isDaytime(state.turn, state.z.dayLength));
2955
+ }
2956
+
2957
+ /*
2958
+ * only_partial during the initial level-entry FOV (ui-display.c:2522 /
2959
+ * cave-view.c:851): C sets this for the first new-level display too, not
2960
+ * only for later change_level transitions.
2961
+ *
2962
+ * The guard used to be load-bearing: with no default updateFov, a host that had
2963
+ * not wired one yet got NO initial flood, and onlyPartial was left set for it to
2964
+ * run later. That is what left an agent with a blank map - it never wired one, so
2965
+ * the else branch was the whole story rather than a fallback. wireGame now
2966
+ * installs a default, so the branch is always taken. The `if` stays because a
2967
+ * host may still replace the seam with its own, and one that deleted it outright
2968
+ * should get silence rather than a crash.
2969
+ */
2970
+ state.chunk.onlyPartial = true;
2971
+ if (state.updateFov) {
2972
+ state.updateFov(state);
2973
+ announceFeeling(state, reg);
2974
+ search(state); /* on_new_level (game-world.c:1052). */
2975
+ state.chunk.onlyPartial = false;
2976
+ }
2977
+
2978
+ return {
2979
+ state,
2980
+ registry: wired.registry,
2981
+ effects: wired.effects,
2982
+ booted,
2983
+ players,
2984
+ flavor: wired.flavor,
2985
+ everseen: wired.everseen,
2986
+ seedFlavor,
2987
+ /* A freshly-birthed game is core-only, deterministic, with no mod bags or
2988
+ * quarantined content (P7.2). A future mod loader seeds a richer manifest. */
2989
+ manifest: coreOnlyManifest(),
2990
+ mods: {},
2991
+ orphans: {},
2992
+ orphansAcknowledged: false,
2993
+ options,
2994
+ randartSeed,
2995
+ changeLevel: makeChangeLevel(state, reg, wired.trapDeps),
2996
+ wizardBundles: wired.wizardBundles,
2997
+ ...makeStoreApi(state, reg, wired.flavor, options),
2998
+ };
2999
+ }
3000
+
3001
+ /**
3002
+ * The store buy/sell/price closures a shell uses: they build the maintenance
3003
+ * context (for restock after a purchase empties a shop), read the live flavour
3004
+ * knowledge and no-selling option, and route through the ported store runtime.
3005
+ * Shared by startGame and loadGame so both StartedGame results expose them.
3006
+ */
3007
+ function makeStoreApi(
3008
+ state: GameState,
3009
+ reg: CoreRegistries,
3010
+ flavor: FlavorKnowledge,
3011
+ options: OptionState,
3012
+ ): Pick<StartedGame, "buy" | "sell" | "sellFloor" | "price" | "willBuy"> {
3013
+ const storeCtx = (): {
3014
+ rng: typeof state.rng;
3015
+ deps: MakeDeps;
3016
+ maxDepth: number;
3017
+ stores: Store[];
3018
+ } => ({
3019
+ rng: state.rng,
3020
+ deps: {
3021
+ reg: reg.objects,
3022
+ alloc: new ObjAllocState(reg.objects, reg.constants),
3023
+ constants: reg.constants,
3024
+ /* allowArtifacts=false in store generation; inert but shared. */
3025
+ artifacts: state.artifacts ?? new ArtifactState(reg.objects.artifacts.length),
3026
+ noArtifacts: state.options?.get("birth_no_artifacts") ?? false,
3027
+ /* class book-rejection / curse-foil / no-selling generation foils. */
3028
+ ...genFoilFields(state),
3029
+ },
3030
+ maxDepth: state.actor.player.maxDepth,
3031
+ stores: state.stores ?? [],
3032
+ });
3033
+ const noSelling = (): boolean => options.get("birth_no_selling") ?? false;
3034
+ /* update_stuff's PU_INVEN after a gear change (obj-gear.c: inven_carry /
3035
+ * gear_object_for_use both set PU_INVEN): rebuild the computed quiver so
3036
+ * bought/sold ammo is routed into (or out of) the quiver, exactly as the
3037
+ * wield/takeoff/use paths already do (obj-cmd.ts). Without this a purchased
3038
+ * bolt stays displayed in the pack instead of the quiver. Mirrors the
3039
+ * calc_inventory opts assembled for the object commands above. */
3040
+ const refreshQuiver = (): void => {
3041
+ calcInventory(state.gear, reg.constants, {
3042
+ store: false,
3043
+ /* Live ammo_tval, as refreshInventory above and calcInvOpts do; without
3044
+ * it a store purchase re-sorted the quiver ignoring the wielded
3045
+ * launcher. */
3046
+ ammoTval: state.playerState?.ammoTval ?? 0,
3047
+ objectValue: (obj: GameObject): number =>
3048
+ computeObjectValue(reg.objects, obj, 1, true),
3049
+ rogueLike: state.options?.get("rogue_like_commands") ?? false,
3050
+ characterDungeon: true,
3051
+ msg: (text: string): void => state.msg?.(text),
3052
+ });
3053
+ };
3054
+ const txnKnow = (obj: GameObject): TxnKnowledge => ({
3055
+ flavor,
3056
+ flavorDeps: flavorAwareDeps(state),
3057
+ aware: flavor.isAware(obj.kind),
3058
+ noSelling: noSelling(),
3059
+ /* do_cmd_sell L1946-1951: selling teaches the runes as well as the flavour.
3060
+ * buildRuneList is rebuilt per call for the same reason effect-item.ts does
3061
+ * it (L930): a mod can add runes mid-session, so a cached list would go
3062
+ * stale. Unused by storeBuy, which learns flavour only. */
3063
+ learnRunes: { env: state.runeEnv, runes: buildRuneList(state.runeEnv) },
3064
+ });
3065
+ return {
3066
+ buy: (store, obj, amt): BuyResult => {
3067
+ /* do_cmd_retrieve (store.c:1783): Home Take is free - no price, no
3068
+ * ORIGIN_STORE, no shuffle/maint RNG. Reuse the existing homeRetrieve. */
3069
+ if (store.feat === FEAT.HOME) {
3070
+ const r = homeRetrieve(store, obj, amt, state.gear, reg.constants);
3071
+ if (!r.ok) {
3072
+ return {
3073
+ ok: false,
3074
+ failure: r.failure === "no-room" ? "no-room" : "not-in-stock",
3075
+ };
3076
+ }
3077
+ refreshQuiver();
3078
+ return r.obj
3079
+ ? { ok: true, price: 0, bought: r.obj }
3080
+ : { ok: true, price: 0 };
3081
+ }
3082
+ const result = storeBuy(storeCtx(), store, obj, amt, state.actor.player, state.gear, txnKnow(obj));
3083
+ if (result.ok) refreshQuiver();
3084
+ return result;
3085
+ },
3086
+ sell: (store, handle, amt): SellResult => {
3087
+ /* do_cmd_stash (store.c:2009): Home Drop is free, pack-style stacking,
3088
+ * no value gate / note-fuel-timeout rewrite. Reuse homeStash/homeCarry. */
3089
+ if (store.feat === FEAT.HOME) {
3090
+ const r = homeStash(
3091
+ store,
3092
+ handle,
3093
+ amt,
3094
+ state.actor.player,
3095
+ state.gear,
3096
+ reg.constants,
3097
+ );
3098
+ if (!r.ok) {
3099
+ return {
3100
+ ok: false,
3101
+ failure: r.failure ?? "no-item",
3102
+ };
3103
+ }
3104
+ refreshQuiver();
3105
+ return {
3106
+ ok: true,
3107
+ price: 0,
3108
+ ...(r.obj ? { sold: r.obj } : {}),
3109
+ ...(r.noneLeft !== undefined ? { noneLeft: r.noneLeft } : {}),
3110
+ carried: true,
3111
+ };
3112
+ }
3113
+ const obj = state.gear.store.get(handle);
3114
+ const know = obj
3115
+ ? txnKnow(obj)
3116
+ : { flavor, flavorDeps: flavorAwareDeps(state), aware: false, noSelling: noSelling() };
3117
+ const result = storeSell(storeCtx(), store, handle, amt, state.actor.player, state.gear, know);
3118
+ /* do_cmd_sell: selling an artifact reveals it (history_find_artifact,
3119
+ * store.c L1928); if the store then discards it, it is lost (L1992). */
3120
+ if (result.ok && result.sold?.artifact) {
3121
+ state.onArtifactFound?.(result.sold.artifact);
3122
+ if (result.carried === false) state.onArtifactLost?.(result.sold.artifact);
3123
+ }
3124
+ /* apply_autoinscription (store.c:1976-1977): "Autoinscribe if we still
3125
+ * have any" - the remaining stack, not the sold copy. */
3126
+ if (result.ok && result.noneLeft === false) {
3127
+ const left = state.gear.store.get(handle);
3128
+ if (left) state.autoinscribeObject?.(left);
3129
+ }
3130
+ if (result.ok) refreshQuiver();
3131
+ return result;
3132
+ },
3133
+ sellFloor: (store, obj, amt): SellResult => {
3134
+ /* Home Drop from the floor pile: room check, detach, home_carry. */
3135
+ if (store.feat === FEAT.HOME) {
3136
+ const n = Math.min(amt, obj.number);
3137
+ const dummy = objectCopyAmt(obj, n);
3138
+ if (!storeCheckNum(store, dummy)) {
3139
+ return { ok: false, failure: "no-room" };
3140
+ }
3141
+ const { usable, noneLeft } = floorObjectForUse(state, obj, n);
3142
+ homeCarry(store, usable, reg.constants);
3143
+ refreshQuiver();
3144
+ return {
3145
+ ok: true,
3146
+ price: 0,
3147
+ sold: usable,
3148
+ noneLeft,
3149
+ carried: true,
3150
+ };
3151
+ }
3152
+ const result = storeSellFloor(
3153
+ storeCtx(),
3154
+ store,
3155
+ obj,
3156
+ amt,
3157
+ state.actor.player,
3158
+ txnKnow(obj),
3159
+ (n) => {
3160
+ const { usable, noneLeft } = floorObjectForUse(state, obj, n);
3161
+ return { obj: usable, noneLeft };
3162
+ },
3163
+ );
3164
+ /* do_cmd_sell artifact reveal / loss, exactly as the gear-handle path. */
3165
+ if (result.ok && result.sold?.artifact) {
3166
+ state.onArtifactFound?.(result.sold.artifact);
3167
+ if (result.carried === false) state.onArtifactLost?.(result.sold.artifact);
3168
+ }
3169
+ if (result.ok) refreshQuiver();
3170
+ return result;
3171
+ },
3172
+ price: (store, obj, storeBuying, qty): number =>
3173
+ priceItem(
3174
+ reg.objects,
3175
+ store,
3176
+ store.owner,
3177
+ obj,
3178
+ storeBuying,
3179
+ qty,
3180
+ flavor.isAware(obj.kind),
3181
+ noSelling(),
3182
+ ),
3183
+ /* runesKnown matches storeSell's know (txnKnow never sets it, so the
3184
+ * transaction treats it as false); keep the filter aligned with the sale. */
3185
+ willBuy: (store, obj): boolean =>
3186
+ storeWillBuy(reg.objects, store, obj, flavor.isAware(obj.kind), noSelling(), false),
3187
+ };
3188
+ }
3189
+
3190
+ /**
3191
+ * do_randart (obj-randart.c): generate a random artifact set from `seed` and
3192
+ * install it in place of the registry's standard set. do_randart preserves the
3193
+ * artifact indices (aidx), so live references and saved-object aidx lookups
3194
+ * keep resolving; only the artifact properties change. Mutates the per-game
3195
+ * ObjRegistry (built fresh by bindCore), never a shared global.
3196
+ */
3197
+ /**
3198
+ * The player-class-and-timed dependent object-generation foils shared by every
3199
+ * live MakeDeps built after wireGame (store stock): append_object_curse's
3200
+ * TIMED_INC foil (gap 3.2), obj_kind_can_browse book rejection (gap 3.5) and
3201
+ * make_gold's birth_no_selling inflation (gap 3.7). The dungeon/loot makeDeps
3202
+ * in wireGame builds these inline from players.timed (state.world is not set
3203
+ * yet at that point). Reads the LIVE timed table so it stays correct.
3204
+ */
3205
+ function genFoilFields(
3206
+ state: GameState,
3207
+ ): Pick<MakeDeps, "timedFoil" | "canBrowseBook" | "noSelling"> {
3208
+ const keys = new Set(
3209
+ state.actor.player.cls.magic.books.map((b) => `${b.tvalIdx},${b.sval}`),
3210
+ );
3211
+ return {
3212
+ timedFoil: buildCurseTimedFoil(state.world?.timedTable ?? []),
3213
+ canBrowseBook: (kind): boolean => keys.has(`${kind.tval},${kind.sval}`),
3214
+ noSelling: state.options?.get("birth_no_selling") ?? false,
3215
+ };
3216
+ }
3217
+
3218
+ /**
3219
+ * Build the effect_summarize_properties summarizer (effects-info.c L898)
3220
+ * injected into remove_contradictory_activation (obj-randart.c L2420, gap 3.8).
3221
+ * Draws no RNG; only ever nulls a redundant activation.
3222
+ */
3223
+ function buildActivationSummarizer(
3224
+ pack: GamePack,
3225
+ reg: CoreRegistries,
3226
+ ): ActivationSummarizer {
3227
+ return makeActivationSummarizer({
3228
+ timedRecords: pack.player.timed as unknown as readonly RawTimedRecord[],
3229
+ brands: reg.objects.brands,
3230
+ slays: reg.objects.slays,
3231
+ ofIndex: (n) => (OF as Record<string, number>)[n] ?? 0,
3232
+ elemIndex: (n) => (ELEM as Record<string, number>)[n] ?? -1,
3233
+ });
3234
+ }
3235
+
3236
+ function swapRandartSet(
3237
+ reg: CoreRegistries,
3238
+ seed: number,
3239
+ timedFoil?: CurseTimedFoil,
3240
+ activationSummarize?: ActivationSummarizer,
3241
+ ): void {
3242
+ /* Thread the RANDNAME_TOLKIEN corpus (names.json section 1, loaded into
3243
+ * CoreRegistries.nameSections at boot) so artifact_gen_name draws faithful
3244
+ * Markov names via randnameMake instead of the local syllable fallback -
3245
+ * this also keeps the RNG draw count identical to upstream for the whole set
3246
+ * (obj-randart.c L2713-L2724). */
3247
+ /* artifact_curse_conflicts TIMED_INC foil (obj-randart.c L2530, gap 3.3):
3248
+ * thread the player-timed fail tables so a randart curse an item property
3249
+ * would foil is rejected / removable. The activation-redundancy summarizer
3250
+ * (gap 3.8, effect_summarize_properties) is threaded via activationSummarize
3251
+ * so remove_contradictory_activation can drop a redundant activation
3252
+ * (obj-randart.c L2420); absent, it stays a conservative no-op. */
3253
+ const randarts = doRandart(
3254
+ reg.objects,
3255
+ seed,
3256
+ reg.nameSections.get(RANDNAME_TOLKIEN),
3257
+ timedFoil || activationSummarize
3258
+ ? { timedFoil, activationSummarize }
3259
+ : undefined,
3260
+ );
3261
+ reg.objects.artifacts.length = 0;
3262
+ reg.objects.artifacts.push(...randarts);
3263
+ }
3264
+
3265
+ /** The worn-armor weight calc_mana penalizes (non-weapon/bow/jewelry slots). */
3266
+ function wornArmorWeight(
3267
+ player: Player,
3268
+ equipment: readonly (import("../obj/object.js").GameObject | null)[],
3269
+ ): number {
3270
+ let weight = 0;
3271
+ for (let i = 0; i < player.body.count; i++) {
3272
+ const slotType = player.body.slots[i]?.type ?? "";
3273
+ if (
3274
+ slotType === "WEAPON" ||
3275
+ slotType === "BOW" ||
3276
+ slotType === "RING" ||
3277
+ slotType === "AMULET" ||
3278
+ slotType === "LIGHT"
3279
+ ) {
3280
+ continue;
3281
+ }
3282
+ const worn = equipment[i];
3283
+ if (worn) weight += worn.weight;
3284
+ }
3285
+ return weight;
3286
+ }
3287
+
3288
+ /** Serialize a started game into the JSON save format (decision 9). */
3289
+ export function saveGame(game: StartedGame): SavedGame {
3290
+ const ids = new ContentIdResolver(game.booted.registries);
3291
+ const save = serializeGame(
3292
+ game.state,
3293
+ game.flavor,
3294
+ game.seedFlavor,
3295
+ ids,
3296
+ game.randartSeed,
3297
+ game.everseen,
3298
+ );
3299
+ /* The mod-lifecycle blocks (P7.2): the manifest fingerprint always travels
3300
+ * with the save; the per-mod bags and any quarantined orphans ride along only
3301
+ * when non-empty, so a core-only save stays clean. */
3302
+ save.manifest = game.manifest;
3303
+ if (Object.keys(game.mods).length > 0) save.mods = game.mods;
3304
+ if (Object.keys(game.orphans).length > 0) save.orphans = game.orphans;
3305
+ if (game.orphansAcknowledged) save.orphansAcknowledged = true;
3306
+ return save;
3307
+ }
3308
+
3309
+ /**
3310
+ * Rebuild a running game from a save: bind the pack, restore every entity
3311
+ * store and the RNG stream (decision 22: reloading resumes the exact
3312
+ * stream, the anti-save-scum posture), rewire the commands, and derive the
3313
+ * combat state from the restored player and gear.
3314
+ */
3315
+ /** Options for loadGame (client settings not stored in the savefile). */
3316
+ export interface LoadGameOptions {
3317
+ /**
3318
+ * The effective "mod rule" flags to seed GameState.modRules with, recomputed
3319
+ * by the host from the enabled mods' manifest `rules` and the player's saved
3320
+ * choices (the same resolution startGame uses). A client setting, not part of
3321
+ * the save; absent/empty = faithful 4.2.6.
3322
+ */
3323
+ modRules?: Readonly<Record<string, boolean>>;
3324
+ /**
3325
+ * The composed behaviour of every enabled mod (mod/hooks.ts). A client setting
3326
+ * like the flags above; absent = faithful 4.2.6.
3327
+ */
3328
+ modHooks?: import("../mod/hooks.js").ModHooks;
3329
+ /**
3330
+ * arg_wizard (savefile.c:631 savefile_load's cheat_death parameter): the game
3331
+ * was launched in wizard mode. Loading a DEAD character this way resurrects it
3332
+ * (savefile.c:647-651) and marks it NOSCORE_WIZARD so it stays off the score
3333
+ * table. A client/launch setting, not part of the save. The web wizard-mode
3334
+ * entry (WP-14) passes this; a normal load leaves it false.
3335
+ */
3336
+ wizard?: boolean;
3337
+ }
3338
+
3339
+ export function loadGame(
3340
+ pack: GamePack,
3341
+ saveIn: SavedGame,
3342
+ present: ReadonlySet<string> = new Set(["core"]),
3343
+ opts: LoadGameOptions = {},
3344
+ ): StartedGame {
3345
+ if (saveIn.version !== SAVE_VERSION) {
3346
+ throw new Error(`save: unsupported version ${saveIn.version}`);
3347
+ }
3348
+ const reg = bindCore(pack);
3349
+ const players = bindPlayer(pack.player);
3350
+ registerBookKinds(reg.objects, players.classes);
3351
+
3352
+ // The mod-lifecycle blocks (P7.2). The manifest fingerprint is core-only for
3353
+ // saves written before the mod substrate. Reconcile the mod set against the
3354
+ // packs present now: first rehydrate any orphans whose pack has returned, then
3355
+ // quarantine live entities whose defining pack is missing or shadowed - so the
3356
+ // deserializers below only ever see ids the present packs can resolve, instead
3357
+ // of throwing on a removed mod. `present` defaults to core-only; a future mod
3358
+ // loader passes the actually-loaded namespace set.
3359
+ const manifest = {
3360
+ ...(saveIn.manifest ?? coreOnlyManifest()),
3361
+ /* Saves written before the gameplay-mod ratchet remain scoreable until a
3362
+ * gameplay-affecting mod is actually enabled. */
3363
+ modNoscore: saveIn.manifest?.modNoscore ?? false,
3364
+ };
3365
+ const isPresent = (ns: string): boolean => present.has(ns);
3366
+ const quarantine = quarantineSave(
3367
+ rehydrateSave(saveIn, isPresent),
3368
+ manifest,
3369
+ isPresent,
3370
+ );
3371
+ const save = quarantine.save;
3372
+
3373
+ // The content-id resolver: every namespaced string id in the save resolves
3374
+ // back to a runtime index against this bound pack (mod/ids.ts). The feature
3375
+ // remap turns the save's terrain-legend indices into this pack's indices.
3376
+ const ids = new ContentIdResolver(reg);
3377
+ const featRemap = buildFeatRemap(save.featLegend, ids);
3378
+
3379
+ // Restore the option store (older saves lack it: table defaults). Do this
3380
+ // before the artifact-set swap so birth_randarts is known.
3381
+ const options = save.options
3382
+ ? OptionState.restore(save.options)
3383
+ : new OptionState();
3384
+
3385
+ // OPT(player, birth_randarts): rebuild the same random artifact set from the
3386
+ // persisted seed, so saved-object aidx references resolve to the identical
3387
+ // randarts (do_randart preserves indices). Off / seed 0: the standard set.
3388
+ const randartSeed = save.randartSeed ?? 0;
3389
+ if (options.get("birth_randarts") && randartSeed) {
3390
+ swapRandartSet(
3391
+ reg,
3392
+ randartSeed,
3393
+ buildCurseTimedFoil(players.timed),
3394
+ buildActivationSummarizer(pack, reg),
3395
+ );
3396
+ }
3397
+
3398
+ // aup_info[] (load.c): the artifact-created registry. Restore the saved
3399
+ // flags (built after swapRandartSet so aidx references align); older saves
3400
+ // predate the field and load with an all-false set (a fresh game's state).
3401
+ const artifacts =
3402
+ save.artifactsCreated || save.artifactsSeen || save.artifactsEverseen
3403
+ ? ArtifactState.restore({
3404
+ created: deserializeArtifactsCreated(
3405
+ save.artifactsCreated,
3406
+ reg.objects.artifacts.length,
3407
+ ids,
3408
+ ),
3409
+ seen: deserializeArtifactFlags(
3410
+ save.artifactsSeen,
3411
+ reg.objects.artifacts.length,
3412
+ ids,
3413
+ ),
3414
+ everseen: deserializeArtifactFlags(
3415
+ save.artifactsEverseen,
3416
+ reg.objects.artifacts.length,
3417
+ ids,
3418
+ ),
3419
+ })
3420
+ : new ArtifactState(reg.objects.artifacts.length);
3421
+
3422
+ const chunk = save.chunk
3423
+ ? deserializeChunk(save.chunk, reg.features, featRemap)
3424
+ : new Chunk(reg.features, 1, 1);
3425
+ if (!save.chunk && save.dungeonDepth !== undefined) {
3426
+ chunk.depth = save.dungeonDepth;
3427
+ }
3428
+ const player = deserializePlayer(save.player, players, reg.objects, ids);
3429
+ const gear = deserializeGear(save.gear, reg.objects, ids);
3430
+
3431
+ const equipment = player.equipment.map((h) => (h ? gearGet(gear, h) : null));
3432
+ const weaponSlot = player.body.slots.findIndex((s) => s.type === "WEAPON");
3433
+ const weapon = weaponSlot >= 0 ? (equipment[weaponSlot] ?? null) : null;
3434
+ const pstate = calcBonuses(player, {
3435
+ equipment,
3436
+ timedEffects: players.timed,
3437
+ curses: reg.objects.curses,
3438
+ update: true,
3439
+ });
3440
+ /* cumber_armor is calc_mana's to set, and a resumed character's msp is
3441
+ * restored from the save rather than recomputed - so derive the flag alone,
3442
+ * without touching msp/csp. Skipping it would make the first refreshDerived
3443
+ * of the session diff false -> true and announce that a mage's armour had
3444
+ * just become too heavy, on a character who simply loaded their save. */
3445
+ pstate.cumberArmor = cumberArmorFrom(player, wornArmorWeight(player, equipment));
3446
+ const combat = toCombatState(pstate);
3447
+
3448
+ const rng = new Rng(1);
3449
+ rng.setState(save.rng);
3450
+
3451
+ const actor: PlayerActor = {
3452
+ player,
3453
+ grid: loc(save.actor.grid.x, save.actor.grid.y),
3454
+ energy: save.actor.energy,
3455
+ speed: pstate.speed,
3456
+ totalEnergy: save.actor.totalEnergy,
3457
+ combat,
3458
+ defense: toDefenderState(pstate),
3459
+ weapon,
3460
+ stealth: combat.skills[SKILL.STEALTH] ?? 0,
3461
+ light: pstate.curLight,
3462
+ unlight: pstate.pflags.has(PF.UNLIGHT),
3463
+ };
3464
+
3465
+ /* load.c:1473-1505 marks cave->decoy while rd_traps reads the active cave. */
3466
+ let decoy: Loc | null = null;
3467
+ const loadedTraps = !save.isDead && reg.traps
3468
+ ? deserializeTraps(
3469
+ save.traps,
3470
+ reg.traps,
3471
+ chunk.width,
3472
+ ids,
3473
+ (grid) => {
3474
+ decoy = grid;
3475
+ },
3476
+ )
3477
+ : new Map();
3478
+
3479
+ const state: GameState = {
3480
+ rng,
3481
+ chunk,
3482
+ actor,
3483
+ gear,
3484
+ decoy,
3485
+ monsters: (save.isDead ? [null] : (save.monsters ?? [null])).map((m) =>
3486
+ m ? deserializeMonster(m, reg.monsters, reg.objects, ids) : null,
3487
+ ),
3488
+ groups: (save.isDead ? [null] : (save.groups ?? [null])).map((g) =>
3489
+ g ? { index: g.index, leader: g.leader, members: [...g.members] } : null,
3490
+ ),
3491
+ floor: !save.isDead && save.floor
3492
+ ? deserializeFloor(save.floor, reg.objects, chunk.width, ids)
3493
+ : new Map(),
3494
+ traps: loadedTraps,
3495
+ known: save.isDead
3496
+ ? newKnownMap(chunk.width, chunk.height)
3497
+ : deserializeKnown(save.known, chunk.width, chunk.height, featRemap),
3498
+ /* birth_levels_persist (#30) frozen-level cache; empty in saves written
3499
+ * before the field or with the option off (back-compat). */
3500
+ levelCache: deserializeLevelCache(
3501
+ save.isDead ? undefined : save.levelCache,
3502
+ reg.features,
3503
+ reg.monsters,
3504
+ reg.objects,
3505
+ reg.traps,
3506
+ ids,
3507
+ ),
3508
+ /*
3509
+ * Terrain-only Town cache (wr_chunks / load.c:1701-1704): restore even when
3510
+ * birth_levels_persist is off so town re-entry keeps seed parity.
3511
+ */
3512
+ ...(() => {
3513
+ if (save.isDead || !save.townChunk) return {};
3514
+ const townRemap = buildFeatRemap(save.townFeatLegend, ids);
3515
+ return {
3516
+ townChunk: deserializeChunk(save.townChunk, reg.features, townRemap),
3517
+ };
3518
+ })(),
3519
+ /* The target is not persisted (as upstream: the savefile carries no
3520
+ * target and loading starts unset). */
3521
+ target: newTargetState(),
3522
+ ignore: new IgnoreSettings(),
3523
+ autoinscribe: new AutoinscriptionRegistry(),
3524
+ runeNotes: new RuneNoteRegistry(),
3525
+ options,
3526
+ artifacts,
3527
+ /* Effective mod-rule flags (declarative bundled-mod seam): the host
3528
+ * recomputes these from the enabled mods + saved choices and passes them on
3529
+ * load, so they are a client setting (like the enabled-mod set), not part of
3530
+ * the savefile. Absent/empty = faithful 4.2.6. */
3531
+ ...(opts.modRules ? { modRules: { ...opts.modRules } } : {}),
3532
+ /* And the composed behaviour, recomputed by the host on load for the same
3533
+ * reason the flags are: which mods are enabled is a client setting. */
3534
+ ...(opts.modHooks ? { modHooks: opts.modHooks } : {}),
3535
+ lore: deserializeLore(save.lore, ids),
3536
+ /* Town stores + accrued daycount (rd_stores, gaps 12.1/12.2/12.3). Restored
3537
+ * from the save (never re-rolled) so the home stash and shop stock survive;
3538
+ * deserializeStores draws no RNG, so the resumed stream stays exact. Absent
3539
+ * in older saves / saves taken before reaching town: left undefined here and
3540
+ * lazily created by refreshTownStores on the next town entry (back-compat). */
3541
+ ...(save.stores && reg.stores
3542
+ ? {
3543
+ stores: deserializeStores(
3544
+ reg.stores.stores,
3545
+ save.stores,
3546
+ reg.objects,
3547
+ ids,
3548
+ reg.constants.storeInvenMax,
3549
+ ),
3550
+ }
3551
+ : {}),
3552
+ ...(save.daycount ? { daycount: save.daycount } : {}),
3553
+ /* Player-side transient scalars (save.c: wr_player). Absent in older saves
3554
+ * / falsy defaults load as unset, matching a fresh character. */
3555
+ ...(save.restingTurn ? { restingTurn: save.restingTurn } : {}),
3556
+ ...(save.skipCmdCoercion ? { skipCmdCoercion: save.skipCmdCoercion } : {}),
3557
+ ...(save.unignoring ? { unignoring: save.unignoring } : {}),
3558
+ ...(save.nameSuffix ? { nameSuffix: save.nameSuffix } : {}),
3559
+ /* Running message log (rd_messages, load.c:471-495, gap 12.8): re-add each
3560
+ * saved entry oldest-first; older saves without the field load empty. */
3561
+ messages: deserializeMessages(save.messages),
3562
+ /* chunk->join of the level in play (gap 9.4/9.6): restore the stair
3563
+ * connectors so a first-visit persistent level still aligns after reload. */
3564
+ ...(!save.isDead && save.currentJoins
3565
+ ? {
3566
+ currentJoins: save.currentJoins.map((j) => ({
3567
+ grid: loc(j.x, j.y),
3568
+ feat: featRemap.get(j.feat) ?? j.feat,
3569
+ ...(j.info ? { info: [...j.info] } : {}),
3570
+ })),
3571
+ }
3572
+ : {}),
3573
+ turn: save.turn,
3574
+ z: {
3575
+ ...DEFAULT_GAME_CONSTANTS,
3576
+ maxSight: reg.constants.maxSight,
3577
+ maxRange: reg.constants.maxRange,
3578
+ floorSize: reg.constants.floorSize,
3579
+ maxDepth: reg.constants.maxDepth,
3580
+ stairSkip: reg.constants.stairSkip,
3581
+ dayLength: reg.constants.dayLength,
3582
+ foodValue: reg.constants.foodValue,
3583
+ allocMonsterChance: reg.constants.allocMonsterChance,
3584
+ storeTurns: reg.constants.storeTurns,
3585
+ lifeDrainPercent: reg.constants.lifeDrainPercent,
3586
+ levelMonsterMax: reg.constants.levelMonsterMax,
3587
+ },
3588
+ brands: reg.objects.brands,
3589
+ slays: reg.objects.slays,
3590
+ /* Placeholder; wireGame installs the full registry-backed env. */
3591
+ runeEnv: makeRuneEnv(
3592
+ () => null,
3593
+ () => false,
3594
+ ),
3595
+ playing: save.playing,
3596
+ isDead: save.isDead,
3597
+ generateLevel: false,
3598
+ nextCommand: (): PlayerCommand | null => null,
3599
+ };
3600
+
3601
+ /* Rebuild the racial counts from the restored monster list (the save
3602
+ * carries the monsters, not the registry-side counters). */
3603
+ countMonsterRaces(state);
3604
+
3605
+ /* SV-01 (load.c:532-535 rd_monster_memory, "ensure dead uniques stay dead";
3606
+ * mon-make.c:257 refuses a unique whose max_num is exhausted):
3607
+ * a fresh registry starts every UNIQUE at maxNum=1, and countMonsterRaces
3608
+ * above only rebuilds curNum from the live monster list - so a unique the
3609
+ * player already killed (restored lore pkills>0, but no live copy) would bind
3610
+ * back at maxNum=1 and could respawn / be re-killed after a reload. Re-derive
3611
+ * maxNum=0 for those uniques, exactly as upstream does on load. */
3612
+ for (const race of reg.monsters.races) {
3613
+ if (!race) continue;
3614
+ if (
3615
+ race.flags.has(RF.UNIQUE) &&
3616
+ (state.lore.get(race.ridx)?.pkills ?? 0) > 0
3617
+ ) {
3618
+ race.maxNum = 0;
3619
+ }
3620
+ }
3621
+
3622
+ /* savefile.c:647-651: loading a DEAD character with the wizard/cheat-death
3623
+ * launch flag set resurrects it (is_dead cleared, HP refilled) and flags it
3624
+ * NOSCORE_WIZARD, keeping it off the high-score table (gap 15.3). A normal
3625
+ * load (wizard off) leaves a dead character dead. */
3626
+ if (save.isDead && opts.wizard) {
3627
+ player.chp = player.mhp;
3628
+ player.chpFrac = 0;
3629
+ player.noscore = markNoscore(player.noscore, NOSCORE.WIZARD);
3630
+ state.isDead = false;
3631
+ }
3632
+
3633
+ /* A save taken in single combat resumes it (the stashed pre-arena
3634
+ * level is gone: winning exits to a fresh level of the same depth). */
3635
+ if (save.arena) {
3636
+ state.arenaLevel = true;
3637
+ state.oldGrid = loc(save.arena.oldGrid.x, save.arena.oldGrid.y);
3638
+ }
3639
+
3640
+ /* seed_flavor from the save (load.c L960). Older saves predate it; fall
3641
+ * back to 0 so flavor_init still produces a stable per-load assignment. */
3642
+ const seedFlavor = save.seedFlavor ?? 0;
3643
+ const wired = wireGame(state, reg, players, pstate, seedFlavor, pack);
3644
+ /* rd_gear's tail (load.c:1187). */
3645
+ buildGearViews(state, reg, pstate.ammoTval);
3646
+ /* restore() replaces the aware/tried sets, so it must run AFTER flavor_init's
3647
+ * aware-marking of non-flavoured kinds - the save is the source of truth for
3648
+ * what the player has actually identified. */
3649
+ wired.flavor.restore(deserializeFlavor(save.flavor, ids));
3650
+ /* kind/ego everseen (save.c L397/L533): absent in saves written before
3651
+ * everseen tracking, which load with an empty set. */
3652
+ if (save.everseen) wired.everseen.restore(deserializeEverseen(save.everseen, ids));
3653
+ if (save.ignore) state.ignore.restore(deserializeIgnore(save.ignore, ids));
3654
+ /* Per-kind autoinscriptions (obj-ignore.c note_aware/note_unaware): absent in
3655
+ * saves written before this block, which load with an empty registry. */
3656
+ if (save.autoinscriptions && state.autoinscribe) {
3657
+ deserializeAutoinscriptions(save.autoinscriptions, state.autoinscribe, ids);
3658
+ }
3659
+ /* Per-RUNE autoinscriptions (rd_ignore's rune block, load.c:934-945):
3660
+ * rd_s16b(runeid) + rd_string, straight into rune_set_note. The savefile keys
3661
+ * by runeKey, so resolve each back to its live buildRuneList index; a key the
3662
+ * running pack no longer builds is dropped. Absent in saves written before
3663
+ * this block, which load with no rune notes. */
3664
+ if (save.runeNotes && state.runeNotes) {
3665
+ const runes = buildRuneList(state.runeEnv);
3666
+ const byKey = new Map<string, number>();
3667
+ runes.forEach((rune, i) => byKey.set(runeKey(rune), i));
3668
+ for (const [key, note] of save.runeNotes) {
3669
+ const i = byKey.get(key);
3670
+ if (i !== undefined) state.runeNotes.set(i, note);
3671
+ }
3672
+ }
3673
+
3674
+ // A renderer-facing view of the restored level (no generation ran).
3675
+ const booted: BootedLevel = {
3676
+ chunk,
3677
+ depth: chunk.depth,
3678
+ playerSpot: actor.grid,
3679
+ monsters: [],
3680
+ objects: [],
3681
+ trapGrids: [],
3682
+ traps: [],
3683
+ lockedDoors: [],
3684
+ rng,
3685
+ registries: reg,
3686
+ };
3687
+
3688
+ /* Resuming in town: re-stock the shops (store stock is not persisted). */
3689
+ refreshTownStores(state, reg);
3690
+
3691
+ return {
3692
+ state,
3693
+ registry: wired.registry,
3694
+ effects: wired.effects,
3695
+ booted,
3696
+ players,
3697
+ flavor: wired.flavor,
3698
+ everseen: wired.everseen,
3699
+ seedFlavor,
3700
+ manifest,
3701
+ mods: save.mods ?? {},
3702
+ orphans: quarantine.orphans,
3703
+ orphansAcknowledged: save.orphansAcknowledged ?? false,
3704
+ options,
3705
+ randartSeed,
3706
+ changeLevel: makeChangeLevel(state, reg, wired.trapDeps, {
3707
+ inArena: !!save.arena,
3708
+ }),
3709
+ wizardBundles: wired.wizardBundles,
3710
+ ...makeStoreApi(state, reg, wired.flavor, options),
3711
+ };
3712
+ }