diff --git a/docs/META.md b/docs/META.md index 6a13b581..c043f17d 100644 --- a/docs/META.md +++ b/docs/META.md @@ -252,7 +252,7 @@ the wave; an attack on every enemy in range counts ×2 — 阵法术师 / 轰击 chain ×1.4 [ASSUMED], `CROWD`). The deployed set maximizes unit value + activated bond tiers (exact counting via `computeBonds`; every deployed focus member counts toward the next threshold). Items by what they do: equipment on the strongest deployed damage dealers (survival items on blockers first, bond signature items on a member), 信标 on a bench single (never the -lineup when a bench single exists), 拟态物质 on a pair, 博士投影 (both qualities — neither takes an elite) on the strongest +lineup when a bench single exists), 拟态物质 on a pair (never on 2 copies whose pool is out), 博士投影 (both qualities — neither takes an elite) on the strongest normal operator, 突变细胞 on the least valuable normal operator below 6阶 (deployed or benched — never an elite, never one of a merge pair, never one already carrying a cell; it comes back after every transformation, and a cell left in temp gets a hand slot made for it, `makeHandRoom` — on a bot's own seat by destroying the cheapest other hand item if no @@ -444,7 +444,8 @@ a promotion or `ctx.destroyPiece` returns) ends with the auto-merge (`acquireIte 装备时…自动合并"), so a player never holds two identical mergeable normal items (`test/match/feedback1b-items.test.js`), except an item gained while 休整期结束 is dispatching (`onPrepEnd`, including a grant nested under it): it is stowed (hand, else temp) and merges at the next prep's start, and nothing already equipped is taken off for the fight. Hand and temp both full still destroys it with 「整备区已满,获得的装备已销毁」. [ASSUMED] every such grant, not only 维多利亚's hammer (owner's decision 2026-10-04). A buy, an onPrepStart grant and a grant at any other time still merge at once (`test/match/feedback3-prep-end-item.test.js`). Built-ins (builtinMeta.js, overridable): 盟约之币 / 骑士储蓄罐 (random funds), 随身身份牌 (layers of the target's bonds), -紧急调度券 (take shop chess), 精打细算玩偶 (+funds each round), 简易通讯机 / 拟态物质 (same-bond chess), 见钱眼开玩偶 +紧急调度券 (take shop chess), 精打细算玩偶 (+funds each round), 简易通讯机 (same-bond chess), 拟态物质 (with 2 copies +owned the 3rd — nothing when the pool has none left, GitHub #207 —, else a same-bond chess), 见钱眼开玩偶 (+funds next round), 人事部文档 (cap 9), 博士投影 (elite now / at the next round start), 寻呼模块 / 信标 (pick-one offers; 信标 gifts the original chess — an elite stays an elite — to the teammate with the most members of its bonds next round, also when the sender was eliminated meanwhile; a failed grant waits for the next round start), 商业包装方案 (every diff --git a/docs/history/0.2.0.md b/docs/history/0.2.0.md index ca399954..a95a4029 100644 --- a/docs/history/0.2.0.md +++ b/docs/history/0.2.0.md @@ -9,7 +9,8 @@ later merges of feedback4 brought the rest of 0.1.4 in. The workstreams, each on after review with its golden scenarios named: R1–R6 (refactor, tooling), W5A (golden results), W5B / W5D / W5G / K1–K3 / W5N (补位), W5H / W5K / O1–O24 (自选编队 and its operator kits), W5I / W5S (languages, phases 1 and 2), W5C / W5E / W5F / W5J / W5L (fidelity, wave 1), WE1 / WE2 (the follow-ups the kit waves reported), W5M, W5R and the feedback4 -merges (the 0.1.4 ports), W5V (the owner's items of 2026-10-06), W5U (packaging) and W5T (the docs). Each rule was +merges (the 0.1.4 ports), W5V (the owner's items of 2026-10-06), W5U (packaging), W5T (the docs) and W5Y (round 18: +three GitHub issues and three community PRs). Each rule was checked against the official data and PRTS; anything no source backs is marked [ASSUMED]. The normative sections were rewritten with the streams: §0 (the DIY slots), §2 (the layout), §5.5 (the enemy damage frame, the block-count target rule), §5.6 (the trigger rules), §8.1 / §8.3 (`welcome.diyKitted`, `standIns`, `diy`, @@ -21,7 +22,7 @@ rules (GitHub #181, #187 / #170, follow-ups) → §25.7; WE2, data / match / UI what stays [ASSUMED] and the questions left to the owner → §25.10. The closing additions: the owner's decisions of 2026-10-06 (望's stock, skill triggers through summon areas, the 突袭 landing, GitHub #148) and the items the sources settled (泥岩's 闭锁; 忍冬, 深靛, 阿) → §25.11; release packaging and the asset manifest (the 自选 voices, the summons -with no Spine model) → §25.12. +with no Spine model) → §25.12; round 18 — 拟态物质 at the copy cap (GitHub #207) → §25.13. Decisions of the owner: 2026-10-04 — the server stays on plain `node:http` + `ws`, no web framework (§25.1); 2026-10-05 — the 补位 plan (§25.3); the 自选编队 rules, the collab operators out of the pool, ISW-A refused (§25.4); @@ -829,3 +830,26 @@ answers and checked the remaining items against PRTS and the client (W5V's commi (no `diy.excluded` operator keeps art; one lower-case module icon per type; no two manifest URLs differ only in case; orphans compared case-insensitively), `test/assets.test.js` (the empty-array rule). Docs: docs/DEPLOY.md §7, the README download section (full and lite), docs/ASSETS.md. + +### 25.13 Round 18: GitHub #207, #175, #169 and the community PRs #192, #209, #211 (W5Y; the owner's decisions of 2026-10-06) — `server/match/builtinMeta.js`, `server/match/bot.js itemTarget` + +The round-18 triage of the open issues and pull requests left three bugs and three community changes for 0.2.0; the +owner decided the open questions on 2026-10-06. Each item is one commit (W5Y) naming the golden scenarios it moved; a +ported PR credits its author there and here. + +#### 25.13.1 拟态物质 with 2 copies owned and the pool out gives nothing (GitHub #207) — `server/match/builtinMeta.js` (`use_equip_reward_char_chess`), `server/match/bot.js itemTarget` + +- **Seen** (GitHub #207, reproduced in the triage): an elite 溯光星源 plus 2 normal copies hold all 5 of the Ⅵ阶 pool's + copies (3 + 2); 拟态物质 on one of the normal copies gave a random same-bond operator (白面鸮 on that seed; 至简 is in + the same roll). Also in 0.1.4. +- **Official**: the item text 「若已拥有至少2名该初始干员,则再获得1名该初始干员;否则随机获得1名同盟约初始干员」 — the 否则 is + the case of fewer than 2 owned; research 06 §7: some effects fail when the copy cap is hit. +- **Cause**: with 2 owned the handler returned only when `grantChess(base)` gave a piece; the null of an empty pool fell + through to the same-bond roll. +- **Now**: with 2 normal copies owned the item asks for the 3rd and stops there — none left in the pool, nothing is + given (the item is still consumed); the same-bond operator is only for fewer than 2 owned. With copies left the 3rd + copy still merges into the elite. The bots' carrier choice skips 2 copies whose pool is out. +- Golden: unchanged (no corpus match equips it on a pair whose pool is out). Tests: `test/content/items.test.js` (the + report — an elite and 2 normal 溯光星源, normal and golden item: nothing granted, no other operator, consumed; 2 copies + with the pool drained: nothing; 2 copies with copies left: the elite), `test/match/bot-smarter.test.js` (no bot carrier + on a pair whose pool is out). Docs: docs/META.md (built-ins, the bots' items), research 04 / 06. diff --git a/docs/research/04-items.md b/docs/research/04-items.md index a93dd909..97805f6c 100644 --- a/docs/research/04-items.md +++ b/docs/research/04-items.md @@ -244,7 +244,7 @@ Param names below are the keys of `items[].normal.params` / `items[].golden.para params: `refresh_cnt=3.0, choice_cnt=1.0`; buff keys: `use_equip_reward_special_goods_char_chess` - **信标** (`chess_item_5_04_e_a`, TV, 3) - On equip: destroy BOTH the item and the target operator; special refresh of refresh_cnt(2) operators of the SAME tier as the target, take choice_cnt(1) free. Multiplayer: at next prep phase send 1 copy of the original operator to the teammate who has the most operators of that operator's bond(s) (tie -> random). Cannot target DIY(自编) operators. Never merges. params: `refresh_cnt=2.0, choice_cnt=1.0`; buff keys: `use_equip_recruit_new_char_and_give_char_to_player_most_bond` -- **拟态物质** (`chess_item_5_05_e_a`, TV, 4) - On equip: destroy item; if player owns >= 2 NORMAL copies of the target operator (field+hand), give 1 more normal copy (usually triggers 3->elite merge); else give 1 random normal operator sharing a bond with the target. Never merges. +- **拟态物质** (`chess_item_5_05_e_a`, TV, 4) - On equip: destroy item; if player owns >= 2 NORMAL copies of the target operator (field+hand), give 1 more normal copy (usually triggers 3->elite merge); else give 1 random normal operator sharing a bond with the target. Never merges. With >= 2 owned and no copy left in the pool (an elite holds 3 of a tier-6's 5) the remake gives nothing: the else is only for < 2 owned (GitHub #207, 0.2.0). params: `-`; buff keys: `use_equip_reward_char_chess` - **博士投影** (`chess_item_5_06_e_a`, TV, 5) - Normal: equip onto any normal operator (occupies a slot until next round start), at the start of the next round destroy item and promote the operator to elite(精锐/golden). Golden: on equip, destroy item and promote immediately. On promotion, equipped items return to hand. params: `-`; buff keys: `equip_round_start_upgrade_char` diff --git a/docs/research/06-multiplayer-ux.md b/docs/research/06-multiplayer-ux.md index 4775f3e7..051184f1 100644 --- a/docs/research/06-multiplayer-ux.md +++ b/docs/research/06-multiplayer-ux.md @@ -358,7 +358,7 @@ Visual [SHOT]: the banner is a dark strip with orange chevrons and orange-skull - A **normal** unit uses 1 copy and an **elite** uses 3. - **Counted:** units on every player's battlefield, bench and temp bench, from any source (buy, reward, strategy effect, 拟态/copy effects). - **Not counted:** cards currently displayed in any shop. -- When owned copies ≥ the cap, a refresh can't roll that name. Some effects fail when the cap is hit (杜遥夜's search; possibly 拟态物质 or 煌's sell effect). +- When owned copies ≥ the cap, a refresh can't roll that name. Some effects fail when the cap is hit (杜遥夜's search; possibly 拟态物质 or 煌's sell effect). [0.2.0: the remake's 拟态物质 gives nothing when 2 copies are owned and none is left — GitHub #207; its 否则 same-bond operator is only for fewer than 2 owned.] - Selling (1 fund per bench unit) returns copies. A player's **defeat or quit returns all their copies immediately**. - The server must own the pool (authoritative) and update it in real time for everyone. diff --git a/server/match/bot.js b/server/match/bot.js index 4dba80e3..1408cb70 100644 --- a/server/match/bot.js +++ b/server/match/bot.js @@ -1730,7 +1730,8 @@ function lastPerfect(m, ps) { * The carrier of an item (null = keep it), by what the item does (itemEffect): * 信标 (destroys its carrier for a pick of two chess of the same tier): the highest-tier bench single, else the * weakest normal deployed operator; - * 拟态物质 (a third copy of a pair, else a member of a bond): a pair (highest tier), else a focus member; + * 拟态物质 (a third copy of a pair, else a member of a bond): a pair (highest tier), else a focus member — never one + * of 2 copies whose pool is out (the item gives nothing then); * 博士投影 (promotes to elite; the normal one at the next round start, the golden one at once): the strongest normal * deployed operator; * 突变细胞 (the carrier becomes a random tier + 1 operator after the battle): cellTarget; @@ -1759,10 +1760,13 @@ export function itemTarget(m, ps, item, ctx = context(m, ps)) { return bench[0] || byVal(deployed.filter(normal), 1)[0] || null; } case 'use_equip_reward_char_chess': { + // with 2 copies owned and none left in the pool the item gives nothing (GitHub #207): such a pair is no carrier const counts = copyCounts(m, ps); - const pair = owned.filter((p) => normal(p) && (counts.get(gd.baseIdOf(p.id)) || 0) + 1 >= mergeNeed(m, gd.baseIdOf(p.id))) + const live = (base) => { const pool = typeof ps.poolOf === 'function' ? ps.poolOf(base) : m.pool; return pool.left(base) > 0 || !pool.has(base); }; + const dead = (p) => (counts.get(gd.baseIdOf(p.id)) || 0) >= 2 && !live(gd.baseIdOf(p.id)); + const pair = owned.filter((p) => normal(p) && !dead(p) && (counts.get(gd.baseIdOf(p.id)) || 0) + 1 >= mergeNeed(m, gd.baseIdOf(p.id))) .sort((a, b) => (chessRec(m, b.id, ps)?.tier || 0) - (chessRec(m, a.id, ps)?.tier || 0) || a.uid - b.uid); - return pair[0] || owned.slice().sort((a, b) => rel(b) - rel(a) || val(b) - val(a) || a.uid - b.uid)[0] || null; + return pair[0] || owned.filter((p) => !dead(p)).sort((a, b) => rel(b) - rel(a) || val(b) - val(a) || a.uid - b.uid)[0] || null; } // 博士投影: the normal one promotes at the next round start, the golden one (缪尔赛思's R1 item, item merges) at once; // both refuse an elite (builtinMeta: BAD_TARGET 'already elite') diff --git a/server/match/builtinMeta.js b/server/match/builtinMeta.js index 9cf37fe4..3ad1329b 100644 --- a/server/match/builtinMeta.js +++ b/server/match/builtinMeta.js @@ -12,7 +12,8 @@ // use_equip_gain_coin_when_next_round_start {count} 见钱眼开玩偶 +count funds next round // equip_destory_deployment_cnt_change {count} 人事部文档 deploy cap = count // use_equip_upgrade_char / equip_round_start_upgrade_char 博士投影 promote (golden: now, normal: next round) -// use_equip_reward_char_chess 拟态物质 3rd copy or a same-bond chess +// use_equip_reward_char_chess 拟态物质 3rd copy (none when the pool is +// out), or with < 2 a same-bond chess // use_equip_reward_special_goods_char_chess {refresh_cnt} 寻呼模块 offer N same-bond chess (≤ shop level) // use_equip_recruit_new_char_and_give_char_to_player_most_bond {refresh_cnt} 信标 destroy target, offer N same-tier // chess, gift the original (an elite stays @@ -130,11 +131,15 @@ const ITEM_HANDLERS = { ctx.promote(holder.uid); }, }, + // 拟态物质 「若已拥有至少2名该初始干员,则再获得1名该初始干员;否则随机获得1名同盟约初始干员」: the 否则 is the owned < 2 + // case only. With 2 copies owned and none left in the pool (an elite holds 3 of a Ⅵ阶's 5) the grant fails and the + // item gives nothing (research 06 §7: some effects fail at the copy cap) — it never falls back to a same-bond operator + // (GitHub #207). use_equip_reward_char_chess: { onEquip(ctx, ev) { const base = ctx.gd.baseIdOf(ev.target.id); const owned = [...ctx.board(), ...ctx.hand(), ...ctx.temp()].filter((p) => p && p.kind === 'chess' && !p.golden && ctx.gd.baseIdOf(p.id) === base).length; - if (owned >= 2) { if (ctx.grantChess(base)) return; } + if (owned >= 2) { ctx.grantChess(base); return; } const id = rollSameBond(ctx, ctx.pieceBonds(ev.target.uid), 6); if (id) ctx.grantChess(id); }, diff --git a/test/content/items.test.js b/test/content/items.test.js index 83d1f1d8..6762f225 100644 --- a/test/content/items.test.js +++ b/test/content/items.test.js @@ -944,6 +944,49 @@ test('拟态物质: owning 2 copies gives the 3rd (→ elite); otherwise a rando cover(A('5_05'), B('5_05')); }); +test('拟态物质 with 2 copies owned and none left in the pool gives nothing — never a same-bond operator; with copies left the 3rd still merges (GitHub #207)', () => { + // the report: an elite 溯光星源 (3 of the Ⅵ阶's 5 copies) and 2 normal ones — the pool is empty, the item text's 否则 + // (a random same-bond operator) is only for fewer than 2 owned + const cid = 'chess_char_6_16_a'; + const elite = DATA.chess[cid].goldenId; + const chessIn = (ps) => [...ps.board.values(), ...ps.hand, ...ps.temp].filter((p) => p && p.kind === 'chess').map((p) => p.id).sort(); + for (const item of [A('5_05'), B('5_05')]) { + const { m, ps, equip } = setup({ seed: 9 }); + give(m, ps, elite, 'hand'); + const t = give(m, ps, cid, 'hand'); + give(m, ps, cid, 'hand'); + assert.equal(m.pool.left(cid), 0, 'elite 3 + 2 normal = the pool cap 5'); + const before = chessIn(ps); + const got = spyGrants(ps); + const it = giveItem(m, ps, item); + assert.deepEqual(equip(it, t), OK, item); + assert.deepEqual(got, [], `${item}: nothing granted`); + assert.deepEqual(chessIn(ps), before, `${item}: no other operator, no second elite`); + assert.ok(!ps.find(it.uid), `${item}: consumed`); + } + // 2 normal copies, no elite, the pool drained by other players: nothing either + { + const { m, ps, equip } = setup({ seed: 9 }); + const t = give(m, ps, cid, 'hand'); + give(m, ps, cid, 'hand'); + m.pool.take(cid, m.pool.left(cid)); + const got = spyGrants(ps); + assert.deepEqual(equip(giveItem(m, ps, A('5_05')), t), OK); + assert.deepEqual(got, []); + assert.deepEqual(chessIn(ps), [cid, cid]); + } + // 2 normal copies with copies left: the 3rd merges into the elite + { + const { m, ps, equip } = setup({ seed: 9 }); + const t = give(m, ps, cid, 'hand'); + give(m, ps, cid, 'hand'); + assert.equal(m.pool.left(cid), 3); + assert.deepEqual(equip(giveItem(m, ps, A('5_05')), t), OK); + assert.deepEqual(chessIn(ps), [elite]); + assert.equal(m.pool.left(cid), 2, 'the elite holds 3 copies'); + } +}); + test('博士投影: golden promotes at once; normal stays equipped and promotes at the next round start', () => { const { h, m, ps, equip } = setup(); const cid = plain((c) => c.tier === 2)[0]; diff --git a/test/match/bot-smarter.test.js b/test/match/bot-smarter.test.js index 5cb2771e..7d243eec 100644 --- a/test/match/bot-smarter.test.js +++ b/test/match/bot-smarter.test.js @@ -108,6 +108,10 @@ test('items: 信标 on a bench single; both 博士投影 on a normal operator ev // 拟态物质 (a third copy when two are owned): the pair const mimic = itemTarget(m, ps, giveItem(m, ps, 'chess_item_5_05_e_a')); assert.ok(mimic && [pairA.uid, pairB.uid].includes(mimic.uid), '拟态物质 on the pair'); + // …but not once the pool has no third copy: the item would give nothing (GitHub #207) + m.pool.take(pairId, m.pool.left(pairId)); + const mimic2 = itemTarget(m, ps, giveItem(m, ps, 'chess_item_5_05_e_a')); + assert.ok(mimic2 && ![pairA.uid, pairB.uid].includes(mimic2.uid), '拟态物质 not on a pair whose pool is out'); m.dispose(); });