@rpgm-tools/neo-angband-core 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/act.d.ts.map +1 -1
  3. package/dist/agent/act.js +2 -1
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/types.d.ts +11 -1
  6. package/dist/agent/types.d.ts.map +1 -1
  7. package/dist/game/context.d.ts +21 -4
  8. package/dist/game/context.d.ts.map +1 -1
  9. package/dist/game/context.js +13 -1
  10. package/dist/game/context.js.map +1 -1
  11. package/dist/game/loop.d.ts +4 -4
  12. package/dist/game/loop.d.ts.map +1 -1
  13. package/dist/game/loop.js +17 -8
  14. package/dist/game/loop.js.map +1 -1
  15. package/dist/game/player-turn.d.ts +26 -0
  16. package/dist/game/player-turn.d.ts.map +1 -1
  17. package/dist/game/player-turn.js +97 -2
  18. package/dist/game/player-turn.js.map +1 -1
  19. package/dist/game/world.d.ts +15 -0
  20. package/dist/game/world.d.ts.map +1 -1
  21. package/dist/game/world.js +48 -1
  22. package/dist/game/world.js.map +1 -1
  23. package/dist/mod/save-blocks.d.ts +1 -1
  24. package/dist/session/game.d.ts.map +1 -1
  25. package/dist/session/game.js +11 -4
  26. package/dist/session/game.js.map +1 -1
  27. package/dist/store/store-cmd.d.ts +58 -0
  28. package/dist/store/store-cmd.d.ts.map +1 -1
  29. package/dist/store/store-cmd.js +162 -0
  30. package/dist/store/store-cmd.js.map +1 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/package.json +1 -1
  34. package/src/agent/act.ts +2 -1
  35. package/src/agent/types.ts +11 -1
  36. package/src/game/context.ts +28 -5
  37. package/src/game/loop.ts +17 -7
  38. package/src/game/player-turn.ts +95 -1
  39. package/src/game/world.ts +53 -1
  40. package/src/session/game.ts +11 -3
  41. package/src/store/store-cmd.ts +186 -0
  42. package/src/version.ts +1 -1
@@ -16,8 +16,20 @@
16
16
  * - or anything that moves the player mid-transaction - reaches these. They are
17
17
  * therefore real guards on the command path, returning upstream's message or
18
18
  * null to proceed, rather than assertions.
19
+ *
20
+ * Also here: `installStoreCommands`, which registers the `shop-buy` /
21
+ * `shop-sell` / `shop-exit` action-registry handlers `createAgentActions`
22
+ * (agent/act.ts) builds commands for. Those three codes had no handler at
23
+ * all until this was added (docs/PLANNED.md, "An agent cannot trade") - a
24
+ * controller's shopping decisions reached the queue and were silently
25
+ * dropped. The handlers use the guards above plus `storeAtGrid` to resolve
26
+ * "which store" fresh from the player's grid, then commit through the same
27
+ * `StartedGame.buy` / `.sell` closures the interactive shop screen calls.
19
28
  */
20
29
  import { FEAT } from "../generated/index.js";
