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