diff --git a/docs/META.md b/docs/META.md index e685fdee..03966198 100644 --- a/docs/META.md +++ b/docs/META.md @@ -458,7 +458,7 @@ bilibili BV1vzyVBuEN9 ≈ 8:24 / BV1Qkw1zMEoR ≈ 7:25, pointed out in PR #2 — is not consumed: PRTS 下半 记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", PRTS 卫戍协议/帮助 "佩戴的 装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)…时自动卸除", players re-inject it every round; player feedback after 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 ("选择一项(特殊)悬赏任务进行挑战", `choice_event hunter_band_1`). The server applies a random bounty instead: content diff --git a/docs/research/04-items.md b/docs/research/04-items.md index 9d8c1530..740a78d3 100644 --- a/docs/research/04-items.md +++ b/docs/research/04-items.md @@ -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}`. - `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] diff --git a/server/sim/content/items/meta.js b/server/sim/content/items/meta.js index ea90ac93..786dff53 100644 --- a/server/sim/content/items/meta.js +++ b/server/sim/content/items/meta.js @@ -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 - // 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. + // 画卷 — trap_copy_front_char: copy the operator in range (elite status included) and its equipment. The copied items + // are gained unequipped, like any gained item — the hand, overflow temp, destroyed with the usual toast when both are + // full — never put on the copy (PRTS 画卷 备注 "使用后销毁,获得的装备为未装备状态"; until 0.2.2 a copied item that did + // not merge was equipped onto the copy). A copied normal item that completes a pair with an owned one merges at once, + // the golden in the hand (research 04 addendum "两个同名道具(无论是否被装备)会自动合并…并自动返回整备区"). A copy that + // 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', () => ({ onArt(ctx, ev) { 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 copy = ctx.grantChess(target.id, { requirePool: false, source: 'item:chess_item_6_02_m' }); if (!copy) { ev.error = 'HAND_FULL'; return; } - 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 && !promoted ? ctx.piece(copy.uid) : null; - if (got && got.id === itemId && holder && holder.kind === 'chess') ctx.equipDirect(got.uid, holder.uid); - } + for (const itemId of itemIds) ctx.grantItem(itemId, { source: 'item:chess_item_6_02_m' }); }, })); diff --git a/test/content/items.test.js b/test/content/items.test.js index 6762f225..2a23cb79 100644 --- a/test/content/items.test.js +++ b/test/content/items.test.js @@ -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'); 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; - // 变形同构体 never merges ⇒ the copy wears it too - assert.deepEqual(copy.items.map((x) => x.id), [A('6_09')]); + // 变形同构体 never merges ⇒ its copy waits in the hand too, unequipped (PRTS 画卷 备注 "获得的装备为未装备状态"; + // 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(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'); 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'); diff --git a/test/match/scroll-copy-merge.test.js b/test/match/scroll-copy-merge.test.js index ca4a597c..795b38e6 100644 --- a/test/match/scroll-copy-merge.test.js +++ b/test/match/scroll-copy-merge.test.js @@ -111,3 +111,44 @@ test('Picture Scroll: a full hand and temp without promotion keeps the refused s assert.deepEqual(ps.privateView(), before); 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(); + } +});