@rpgm-tools/neo-angband-core 0.19.0 → 0.20.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 (417) hide show
  1. package/README.md +1 -1
  2. package/dist/cmd.d.ts +44 -2
  3. package/dist/cmd.d.ts.map +1 -1
  4. package/dist/cmd.js +59 -3
  5. package/dist/cmd.js.map +1 -1
  6. package/dist/game/context.d.ts +31 -0
  7. package/dist/game/context.d.ts.map +1 -1
  8. package/dist/game/context.js.map +1 -1
  9. package/dist/game/display.d.ts +26 -0
  10. package/dist/game/display.d.ts.map +1 -1
  11. package/dist/game/display.js +116 -22
  12. package/dist/game/display.js.map +1 -1
  13. package/dist/game/dump-level.d.ts +1 -1
  14. package/dist/game/dump-level.js +1 -1
  15. package/dist/game/effect-attack.d.ts.map +1 -1
  16. package/dist/game/effect-attack.js +1 -3
  17. package/dist/game/effect-attack.js.map +1 -1
  18. package/dist/game/effect-general.d.ts.map +1 -1
  19. package/dist/game/effect-general.js +12 -11
  20. package/dist/game/effect-general.js.map +1 -1
  21. package/dist/game/effect-item.d.ts.map +1 -1
  22. package/dist/game/effect-item.js +10 -4
  23. package/dist/game/effect-item.js.map +1 -1
  24. package/dist/game/effect-teleport.d.ts.map +1 -1
  25. package/dist/game/effect-teleport.js +21 -12
  26. package/dist/game/effect-teleport.js.map +1 -1
  27. package/dist/game/effect-terrain.d.ts +1 -1
  28. package/dist/game/effect-terrain.js +1 -1
  29. package/dist/game/equip-cmp.d.ts +17 -6
  30. package/dist/game/equip-cmp.d.ts.map +1 -1
  31. package/dist/game/equip-cmp.js +43 -24
  32. package/dist/game/equip-cmp.js.map +1 -1
  33. package/dist/game/floor.d.ts +32 -0
  34. package/dist/game/floor.d.ts.map +1 -1
  35. package/dist/game/floor.js +42 -0
  36. package/dist/game/floor.js.map +1 -1
  37. package/dist/game/gear.d.ts +17 -3
  38. package/dist/game/gear.d.ts.map +1 -1
  39. package/dist/game/gear.js +2 -2
  40. package/dist/game/gear.js.map +1 -1
  41. package/dist/game/inscription-confirm.d.ts +13 -6
  42. package/dist/game/inscription-confirm.d.ts.map +1 -1
  43. package/dist/game/inscription-confirm.js +13 -6
  44. package/dist/game/inscription-confirm.js.map +1 -1
  45. package/dist/game/known.d.ts +31 -6
  46. package/dist/game/known.d.ts.map +1 -1
  47. package/dist/game/known.js +81 -13
  48. package/dist/game/known.js.map +1 -1
  49. package/dist/game/mon-cast.d.ts.map +1 -1
  50. package/dist/game/mon-cast.js +9 -5
  51. package/dist/game/mon-cast.js.map +1 -1
  52. package/dist/game/mon-cmd.d.ts.map +1 -1
  53. package/dist/game/mon-cmd.js +4 -7
  54. package/dist/game/mon-cmd.js.map +1 -1
  55. package/dist/game/mon-message.d.ts.map +1 -1
  56. package/dist/game/mon-message.js +0 -1
  57. package/dist/game/mon-message.js.map +1 -1
  58. package/dist/game/mon-side.d.ts.map +1 -1
  59. package/dist/game/mon-side.js +4 -7
  60. package/dist/game/mon-side.js.map +1 -1
  61. package/dist/game/monster-turn.d.ts +4 -1
  62. package/dist/game/monster-turn.d.ts.map +1 -1
  63. package/dist/game/monster-turn.js +8 -4
  64. package/dist/game/monster-turn.js.map +1 -1
  65. package/dist/game/obj-cmd.d.ts.map +1 -1
  66. package/dist/game/obj-cmd.js +21 -3
  67. package/dist/game/obj-cmd.js.map +1 -1
  68. package/dist/game/player-turn.d.ts +2 -1
  69. package/dist/game/player-turn.d.ts.map +1 -1
  70. package/dist/game/player-turn.js +3 -3
  71. package/dist/game/player-turn.js.map +1 -1
  72. package/dist/game/project-cast.d.ts.map +1 -1
  73. package/dist/game/project-cast.js +9 -0
  74. package/dist/game/project-cast.js.map +1 -1
  75. package/dist/game/project-monster.d.ts +10 -0
  76. package/dist/game/project-monster.d.ts.map +1 -1
  77. package/dist/game/project-monster.js +11 -2
  78. package/dist/game/project-monster.js.map +1 -1
  79. package/dist/game/projection-handlers.d.ts +9 -1
  80. package/dist/game/projection-handlers.d.ts.map +1 -1
  81. package/dist/game/projection-handlers.js +9 -1
  82. package/dist/game/projection-handlers.js.map +1 -1
  83. package/dist/game/ranged-cmd.d.ts.map +1 -1
  84. package/dist/game/ranged-cmd.js +6 -2
  85. package/dist/game/ranged-cmd.js.map +1 -1
  86. package/dist/game/repeat.d.ts +38 -1
  87. package/dist/game/repeat.d.ts.map +1 -1
  88. package/dist/game/repeat.js +24 -1
  89. package/dist/game/repeat.js.map +1 -1
  90. package/dist/game/shape-inspect.d.ts +2 -2
  91. package/dist/game/shape-inspect.js +3 -3
  92. package/dist/game/spoil.js +1 -1
  93. package/dist/game/take-hit-hooks.d.ts.map +1 -1
  94. package/dist/game/take-hit-hooks.js +4 -12
  95. package/dist/game/take-hit-hooks.js.map +1 -1
  96. package/dist/game/target-loop.d.ts +13 -8
  97. package/dist/game/target-loop.d.ts.map +1 -1
  98. package/dist/game/target-loop.js +43 -14
  99. package/dist/game/target-loop.js.map +1 -1
  100. package/dist/game/ui-entry-registry.d.ts +124 -0
  101. package/dist/game/ui-entry-registry.d.ts.map +1 -0
  102. package/dist/game/ui-entry-registry.js +102 -0
  103. package/dist/game/ui-entry-registry.js.map +1 -0
  104. package/dist/game/ui-entry.d.ts +90 -20
  105. package/dist/game/ui-entry.d.ts.map +1 -1
  106. package/dist/game/ui-entry.js +510 -315
  107. package/dist/game/ui-entry.js.map +1 -1
  108. package/dist/game/wizard.d.ts +27 -23
  109. package/dist/game/wizard.d.ts.map +1 -1
  110. package/dist/game/wizard.js +38 -34
  111. package/dist/game/wizard.js.map +1 -1
  112. package/dist/gen/gen-monster.d.ts +4 -2
  113. package/dist/gen/gen-monster.d.ts.map +1 -1
  114. package/dist/gen/gen-monster.js +11 -8
  115. package/dist/gen/gen-monster.js.map +1 -1
  116. package/dist/gen/util.d.ts +1 -1
  117. package/dist/gen/util.d.ts.map +1 -1
  118. package/dist/index.d.ts +1 -0
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +4 -0
  121. package/dist/index.js.map +1 -1
  122. package/dist/mod/extension.d.ts +20 -0
  123. package/dist/mod/extension.d.ts.map +1 -1
  124. package/dist/mod/extension.js +12 -2
  125. package/dist/mod/extension.js.map +1 -1
  126. package/dist/mod/ids.d.ts +12 -0
  127. package/dist/mod/ids.d.ts.map +1 -1
  128. package/dist/mod/ids.js +119 -14
  129. package/dist/mod/ids.js.map +1 -1
  130. package/dist/mod/message-declarations.d.ts +132 -0
  131. package/dist/mod/message-declarations.d.ts.map +1 -0
  132. package/dist/mod/message-declarations.js +223 -0
  133. package/dist/mod/message-declarations.js.map +1 -0
  134. package/dist/mod/record-keys.d.ts +4 -3
  135. package/dist/mod/record-keys.d.ts.map +1 -1
  136. package/dist/mod/record-keys.js +4 -3
  137. package/dist/mod/record-keys.js.map +1 -1
  138. package/dist/mod/registry-host.d.ts +167 -1
  139. package/dist/mod/registry-host.d.ts.map +1 -1
  140. package/dist/mod/registry-host.js +95 -1
  141. package/dist/mod/registry-host.js.map +1 -1
  142. package/dist/mod/save-blocks.d.ts +8 -3
  143. package/dist/mod/save-blocks.d.ts.map +1 -1
  144. package/dist/mod/save-blocks.js +153 -14
  145. package/dist/mod/save-blocks.js.map +1 -1
  146. package/dist/mon/bind.d.ts +5 -0
  147. package/dist/mon/bind.d.ts.map +1 -1
  148. package/dist/mon/bind.js +12 -10
  149. package/dist/mon/bind.js.map +1 -1
  150. package/dist/mon/knowledge-groups.js +1 -1
  151. package/dist/mon/lore-describe.d.ts.map +1 -1
  152. package/dist/mon/lore-describe.js +3 -2
  153. package/dist/mon/lore-describe.js.map +1 -1
  154. package/dist/mon/lore-file.d.ts +4 -4
  155. package/dist/mon/lore-file.d.ts.map +1 -1
  156. package/dist/mon/lore-file.js +22 -12
  157. package/dist/mon/lore-file.js.map +1 -1
  158. package/dist/mon/lore.d.ts +6 -0
  159. package/dist/mon/lore.d.ts.map +1 -1
  160. package/dist/mon/lore.js +10 -3
  161. package/dist/mon/lore.js.map +1 -1
  162. package/dist/mon/predicate.d.ts.map +1 -1
  163. package/dist/mon/predicate.js +3 -2
  164. package/dist/mon/predicate.js.map +1 -1
  165. package/dist/mon/project-mon.d.ts +48 -4
  166. package/dist/mon/project-mon.d.ts.map +1 -1
  167. package/dist/mon/project-mon.js +47 -4
  168. package/dist/mon/project-mon.js.map +1 -1
  169. package/dist/mon/spell-declarations.d.ts +44 -0
  170. package/dist/mon/spell-declarations.d.ts.map +1 -0
  171. package/dist/mon/spell-declarations.js +57 -0
  172. package/dist/mon/spell-declarations.js.map +1 -0
  173. package/dist/mon/spell-registry.d.ts +136 -0
  174. package/dist/mon/spell-registry.d.ts.map +1 -0
  175. package/dist/mon/spell-registry.js +178 -0
  176. package/dist/mon/spell-registry.js.map +1 -0
  177. package/dist/mon/spell.js +2 -2
  178. package/dist/mon/spell.js.map +1 -1
  179. package/dist/mon/types.d.ts +12 -1
  180. package/dist/mon/types.d.ts.map +1 -1
  181. package/dist/mon/types.js +18 -1
  182. package/dist/mon/types.js.map +1 -1
  183. package/dist/msg.d.ts +26 -0
  184. package/dist/msg.d.ts.map +1 -1
  185. package/dist/msg.js +28 -1
  186. package/dist/msg.js.map +1 -1
  187. package/dist/obj/artifact-fake.d.ts +1 -1
  188. package/dist/obj/artifact-fake.js +1 -1
  189. package/dist/obj/artifact-known.d.ts +4 -4
  190. package/dist/obj/artifact-known.js +3 -3
  191. package/dist/obj/bind.d.ts +2 -2
  192. package/dist/obj/bind.d.ts.map +1 -1
  193. package/dist/obj/bind.js +5 -4
  194. package/dist/obj/bind.js.map +1 -1
  195. package/dist/obj/desc.d.ts +1 -1
  196. package/dist/obj/desc.js +1 -1
  197. package/dist/obj/fake-object.d.ts +2 -2
  198. package/dist/obj/fake-object.js +2 -2
  199. package/dist/obj/knowledge.d.ts +2 -2
  200. package/dist/obj/knowledge.js +2 -2
  201. package/dist/obj/make.d.ts +1 -1
  202. package/dist/obj/make.d.ts.map +1 -1
  203. package/dist/obj/make.js +3 -3
  204. package/dist/obj/make.js.map +1 -1
  205. package/dist/obj/object-info.d.ts +1 -1
  206. package/dist/obj/object-info.js +2 -2
  207. package/dist/obj/object-info.js.map +1 -1
  208. package/dist/obj/object.d.ts +12 -5
  209. package/dist/obj/object.d.ts.map +1 -1
  210. package/dist/obj/object.js +15 -6
  211. package/dist/obj/object.js.map +1 -1
  212. package/dist/obj/power.d.ts +2 -2
  213. package/dist/obj/power.js +4 -4
  214. package/dist/obj/power.js.map +1 -1
  215. package/dist/obj/randart-build.d.ts.map +1 -1
  216. package/dist/obj/randart-build.js +22 -46
  217. package/dist/obj/randart-build.js.map +1 -1
  218. package/dist/obj/randart-data.d.ts.map +1 -1
  219. package/dist/obj/randart-data.js +16 -33
  220. package/dist/obj/randart-data.js.map +1 -1
  221. package/dist/obj/types.d.ts +25 -0
  222. package/dist/obj/types.d.ts.map +1 -1
  223. package/dist/parser.d.ts +26 -46
  224. package/dist/parser.d.ts.map +1 -1
  225. package/dist/parser.js +26 -115
  226. package/dist/parser.js.map +1 -1
  227. package/dist/player/birth.d.ts +1 -1
  228. package/dist/player/birth.js +2 -2
  229. package/dist/player/options-file.d.ts +55 -29
  230. package/dist/player/options-file.d.ts.map +1 -1
  231. package/dist/player/options-file.js +125 -139
  232. package/dist/player/options-file.js.map +1 -1
  233. package/dist/player/options.d.ts +1 -1
  234. package/dist/player/options.js +1 -1
  235. package/dist/player/shape-lore.d.ts +1 -1
  236. package/dist/player/shape-lore.d.ts.map +1 -1
  237. package/dist/player/shape-lore.js +16 -13
  238. package/dist/player/shape-lore.js.map +1 -1
  239. package/dist/player/spell.d.ts.map +1 -1
  240. package/dist/player/spell.js +24 -2
  241. package/dist/player/spell.js.map +1 -1
  242. package/dist/score/display.d.ts.map +1 -1
  243. package/dist/score/display.js +39 -18
  244. package/dist/score/display.js.map +1 -1
  245. package/dist/score/score.d.ts +7 -7
  246. package/dist/score/score.js +3 -3
  247. package/dist/score/types.d.ts +51 -0
  248. package/dist/score/types.d.ts.map +1 -1
  249. package/dist/score/types.js.map +1 -1
  250. package/dist/session/boot.d.ts +2 -0
  251. package/dist/session/boot.d.ts.map +1 -1
  252. package/dist/session/boot.js +22 -0
  253. package/dist/session/boot.js.map +1 -1
  254. package/dist/session/game.d.ts.map +1 -1
  255. package/dist/session/game.js +113 -6
  256. package/dist/session/game.js.map +1 -1
  257. package/dist/session/save-migrate.d.ts +3 -3
  258. package/dist/session/save-migrate.d.ts.map +1 -1
  259. package/dist/session/save-migrate.js +429 -21
  260. package/dist/session/save-migrate.js.map +1 -1
  261. package/dist/session/save.d.ts +442 -28
  262. package/dist/session/save.d.ts.map +1 -1
  263. package/dist/session/save.js +691 -51
  264. package/dist/session/save.js.map +1 -1
  265. package/dist/sound/engine.d.ts +17 -0
  266. package/dist/sound/engine.d.ts.map +1 -1
  267. package/dist/sound/engine.js +55 -6
  268. package/dist/sound/engine.js.map +1 -1
  269. package/dist/sound/index.d.ts +2 -0
  270. package/dist/sound/index.d.ts.map +1 -1
  271. package/dist/sound/index.js +2 -0
  272. package/dist/sound/index.js.map +1 -1
  273. package/dist/sound/message-types.d.ts +89 -0
  274. package/dist/sound/message-types.d.ts.map +1 -0
  275. package/dist/sound/message-types.js +126 -0
  276. package/dist/sound/message-types.js.map +1 -0
  277. package/dist/sound/sound-registry.d.ts +93 -0
  278. package/dist/sound/sound-registry.d.ts.map +1 -0
  279. package/dist/sound/sound-registry.js +117 -0
  280. package/dist/sound/sound-registry.js.map +1 -0
  281. package/dist/store/store.d.ts +12 -11
  282. package/dist/store/store.d.ts.map +1 -1
  283. package/dist/store/store.js +16 -16
  284. package/dist/store/store.js.map +1 -1
  285. package/dist/store/transact.d.ts +25 -7
  286. package/dist/store/transact.d.ts.map +1 -1
  287. package/dist/store/transact.js +27 -9
  288. package/dist/store/transact.js.map +1 -1
  289. package/dist/version.d.ts +1 -1
  290. package/dist/version.js +1 -1
  291. package/dist/visuals/glyph-table.d.ts +2 -2
  292. package/dist/visuals/glyph-table.js +2 -2
  293. package/dist/visuals/hallucination.d.ts +140 -0
  294. package/dist/visuals/hallucination.d.ts.map +1 -0
  295. package/dist/visuals/hallucination.js +122 -0
  296. package/dist/visuals/hallucination.js.map +1 -0
  297. package/dist/visuals/index.d.ts +1 -0
  298. package/dist/visuals/index.d.ts.map +1 -1
  299. package/dist/visuals/index.js +1 -0
  300. package/dist/visuals/index.js.map +1 -1
  301. package/dist/visuals/map-text.d.ts +5 -4
  302. package/dist/visuals/map-text.d.ts.map +1 -1
  303. package/dist/visuals/map-text.js +5 -4
  304. package/dist/visuals/map-text.js.map +1 -1
  305. package/dist/visuals/object-glyph.d.ts +5 -0
  306. package/dist/visuals/object-glyph.d.ts.map +1 -1
  307. package/dist/visuals/object-glyph.js +7 -2
  308. package/dist/visuals/object-glyph.js.map +1 -1
  309. package/dist/visuals/prefs.d.ts +172 -17
  310. package/dist/visuals/prefs.d.ts.map +1 -1
  311. package/dist/visuals/prefs.js +221 -25
  312. package/dist/visuals/prefs.js.map +1 -1
  313. package/dist/world/trap.d.ts +1 -1
  314. package/package.json +1 -1
  315. package/src/cmd.ts +67 -3
  316. package/src/effects/effect-info.ts +1005 -1005
  317. package/src/game/context.ts +31 -0
  318. package/src/game/display.ts +144 -22
  319. package/src/game/dump-level.ts +1 -1
  320. package/src/game/effect-attack.ts +1 -2
  321. package/src/game/effect-general.ts +12 -11
  322. package/src/game/effect-item.ts +1080 -1075
  323. package/src/game/effect-teleport.ts +21 -12
  324. package/src/game/effect-terrain.ts +1 -1
  325. package/src/game/equip-cmp.ts +62 -32
  326. package/src/game/floor.ts +52 -0
  327. package/src/game/gear.ts +18 -4
  328. package/src/game/inscription-confirm.ts +15 -7
  329. package/src/game/known.ts +83 -12
  330. package/src/game/loop.ts +695 -695
  331. package/src/game/mon-cast.ts +8 -6
  332. package/src/game/mon-cmd.ts +4 -7
  333. package/src/game/mon-message.ts +0 -1
  334. package/src/game/mon-side.ts +4 -6
  335. package/src/game/monster-turn.ts +8 -4
  336. package/src/game/obj-cmd.ts +19 -2
  337. package/src/game/player-turn.ts +2 -3
  338. package/src/game/project-cast.ts +9 -0
  339. package/src/game/project-monster.ts +24 -3
  340. package/src/game/projection-handlers.ts +12 -1
  341. package/src/game/ranged-cmd.ts +5 -3
  342. package/src/game/repeat.ts +56 -1
  343. package/src/game/shape-inspect.ts +3 -3
  344. package/src/game/spoil.ts +1 -1
  345. package/src/game/take-hit-hooks.ts +4 -11
  346. package/src/game/target-loop.ts +44 -12
  347. package/src/game/ui-entry-registry.ts +159 -0
  348. package/src/game/ui-entry.ts +2528 -2222
  349. package/src/game/wizard.ts +39 -35
  350. package/src/gen/cave.ts +3057 -3057
  351. package/src/gen/gen-monster.ts +11 -8
  352. package/src/gen/generate.ts +576 -576
  353. package/src/gen/util.ts +1 -1
  354. package/src/index.ts +311 -307
  355. package/src/mod/extension.ts +37 -2
  356. package/src/mod/hooks.ts +521 -521
  357. package/src/mod/ids.ts +160 -13
  358. package/src/mod/message-declarations.ts +293 -0
  359. package/src/mod/record-keys.ts +4 -3
  360. package/src/mod/registry-host.ts +1274 -1009
  361. package/src/mod/save-blocks.ts +162 -15
  362. package/src/mon/bind.ts +14 -10
  363. package/src/mon/knowledge-groups.ts +1 -1
  364. package/src/mon/lore-describe.ts +3 -2
  365. package/src/mon/lore-file.ts +22 -12
  366. package/src/mon/lore.ts +10 -3
  367. package/src/mon/predicate.ts +3 -2
  368. package/src/mon/project-mon.ts +65 -4
  369. package/src/mon/spell-declarations.ts +80 -0
  370. package/src/mon/spell-registry.ts +221 -0
  371. package/src/mon/spell.ts +2 -2
  372. package/src/mon/types.ts +27 -1
  373. package/src/msg.ts +29 -1
  374. package/src/obj/artifact-fake.ts +1 -1
  375. package/src/obj/artifact-known.ts +4 -4
  376. package/src/obj/bind.ts +4 -4
  377. package/src/obj/desc.ts +1 -1
  378. package/src/obj/fake-object.ts +2 -2
  379. package/src/obj/knowledge.ts +2 -2
  380. package/src/obj/make.ts +3 -2
  381. package/src/obj/object-info.ts +2 -2
  382. package/src/obj/object.ts +15 -6
  383. package/src/obj/power.ts +4 -4
  384. package/src/obj/randart-build.ts +1847 -1875
  385. package/src/obj/randart-data.ts +1519 -1547
  386. package/src/obj/randart-registry.ts +247 -247
  387. package/src/obj/randart-vectors.ts +369 -369
  388. package/src/obj/randart.ts +722 -722
  389. package/src/obj/types.ts +22 -0
  390. package/src/parser.ts +26 -115
  391. package/src/player/birth.ts +2 -2
  392. package/src/player/options-file.ts +128 -164
  393. package/src/player/options.ts +1 -1
  394. package/src/player/shape-lore.ts +16 -13
  395. package/src/player/spell.ts +31 -2
  396. package/src/score/display.ts +38 -17
  397. package/src/score/score.ts +8 -8
  398. package/src/score/types.ts +52 -0
  399. package/src/session/boot.ts +50 -0
  400. package/src/session/game.ts +117 -6
  401. package/src/session/save-migrate.ts +517 -22
  402. package/src/session/save.ts +917 -74
  403. package/src/sound/engine.ts +59 -4
  404. package/src/sound/index.ts +2 -0
  405. package/src/sound/message-types.ts +164 -0
  406. package/src/sound/sound-registry.ts +149 -0
  407. package/src/store/store.ts +21 -19
  408. package/src/store/transact.ts +30 -12
  409. package/src/version.ts +1 -1
  410. package/src/visuals/glyph-table.ts +2 -2
  411. package/src/visuals/hallucination.ts +200 -0
  412. package/src/visuals/index.ts +1 -0
  413. package/src/visuals/map-text.ts +5 -4
  414. package/src/visuals/object-glyph.ts +7 -2
  415. package/src/visuals/prefs.ts +1236 -971
  416. package/src/visuals/tile-prefs.ts +215 -215
  417. package/src/world/trap.ts +1 -1
