feedback6 R21D: a consume-on-equip item on a full carrier opens the replace dialog and replaces the picked item first (GitHub #263, PR #264); golden: matches 10 of 18 moved (a targeted consumable on a full carrier now destroys the oldest item), the rest unchanged

PRTS 卫戍协议/帮助 §装备: 「达到上限强行佩戴会改为替换装备:指定一件已佩戴装备替换为将要佩戴的装备,并销毁被指定的装备」,
no exception for the 装备时销毁 items. Before: a normal 博士投影 opened no dialog and its kept item pushed the OLDEST
equipped item out (replaceUid ignored); 盟约之币 and the other consumables kept both items.

- server PlayerState.equip (player/items.js): on a full carrier the consume path takes the pick (replaceUid, else the
  oldest) off before onEquip (_takeReplaced), destroys it (onDestroy reason replace) once the effect went through —
  the normal 博士投影 takes the freed slot —, and puts it back at its index when the effect refuses (博士投影 on an elite:
  nothing destroyed)
- client gameLogic equipReplaces: the replace dialog opens for every item on a full carrier, consumables included,
  except an attaching item that completes an item merge and a 博士投影 on an elite; itemAttaches removed
- bots (itemTarget): a consumable whose effect is the same on anyone goes to a carrier with a free slot first (PR #264)
- docs: design/match.md equipment rules, META §2.5, PLAYING 装备 row, research 04

golden (matches only; each traced equip by equip against the parent — the first change of state is a bot's, or the
AI 托管 human seat's, targeted consumable on a full carrier destroying its oldest item): solo-FUNNY-1 (r9 ai_0 随身身份牌),
solo-FUNNY-2 (r9 ai_0 寻呼模块), solo-HARD-2 (r9 ai_0 随身身份牌, r12 简易通讯机), coop2-NORMAL-3 (r9 ai_1 随身身份牌),
coop3-ABYSS-5 (r11 ai_1 随身身份牌), coop4-FUNNY-6 (r4 ai_1 随身身份牌), solo-HARD-9-boosted (r14 ai_0 随身身份牌),
coop2-ABYSS-10-boosted (r13 ai_0 随身身份牌, r14 ai_1 简易通讯机), coop2-NORMAL-14-standins (r9 ai_0 随身身份牌; now won,
14 rounds), coop2-NORMAL-70-diy (r14 h_0 随身身份牌); the bots' free-slot carrier for target-agnostic consumables moves
no scenario by itself; roster / bonds / fields / standins / diy unchanged
This commit is contained in:
sganggs committed 2026-10-07 18:03:53 +08:00
1 parent 783bd30afd
commit 8cef750e14
14 files changed
+277 -108

No files matched your search

+25 -1
View File
@@ -15,6 +15,8 @@ export class PlayerItems {
* g.equip {itemUid, targetUid, replaceUid?}. A third item on a carrier with both slots used replaces the equipped item
* the player picked in the replace dialog (`replaceUid`, research 09 §1.2 UseEquipUp.unloadInstId; absent ⇒ the
* oldest); the replaced item is destroyed. A `replaceUid` that is not one of the target's equipped items is refused.
* A consume-on-equip item replaces the same way before it resolves (GitHub #263): the carrier keeps a free slot, and a
* target its effect refuses (博士投影 on an elite) gets the picked item back — nothing is destroyed.
*/
equip(itemUid, targetUid, replaceUid = null) {
const g = this._gate(); if (g) return g;
@@ -31,9 +33,18 @@ export class PlayerItems {
if (consume) {
const key = 'item:' + itemKey(item.id);
if (!this.m.registry.has(key)) return fail(ERR.BAD_TARGET, 'effect not available');
// a full carrier replaces first, consumables included (PRTS 卫戍协议/帮助 "达到上限强行佩戴会改为替换装备:指定一件已佩戴
// 装备替换为将要佩戴的装备,并销毁被指定的装备"; GitHub #263): the pick (else the oldest) comes off before the effect —
// which so sees the carrier without it (its bonds) — and is destroyed once the effect went through, leaving the slot
// free (the normal 博士投影 then takes it: ev.keep, so it no longer pushes the oldest item out). A refusal puts it back.
const off = this._takeReplaced(target, replaceUid);
const ev = { item, target, golden: !!rec.isGolden, keep: false, error: null, consumed: true };
this.m.dispatchItem(this, item, target, 'onEquip', ev);
if (ev.error) return fail(ERR[ev.error] ? ev.error : ERR.BAD_TARGET, typeof ev.detail === 'string' ? ev.detail : undefined);
if (ev.error) {
if (off) target.items.splice(Math.min(off.idx, target.items.length), 0, off.piece);
return fail(ERR[ev.error] ? ev.error : ERR.BAD_TARGET, typeof ev.detail === 'string' ? ev.detail : undefined);
}
if (off) this.m.dispatchItem(this, off.piece, target, 'onDestroy', { item: off.piece, holder: target, reason: 'replace' });
// the handler may have destroyed the target (信标) — resolve the item again
const again = this.find(item.uid);
if (again && again.area !== 'equipped') this._detach(again);
@@ -76,6 +87,19 @@ export class PlayerItems {
target.items.push(item);
}
/**
* A consume-on-equip item on a full carrier: take the item `replaceUid` names (else the oldest) off `target` — not yet
* destroyed — and return { piece, idx } (null while a slot is free).
*/
_takeReplaced(target, replaceUid = null) {
target.items = target.items || [];
if (target.items.length < this.gd.equipPerChess) return null;
const i = replaceUid != null ? target.items.findIndex((x) => x.uid === replaceUid) : -1;
const idx = i >= 0 ? i : 0;
const [piece] = target.items.splice(idx, 1);
return { piece, idx };
}
/** g.art {itemUid, row, col, dir?}: the Art's range grid is rotated by `dir` (absent ⇒ RIGHT). */
useArt(itemUid, row, col, dir) {
const g = this._gate(); if (g) return g;