30
+ import { describeObject } from "../game/describe.js";
31
+ import { gearGet } from "../game/gear.js";
32
+ import { ODESC } from "../obj/desc.js";
21
33
  /**
22
34
  * do_cmd_buy (store.c:1667-1670): purchasing requires standing in a store.
23
35
  * Returns the refusal message, or null when the command may proceed.
@@ -70,4 +82,154 @@ export function enterStoreGuard(store) {
70
82
  return "You see no store here.";
71
83
  return null;
72
84
  }
85
+ /**
86
+ * store_at (store.c, read fresh from `square_shopnum`): the live store whose
87
+ * entrance feature the player is standing on right now, or null. Every store
88
+ * command below re-resolves this rather than trust a Store handed to it, for
89
+ * the same reason the guards above exist - do_cmd_buy / do_cmd_sell /
90
+ * do_cmd_retrieve / do_cmd_stash all call store_at(cave, player->grid) fresh,
91
+ * every time (store.c:1665, :1795, :1872, :2014). The shell's own
92
+ * `storeAtPlayer` (packages/web/src/main.ts) does the same lookup for the
93
+ * interactive shop screen; this is the engine-side twin an agent's direct
94
+ * commands need, since they carry no Store reference at all - only a stock
95
+ * index or a gear handle.
96
+ */
97
+ export function storeAtGrid(state) {
98
+ const feat = state.chunk.feat(state.actor.grid);
99
+ return state.stores?.find((s) => s.feat === feat) ?? null;
100
+ }
101
+ const BUY_FAILURE_SHOP = {
102
+ "not-in-stock": "You cannot buy that item because it's not in the store.",
103
+ "no-room": "You cannot carry that many items.",
104
+ "cannot-afford": "You cannot afford that purchase.",
105
+ };
106
+ const BUY_FAILURE_HOME = {
107
+ "not-in-stock": "You cannot retrieve that item because it's not in the home.",
108
+ "no-room": "You cannot carry that many items.",
109
+ };
110
+ /**
111
+ * Register `shop-buy`, `shop-sell` and `shop-exit` on the action registry -
112
+ * the three command codes `createAgentActions` (agent/act.ts) builds and
113
+ * nothing in the engine handled (docs/PLANNED.md, "An agent cannot trade").
114
+ * On the same terms as every other verb: a plain `registry.register` entry
115
+ * that runs whether the command arrived from a keystroke or an agent's
116
+ * controller, and spends no energy - do_cmd_buy/_sell/_retrieve/_stash pass
117
+ * no game turn (a shop visit does not let monsters act), and there is no
118
+ * upstream leave-store command to spend one either.
119
+ *
120
+ * `shop-buy`/`shop-sell` carry no Store reference (an agent addresses a ware
121
+ * by its stock index, an item by its gear handle - AgentView.stores()[n]
122
+ * and view.inventory()[n], never a live object), so the handler resolves
123
+ * "which store" itself via storeAtGrid and re-runs the entry guard
124
+ * do_cmd_buy/_sell/_retrieve/_stash each run first, exactly as those C
125
+ * functions do. `shop-exit` has no upstream engine counterpart at all - the
126
+ * C command is a UI-only "close the shop screen" with no game-state effect -
127
+ * so it is registered as a legitimate no-op rather than left unhandled.
128
+ */
129
+ export function installStoreCommands(registry, deps) {
130
+ registry.register("shop-buy", (state, cmd) => {
131
+ const args = cmd.args ?? {};
132
+ const index = typeof args["index"] === "number" ? args["index"] : null;
133
+ if (index === null)
134
+ return 0;
135
+ const store = storeAtGrid(state);
136
+ if (!store) {
137
+ state.msg?.(storeBuyGuard(null) ?? "You cannot purchase items when not in a store.");
138
+ return 0;
139
+ }
140
+ const isHome = store.feat === FEAT.HOME;
141
+ const refusal = isHome ? storeRetrieveGuard(store) : storeBuyGuard(store);
142
+ if (refusal) {
143
+ state.msg?.(refusal);
144
+ return 0;
145
+ }
146
+ const obj = store.stock[index];
147
+ if (!obj) {
148
+ state.msg?.(isHome
149
+ ? "You cannot retrieve that item because it's not in the home."
150
+ : "You cannot buy that item because it's not in the store.");
151
+ return 0;
152
+ }
153
+ const rawQty = typeof args["quantity"] === "number" ? args["quantity"] : 1;
154
+ const amt = Math.min(Math.max(1, rawQty), obj.number);
155
+ const result = deps.buy(store, obj, amt);
156
+ if (!result.ok) {
157
+ const table = isHome ? BUY_FAILURE_HOME : BUY_FAILURE_SHOP;
158
+ state.msg?.(table[result.failure ?? ""] ?? "The purchase failed.");
159
+ return 0;
160
+ }
161
+ const bought = result.bought
162
+ ? describeObject(state, result.bought, ODESC.PREFIX | ODESC.FULL)
163
+ : "the item";
164
+ /* comment_accept (do_cmd_buy L1717) prints before the sale line, exactly
165
+ * as the interactive shop screen orders it. */
166
+ if (result.acceptComment)
167
+ state.msg?.(result.acceptComment);
168
+ if (isHome)
169
+ state.msg?.(`You have ${bought}.`);
170
+ else
171
+ state.msg?.(`You bought ${bought} for ${result.price} gold.`);
172
+ if (result.emptied === "retired")
173
+ state.msg?.("The shopkeeper retires.");
174
+ else if (result.emptied === "restocked") {
175
+ state.msg?.("The shopkeeper brings out some new stock.");
176
+ }
177
+ return 0;
178
+ });
179
+ registry.register("shop-sell", (state, cmd) => {
180
+ const args = cmd.args ?? {};
181
+ const handle = typeof args["handle"] === "number" ? args["handle"] : null;
182
+ if (handle === null)
183
+ return 0;
184
+ const store = storeAtGrid(state);
185
+ if (!store) {
186
+ state.msg?.(storeSellGuard(null) ?? "You cannot sell items when not in a store.");
187
+ return 0;
188
+ }
189
+ const isHome = store.feat === FEAT.HOME;
190
+ const refusal = isHome ? storeStashGuard(store) : storeSellGuard(store);
191
+ if (refusal) {
192
+ state.msg?.(refusal);
193
+ return 0;
194
+ }
195
+ const obj = gearGet(state.gear, handle);
196
+ if (!obj) {
197
+ state.msg?.("You do not have that item.");
198
+ return 0;
199
+ }
200
+ const rawQty = typeof args["quantity"] === "number" ? args["quantity"] : obj.number;
201
+ const amt = Math.min(Math.max(1, rawQty), obj.number);
202
+ const result = deps.sell(store, handle, amt);
203
+ if (!result.ok) {
204
+ const why = {
205
+ "no-item": "You do not have that item.",
206
+ stuck: "Hmmm, it seems to be stuck.",
207
+ refused: "I do not wish to purchase this item.",
208
+ "no-room": isHome
209
+ ? "Your home is full."
210
+ : "I have not the room in my store to keep it.",
211
+ };
212
+ state.msg?.(why[result.failure ?? ""] ?? "The sale failed.");
213
+ return 0;
214
+ }
215
+ const sold = result.sold
216
+ ? describeObject(state, result.sold, ODESC.PREFIX | ODESC.FULL)
217
+ : "the item";
218
+ if (isHome)
219
+ state.msg?.(`You have stashed ${sold}.`);
220
+ else if (result.price)
221
+ state.msg?.(`You sold ${sold} for ${result.price} gold.`);
222
+ else
223
+ state.msg?.(`You had ${sold}.`);
224
+ /* purchase_analyze's reaction comment (do_cmd_sell L1972), same ordering
225
+ * as the interactive shop screen. */
226
+ if (result.reactionComment)
227
+ state.msg?.(result.reactionComment);
228
+ return 0;
229
+ });
230
+ /* leave_store (ui-store.c): a UI-only close with no upstream game-state
231
+ * effect. Registered so the code has a real handler rather than falling
232
+ * through to the base registry's no-op stub silently. */
233
+ registry.register("shop-exit", () => 0);
234
+ }
73
235
  //# sourceMappingURL=store-cmd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"store-cmd.js","sourceRoot":"","sources":["../../src/store/store-cmd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAG7C;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAmB;IAC/C,IAAI,CAAC,KAAK;QAAE,OAAO,gDAAgD,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAmB;IACpD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,gCAAgC,CAAC;IACtE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmB;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,4CAA4C,CAAC;IAChE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAmB;IACjD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,2BAA2B,CAAC;IAC3E,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAmB;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO,wBAAwB,CAAC;IAC5C,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"store-cmd.js","sourceRoot":"","sources":["../../src/store/store-cmd.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -21,5 +21,5 @@ export declare const PARITY_BASELINE = "4.2.6";