@@ -38,9 +38,12 @@
38
38
  * ported - mon/lore-file.ts - and is laid
39
39
  * over this on load, exactly as upstream's
40
40
  * startup order does. The save keeps carrying
41
- * the whole record because narrowing it is a
42
- * SAVE_VERSION change, and a bump reads as
43
- * corruption to an existing character.)
41
+ * the whole record; narrowing it would be a
42
+ * SAVE_VERSION change with nothing to gain.
43
+ * The observed SPELL set is written by NAME
44
+ * as of version 5 (SavedLore.spellsKnown) and
45
+ * the known race FLAGS as of version 6
46
+ * (SavedLore.flagsKnown).)
44
47
  * wr_object_memory (save.c:377) -> flavor.aware / .tried, everseen.kinds,
45
48
  * ignore.kindAware / .kindUnaware
46
49
  * wr_quests (save.c:405) -> player.quests
@@ -77,15 +80,22 @@ import { loc } from "../loc.js";
77
80
  import { FlagSet } from "../bitflag.js";
78
81
  import { Chunk, SQUARE_SIZE } from "../world/chunk.js";
79
82
  import { objectNew, tvalIsMoney } from "../obj/object.js";
83
+ import { ELEMENT_NAMES, OBJ_MOD_NAMES } from "../obj/bind.js";
84
+ import { ELEM_MAX, OBJ_MOD_MAX, OF_SIZE, newElemInfo } from "../obj/types.js";
80
85
  import { buildRuneList, runeKey } from "../obj/knowledge.js";
