feedback7 R22-LATE: a 画卷 copy that completes a three-copy merge copies the target's equipment again; golden: unchanged

GitHub #389 / PR #390 by @Xinglan233 (this commit is substantially the PR: the snapshot and its test file, ported).
onArt read target.items after grantChess; a gain that completes a merge runs _mergeChess first, which consumes the
target and empties its items, so the copy loop ran on an empty list (two base 维式重锤 never became the +25% one).
The item ids are now recorded before the gain.

Added on top of the PR: after a promotion the copied items are never equipped onto the elite (an already-advanced
item used to go straight back onto it via equipDirect). The promotion unequips the copies (PRTS 帮助 自动卸除 on
合并) and the 画卷 note says the obtained equipment arrives unequipped, so they wait in the hand; a copied normal item
merges with the returned original as before. The non-promotion path is unchanged.

Tests: test/match/scroll-copy-merge.test.js (8; 4 fail on the parent, the advanced-item case also fails with the
PR's snapshot alone). Golden unchanged (no scenario casts 画卷 into a promotion).
This commit is contained in:
Xinglan233 authored and sganggs committed 2026-10-08 21:04:58 +08:00
1 parent dc609ea13f
commit 19c7b26850
2 files changed
+123 -4

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
// A Picture Scroll (画卷) gain can consume its own target in an immediate three-copy promotion (GitHub #389, PR #390).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeMatch, legalTileFor } from './harness.js';
const OP = 'chess_char_1_06_a'; // 刺玫, the operator of the report
const SCROLL = 'chess_item_6_02_m';
const HAMMER = 'chess_item_1_01_e_a';
const SHIELD = 'chess_item_1_02_e_a';
const FILLER = 'chess_item_6_09_e_a'; // non-mergeable
const OK = { ok: true };
function prep(t, { copies = 2, golden = false, items = [HAMMER] } = {}) {
const h = makeMatch({ mode: 'solo', difficulty: 'FUNNY', seed: 8 }).start();
t.after(() => h.m.dispose());
h.toPrep(1, { band: 'band_dusk' });
const m = h.m, ps = h.ps('p_0');
assert.equal(m.gd.mergeCount(OP), 3);
const id = golden ? m.gd.goldenIdOf(OP) : OP;
const target = ps.acquireChess(id);
const [row, col] = legalTileFor(m, ps, id);
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: target.uid, to: { area: 'board', row, col }, dir: 'RIGHT' }), OK);
for (let i = 1; i < copies; i++) ps.acquireChess(id);
for (const itemId of items) {
const item = ps.acquireItem(itemId);
assert.deepEqual(m.handle('p_0', { t: 'g.equip', itemUid: item.uid, targetUid: target.uid }), OK);
}
const art = ps.hand.find((p) => p?.id === SCROLL);
assert.ok(art, 'Dusk grants the scroll in round 1');
h.invariants();
const cast = () => m.handle('p_0', { t: 'g.art', itemUid: art.uid, row, col, dir: 'RIGHT' });
return { h, m, ps, target, art, row, col, cast };
}
function itemIds(ps) {
return [...ps.hand, ...ps.temp, ...ps.board.values()].filter(Boolean)
.flatMap((p) => p.kind === 'item' ? [p.id] : (p.items || []).map((it) => it.id)).sort();
}
test('Picture Scroll: a third normal copy preserves the target equipment through promotion', (t) => {
const { h, m, ps, target, art, row, col, cast } = prep(t);
const funds = ps.funds;
assert.deepEqual(cast(), OK);
const elite = ps.board.get(`${row},${col}`);
assert.equal(elite.id, m.gd.goldenIdOf(OP));
assert.equal(elite.dir, 'RIGHT');
assert.equal(ps.find(target.uid), null, 'the original target was consumed');
assert.equal(ps.find(art.uid), null, 'the scroll was consumed');
assert.deepEqual(itemIds(ps), [m.gd.item(HAMMER).goldenId]);
assert.ok(ps.hand.some((p) => p?.id === m.gd.item(HAMMER).goldenId), 'the original and copied hammer merge into an advanced hammer in hand');
assert.deepEqual(elite.items, []);
assert.equal(ps.stats.merges, 1);
assert.equal(ps.stats.itemMerges, 1);
assert.equal(ps.funds, funds);
h.invariants();
});
for (const advanced of [false, true]) {
test(`Picture Scroll: promotion copies both ${advanced ? 'advanced' : 'normal'} equipped items, none of them worn by the elite`, (t) => {
const items = [HAMMER, SHIELD].map((id) => advanced ? id.replace(/_a$/, '_b') : id);
const { h, m, ps, row, col, cast } = prep(t, { items });
assert.deepEqual(cast(), OK);
const elite = ps.board.get(`${row},${col}`);
const upgraded = items.map((id) => m.gd.item(id).goldenId || id).sort();
// advanced items do not merge: the returned originals and the copies all wait in the hand ("获得的装备为未装备状态")
assert.deepEqual(itemIds(ps), advanced ? [...items, ...items].sort() : upgraded);
assert.deepEqual(elite.items, [], 'the promotion unequips, and the copies are not put back on the elite');
assert.equal([...ps.hand, ...ps.temp].filter((p) => p?.kind === 'item').length, advanced ? 4 : 2);
h.invariants();
});
}
for (const golden of [false, true]) {
test(`Picture Scroll: one ${golden ? 'elite' : 'normal'} target still copies its equipment without promotion`, (t) => {
const { h, m, ps, target, cast } = prep(t, { copies: 1, golden });
assert.deepEqual(cast(), OK);
assert.equal(ps.find(target.uid)?.piece, target);
assert.equal(ps.allChess().length, 2);
assert.equal(ps.allChess().filter((p) => p.id === target.id).length, 2);
assert.deepEqual(itemIds(ps), [m.gd.item(HAMMER).goldenId]);
assert.equal(ps.stats.merges, 0);
h.invariants();
});
}
test('Picture Scroll: a third copy without equipment still promotes normally', (t) => {
const { h, m, ps, row, col, cast } = prep(t, { items: [] });
assert.deepEqual(cast(), OK);
assert.equal(ps.board.get(`${row},${col}`).id, m.gd.goldenIdOf(OP));
assert.deepEqual(itemIds(ps), []);
h.invariants();
});
test('Picture Scroll: a promotion at full hand and temp still copies the hammer', (t) => {
const { h, m, ps, cast } = prep(t);
while (ps.hand.some((p) => p == null) || ps.temp.some((p) => p == null)) assert.ok(ps.acquireItem(FILLER));
h.invariants();
assert.deepEqual(cast(), OK);
assert.deepEqual(itemIds(ps).filter((id) => id !== FILLER), [m.gd.item(HAMMER).goldenId]);
assert.equal(itemIds(ps).filter((id) => id === FILLER).length, 13);
h.invariants();
});
test('Picture Scroll: a full hand and temp without promotion keeps the refused scroll and target equipment', (t) => {
const { h, ps, target, art, cast } = prep(t, { copies: 1 });
while (ps.hand.some((p) => p == null) || ps.temp.some((p) => p == null)) assert.ok(ps.acquireItem(FILLER));
const before = ps.privateView();
assert.equal(cast().error, 'HAND_FULL');
assert.ok(ps.find(art.uid));
assert.deepEqual(target.items.map((p) => p.id), [HAMMER]);
assert.deepEqual(ps.privateView(), before);
h.invariants();
});