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:
1 parent
dc609ea13f
commit
19c7b26850
2 files changed
+123
-4
No files matched your search
@@ -119,17 +119,23 @@ export function registerMeta(registry) {
|
||||
// 画卷 — trap_copy_front_char: copy the operator in range (elite status included) with its equipment. A copied
|
||||
// normal item that completes a pair with an owned one merges at once and the golden stays in the hand (research 04
|
||||
// addendum "两个同名道具(无论是否被装备)会自动合并…并自动返回整备区"); the built-in equipped that golden on the copy.
|
||||
// A copy that completes a three-copy merge (GitHub #389, PR #390): the elite wears nothing — the promotion returns the
|
||||
// copies' equipment to the hand (PRTS 帮助 "在失去该干员(…合并等)…时自动卸除") — and the copied items stay in the hand
|
||||
// too (PRTS 画卷 备注 "获得的装备为未装备状态"), a copied normal item merging with the returned original.
|
||||
wrap(registry, 'chess_item_6_02_m', () => ({
|
||||
onArt(ctx, ev) {
|
||||
const target = (ev.targets || []).find((p) => p && p.kind === 'chess');
|
||||
if (!target) { ev.error = 'BAD_TARGET'; ev.detail = 'no operator in range'; return; }
|
||||
// snapshot before the gain: a gain that completes a merge consumes the target and empties `target.items`, and an
|
||||
// item merge detaches the original's copy from `target.items` while we iterate (the built-in's live loop then
|
||||
// skipped the next item)
|
||||
const itemIds = (target.items || []).map((it) => it.id);
|
||||
const copy = ctx.grantChess(target.id, { requirePool: false, source: 'item:chess_item_6_02_m' });
|
||||
if (!copy) { ev.error = 'HAND_FULL'; return; }
|
||||
// snapshot first: a merge detaches the original's copy of the item from `target.items` while we iterate
|
||||
// (the built-in's live loop then skipped the next item)
|
||||
for (const itemId of (target.items || []).map((it) => it.id)) {
|
||||
const promoted = copy.id !== target.id;
|
||||
for (const itemId of itemIds) {
|
||||
const got = ctx.grantItem(itemId, { source: 'item:chess_item_6_02_m' });
|
||||
const holder = got ? ctx.piece(copy.uid) : null;
|
||||
const holder = got && !promoted ? ctx.piece(copy.uid) : null;
|
||||
if (got && got.id === itemId && holder && holder.kind === 'chess') ctx.equipDirect(got.uid, holder.uid);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
Reference in new issue
Block a user