81
86
  import { blankMonster, GROUP_MAX } from "../mon/monster.js";
87
+ import { RF_FLAG_NAMES } from "../mon/lore-file.js";
88
+ import { MFLAG, MON_TMD, OF, SQUARE_FLAG_ENTRIES, STAT, TMD, TRF, } from "../generated/index.js";
89
+ import { MFLAG_SIZE, RF_SIZE } from "../mon/types.js";
90
+ import { rsfMax, rsfSize, spellIndexOf, spellNameAt, } from "../mon/spell-registry.js";
82
91
  import { blankPlayer } from "../player/player.js";
83
92
  import { playerQuestsReset } from "../game/quest.js";
93
+ import { TRF_SIZE } from "../world/trap.js";
84
94
  import { MessageLog } from "../msg.js";
85
95
  import { newKnownMap } from "../game/known.js";
86
96
  import { fnv1aIntegrity, stampSavefile, verifyStampedSavefile, } from "../save/integrity.js";
87
97
  import { applyCodec, findCodec, stripCodec } from "../save/compress.js";
88
- import { PY_MAX_LEVEL, TMD_MAX } from "../player/types.js";
98
+ import { PY_MAX_LEVEL, SKILL, SKILL_MAX, STAT_MAX, TMD_MAX, } from "../player/types.js";
89
99
  /**
90
100
  * The save format version this build writes. Version 2 replaced every numeric
91
101
  * content index (kidx/eidx/aidx/ridx/tidx/feat, and the positional curse/
@@ -93,7 +103,38 @@ import { PY_MAX_LEVEL, TMD_MAX } from "../player/types.js";
93
103
  * load-bearing rule of the mod substrate (MOD_LIFECYCLE decision 1). Version 3
94
104
  * finished that job: `flavor`, `everseen` and `ignore` were the last blocks
95
105
  * still keyed by raw kidx/eidx, and the rune-autoinscription block of wr_ignore
96
- * (save.c:586-605) arrived with them.
106
+ * (save.c:586-605) arrived with them. Version 5 started on the POSITIONS a save
107
+ * held: monster lore recorded which spells the player had seen as RSF bit
108
+ * positions, so `MON_SPELL_ENTRIES` could never grow without renumbering an
109
+ * existing character's memory. It is written by name now
110
+ * (`SavedLore.spellsKnown`).
111
+ *
112
+ * Version 5's own comment claimed that was the LAST position a save held. It was
113
+ * not, and version 6 (#273) finished the count: race lore still wrote RF bit
114
+ * positions, and every object-property carrier in the document - `SavedObject`,
115
+ * `SavedPlayer.objKnown`, `SavedMonster.knownPstate` - wrote OF bit positions,
116
+ * OBJ_MOD indices and ELEM indices. All four tables are persisted by NAME now;
117
+ * see "Position-free persistence" below.
118
+ *
119
+ * VERSION 6'S COMMENT MADE THE SAME CLAIM, and version 7 (#274) is the count
120
+ * actually finishing. Seven more tables were still persisted by position, and
121
+ * V5_TO_V6's own discriminator comment named two of them as "out of scope":
122
+ *
123
+ * MFLAG `SavedMonster.mflag` -> `mflagNames` (bit positions)
124
+ * TRF `SavedTrap.flags` -> `trapFlagNames` (bit positions)
125
+ * SQUARE every chunk's per-grid info -> `squareInfoLegend` (bit positions)
126
+ * MON_TMD `SavedMonster.mTimed` -> `monsterTimed` (dense indices)
127
+ * TMD `SavedPlayer.timed` -> `timedValues` (dense indices)
128
+ * SKILL `SavedPlayer.skills` -> `skillValues` (dense indices)
129
+ * STAT `SavedPlayer.stat*` -> `stat*Values` / `statMapNames`
130
+ *
131
+ * plus `SavedPlayer.objKnownModifiers`, the version-1 legacy rune block, which
132
+ * every migration since has carried forward untouched as a dense OBJ_MOD array
133
+ * (`objKnownModifierValues` now). Version 6 converted the modern spelling of
134
+ * that field and left the legacy one behind, which is precisely the shape of
135
+ * defect this ticket exists to end.
136
+ *
137
+ * SQUARE IS THE ONE THAT IS NOT A LIST OF NAMES. See `SQUARE_INFO_LEGEND`.
97
138
  *
98
139
  * OLDER SAVES ARE MIGRATED, NOT REJECTED. Every version below this one has a
99
140
  * conversion step in session/save-migrate.ts, and `saveMigrationsAreComplete()`
@@ -103,7 +144,561 @@ import { PY_MAX_LEVEL, TMD_MAX } from "../player/types.js";
103
144
  * character into "Could not read the save; starting a new game", which in a
104
145
  * permadeath game reads as "your character is gone".
105
146
  */
