feedback7 R22-LATE: 画卷 never puts the copied items on the copy — they are gained unequipped into the hand / temp like any item; golden: unchanged

PRTS 卫戍协议:盟约 PRTS盟约记录, 法术编纂集, 画卷 备注: 「选择策略<夕>时可获得<br>使用后销毁,获得的装备为未装备状态」.
Without a promotion the 画卷 handler still equipped every copied item that did not merge (an advanced item, a
变形同构体) onto the copy in the hand (the promotion case was fixed for #389). Now each copied item is only gained
(ctx.grantItem → acquireItem): the hand, overflow temp, and with both full it is destroyed with the usual toast
「整备区已满,获得的装备已销毁」, as for any gained item. A copied normal item that completes a pair still merges at once
(the golden in the hand), and the copy itself still follows the chess gain rules (a full hand and temp refuse the
scroll: HAND_FULL, the scroll kept). Full-hand check: a full hand with 3 free temp slots holds the copy and both
copied advanced items; with 2 free slots the copy and one item fit and the other is destroyed with the toast (before,
both rode on the copy).

Docs: META.md (Arts list), research/04-items.md (画卷 copy). The content test that pinned the old behaviour
(test/content/items.test.js: 变形同构体 "the copy wears it too") now expects it in the hand.
Tests: test/match/scroll-copy-merge.test.js +3 (no promotion, normal and elite targets with advanced items; the full
hand at 3 / 2 free temp slots) — all 3 fail on the parent; every test mentioning 画卷: 177 pass. Golden unchanged.
This commit is contained in:
sganggs committed 2026-10-08 21:53:51 +08:00
1 parent 7c56335c06
commit ac4231846f
5 files changed
+56 -17

No files matched your search

+1 -1
View File
@@ -458,7 +458,7 @@ bilibili BV1vzyVBuEN9 ≈ 8:24 / BV1Qkw1zMEoR ≈ 7:25, pointed out in PR #2 —
is not consumed: PRTS 下半 记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", PRTS 卫戍协议/帮助 "佩戴的 is not consumed: PRTS 下半 记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", PRTS 卫戍协议/帮助 "佩戴的
装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)…时自动卸除", players re-inject it every round; player feedback after 装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)…时自动卸除", players re-inject it every round; player feedback after
0.1.0), 画卷 (copy the operator in range with its 0.1.0), 画卷 (copy the operator in range with its
items), 教鞭 / “神秘顾客” (a random bounty is added). items — the copied items arrive unequipped in the hand, PRTS 画卷 备注 "获得的装备为未装备状态"), 教鞭 / “神秘顾客” (a random bounty is added).
**教鞭 / “神秘顾客” stay a random bounty (deliberate).** The official Arts open a personal 悬赏 choice **教鞭 / “神秘顾客” stay a random bounty (deliberate).** The official Arts open a personal 悬赏 choice
("选择一项(特殊)悬赏任务进行挑战", `choice_event hunter_band_1`). The server applies a random bounty instead: content ("选择一项(特殊)悬赏任务进行挑战", `choice_event hunter_band_1`). The server applies a random bounty instead: content
+1 -1
View File
@@ -352,7 +352,7 @@ Combo check: `equip_chess_id` in blackboard is the partner's id WITHOUT the `_a/
- Buff keys: 画卷 `trap_copy_front_char{}`; 教鞭 `trap_create_self_choice{choice_event=hunter_band_1}`; 神秘顾客 additionally `trap_disney_special{count=1}`. - Buff keys: 画卷 `trap_copy_front_char{}`; 教鞭 `trap_create_self_choice{choice_event=hunter_band_1}`; 神秘顾客 additionally `trap_disney_special{count=1}`.
- `hunter_band_1` (choiceType PERSONAL_CHOOSE, "悬赏决策") pool is server-side. [ASSUMED] offer 3 random `enemyeffect_b_*` bounties (e.g. 碎骨·悬赏 +1, 萨卡兹百夫长·悬赏 +2, 泥岩·悬赏 +4, 锏·悬赏 +5, “复仇者”·悬赏 +6 funds; buff `add_enemy_kill_gain_coin{count,enemy_id,coin,round=1}`): adds 1 enemy to your next battle, killer gets `coin` funds. - `hunter_band_1` (choiceType PERSONAL_CHOOSE, "悬赏决策") pool is server-side. [ASSUMED] offer 3 random `enemyeffect_b_*` bounties (e.g. 碎骨·悬赏 +1, 萨卡兹百夫长·悬赏 +2, 泥岩·悬赏 +4, 锏·悬赏 +5, “复仇者”·悬赏 +6 funds; buff `add_enemy_kill_gain_coin{count,enemy_id,coin,round=1}`): adds 1 enemy to your next battle, killer gets `coin` funds.
- 画卷 copy: duplicates the operator (same chess id incl. elite `_b`) AND clones its equipped items into the copy [DATA desc "包括精锐状态和装备"]; copy goes to hand [ASSUMED]; if hand full -> temporary hand. - 画卷 copy: duplicates the operator (same chess id incl. elite `_b`) AND its equipped items [DATA desc "包括精锐状态和装备"]; the copied items arrive unequipped (PRTS 画卷 备注 "使用后销毁,获得的装备为未装备状态"), gained like any item — the hand, overflow temp (destroyed with the usual toast when both are full); copy goes to hand [ASSUMED]; if hand full -> temporary hand.
## 8. Item sources other than the shop [DATA] ## 8. Item sources other than the shop [DATA]
+9 -12
View File
@@ -116,12 +116,14 @@ export function registerMeta(registry) {
})); }));
} }
// 画卷 — trap_copy_front_char: copy the operator in range (elite status included) with its equipment. A copied // 画卷 — trap_copy_front_char: copy the operator in range (elite status included) and its equipment. The copied items
// normal item that completes a pair with an owned one merges at once and the golden stays in the hand (research 04 // are gained unequipped, like any gained item — the hand, overflow temp, destroyed with the usual toast when both are
// addendum "两个同名道具(无论是否被装备)会自动合并…并自动返回整备区"); the built-in equipped that golden on the copy. // full — never put on the copy (PRTS 画卷 备注 "使用后销毁,获得的装备为未装备状态"; until 0.2.2 a copied item that did
// A copy that completes a three-copy merge (GitHub #389, PR #390): the elite wears nothing — the promotion returns the // not merge was equipped onto the copy). A copied normal item that completes a pair with an owned one merges at once,
// copies' equipment to the hand (PRTS 帮助 "在失去该干员(…合并等)…时自动卸除") — and the copied items stay in the hand // the golden in the hand (research 04 addendum "两个同名道具(无论是否被装备)会自动合并…并自动返回整备区"). A copy that
// too (PRTS 画卷 备注 "获得的装备为未装备状态"), a copied normal item merging with the returned original. // completes a three-copy merge (GitHub #389, PR #390): the promotion returns the copies' equipment to the hand too
// (PRTS 帮助 "在失去该干员(…合并等)…时自动卸除"), so the elite wears nothing and a copied normal item merges with the
// returned original.
wrap(registry, 'chess_item_6_02_m', () => ({ wrap(registry, 'chess_item_6_02_m', () => ({
onArt(ctx, ev) { onArt(ctx, ev) {
const target = (ev.targets || []).find((p) => p && p.kind === 'chess'); const target = (ev.targets || []).find((p) => p && p.kind === 'chess');
@@ -132,12 +134,7 @@ export function registerMeta(registry) {
const itemIds = (target.items || []).map((it) => it.id); const itemIds = (target.items || []).map((it) => it.id);
const copy = ctx.grantChess(target.id, { requirePool: false, source: 'item:chess_item_6_02_m' }); const copy = ctx.grantChess(target.id, { requirePool: false, source: 'item:chess_item_6_02_m' });
if (!copy) { ev.error = 'HAND_FULL'; return; } if (!copy) { ev.error = 'HAND_FULL'; return; }
const promoted = copy.id !== target.id; for (const itemId of itemIds) ctx.grantItem(itemId, { source: 'item:chess_item_6_02_m' });
for (const itemId of itemIds) {
const got = ctx.grantItem(itemId, { source: 'item:chess_item_6_02_m' });
const holder = got && !promoted ? ctx.piece(copy.uid) : null;
if (got && got.id === itemId && holder && holder.kind === 'chess') ctx.equipDirect(got.uid, holder.uid);
}
}, },
})); }));
+4 -3
View File
@@ -1121,10 +1121,11 @@ test('画卷 (Art): copies the operator on the tile / in front with its elite st
const copy = ps.hand.find((p) => p && p.kind === 'chess'); const copy = ps.hand.find((p) => p && p.kind === 'chess');
assert.equal(copy && copy.id, DATA.chess[cid].goldenId, 'elite copy in the hand'); assert.equal(copy && copy.id, DATA.chess[cid].goldenId, 'elite copy in the hand');
// the copied 激光发射器 pairs with the original's (items merge "无论是否被装备") ⇒ the golden goes to the hand; // the copied 激光发射器 pairs with the original's (items merge "无论是否被装备") ⇒ the golden goes to the hand;
// 变形同构体 never merges ⇒ the copy wears it too // 变形同构体 never merges ⇒ its copy waits in the hand too, unequipped (PRTS 画卷 备注 "获得的装备为未装备状态";
assert.deepEqual(copy.items.map((x) => x.id), [A('6_09')]); // until 0.2.2 the copy wore it)
assert.deepEqual(copy.items.map((x) => x.id), [], 'the copy wears nothing');
assert.deepEqual(ps.find(target.uid).piece.items.map((x) => x.id), [A('6_09')]); assert.deepEqual(ps.find(target.uid).piece.items.map((x) => x.id), [A('6_09')]);
assert.deepEqual(handIds(ps, 'item'), [B('3_03')], 'merged golden in the hand (regression: was equipped on the copy)'); assert.deepEqual(handIds(ps, 'item').slice().sort(), [B('3_03'), A('6_09')].sort(), 'the merged golden and the copied 变形同构体 in the hand');
const art2 = giveItem(m, ps, 'chess_item_6_02_m'); const art2 = giveItem(m, ps, 'chess_item_6_02_m');
assert.equal(m.handle('p_0', { t: 'g.art', itemUid: art2.uid, row: 11, col: 7 }).error, 'BAD_TARGET', 'no operator in range'); assert.equal(m.handle('p_0', { t: 'g.art', itemUid: art2.uid, row: 11, col: 7 }).error, 'BAD_TARGET', 'no operator in range');
cover('chess_item_6_02_m'); cover('chess_item_6_02_m');
+41
View File
@@ -111,3 +111,44 @@ test('Picture Scroll: a full hand and temp without promotion keeps the refused s
assert.deepEqual(ps.privateView(), before); assert.deepEqual(ps.privateView(), before);
h.invariants(); h.invariants();
}); });
// PRTS 画卷 备注 「使用后销毁,获得的装备为未装备状态」: without a promotion too, the copied items are gained like any item —
// the hand, overflow temp — and never put on the copy (until 0.2.2 a copy that did not merge was equipped onto it)
const ADV = [HAMMER, SHIELD].map((id) => id.replace(/_a$/, '_b'));
const looseItems = (ps) => [...ps.hand, ...ps.temp].filter((p) => p?.kind === 'item').map((p) => p.id).sort();
for (const golden of [false, true]) {
test(`Picture Scroll: one ${golden ? 'elite' : 'normal'} target — the copied (advanced) items arrive unequipped in the hand`, (t) => {
const { h, m, ps, target, cast } = prep(t, { copies: 1, golden, items: ADV });
assert.deepEqual(cast(), OK);
const copy = ps.allChess().find((p) => p.id === target.id && p !== target);
assert.ok(copy, 'the copy was gained');
assert.deepEqual(copy.items, [], 'the copy wears nothing');
assert.deepEqual(target.items.map((p) => p.id).sort(), [...ADV].sort(), 'the target keeps its own');
assert.deepEqual(looseItems(ps), [...ADV].sort(), 'the copied items wait in the hand');
assert.equal(m.gd.chess(copy.id).isGolden, golden);
h.invariants();
});
}
test('Picture Scroll: a full hand — the copy and its items overflow into temp; an item with no slot left is destroyed like any gained item', (t) => {
for (const free of [3, 2]) {
const { h, ps, target, cast } = prep(t, { copies: 1, items: ADV });
while (ps.hand.some((p) => p == null)) assert.ok(ps.acquireItem(FILLER));
while (ps.temp.filter((p) => p == null).length > free) assert.ok(ps.acquireItem(FILLER));
const toasts = () => h.allTo('p_0', 'm.toast').map((x) => JSON.stringify(x));
const before = toasts().length;
assert.deepEqual(cast(), OK);
const copy = ps.allChess().find((p) => p.id === target.id && p !== target);
const where = copy ? ps.find(copy.uid)?.area : null;
assert.ok(where === 'temp' || where === 'hand', `free ${free}: the copy was gained into temp (the hand fills from temp once the scroll left)`);
assert.deepEqual(copy.items, [], `free ${free}: the copy wears nothing`);
assert.deepEqual(target.items.map((p) => p.id).sort(), [...ADV].sort());
const got = looseItems(ps).filter((id) => id !== FILLER);
if (free === 3) assert.deepEqual(got, [...ADV].sort(), 'room for the copy and both items');
else {
assert.equal(got.length, 1, 'room for the copy and one item: the other is destroyed');
assert.ok(toasts().slice(before).some((s) => s.includes('整备区已满,获得的装备已销毁')), 'with the usual toast');
}
h.invariants();
}
});