feedback1 final: merge the 突变细胞 bench rule (carrier destroyed, tile freed, the new operator gained into the 整备区; pointed out in PR #2)
Conflicts resolved keeping both sides: - effectsMeta.js header: the QA residual's "every [holder, item] pair, taken before the first item runs" and "an item moved during it runs at most once", with the cell branch's wording of what 突变细胞 does mid-walk. - DESIGN §6.6: the band pick that skips a strategy tied to a switched-off bond and the deployment of the operator a transformation gains / the bench shed that spares it. - DESIGN §21.25: PR #2's bench rule credited (the cell branch) plus the QA residual's snapshot-timing sentence. - PLAYING: the new 突变细胞 sentence with the residual's hammer sentence (revives, borrowed hammers, the lend window). - docs-consistency: both new tests (batch 6 QA residuals; 突变细胞 after the WA merge).
This commit is contained in:
commit
77eaeaa15f
23 files changed
+527
-214
No files matched your search
@@ -43,6 +43,7 @@
|
||||
|
||||
- 拉普兰德:获得她之后本回合的首次刷新也会叠叙拉古层数,每名拉普兰德各自计算。
|
||||
- 昆图斯的突变细胞不会用完消失:每场战斗后装备者换成高一阶的随机干员,突变细胞和其他装备退回整备区,可以再次配发。
|
||||
- 突变细胞换来的新干员和官方一样进入整备区,原来的格子空出来、可放置角色加 1,需要自己重新部署(人机和 AI 托管会照常部署它);凑齐同名干员时照常合成精锐(PR #2 指出)。
|
||||
- 标准模拟按官方禁用奥术等盟约,卡片和盟约详情标明「本局禁用」。
|
||||
- 变形同构体配转职装备时,盟约成员列表和干员卡片算上装备者(标「同构」)。
|
||||
- 带奥术法阵时,场上炎佑造成伤害也会使目标失去特殊能力 5 秒;带源石溶剂时炎佑每秒受到 60 点伤害。
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"chess_item_1_01_e_a":{"id":"chess_item_1_01_e_a","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":false,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":267,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_01_e_b","duration":-1,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm041","effectName":"维式重锤","desc":"攻击力+15%","descRaw":"攻击力<@ba.vup>+15%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.15},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.15,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","note":null,"rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_01_e_b":{"id":"chess_item_1_01_e_b","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":true,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":326,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm041","effectName":"维式重锤","desc":"攻击力+25%","descRaw":"攻击力<@ba.vup>+25%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.25},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.25,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","note":null,"rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_02_e_a":{"id":"chess_item_1_02_e_a","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":false,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":273,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_02_e_b","duration":-1,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm047","effectName":"坚守盾牌","desc":"防御力+20%","descRaw":"防御力<@ba.vup>+20%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.2},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.2,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","note":null,"rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_02_e_b":{"id":"chess_item_1_02_e_b","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":true,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":332,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm047","effectName":"坚守盾牌","desc":"防御力+35%","descRaw":"防御力<@ba.vup>+35%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.35},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.35,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","note":null,"rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_03_e_a":{"id":"chess_item_1_03_e_a","baseId":"chess_item_1_03_e_a","goldenId":"chess_item_1_03_e_b","isGolden":false,"trapId":"trap_1066_acarm066","iconId":"trap_1066_acarm066","identifier":292,"name":"盟约之币","itemType":"EQUIP","tier":1,"shopSortId":3,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_03_e_b","duration":-1,"giveBondId":null,"givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm066","effectName":"盟约之币","desc":"装备时销毁,并获得1资金","descRaw":"装备时销毁,并获得<@ba.vup>1</>资金","buffs":[{"key":"equip_destory_gain_random_coin","countType":"NONE","bb":{"min":1,"max":1},"bbStr":{}}],Line truncated
|
||||
{"chess_item_1_01_e_a":{"id":"chess_item_1_01_e_a","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":false,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":267,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_01_e_b","duration":-1,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm041","effectName":"维式重锤","desc":"攻击力+15%","descRaw":"攻击力<@ba.vup>+15%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.15},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.15,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","note":null,"rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_01_e_b":{"id":"chess_item_1_01_e_b","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":true,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":326,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm041","effectName":"维式重锤","desc":"攻击力+25%","descRaw":"攻击力<@ba.vup>+25%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.25},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.25,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","note":null,"rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_02_e_a":{"id":"chess_item_1_02_e_a","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":false,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":273,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_02_e_b","duration":-1,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm047","effectName":"坚守盾牌","desc":"防御力+20%","descRaw":"防御力<@ba.vup>+20%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.2},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.2,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","note":null,"rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_02_e_b":{"id":"chess_item_1_02_e_b","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":true,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":332,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm047","effectName":"坚守盾牌","desc":"防御力+35%","descRaw":"防御力<@ba.vup>+35%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.35},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.35,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","note":null,"rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_03_e_a":{"id":"chess_item_1_03_e_a","baseId":"chess_item_1_03_e_a","goldenId":"chess_item_1_03_e_b","isGolden":false,"trapId":"trap_1066_acarm066","iconId":"trap_1066_acarm066","identifier":292,"name":"盟约之币","itemType":"EQUIP","tier":1,"shopSortId":3,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_03_e_b","duration":-1,"giveBondId":null,"givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm066","effectName":"盟约之币","desc":"装备时销毁,并获得1资金","descRaw":"装备时销毁,并获得<@ba.vup>1</>资金","buffs":[{"key":"equip_destory_gain_random_coin","countType":"NONE","bb":{"min":1,"max":1},"bbStr":{}}],Line truncated
|
||||
+2
-2
@@ -287,8 +287,8 @@ everything needed to resolve a unit for `(chessId, skillIndex, moduleId)` (`simd
|
||||
| `requiresBondId` | `null` / `"yanShip"` | VI-tier bond signature items (research 04) |
|
||||
| `effectId`, `effectName`, `desc`, `descRaw` | `"eff_acarm041"`, …, `"攻击力+15%"` | |
|
||||
| `buffs[]`, `params` | `[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.15},"bbStr":{"key":"attr_common_global_buff"}}]` | |
|
||||
| `category`, `kind`, `family`, `implFormula` | `"STAT"`, `"passive"`, `null`, `"ATK% += atk …"` | research 04 (categories: STAT, ON_HIT, SURVIVAL, SP, ECONOMY, RECRUIT, BOND, BOND_GRANT, SET, BOND_SIGNATURE, MAGIC); `implFormula` is overridden by `tools/build-data.mjs ITEM_RULES` where research 04 was wrong (突变细胞) |
|
||||
| `note` | `null` / `"生效时原干员销毁,…突变细胞与其他装备退回整备区,可再次配发"` | a rule the official text leaves out (`ITEM_RULES`, sourced; normal and golden), shown under the effect in the item card — 突变细胞 is not consumed (player feedback after 0.1.0: PRTS 下半 记录 备注 "生效时,原干员销毁…", players re-inject it every round) |
|
||||
| `category`, `kind`, `family`, `implFormula` | `"STAT"`, `"passive"`, `null`, `"ATK% += atk …"` | research 04 (categories: STAT, ON_HIT, SURVIVAL, SP, ECONOMY, RECRUIT, BOND, BOND_GRANT, SET, BOND_SIGNATURE, MAGIC); `implFormula` is overridden by `tools/build-data.mjs ITEM_RULES` where research 04 was wrong (突变细胞; its research entry carries the correction too) |
|
||||
| `note` | `null` / `"生效时原干员销毁,突变细胞与其他装备退回整备区,可再次配发;随后获得一名高一阶的随机初始干员(最高6阶),进入整备区,需要重新部署"` | a rule the official text leaves out (`ITEM_RULES`, sourced; normal and golden), shown under the effect in the item card — 突变细胞 is not consumed (player feedback after 0.1.0: PRTS 下半 记录 备注 "生效时,原干员销毁…", players re-inject it every round) and its new operator joins the 整备区, the carrier's tile left empty (official footage; PR #2) |
|
||||
| `rangeGrid` | `[[0,0]]` | Arts area (画卷 `[[0,0],[0,1]]`) |
|
||||
| `flavor` | | |
|
||||
|
||||
|
||||
+16
-15
@@ -359,11 +359,11 @@ Intent handlers (validate → mutate → recompute bonds → push private view):
|
||||
|
||||
Summons (§20.1): an operator on the board sends its manually deployable summons to the hand — deploy-limit copies in one stack, no deploy slot (tokens.json `placeable`, `gamedata.placeableTokens`); moving or swapping it sends its placed summons back to their stack, withdrawing or selling it removes them. A range-bound summon (tokens.json `ownerRange`: 狼群, 流形) goes only on a tile of its owner's attack range (`PlayerState._legal` / `summonRange`); in a swap each piece is checked from the tile it ends on; an in-place re-orientation or promotion that leaves it outside sends it back to its stack with a toast (one still inside stays [ASSUMED], user-confirmed 2026-10-03), and a re-orientation whose lifted summon would have no room is refused with `HAND_FULL`; on the other paths it leaves the board and returns at the next round start (§21.3).
|
||||
|
||||
Equipment (research 04 §2 + addendum, 09 §1.2): 2 slots per chess; a third item on a full carrier replaces the equipped item `replaceUid` names (the replace dialog's pick, official `UseEquipUp.unloadInstId`; absent ⇒ the oldest; a uid not equipped on the target ⇒ `BAD_TARGET`, nothing changes) and the replaced item is destroyed. Equipped items are otherwise **locked**: `destroy` (销毁) accepts hand / temp items only (`BAD_TARGET 'equipped items are locked'`); equipment goes back to the hand only when its carrier merges, is sold, or is destroyed by 突变细胞's transformation (the cell itself returns too; PRTS 帮助 "在失去该干员(干员出售、销毁、合并等)…时自动卸除", §21.1) — a sale is refused with `HAND_FULL` when there is no room for it; returned equipment is stowed before the new piece's summon cards, and every path that hands the player an item — returned equipment included — ends with the auto-merge, so no player holds two identical mergeable normal items (§21.7).
|
||||
Equipment (research 04 §2 + addendum, 09 §1.2): 2 slots per chess; a third item on a full carrier replaces the equipped item `replaceUid` names (the replace dialog's pick, official `UseEquipUp.unloadInstId`; absent ⇒ the oldest; a uid not equipped on the target ⇒ `BAD_TARGET`, nothing changes) and the replaced item is destroyed. Equipped items are otherwise **locked**: `destroy` (销毁) accepts hand / temp items only (`BAD_TARGET 'equipped items are locked'`); equipment goes back to the hand only when its carrier merges, is sold, or is destroyed by 突变细胞's transformation (the cell itself returns too; PRTS 帮助 "在失去该干员(干员出售、销毁、合并等)…时自动卸除", §21.1) — a sale is refused with `HAND_FULL` when there is no room for it; returned equipment is stowed before the operator a transformation gains and before a merge elite's summon cards, and every path that hands the player an item — returned equipment included — ends with the auto-merge, so no player holds two identical mergeable normal items (§21.7).
|
||||
|
||||
Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income `min(3+r,12)`; prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; shared pool copies 12/14/18/16/8/5 (缪尔赛思 4) across all players; elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite onto the consumed deployed copy's tile, else to the hand (§20.11) + reward offer of 3 **different** free chess of tier `min(level+1, 6)` (copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); purchase completing a merge allowed with full hand; full hand blocks buy; overflow to temp; temp blocks ready).
|
||||
|
||||
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants, battle-result rewards, equipment returned by a merged / sold carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
||||
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants — the operator a 突变细胞 transformation gains included, §21.1 —, battle-result rewards, equipment returned by a merged / sold / transformed carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
||||
|
||||
### 6.3 Bonds & layers (bondsMeta.js)
|
||||
|
||||
@@ -384,7 +384,7 @@ Per match: pick stage (weighted among the mode's `config.modes[m].stages`, weigh
|
||||
|
||||
### 6.6 AI players (bot.js)
|
||||
|
||||
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP; never one built around a bond the mode switches off — `gd.bandBondIds`, §21.26); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within `PlayerState.summonRange`; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (`botPrepBeginSteps`, 25–70 ms late in a 4-bot match), the layout rehearsal and `botPrepEndSteps` — runs in wall-clock slices of `botSliceMs` (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time).
|
||||
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP; never one built around a bond the mode switches off — `gd.bandBondIds`, §21.26); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within `PlayerState.summonRange`; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too; the buy loop's bench shed never sells a piece gained since the bot's last prep, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (`botPrepBeginSteps`, 25–70 ms late in a 4-bot match), the layout rehearsal and `botPrepEndSteps` — runs in wall-clock slices of `botSliceMs` (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time).
|
||||
|
||||
---
|
||||
|
||||
@@ -968,7 +968,7 @@ Settled by the user ("保持固定血量"): the co-op leader pool does not shrin
|
||||
|
||||
Settled by the user (2026-10-02, after v2.5.2: "共享阵地按你说的规则就行"): on a shared field (联防, Final Assault / Hidden Core) the bond strip follows the player of the half the ‹ › pill points at; on 全景 or an empty half it shows your own bonds when you fight there, else the teammate picked with 前往查看 or the field's first player — never your own (§20.15); rule: `public/js/ui/watchBonds.js bondOwnerId`.
|
||||
|
||||
Still [ASSUMED] (each in its section): the class rows covering every MANUAL skill (§20.2); skill-summon redeploys free and never while the owner is away (§20.1); the 1-HP pool floor and the server-order verdict (§20.5); a short promotion tier topping up from below and “神秘顾客” keeping the band bounties (§20.6); the 弹道 column for unclassified pushes and the other displacement details (§20.3); 枯朽战车's skill firing on its blocker at 100 % and content enemy skills keeping a point radius (§20.4); the gauge's refill colour and placement (§20.8); the 机变 card's phone sizes (§20.7); the elite's tile among several deployed copies (the first in deploy order), a transformed carrier's tile and the elite's fresh summon stack (§20.11); "strongest" rather than "latest" for same-named debuffs, which "最大生命值" the 胄 drone link reads (the pool), the 剑 / 锤 dive hits passing on, the springs' split and the solo pool × 0.25 (§20.10); the scene server clamping prep gains like the client, a cancelled hit showing nothing, a passed-on loss being one hit and the order blocks → 限伤 → HP shields (§20.12); 胄's unread `max_hp` 0.5, the untargetable 初始模式 and a replacement copy inheriting only the HP (§20.13); talent debuffs keyed per source unit (two copies of one operator each apply theirs, §20.14).
|
||||
Still [ASSUMED] (each in its section): the class rows covering every MANUAL skill (§20.2); skill-summon redeploys free and never while the owner is away (§20.1); the 1-HP pool floor and the server-order verdict (§20.5); a short promotion tier topping up from below and “神秘顾客” keeping the band bounties (§20.6); the 弹道 column for unclassified pushes and the other displacement details (§20.3); 枯朽战车's skill firing on its blocker at 100 % and content enemy skills keeping a point radius (§20.4); the gauge's refill colour and placement (§20.8); the 机变 card's phone sizes (§20.7); the elite's tile among several deployed copies (the first in deploy order) and the elite's fresh summon stack (§20.11; a transformed carrier's tile no longer counts, §21.1); "strongest" rather than "latest" for same-named debuffs, which "最大生命值" the 胄 drone link reads (the pool), the 剑 / 锤 dive hits passing on, the springs' split and the solo pool × 0.25 (§20.10); the scene server clamping prep gains like the client, a cancelled hit showing nothing, a passed-on loss being one hit and the order blocks → 限伤 → HP shields (§20.12); 胄's unread `max_hp` 0.5, the untargetable 初始模式 and a replacement copy inheriting only the HP (§20.13); talent debuffs keyed per source unit (two copies of one operator each apply theirs, §20.14).
|
||||
|
||||
### 20.10 Final Assault leader HP and damage (user report after playtest #6, 2026-10-01) — `server/sim/constants.js DIRECT_BONUS_STACKING`, `content/support directMods`, `sim/Battle.js applyStrongest / loseHp sourceless`, content bonds / bands / items / choices / garrisons / kits tier4, `content/bosses.js PART_TRANSFER / DRONE_LINK_BASE`, `match/gamedata.js bossPoolHp / bossPoolShare`, `match/finalAssault.js`, `data/config.json bossHpScale`
|
||||
|
||||
@@ -992,9 +992,9 @@ Still [ASSUMED] (each in its section): the class rows covering every MANUAL skil
|
||||
- **Seen** (the user, first-hand, after playtest #6): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置".
|
||||
- **Cause**: research 01 A1 row 7 read PRTS 帮助 as "1 elite goes to the hand, not to a board tile" (marked VERIFIED, repeated in `01-core-data.json` promotion.eliteGoesTo), so `_mergeChess` put every elite in the hand and used a consumed copy's tile only when the hand and temp were both full.
|
||||
- **Official** (PRTS 卫戍协议/帮助): §干员的获得与精锐化 "…发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; tip "干员晋级后已配发装备会回收至整备区,需要重新配置"; §战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区", deploy order "从上到下>从左到右". The text does not say which tile wins among several deployed copies.
|
||||
- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — row desc, then col asc, the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A deployed piece transformed into the completing copy (突变细胞, `transformChess` `fromKey` / `fromDir`) counts as deployed with its own tile [ASSUMED]. A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
|
||||
- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — row desc, then col asc, the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A 突变细胞 transformation destroys its carrier before the gain, so the carrier's freed tile is no copy's (§21.1 — official footage; the 0.1.1 WA rule that counted it, `transformChess` `fromKey` / `fromDir` [ASSUMED], is withdrawn). A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
|
||||
- **Now (client)**: `gameLogic.mergeTarget(priv, chessId)` mirrors `mergeTile`; a shop or reward card armed for a merge-completing buy lights the target tile in gold (`ShopBar onArm` → `screens/game.js` highlight group `mergeTile`); the 可晋升 tag says 精锐干员将出现在作战区原位置 or 精锐干员将进入整备区; an elite card never merges (`completesChessMerge` refuses `isGolden`, `mergeProgress` reports 0 copies: no pips, tag or tile); `render/promote.js promotionsOf` recognises a merge in setPrep and the elite plays `fx.promote` (gold pillar, shockwave, hex ring, streaks from the other copies) instead of `fx.deploy` (`view.debug.promotions`).
|
||||
- **Checks**: `audit.js` checks every merge — the tile is `mergeTile` of the deployed copies legal for the elite, with that copy's facing; the deploy count did not grow (a carrier `transformChess` took off the board first still counts); otherwise the elite is not on the board, except the no-room fallback. `tools/matchrun --check` sweeps are clean.
|
||||
- **Checks**: `audit.js` checks every merge — the tile is `mergeTile` of the deployed copies legal for the elite, with that copy's facing; the deploy count did not grow; otherwise the elite is not on the board, except the no-room fallback. `tools/matchrun --check` sweeps are clean.
|
||||
- Normative lines rewritten: §2 (`promote.js`), §6.2 (the economy line and the temp-overflow paragraph), §9 (the promotion cue).
|
||||
- Tests: `test/match/playtest6-elite-to-board.test.js`, `test/match/merge.test.js`, `hardening.test.js`, `facing.test.js`, `test/ui/playtest6-elite.test.js`, `test/render/playtest6-promote.test.js`, `test/docs-consistency.test.js`; opt-in `SP_E2E=1 node --test test/ui/playtest6-elite.e2e.test.js` (real server and prep: the armed card lights the deployed copy's tile, the elite stands there after the buy, 剩余可放置角色 unchanged).
|
||||
|
||||
@@ -1115,20 +1115,21 @@ In SETTLE it is the field the last battle left on screen. In prep without watchi
|
||||
|
||||
## 21. Player feedback after 0.1.0 (v0.1.1)
|
||||
|
||||
Thirty-two reports after the public 0.1.0 release (2026-10-02), relayed by the user in six batches; most were not tested by the user. Verdicts: **real** (fixed), **partly** (the rule was right, what a player sees was wrong or a related part was wrong — fixed), **not real** (not reproduced; tests lock the rule). Where each is handled: batch 1 — #1 拉普兰德's "获得该干员后该回合的首次刷新" should stack 叙拉古 → §21.1 (real); #2 late bounty enemies offered in early bounty drafts → §21.2 (real); #3 the water pool map: operators deployable in the water, 突袭 operators land in it → §21.3 (real); #4 昆图斯's 突变细胞 gone after one use → §21.1 (real: not consumed); #5 转译基底 does not transform → §21.4 (real); #6 凯瑟琳's three upgrade items shown wrong → §21.2 (real); #7 鸭爵's 圆仔 blockable, audit the other 鸭爵 enemies → §21.5 (real); #8 深池逐火战士's 怨恨的余烬 cannot be killed → §21.4 (real); #9 tacticians (伺夜) can place the tactical point outside their attack range → §21.3 (real); #10 bots too dumb, smarter without more CPU → §21.6 (real). Batch 2 — B1 奥术 not working → §21.7 (partly: the bond works, 标准 switches it off); B2 源石溶剂 white + white → gold, then an extra white → §21.7 (not real); B3 维娜·维多利亚 S3 summons one 黄金盟誓 → §21.8 (real); B4 玛恩纳's skill deals no damage → §21.8 (real, on flying waves); B5 薄绿 can pull flying enemies → §21.9 (real). Batch 3 — C1 the hidden leader's "造成50%伤害" broadcast before the fight → §21.10 (real); C2 变形同构体 + class item: the bond member count excludes the wearer → §21.11 (partly: display only). Batch 4 — D1 信仰搅拌机: 源石溶剂's HP drain should trigger S3 → §21.12 (real); D2 野鬃's skill sometimes pushes the wrong way → §21.13 (real); D3 special 源石虫 all drawn as the plain one → §21.14 (partly); D4 暴鸰's bombs do not drop properly → §21.14 (partly); D5 the originium-floor map's lower route should cut diagonally from row 4 to row 3 → §21.15 (real). Batch 5 — E1 烛煌 S3's attack range does not change → §21.16 (partly: the card); E2 medics that block a roaming leader stop healing and attack it → §21.17 (not real); E3 卡涅利安's attack is not a true AoE → §21.18 (real). Batch 6 — F1 坚固维式重锤's lock does not work → §21.21 (partly: the lock works; it was once per battle, now once per deployment as the user settled, and an M3茧甲 equipped first took the first lethal hit); F2 a free 谢拉格 uses up 休露丝's discount → §21.21 (not real); F3 蒂比 still hit after taking off → §21.22 (real); F4 深巡 on a fenced tile does not attack → §21.23 (not real; a fence blocking fault found on the way is fixed); F5 突袭 operators land on a knocked-out operator's body → §21.24 (real). Two hardening fixes are credited to pull requests (PR #2, PR #7) → §21.25.
|
||||
Thirty-two reports after the public 0.1.0 release (2026-10-02), relayed by the user in six batches; most were not tested by the user. Verdicts: **real** (fixed), **partly** (the rule was right, what a player sees was wrong or a related part was wrong — fixed), **not real** (not reproduced; tests lock the rule). Where each is handled: batch 1 — #1 拉普兰德's "获得该干员后该回合的首次刷新" should stack 叙拉古 → §21.1 (real); #2 late bounty enemies offered in early bounty drafts → §21.2 (real); #3 the water pool map: operators deployable in the water, 突袭 operators land in it → §21.3 (real); #4 昆图斯's 突变细胞 gone after one use → §21.1 (real: not consumed; its new operator joins the bench, not the carrier's tile — corrected after the merge, pointed out in PR #2); #5 转译基底 does not transform → §21.4 (real); #6 凯瑟琳's three upgrade items shown wrong → §21.2 (real); #7 鸭爵's 圆仔 blockable, audit the other 鸭爵 enemies → §21.5 (real); #8 深池逐火战士's 怨恨的余烬 cannot be killed → §21.4 (real); #9 tacticians (伺夜) can place the tactical point outside their attack range → §21.3 (real); #10 bots too dumb, smarter without more CPU → §21.6 (real). Batch 2 — B1 奥术 not working → §21.7 (partly: the bond works, 标准 switches it off); B2 源石溶剂 white + white → gold, then an extra white → §21.7 (not real); B3 维娜·维多利亚 S3 summons one 黄金盟誓 → §21.8 (real); B4 玛恩纳's skill deals no damage → §21.8 (real, on flying waves); B5 薄绿 can pull flying enemies → §21.9 (real). Batch 3 — C1 the hidden leader's "造成50%伤害" broadcast before the fight → §21.10 (real); C2 变形同构体 + class item: the bond member count excludes the wearer → §21.11 (partly: display only). Batch 4 — D1 信仰搅拌机: 源石溶剂's HP drain should trigger S3 → §21.12 (real); D2 野鬃's skill sometimes pushes the wrong way → §21.13 (real); D3 special 源石虫 all drawn as the plain one → §21.14 (partly); D4 暴鸰's bombs do not drop properly → §21.14 (partly); D5 the originium-floor map's lower route should cut diagonally from row 4 to row 3 → §21.15 (real). Batch 5 — E1 烛煌 S3's attack range does not change → §21.16 (partly: the card); E2 medics that block a roaming leader stop healing and attack it → §21.17 (not real); E3 卡涅利安's attack is not a true AoE → §21.18 (real). Batch 6 — F1 坚固维式重锤's lock does not work → §21.21 (partly: the lock works; it was once per battle, now once per deployment as the user settled, and an M3茧甲 equipped first took the first lethal hit); F2 a free 谢拉格 uses up 休露丝's discount → §21.21 (not real); F3 蒂比 still hit after taking off → §21.22 (real); F4 深巡 on a fenced tile does not attack → §21.23 (not real; a fence blocking fault found on the way is fixed); F5 突袭 operators land on a knocked-out operator's body → §21.24 (real). Two hardening fixes are credited to pull requests (PR #2, PR #7) → §21.25.
|
||||
|
||||
Eighteen workstreams (WA–WR, each on its own branch from master 7f13473) were merged into `feedback1` in the order WB, WA, WE, WD, WC, WI, WM, WN, WO, WL, WG, WH, WJ, WK, WP, WR, WQ, WF, then WB's follow-up from 66 screenshots of 22 matches (bounty lists, the 机密商店 and 战术决策 with repeats) after QA, then batch 6's four in the order WS, WU, WT (on WI's branch), WV (on WC's); data/*.json was rebuilt by `tools/build-data.mjs --offline` after every builder merge, never hand-merged. The integration fixed a bug already in 0.1.0 (a 联防 battle timed out during 隐德来希 S3 reported its 心烛 as leaks, so the server refused an honest client result — candles carry `mem.noLeak`), the last review of the transform workstream (form fx were still dropped by the render engine), every cross-workstream note worth doing with a primary source, and reconciled the bot with the merged rules — §21.19. §21.20 lists the settled decisions and what stays [ASSUMED]; batch 6 and the pull-request fixes follow it (§21.21–§21.25), with what QA of that integration found and the integration fixed: a lent 坚固维式重锤 and in-place revives against the once-per-deployment lock (§21.21), 卢西恩 / 锏 casting for an airborne 蒂比 alone (§21.22), rule 3 for a summon that is gone (§21.24) and when the dispatcher takes its item snapshot (§21.25). Normative lines rewritten with it: §2 (`grid.js`), §3 (the effective `buildable`, the 深水区, range-bound summons, 攻击距离 vs skill ranges), §5.2 (`liveRangeGrid`, `form`), §5.3 (stealth vs area damage, the 奥术 refresh), §5.4 (`kill` / `death`, `foesInRadius`, `loseHp` vs damage, 静态刚体), §5.5 (healers that block, `rangeAoe`, armed enemies, results), §5.6 (TAKE_DAMAGE, `noRangeExtend` / `showOwnRange`, the profession splash radii), §6.2 (rewardOffer, piece `meta.round`, summons in range, returned equipment), §6.3 (变形同构体), §6.6 (the bot), §7 (bounty drafts, 鸭爵, knock-out forms), §8.2 / §8.3 (fx `form`, `m.ticker`, `range`, UnitInfo `items` / `form`, the stealth bit, rewardOffer), §9 (forms, alias tint), §10 (offers, ticker, 本局禁用), §13 (`spine/enemy`), §14 (ground pathing, boss tickers, the runner's form fx), §16, §17.3, §18.5, §18.6 #10, §19.1, §19.7, §20.1, §20.3, §20.6, §20.9; with batch 6: §3 (fenced tiles), §5.3 (`liftoff`), §5.4 (`fatal`, `ignoreSelect`, `redeploy`), §5.5 (bodies and redeploys, blocking, enemy AI), §6.4 (`onBuy`, the item snapshot), §8.2 (`down` row, col), §18.3, §19.7.
|
||||
|
||||
### 21.1 拉普兰德's first refresh; 突变细胞 is not consumed (#1, #4) — `sim/content/garrisons/meta.js SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT`, `match/PlayerState.js pieceRoundCount / bumpPieceRoundCount / _mergeChess / transformChess`, `match/effectsMeta.js ctx.pieceCounter / incPieceCounter / transform`, `match/builtinMeta.js char_chess_transformation_equip`, `match/bot.js cellTarget / makeHandRoom`, `tools/build-data.mjs ITEM_RULES`, `ui/detailPanel.js ItemDetail`
|
||||
### 21.1 拉普兰德's first refresh; 突变细胞 is not consumed and its new operator joins the bench (#1, #4) — `sim/content/garrisons/meta.js SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT`, `match/PlayerState.js pieceRoundCount / bumpPieceRoundCount / _mergeChess / transformChess / acquireChess`, `match/effectsMeta.js ctx.pieceCounter / incPieceCounter / transform`, `match/builtinMeta.js char_chess_transformation_equip`, `match/audit.js`, `match/bot.js cellTarget / makeHandRoom`, `tools/build-data.mjs ITEM_RULES`, `ui/detailPanel.js ItemDetail`
|
||||
|
||||
- **#1 seen**: with 普罗旺斯 + 德克萨斯 deployed — refresh, buy 拉普兰德, deploy her (3 叙拉古), refresh again: no 叙拉古 layers. Players: officially "获得该干员后该回合的首次刷新" also stacks. **Cause**: the handler compared the PLAYER's round refresh count with `refresh_cnt` 1.
|
||||
- **#1 official**: garrison_123_a/_b = SERVER_REFRESH_SHOP / SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT, bb `refresh_cnt` 1, layer 4 / 8, "若为本回合首次主动刷新…此干员在整备区时也有效" (activity_table act2autochess). Prep effects run on the scene server (research 11 §1.2); PRTS has no timing note. Text and report agree when the count belongs to the trait instance; the "本回合每刷新过1次" multipliers (阿罗玛 122, 安洁莉娜 147) fire on other events and keep the player's count.
|
||||
- **#1 now**: each copy counts the manual refreshes it sees this round, on the board or in the hand (`piece.meta.round`, module-prefixed key `garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes`), and fires on the `refresh_cnt`-th. A new piece starts at 0; a move or an in-place promotion keeps the count; a re-triggered trait neither fires nor counts; temp pieces do not count.
|
||||
- **#4 seen**: "昆图斯策略给的突变细胞用完就没了". **Cause**: research 04's [ASSUMED] "item consumed"; the built-in and a content override both destroyed the cell.
|
||||
- **#4 official**: the text "战斗结束后,装备者替换为高一阶的随机干员" never says 销毁 (every consumable's text does; data EQUIP, upgradeNum 100); PRTS 下半/PRTS盟约记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)"; PRTS 卫戍协议/帮助 "佩戴的装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除"; community guides re-inject it every round (bilibili cv47000418, cv48003106; cg.163.com "这个道具可以无限使用").
|
||||
- **#4 now**: after every battle the carrier becomes a random NORMAL operator one tier higher (max 6; an elite carrier too) on its tile when legal; its equipment, the cell included, returns to the hand (overflow temp; with both full it stays on the new operator) — before the new operator's or elite's summon cards (as `_mergeChess` already does), so a 狼群 card never pushes the cell into temp. One implementation (the built-in); the item card states the rule (items.json `note`). Bots inject their least valuable single normal operator below 6阶 (`cellTarget`: never an elite, a merge pair or a carrier of a cell) and make a hand slot for a cell left in temp (`makeHandRoom`).
|
||||
- [ASSUMED]: an elite merged this round keeps its copies' highest count (no second firing that round); a copy re-bought after a sale fires on its own first refresh ("获得该干员后"; each extra +4 costs 3 + 1 − 1 = 3 funds); the carrier transforms wherever it stands; a 6阶 carrier becomes another 6阶.
|
||||
- Tests: `test/match/feedback1-meta.test.js`, `test/ui/feedback1-cell.test.js`; `items.test.js`, `merge.test.js` updated.
|
||||
- **#4 where the new operator goes** (corrected after the WA merge; pointed out in the closed PR #2, whose playtest claim the footage confirms): official gameplay footage — bilibili BV1vzyVBuEN9 (上半, ≈ 8:24) and BV1Qkw1zMEoR (下半, ≈ 7:25) — shows the next prep with the carrier's tile **empty**, 剩余可放置角色 one higher and the 整备区 holding the returned equipment and the new tier+1 operator, which the player then deploys by hand. It reads the PRTS 备注 as written — a destroy followed by a gain ("原干员销毁,获得一名…") — and PRTS 帮助 sends whatever a player recruits or gains to the 手牌区 (= 整备区; "被发送至手牌区的物资优先从右到左填充空位"). WA had kept the new operator on the carrier's tile when legal [ASSUMED] and counted that tile for a merge the transformation completed [ASSUMED]; both are withdrawn (PR #2 itself still let the merge use the carrier's tile — not adopted).
|
||||
- **#4 now**: after every battle the carrier is destroyed wherever it stands (board, hand or temp — a board tile is freed and the deploy count drops; its summons are removed, its copies return to the pool). Its equipment, the cell included, returns to the hand **first** (overflow temp; auto-merging like any gain). Then a random NORMAL operator one tier higher (max 6; an elite carrier too) is gained like any gained operator (`PlayerState.transformChess` → `acquireChess`, onGain source `'transform'`): the hand (right → left), overflow temp, and with both full the no-room rule of every gain (back to the pool, toast 整备区已满,获得的干员已返还; an item that found no slot takes one the gain freed, else stays on the gained operator up to 2 slots, else it is destroyed with a log warning, as in a merge). Like any operator on the bench it brings no summon card until it is deployed. A gain that completes a merge merges like any gained copy (§20.11): the elite on a consumed **deployed copy's** tile, else the hand — the carrier's freed tile is no copy's (`_mergeChess` lost its `fromKey` / `fromDir` options, `audit.js` its exception for them); the returned cell is stowed before the elite's summon card. Per-piece state stays with the returned items (商业包装方案's sell count, keyed by the item). One implementation (the built-in); the item card states the rule (items.json `note`). Bots inject their least valuable single normal operator below 6阶 (`cellTarget`: never an elite, a merge pair or a carrier of a cell), make a hand slot for a cell left in temp (`makeHandRoom`), and — AI 托管 too — deploy the gained operator in the next prep's lineup step like any owned unit (a real co-op round in the tests: SETTLE → the next prep). The buy loop's pre-emptive bench shed, which sells the weakest bench single by piece value while the bench is crowded, no longer sells a piece gained since the bot's last prep ended (`bot.js rememberOwned`): on the new rule it had sold 44 of 222 gained operators in 20 solo 绝境 matches on 昆图斯 before any placement step saw them (now none; a gained operator stays off the board only when the board is full). Measured (`tools/botbench.mjs`, decisions only): strategy 昆图斯 (`--band band_quintus`, seeds 201–220) 绝境 8 / 20 wins on the WA build → 7 with the bench rule alone → 11 with the shed fix (LP left 16.8 → 13.3 → 17.1), 险境 15 → 16 → 17 (16.3 → 16.2 → 17.6); the bots' own strategies, bench rule alone → with the fix: solo 绝境 (seeds 1–12) 8 → 8 / 12, solo 险境 (seeds 13–36) 19 → 19 / 24, co-op 绝境 4 AI (seeds 1–36) 9 → 8 / 36 (LP left within 0.6) — noise; the shed's cost is a set of the owned uids per prep. The client needs no special case: `render/app.js setPrep` drops the carrier's view and draws the new piece on its bench slot (checked in headless Chrome: the tile empty, the operator and the cell on the bench, no promotion cue, no console error).
|
||||
- [ASSUMED]: an elite merged this round keeps its copies' highest count (no second firing that round); a copy re-bought after a sale fires on its own first refresh ("获得该干员后"; each extra +4 costs 3 + 1 − 1 = 3 funds); a bench carrier (hand or temp) transforms too (the footage shows deployed carriers); a 6阶 carrier becomes another 6阶; with the hand and temp both full the gained operator and an item without a slot follow the rules of every gain (no source shows that case).
|
||||
- Tests: `test/match/feedback1-meta.test.js` (a deployed and a bench carrier, the hand full → temp, the hand and temp full, a merge with and without a deployed copy, 商业包装方案's count, bots and AI 托管 redeploying, a real solo 绝境 昆图斯 bot match: no gained operator shed in its first prep, none waiting on the bench beside a free deployment), `test/ui/feedback1-cell.test.js`; `items.test.js`, `merge.test.js`, `facing.test.js`, `playtest6-elite-to-board.test.js` updated.
|
||||
|
||||
### 21.2 Bounty drafts by the official round structures; special offers shown as what they hold (#2, #6) — `tools/build-data.mjs BOUNTY_INITIAL_SETS / BOUNTY_BOSS_GROUPS / BOUNTY_HUNTER_GROUPS / SHOP_DRAFT / TACTIC_DRAFT`, `match/choices.js bountyDraftCards / shopDraftCards / tacticDraftCards`, choices.json `bountyDrafts` / `shopDraft` / `tacticDraft` / `draftPool` / schedule `bountyDraft`, `test/fixtures/official-bounty-drafts.json`; `match/PlayerState.js pushRewardOffer / pushItemOffer`, `match/effectsMeta.js offerLabel`, `ui/gameLogic.js offerHeader`, `ui/shopBar.js RewardCards`, `ui/rewardOverlay.js`
|
||||
|
||||
@@ -1182,7 +1183,7 @@ Eighteen workstreams (WA–WR, each on its own branch from master 7f13473) were
|
||||
- **Measured on the 0.1.0 bot** (`tools/botbench.mjs`, same seeds on two builds): 0–2 refreshes a prep and side-grade singles sold again for 1 (≈ 2 merges a match, ≈ 2 elites at R13); a level-up could spend the funds a third copy needed; the bounty score ignored the enemy (co-op 绝境: 155 of ≈ 310 bounty enemies leaked); 信标 (destroys its carrier) went on the best operator; DPS ignored DEF / RES; the focus bond flip-flopped.
|
||||
- **Now** (decisions only, no rule or balance change): refresh vs buy by the shop odds of completing held pairs (a full board buys only merge progress or a lineup upgrade worth more than the refreshes its price would pay for; an elite's value includes the merge's free pick); ≤ 4 pairs on the bench, merges before level-ups, a freeze for an unaffordable third copy; one committed focus core bond + a second bond, a teammate's main core bond read from its public bond strip (humans too) counting against it; dealers valued by DPS against the round's DEF / RES; 坎诺特 keeps its interest capital (5) back; bounty picks by expected payout − expected leaks × (2 + 20 / LP) with the kill chance from the exposure model — an expected-value pick (co-op leaks are bound by the drafts, §21.2); items by effect (信标 on a bench single, 拟态物质 on a pair, both 博士投影 on the strongest normal operator, 突变细胞 per §21.1, bond items on a focus member, survival items on blockers); 教鞭 / “神秘顾客” used after a perfect battle and kept otherwise — a bot drops a kept one only for a hand slot, a human's seat under AI 托管 never; a tactician's 援军 inside `PlayerState.summonRange`.
|
||||
- **Result** (seeds 1–40, rehearsal 3, on the WF branch): 独立 绝境 19 → 26 / 40 wins (LP left 14.3 → 16.6), 同盟 绝境 (4 AI) 5 → 12 / 40 (5.1 → 12.7), 标准 unchanged; merges per bot ≈ 2 → 3–4; forced strategies (solo 绝境, seeds 201–220): 坎诺特 9 → 14, 昆图斯 9 → 11, 杜宾 8 → 9. **Decision time unchanged**: heuristics CPU p50 / p95 31 / 74 → 28 / 70 ms solo and 17 / 39 → 17 / 38 ms co-op; the whole prep with its rehearsal 89 / 205 and 83 / 162 ms wall.
|
||||
- **Reconciled with the merged rules** (§21.19): the server's legality (deploy map without the 深水区, `summonRange`), WA's cell rule, WB's draft structures (it scores whatever is offered), 本局禁用 bonds (never planned), the range a unit is deployed with, and the AoE of §21.18 in its DPS value. On the merged build, seeds 1–40: solo 绝境 27 → 27 wins, co-op 绝境 9 → 11 (within the noise); decision time stays (heuristics CPU 24 / 57 ms solo, 16 / 36 ms co-op; the whole prep 78 / 153 and 73 / 151 ms wall; BALANCE §5). After the QA: WB's 22-match bounty lists (a giant in every R11 bounty list, 14 of 22 R11s a 悬赏决策) take the same bot to 22 / 40 and 9 / 40 — a rule change; the bench-shed fix (never sell a piece of this prep, §21.19) then 24 / 40 and 10 / 40. **The 0.1.1 build against 0.1.0 (19 / 40, 5 / 40): 24 / 40 solo 绝境, 10 / 40 co-op 绝境 (4 AI)** — part of it is the rules: 0.1.0's bot on the 0.1.1 rules wins 23 / 40 and 7 / 40 (LP left 14.6 / 5.2 against 17.4 / 13.1, merges per bot 2.2 / 2.0 against 3.8 / 3.1); same-prep buy → sell in co-op 绝境 (2 AI 托管 + 2 bots, seeds 21–26) 29 → 3; decision time unchanged (one thread, seeds 1–3: the whole prep 81 / 187 → 81 / 184 ms wall solo, 71 / 153 → 71 / 129 ms co-op; heuristics CPU 36 / 88 → 35 / 86 and 16 / 40 → 16 / 40 ms).
|
||||
- **Reconciled with the merged rules** (§21.19): the server's legality (deploy map without the 深水区, `summonRange`), WA's cell rule (and its destroy-then-gain correction: the gained operator is deployed by the lineup step, the bench shed spares a piece gained since the last prep — §21.1), WB's draft structures (it scores whatever is offered), 本局禁用 bonds (never planned), the range a unit is deployed with, and the AoE of §21.18 in its DPS value. On the merged build, seeds 1–40: solo 绝境 27 → 27 wins, co-op 绝境 9 → 11 (within the noise); decision time stays (heuristics CPU 24 / 57 ms solo, 16 / 36 ms co-op; the whole prep 78 / 153 and 73 / 151 ms wall; BALANCE §5). After the QA: WB's 22-match bounty lists (a giant in every R11 bounty list, 14 of 22 R11s a 悬赏决策) take the same bot to 22 / 40 and 9 / 40 — a rule change; the bench-shed fix (never sell a piece of this prep, §21.19) then 24 / 40 and 10 / 40. **The 0.1.1 build against 0.1.0 (19 / 40, 5 / 40): 24 / 40 solo 绝境, 10 / 40 co-op 绝境 (4 AI)** — part of it is the rules: 0.1.0's bot on the 0.1.1 rules wins 23 / 40 and 7 / 40 (LP left 14.6 / 5.2 against 17.4 / 13.1, merges per bot 2.2 / 2.0 against 3.8 / 3.1); same-prep buy → sell in co-op 绝境 (2 AI 托管 + 2 bots, seeds 21–26) 29 → 3; decision time unchanged (one thread, seeds 1–3: the whole prep 81 / 187 → 81 / 184 ms wall solo, 71 / 153 → 71 / 129 ms co-op; heuristics CPU 36 / 88 → 35 / 86 and 16 / 40 → 16 / 40 ms).
|
||||
- [ASSUMED] (tuned; the official AI is undocumented): MERGE_HIT 36, MAX_PAIRS 4, BENCH_BUDGET 6, FOCUS_MEMBER 4, ARMOR_WEIGHT 10, BOUNTY_KILL 2, the LP value 2 + 20 / LP, the teammate penalty 6, the 坎诺特 reserve = the band's interest capital, `CROWD` (×2 / ×1.3 / ×1.4).
|
||||
- Tests: `test/match/bot-smarter.test.js` (bounty picks, item carriers, Arts kept / AI 托管 safe, tactician range, pair via buy or freeze, 坎诺特, the bond strip, the merged rules, a CPU cost guard < 300 ms), `test/match/feedback1-meta.test.js` (AI 托管 never destroys an item to clear temp).
|
||||
|
||||
@@ -1339,10 +1340,10 @@ Settled by the user:
|
||||
| Multi-round bounties (§20.6, unchanged) | last two battles — now only 教鞭's card | `server/match/choices.js MULTI_ROUND_BOUNTY_BATTLES = null` |
|
||||
| §20.9's decisions | unchanged (start-deploy summons, 凯瑟琳's hand card, every blocker hits, the collider reach, the elite to the board, the fixed leader pool, the watched bond strip) | as §20.9 |
|
||||
|
||||
Official since this release (each with its source in its section): 突变细胞 not consumed (§21.1); the 深水区 refuses deployment and a tactical point stays in range (§21.3); the knock-out forms and kill accounting (§21.4); 圆仔 / 鸭爵 unblockable, +300 % speed, swaps in leader rounds, lpr 1 (§21.5); 标准's 本局禁用 bonds (§21.7); 维娜's lions on every free tile, 玛恩纳 S3 可对空 and his per-kill rule (§21.8); 静态刚体 (§21.9); the per-round BOSS_HIT share and the priority strip (§21.10, §21.19); the 变形同构体 member (§21.11); 源石溶剂 as damage, 信仰搅拌机's counters, the BUFF-damage enemies (§21.12); 野鬃's directional push and 琳琅's radial coins (§21.13); 暴鸰's drop (§21.14); diagonal routes (§21.15); 烛煌 S3's splash, 攻击距离 vs 信仰搅拌机 S3 (§21.16); medics heal while blocking (§21.17); 阵法术师 / 轰击术师 strike all in range (§21.18); area damage vs 隐匿, the splash radii, 阿罗玛's shared mark, 奥术法阵 on 炎佑, 重生 cooldowns, the 自行炮, 琳琅's coin range, 阿戈尔's 物理流失, no status on a 无敌 + 无法选中 unit, 余 S2 vs 自缚, 信仰搅拌机 S2 with too few bullets (§21.19); the R11 机密商店 composition and repeats in the 机密商店 and the 战术决策 (§21.2); M3茧甲's revive after every 不死 (§21.21); 起飞 = 对地规避 (§21.22); no ground blocking from a fenced tile (§21.23); a knocked-out operator keeps its tile and redeploys there, rule 3 included (§21.24).
|
||||
Official since this release (each with its source in its section): 突变细胞 not consumed, its carrier destroyed (tile freed) and the new operator gained into the 整备区 — official footage, pointed out in PR #2 (§21.1); the 深水区 refuses deployment and a tactical point stays in range (§21.3); the knock-out forms and kill accounting (§21.4); 圆仔 / 鸭爵 unblockable, +300 % speed, swaps in leader rounds, lpr 1 (§21.5); 标准's 本局禁用 bonds (§21.7); 维娜's lions on every free tile, 玛恩纳 S3 可对空 and his per-kill rule (§21.8); 静态刚体 (§21.9); the per-round BOSS_HIT share and the priority strip (§21.10, §21.19); the 变形同构体 member (§21.11); 源石溶剂 as damage, 信仰搅拌机's counters, the BUFF-damage enemies (§21.12); 野鬃's directional push and 琳琅's radial coins (§21.13); 暴鸰's drop (§21.14); diagonal routes (§21.15); 烛煌 S3's splash, 攻击距离 vs 信仰搅拌机 S3 (§21.16); medics heal while blocking (§21.17); 阵法术师 / 轰击术师 strike all in range (§21.18); area damage vs 隐匿, the splash radii, 阿罗玛's shared mark, 奥术法阵 on 炎佑, 重生 cooldowns, the 自行炮, 琳琅's coin range, 阿戈尔's 物理流失, no status on a 无敌 + 无法选中 unit, 余 S2 vs 自缚, 信仰搅拌机 S2 with too few bullets (§21.19); the R11 机密商店 composition and repeats in the 机密商店 and the 战术决策 (§21.2); M3茧甲's revive after every 不死 (§21.21); 起飞 = 对地规避 (§21.22); no ground blocking from a fenced tile (§21.23); a knocked-out operator keeps its tile and redeploys there, rule 3 included (§21.24).
|
||||
|
||||
Still [ASSUMED] (each in its section):
|
||||
- §21.1: an elite merged this round keeps its copies' refresh count; a 拉普兰德 re-bought after a sale fires again; a 6阶 突变细胞 carrier becomes another 6阶.
|
||||
- §21.1: an elite merged this round keeps its copies' refresh count; a 拉普兰德 re-bought after a sale fires again; a bench 突变细胞 carrier transforms too; a 6阶 carrier becomes another 6阶; with the hand and temp both full the gained operator and an item without a slot follow the rules of every gain.
|
||||
- §21.2: the weights inside a group; the unseen 10th R3 set and 险境 R6 drafting like R3; R9 groups by the matches they came in; R11 one of the 7 seen lists and the family weights (solo = co-op); the 机密商店 slots / weights and the earlier shops; the 战术决策 weights, independent draws and twin passives stacking; solo shows 3 of 6. Open for the user: each match's 确认本局信息 page (what decides the R9 group), more R11 悬赏决策 / 机密商店 / 战术决策, solo 绝境 / 终极 R3 / R9 / R11.
|
||||
- §21.3: the range is the loadout grid (items and bonds do not widen it); the canoe in the deploy map only.
|
||||
- §21.4: the translator immobile and damage-cancelled through its change; the 特战术师 form hits flyers (open for the user); the ember's 10 s after the 重生; 隐匿 back at once after a block; the 重生 whitelist; “余音” unmapped.
|
||||
@@ -1405,7 +1406,7 @@ Not done (noted, no primary source or out of scope): whether 华法琳 S2's 流
|
||||
|
||||
### 21.25 Hardening credited to pull requests (PR #2, PR #7) — `match/effectsMeta.js dispatch / stillEquipped`, `tools/fetch-assets.mjs shrinkGuard`, `tools/assets/manifest.mjs droppedEntries`
|
||||
|
||||
- **The prep dispatcher's equipped items** (pointed out in PR #2, whose 突变细胞 rule is superseded by §21.1): step 5 iterated `holder.items` live while handlers move and destroy pieces — normal 博士投影 destroys itself at the round start (the splice skipped the next item on that holder), 突变细胞 transforms its holder (the next item ran with a holder that was gone). It now walks a snapshot of the owned chess and of each holder's items and runs an item only while it is still equipped on its still-owned holder; a piece gained or equipped meanwhile waits for the next dispatch (effectsMeta header, META §2.2). No shipped item has a second hook on those events, so play is unchanged; a synthetic spy handler pins the rule (`test/match/feedback1-dispatch-snapshot.test.js`). QA after the integration: the items were read per holder as the walk reached it, so an item equipped meanwhile onto a holder not yet walked ran in the same dispatch and an item moved to a later holder ran twice (latent: no shipped item equips or moves items on those hooks); every [holder, item] pair is now taken before the first item runs, and two more synthetic cases pin it.
|
||||
- **The prep dispatcher's equipped items** (pointed out in PR #2, whose 突变细胞 bench rule §21.1 now follows — official footage confirmed it —, its merge path excepted): step 5 iterated `holder.items` live while handlers move and destroy pieces — normal 博士投影 destroys itself at the round start (the splice skipped the next item on that holder), 突变细胞 transforms its holder (the next item ran with a holder that was gone). It now walks a snapshot of the owned chess and of each holder's items and runs an item only while it is still equipped on its still-owned holder; a piece gained or equipped meanwhile waits for the next dispatch (effectsMeta header, META §2.2). No shipped item has a second hook on those events, so play is unchanged; a synthetic spy handler pins the rule (`test/match/feedback1-dispatch-snapshot.test.js`). QA after the integration: the items were read per holder as the walk reached it, so an item equipped meanwhile onto a holder not yet walked ran in the same dispatch and an item moved to a later holder ran twice (latent: no shipped item equips or moves items on those hooks); every [holder, item] pair is now taken before the first item runs, and two more synthetic cases pin it.
|
||||
- **fetch-assets never shrinks the committed manifest by accident** (pointed out in PR #7, whose `data/assets.json`, regenerated on a machine where some downloads failed, was 42 audio entries short — all 42 still resolve upstream): a rebuilt manifest leaves out every entry whose files are missing on that machine. When it would lose an entry of the current `data/assets.json`, the run keeps the current file, prints the entries it would drop (also the report's `droppedEntries` / `manifestWritten`) and exits 1; `--allow-shrink` (or `--prune`) writes it (ASSETS.md; `test/fetch-assets-shrink.test.js`).
|
||||
|
||||
### 21.26 Gaps found while triaging GitHub issues #1 / #8 (v0.1.1)
|
||||
|
||||
+16
-10
@@ -242,7 +242,10 @@ of a merge pair, never one already carrying a cell; it comes back after every tr
|
||||
gets a hand slot made for it, `makeHandRoom` — on a bot's own seat by destroying the cheapest other hand item if no
|
||||
chess can be sold, never a human's item under AI 托管 — on such a seat any item left in temp with a full hand gets a
|
||||
hand slot the same way, a bench operator sold for it; only a hand of nothing but items still drops it, as the temp
|
||||
deadline would), 身份牌 / 通讯机 / 寻呼模块 on a focus member; 画卷 copies the most
|
||||
deadline would; the operator a transformation gains waits on the bench and the next prep's lineup step deploys it
|
||||
like any owned unit — AI 托管 too; the buy loop's bench shed, which sells by piece value while the bench is crowded,
|
||||
never sells a piece gained since the bot's last prep ended, `rememberOwned`), 身份牌 / 通讯机 / 寻呼模块 on a focus
|
||||
member; 画卷 copies the most
|
||||
valuable deployed operator; 教鞭 / “神秘顾客” are used after a perfect battle and otherwise kept in the hand — a bot drops
|
||||
a kept one only when it needs the hand slot (“神秘顾客” then pays its fund), a human's seat under AI 托管 never. 机变: a
|
||||
bounty card is scored by its expected payout minus its expected leaks × the value of an LP (2 + 20 / LP), the kill
|
||||
@@ -395,7 +398,7 @@ Writes (all validated, never throw on bad input, never make funds / pools negati
|
||||
| `rollChess({ maxTier, tier, bond, filter })` / `rollItem({ pool, tier, maxTier })` | copy-weighted chess id from the shared pool / item id (choices.json pools) |
|
||||
| `grantFreeRefresh(n)` | free refreshes (stack) |
|
||||
| `modifyPrice(delta)` / `setPrice(v)` | onPrice only: edit `ev.price` |
|
||||
| `promote(uid)` / `transform(uid, chessId, { returnItems })` / `upgradeItem(uid)` | elite in place / replace a chess (keeps tile; keeps its equipment, or with `returnItems` sends it to the hand like a destroyed operator's — 突变细胞) / item → golden |
|
||||
| `promote(uid)` / `transform(uid, chessId)` / `upgradeItem(uid)` | elite in place / 突变细胞's transformation: destroy the chess wherever it stands (a board tile is freed; its equipment returns to the hand first, overflow temp), then gain `chessId` like `grantChess` (hand, overflow temp, the no-room rule; a merge it completes as usual — the carrier's tile is no copy's) and return the gained piece (the elite after a merge) / item → golden |
|
||||
| `destroyPiece(uid)` / `equipDirect(itemUid, chessUid)` | remove a piece (chess copies return, items go back) / attach without equip effects |
|
||||
| `offerChess(ids, { tier, label })` | queue a pick-one offer (shown as `shop.rewardOffer`, free) — 寻呼模块 / 信标 style; `label` (default `effectsMeta.offerLabel(source)`: the strategy's effect name, the item's name or the 特质's operator) is the shop bar's header instead of 晋升奖励 (`rewardOffer.source` `'special'`; the promotion reward is `'merge'`) |
|
||||
| `offerItems(ids, { tier, label })` | the same for items (slots of kind `'item'`, drawn as item cards) — 凯瑟琳 定向投放 style (player report #6 after 0.1.0) |
|
||||
@@ -428,11 +431,13 @@ Built-ins (builtinMeta.js, overridable): 盟约之币 / 骑士储蓄罐 (random
|
||||
紧急调度券 (take shop chess), 精打细算玩偶 (+funds each round), 简易通讯机 / 拟态物质 (same-bond chess), 见钱眼开玩偶
|
||||
(+funds next round), 人事部文档 (cap 9), 博士投影 (elite now / at the next round start), 寻呼模块 / 信标 (pick-one
|
||||
offers; 信标 gifts the original chess to the teammate with the most members of its bonds next round), 商业包装方案 (every
|
||||
N sells → same-bond chess), 突变细胞 (after battle → a random NORMAL chess one tier higher, max 6, on the carrier's tile;
|
||||
the carrier's equipment, the cell included, returns to the hand before the new operator's summon cards — the cell is
|
||||
not consumed: PRTS 下半 记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", PRTS 卫戍协议/帮助 "佩戴的装备
|
||||
无法手动卸除,在失去该干员(干员出售、销毁、合并等)…时自动卸除", players re-inject it every round; player feedback after 0.1.0),
|
||||
画卷 (copy the operator in range with its
|
||||
N sells → same-bond chess), 突变细胞 (after battle the carrier — deployed or on the bench — is destroyed, its tile freed;
|
||||
its equipment, the cell included, returns to the hand first; then a random NORMAL chess one tier higher, max 6, is
|
||||
gained like any gained operator: into the 整备区, overflow temp, never onto the carrier's tile — official footage,
|
||||
bilibili BV1vzyVBuEN9 ≈ 8:24 / BV1Qkw1zMEoR ≈ 7:25, pointed out in PR #2 — and a merge it completes as usual. The cell
|
||||
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).
|
||||
|
||||
**教鞭 / “神秘顾客” stay a random bounty (deliberate).** The official Arts open a personal 悬赏 choice
|
||||
@@ -498,8 +503,8 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
copies are consumed). Where it goes (PRTS 卫戍协议/帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
|
||||
则发送至作战区对应位置)", the user's playtest #6 follow-up): when a consumed copy stood on the board, onto that copy's
|
||||
tile with its facing — of several, the one that deploys first (row desc, then col asc; `board.js mergeTile`,
|
||||
[ASSUMED]); a deployed piece transformed into the completing copy (突变细胞, `transformChess`) counts with its own tile;
|
||||
a tile the elite may not use (a stale terrain change) is skipped. It replaces a deployed copy, so the deploy count never
|
||||
[ASSUMED]); a 突变细胞 carrier is destroyed before its gain, so its freed tile is no copy's (`transformChess`, DESIGN
|
||||
§21.1); a tile the elite may not use (a stale terrain change) is skipped. It replaces a deployed copy, so the deploy count never
|
||||
grows (no BOARD_FULL), and as a deployment its manually deployable summons join the hand (`grantTokensFor`, the
|
||||
player's loadout). Otherwise the elite goes to the hand, overflowing into temp. Equipment returns to the hand (overflow
|
||||
temp; with both full it stays on the elite, up to its 2 equip slots — any further item is destroyed); summons of
|
||||
@@ -764,7 +769,8 @@ round was over. The official 1 s `broadcastBeginDelay` is not modelled.
|
||||
* Level-up does not add slots until the next roll (refresh / round start).
|
||||
* Buying a second copy of an equipped normal item merges into the golden item in the hand (not equipped).
|
||||
* Promotions by effects (升华, 博士投影) keep the equipment; merges return it; 突变细胞's transformation returns it (the
|
||||
cell included).
|
||||
cell included) before its new operator is gained into the 整备区 — the carrier's tile is left empty (official footage,
|
||||
DESIGN §21.1).
|
||||
* An elite merged in a round keeps the highest per-piece round counter of its copies (`pieceRoundCount`): an elite made
|
||||
from 拉普兰德 copies that already fired this round does not fire again before the next round (conservative; the
|
||||
official server's instance handling is not observable). A 拉普兰德 bought after selling one in the same round is a new
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@
|
||||
| 招募价格(按阶级 I–VI) | 2 / 3 / 3 / 3 / 4 / 4 |
|
||||
| 刷新 | 1 资金 |
|
||||
| 出售干员 | 任何干员(含精锐)都只返还 **1** 资金 |
|
||||
| 装备 / 法术 | 调度中心出售除特殊装备外的所有装备:4 把特殊维式重锤(战栗 / 坚固 / 加速 / 灼燃)只来自维多利亚盟约(每 25 层)和洛洛的定制品,突变细胞只来自昆图斯的策略。装备不能出售,整备区里的只能「销毁」(不返还)。已配发的装备锁定在干员身上:只能被第三件装备替换(弹出替换窗口,选择要替换的一件,它会被销毁),或在干员出售 / 合成精锐时退回整备区。**突变细胞**不会用完:战斗结束后装备者被替换为高一阶(最高 VI)的随机普通干员(精锐也一样),突变细胞和它的其他装备退回整备区,可以再次配发给任意干员。**坚固维式重锤**首次受到致命伤害时锁血 8 秒,**每次部署一次**:被击倒再部署(或突袭跳跃再部署、被 M3茧甲 / 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样;借用到期时正在锁血的会锁满 8 秒;站在阿戈尔干员身前被开场吞噬、自带源石溶剂掉血时也会用掉这一次 |
|
||||
| 装备 / 法术 | 调度中心出售除特殊装备外的所有装备:4 把特殊维式重锤(战栗 / 坚固 / 加速 / 灼燃)只来自维多利亚盟约(每 25 层)和洛洛的定制品,突变细胞只来自昆图斯的策略。装备不能出售,整备区里的只能「销毁」(不返还)。已配发的装备锁定在干员身上:只能被第三件装备替换(弹出替换窗口,选择要替换的一件,它会被销毁),或在干员出售 / 合成精锐时退回整备区。**突变细胞**不会用完:战斗结束后装备者被销毁(部署在场上的话,原来的格子空出来,剩余可放置角色加 1),突变细胞和它的其他装备先退回整备区,可以再次配发给任意干员;随后获得一名高一阶(最高 VI)的随机普通干员(装备者是精锐也一样),和其他新获得的干员一样进入整备区(满了进入临时整备区),需要自己重新部署;如果它凑齐了合成所需的同名干员,照常合成精锐。**坚固维式重锤**首次受到致命伤害时锁血 8 秒,**每次部署一次**:被击倒再部署(或突袭跳跃再部署、被 M3茧甲 / 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样;借用到期时正在锁血的会锁满 8 秒;站在阿戈尔干员身前被开场吞噬、自带源石溶剂掉血时也会用掉这一次 |
|
||||
| 冻结 | 免费,保留当前所有未买的格子到下一回合;冻结状态下仍可主动刷新 |
|
||||
| 升级调度中心(1→2 … 5→6) | 险境及以上 5 / 8 / 11 / 12 / 13;同盟标准 5 / 8 / 10 / 11 / 11;独立标准 1 / 1 / 5 / 8 / 10。**每过一回合价格 −1**(最低 0),升级后按下一级的价格重新计算 |
|
||||
| 其他收入 | 悬赏敌人、盟约「远见」「奇迹」、装备「骑士储蓄罐」、部分策略和特质 |
|
||||
|
||||
@@ -842,7 +842,7 @@ Local copies are in `scratchpad/gd/extra/critic/` (`bw_盟约.txt`, `bw_sweep.tx
|
||||
| new | – | At **combat start, all unfrozen shop contents are cleared** ("进入作战期后,栏上若未处于冻结状态则清空全部物资"). | VERIFIED |
|
||||
| 4.2 / 8 | bench 10 + temp area | **Hand (手牌区/整备区) = 10 regular + 5 temporary slots**, shared by operators, **items** and hand-deployable **summons**. Several summons of one owner stack into 1 slot; summons of different owners don't stack.<br>When the regular hand is full, every action that would add a card is refused: buy, and withdraw from the board. Exception: a purchase that completes a 3-merge nets −1 card, so allow it.<br>Passive gains (merge result, band/trait/item grants, items returned by promotion) overflow into the temp slots. **While temp is non-empty the player cannot start combat.** Temp is wiped when the next phase starts.<br>A board↔hand swap is allowed even with a full hand. Cards fill hand slots **right→left**. | VERIFIED |
|
||||
| 5.2 | – | **Items cannot be sold** ("多余的装备无法进行出售,可以销毁"). They can only be destroyed for 0 funds. Operators sell for 1 from board or hand. | VERIFIED |
|
||||
| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:<br>1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.<br>2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (top→bottom, then left→right — the official deploy order) with its facing; a deployed operator transformed into the completing copy (突变细胞) counts as deployed; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows.<br>3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.<br>4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
|
||||
| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:<br>1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.<br>2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (top→bottom, then left→right — the official deploy order) with its facing; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows. A 突变细胞 carrier is destroyed before its new operator is gained (official footage, DESIGN §21.1), so a merge that operator completes never uses the carrier's tile.<br>3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.<br>4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
|
||||
| 2 | BAND_CHECK "3 random + reroll" [ASSUMED] | **All unlocked bands are shown.** In co-op, players pick **one at a time in a random order** shown on the left ("以随机顺序进行策略轮选"). Each co-op player may **skip once**. The step lasts 50 s; on timeout the band is 华法琳. **Duplicates are allowed:** the official illustration shows 2 teammates' picks still un-greyed in the grid. [ASSUMED] 12 s per turn inside the 50 s step. | VERIFIED except the per-turn timer |
|
||||
| 2 | banned bonds "1 core + 1 add-on" [ASSUMED] | Replaced by the verified rule in **A2**. | VERIFIED |
|
||||
| 1, 12 | one ×0.7/×0.8 multiplier per mode; `aceffect_enemy_*` mapping [ASSUMED] | Replaced by the **per-round table in A3**. | VERIFIED (PRTS, user-sourced) |
|
||||
|
||||
@@ -5493,7 +5493,7 @@
|
||||
"talentNotes": [
|
||||
"本装备不会合并为进阶品质"
|
||||
],
|
||||
"implFormula": "After the battle ends: replace the carrier with a random operator one tier higher (max tier 6); item consumed [ASSUMED]. Keeps elite status? [ASSUMED: result is NORMAL quality; other equipped item returns to hand]. Never merges.",
|
||||
"implFormula": "After the battle ends: the carrier is destroyed wherever it stands (a board tile is freed); its equipment, the cell included, returns to the hand first (the cell is not consumed); then a random NORMAL operator one tier higher (max tier 6; an elite carrier too) is gained like any gained operator, into the 整备区 (overflow temp), never onto the carrier's tile. Never merges. [Corrected after 0.1.0 — the first reading was 'item consumed [ASSUMED]': PRTS 下半 记录 备注 '生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)'; official footage bilibili BV1vzyVBuEN9 / BV1Qkw1zMEoR (the tile empty, the operator on the bench; PR #2); see the 04-items.md addenda.]",
|
||||
"normal": {
|
||||
"chessId": "chess_item_5_08_e_a",
|
||||
"isGolden": false,
|
||||
|
||||
@@ -252,6 +252,7 @@ Param names below are the keys of `items[].normal.params` / `items[].golden.para
|
||||
params: `count=8.0/7.0`; buff keys: `sell_char_count_gain_equip_owner_bond`
|
||||
- **突变细胞** (`chess_item_5_08_e_a`, TV, 2) - After the battle ends: replace the carrier with a random operator one tier higher (max tier 6); item consumed [ASSUMED]. Keeps elite status? [ASSUMED: result is NORMAL quality; other equipped item returns to hand]. Never merges.
|
||||
**Addendum (player feedback after 0.1.0):** the cell is **not consumed**. PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注: "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)" (normal result confirmed); PRTS 卫戍协议/帮助: "佩戴的装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除" (a destroyed operator's equipment comes off); the official text never says 销毁 for the cell (every consumable's does); players re-inject it every round (bilibili cv47000418 "之后就是一直打针,扎到核心卡或者叠层手干员就换人扎"; cg.163.com guide 2025-11-15 "这个道具可以无限使用"; bilibili cv48003106 "扎了好几次") — the destroyed operator's equipment, the cell included, returns to the hand.
|
||||
**Addendum 2 (where the new operator goes):** it is **gained into the 整备区**, not left on the carrier's tile. Official gameplay footage — bilibili BV1vzyVBuEN9 (上半, ≈ 8:24) and BV1Qkw1zMEoR (下半, ≈ 7:25) — shows the next prep with the carrier's tile empty, the remaining-deploy counter (剩余可放置角色) one higher and the bench holding the returned equipment and the new tier+1 operator, which the player then deploys by hand. This is the 备注 read as written (a destroy, then a gain) and PRTS 卫戍协议/帮助's rule that everything recruited or gained is sent to the 手牌区 ("被发送至手牌区的物资优先从右到左填充空位"); an outside contributor's playtest (the closed PR #2) said the same. A merge the new operator completes is an ordinary gained copy's merge: the carrier is gone first, so its tile is no copy's (DESIGN §21.1).
|
||||
params: `-`; buff keys: `char_chess_transformation_equip`
|
||||
- **人事部文档** (`chess_item_6_08_e_a`, TVI, 4) - On equip: destroy item; player's max deployable operator count becomes `count` (9) (base maxBattleChessCnt = 8). Never merges; a second copy has no further effect [ASSUMED].
|
||||
params: `count=9.0`; buff keys: `equip_destory_deployment_cnt_change`
|
||||
@@ -442,7 +443,7 @@ Pools referenced but NOT defined in client data (server-side) - proposed content
|
||||
3. Whether equipped copies count toward the 2-copy merge (assumed yes).
|
||||
4. Exact contents of server pools (`pool_equip_*`) and of `hunter_band_1` bounty choices.
|
||||
5. 道具补给 / 机密商店 offer size and tier (3-pick-1 assumed); round schedule taken from a community post.
|
||||
6. M3茧甲 revive HP (full assumed); 突变细胞 result quality and equipment — resolved after 0.1.0: a normal operator (PRTS 备注 "随机初始干员"), the equipment and the cell return to the hand (addendum above).
|
||||
6. M3茧甲 revive HP (full assumed); 突变细胞 result quality, equipment and placement — resolved after 0.1.0: a normal operator (PRTS 备注 "随机初始干员"), the equipment and the cell return to the hand, and the new operator is gained into the 整备区 with the carrier's tile left empty (addenda above; official footage).
|
||||
7. 天马之枪 "30% true damage": `atk_scale=0.3` suggests 30% of ATK per damage instance; alternative reading 30% of damage dealt. We use 30% ATK.
|
||||
8. 蒸汽之心 doubling for 战栗锤: prob or duration? (we double prob 10%->20%).
|
||||
|
||||
|
||||
+42
-48
@@ -33,9 +33,12 @@
|
||||
// * Per-piece round counters (pieceRoundCount, piece.meta.round): an operator's own counts of the current round
|
||||
// (拉普兰德: the manual refreshes she witnessed — player feedback after 0.1.0); a new piece starts at 0, an elite
|
||||
// merged this round keeps the highest of its copies' [ASSUMED].
|
||||
// * Transformations (transformChess, 突变细胞): the new chess keeps the tile when legal; `returnItems` sends the old
|
||||
// piece's equipment to the hand (overflow temp) like a destroyed operator's — before the new piece's summon stack,
|
||||
// as in a merge, so a summon card never pushes the returned 突变细胞 into temp.
|
||||
// * Transformations (transformChess, 突变细胞 — PRTS 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员"): a destroy
|
||||
// followed by a gain. The carrier leaves wherever it stands (a board tile is freed, the deploy count drops), its
|
||||
// equipment — the cell included — returns to the hand first (overflow temp), then the new chess is gained like any
|
||||
// other (acquireChess: hand, overflow temp, the no-room rule; a merge it completes puts the elite on a consumed
|
||||
// deployed copy's tile — never the carrier's —, else in the hand). Official footage: the tile is empty at the next
|
||||
// prep and the new operator waits in the 整备区 (pointed out in PR #2).
|
||||
// * Items: equip max 2 (a 3rd replaces the equipped item the player picks — g.equip replaceUid, the oldest when
|
||||
// absent; equipped items are otherwise locked: g.destroy refuses them),
|
||||
// 2 identical normal items (hand/temp/equipped) merge into the golden item in the hand, items are never sold
|
||||
@@ -58,9 +61,9 @@
|
||||
// the next round start (grantTokensFor) — no out-of-range placement reaches the battle.
|
||||
// * Facing (DESIGN §3, research 09 §1.2): every board piece has `dir` ∈ UP|RIGHT|DOWN|LEFT (server/sim/dir.js), set
|
||||
// by g.move {…, dir} (absent ⇒ RIGHT) and kept across rounds. g.move onto the piece's OWN tile re-orients it in
|
||||
// place; a swap keeps the occupant's dir; pieces put on the board by effects (a merge elite taking a consumed
|
||||
// copy's tile, a transformation keeping the tile) keep that tile's dir, anything else defaults to RIGHT
|
||||
// (`pieceDir`). g.art {…, dir} rotates the Art's range (画卷 1-1: its tile + the tile in front).
|
||||
// place; a swap keeps the occupant's dir; a piece put on the board by an effect (a merge elite taking a consumed
|
||||
// copy's tile) keeps that tile's dir, anything else defaults to RIGHT (`pieceDir`). g.art {…, dir} rotates the
|
||||
// Art's range (画卷 1-1: its tile + the tile in front).
|
||||
// * Operator loadout (DESIGN §16): the human's checked `seat.loadout` ({ [baseChessId]: { skill, module } }, entries
|
||||
// equal to the defaults dropped) is re-checked against this match's data (shared/protocol.js checkLoadout; a
|
||||
// mismatch falls back to the defaults) and kept frozen; bots always use the defaults. Match.setLoadout may replace
|
||||
@@ -507,10 +510,10 @@ export class PlayerState {
|
||||
* the elite — PRTS 卫戍协议/帮助 §干员的获得与精锐化: "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
|
||||
* 则发送至作战区对应位置)" (the user's playtest #6 follow-up confirms it). The tile (`mergeTile`): when a consumed copy
|
||||
* stood on the board the elite takes its tile and facing — of several, the one that deploys first (board reading
|
||||
* order: top → bottom, then left → right) [ASSUMED]; `fromKey` / `fromDir` = the tile the incoming piece stood on (a
|
||||
* transformation of a deployed operator) counts as such a copy. It replaces a deployed copy, so the deploy count never
|
||||
* grows. Otherwise the elite goes to the hand, overflow temp — outside PREP too (a SETTLE merge's elite waits in temp
|
||||
* through the next prep, tempDue). The copies' equipment returns to the hand ("干员晋级后已配发装备会回收至整备区";
|
||||
* order: top → bottom, then left → right) [ASSUMED]. The incoming copy is never deployed (a 突变细胞 transformation
|
||||
* destroyed its carrier before the gain: that tile is no copy's). It replaces a deployed copy, so the deploy count
|
||||
* never grows. Otherwise the elite goes to the hand, overflow temp — outside PREP too (a SETTLE merge's elite waits in
|
||||
* temp through the next prep, tempDue). The copies' equipment returns to the hand ("干员晋级后已配发装备会回收至整备区";
|
||||
* overflow temp; with both full it stays on the elite, up to its equipPerChess (2) slots — any further item is
|
||||
* destroyed with a log warning, as before the official rule) and an identical normal pair among it merges like any gain
|
||||
* (checkItemMerges); their summons are removed, and an elite on the board
|
||||
@@ -518,15 +521,14 @@ export class PlayerState {
|
||||
* the elite could not be stored).
|
||||
* @param {string} baseId
|
||||
* @param {any} incoming the acquired, not yet stowed copy (null: only owned copies)
|
||||
* @param {{ fromKey?: string|null, fromDir?: string }} [opts]
|
||||
*/
|
||||
_mergeChess(baseId, incoming, { fromKey = null, fromDir = undefined } = {}) {
|
||||
_mergeChess(baseId, incoming) {
|
||||
const need = this.gd.mergeCount(baseId);
|
||||
const goldenId = this.gd.goldenIdOf(baseId);
|
||||
if (!(need > 1) || !goldenId) return null;
|
||||
const locs = this._chessLocations().filter((l) => !this.gd.isGolden(l.piece.id) && this.gd.baseIdOf(l.piece.id) === baseId);
|
||||
const consumed = [];
|
||||
if (incoming) consumed.push({ piece: incoming, area: 'new', key: fromKey || undefined, dir: fromDir });
|
||||
if (incoming) consumed.push({ piece: incoming, area: 'new' });
|
||||
for (const l of locs) { if (consumed.length >= need) break; consumed.push(l); }
|
||||
if (consumed.length < need) return null;
|
||||
let copies = 0;
|
||||
@@ -545,7 +547,7 @@ export class PlayerState {
|
||||
const rc = l.piece.meta && l.piece.meta.round;
|
||||
if (rc && rc.r === this.m.round) for (const [k, v] of Object.entries(rc.n)) this.bumpPieceRoundCount(elite, k, Math.max(0, v - this.pieceRoundCount(elite, k)));
|
||||
}
|
||||
const deployed = consumed.filter((l) => l.key && !this.board.has(l.key)).map((l) => ({ key: l.key, dir: l.area === 'new' ? l.dir : pieceDir(l.piece) }));
|
||||
const deployed = consumed.filter((l) => l.key && !this.board.has(l.key)).map((l) => ({ key: l.key, dir: pieceDir(l.piece) }));
|
||||
const toTile = (t) => { elite.dir = parseDir(t.dir) || 'RIGHT'; this.board.set(t.key, elite); return 'board'; };
|
||||
const tile = mergeTile(deployed, (r, c) => this._legal(elite, r, c));
|
||||
let where = tile ? toTile(tile) : this.stow(elite, { allowTemp: true });
|
||||
@@ -591,51 +593,43 @@ export class PlayerState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a chess piece by another chess (突变细胞 and similar). The new piece keeps the tile when legal (else goes
|
||||
* to the hand/temp) and keeps the equipment — or, with `returnItems`, the equipment returns to the hand like the
|
||||
* equipment of a destroyed / promoted operator (突变细胞: PRTS 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员"; the
|
||||
* cell itself comes back, player feedback after 0.1.0), overflowing into temp; with both full an item stays on the new
|
||||
* piece up to its equipPerChess slots (any further one is destroyed with a log warning, as in a merge). Pool copies are
|
||||
* swapped; completes a merge when possible — a deployed piece's tile then counts as a consumed copy's for the elite
|
||||
* (_mergeChess fromKey, when legal for it), and the merge returns the equipment itself. A new piece on the board gets
|
||||
* its summon stack only after the equipment came back (like _mergeChess: a summon card must not push the returned
|
||||
* equipment — 突变细胞 — into temp, where it would be lost at the deadline).
|
||||
* @param {{ returnItems?: boolean }} [opts]
|
||||
* Transformation (突变细胞 "战斗结束后,装备者替换为高一阶的随机干员"; PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "生效时,原
|
||||
* 干员销毁,获得一名高一阶的随机初始干员(最高六阶)"): a destroy followed by a gain. The carrier is destroyed wherever it
|
||||
* stands — a board tile is freed (the deploy count drops), its summons are removed, its pool copies return. Its
|
||||
* equipment, the cell included, comes off first (PRTS 卫戍协议/帮助 "在失去该干员(干员出售、销毁、合并等)…时自动卸除"): to
|
||||
* the hand, overflowing into temp, auto-merging like any gain. Then `newId` is gained like any other gained operator
|
||||
* (acquireChess, onGain source 'transform'): the hand, overflow temp ("被发送至手牌区的物资优先从右到左填充空位"), and with
|
||||
* both full it goes back to the pool ("整备区已满,获得的干员已返还"); it gets no summon card in the hand (only a deployment
|
||||
* brings one), and a merge it completes follows the ordinary rule (_mergeChess: the elite on a consumed deployed copy's
|
||||
* tile, else the hand — the carrier's freed tile is no copy's). An item that found no slot takes one the gain freed (a
|
||||
* merge consumes copies), else stays on the gained operator up to its equipPerChess slots, else it is destroyed with a
|
||||
* log warning (as in a merge). Official footage (bilibili BV1vzyVBuEN9, BV1Qkw1zMEoR; pointed out in PR #2): at the
|
||||
* next prep the carrier's tile is empty, one more deployment is left and the new operator waits in the 整备区.
|
||||
* @param {any} piece the carrier (an owned chess piece)
|
||||
* @param {string} newId chess id gained in its place
|
||||
* @returns {any} the gained piece (the elite when it completed a merge) or null
|
||||
*/
|
||||
transformChess(piece, newId, { returnItems = false } = {}) {
|
||||
transformChess(piece, newId) {
|
||||
const loc = this.find(piece.uid);
|
||||
const rec = this.gd.chess(newId);
|
||||
if (!loc || loc.piece.kind !== 'chess' || !rec) return null;
|
||||
if (!loc || loc.piece.kind !== 'chess' || !this.gd.chess(newId)) return null;
|
||||
// 原干员销毁: off its tile / slot, its summons removed, its copies back to the pool
|
||||
this._detach(loc);
|
||||
this.removeTokensOf(piece.uid);
|
||||
const items = piece.items || [];
|
||||
piece.items = [];
|
||||
this.returnCopies(piece);
|
||||
const base = this.gd.baseIdOf(newId);
|
||||
const taken = this.m.pool.take(base, rec.isGolden ? this.gd.goldenCopies : 1);
|
||||
let np = this.newPiece('chess', newId, { poolCopies: taken });
|
||||
const merging = !rec.isGolden && this.completesChessMerge(newId);
|
||||
// a merge returns the consumed copies' equipment (this one's too) before the elite's summons (_mergeChess)
|
||||
const pending = returnItems && !merging ? items : [];
|
||||
np.items = returnItems && !merging ? [] : items;
|
||||
let deployed = false;
|
||||
if (merging) {
|
||||
np = this._mergeChess(base, np, loc.area === 'board' ? { fromKey: loc.key, fromDir: pieceDir(piece) } : undefined);
|
||||
} else if (loc.area === 'board') {
|
||||
const [r, c] = parseKey(loc.key);
|
||||
if (this._legal(np, r, c)) { np.dir = pieceDir(piece); this.board.set(loc.key, np); deployed = true; } else if (!this.stow(np, { allowTemp: true })) { this.returnCopies(np); np = null; }
|
||||
} else if (!this._putBack(loc, np)) { this.returnCopies(np); np = null; }
|
||||
// after the new piece took its place (a hand piece keeps its slot), its predecessor's equipment comes off
|
||||
for (const it of pending) {
|
||||
// its equipment comes off first (the returned pair auto-merges, which may free a slot for the gain)
|
||||
const left = items.filter((it) => !this.stow(it, { allowTemp: true }));
|
||||
this.checkItemMerges();
|
||||
// 获得一名…干员: gained like any other gained operator
|
||||
const np = this.acquireChess(newId, { source: 'transform' });
|
||||
for (const it of left) {
|
||||
if (this.stow(it, { allowTemp: true })) continue;
|
||||
if (np && np.items.length < this.gd.equipPerChess) { np.items.push(it); continue; }
|
||||
if (np && this.find(np.uid) && np.items.length < this.gd.equipPerChess) { np.items.push(it); continue; }
|
||||
this.m.log.warn?.(`[match ${this.m.roomCode}] ${this.playerId}: returned item ${it.id} destroyed (no space)`);
|
||||
}
|
||||
if (returnItems) this.checkItemMerges();
|
||||
// deployed in its predecessor's place: its manually deployable summons join the hand, after the returned equipment
|
||||
if (deployed) this.grantTokensFor(np);
|
||||
if (left.length) this.checkItemMerges();
|
||||
this.recompute();
|
||||
if (np) this.m.dispatch(this, 'onGain', { piece: np, kind: 'chess', source: 'transform' });
|
||||
return np;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
// prep handlers buy / sell / refresh / levelUp pay exactly price / +sell price / refresh price (free first) / level
|
||||
// price, the level rises by one and its price resets to the next base; Ready only with an empty temp
|
||||
// merges a merge consuming a deployed copy puts the elite on that copy's tile (of several, the first in deploy
|
||||
// order legal for it — board.js mergeTile; a transformed piece's own tile counts) with its facing, else
|
||||
// into the hand / temp; the deploy count never grows (PRTS 卫戍协议/帮助, user playtest #6 follow-up)
|
||||
// order legal for it — board.js mergeTile; a 突变细胞 carrier destroyed before the gain is no copy) with
|
||||
// its facing, else into the hand / temp; the deploy count never grows (PRTS 卫戍协议/帮助, user playtest
|
||||
// #6 follow-up)
|
||||
// combat start nothing overdue in temp, everyone ready, funds lost (carry bands excepted), unfrozen shop cleared,
|
||||
// one field per alive player
|
||||
// drafts every seat holds an allowed band with LP = totalHp; 机变: one card per alive player, card ↔ picker
|
||||
@@ -227,15 +228,14 @@ export function attachAudit(m, { invariants = true, limit = 200 } = {}) {
|
||||
});
|
||||
// merges (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置", user playtest #6 follow-up): with a
|
||||
// deployed copy among the consumed ones the elite stands on the first such tile in deploy order that is legal for
|
||||
// it (the incoming piece's own tile when a transformation merged it in place), with that copy's facing; else in the
|
||||
// hand / temp. A merge never grows the deploy count. Every owned normal copy is consumed (merges are immediate).
|
||||
wrap(ps, '_mergeChess', function (orig, baseId, incoming, opts) {
|
||||
// it, with that copy's facing; else in the hand / temp (the incoming copy is never deployed: a 突变细胞
|
||||
// transformation destroyed its carrier before the gain, so that freed tile is no copy's). A merge never grows the
|
||||
// deploy count. Every owned normal copy is consumed (merges are immediate).
|
||||
wrap(ps, '_mergeChess', function (orig, baseId, incoming) {
|
||||
const tiles = new Map(); // tile key → facing of the copy standing there
|
||||
for (const [k, p] of ps.board) if (p.kind === 'chess' && !gd.isGolden(p.id) && gd.baseIdOf(p.id) === baseId) tiles.set(k, pieceDir(p));
|
||||
if (opts && opts.fromKey) tiles.set(opts.fromKey, pieceDir({ dir: opts.fromDir }));
|
||||
// a transformation (transformChess) detached its deployed carrier before the merge: it still counts as deployed
|
||||
const deployed0 = ps.deployCount + (opts && opts.fromKey ? 1 : 0);
|
||||
const elite = orig(baseId, incoming, opts);
|
||||
const deployed0 = ps.deployCount;
|
||||
const elite = orig(baseId, incoming);
|
||||
if (elite) check('merge', () => {
|
||||
const id = ps.playerId;
|
||||
const loc = ps.find(elite.uid);
|
||||
|
||||
+26
-5
@@ -53,7 +53,9 @@
|
||||
// 5. items by what they do (itemTarget): equipment on the strongest deployed damage dealers (survival items on
|
||||
// blockers, bond signature items on a member), 信标 on a bench single, 拟态物质 on a pair, 博士投影 (both
|
||||
// qualities) on the strongest normal operator, 突变细胞 on the least valuable normal single below 6阶 (cellTarget:
|
||||
// never an elite or a pair member), bond items on a focus member; Arts (useArt): 画卷 copies the most valuable
|
||||
// never an elite or a pair member; the operator its transformation gains joins the bench — the buy loop's bench
|
||||
// shed leaves a piece gained since the last prep alone, rememberOwned — and steps 3–4 of the next prep deploy it
|
||||
// like any owned unit), bond items on a focus member; Arts (useArt): 画卷 copies the most valuable
|
||||
// deployed operator, 教鞭 / “神秘顾客” are used after a perfect battle and kept otherwise (consume-on-equip items /
|
||||
// Arts only with a handler)
|
||||
// 6. resolve the temp slots (a 突变细胞 left there — it comes back after every transformation — gets a hand slot made
|
||||
@@ -1182,7 +1184,10 @@ function refreshValue(m, ps, ctx) {
|
||||
/**
|
||||
* Sell the weakest bench chess (temp and hand; the hand only with `handOnly`) that is not part of a merge pair (or
|
||||
* anything when keepPairs is false) and does not carry a 突变细胞 (its transformation after the battle is the point).
|
||||
* `keepFresh`: never a piece that came this prep (`boughtRound`: bought for the lineup, an elite just merged, a reward).
|
||||
* `keepFresh`: never a piece that came this prep (`boughtRound`: bought for the lineup, an elite just merged, a reward)
|
||||
* nor one gained since the bot's last prep ended (unseen, see rememberOwned: the operator a 突变细胞 transformation
|
||||
* gains joins the bench while its carrier's deployment is free — the placement step decides about it, not the shed's
|
||||
* piece value).
|
||||
*/
|
||||
function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly = false, keepFresh = false } = {}) {
|
||||
const ctx = context(m, ps);
|
||||
@@ -1192,7 +1197,7 @@ function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly =
|
||||
if (!p || p.kind !== 'chess' || (p.items || []).some((it) => isMutationCell(m.gd, it.id))) continue;
|
||||
const base = m.gd.baseIdOf(p.id);
|
||||
if (keepPairs && !m.gd.isGolden(p.id) && (ctx.copies.get(base) || 0) >= 2) continue;
|
||||
if (keepFresh && p.boughtRound === m.round) continue;
|
||||
if (keepFresh && (p.boughtRound === m.round || unseen(ps, p))) continue;
|
||||
const v = pieceValue(m, ps, p, ctx);
|
||||
if (v < worstV) { worstV = v; worst = p; }
|
||||
}
|
||||
@@ -1200,6 +1205,17 @@ function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly =
|
||||
return tryDo(() => ps.sell(worst.uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* The chess the bot owned when its last prep ended (`ps._botSeenUids`, written by botPrepEndSteps before Ready). A
|
||||
* piece outside it was gained after that prep — at SETTLE (a 突变细胞 transformation's operator, battle-result grants),
|
||||
* at the round start or in 机变 — and has not been through a placement step yet. No memory yet (a bot's first prep, a
|
||||
* seat just put under AI 托管): nothing counts as unseen.
|
||||
*/
|
||||
function rememberOwned(ps) {
|
||||
ps._botSeenUids = new Set(ps.allChess().map((p) => p.uid));
|
||||
}
|
||||
const unseen = (ps, p) => !!ps._botSeenUids && !ps._botSeenUids.has(p.uid);
|
||||
|
||||
/** Lineup gain of one copy of chess `id` (full board): best single swap into the current best lineup `cur`. */
|
||||
function lineupGain(m, ps, id, ctx, cur) {
|
||||
const probe = { uid: -1, kind: 'chess', id, items: [] };
|
||||
@@ -1311,6 +1327,8 @@ export function* botPrepEndSteps(m, ps, job = null) {
|
||||
// 5. temp → hand / sell / destroy; keep one hand slot free for next round's merges
|
||||
resolveTemp(m, ps);
|
||||
if (freeSlot(ps.hand) < 0) freeHandSlot(m, ps);
|
||||
// what the next prep's bench shed may judge by value: what is owned now (later gains wait for a placement step)
|
||||
rememberOwned(ps);
|
||||
tryDo(() => ps.setReady(true));
|
||||
}
|
||||
|
||||
@@ -1395,7 +1413,9 @@ function* buyLoopSteps(m, ps, { fillOnly = false, maxRefreshes = 0 } = {}) {
|
||||
const mergesOnly = fillOnly && boardFull;
|
||||
// keep room for merges: a crowded bench sheds its weakest single — never one that came this prep (the buy picks by
|
||||
// lineup gain, the shed by piece value: it used to sell the single just bought for 3–5 back for 1; QA of the 0.1.1
|
||||
// bots). The shed is pre-emptive (one slot is still free), so with only fresh singles it waits for the next prep.
|
||||
// bots) nor one gained since the last prep (the operator a 突变细胞 transformation put on the bench, its deployment
|
||||
// freed: in 20 solo 绝境 昆图斯 matches the shed sold 44 of 222 before the placement step could deploy them). The
|
||||
// shed is pre-emptive (one slot is still free), so with only fresh singles it waits for the next prep.
|
||||
const used = ps.hand.filter(Boolean).length;
|
||||
if (used >= gd.benchSize - 1) sellWeakestHand(m, ps, { keepFresh: true });
|
||||
let best = -1;
|
||||
@@ -1629,7 +1649,8 @@ const isMutationCell = (gd, itemId) => itemEffect(gd.item(itemId)) === 'char_che
|
||||
|
||||
/**
|
||||
* Whom the bot injects with 突变细胞 (after the battle its carrier — deployed or on the bench: every owned carrier's item
|
||||
* hooks run — becomes a random operator one tier higher; items meta char_chess_transformation_equip): the least
|
||||
* hooks run — is destroyed and a random operator one tier higher joins the bench, PlayerState.transformChess; the next
|
||||
* prep's lineup step deploys it like any owned unit, so a deployed carrier only costs a re-placement): the least
|
||||
* valuable normal operator below 6阶 with a free equip slot — never an elite, never one of a merge pair (the merge
|
||||
* progress would be lost), nobody already carrying a cell. null: the cell waits in the hand.
|
||||
*/
|
||||
|
||||
+14
-10
@@ -17,9 +17,10 @@
|
||||
// use_equip_recruit_new_char_and_give_char_to_player_most_bond {refresh_cnt} 信标 destroy target, offer N same-tier
|
||||
// chess, gift the original next prep
|
||||
// sell_char_count_gain_equip_owner_bond {count} 商业包装方案 every `count` sells → same-bond chess
|
||||
// char_chess_transformation_equip 突变细胞 after battle: holder → random NORMAL tier+1
|
||||
// (max 6); its equipment, the cell included,
|
||||
// returns to the hand (not consumed)
|
||||
// char_chess_transformation_equip 突变细胞 after battle: holder destroyed, its equipment
|
||||
// (the cell included — not consumed) back to
|
||||
// the hand, then a random NORMAL tier+1 (max 6)
|
||||
// operator gained into the hand
|
||||
// trap_copy_front_char 画卷 (Art) copy the chess on the tile / in front
|
||||
// trap_create_self_choice {choice_event} 教鞭 / 神秘顾客 (Art) add a random bounty to your next battle
|
||||
// [ASSUMED simplification, documented in docs/META.md: 教鞭/神秘顾客 pick the bounty for the player instead of opening
|
||||
@@ -177,12 +178,15 @@ const ITEM_HANDLERS = {
|
||||
}
|
||||
},
|
||||
},
|
||||
// 突变细胞 "战斗结束后,装备者替换为高一阶的随机干员": the carrier is replaced by a random NORMAL operator one tier higher
|
||||
// (at most 6; an elite carrier too — PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "生效时,原干员销毁,获得一名高一阶的随机
|
||||
// 初始干员(最高六阶)"); the destroyed operator's equipment, the cell included, returns to the hand (PRTS 卫戍协议/帮助
|
||||
// "佩戴的装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除"; the official text never
|
||||
// says 销毁 for the cell — unlike every consumable item —, and players re-inject it every round: "之后就是一直打针,扎到
|
||||
// 核心卡…就换人扎", bilibili cv47000418; player feedback after 0.1.0). The new operator keeps the tile when legal.
|
||||
// 突变细胞 "战斗结束后,装备者替换为高一阶的随机干员": PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "生效时,原干员销毁,获得
|
||||
// 一名高一阶的随机初始干员(最高六阶)" — the carrier (deployed or on the bench) is destroyed and a random NORMAL operator
|
||||
// one tier higher (at most 6; an elite carrier too) is gained like any gained operator: into the 整备区 (overflow temp),
|
||||
// never onto the carrier's tile — official footage (bilibili BV1vzyVBuEN9 ≈ 8:24, BV1Qkw1zMEoR ≈ 7:25): at the next
|
||||
// prep the tile is empty, one more deployment is left and the new operator waits on the bench (pointed out in PR #2).
|
||||
// The destroyed operator's equipment, the cell included, returns to the hand first (PRTS 卫戍协议/帮助 "佩戴的装备无法
|
||||
// 手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除"; the official text never says 销毁 for the
|
||||
// cell — unlike every consumable item —, and players re-inject it every round: "之后就是一直打针,扎到核心卡…就换人扎",
|
||||
// bilibili cv47000418; player feedback after 0.1.0). PlayerState.transformChess.
|
||||
char_chess_transformation_equip: {
|
||||
onBattleResult(ctx) {
|
||||
const { piece, holder } = ctx.source;
|
||||
@@ -190,7 +194,7 @@ const ITEM_HANDLERS = {
|
||||
const tier = Math.min(6, ctx.gd.tierOf(holder.id) + 1);
|
||||
const id = ctx.rollChess({ tier });
|
||||
if (!id) return;
|
||||
ctx.transform(holder.uid, ctx.gd.baseIdOf(id), { returnItems: true });
|
||||
ctx.transform(holder.uid, ctx.gd.baseIdOf(id));
|
||||
},
|
||||
},
|
||||
trap_copy_front_char: {
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
// owned chess (ev.source.holder). That step walks a snapshot — the owned chess and each holder's items as they stand
|
||||
// when it begins (every [holder, item] pair, taken before the first item runs) — and runs an item only if, when its
|
||||
// turn comes, it is still equipped on that holder and the holder is still owned: handlers move and destroy pieces
|
||||
// mid-walk (突变细胞 transforms its holder and returns the equipment to the hand; normal 博士投影 destroys itself, which
|
||||
// splices holder.items), and a live walk skipped the next item or ran it with a holder that was gone. Pieces gained or
|
||||
// equipped during the walk wait for the next dispatch; an item moved during it runs at most once (on the holder it
|
||||
// stood on, if its turn came before the move).
|
||||
// mid-walk (突变细胞 destroys its holder, returns the equipment to the hand and gains an operator; normal 博士投影
|
||||
// destroys itself, which splices holder.items), and a live walk skipped the next item or ran it with a holder that was
|
||||
// gone. Pieces gained or equipped during the walk wait for the next dispatch; an item moved during it runs at most once
|
||||
// (on the holder it stood on, if its turn came before the move).
|
||||
// Dispatch order per player: global → band → bonds → garrisons (board reading order,
|
||||
// then hand) → equipped items → effects (insertion order); onPrice runs the priced chess's own 特质 first (购买价格为N
|
||||
// sets the price the discounts and caps of bonds / strategies then act on — user playtest #5). Every call is
|
||||
@@ -519,9 +519,10 @@ export function makeCtx(m, ps, source, hook, ev = null) {
|
||||
return m.dispatcher.triggerGarrisons(ps, l.piece, eventType, { asPiece: as ? as.piece : null, triggeredBy: source.key || null });
|
||||
},
|
||||
promote: (uid) => { const l = ps.find(uid); return l ? ps.promote(l.piece) : false; },
|
||||
/** Replace a chess piece by another chess (keeps its tile when legal, keeps its equipment — `{ returnItems: true }`:
|
||||
* the equipment returns to the hand instead, as from a destroyed operator; PlayerState.transformChess). */
|
||||
transform: (uid, chessId, opts = {}) => { const l = ps.find(uid); if (!l || l.piece.kind !== 'chess') return null; const p = ps.transformChess(l.piece, chessId, { returnItems: !!(opts && opts.returnItems) }); return p ? view(p) : null; },
|
||||
/** 突变细胞's transformation: destroy a chess piece wherever it stands (its equipment returns to the hand first), then
|
||||
* gain `chessId` like any gained operator — hand, overflow temp, a completed merge as usual
|
||||
* (PlayerState.transformChess). Returns the gained piece's view (the elite after a merge) or null. */
|
||||
transform: (uid, chessId) => { const l = ps.find(uid); if (!l || l.piece.kind !== 'chess') return null; const p = ps.transformChess(l.piece, chessId); return p ? view(p) : null; },
|
||||
/** Normal item piece → its golden version in place. */
|
||||
upgradeItem: (uid) => { const l = ps.find(uid); return l && l.piece.kind === 'item' ? ps.upgradeItem(l.piece) : false; },
|
||||
/** Attach an owned hand/temp item to an owned chess without equip effects (copies, restores). */
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
// The engine built-ins (server/match/builtinMeta.js) already implement most consume-on-equip items and Arts from the
|
||||
// concrete item's params; they are reviewed and kept (盟约之币, 骑士储蓄罐, 随身身份牌, 精打细算玩偶, 简易通讯机 (tier ≤
|
||||
// shop level), 见钱眼开玩偶, 人事部文档 (cap 9), 博士投影 (golden now / normal at the next round start), 拟态物质, 信标,
|
||||
// 商业包装方案 (count from the concrete record: 8 / golden 7), 突变细胞 (after the battle the carrier becomes a NORMAL
|
||||
// random tier+1 operator, max 6; its equipment — the cell too, which is not consumed (player feedback after 0.1.0) —
|
||||
// returns to the hand)). The Arts' rangeGrid (画卷 = placed tile + the tile in front) and the per-round limit are engine
|
||||
// rules.
|
||||
// 商业包装方案 (count from the concrete record: 8 / golden 7), 突变细胞 (after the battle the carrier is destroyed, its
|
||||
// equipment — the cell too, which is not consumed (player feedback after 0.1.0) — returns to the hand, then a NORMAL
|
||||
// random tier+1 operator, max 6, is gained into the hand, not onto the carrier's tile (PR #2)). The Arts' rangeGrid
|
||||
// (画卷 = placed tile + the tile in front) and the per-round limit are engine rules.
|
||||
// Overridden / added here (a built-in, when present, is wrapped — never re-implemented):
|
||||
// 画卷 the copy keeps the target's items; a copied normal item that pairs with an owned one merges and the
|
||||
// golden stays in the hand (the built-in equipped the merged golden on the copy, and its live loop over
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { makeMatch, give, giveItem, DATA } from '../match/harness.js';
|
||||
import { makeMatch, give, giveItem, DATA, legalTileFor } from '../match/harness.js';
|
||||
import { createRegistry } from '../../server/match/effectsMeta.js';
|
||||
import { lendItemEffects, itemGrants, PRIO_REVIVE } from '../../server/sim/content/items/battle.js';
|
||||
import { unitBonds } from '../../server/sim/content/support/index.js';
|
||||
@@ -993,6 +993,24 @@ test('突变细胞: after a battle the carrier becomes a random NORMAL tier+1 op
|
||||
cover(A('5_08'), B('5_08'));
|
||||
});
|
||||
|
||||
test('突变细胞: a DEPLOYED carrier leaves the field — the new operator is gained into the 整备区, the deploy slot comes back (PR #2)', () => {
|
||||
// official footage (bilibili BV1vzyVBuEN9, BV1Qkw1zMEoR): the tile is empty at the next prep and the new operator waits
|
||||
// on the bench; the case added by the closed PR #2, on the destroy-then-gain rule (DESIGN §21.1)
|
||||
const { m, ps, equip } = setup({ seed: 3 });
|
||||
const cid = plain((c) => c.tier === 2)[0];
|
||||
const holder = give(m, ps, cid, 'board', legalTileFor(m, ps, cid));
|
||||
assert.deepEqual(equip(giveItem(m, ps, A('5_08')), holder), OK);
|
||||
assert.equal(ps.deployCount, 1, 'the carrier is deployed');
|
||||
m.dispatch(ps, 'onBattleResult', { result: {}, lpLoss: 0, perfect: true });
|
||||
assert.equal(ps.board.size, 0, 'the carrier left the field');
|
||||
assert.equal(ps.deployCount, 0, 'the deploy slot came back');
|
||||
const chess = ps.hand.filter((p) => p && p.kind === 'chess');
|
||||
assert.equal(chess.length, 1, 'the new operator waits in the 整备区');
|
||||
assert.equal(DATA.chess[chess[0].id].tier, 3, 'the random tier+1 operator');
|
||||
assert.equal(DATA.chess[chess[0].id].isGolden, false);
|
||||
assert.ok(handIds(ps, 'item').includes(A('5_08')), 'the cell back in the hand');
|
||||
});
|
||||
|
||||
test('人事部文档: deploy cap becomes 9 (a second copy adds nothing)', () => {
|
||||
const { m, ps, equip } = setup();
|
||||
const t = give(m, ps, plain((c) => c.tier === 1)[0], 'hand');
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
// playtest6b follow-up (DESIGN §20.10–§20.13): leader HP and 直接乘算, the elite on the consumed copy's tile, the official
|
||||
// 999 layer cap (one implementation) and 限伤 300000, the 假想敌:胄 kit — and the normative lines they rewrote; player
|
||||
// feedback after 0.1.0 (DESIGN §21, v0.1.1) including batch 6 (§21.21–§21.25: 坚固维式重锤 once per deployment, 起飞,
|
||||
// fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7).
|
||||
// fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7) and
|
||||
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -765,3 +766,28 @@ test('batch 6 QA residuals (DESIGN §21.21–§21.25): the lock per deployment f
|
||||
assert.match(log, /卢西恩、锏不会再因为身边只有起飞的蒂比就放出打不到人的范围技能/);
|
||||
assert.match(log, /倒在已消失的召唤物(如浊心斯卡蒂的海嗣)初始位置上的干员会回到自己的初始位置躺下/);
|
||||
});
|
||||
|
||||
test('突变细胞 after the WA merge (DESIGN §21.1): the carrier is destroyed, its new operator is gained into the 整备区 — code and every doc agree', async () => {
|
||||
const S21 = DESIGN.slice(DESIGN.indexOf('## 21.'));
|
||||
const s211 = S21.slice(S21.indexOf('### 21.1 '), S21.indexOf('\n### 21.2 '));
|
||||
// the evidence: the two official videos, PRTS, the pull request
|
||||
for (const re of [/BV1vzyVBuEN9 \(上半, ≈ 8:24\)/, /BV1Qkw1zMEoR \(下半, ≈ 7:25\)/, /"原干员销毁,获得一名…"/, /被发送至手牌区的物资优先从右到左填充空位/, /PR #2/]) assert.match(s211, re);
|
||||
// the WA wording (the new operator on the carrier's tile, the tile counting for a merge) is gone everywhere
|
||||
assert.ok(!/one tier higher \(max 6; an elite carrier too\) on its tile when legal/.test(DESIGN), 'DESIGN §21.1: the keep-the-tile rule is gone');
|
||||
assert.ok(!/counts as deployed with its own tile \[ASSUMED\]/.test(DESIGN), 'DESIGN §20.11: the transformed tile no longer counts');
|
||||
assert.ok(!/a transformed carrier's tile and the elite's fresh summon stack/.test(DESIGN), 'DESIGN §20.9: no longer [ASSUMED]');
|
||||
assert.ok(!/whose 突变细胞 rule is superseded by §21\.1/.test(DESIGN), 'DESIGN §21.25: PR #2 credited, not superseded');
|
||||
assert.match(META, /never onto the carrier's tile/);
|
||||
assert.ok(!/one tier higher, max 6, on the carrier's tile/.test(META), 'META: the built-in line follows the rule');
|
||||
assert.ok(!/counts with its own tile/.test(META), 'META: the merge line follows the rule');
|
||||
assert.match(PLAYING, /原来的格子空出来,剩余可放置角色加 1/);
|
||||
assert.match(DATA.items.chess_item_5_08_e_a.note, /进入整备区,需要重新部署/);
|
||||
// the code: a destroy, then a gain through acquireChess; _mergeChess has no carrier-tile option left
|
||||
const PS = doc('server/match/PlayerState.js');
|
||||
const { PlayerState } = await import('../server/match/PlayerState.js');
|
||||
assert.equal(PlayerState.prototype.transformChess.length, 2, 'transformChess(piece, newId)');
|
||||
assert.equal(PlayerState.prototype._mergeChess.length, 2, '_mergeChess(baseId, incoming)');
|
||||
assert.ok(!/fromKey|returnItems/.test(PS), 'no fromKey / returnItems left in PlayerState');
|
||||
assert.match(PS, /const np = this\.acquireChess\(newId, \{ source: 'transform' \}\);/);
|
||||
assert.ok(!/fromKey/.test(doc('server/match/audit.js')), 'audit.js: no exception for a transformed carrier\'s tile');
|
||||
});
|
||||
@@ -134,14 +134,16 @@ test('the dir persists across rounds and reaches the round\'s battle', () => {
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('effect-placed pieces: a transformation / a merge elite taking a consumed copy\'s tile keep that tile\'s dir', () => {
|
||||
test('effect-placed pieces: a transformation leaves the tile (the new operator waits in the hand); a merge elite taking a consumed copy\'s tile keeps that tile\'s dir', () => {
|
||||
const { m, ps } = prepMatch({ seed: 13 });
|
||||
const [id, other] = meleeIds(m, 2);
|
||||
const a = give(m, ps, id, 'board', [9, 3]);
|
||||
a.dir = 'DOWN';
|
||||
const np = ps.transformChess(a, other);
|
||||
assert.ok(np && ps.board.get(tileKey(9, 3)) === np, 'kept the tile');
|
||||
assert.equal(np.dir, 'DOWN');
|
||||
// 突变细胞 (PRTS 备注 "原干员销毁,获得一名…干员"; official footage, PR #2): the tile is freed, nothing inherits its dir
|
||||
assert.ok(np && !ps.board.has(tileKey(9, 3)), 'the tile is freed');
|
||||
assert.ok(ps.hand.includes(np), 'the new operator waits in the hand');
|
||||
assert.equal(ps.deployCount, 0, 'one deployment fewer');
|
||||
// merge of deployed copies (PRTS: the elite goes to the consumed copy's board position), with its dir
|
||||
ps.board.clear();
|
||||
ps.hand.fill(null);
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
// (each copy counts the manual refreshes it witnessed this round), not the player's.
|
||||
// #4 昆图斯 突变细胞 "战斗结束后,装备者替换为高一阶的随机干员": the cell is not consumed — the original operator is
|
||||
// destroyed (PRTS 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)") and its equipment, the cell
|
||||
// included, returns to the hand, to be equipped again ("之后就是一直打针,扎到核心卡…就换人扎").
|
||||
// included, returns to the hand, to be equipped again ("之后就是一直打针,扎到核心卡…就换人扎"); then the new operator
|
||||
// is gained like any gained operator — into the 整备区, never onto the carrier's tile (official footage: bilibili
|
||||
// BV1vzyVBuEN9 ≈ 8:24, BV1Qkw1zMEoR ≈ 7:25 — the tile is empty at the next prep, one more deployment is left, the
|
||||
// new operator waits on the bench; pointed out in PR #2). Bots / AI 托管 deploy it in their normal placement step.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeMatch, give, giveItem, legalTileFor, checkInvariants, DATA } from './harness.js';
|
||||
import { createRegistry } from '../../server/match/effectsMeta.js';
|
||||
import { botPrep, botPrepEnd, cellTarget } from '../../server/match/bot.js';
|
||||
import { mergeTile } from '../../server/match/board.js';
|
||||
|
||||
const QUIET = { warn() {}, error() {}, info() {} };
|
||||
const REG = createRegistry({ log: QUIET });
|
||||
@@ -164,37 +168,91 @@ const plainOf = (m, tier) => Object.values(DATA.chess)
|
||||
.map((c) => c.chessId).sort();
|
||||
const ownedItems = (ps, id) => [...ps.hand, ...ps.temp].filter((p) => p && p.kind === 'item' && p.id === id);
|
||||
const equip = (m, item, target) => m.handle('p_0', { t: 'g.equip', itemUid: item.uid, targetUid: target.uid });
|
||||
const SWORD = 'chess_item_1_01_e_a'; // 维式重锤
|
||||
const PACK = 'chess_item_5_07_e_a'; // 商业包装方案 (counts sells per item: player counter `pack:<item uid>`)
|
||||
const VIGIL = 'chess_char_3_19_a'; // 伺夜 (Ⅲ) — a placeable 狼群 summon card
|
||||
/** Distinct normal equipment ids (never merge with each other), the cell excluded. */
|
||||
const fillerItems = () => Object.values(DATA.items).filter((it) => it.itemType === 'EQUIP' && !it.isGolden && it.id !== CELL).map((it) => it.id).sort();
|
||||
const unusedFillers = (ps) => fillerItems().filter((id) => ![...ps.hand, ...ps.temp].some((p) => p && p.id === id));
|
||||
/** Fill every free hand slot but `leave` with distinct items. */
|
||||
const fillHand = (m, ps, leave = 1) => {
|
||||
const ids = unusedFillers(ps);
|
||||
while (ps.hand.filter((x) => x == null).length > leave) giveItem(m, ps, ids.shift());
|
||||
};
|
||||
/** Fill every free temp slot with distinct items. */
|
||||
const fillTemp = (m, ps) => {
|
||||
const ids = unusedFillers(ps);
|
||||
while (ps.temp.some((x) => x == null)) giveItem(m, ps, ids.shift(), 'temp');
|
||||
};
|
||||
/** Force the cell's roll (the random draw) of `tier` to `id`; everything else stays the real path. */
|
||||
const forceRoll = (m, tier, id) => { const roll = m.pool.roll.bind(m.pool); m.pool.roll = (rng, o = {}) => (o.tier === tier ? id : roll(rng, o)); };
|
||||
const where = (ps, pred) => (ps.hand.some((p) => p && pred(p)) ? 'hand' : ps.temp.some((p) => p && pred(p)) ? 'temp' : null);
|
||||
const handIdx = (ps, pred) => ps.hand.findIndex((p) => p && pred(p));
|
||||
/** The round loop's SETTLE dispatch, alone. */
|
||||
const battleResult = (m, ps) => m.dispatch(ps, 'onBattleResult', { result: {}, lpLoss: 0, perfect: true });
|
||||
|
||||
test('#4 players\' report: 突变细胞 is not used up — after each battle the carrier is replaced (normal, tier +1) and the cell returns to the hand', () => {
|
||||
test('#4 players\' report and the official flow: after each battle the deployed carrier is destroyed — its tile empties, one more deployment is left —, its equipment (the cell too) comes back first, then a NORMAL tier+1 operator joins the hand', () => {
|
||||
const s = setup({ seed: 5 });
|
||||
const { h, m, ps } = s;
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
const tile = ps.find(carrier.uid).key;
|
||||
const cell = giveItem(m, ps, CELL);
|
||||
const sword = giveItem(m, ps, 'chess_item_1_01_e_a'); // 维式重锤: other equipment returns too
|
||||
const sword = giveItem(m, ps, SWORD); // other equipment returns too
|
||||
assert.deepEqual(equip(m, cell, carrier), OK);
|
||||
assert.deepEqual(equip(m, sword, carrier), OK);
|
||||
assert.equal(ps.deployCount, 1);
|
||||
h.toPrep(2);
|
||||
assert.ok(!ps.find(carrier.uid), 'the original operator is gone (原干员销毁)');
|
||||
const next = ps.board.get(tile);
|
||||
assert.ok(next && next.kind === 'chess', 'the new operator takes the tile');
|
||||
assert.ok(!ps.board.has(tile), 'its tile is empty (bilibili BV1vzyVBuEN9 ≈ 8:24, BV1Qkw1zMEoR ≈ 7:25)');
|
||||
assert.equal(ps.deployCount, 0, 'one more deployment is left');
|
||||
const next = ps.hand.find((p) => p && p.kind === 'chess');
|
||||
assert.ok(next, 'the new operator waits in the hand (整备区)');
|
||||
assert.equal(m.gd.chess(next.id).tier, 3, 'one tier higher');
|
||||
assert.ok(!m.gd.isGolden(next.id), 'an initial (normal) operator');
|
||||
assert.deepEqual(next.items, [], 'its equipment came off');
|
||||
assert.deepEqual(next.items, [], 'nothing equipped');
|
||||
assert.equal(ownedItems(ps, CELL).length, 1, 'the cell is back in the hand, not consumed');
|
||||
assert.equal(ownedItems(ps, 'chess_item_1_01_e_a').length, 1, 'the other equipment too');
|
||||
// "一直打针": inject again — the same new operator here; it climbs once more
|
||||
assert.equal(ownedItems(ps, SWORD).length, 1, 'the other equipment too');
|
||||
// the hand fills right → left ("被发送至手牌区的物资优先从右到左填充空位"): the equipment came off first, then the gain
|
||||
assert.deepEqual([handIdx(ps, (p) => p.id === CELL), handIdx(ps, (p) => p.id === SWORD), ps.hand.indexOf(next)], [9, 8, 7]);
|
||||
assert.ok(ps.tempEmpty);
|
||||
// the player deploys it by hand, then "一直打针": inject again — it climbs once more and comes back to the bench
|
||||
const [row, col] = legalTileFor(m, ps, next.id);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: next.uid, to: { area: 'board', row, col } }), OK);
|
||||
assert.equal(ps.deployCount, 1);
|
||||
assert.deepEqual(equip(m, ownedItems(ps, CELL)[0], next), OK);
|
||||
h.toPrep(3);
|
||||
const third = ps.board.get(tile);
|
||||
assert.equal(m.gd.chess(third.id).tier, 4, 'tier 4 after the second battle');
|
||||
assert.ok(!ps.find(next.uid) && !ps.board.has(`${row},${col}`), 'the second carrier is gone too, its tile empty');
|
||||
assert.equal(ps.deployCount, 0);
|
||||
const third = ps.hand.find((p) => p && p.kind === 'chess');
|
||||
assert.equal(m.gd.chess(third.id).tier, 4, 'tier 4 after the second battle, on the bench');
|
||||
assert.equal(ownedItems(ps, CELL).length, 1, 'and the cell is back again');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 突变细胞 details: an elite carrier → a NORMAL operator one tier higher; 6阶 → another 6阶; no room → it stays on the new operator', () => {
|
||||
test('#4 bench carriers transform too: a hand or temp carrier leaves its slot; the cell, then the new operator, fill the hand right → left', () => {
|
||||
for (const area of ['hand', 'temp']) {
|
||||
const s = setup({ seed: 6 });
|
||||
const { m, ps } = s;
|
||||
const t1 = plainOf(m, 1)[0];
|
||||
const carrier = give(m, ps, t1, area);
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
battleResult(m, ps);
|
||||
assert.ok(!ps.find(carrier.uid), `${area}: the carrier is destroyed`);
|
||||
const got = ps.allChess();
|
||||
assert.equal(got.length, 1, `${area}: one operator gained`);
|
||||
assert.equal(m.gd.chess(got[0].id).tier, 2, `${area}: one tier higher`);
|
||||
assert.ok(!m.gd.isGolden(got[0].id));
|
||||
assert.deepEqual([handIdx(ps, (p) => p.id === CELL), ps.hand.indexOf(got[0])], [9, 8], `${area}: the cell first, then the gain`);
|
||||
assert.ok(ps.tempEmpty, `${area}: nothing waits in temp`);
|
||||
assert.equal(ps.deployCount, 0);
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('#4 突变细胞 details: an elite carrier → a NORMAL operator one tier higher; 6阶 → another 6阶', () => {
|
||||
{
|
||||
const s = setup({ seed: 9 });
|
||||
const { h, m, ps } = s;
|
||||
@@ -222,23 +280,85 @@ test('#4 突变细胞 details: an elite carrier → a NORMAL operator one tier h
|
||||
assert.equal(ownedItems(ps, CELL).length, 1);
|
||||
m.dispose();
|
||||
}
|
||||
{
|
||||
// hand and temp full: the cell cannot come off and stays on the new operator (nothing is lost)
|
||||
const s = setup({ seed: 11 });
|
||||
const { m, ps } = s;
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
const at = ps.find(carrier.uid).key;
|
||||
for (let i = 0; i < ps.hand.length; i++) if (!ps.hand[i]) giveItem(m, ps, 'chess_item_1_01_e_a', 'hand', i);
|
||||
for (let i = 0; i < ps.temp.length; i++) if (!ps.temp[i]) giveItem(m, ps, 'chess_item_1_01_e_a', 'temp', i);
|
||||
m.dispatch(ps, 'onBattleResult', { result: {}, lpLoss: 0, perfect: true });
|
||||
const next = ps.board.get(at);
|
||||
assert.ok(next && next.uid !== carrier.uid && m.gd.chess(next.id).tier === 3);
|
||||
assert.deepEqual(next.items.map((it) => it.id), [CELL], 'kept on the new operator');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('#4 hand full: the returned cell takes the last hand slot, the new operator overflows into temp — with no summon card until it is deployed', () => {
|
||||
const s = setup({ seed: 31 });
|
||||
const { m, ps } = s;
|
||||
forceRoll(m, 3, VIGIL);
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
const at = ps.find(carrier.uid).key;
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
fillHand(m, ps, 1);
|
||||
battleResult(m, ps);
|
||||
assert.ok(!ps.board.has(at), 'the carrier\'s tile is empty');
|
||||
assert.equal(ps.deployCount, 0);
|
||||
assert.equal(where(ps, (p) => p.id === CELL), 'hand', 'the cell came back first, into the last hand slot');
|
||||
const vigil = ps.temp.find((p) => p && p.id === VIGIL);
|
||||
assert.ok(vigil, '伺夜 overflows into temp like any gain with a full hand');
|
||||
assert.equal(where(ps, (p) => p.kind === 'token'), null, 'no 狼群 card yet: a summon card comes with a deployment');
|
||||
// deployed by hand from temp, it brings its card like any operator placed there
|
||||
const [row, col] = legalTileFor(m, ps, VIGIL);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: vigil.uid, to: { area: 'board', row, col } }), OK);
|
||||
assert.ok(where(ps, (p) => p.kind === 'token' && p.ownerUid === vigil.uid), 'its 狼群 card arrived');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 hand and temp full: the gained operator follows the no-room rule of every gain (back to the pool, the same toast); the carrier is destroyed all the same', () => {
|
||||
const s = setup({ seed: 11 });
|
||||
const { h, m, ps } = s;
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const t3 = plainOf(m, 3)[0];
|
||||
forceRoll(m, 3, t3);
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
const at = ps.find(carrier.uid).key;
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
fillHand(m, ps, 0);
|
||||
fillTemp(m, ps);
|
||||
const left0 = m.pool.left(t3);
|
||||
const left2 = m.pool.left(t2);
|
||||
const toasts = [];
|
||||
const toast = m.toast.bind(m);
|
||||
m.toast = (who, kind, text, ...rest) => { if (who === ps) toasts.push(text); return toast(who, kind, text, ...rest); };
|
||||
battleResult(m, ps);
|
||||
assert.ok(!ps.board.has(at), 'the carrier is destroyed wherever it stands');
|
||||
assert.equal(ps.deployCount, 0);
|
||||
assert.equal(m.pool.left(t2), left2 + 1, 'its copy went back to the pool');
|
||||
assert.ok(!ps.allChess().some((p) => p.id === t3), 'no slot anywhere: the gained operator is not kept');
|
||||
assert.equal(m.pool.left(t3), left0, 'its copy went back to the pool (PlayerState.acquireChess)');
|
||||
assert.ok(toasts.includes('整备区已满,获得的干员已返还'), 'the toast of any gained operator with no room');
|
||||
// the cell found no slot and no gained operator to stay on: destroyed with a log warning, as in a merge
|
||||
assert.equal(ownedItems(ps, CELL).length, 0);
|
||||
assert.ok(h.logs.warn.some((w) => w.includes(`returned item ${CELL} destroyed (no space)`)), 'logged');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 per-piece state rides on the returned equipment: 商业包装方案 keeps its sell count through the transformation', () => {
|
||||
const s = setup({ seed: 36 });
|
||||
const { m, ps } = s;
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
const pack = giveItem(m, ps, PACK);
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
assert.deepEqual(equip(m, pack, carrier), OK);
|
||||
const [fodder] = plainOf(m, 1);
|
||||
const sell = () => assert.deepEqual(m.handle('p_0', { t: 'g.sell', uid: give(m, ps, fodder, 'hand').uid }), OK);
|
||||
for (let i = 0; i < 3; i++) sell();
|
||||
const key = `pack:${pack.uid}`;
|
||||
assert.equal(ps.counters[key], 3, 'three sells counted while equipped');
|
||||
battleResult(m, ps);
|
||||
const back = ownedItems(ps, PACK)[0];
|
||||
assert.ok(back && back.uid === pack.uid, 'the same item came back to the hand');
|
||||
assert.equal(ps.counters[key], 3, 'with its progress');
|
||||
const next = ps.hand.find((p) => p && p.kind === 'chess');
|
||||
assert.deepEqual(equip(m, back, next), OK);
|
||||
sell();
|
||||
assert.equal(ps.counters[key], 4, 'it goes on counting on its next carrier');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 strategy 昆图斯 end to end: the R3 cell survives its first transformation; a bot injects its weakest single normal operator, never an elite', () => {
|
||||
@@ -281,55 +401,158 @@ test('#4 strategy 昆图斯 end to end: the R3 cell survives its first transform
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// #4 follow-up: the returned cell must not be pushed into temp by the new operator's summon card (it would be lost at
|
||||
// the deadline), and a bot never throws it away
|
||||
// #4: a gain that completes a merge merges like any gained copy (DESIGN §20.11) — the elite goes to a consumed DEPLOYED
|
||||
// copy's tile (board.js mergeTile), else to the hand; the destroyed carrier's tile is no copy's. The returned cell is
|
||||
// stowed before the elite's summon card (it would be lost in temp at the deadline).
|
||||
|
||||
const VIGIL = 'chess_char_3_19_a'; // 伺夜 (Ⅲ) — a placeable 狼群 summon card
|
||||
/** Distinct normal equipment ids (never merge with each other), the cell excluded. */
|
||||
const fillerItems = () => Object.values(DATA.items).filter((it) => it.itemType === 'EQUIP' && !it.isGolden && it.id !== CELL).map((it) => it.id).sort();
|
||||
/** Fill every free hand slot but `leave` with distinct items. */
|
||||
const fillHand = (m, ps, leave = 1) => {
|
||||
const ids = fillerItems().filter((id) => ![...ps.hand, ...ps.temp].some((p) => p && p.id === id));
|
||||
while (ps.hand.filter((x) => x == null).length > leave) giveItem(m, ps, ids.shift());
|
||||
};
|
||||
/** Force the cell's roll (the random draw) to 伺夜 for tier 3; everything else stays the real path. */
|
||||
const forceVigil = (m) => { const roll = m.pool.roll.bind(m.pool); m.pool.roll = (rng, o = {}) => (o.tier === 3 ? VIGIL : roll(rng, o)); };
|
||||
const where = (ps, pred) => (ps.hand.some((p) => p && pred(p)) ? 'hand' : ps.temp.some((p) => p && pred(p)) ? 'temp' : null);
|
||||
|
||||
test('#4 one free hand slot, the new operator brings a summon card: the returned cell takes the slot, the card waits in temp', () => {
|
||||
const s = setup({ seed: 31 });
|
||||
test('#4 the gain completes a merge with deployed copies: the elite takes the first deployed copy\'s tile, never the carrier\'s; the cell is returned before the elite\'s summon card', () => {
|
||||
const s = setup({ seed: 32 });
|
||||
const { m, ps } = s;
|
||||
forceVigil(m);
|
||||
forceRoll(m, 3, VIGIL);
|
||||
// the carrier deploys FIRST: its tile leads the deploy order (under the 0.1.1 WA rule the elite took it)
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
const at = ps.find(carrier.uid).key;
|
||||
const ct = ps.find(carrier.uid).key;
|
||||
const copies = [give(m, ps, VIGIL, 'board', legalTileFor(m, ps, VIGIL))];
|
||||
copies.push(give(m, ps, VIGIL, 'board', legalTileFor(m, ps, VIGIL)));
|
||||
copies[0].dir = 'UP';
|
||||
copies[1].dir = 'DOWN';
|
||||
for (const c of copies) ps.removeTokensOf(c.uid); // keep the hand count exact
|
||||
const copyTiles = copies.map((c) => ps.find(c.uid).key);
|
||||
assert.equal(mergeTile([ct, ...copyTiles].map((key) => ({ key }))).key, ct, 'the carrier\'s tile is the first in deploy order');
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
fillHand(m, ps, 1);
|
||||
m.dispatch(ps, 'onBattleResult', { result: {}, lpLoss: 0, perfect: true });
|
||||
const next = ps.board.get(at);
|
||||
assert.equal(next && next.id, VIGIL, '伺夜 takes the carrier\'s tile');
|
||||
assert.equal(where(ps, (p) => p.id === CELL), 'hand', 'the cell came back into the hand (it would be lost in temp at the deadline)');
|
||||
assert.equal(where(ps, (p) => p.kind === 'token' && p.ownerUid === next.uid), 'temp', 'the 狼群 card overflows into temp (it comes back at the next round start)');
|
||||
battleResult(m, ps);
|
||||
const want = mergeTile(copyTiles.map((key) => ({ key })));
|
||||
const elite = ps.board.get(want.key);
|
||||
assert.ok(elite && elite.id === m.gd.goldenIdOf(VIGIL), 'the elite 伺夜 stands on the first deployed copy\'s tile');
|
||||
assert.equal(elite.dir, want.key === copyTiles[0] ? 'UP' : 'DOWN', 'with that copy\'s facing');
|
||||
assert.ok(!ps.board.has(ct), 'the carrier\'s tile is empty');
|
||||
assert.equal(ps.deployCount, 1, 'carrier + two copies deployed → the elite alone');
|
||||
assert.equal(where(ps, (p) => p.id === CELL), 'hand', 'the cell came back first, into the last hand slot');
|
||||
assert.equal(where(ps, (p) => p.kind === 'token' && p.ownerUid === elite.uid), 'temp', 'the elite\'s 狼群 card overflows into temp');
|
||||
assert.equal(ps.stats.merges, 1);
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 the transformation completes a merge on the board: the cell is returned before the elite\'s summon card', () => {
|
||||
const s = setup({ seed: 32 });
|
||||
test('#4 the gain completes a merge with no deployed copy: the elite goes to the hand (no summon card); the carrier\'s tile stays empty', () => {
|
||||
const s = setup({ seed: 35 });
|
||||
const { m, ps } = s;
|
||||
forceVigil(m);
|
||||
const copies = [give(m, ps, VIGIL, 'board', legalTileFor(m, ps, VIGIL))];
|
||||
copies.push(give(m, ps, VIGIL, 'board', legalTileFor(m, ps, VIGIL)));
|
||||
for (const c of copies) ps.removeTokensOf(c.uid); // keep the hand count exact
|
||||
forceRoll(m, 3, VIGIL);
|
||||
const t2 = plainOf(m, 2)[0];
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
const ct = ps.find(carrier.uid).key;
|
||||
const copies = [give(m, ps, VIGIL, 'hand'), give(m, ps, VIGIL, 'hand')];
|
||||
assert.deepEqual(equip(m, giveItem(m, ps, CELL), carrier), OK);
|
||||
fillHand(m, ps, 1);
|
||||
m.dispatch(ps, 'onBattleResult', { result: {}, lpLoss: 0, perfect: true });
|
||||
const elite = [...ps.board.values()].find((p) => p.id === m.gd.goldenIdOf(VIGIL));
|
||||
assert.ok(elite, 'the elite 伺夜 is deployed on a consumed copy\'s tile');
|
||||
assert.equal(where(ps, (p) => p.id === CELL), 'hand', 'the cell is in the hand');
|
||||
assert.equal(where(ps, (p) => p.kind === 'token' && p.ownerUid === elite.uid), 'temp', 'the elite\'s 狼群 card in temp');
|
||||
battleResult(m, ps);
|
||||
const elite = ps.allChess().find((p) => p.id === m.gd.goldenIdOf(VIGIL));
|
||||
assert.ok(elite, 'the gain completed the merge');
|
||||
assert.ok(copies.every((c) => !ps.find(c.uid)), 'both bench copies were consumed');
|
||||
assert.equal(ps.find(elite.uid).area, 'hand', 'no consumed copy was deployed (the carrier\'s tile is no copy\'s): the hand');
|
||||
assert.ok(!ps.board.has(ct), 'the carrier\'s tile is empty');
|
||||
assert.equal(ps.deployCount, 0);
|
||||
assert.equal(where(ps, (p) => p.kind === 'token'), null, 'an elite on the bench brings no summon card');
|
||||
assert.equal(where(ps, (p) => p.id === CELL), 'hand');
|
||||
assert.equal(ps.stats.merges, 1);
|
||||
assert.deepEqual(ps.offers.map((o) => o.source), ['merge'], 'the promotion reward as for any merge');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 bots and AI 托管 deploy the gained operator in their normal placement step (a real co-op round: SETTLE → the next prep)', () => {
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 1, bots: 1, seed: 41, registry: REG, fake: true }).start();
|
||||
const m = h.m;
|
||||
h.toPrep(1);
|
||||
const human = h.ps('p_0');
|
||||
const bot = h.ps('ai_0');
|
||||
h.run(() => bot.ready); // the bot's own R1 prep is done
|
||||
// each seat: a small board — a tier-2 carrier of the cell and a tier-1 operator, nothing on the bench
|
||||
const [t1a, t1b] = plainOf(m, 1);
|
||||
const [t2a, t2b] = plainOf(m, 2);
|
||||
const before = new Map();
|
||||
for (const [ps, t1, t2] of [[human, t1a, t2a], [bot, t1b, t2b]]) {
|
||||
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean), ...ps.temp.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
||||
ps.board.clear();
|
||||
ps.hand.fill(null);
|
||||
ps.temp.fill(null);
|
||||
ps.offers.length = 0;
|
||||
const carrier = give(m, ps, t2, 'board', legalTileFor(m, ps, t2));
|
||||
give(m, ps, t1, 'board', legalTileFor(m, ps, t1));
|
||||
carrier.items.push(ps.newPiece('item', CELL));
|
||||
ps.recompute();
|
||||
before.set(ps, { uids: new Set(ps.allChess().map((p) => p.uid)), carrier });
|
||||
}
|
||||
// SETTLE: each carrier is destroyed and a tier-3 operator gained into the hand
|
||||
const gained = new Map();
|
||||
const settle = m.settle.bind(m);
|
||||
m.settle = (...a) => {
|
||||
const r = settle(...a);
|
||||
for (const [ps, b] of before) gained.set(ps, ps.allChess().find((p) => !b.uids.has(p.uid)) || null);
|
||||
return r;
|
||||
};
|
||||
// the layout each seat readies with in R2 (its bot prep ends with g.ready)
|
||||
const atReady = new Map();
|
||||
for (const ps of [human, bot]) {
|
||||
const setReady = ps.setReady.bind(ps);
|
||||
ps.setReady = (on) => {
|
||||
const g = gained.get(ps);
|
||||
if (on && m.round === 2 && g && !atReady.has(ps)) atReady.set(ps, { area: ps.find(g.uid)?.area ?? null, deployed: ps.deployCount });
|
||||
return setReady(on);
|
||||
};
|
||||
}
|
||||
h.toPrep(2);
|
||||
for (const [ps, b] of before) {
|
||||
const g = gained.get(ps);
|
||||
assert.ok(g && m.gd.chess(g.id).tier === 3 && !m.gd.isGolden(g.id), `${ps.playerId}: a NORMAL tier-3 operator was gained at SETTLE`);
|
||||
assert.ok(!ps.find(b.carrier.uid), `${ps.playerId}: the carrier is gone`);
|
||||
assert.notEqual(ps.find(g.uid)?.area, 'board', `${ps.playerId}: it starts the prep on the bench`);
|
||||
}
|
||||
assert.ok(!human.ready, 'the human has not readied yet');
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.autoplay', on: true }), OK); // AI 托管 plays the human seat from here
|
||||
h.run(() => atReady.size === 2);
|
||||
for (const ps of [bot, human]) {
|
||||
const r = atReady.get(ps);
|
||||
assert.ok(r, `${ps.playerId}: readied in R2`);
|
||||
assert.equal(r.area, 'board', `${ps.playerId}: the gained operator was deployed by the placement step`);
|
||||
assert.ok(r.deployed >= 2, `${ps.playerId}: the freed deployment is used (${r.deployed} deployed)`);
|
||||
}
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 a bot on 昆图斯 lets the placement step judge each gained operator: the bench shed never sells one in its first prep, and no deployment stays free while one waits on the bench', () => {
|
||||
// a real solo 绝境 bot match on strategy 昆图斯 (the only band on offer, as tools/botbench.mjs --band) through R8: the
|
||||
// buy loop's pre-emptive bench shed sells by piece value while the bench is crowded — before rememberOwned it sold this
|
||||
// seed's R7 operator in the R8 prep (20 such matches: 44 of 222 gained operators, before any placement step saw them)
|
||||
const seats = [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }];
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'HARD', seats, seed: 9, registry: REG });
|
||||
const m = h.m;
|
||||
m.gd.bandIds = () => ['band_quintus'];
|
||||
const ps = h.ps('ai_0');
|
||||
const gained = new Map(); // uid → round of the SETTLE that gained it
|
||||
const shed = [];
|
||||
const waiting = [];
|
||||
const transform = ps.transformChess.bind(ps);
|
||||
ps.transformChess = (...a) => { const np = transform(...a); if (np) gained.set(np.uid, m.round); return np; };
|
||||
const sell = ps.sell.bind(ps);
|
||||
ps.sell = (uid) => {
|
||||
if (gained.get(uid) === m.round - 1 && /buyLoopSteps/.test(new Error().stack)) shed.push(`R${m.round} ${ps.find(uid)?.piece.id}`);
|
||||
return sell(uid);
|
||||
};
|
||||
const setReady = ps.setReady.bind(ps);
|
||||
ps.setReady = (on) => {
|
||||
for (const [uid, r] of gained) {
|
||||
const loc = r === m.round - 1 ? ps.find(uid) : null;
|
||||
if (on && loc && loc.area !== 'board' && ps.deployCount < ps.deployCap) waiting.push(`R${m.round} ${loc.piece.id} ${ps.deployCount}/${ps.deployCap}`);
|
||||
}
|
||||
return setReady(on);
|
||||
};
|
||||
h.start();
|
||||
h.run(() => h.ended != null || (m.phase === 'PREP' && m.round === 9));
|
||||
assert.equal(ps.bandId, 'band_quintus');
|
||||
assert.ok(gained.size >= 5, `the cell transformed after every battle from R3 (${gained.size})`);
|
||||
assert.deepEqual(shed, [], 'no operator sold by the bench shed in the prep after its gain');
|
||||
assert.deepEqual(waiting, [], 'a gained operator off the board only when the board is full');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
@@ -296,7 +296,7 @@ test('item merge with a full hand AND a full temp: the golden item takes the equ
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('a merge completed during SETTLE (突变细胞) keeps its reward offer for the next prep; the elite takes a deployed copy\'s tile (the carrier\'s counts) or goes to the hand', () => {
|
||||
test('a merge completed during SETTLE (突变细胞) keeps its reward offer for the next prep; the elite takes a deployed copy\'s tile (never the carrier\'s) or goes to the hand', () => {
|
||||
const X = 'chess_char_2_04_a';
|
||||
const fillers = Object.values(DATA.items).filter((i) => i.itemType === 'EQUIP' && !i.isGolden && !String(i.kind || '').startsWith('consume')).map((i) => i.itemId ?? i.id).filter(Boolean);
|
||||
// where the carrier of 突变细胞 and the two copies of X are: hand/hand, board/hand, board/board
|
||||
@@ -317,7 +317,9 @@ test('a merge completed during SETTLE (突变细胞) keeps its reward offer for
|
||||
const holder = carrierAt === 'board' ? give(m, ps, T, 'board', legalTileFor(m, ps, T)) : give(m, ps, T);
|
||||
holder.items.push(ps.newPiece('item', 'chess_item_5_08_e_a'));
|
||||
const copies = copiesAt === 'board' ? [0, 1].map(() => give(m, ps, X, 'board', legalTileFor(m, ps, X))) : [give(m, ps, X), give(m, ps, X)];
|
||||
const deployedTiles = [holder, ...copies].map((p) => ps.find(p.uid)).filter((l) => l.area === 'board').map((l) => l.key);
|
||||
// the carrier is destroyed before its gain (PRTS 备注 "原干员销毁,获得一名…"): only the copies' tiles count
|
||||
const holderTile = ps.find(holder.uid).key || null;
|
||||
const deployedTiles = copies.map((p) => ps.find(p.uid)).filter((l) => l.area === 'board').map((l) => l.key);
|
||||
const deployed0 = ps.deployCount;
|
||||
for (let i = 0; ps.hand.some((x) => x == null); i++) giveItem(m, ps, fillers[i]);
|
||||
const fillers0 = ps.hand.filter((p) => p && p.kind === 'item').length;
|
||||
@@ -332,10 +334,10 @@ test('a merge completed during SETTLE (突变细胞) keeps its reward offer for
|
||||
assert.deepEqual(ps.offers.map((o) => o.source), ['merge'], 'the promotion reward waits for this prep');
|
||||
assert.ok(ps.privateView().shop.rewardOffer, 'and is shown');
|
||||
const loc = ps.find(elite.uid);
|
||||
if (holderTile) assert.ok(!ps.board.has(holderTile), `${label}: the carrier's tile is empty`);
|
||||
if (deployedTiles.length) {
|
||||
// PRTS 卫戍协议/帮助: a merge consuming a deployed copy sends the elite to that copy's tile — outside PREP too; the
|
||||
// transformed carrier stood on the board, so its tile counts (when legal for the elite); of several, the first
|
||||
// in deploy order (row desc, col asc)
|
||||
// PRTS 卫戍协议/帮助: a merge consuming a deployed copy sends the elite to that copy's tile — outside PREP too; of
|
||||
// several, the first in deploy order (row desc, col asc)
|
||||
const map = ps.deployMap();
|
||||
const pos = positionClass(m.gd.chess(elite.id));
|
||||
const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
|
||||
@@ -346,6 +348,8 @@ test('a merge completed during SETTLE (突变细胞) keeps its reward offer for
|
||||
} else {
|
||||
assert.equal(loc.area, 'hand', `${label}: the consumed hand pieces freed the slots`);
|
||||
}
|
||||
// the carrier's deployment is gone; deployed copies were replaced by at most the one elite
|
||||
assert.equal(ps.deployCount, deployed0 - (holderTile ? 1 : 0) - deployedTiles.length + (loc.area === 'board' ? 1 : 0), `${label}: deploy count`);
|
||||
const CELL = 'chess_item_5_08_e_a';
|
||||
assert.equal(ps.hand.filter((p) => p && p.kind === 'item' && p.id !== CELL).length, fillers0, `${label}: the hand's equipment stays`);
|
||||
// the cell is not consumed (player feedback after 0.1.0): it came back to the hand — or temp, the hand being full
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// official game): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置". PRTS 卫戍协议/帮助 §干员的获得与精锐化:
|
||||
// "不获得第3名干员,销毁已有的2名初始干员,发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区
|
||||
// 对应位置)". Remake (server/match/PlayerState.js _mergeChess, board.js mergeTile): the elite takes the tile and facing of the
|
||||
// consumed copy that deploys first (row desc, col asc — [ASSUMED] when several stood on the board); a transformed deployed
|
||||
// piece's own tile counts; with no deployed copy it goes to the hand (overflow temp). Equipment returns to the hand
|
||||
// ("干员晋级后已配发装备会回收至整备区"), the copies' summons are removed and the elite on the board gets its own stack
|
||||
// (its loadout), the deploy count never grows, the reward offer is unchanged — for buys, rewards, effect grants, SETTLE
|
||||
// merges and the boss-field prep alike. audit.js checks the rule in every audited match.
|
||||
// consumed copy that deploys first (row desc, col asc — [ASSUMED] when several stood on the board); a 突变细胞 carrier is
|
||||
// destroyed before its gain, so its tile never counts; with no deployed copy it goes to the hand (overflow temp).
|
||||
// Equipment returns to the hand ("干员晋级后已配发装备会回收至整备区"), the copies' summons are removed and the elite on
|
||||
// the board gets its own stack (its loadout), the deploy count never grows, the reward offer is unchanged — for buys,
|
||||
// rewards, effect grants, SETTLE merges and the boss-field prep alike. audit.js checks the rule in every audited match.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ERR, PHASE } from '../../shared/constants.js';
|
||||
@@ -112,20 +112,22 @@ test('buy: two deployed copies → the copy that deploys first (row desc, then c
|
||||
}
|
||||
});
|
||||
|
||||
test('three deployed copies (a deployed operator transformed into the third, 突变细胞-style) → the first of the three tiles in deploy order', () => {
|
||||
test('a deployed operator transformed into the third copy (突变细胞) → the first deployed COPY\'s tile, never the carrier\'s (destroyed first)', () => {
|
||||
const { m, ps } = prep({ seed: 43 });
|
||||
const [id, other] = meleeTier1(m);
|
||||
// the carrier deploys first: under the 0.1.1 WA rule its tile won; the official destroy-then-gain frees it instead
|
||||
const carrier = deploy(m, ps, other, 'UP');
|
||||
const c1 = deploy(m, ps, id, 'LEFT');
|
||||
const c2 = deploy(m, ps, id, 'DOWN');
|
||||
const tiles = [carrier, c1, c2].map((p) => keyOf(ps, p));
|
||||
const dirOf = { [tiles[0]]: 'UP', [tiles[1]]: 'LEFT', [tiles[2]]: 'DOWN' };
|
||||
const [ct, t1, t2] = [carrier, c1, c2].map((p) => keyOf(ps, p));
|
||||
assert.equal(mergeTile([ct, t1, t2].map((key) => ({ key }))).key, ct, 'the carrier\'s tile is the first in deploy order');
|
||||
const elite = ps.transformChess(carrier, id);
|
||||
assert.ok(elite && elite.id === chess(id).goldenId, 'the transformation completed the merge');
|
||||
const want = mergeTile(tiles.map((key) => ({ key })));
|
||||
assert.equal(keyOf(ps, elite), want.key, 'the elite takes the first deployed tile');
|
||||
assert.equal(elite.dir, dirOf[want.key]);
|
||||
assert.equal(ps.deployCount, 1, 'three deployed → one');
|
||||
assert.ok(elite && elite.id === chess(id).goldenId, 'the gain completed the merge');
|
||||
const want = mergeTile([t1, t2].map((key) => ({ key })));
|
||||
assert.equal(keyOf(ps, elite), want.key, 'the elite takes the first deployed copy\'s tile');
|
||||
assert.equal(elite.dir, want.key === t1 ? 'LEFT' : 'DOWN', 'with that copy\'s facing');
|
||||
assert.ok(!ps.board.has(ct), 'the carrier\'s tile is empty');
|
||||
assert.equal(ps.deployCount, 1, 'carrier + two copies deployed → the elite alone');
|
||||
assert.equal(ps.stats.merges, 1);
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
@@ -420,7 +422,7 @@ test('audit.js flags an elite that misses the deployed copy\'s tile (and passes
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('audit.js passes transformations that complete a merge (突变细胞 ctx.transform; a deployed carrier is detached first), in PREP and SETTLE', () => {
|
||||
test('audit.js passes transformations that complete a merge (突变细胞 ctx.transform: the carrier is destroyed before the gain, its tile no copy\'s), in PREP and SETTLE', () => {
|
||||
// carrier / the two copies: 'board' or 'hand'
|
||||
const layouts = [['board', 'hand', 'hand'], ['board', 'board', 'hand'], ['board', 'board', 'board'], ['hand', 'board', 'board'], ['hand', 'hand', 'hand']];
|
||||
for (const phase of ['PREP', 'SETTLE']) {
|
||||
@@ -436,23 +438,26 @@ test('audit.js passes transformations that complete a merge (突变细胞 ctx.tr
|
||||
const [id, other] = meleeTier1(m);
|
||||
const place = (cid, at, dir) => (at === 'board' ? deploy(m, ps, cid, dir) : give(m, ps, cid));
|
||||
const carrier = place(other, carrierAt, 'UP');
|
||||
const carrierTile = keyOf(ps, carrier);
|
||||
const copies = copiesAt.map((at, j) => place(id, at, j ? 'DOWN' : 'LEFT'));
|
||||
const dirOf = new Map([carrier, ...copies].filter((p) => keyOf(ps, p)).map((p) => [keyOf(ps, p), p.dir]));
|
||||
// only the copies' tiles count (PRTS: a destroy, then a gain — the carrier is no copy of the merge)
|
||||
const dirOf = new Map(copies.filter((p) => keyOf(ps, p)).map((p) => [keyOf(ps, p), p.dir]));
|
||||
const before = ps.deployCount;
|
||||
if (phase === 'SETTLE') h.drive(() => m.phase === PHASE.SETTLE);
|
||||
assert.equal(m.phase, PHASE[phase]);
|
||||
const ctx = makeCtx(m, ps, { kind: 'item', key: 'item:test' }, phase === 'SETTLE' ? 'onSettle' : 'onBuy');
|
||||
const got = ctx.transform(carrier.uid, id);
|
||||
assert.ok(got && got.id === chess(id).goldenId, `${label}: the transformation completed the merge`);
|
||||
assert.ok(got && got.id === chess(id).goldenId, `${label}: the gain completed the merge`);
|
||||
const want = mergeTile([...dirOf.keys()].map((key) => ({ key })));
|
||||
const loc = ps.find(got.uid);
|
||||
if (carrierTile) assert.ok(!ps.board.has(carrierTile), `${label}: the carrier's tile is empty`);
|
||||
if (want) {
|
||||
assert.equal(loc.key, want.key, `${label}: the elite on the first deployed tile`);
|
||||
assert.equal(loc.key, want.key, `${label}: the elite on the first deployed copy's tile`);
|
||||
assert.equal(loc.piece.dir, dirOf.get(want.key), `${label}: with that copy's facing`);
|
||||
assert.equal(ps.deployCount, 1, `${label}: ${before} deployed → 1`);
|
||||
} else {
|
||||
assert.equal(loc.area, 'hand', `${label}: no deployed copy → the hand`);
|
||||
assert.equal(ps.deployCount, 0);
|
||||
assert.equal(ps.deployCount, 0, `${label}: ${before} deployed → 0`);
|
||||
}
|
||||
assert.deepEqual(audit.violations, [], `${label}: no audit violation`);
|
||||
m.dispose();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Player feedback after 0.1.0, #4 (workstream WA): the 突变细胞 item card says what the official text leaves out — the
|
||||
// cell is not consumed, it returns to the hand with the destroyed operator's other equipment (items.json `note`, built
|
||||
// by tools/build-data.mjs ITEM_RULES; server: builtinMeta char_chess_transformation_equip).
|
||||
// cell is not consumed, it returns to the hand with the destroyed operator's other equipment, and the new operator joins
|
||||
// the 整备区 to be deployed again (official footage; PR #2) (items.json `note`, built by tools/build-data.mjs ITEM_RULES;
|
||||
// server: builtinMeta char_chess_transformation_equip).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -35,7 +36,7 @@ const textOf = (v) => {
|
||||
};
|
||||
const hasClass = (v, c) => typeof v?.props?.class === 'string' && v.props.class.split(/\s+/).includes(c);
|
||||
|
||||
test('ItemDetail: 突变细胞 (both qualities) says it returns to the hand; an ordinary item has no rule line', async () => {
|
||||
test('ItemDetail: 突变细胞 (both qualities) says it returns to the hand and the new operator joins the 整备区; an ordinary item has no rule line', async () => {
|
||||
await data.loadAll('items', 'assets');
|
||||
const rule = (item) => [...walk(ItemDetail({ item, piece: null, editable: false }))].find((n) => hasClass(n, 'dhint--rule'));
|
||||
for (const id of ['chess_item_5_08_e_a', 'chess_item_5_08_e_b']) {
|
||||
@@ -44,6 +45,7 @@ test('ItemDetail: 突变细胞 (both qualities) says it returns to the hand; an
|
||||
const t = textOf(rule(cell));
|
||||
assert.match(t, /原干员销毁/);
|
||||
assert.match(t, /突变细胞与其他装备退回整备区,可再次配发/);
|
||||
assert.match(t, /高一阶的随机初始干员(最高6阶),进入整备区,需要重新部署/);
|
||||
}
|
||||
const plain = data.lookup('items', 'chess_item_1_01_e_a'); // 维式重锤
|
||||
assert.equal(plain.note, null);
|
||||
|
||||
@@ -1368,14 +1368,18 @@ const SHOP_EXCLUDED_ITEMS = Object.freeze({
|
||||
* 获得一名高一阶的随机初始干员(最高六阶)", and a destroyed operator's equipment comes off (PRTS 卫戍协议/帮助 "佩戴的
|
||||
* 装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除"); the text never says 销毁 for
|
||||
* the cell (every consumable item's does), and players re-inject it every round ("之后就是一直打针,扎到核心卡或者叠层
|
||||
* 手干员就换人扎", bilibili cv47000418; "这个道具可以无限使用", cg.163.com guide 2025-11-15).
|
||||
* 手干员就换人扎", bilibili cv47000418; "这个道具可以无限使用", cg.163.com guide 2025-11-15). The new operator is gained
|
||||
* into the 整备区, never onto the carrier's tile — official footage (bilibili BV1vzyVBuEN9 ≈ 8:24, BV1Qkw1zMEoR ≈ 7:25):
|
||||
* at the next prep the tile is empty, one more deployment is left and the new operator waits on the bench (PRTS 帮助:
|
||||
* what a player gains goes to the 手牌区; pointed out in PR #2).
|
||||
*/
|
||||
const ITEM_RULES = Object.freeze({
|
||||
chess_item_5_08_e_a: {
|
||||
note: '生效时原干员销毁,获得一名高一阶的随机初始干员(最高6阶);突变细胞与其他装备退回整备区,可再次配发',
|
||||
implFormula: 'After the battle: the carrier is destroyed and replaced by a random NORMAL operator one tier higher (max 6; '
|
||||
+ 'an elite carrier too) on its tile when legal; its equipment, the cell included, returns to the hand (the cell is '
|
||||
+ 'not consumed). Never merges.',
|
||||
note: '生效时原干员销毁,突变细胞与其他装备退回整备区,可再次配发;随后获得一名高一阶的随机初始干员(最高6阶),进入整备区,需要重新部署',
|
||||
implFormula: 'After the battle: the carrier is destroyed wherever it stands (a board tile is freed); its equipment, the '
|
||||
+ 'cell included, returns to the hand first (overflow temp; the cell is not consumed); then a random NORMAL operator '
|
||||
+ 'one tier higher (max 6; an elite carrier too) is gained like any gained operator: the hand, overflow temp, a '
|
||||
+ 'completed merge as usual (the elite on a consumed deployed copy\'s tile, never the carrier\'s). Never merges.',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user