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