feedback6: merge R21D — with both slots full, every consumable (博士投影, 盟约之币 …) opens the replace dialog and destroys only the picked item once its effect succeeds (PRTS 卫戍协议/帮助 「达到上限强行佩戴会改为替换装备…并销毁被指定的装备」; GitHub #263, PR #264 by @SpiritedAwayCN, ported); golden as R21D's commit (10 matches: bots using a consumable on a full operator), the merged tree differs from the branch only in R21B's 联防 scenarios

This commit is contained in:
sganggs committed 2026-10-07 18:11:53 +08:00
commit 3c36cdff48
14 files changed
+272 -103

No files matched your search

+2 -1
View File
@@ -45,7 +45,8 @@
// * Items: equip max 2 (a 3rd replaces the equipped item the player picks — g.equip replaceUid, the oldest when
// absent; equipped items are otherwise locked: g.destroy refuses them),
// 2 identical normal items (hand/temp/equipped) merge into the golden item in the hand, items are never sold
// (destroy for 0). consume-on-equip items resolve through the effect registry and never take a slot.
// (destroy for 0). consume-on-equip items resolve through the effect registry and never take a slot — on a full
// carrier they still replace (destroy) the picked item first, leaving a free slot (PRTS 帮助, GitHub #263).
// * Tokens (PRTS 卫戍协议/帮助 §战斗部署, user playtest #6): placing an owner with manually deployable summons
// (tokens.json `placeable`: 赫默's 医疗探机 and 巫恋's 诅咒娃娃 with their S2, 凯瑟琳's 爬行号·防护单元, 海嗣 / 狼群 /
// 流形) sends one stack (deployLimit copies — 凯瑟琳 2) to the hand, placed by hand like any piece (no deploy slot);
+5 -2
View File
@@ -1736,7 +1736,8 @@ function lastPerfect(m, ps) {
* deployed operator;
* 突变细胞 (the carrier becomes a random tier + 1 operator after the battle): cellTarget;
* 随身身份牌 / 简易通讯机 / 寻呼模块 (act on the carrier's bonds): the operator whose bonds matter most (focus first);
* funds, 紧急调度券, 人事部文档 …: anyone; a bond signature item (requiresBondId): a member of that bond;
* funds, 紧急调度券, 人事部文档 …: anyone, one with a free slot first (on a full carrier the item replaces one of its
* items, GitHub #263); a bond signature item (requiresBondId): a member of that bond;
* other equipment: the strongest deployed damage dealers with a free slot (SURVIVAL items blockers first).
*/
export function itemTarget(m, ps, item, ctx = context(m, ps)) {
@@ -1784,8 +1785,10 @@ export function itemTarget(m, ps, item, ctx = context(m, ps)) {
}
const consume = typeof rec.kind === 'string' && rec.kind.startsWith('consume_on_equip');
const list = byVal(deployed);
if (consume) return list[0] || owned[0] || null;
const free = (p) => (p.items || []).length < gd.equipPerChess;
// a consumable on a full carrier destroys one of its items (the replace rule, GitHub #263): a carrier with a free slot
// first — the effect is the same on anyone
if (consume) return list.find(free) || owned.find(free) || list[0] || owned[0] || null;
if (rec.requiresBondId) {
const member = list.find((p) => free(p) && (chessRec(m, p.id, ps)?.bonds || []).includes(rec.requiresBondId));
if (member) return member;
+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;