feedback1 integrate: the prep dispatcher walks a snapshot of equipped items (pointed out in PR #2)

Step 5 iterated holder.items live while handlers move and destroy pieces: normal 博士投影
splices itself out (the next item on that holder was skipped) and 突变细胞 transforms its
holder (the next item ran with a holder that was gone). It now walks a copy and runs an item
only while it is still equipped on its still-owned holder; the rule is in the header and
META.md. Tests use a synthetic spy handler after the 突变细胞 and after 博士投影.
This commit is contained in:
sganggs committed 2026-10-03 13:43:56 +08:00
1 parent 07c47b3a19
commit cb01bb3fcf
3 files changed
+133 -4

No files matched your search

+17 -4
View File
@@ -22,7 +22,12 @@
// 投资人 (investShip) active ⇒ SERVER_GAIN garrisons run ×2 (×3 at ≥ 100 layers) — owned by the dispatcher.
// ctx.triggerGarrisons(uid, eventType) re-runs another piece's garrisons (铃兰, "触发…的获得时效果", 特质相同).
// Items: onEquip / onArt / onDestroy go to the item's own handler only; every other hook runs for items equipped on
// owned chess (ev.source.holder). Dispatch order per player: global → band → bonds → garrisons (board reading order,
// owned chess (ev.source.holder). That step walks a snapshot — the owned chess and each holder's items as they stand
// when it begins — and runs an item only if, when its turn comes, it is still equipped on that holder and the holder
// is still owned: handlers move and destroy pieces mid-walk (突变细胞 transforms its holder and returns the equipment
// to the hand; normal 博士投影 destroys itself, which splices holder.items), and a live walk skipped the next item or
// ran it with a holder that was gone. Pieces gained or equipped during the walk wait for the next dispatch.
// Dispatch order per player: global → band → bonds → garrisons (board reading order,
// then hand) → equipped items → effects (insertion order); onPrice runs the priced chess's own 特质 first (购买价格为N
// sets the price the discounts and caps of bonds / strategies then act on — user playtest #5). Every call is
// try/catch-guarded; nested dispatch depth is capped (MAX_DEPTH) so content can never loop the server.
@@ -207,13 +212,14 @@ export class EffectDispatcher {
}
// 4. garrisons (onPrice: already run as step 0)
if (hook !== 'onPrice') this._garrisons(ps, hook, ev);
// 5. equipped items (not for the item-specific hooks)
// 5. equipped items (not for the item-specific hooks): a snapshot of the owned chess and of each holder's items,
// each run only while still equipped on its still-owned holder — handlers move / destroy pieces (header)
if (hook !== 'onEquip' && hook !== 'onArt' && hook !== 'onDestroy') {
for (const holder of ownedChess(ps)) {
for (const it of holder.items || []) {
for (const it of (holder.items || []).slice()) {
const key = `item:${itemKey(it.id)}`;
const h = reg.get(key);
if (h) this._call(ps, key, h, hook, { kind: 'item', key, piece: it, holder, item: this.m.gd.item(it.id) }, ev);
if (h && stillEquipped(ps, holder, it)) this._call(ps, key, h, hook, { kind: 'item', key, piece: it, holder, item: this.m.gd.item(it.id) }, ev);
}
}
}
@@ -346,6 +352,13 @@ function ownedChess(ps) {
return out;
}
/** Dispatch step 5: is `it` still equipped on `holder`, and `holder` still an owned chess of `ps`? */
function stillEquipped(ps, holder, it) {
if (!Array.isArray(holder.items) || !holder.items.includes(it)) return false;
const loc = ps.find(holder.uid);
return !!loc && loc.piece === holder;
}
// =====================================================================================================
// handler context — the ONLY way content mutates player state (see docs/META.md)