106
- export const SAVE_VERSION = 4;
147
+ export const SAVE_VERSION = 7;
148
+ /* ------------------------------------------------------------------ *
149
+ * Position-free persistence for the generated tables.
150
+ *
151
+ * A save that stores a flag as its BIT POSITION - or a value at its enum's
152
+ * INDEX - can only be read by a build whose table has the identical length and
153
+ * order. Remove or reorder one entry and every existing character's data
154
+ * silently re-points at a different flag; that is why `MON_SPELL_ENTRIES` could
155
+ * not be opened to mods until #269 (MOD_REACH row 22), and it was equally true
156
+ * of `MON_RACE_FLAG_ENTRIES`, `OBJECT_FLAG_ENTRIES`, `OBJECT_MODIFIER_ENTRIES`
157
+ * and `ELEMENT_ENTRIES` until #273 - and of `MON_TEMP_FLAG_ENTRIES`,
158
+ * `TRAP_FLAG_ENTRIES`, `SQUARE_FLAG_ENTRIES`, `MON_TIMED_ENTRIES`,
159
+ * `PLAYER_TIMED_ENTRIES`, `SKILL` and `STAT_ENTRIES` until #274.
160
+ *
161
+ * Names have no position. A build whose table is larger, smaller or reordered
162
+ * reads back exactly what was written, and a name it no longer has is DROPPED
163
+ * rather than mis-resolved - the same rule `deserializeLore` already applies to
164
+ * a race whose mod is gone. session/save-flag-names.test.ts is the control:
165
+ * it renumbers each table and reads the same data under both schemes.
166
+ *
167
+ * TWO ENCODINGS, AND THE RULE FOR PICKING ONE.
168
+ *
169
+ * A FLAG SET writes `string[]` - the set flags, ascending, so an unchanged
170
+ * entity writes identical bytes. Absence is the whole meaning of an unset
171
+ * flag, so there is nothing to write for one.
172
+ *
173
+ * A DENSE VALUE ARRAY writes `Record<name, value>`. Where zero genuinely
174
+ * means "no such effect" (`timed`, `mTimed`, the object modifiers) the zeroes
175
+ * are omitted and the block stays proportional to what the entity HAS rather
176
+ * than to the table's length. Where the vector is small and always fully
177
+ * populated (`skills`, the four stat arrays) every entry is written, because
178
+ * zero there is a value and not an absence; omitting it would round-trip
179
+ * correctly today and read as a lie the first time someone inspects a save.
180
+ *
181
+ * AND ONE THAT IS NEITHER: see `SQUARE_INFO_LEGEND`, where a name per grid was
182
+ * measured and rejected.
183
+ * ------------------------------------------------------------------ */
184
+ /**
185
+ * A flag/enum value -> name table, INVERTED FROM THE ENUM rather than read off
186
+ * the generated entry tuple.
187
+ *
188
+ * THE THREE ENTRY TUPLES DO NOT SHARE A BASE, and every call site that reaches
189
+ * for one has to know which it is holding:
190
+ * `MON_RACE_FLAG_ENTRIES[flag]` (RF_NONE kept at [0]), `OBJECT_FLAG_ENTRIES
191
+ * [flag - 1]` (OF_NONE dropped), `OBJECT_MODIFIER_ENTRIES[value - 5]` (the five
192
+ * list-stats.h entries come first). The enum is generated from the same header
193
+ * as the values themselves, so inverting it cannot be off by one in any
194
+ * direction - and save-flag-names.test.ts pins each inverted table against its
195
+ * own ENTRIES tuple AT ITS OWN BASE, because a comment claiming a base is
196
+ * exactly the kind of thing that has been wrong here before.
197
+ */
198
+ function nameTable(en) {
199
+ const out = [];
200
+ for (const [name, value] of Object.entries(en))
201
+ out[value] = name;
202
+ return out;
203
+ }
204
+ /**
205
+ * OF_ names by flag number. [0] is the OF_NONE sentinel, [OF.MAX] is the "MAX"
206
+ * one. Nothing in the tree exported this before; RF_FLAG_NAMES and
207
+ * RSF_FLAG_NAMES (mon/lore-file.ts) are its siblings and are built the same way.
208
+ */
209
+ export const OF_FLAG_NAMES = nameTable(OF);
210
+ /*
211
+ * OBJ_MOD and ELEM already have exported name tables - `OBJ_MOD_NAMES` and
212
+ * `ELEMENT_NAMES` in obj/bind.ts, the port of the C `obj_mods[]` and
213
+ * `element_names[]` string arrays, both indexed by the enum VALUE. A second
214
+ * copy here would be a second thing to keep right, so this file uses those; the
215
+ * inverted enum is applied to them AS A CHECK in save-flag-names.test.ts
216
+ * instead, which is the only place the two derivations can be compared.
217
+ */
218
+ /**
219
+ * The set object flags as OF_ names, ascending by flag number so the save is
220
+ * byte-stable for an unchanged object.
221
+ *
222
+ * The bound is OF.MAX for the reason serializeLoreSpells bounds on RSF.MAX:
223
+ * OF_SIZE rounds 39 flags up to 5 bytes, so a set has 40 addressable bits and
224
+ * index 39 reads back as the enum's own `"MAX"` sentinel. Writing that would
225
+ * put a non-flag in the save and, on the way back, set a bit no property owns.
226
+ */
227
+ export function serializeObjectFlags(flags) {
228
+ const out = [];
229
+ for (const flag of flags) {
230
+ if (flag >= OF.MAX)
231
+ break;
232
+ const name = OF_FLAG_NAMES[flag];
233
+ if (name !== undefined)
234
+ out.push(name);
235
+ }
236
+ return out;
237
+ }
238
+ /**
239
+ * The inverse: an OF_SIZE FlagSet with exactly the named flags on. A name this
240
+ * build does not have (a mod's property, uninstalled - or the `"MAX"` sentinel)
241
+ * is dropped, which is how the whole scheme stays safe: an unknown NAME cannot
242
+ * land on some other flag's bit the way an out-of-range index would.
243
+ */
244
+ export function deserializeObjectFlags(names) {
245
+ const set = new FlagSet(OF_SIZE);
246
+ for (const name of names ?? []) {
247
+ const flag = OF_FLAG_NAMES.indexOf(name);
248
+ if (flag > 0 && flag < OF.MAX)
249
+ set.on(flag);
250
+ }
251
+ return set;
252
+ }
253
+ /**
254
+ * The known race flags as RF_ names, ascending.
255
+ *
256
+ * RF has NO `MAX` member - the enum stops at NO_SLOW and `RF_SIZE` is
257
+ * `flagSize(MON_RACE_FLAG_ENTRIES.length)`, so a set has a few unnamed padding
258
+ * bits above the last flag and nothing else. `RF_FLAG_NAMES[flag] === undefined`
259
+ * is therefore the entire bound, and it is derived from the table rather than
260
+ * from a hand-written constant that could part company with it.
261
+ */
262
+ export function serializeLoreFlags(flags) {
263
+ const out = [];
264
+ for (const flag of flags) {
265
+ const name = RF_FLAG_NAMES[flag];
266
+ if (name !== undefined)
267
+ out.push(name);
268
+ }
269
+ return out;
270
+ }
271
+ /**
272
+ * The inverse: an RF_SIZE FlagSet with exactly the named flags on. Index 0 is
273
+ * upstream's RF_NONE, which is never set, so `flag > 0` excludes it; every
274
+ * other named flag fits by construction, because RF_SIZE is sized from the very
275
+ * table the name was looked up in.
276
+ */
277
+ export function deserializeLoreFlags(names) {
278
+ const set = new FlagSet(RF_SIZE);
279
+ for (const name of names ?? []) {
280
+ const flag = RF_FLAG_NAMES.indexOf(name);
281
+ if (flag > 0)
282
+ set.on(flag);
283
+ }
284
+ return set;
285
+ }
286
+ /**
287
+ * The non-zero modifiers as an OBJ_MOD_ name -> value map, in ascending index
288
+ * order so an unchanged object writes identical bytes.
289
+ *
290
+ * NOT A FLAG SET: index 0 is OBJ_MOD_STR, a real modifier, so there is no
291
+ * sentinel to skip at either end and the `flag > 0` guard the flag helpers use
292
+ * would silently drop every strength bonus in the game. A zero IS the absence
293
+ * of a modifier everywhere it is read (`obj.modifiers[k] ?? 0`), so omitting
294
+ * zeroes loses nothing and keeps the block proportional to what the object
295
+ * actually has rather than to the table's length.
296
+ */
297
+ export function serializeObjectModifiers(mods) {
298
+ const out = {};
299
+ for (let i = 0; i < mods.length; i++) {
300
+ const value = mods[i] ?? 0;
301
+ if (value === 0)
302
+ continue;
303
+ const name = OBJ_MOD_NAMES[i];
304
+ if (name !== undefined)
305
+ out[name] = value;
306
+ }
307
+ return out;
308
+ }
309
+ /**
310
+ * The inverse: a full-length OBJ_MOD array, zero wherever the save said
311
+ * nothing. The length comes from THIS build's table, never from the document -
312
+ * a save written against a longer table hands back an array the engine's own
313
+ * loops would run off the end of.
314
+ */
315
+ export function deserializeObjectModifiers(saved) {
316
+ const out = new Array(OBJ_MOD_MAX).fill(0);
317
+ for (const [name, value] of Object.entries(saved ?? {})) {
318
+ const i = OBJ_MOD_NAMES.indexOf(name);
319
+ if (i >= 0 && i < out.length && typeof value === "number")
320
+ out[i] = value;
321
+ }
322
+ return out;
323
+ }
324
+ /**
325
+ * The element info as an ELEM_ name -> {resLevel, flags} map, ascending, with
326
+ * the untouched elements omitted. `flags` stays a raw number: EL_INFO_HATES /
327
+ * EL_INFO_IGNORE are hand-written constants in obj/types.ts, not a generated
328
+ * list, so nothing a mod can do renumbers them.
329
+ */
330
+ export function serializeObjectElements(elInfo) {
331
+ const out = {};
332
+ for (let i = 0; i < elInfo.length; i++) {
333
+ const e = elInfo[i];
334
+ if (e === undefined || (e.resLevel === 0 && e.flags === 0))
335
+ continue;
336
+ const name = ELEMENT_NAMES[i];
337
+ if (name !== undefined)
338
+ out[name] = { resLevel: e.resLevel, flags: e.flags };
339
+ }
340
+ return out;
341
+ }
342
+ /** The inverse: a zeroed ELEM_MAX array with the named elements filled in. */
343
+ export function deserializeObjectElements(saved) {
344
+ const out = newElemInfo();
345
+ for (const [name, e] of Object.entries(saved ?? {})) {
346
+ const i = ELEMENT_NAMES.indexOf(name);
347
+ if (i < 0 || i >= out.length || e === null || typeof e !== "object")
348
+ continue;
349
+ out[i] = { resLevel: e.resLevel ?? 0, flags: e.flags ?? 0 };
350
+ }
351
+ return out;
352
+ }
353
+ /**
354
+ * The el_info[].res_level half on its own, for the one carrier that stores
355
+ * resistance levels without the EL_INFO_ flags beside them: a monster's
356
+ * `knownPstate.elInfo`, which is an Int16Array (mon/monster.ts).
357
+ */
358
+ export function serializeElementLevels(levels) {
359
+ const out = {};
360
+ for (let i = 0; i < levels.length; i++) {
361
+ const value = levels[i] ?? 0;
362
+ if (value === 0)
363
+ continue;
364
+ const name = ELEMENT_NAMES[i];
365
+ if (name !== undefined)
366
+ out[name] = value;
367
+ }
368
+ return out;
369
+ }
370
+ /** The inverse: a zeroed ELEM_MAX array of resistance levels. */
371
+ export function deserializeElementLevels(saved) {
372
+ const out = new Array(ELEM_MAX).fill(0);
373
+ for (const [name, value] of Object.entries(saved ?? {})) {
374
+ const i = ELEMENT_NAMES.indexOf(name);
375
+ if (i >= 0 && i < out.length && typeof value === "number")
376
+ out[i] = value;
377
+ }
378
+ return out;
379
+ }
380
+ /* ------------------------------------------------------------------ *
381
+ * #274: the seven remaining position-persisted tables.
382
+ * ------------------------------------------------------------------ */
383
+ /**
384
+ * MFLAG_ names by flag number. [0] is upstream's MFLAG_NONE; there is no MAX
385
+ * member, so "has a name" is the whole bound, exactly as for RF.
386
+ */
387
+ export const MFLAG_NAMES = nameTable(MFLAG);
388
+ /** TRF_ names by flag number. [0] is TRF_NONE; no MAX member, as for MFLAG. */
389
+ export const TRF_NAMES = nameTable(TRF);
390
+ /**
391
+ * MON_TMD_ names by index. Unlike TMD, this enum DOES carry its own `MAX`
392
+ * member (mon-timed.h ends with MON_TMD_MAX), and `mon.mTimed` is exactly
393
+ * `MON_TMD.MAX` long - so index `MON_TMD.MAX` is the sentinel and never a slot.
394
+ */
395
+ export const MON_TMD_NAMES = nameTable(MON_TMD);
396
+ /**
397
+ * TMD_ names by index. The player-timed enum has NO sentinel at either end:
398
+ * `TMD_MAX` is `PLAYER_TIMED_ENTRIES.length` rather than an enum member, so
399
+ * every index in the table names a real effect.
400
+ */
401
+ export const TMD_NAMES = nameTable(TMD);
402
+ /** SKILL_ names by index. No sentinels; `SKILL_MAX` is a hand-written 10. */
403
+ export const SKILL_NAMES = nameTable(SKILL);
404
+ /** STAT_ names by index. No sentinels; five entries, from list-stats.h. */
405
+ export const STAT_NAMES = nameTable(STAT);
406
+ /**
407
+ * A FlagSet -> ascending names, bounded by "this build has a name for it".
408
+ * Index 0 is the NONE sentinel in every table that has one and is never set,
409
+ * so it costs nothing to let the name table decide.
410
+ */
411
+ function flagNames(flags, names) {
412
+ const out = [];
413
+ for (const flag of flags) {
414
+ if (flag === 0)
415
+ continue;
416
+ const name = names[flag];
417
+ if (name !== undefined)
418
+ out.push(name);
419
+ }
420
+ return out;
421
+ }
422
+ /** The inverse. An unknown name is dropped, never landed on another bit. */
423
+ function flagsFromNames(saved, names, size) {
424
+ const set = new FlagSet(size);
425
+ for (const name of saved ?? []) {
426
+ const flag = names.indexOf(name);
427
+ if (flag > 0 && flag < size * 8)
428
+ set.on(flag);
429
+ }
430
+ return set;
431
+ }
432
+ /**
433
+ * A dense value array -> a name -> value map, ascending by index, omitting
434
+ * zeroes. `all` writes every named slot instead, for the vectors where zero is
435
+ * a value rather than an absence (see the encoding rule above).
436
+ */
437
+ function valuesByName(values, names, all = false) {
438
+ const out = {};
439
+ for (let i = 0; i < values.length; i++) {
440
+ const value = values[i] ?? 0;
441
+ if (!all && value === 0)
442
+ continue;
443
+ const name = names[i];
444
+ if (name !== undefined)
445
+ out[name] = value;
446
+ }
447
+ return out;
448
+ }
449
+ /**
450
+ * The inverse: a full-length array, zero where the document said nothing. The
451
+ * LENGTH IS THIS BUILD'S, never the document's - a save written against a
452
+ * longer table would otherwise hand back an array the engine's own loops run
453
+ * off the end of.
454
+ */
455
+ function valuesFromNames(saved, names, length) {
456
+ const out = new Array(length).fill(0);
457
+ for (const [name, value] of Object.entries(saved ?? {})) {
458
+ const i = names.indexOf(name);
459
+ if (i >= 0 && i < length && typeof value === "number")
460
+ out[i] = value;
461
+ }
462
+ return out;
463
+ }
464
+ /**
465
+ * MFLAG: a monster's transient flags (save.c:228-229, one byte per MFLAG_SIZE).
466
+ *
467
+ * Upstream reads exactly `mflag_size` bytes - THIS build's size, off a stream
468
+ * that was written with the writing build's - so a table that grew by one flag
469
+ * desynchronises the whole rest of the monster block rather than merely
470
+ * mis-naming a flag. There is no discard rule to preserve here because upstream
471
+ * has no notion of a flag it does not recognise; the name form gives it one.
472
+ */
473
+ export function serializeMonsterFlags(flags) {
474
+ return flagNames(flags, MFLAG_NAMES);
475
+ }
476
+ /** The inverse: an MFLAG_SIZE FlagSet with exactly the named flags on. */
477
+ export function deserializeMonsterFlags(names) {
478
+ return flagsFromNames(names, MFLAG_NAMES, MFLAG_SIZE);
479
+ }
480
+ /** TRF: a trap instance's flags (save.c:275-276, rd_trap at load.c:376-377). */
481
+ export function serializeTrapFlags(flags) {
482
+ return flagNames(flags, TRF_NAMES);
483
+ }
484
+ /** The inverse: a TRF_SIZE FlagSet with exactly the named flags on. */
485
+ export function deserializeTrapFlags(names) {
486
+ return flagsFromNames(names, TRF_NAMES, TRF_SIZE);
487
+ }
488
+ /**
489
+ * MON_TMD: a monster's timed effects (save.c:223-226).
490
+ *
491
+ * Zero means "not affected" everywhere `m_timed` is read, so zeroes are
492
+ * omitted. UPSTREAM HAS NO DISCARD RULE HERE AND THAT IS A BUG, not a
493
+ * behaviour to reproduce: save.c:223 writes `MON_TMD_MAX` and load.c:290-292
494
+ * reads that count straight into `mon->m_timed[j]`, which is `MON_TMD_MAX`
495
+ * long in the READING build - a longer table overruns the array. Names cannot
496
+ * overrun anything, so the port's reader simply drops an effect it does not
497
+ * have, which is what upstream's rd_player does two blocks later for the
498
+ * player's own timed effects and evidently meant to do here.
499
+ */
500
+ export function serializeMonsterTimed(timed) {
501
+ return valuesByName(timed, MON_TMD_NAMES);
502
+ }
503
+ /** The inverse: a MON_TMD.MAX-long array, zero where the save said nothing. */
504
+ export function deserializeMonsterTimed(saved) {
505
+ return valuesFromNames(saved, MON_TMD_NAMES, MON_TMD.MAX);
506
+ }
507
+ /**
508
+ * TMD: the player's timed effects (save.c:508-511).
509
+ *
510
+ * THE ONE FIELD WITH AN EXPLICIT UPSTREAM DISCARD RULE. load.c:811-829 reads
511
+ * the saved count and, when it exceeds TMD_MAX, keeps the ones it supports,
512
+ * strips the rest and notes "Discarded unsupported timed effects" - an extra
513
+ * timed effect is not an error. `valuesFromNames` drops an unrecognised name
514
+ * for exactly that reason, and the pre-#274 reader's `slice(0, TMD_MAX)` said
515
+ * the same thing positionally.
516
+ */
517
+ export function serializePlayerTimed(timed) {
518
+ return valuesByName(timed, TMD_NAMES);
519
+ }
520
+ /** The inverse: a TMD_MAX-long array, zero where the save said nothing. */
521
+ export function deserializePlayerTimed(saved) {
522
+ return valuesFromNames(saved, TMD_NAMES, TMD_MAX);
523
+ }
524
+ /**
525
+ * SKILL: the derived level-based skills.
526
+ *
527
+ * NOT AN UPSTREAM FIELD AT ALL - `grep -i skill save.c load.c` is empty, and
528
+ * player->state.skills is rebuilt by calc_bonuses on every load. The port
529
+ * persists them anyway (player/player.ts `skills`), so the same position
530
+ * defect applied; there is no upstream discard rule to match because there is
531
+ * no upstream block. Every slot is written: a skill of 0 is a value, and the
532
+ * whole vector is ten entries.
533
+ */
534
+ export function serializePlayerSkills(skills) {
535
+ return valuesByName(skills, SKILL_NAMES, true);
536
+ }
537
+ /** The inverse: a SKILL_MAX-long array. */
538
+ export function deserializePlayerSkills(saved) {
539
+ return valuesFromNames(saved, SKILL_NAMES, SKILL_MAX);
540
+ }
541
+ /**
542
+ * STAT: one of stat_max / stat_cur / stat_birth (save.c:443-446).
543
+ *
544
+ * Upstream's rule is NOT the timed one: load.c:723-727 reads the saved count
545
+ * and FAILS THE LOAD when it exceeds STAT_MAX ("Too many stats (%d)."), while
546
+ * a shorter count is read as far as it goes and the rest left at their blank
547
+ * values. So "extra" is an error there and "missing" is not. By name that
548
+ * becomes: a stat this build does not have contributes nothing (it has no slot
549
+ * to corrupt), and a stat the document does not mention keeps its blank value.
550
+ * Refusing the load over a name would be strictly worse than upstream, which
551
+ * only refuses because a longer array would overrun a fixed C buffer.
552
+ */
553
+ export function serializeStatValues(stats) {
554
+ return valuesByName(stats, STAT_NAMES, true);
555
+ }
556
+ /** The inverse: a STAT_MAX-long array. */
557
+ export function deserializeStatValues(saved) {
558
+ return valuesFromNames(saved, STAT_NAMES, STAT_MAX);
559
+ }
560
+ /**
561
+ * STAT, THE PERMUTATION. `player->stat_map` (save.c:445) is the trap in this
562
+ * set: it is the only one of the four stat arrays whose VALUES are themselves
563
+ * stat indices rather than magnitudes.
564
+ *
565
+ * player-util.c player_scramble_stats/player_fix_scramble is the authority -
566
+ * `new_cur[stat_map[i]] = stat_cur[i]`, so slot `i` currently holds the value
567
+ * that belongs to stat `stat_map[i]`, and at birth the map is the identity.
568
+ * Naming only the KEYS would have left every value a raw STAT index, so a
569
+ * reordered stat table would still have re-pointed a scrambled character's
570
+ * strength at their intelligence. Both halves are names: `{ STR: "INT" }` reads
571
+ * "the value in the STR slot really belongs to INT".
572
+ *
573
+ * A name this build does not have leaves that slot at the identity, which is
574
+ * what player_fix_scramble resets it to anyway.
575
+ */
576
+ export function serializeStatMap(map) {
577
+ const out = {};
578
+ for (let i = 0; i < map.length; i++) {
579
+ const from = STAT_NAMES[i];
580
+ const to = STAT_NAMES[map[i] ?? i];
581
+ if (from !== undefined && to !== undefined)
582
+ out[from] = to;
583
+ }
584
+ return out;
585
+ }
586
+ /**
587
+ * The inverse, with a permutation check.
588
+ *
589
+ * A HALF-APPLIED MAP IS WORSE THAN NO MAP. Dropping one unresolvable name from
590
+ * a permutation leaves two slots pointing at the same stat and one at none, and
591
+ * player_fix_scramble would then duplicate one stat's value and lose another's
592
+ * - a silent, permanent stat loss. So the result is validated as a permutation
593
+ * of 0..STAT_MAX-1 and falls back WHOLE to the identity if it is not, which is
594
+ * the value the map legitimately holds for every unscrambled character.
595
+ */
596
+ export function deserializeStatMap(saved) {
597
+ const out = Array.from({ length: STAT_MAX }, (_, i) => i);
598
+ if (saved === undefined)
599
+ return out;
600
+ for (const [from, to] of Object.entries(saved)) {
601
+ const i = STAT_NAMES.indexOf(from);
602
+ const j = STAT_NAMES.indexOf(to);
603
+ if (i >= 0 && i < STAT_MAX && j >= 0 && j < STAT_MAX)
604
+ out[i] = j;
605
+ }
606
+ const seen = new Set(out);
607
+ if (seen.size !== STAT_MAX)
608
+ return Array.from({ length: STAT_MAX }, (_, i) => i);
609
+ return out;
610
+ }
611
+ /* ------------------------------------------------------------------ *
612
+ * SQUARE: the one table a per-entity name list could not carry.
613
+ * ------------------------------------------------------------------ */
614
+ /**
615
+ * THE MEASUREMENT, AND WHY THIS ONE IS A LEGEND.
616
+ *
617
+ * Every other table in #273/#274 belongs to an entity there are tens or
618
+ * hundreds of. `square.info` belongs to a GRID, and a 63x188 level is 11,844 of
619
+ * them - written once for the level in play, once for the Town, and once more
620
+ * for every level in the birth_levels_persist cache.
621
+ *
622
+ * Measured on a real generated level (seed 4242, depth 3, 11,844 grids):
623
+ *
624
+ * numeric, 3 bytes/grid, as written today 97,731 bytes
625
+ * one name list per grid, freshly generated 76,624 bytes
626
+ * one name list per grid, EXPLORED 462,289 bytes (4.7x)
627
+ * one name list per grid, every flag set 2,463,553 bytes (25x)
628
+ * this build's whole ordered name list, ONCE 214 bytes
629
+ *
630
+ * The freshly-generated figure is the trap: an unexplored level averages 0.49
631
+ * set flags per grid, so `[]` beats `[0,0,0]` and the name form looks FREE. It
632
+ * is not. The instant a character walks the level, MARK / GLOW / ROOM / SEEN /
633
+ * VIEW land on most grids and the same encoding costs 4.7x - a third of a
634
+ * megabyte per level, on a web build whose storage risk is already the reason
635
+ * saves are compressed at all.
636
+ *
637
+ * So the per-grid payload stays numeric and the DOCUMENT carries the legend:
638
+ * the ordered SQUARE_ names, once, alongside the bits that index into them. The
639
+ * legend travels with the save, so it describes the WRITING build's numbering
640
+ * no matter which build reads it - which is the whole property names were
641
+ * bought for, at 0.2% of one level's cost instead of 470%.
642
+ *
643
+ * Upstream reached the same shape by accident and stopped one step short:
644
+ * save.c:967 writes `SQUARE_SIZE` into the file and load.c:1512 reads it back
645
+ * into a file-scope `square_size` that rd_dungeon_aux then uses as its loop
646
+ * bound (load.c:1306). The file already describes its own square encoding
647
+ * there; it just describes the LENGTH and not the ORDER, so a reordered table
648
+ * still lands every flag somewhere else.
649
+ */
650
+ export const SQUARE_INFO_LEGEND = SQUARE_FLAG_ENTRIES.map((e) => e.name);
651
+ /**
652
+ * A saved legend -> this build's bit numbering, or `null` when the two agree
653
+ * and every byte can be taken as-is (the overwhelmingly common case: the same
654
+ * build, or any build whose SQUARE table has not moved).
655
+ *
656
+ * An absent legend also reads as `null`. That is deliberate and it is safe for
657
+ * one reason only: `V6_TO_V7` stamps the legend onto every document it
658
+ * converts, and no build that could have reordered SQUARE can have written a
659
+ * document at version 6 or below - the same argument `savedFlagSet` rests on in
660
+ * save-migrate.ts.
661
+ */
662
+ export function buildSquareInfoRemap(legend) {
663
+ if (legend === undefined)
664
+ return null;
665
+ let moved = false;
666
+ const remap = [];
667
+ for (let bit = 0; bit < legend.length; bit++) {
668
+ const name = legend[bit];
669
+ const now = name === undefined ? -1 : SQUARE_INFO_LEGEND.indexOf(name);
670
+ remap[bit] = now < 0 ? undefined : now;
671
+ if (now !== bit)
672
+ moved = true;
673
+ }
674
+ if (!moved && legend.length === SQUARE_INFO_LEGEND.length)
675
+ return null;
676
+ return remap;
677
+ }
678
+ /**
679
+ * One grid's info bytes, re-expressed in this build's bit numbering. A bit
680
+ * whose name this build no longer has is dropped, exactly as an unknown flag
681
+ * NAME is dropped everywhere else in this file.
682
+ */
683
+ export function remapSquareInfo(bytes, remap) {
684
+ const out = new Array(SQUARE_SIZE).fill(0);
685
+ for (let b = 0; b < bytes.length; b++) {
686
+ const byte = bytes[b] ?? 0;
687
+ if (byte === 0)
688
+ continue;
689
+ for (let k = 0; k < 8; k++) {
690
+ if ((byte & (1 << k)) === 0)
691
+ continue;
692
+ const now = remap[b * 8 + k];
693
+ if (now === undefined)
694
+ continue;
695
+ const target = now >> 3;
696
+ if (target < SQUARE_SIZE)
697
+ out[target] = (out[target] ?? 0) | (1 << (now & 7));
698
+ }
699
+ }
700
+ return out;
701
+ }
107
702
  export function serializeObject(obj, ids) {
108
703
  return {
109
704
  kindId: ids.kindId(obj.kind.kidx),
@@ -121,9 +716,9 @@ export function serializeObject(obj, ids) {
121
716
  toA: obj.toA,
122
717
  toH: obj.toH,
123
718
  toD: obj.toD,
124
- flags: Array.from(obj.flags.bits),
125
- modifiers: [...obj.modifiers],
126
- elInfo: obj.elInfo.map((e) => ({ ...e })),
719
+ flagNames: serializeObjectFlags(obj.flags),
720
+ modifierValues: serializeObjectModifiers(obj.modifiers),
721
+ elementInfo: serializeObjectElements(obj.elInfo),
127
722
  brands: serializeBrandList(obj.brands, ids),
128
723
  slays: serializeSlayList(obj.slays, ids),
129
724
  curses: serializeCurseList(obj.curses, ids),
@@ -221,9 +816,9 @@ export function deserializeObject(data, reg, ids) {
221
816
  toA: data.toA,
222
817
  toH: data.toH,
223
818
  toD: data.toD,
224
- flags: new FlagSet(Uint8Array.from(data.flags)),
225
- modifiers: [...data.modifiers],
226
- elInfo: data.elInfo.map((e) => ({ ...e })),
819
+ flags: deserializeObjectFlags(data.flagNames),
820
+ modifiers: deserializeObjectModifiers(data.modifierValues),
821
+ elInfo: deserializeObjectElements(data.elementInfo),
227
822
  brands: deserializeBrandList(data.brands, reg, ids),
228
823
  slays: deserializeSlayList(data.slays, reg, ids),
229
824
  curses: deserializeCurseList(data.curses, reg, ids),
@@ -309,11 +904,11 @@ export function serializeMonster(mon, ids) {
309
904
  grid: { x: mon.grid.x, y: mon.grid.y },
310
905
  hp: mon.hp,
311
906
  maxhp: mon.maxhp,
312
- mTimed: Array.from(mon.mTimed),
907
+ monsterTimed: serializeMonsterTimed(mon.mTimed),
313
908
  mspeed: mon.mspeed,
314
909
  energy: mon.energy,
315
910
  cdis: mon.cdis,
316
- mflag: Array.from(mon.mflag.bits),
911
+ mflagNames: serializeMonsterFlags(mon.mflag),
317
912
  mimickedObj: mon.mimickedObj,
318
913
  heldObj: mon.heldObj.map((o) => serializeObject(o, ids)),
319
914
  attr: mon.attr,
@@ -324,8 +919,8 @@ export function serializeMonster(mon, ids) {
324
919
  groupInfo: mon.groupInfo.map((g) => ({ ...g })),
325
920
  minRange: mon.minRange,
326
921
  bestRange: mon.bestRange,
327
- knownPstateFlags: Array.from(mon.knownPstate.flags.bits),
328
- knownPstateElInfo: Array.from(mon.knownPstate.elInfo),
922
+ knownPstateFlagNames: serializeObjectFlags(mon.knownPstate.flags),
923
+ knownPstateElementRes: serializeElementLevels(mon.knownPstate.elInfo),
329
924
  };
330
925
  }
331
926
  /** rd_monster (load.c:259), over JSON instead of the binary block. */
@@ -343,18 +938,18 @@ export function deserializeMonster(data, monsters, objects, ids) {
343
938
  mon.grid = loc(data.grid.x, data.grid.y);
344
939
  mon.hp = data.hp;
345
940
  mon.maxhp = data.maxhp;
346
- mon.mTimed.set(data.mTimed);
941
+ mon.mTimed.set(deserializeMonsterTimed(data.monsterTimed));
347
942
  mon.mspeed = data.mspeed;
348
943
  mon.energy = data.energy;
349
944
  mon.cdis = data.cdis;
350
- mon.mflag.bits.set(data.mflag);
351
- if (data.knownPstateFlags) {
945
+ mon.mflag.copy(deserializeMonsterFlags(data.mflagNames));
946
+ if (data.knownPstateFlagNames) {
352
947
  /* load.c:302 restores known_pstate.flags before the monster goes live. */
353
- mon.knownPstate.flags.bits.set(data.knownPstateFlags);
948
+ mon.knownPstate.flags.copy(deserializeObjectFlags(data.knownPstateFlagNames));
354
949
  }
355
- if (data.knownPstateElInfo) {
950
+ if (data.knownPstateElementRes) {
356
951
  /* load.c:305 restores known_pstate.el_info[].res_level before live use. */
357
- mon.knownPstate.elInfo.set(data.knownPstateElInfo);
952
+ mon.knownPstate.elInfo.set(deserializeElementLevels(data.knownPstateElementRes));
358
953
  }
359
954
  mon.mimickedObj = data.mimickedObj;
360
955
  mon.heldObj = data.heldObj.map((o) => deserializeObject(o, objects, ids));
@@ -397,11 +992,11 @@ export function serializePlayer(p, ids) {
397
992
  msp: p.msp,
398
993
  csp: p.csp,
399
994
  cspFrac: p.cspFrac,
400
- statMax: [...p.statMax],
401
- statCur: [...p.statCur],
402
- statMap: [...p.statMap],
403
- statBirth: [...p.statBirth],
404
- timed: Array.from(p.timed),
995
+ statMaxValues: serializeStatValues(p.statMax),
996
+ statCurValues: serializeStatValues(p.statCur),
997
+ statMapNames: serializeStatMap(p.statMap),
998
+ statBirthValues: serializeStatValues(p.statBirth),
999
+ timedValues: serializePlayerTimed(p.timed),
405
1000
  spellFlags: [...p.spellFlags],
406
1001
  spellOrder: [...p.spellOrder],
407
1002
  playerHp: [...p.playerHp],
@@ -422,22 +1017,22 @@ export function serializePlayer(p, ids) {
422
1017
  })),
423
1018
  equipment: [...p.equipment],
424
1019
  objKnown: {
425
- modifiers: [...p.objKnown.modifiers],
1020
+ modifierValues: serializeObjectModifiers(p.objKnown.modifiers),
426
1021
  toA: p.objKnown.toA,
427
1022
  toH: p.objKnown.toH,
428
1023
  toD: p.objKnown.toD,
429
1024
  dd: p.objKnown.dd,
430
1025
  ds: p.objKnown.ds,
431
1026
  ac: p.objKnown.ac,
432
- elInfo: p.objKnown.elInfo.map((e) => ({ ...e })),
433
- flags: Array.from(p.objKnown.flags.bits),
1027
+ elementInfo: serializeObjectElements(p.objKnown.elInfo),
1028
+ flagNames: serializeObjectFlags(p.objKnown.flags),
434
1029
  /* The learned runes save as the ids of the known brands/slays/curses. */
435
1030
  brands: serializeBrandList(p.objKnown.brands, ids) ?? [],
436
1031
  slays: serializeSlayList(p.objKnown.slays, ids) ?? [],
437
1032
  curses: serializeKnownCurseList(p.objKnown.curses, ids),
438
1033
  },
439
1034
  shapeName: p.shape ? p.shape.name : null,
440
- skills: [...p.skills],
1035
+ skillValues: serializePlayerSkills(p.skills),
441
1036
  /* THE THREE DECLARED FIELDS, NAMED. `{ ...p.upkeep }` wrote whatever the live
442
1037
  * object happened to hold, which is not what the type above says: `notice`,
443
1038
  * `dropping`, `repeatPrevAllowed` and `lastCmdUsedFloorItem` are transients
@@ -500,12 +1095,15 @@ quests) {
500
1095
  p.msp = data.msp;
501
1096
  p.csp = data.csp;
502
1097
  p.cspFrac = data.cspFrac;
503
- p.statMax = [...data.statMax];
504
- p.statCur = [...data.statCur];
505
- p.statMap = [...data.statMap];
506
- p.statBirth = [...data.statBirth];
507
- /* load.c:807-823 reads only supported timed entries and discards extras. */
508
- p.timed.set(data.timed.slice(0, TMD_MAX));
1098
+ /* load.c:723-732: a saved stat this build does not have contributes nothing,
1099
+ * and one the save omits keeps its blank value - see serializeStatValues. */
1100
+ p.statMax = deserializeStatValues(data.statMaxValues);
1101
+ p.statCur = deserializeStatValues(data.statCurValues);
1102
+ p.statMap = deserializeStatMap(data.statMapNames);
1103
+ p.statBirth = deserializeStatValues(data.statBirthValues);
1104
+ /* load.c:811-829 reads only supported timed entries and discards extras. By
1105
+ * name that is a name this build does not have, dropped without complaint. */
1106
+ p.timed.set(deserializePlayerTimed(data.timedValues));
509
1107
  p.spellFlags = [...data.spellFlags];
510
1108
  p.spellOrder = [...data.spellOrder];
511
1109
  p.playerHp = [...data.playerHp];
@@ -545,7 +1143,7 @@ quests) {
545
1143
  p.equipment = [...data.equipment];
546
1144
  if (data.objKnown) {
547
1145
  p.objKnown = {
548
- modifiers: [...data.objKnown.modifiers],
1146
+ modifiers: deserializeObjectModifiers(data.objKnown.modifierValues),
549
1147
  toA: data.objKnown.toA,
550
1148
  toH: data.objKnown.toH,
551
1149
  toD: data.objKnown.toD,
@@ -554,22 +1152,23 @@ quests) {
554
1152
  dd: data.objKnown.dd ?? 1,
555
1153
  ds: data.objKnown.ds ?? 1,
556
1154
  ac: data.objKnown.ac ?? 1,
557
- elInfo: data.objKnown.elInfo.map((e) => ({ ...e })),
558
- flags: new FlagSet(Uint8Array.from(data.objKnown.flags)),
1155
+ elInfo: deserializeObjectElements(data.objKnown.elementInfo),
1156
+ flags: deserializeObjectFlags(data.objKnown.flagNames),
559
1157
  brands: deserializeBrandList(data.objKnown.brands, objReg, ids) ?? [],
560
1158
  slays: deserializeSlayList(data.objKnown.slays, objReg, ids) ?? [],
561
1159
  curses: deserializeKnownCurseList(data.objKnown.curses, objReg, ids),
562
1160
  };
563
1161
  }
564
- else if (data.objKnownModifiers) {
565
- /* Legacy pre-#13 save: only the modifier runes were tracked. */
566
- p.objKnown.modifiers = [...data.objKnownModifiers];
1162
+ else if (data.objKnownModifierValues) {
1163
+ /* Legacy pre-#13 save: only the modifier runes were tracked. By name as of
1164
+ * version 7, through the same helper the modern field uses. */
1165
+ p.objKnown.modifiers = deserializeObjectModifiers(data.objKnownModifierValues);
567
1166
  }
568
1167
  p.shape =
569
1168
  data.shapeName !== null
570
1169
  ? (players.shapes.find((s) => s.name === data.shapeName) ?? null)
571
1170
  : null;
572
- p.skills = [...data.skills];
1171
+ p.skills = deserializePlayerSkills(data.skillValues);
573
1172
  /* notice is NOT in the savefile, exactly as upstream's is not: it is a queue
574
1173
  * of work owed within a turn, and a save can only happen with the queue
575
1174
  * drained. A loaded character therefore starts it at 0 rather than at
@@ -699,6 +1298,43 @@ function deserializeKnownObject(m, floor, reg, ids) {
699
1298
  obj.grid = null;
700
1299
  return { obj, sensed: m.sensed === true };
701
1300
  }
1301
+ /**
1302
+ * The observed spell set as RSF_ names, ascending by flag number so the save is
1303
+ * byte-stable for an unchanged record.
1304
+ *
1305
+ * The bound is rsfMax() (live, including a mod's spells), not the module-load
1306
+ * RSF.MAX. spellNameAt returns null for an unknown index the same way the old
1307
+ * RSF_FLAG_NAMES array returned undefined - including the MAX sentinel when no
1308
+ * mod occupies that slot - so a non-spell never reaches the save.
1309
+ */
1310
+ export function serializeLoreSpells(spellFlags) {
1311
+ const out = [];
1312
+ for (const flag of spellFlags) {
1313
+ if (flag >= rsfMax())
1314
+ break;
1315
+ const name = spellNameAt(flag);
1316
+ if (name !== null)
1317
+ out.push(name);
1318
+ }
1319
+ return out;
1320
+ }
1321
+ /**
1322
+ * The inverse: a lore-sized FlagSet with exactly the named spells on. A name
1323
+ * this build does not have (a mod's spell, uninstalled - or the `"MAX"`
1324
+ * sentinel) is dropped, which is how the whole scheme stays safe: an unknown
1325
+ * NAME cannot land on some other spell's bit the way an out-of-range index
1326
+ * would. spellIndexOf returns -1 for unknown, matching the existing flag > 0
1327
+ * guard's intent.
1328
+ */
1329
+ export function deserializeLoreSpells(names) {
1330
+ const set = new FlagSet(rsfSize());
1331
+ for (const name of names ?? []) {
1332
+ const flag = spellIndexOf(name);
1333
+ if (flag > 0 && flag < rsfMax())
1334
+ set.on(flag);
1335
+ }
1336
+ return set;
1337
+ }
702
1338
  /** Build the feature legend for every fidx appearing in the terrain grids. */
703
1339
  function buildFeatLegend(feats, known, ids) {
704
1340
  const present = new Set();
@@ -762,7 +1398,7 @@ export function serializeGame(state, flavor, seedFlavor, ids, randartSeed = 0, e
762
1398
  grid: { x: t.grid.x, y: t.grid.y },
763
1399
  power: t.power,
764
1400
  timeout: t.timeout,
765
- flags: Array.from(t.flags.bits),
1401
+ trapFlagNames: serializeTrapFlags(t.flags),
766
1402
  })),
767
1403
  });
768
1404
  }
@@ -793,6 +1429,10 @@ export function serializeGame(state, flavor, seedFlavor, ids, randartSeed = 0, e
793
1429
  const runeNotes = runeNoteEntries.length > 0 ? runeNoteEntries : undefined;
794
1430
  return {
795
1431
  version: SAVE_VERSION,
1432
+ /* Written unconditionally, including for a dead save that carries no
1433
+ * chunk: a legend costs 214 bytes and a document that describes its own
1434
+ * square encoding in only some cases is a document with two rules. */
1435
+ squareInfoLegend: [...SQUARE_INFO_LEGEND],
796
1436
  player: serializePlayer(state.actor.player, ids),
797
1437
  actor: {
798
1438
  grid: { x: state.actor.grid.x, y: state.actor.grid.y },
@@ -950,8 +1590,8 @@ export function serializeGame(state, flavor, seedFlavor, ids, randartSeed = 0, e
950
1590
  castSpell: l.castSpell,
951
1591
  blowTimesSeen: [...l.blowTimesSeen],
952
1592
  blowKnown: [...l.blowKnown],
953
- flags: Array.from(l.flags.bits),
954
- spellFlags: Array.from(l.spellFlags.bits),
1593
+ flagsKnown: serializeLoreFlags(l.flags),
1594
+ spellsKnown: serializeLoreSpells(l.spellFlags),
955
1595
  allKnown: l.allKnown,
956
1596
  armourKnown: l.armourKnown,
957
1597
  dropKnown: l.dropKnown,
@@ -985,8 +1625,8 @@ export function deserializeLore(data, ids) {
985
1625
  castSpell: l.castSpell,
986
1626
  blowTimesSeen: [...l.blowTimesSeen],
987
1627
  blowKnown: [...l.blowKnown],
988
- flags: new FlagSet(Uint8Array.from(l.flags)),
989
- spellFlags: new FlagSet(Uint8Array.from(l.spellFlags)),
1628
+ flags: deserializeLoreFlags(l.flagsKnown),
1629
+ spellFlags: deserializeLoreSpells(l.spellsKnown),
990
1630
  allKnown: l.allKnown,
991
1631
  armourKnown: l.armourKnown,
992
1632
  dropKnown: l.dropKnown,
@@ -1222,7 +1862,7 @@ export function deserializeTraps(data, kinds, width, ids, onDecoy) {
1222
1862
  grid,
1223
1863
  power: t.power,
1224
1864
  timeout: t.timeout,
1225
- flags: new FlagSet(Uint8Array.from(t.flags)),
1865
+ flags: deserializeTrapFlags(t.trapFlagNames),
1226
1866
  };
1227
1867
  }));
1228
1868
  }
@@ -1338,7 +1978,7 @@ export function serializeStoredLevel(depth, level, ids) {
1338
1978
  grid: { x: t.grid.x, y: t.grid.y },
1339
1979
  power: t.power,
1340
1980
  timeout: t.timeout,
1341
- flags: Array.from(t.flags.bits),
1981
+ trapFlagNames: serializeTrapFlags(t.flags),
1342
1982
  })),
1343
1983
  });
1344
1984
  }