21
21
  * released tag is iterated takes a MINOR bump, because a published tag is pinned
22
22
  * by digest in a catalogue and must never be moved.
23
23
  */
24
- export declare const ENGINE_VERSION = "0.26.0";
24
+ export declare const ENGINE_VERSION = "0.27.0";
25
25
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -21,5 +21,5 @@ export const PARITY_BASELINE = "4.2.6";
21
21
  * released tag is iterated takes a MINOR bump, because a published tag is pinned
22
22
  * by digest in a catalogue and must never be moved.
23
23
  */
24
- export const ENGINE_VERSION = "0.26.0";
24
+ export const ENGINE_VERSION = "0.27.0";
25
25
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgm-tools/neo-angband-core",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Headless Neo Angband game engine: rules, world, entities, effects, generation",
5
5
  "license": "GPL-2.0-only",
6
6
  "author": "neostryder (RPGM Tools)",
package/src/agent/act.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { GameState } from "../game/context.js";
20
+ import { REST_COMPLETE } from "../game/context.js";
20
21
  import { targetSetLocation, targetSetMonster } from "../game/target.js";
21
22
  import { loc } from "../loc.js";
22
23
  import { AgentCapabilityError } from "./types.js";
@@ -67,7 +68,7 @@ export function createAgentActions(
67
68
  move: (dir) => dirCmd("walk", dir),
68
69
  melee: (dir) => dirCmd("walk", dir),
69
70
  hold: () => cmd("hold"),
70
- rest: () => cmd("rest"),
71
+ rest: (count) => cmd("rest", { count: count ?? REST_COMPLETE }),
71
72
  descend: () => cmd("descend"),
72
73
  ascend: () => cmd("ascend"),
73
74
  tunnel: (dir) => dirCmd("tunnel", dir),
@@ -634,7 +634,17 @@ export interface AgentActions {
634
634
  /** Melee an adjacent monster (a walk into its grid). */
635
635
  melee(dir: number): AgentCommand;
636
636
  hold(): AgentCommand;
637
- rest(): AgentCommand;
637
+ /**
638
+ * do_cmd_rest (cmd-cave.c:1619): rest for `count` game turns, or one of the
639
+ * REST_ special modes (REST_COMPLETE / REST_ALL_POINTS / REST_SOME_POINTS,
640
+ * player-util.h:53-55, exported from game/context.js). One command turn is
641
+ * many game turns: the engine self-continues on its internal queue exactly
642
+ * like a run, so a caller normally issues this ONCE and is not asked for
643
+ * another command until the rest ends (disturbed, or its own condition met).
644
+ * Omitting `count` rests as needed (REST_COMPLETE), matching the human UI's
645
+ * own default prompt answer ("&").
646
+ */
647
+ rest(count?: number): AgentCommand;
638
648
  descend(): AgentCommand;
639
649
  ascend(): AgentCommand;
640
650
  tunnel(dir: number): AgentCommand;
@@ -458,8 +458,7 @@ export interface GameState {
458
458
  * (the per-session file-static player_turns_rested that gates the x2 regen):
459
459
  * resting_turn accumulates for the character's whole life. Bumped once per rested
460
460
  * turn (player-util.c:1487). Persists in the save; absent is treated as 0.
461
- * The live producer (the rest command) lives outside this module - see the
462
- * WIRING-NEEDED note in session/save.ts.
461
+ * The live producer is the rest command's restAction (game/player-turn.ts).
463
462
  */
464
463
  restingTurn?: number;
465
464
  /**
@@ -604,11 +603,20 @@ export interface GameState {
604
603
  * player->upkeep->resting + the file-static player_turns_rested
605
604
  * (player-util.c:1417,1472). count mirrors upkeep->resting (>0 = timed turns
606
605
  * left, or a REST_ special: COMPLETE=-2, ALL_POINTS=-1, SOME_POINTS=-3);
607
- * turnsRested gates the x2 regen. Set/tracked by the web rest command
608
- * (packages/web main.ts driveRest); absent when not resting.
606
+ * turnsRested gates the x2 regen. Set/tracked by the rest command
607
+ * (restAction, game/player-turn.ts); absent when not resting.
609
608
  */
610
609
  resting?: { count: number; turnsRested: number };
611
610
 
611
+ /**
612
+ * player_resting_repeat_count (player-util.c:1523): the file-static count
613
+ * remembered from the last TIMED rest, so re-issuing rest with count === 1
614
+ * repeats it (cmd-cave.c:1638-1643). Session-transient like the file-static
615
+ * it mirrors - never persisted, reset only by a fresh rest that supplies a
616
+ * new count > 1. Absent is the upstream static's zero-initialized state.
617
+ */
618
+ restRepeatCount?: number;
619
+
612
620
  /* --- injected hooks --- */
613
621
  /**
614
622
  * The EffectBuilder injections this session was wired with: the resolvers for
@@ -1168,6 +1176,17 @@ export interface RunState {
1168
1176
  * reach for. The HOST still resolves the choice map (it is save state, and the
1169
1177
  * Fixes & tweaks menu is built from it) - that reading lives in the host. */
1170
1178
 
1179
+ /**
1180
+ * The REST_ constants (player-util.h:53-55): conditional rest modes, which
1181
+ * rest until a condition is met rather than for a fixed turn count. Exported
1182
+ * so callers outside the engine (the agent facade, a mod, a host UI) can build
1183
+ * a rest command's `args.count` without re-declaring the magic numbers - the
1184
+ * web shell used to carry its own private copy of exactly these three.
1185
+ */
1186
+ export const REST_COMPLETE = -2;
1187
+ export const REST_ALL_POINTS = -1;
1188
+ export const REST_SOME_POINTS = -3;
1189
+
1171
1190
  /**
1172
1191
  * player_resting_is_special (player-util.c:1382): the conditional REST_ modes,
1173
1192
  * which rest until a condition is met rather than for a fixed count.
@@ -1180,7 +1199,11 @@ export interface RunState {
1180
1199
  * three bodies only agrees by luck.
1181
1200
  */
1182
1201
  export function playerRestingIsSpecial(count: number): boolean {
1183
- return count === -1 || count === -2 || count === -3;
1202
+ return (
1203
+ count === REST_ALL_POINTS ||
1204
+ count === REST_COMPLETE ||
1205
+ count === REST_SOME_POINTS
1206
+ );
1184
1207
  }
1185
1208
 
1186
1209
  /**
package/src/game/loop.ts CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  playAmbientSound,
50
50
  playerHasWorld,
51
51
  playerOfHasWorld,
52
+ playerRestingCompleteSpecial,
52
53
  playerUpdateLight,
53
54
  processDamageOverTime,
54
55
  processExpDrain,
@@ -177,7 +178,8 @@ const REST_REQUIRED_FOR_REGEN = 5;
177
178
  * player_resting_can_regenerate (player-util.c:1461): the player earns the x2
178
179
  * regeneration bonus once REST_REQUIRED_FOR_REGEN turns of the current rest
179
180
  * have elapsed, or immediately for the conditional REST_ modes. The rest
180
- * command (WP-11, web) sets state.resting; absent when not resting. No RNG.
181
+ * command's restAction (game/player-turn.ts) sets state.resting; absent when
182
+ * not resting. No RNG.
181
183
  */
182
184
  function playerRestingCanRegenerate(state: GameState): boolean {
183
185
  const r = state.resting;
@@ -551,10 +553,10 @@ function loopStop(state: GameState): LoopStatus | null {
551
553
  * keeps the headless harnesses, the borg and the tests driving a whole run
552
554
  * inside one call.
553
555
  *
554
- * The upstream call sits at the top of process_player, before its do-while;
555
- * calling it immediately before processPlayer() is the same point in the order.
556
- * player_resting_complete_special, signalled on the line above it, is ported as
557
- * a predicate the host's rest lifecycle owns (restingCompleteSpecial, WP-11).
556
+ * The upstream call sits at the top of process_player, right after
557
+ * player_resting_complete_special (game-world.c:936-937); processPlayerChecked
558
+ * calls playerRestingCompleteSpecial immediately before this, which is the
559
+ * same point in the order.
558
560
  */
559
561
  export function checkForPlayerInterrupt(state: GameState): InterruptResponse {
560
562
  if (!state.checkInterrupt) return "go";
@@ -582,8 +584,15 @@ export function checkForPlayerInterrupt(state: GameState): InterruptResponse {
582
584
  }
583
585
 
584
586
  /**
585
- * process_player (game-world.c:933): the interrupt check, then the turn. "pause"
586
- * is the host asking for its event loop back before the turn is taken.
587
+ * process_player (game-world.c:933-937): player_resting_complete_special, the
588
+ * interrupt check, then the turn. "pause" is the host asking for its event
589
+ * loop back before the turn is taken.
590
+ *
591
+ * playerRestingCompleteSpecial runs first, exactly as upstream orders it -
592
+ * a conditional rest (REST_ALL_POINTS / REST_COMPLETE / REST_SOME_POINTS) that
593
+ * has already met its own stopping condition (HP/SP full, no bad statuses) is
594
+ * disturbed before the interrupt check or the turn, so it does not spend one
595
+ * extra turn beyond upstream's.
587
596
  *
588
597
  * mayPause is false for the FIRST turn of a runGameLoop call. A call resumed
589
598
  * after LOOP_STATUS.PAUSE re-enters at that same point, so pausing there would
@@ -596,6 +605,7 @@ function processPlayerChecked(
596
605
  registry: ActionRegistry,
597
606
  mayPause: boolean,
598
607
  ): PlayerTurnResult | "pause" {
608
+ playerRestingCompleteSpecial(state);
599
609
  if (checkForPlayerInterrupt(state) === "pause" && mayPause) return "pause";
600
610
  return processPlayer(state, registry);
601
611
  }
@@ -59,6 +59,8 @@ import {
59
59
  deleteMonster,
60
60
  gameTakeHitHooks,
61
61
  movePlayer,
62
+ playerIsResting,
63
+ playerRestingIsSpecial,
62
64
  squareMonster,
63
65
  } from "./context.js";
64
66
  import { gearGet } from "./gear.js";
@@ -800,6 +802,98 @@ export function sleepAction(state: GameState, _cmd: PlayerCommand): number {
800
802
  return state.z.moveEnergy;
801
803
  }
802
804
 
805
+ /**
806
+ * rest (do_cmd_rest, cmd-cave.c:1619-1669): rest for `cmd.args.count` turns, or
807
+ * until one of the REST_ special conditions (REST_COMPLETE / REST_ALL_POINTS /
808
+ * REST_SOME_POINTS, player-util.h:53-55). A DIFFERENT upstream function from
809
+ * hold/sleep - it calls neither search nor autopickup nor the shop-entry
810
+ * disturb (see holdAction's doc for the contrast).
811
+ *
812
+ * ONE CALL IS ONE GAME TURN, exactly like upstream's own do_cmd_rest: the C
813
+ * takes a single player_resting_step_turn and then re-queues itself with
814
+ * cmdq_push(CMD_REST) so process_player's do-while keeps draining it without a
815
+ * fresh keypress. This is the same shape as runAction's self-continuation
816
+ * (player-path.ts) - the command pushes its own follow-up onto
817
+ * state.cmdQueue, which processPlayer drains before asking the host's
818
+ * nextCommand for anything. A host that never pauses (a headless harness, an
819
+ * agent, the tests) therefore gets every turn of a whole rest inside one
820
+ * runGameLoop call, same as it already does for a run.
821
+ *
822
+ * disturb() (player-path.ts) is what stops the self-continuation: it deletes
823
+ * state.resting AND empties state.cmdQueue, so a rest interrupted mid-flight
824
+ * (a monster becoming visible, damage taken, the HP/SP-full check in
825
+ * playerRestingCompleteSpecial) simply has nothing left to pop next time
826
+ * process_player asks - upstream's player_rest_disturb flag achieves the same
827
+ * "the stale queued continuation must not fire" result with a lagging flag;
828
+ * this port's synchronous queue-clear needs no flag at all.
829
+ */
830
+ export function restAction(state: GameState, cmd: PlayerCommand): number {
831
+ /* cmd_get_arg_choice returning != CMD_OK (cmd-cave.c:1624-1625): no count at
832
+ * all is a silent no-op, taking no turn. */
833
+ const arg = cmd.args?.count;
834
+ let n = typeof arg === "number" ? arg : NaN;
835
+ if (!Number.isFinite(n)) return 0;
836
+
837
+ /* "A little sanity checking on the input" (cmd-cave.c:1627-1632): only the
838
+ * three REST_ codes are valid negative choices. */
839
+ if (n < 0 && !playerRestingIsSpecial(n)) return 0;
840
+
841
+ /* Do some upkeep on the first turn of rest (cmd-cave.c:1634-1644). Checked
842
+ * BEFORE this call's count takes effect, exactly as upstream checks
843
+ * player_is_resting() before player_resting_set_count() runs. */
844
+ if (!playerIsResting(state)) {
845
+ /* player->upkeep->update |= PU_BONUS (cmd-cave.c:1636): applied eagerly
846
+ * here rather than deferred, matching how the rest of this port recomputes
847
+ * derived state through direct hook calls instead of dirty bits. */
848
+ state.updateBonuses?.();
849
+
850
+ if (n > 1) state.restRepeatCount = n;
851
+ else if (n === 1) n = state.restRepeatCount ?? 0;
852
+ }
853
+
854
+ /* player_resting_set_count (player-util.c:1425-1445): store the count (or
855
+ * cancel to the not-resting sentinel), clamped at 9999. This port represents
856
+ * "not resting" as state.resting being absent rather than count === 0, so a
857
+ * count of 0 (an explicit "0 turns", or a repeat with nothing remembered)
858
+ * deletes the field instead of storing a zero. */
859
+ if (n === 0 || (n < 0 && !playerRestingIsSpecial(n))) {
860
+ delete state.resting;
861
+ } else if (!state.resting) {
862
+ state.resting = { count: Math.min(n, 9999), turnsRested: 0 };
863
+ } else {
864
+ state.resting.count = Math.min(n, 9999);
865
+ }
866
+
867
+ /* "Set the counter, and stop if told to" (cmd-cave.c:1646-1649). */
868
+ if (!playerIsResting(state)) return 0;
869
+ const r = state.resting!;
870
+
871
+ /* player_resting_step_turn (player-util.c:1472-1489): spend the turn,
872
+ * decrement a timed count, bump both rested counters. */
873
+ if (r.count > 0) r.count--;
874
+ r.turnsRested++;
875
+ state.restingTurn = (state.restingTurn ?? 0) + 1;
876
+
877
+ /* "Prepare to continue, or cancel and clean up" (cmd-cave.c:1657-1667). `n`
878
+ * here is upstream's own local n: unchanged since the upkeep block above for
879
+ * a special rest, or the count this call was asked to do, decremented by
880
+ * step_turn's already-applied r.count-- for a timed one - so r.count IS
881
+ * upstream's `n - 1` at this point, and pushing it as the next choice is
882
+ * pushing exactly that. */
883
+ if (r.count > 0) {
884
+ if (!state.cmdQueue) state.cmdQueue = [];
885
+ state.cmdQueue.push({ code: "rest", args: { count: r.count } });
886
+ } else if (playerRestingIsSpecial(n)) {
887
+ if (!state.cmdQueue) state.cmdQueue = [];
888
+ state.cmdQueue.push({ code: "rest", args: { count: n } });
889
+ state.restRepeatCount = 0;
890
+ } else {
891
+ delete state.resting;
892
+ }
893
+
894
+ return state.z.moveEnergy;
895
+ }
896
+
803
897
  /**
804
898
  * descend / ascend: signal a level change (player->upkeep->generate_level).
805
899
  * The generation and depth change belong to the world integration, which does
@@ -854,7 +948,7 @@ export function createDefaultRegistry(): ActionRegistry {
854
948
  reg.register("walk", walkAction);
855
949
  reg.register("jump", jumpAction);
856
950
  reg.register("hold", holdAction);
857
- reg.register("rest", holdAction);
951
+ reg.register("rest", restAction);
858
952
  reg.register("sleep", sleepAction);
859
953
  reg.register("descend", descendAction);
860
954
  reg.register("ascend", ascendAction);
package/src/game/world.ts CHANGED
@@ -59,7 +59,13 @@ import { floorPile } from "./floor.js";
59
59
  import { monsterGroupChangeIndex } from "./mon-group.js";
60
60
  import { disturb } from "./player-path.js";
61
61
  import { DEFAULT_HITPOINT_WARN } from "./project-cast.js";
62
- import { deleteMonster } from "./context.js";
62
+ import {
63
+ REST_ALL_POINTS,
64
+ REST_COMPLETE,
65
+ REST_SOME_POINTS,
66
+ deleteMonster,
67
+ playerRestingIsSpecial,
68
+ } from "./context.js";
63
69
  import type { GameState } from "./context.js";
64
70
 
65
71
  /** player-util.h PY_EXERT_* over-exertion bit flags. */
@@ -126,6 +132,52 @@ export function playerHasWorld(state: GameState, flag: number): boolean {
126
132
  return state.playerState?.pflags.has(flag) ?? false;
127
133
  }
128
134
 
135
+ /**
136
+ * player_resting_complete_special (player-util.c:1495-1520): decide whether a
137
+ * conditional rest (one of the REST_ special counts) has met its own stopping
138
+ * condition, and disturb() to end it if so. A no-op when not resting on a
139
+ * special count. No RNG.
140
+ *
141
+ * Lives here rather than in context.ts (where state.resting and the REST_
142
+ * constants are declared) because it needs disturb() (player-path.ts) and
143
+ * playerHasWorld (below); context.ts cannot import either without a cycle
144
+ * (player-path.ts and this module both import GameState from context.ts).
145
+ * loop.ts calls this at the top of every process_player equivalent
146
+ * (game-world.c:936, immediately before EVENT_CHECK_INTERRUPT), which is
147
+ * upstream's own call site.
148
+ */
149
+ export function playerRestingCompleteSpecial(state: GameState): void {
150
+ const r = state.resting;
151
+ if (!r || !playerRestingIsSpecial(r.count)) return;
152
+ const p = state.actor.player;
153
+ const t = p.timed;
154
+
155
+ if (r.count === REST_ALL_POINTS) {
156
+ if (p.chp === p.mhp && p.csp === p.msp) disturb(state);
157
+ } else if (r.count === REST_COMPLETE) {
158
+ if (
159
+ p.chp === p.mhp &&
160
+ (p.csp === p.msp || playerHasWorld(state, PF.COMBAT_REGEN)) &&
161
+ !(t[TMD.BLIND] ?? 0) &&
162
+ !(t[TMD.CONFUSED] ?? 0) &&
163
+ !(t[TMD.POISONED] ?? 0) &&
164
+ !(t[TMD.AFRAID] ?? 0) &&
165
+ !(t[TMD.TERROR] ?? 0) &&
166
+ !(t[TMD.STUN] ?? 0) &&
167
+ !(t[TMD.CUT] ?? 0) &&
168
+ !(t[TMD.SLOW] ?? 0) &&
169
+ !(t[TMD.PARALYZED] ?? 0) &&
170
+ !(t[TMD.IMAGE] ?? 0) &&
171
+ !p.wordRecall &&
172
+ !p.deepDescent
173
+ ) {
174
+ disturb(state);
175
+ }
176
+ } else if (r.count === REST_SOME_POINTS) {
177
+ if (p.chp === p.mhp || p.csp === p.msp) disturb(state);
178
+ }
179
+ }
180
+
129
181
  /** state.dam_red / state.perc_dam_red from the last calc_bonuses (else zero). */
130
182
  function worldReduction(state: GameState): DamageReduction {
131
183
  const ps = state.playerState;
@@ -215,6 +215,7 @@ import {
215
215
  } from "../store/transact.js";
216
216
  import type { BuyResult, SellResult, TxnKnowledge } from "../store/transact.js";
217
217
  import { storeCheckNum } from "../store/store.js";
218
+ import { installStoreCommands } from "../store/store-cmd.js";
218
219
  import { priceItem } from "../store/price.js";
219
220
  import {
220
221
  addMonsterMessage,
@@ -3803,7 +3804,7 @@ export function startGame(pack: GamePack, opts: StartGameOptions = {}): StartedG
3803
3804
  changeLevel,
3804
3805
  ),
3805
3806
  wizardBundles: wired.wizardBundles,
3806
- ...makeStoreApi(state, reg, wired.flavor, options),
3807
+ ...makeStoreApi(state, reg, wired.flavor, options, wired.registry),
3807
3808
  };
3808
3809
  }
3809
3810
 
@@ -3818,6 +3819,7 @@ function makeStoreApi(
3818
3819
  reg: CoreRegistries,
3819
3820
  flavor: FlavorKnowledge,
3820
3821
  options: OptionState,
3822
+ registry: ActionRegistry,
3821
3823
  ): Pick<StartedGame, "buy" | "sell" | "sellFloor" | "price" | "willBuy"> {
3822
3824
  const storeCtx = (): StoreMaintContext => ({
3823
3825
  rng: state.rng,
@@ -3901,7 +3903,7 @@ function makeStoreApi(
3901
3903
  * stale. Unused by storeBuy, which learns flavour only. */
3902
3904
  learnRunes: { env: state.runeEnv, runes: buildRuneList(state.runeEnv) },
3903
3905
  });
3904
- return {
3906
+ const storeApi: Pick<StartedGame, "buy" | "sell" | "sellFloor" | "price" | "willBuy"> = {
3905
3907
  buy: (store, obj, amt): BuyResult => {
3906
3908
  /* do_cmd_retrieve (store.c:1783): Home Take is free - no price, no
3907
3909
  * ORIGIN_STORE, no shuffle/maint RNG. Reuse the existing homeRetrieve. */
@@ -4055,6 +4057,12 @@ function makeStoreApi(
4055
4057
  state.storeBehaviour,
4056
4058
  ),
4057
4059
  };
4060
+ /* shop-buy / shop-sell / shop-exit (docs/PLANNED.md, "An agent cannot
4061
+ * trade"): wire the agent-facing store commands through the SAME buy/sell
4062
+ * closures the interactive shop screen calls, so a keystroke and an
4063
+ * agent's command reach identical pack/gold/knowledge effects. */
4064
+ installStoreCommands(registry, { buy: storeApi.buy, sell: storeApi.sell });
4065
+ return storeApi;
4058
4066
  }
4059
4067
 
4060
4068
  /**
@@ -4691,6 +4699,6 @@ export function loadGame(
4691
4699
  changeLevel,
4692
4700
  ),
4693
4701
  wizardBundles: wired.wizardBundles,
4694
- ...makeStoreApi(state, reg, wired.flavor, options),
4702
+ ...makeStoreApi(state, reg, wired.flavor, options, wired.registry),
4695
4703
  };
4696
4704
  }