feedback1 WA: return the Mutation Cell before summon cards; bots keep it
Review follow-ups for reports #1 and #4. - transformChess: the returned equipment is stowed before the new operator's summon stack is granted (as in _mergeChess); when the transformation completes a merge, the carrier's equipment goes through _mergeChess, which also returns it before the elite's summons. A summon card no longer pushes the returned 突变细胞 into temp, where it would be lost at the deadline. - bot: a 突变细胞 left in temp with a full hand gets a hand slot (sell the weakest single hand chess, else destroy the cheapest other hand item, else sell any hand chess) instead of being destroyed. - 拉普兰德's per-piece counter key is module-prefixed; the re-bought copy case is marked [ASSUMED] in the code and docs/META.md. - .dhint--rule gets a style; sources quote PRTS 卫戍协议/帮助 exactly ("在失去该干员(干员出售、销毁、合并等)…时自动卸除").
This commit is contained in:
1 parent
f8e35d5055
commit
85d8bd40b4
10 files changed
+154
-36
No files matched your search
+11
-6
@@ -175,7 +175,8 @@ Buys toward a full board first (the cap is 8 from R1; leftover funds are lost),
|
||||
spends the rest on merge progress, bond thresholds around a focus core bond (most owned members, ties → most copies
|
||||
left in the shared pool), role needs (2 blockers, anti-air when the wave flies, ≤ 2 healers), 特质 that keep adding
|
||||
layers (every prep / refresh — the layer engines) and items for free carriers (突变细胞 on its least valuable single
|
||||
normal operator below 6阶 — never an elite or one of a merge pair, `bot.js cellTarget`); refreshes while a purchase stays
|
||||
normal operator below 6阶 — never an elite or one of a merge pair, `bot.js cellTarget`; a cell left in temp gets a hand
|
||||
slot made for it, `makeHandRoom`); refreshes while a purchase stays
|
||||
affordable; sells bench chess that neither make the lineup nor build toward something (a live pair, the focus bond,
|
||||
an elite). The deployed set maximizes unit value + activated bond tiers (exact counting via `computeBonds`). Placement
|
||||
uses the round's enemy preview: every route is traced over the own board (ground: the stage's device-aware ground
|
||||
@@ -297,8 +298,9 @@ shopSlots() effect(id)` (`piece(uid)` adds `area`, `holderUid`, `idx`; "身前
|
||||
counters `counter(k) setCounter(k, v) incCounter(k, n)` (player scope, persistent; prefix keys with your module) and
|
||||
`pieceCounter(uid, k) incPieceCounter(uid, k, n)` (per piece, current round only: 0 in a new round and for a new piece —
|
||||
bought, granted, transformed —; a move keeps it; an elite merged this round keeps the highest of its copies'
|
||||
[ASSUMED]; `PlayerState.pieceRoundCount`) — 拉普兰德's "本回合首次主动刷新" is the first manual refresh that copy witnesses
|
||||
(player feedback after 0.1.0: "获得该干员后该回合的首次刷新" also stacks).
|
||||
[ASSUMED]; `PlayerState.pieceRoundCount`; prefix keys with your module too) — 拉普兰德's "本回合首次主动刷新" is the
|
||||
first manual refresh that copy witnesses (player feedback after 0.1.0: "获得该干员后该回合的首次刷新" also stacks; a copy
|
||||
bought after selling one this round is a new copy and fires on its own first refresh [ASSUMED]).
|
||||
|
||||
Writes (all validated, never throw on bad input, never make funds / pools negative):
|
||||
|
||||
@@ -343,8 +345,9 @@ Built-ins (builtinMeta.js, overridable): 盟约之币 / 骑士储蓄罐 (random
|
||||
(+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 — the cell is not consumed: PRTS 下半 记录 备注 "生效时,
|
||||
原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", players re-inject it every round; player feedback after 0.1.0),
|
||||
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
|
||||
items), 教鞭 / “神秘顾客” (a random bounty is added).
|
||||
|
||||
@@ -640,7 +643,9 @@ receiver only), plus CUSTOM texts (eliminations, 联防, hidden core).
|
||||
cell included).
|
||||
* 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).
|
||||
official server's instance handling is not observable). A 拉普兰德 bought after selling one in the same round is a new
|
||||
copy and fires on its own first refresh ("获得该干员后"; each such +4 costs 3 + 1 refresh − 1 refund and needs her in
|
||||
the shop).
|
||||
* Chess granted by effects need a free pool copy unless `requirePool: false` (then they hold 0 copies).
|
||||
* Boss-round `local` pack spawns (boss parts) all spawn; content scripts (bosses.js) decide their behaviour.
|
||||
* The Final Assault ends as a defeat when every field finished with the boss pool above 0 (boss escaped).
|
||||
|
||||
@@ -251,7 +251,7 @@ Param names below are the keys of `items[].normal.params` / `items[].golden.para
|
||||
- **商业包装方案** (`chess_item_5_07_e_a`, TV, 3) - Stays equipped. Counter of operators SOLD by the player (any): every `count` sales, give 1 random NORMAL operator sharing a bond with the carrier, tier <= current shop level.
|
||||
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); the official text never says 销毁 (every consumable's does); players re-inject it every round (bilibili cv47000418 "之后就是一直打针,扎到核心卡或者叠层手干员就换人扎", BV1pEQ3BhEEi "如果四回合出了伊内丝,就考虑先不扎针") — the destroyed operator's equipment, the cell included, returns to the hand.
|
||||
**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.
|
||||
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`
|
||||
|
||||
@@ -201,6 +201,8 @@
|
||||
.dhint .icon { width: .16rem; height: .16rem; margin-top: .03rem; flex: none; }
|
||||
/* a merge-completing shop / reward card's 可晋升 destination (the tag's colour; touch shows no title — QA 6b) */
|
||||
.dhint--merge { color: var(--gold); }
|
||||
/* a rule the official item text leaves out (items.json `note`: 突变细胞 returns to the hand after each use) */
|
||||
.dhint--rule { color: var(--text-md); }
|
||||
.dactions { display: flex; justify-content: flex-end; gap: .1rem; padding-top: .06rem; border-top: 1px solid var(--line); }
|
||||
.dsell { margin-left: .06rem; color: #1d1400; }
|
||||
.dfactions { display: flex; flex-wrap: wrap; gap: .06rem; }
|
||||
|
||||
+19
-13
@@ -34,7 +34,8 @@
|
||||
// (拉普兰德: 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.
|
||||
// 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.
|
||||
// * 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
|
||||
@@ -585,7 +586,9 @@ export class PlayerState {
|
||||
* 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).
|
||||
* (_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]
|
||||
*/
|
||||
transformChess(piece, newId, { returnItems = false } = {}) {
|
||||
@@ -600,23 +603,26 @@ export class PlayerState {
|
||||
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 });
|
||||
np.items = returnItems ? [] : items;
|
||||
if (!rec.isGolden && this.completesChessMerge(newId)) {
|
||||
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);
|
||||
if (!np && !returnItems) { this.recompute(); return null; }
|
||||
} 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); this.grantTokensFor(np); } else if (!this.stow(np, { allowTemp: true })) { this.returnCopies(np); np = null; }
|
||||
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
|
||||
if (returnItems) {
|
||||
for (const it of items) {
|
||||
if (this.stow(it, { allowTemp: true })) continue;
|
||||
if (np && 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)`);
|
||||
}
|
||||
this.checkItemMerges();
|
||||
for (const it of pending) {
|
||||
if (this.stow(it, { allowTemp: true })) continue;
|
||||
if (np && 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);
|
||||
this.recompute();
|
||||
if (np) this.m.dispatch(this, 'onGain', { piece: np, kind: 'chess', source: 'transform' });
|
||||
return np;
|
||||
|
||||
+27
-4
@@ -38,7 +38,8 @@
|
||||
// 5. equip items on the strongest deployed damage dealers (consume-on-equip items / Arts only with a handler);
|
||||
// 突变细胞 (it comes back after every transformation) on the least valuable single normal operator below 6阶 —
|
||||
// never an elite or one of a merge pair (cellTarget)
|
||||
// 6. resolve the temp slots, keep one hand slot free, then Ready.
|
||||
// 6. resolve the temp slots (a 突变细胞 left there gets a hand slot made for it — makeHandRoom — instead of being
|
||||
// destroyed), keep one hand slot free, then Ready.
|
||||
// Placement quality (tools/matchrun sweeps, research-faithful waves): the planner beats random layouts by ≈ 8 points
|
||||
// of kill rate and rehearsal adds ≈ 5 more; see docs/META.md §1.5.
|
||||
|
||||
@@ -806,12 +807,15 @@ function context(m, ps) {
|
||||
return { owned, focus: focusBond(m, ps, owned), roles: roles(m, ps), fly: model.flyTotal, model };
|
||||
}
|
||||
|
||||
/** Sell the weakest bench chess that is not part of a merge pair (or anything when keepPairs is false). */
|
||||
function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity } = {}) {
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly = false } = {}) {
|
||||
const ctx = context(m, ps);
|
||||
let worst = null;
|
||||
let worstV = Infinity;
|
||||
for (const p of [...ps.temp, ...ps.hand]) {
|
||||
for (const p of handOnly ? ps.hand : [...ps.temp, ...ps.hand]) {
|
||||
if (!p || p.kind !== 'chess') continue;
|
||||
const base = m.gd.baseIdOf(p.id);
|
||||
if (keepPairs && !m.gd.isGolden(p.id) && ps.countCopies(base) >= 2) continue;
|
||||
@@ -1215,6 +1219,20 @@ function equipItems(m, ps) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free a hand slot for an item worth keeping (突变细胞): sell the weakest single hand chess, else destroy the cheapest
|
||||
* other hand item, else sell the weakest hand chess even of a pair. false: the hand stays full (summon cards only).
|
||||
*/
|
||||
function makeHandRoom(m, ps) {
|
||||
if (freeSlot(ps.hand) >= 0) return true;
|
||||
if (sellWeakestHand(m, ps, { handOnly: true }) && freeSlot(ps.hand) >= 0) return true;
|
||||
const gd = m.gd;
|
||||
const junk = ps.hand.filter((p) => p && p.kind === 'item' && !isMutationCell(gd, p.id))
|
||||
.sort((a, b) => ((gd.item(a.id) || {}).price || 0) - ((gd.item(b.id) || {}).price || 0) || a.uid - b.uid)[0];
|
||||
if (junk && tryDo(() => ps.destroy(junk.uid)) && freeSlot(ps.hand) >= 0) return true;
|
||||
return sellWeakestHand(m, ps, { handOnly: true, keepPairs: false }) && freeSlot(ps.hand) >= 0;
|
||||
}
|
||||
|
||||
function resolveTemp(m, ps) {
|
||||
for (let i = 0; i < ps.temp.length; i++) {
|
||||
const p = ps.temp[i];
|
||||
@@ -1228,6 +1246,11 @@ function resolveTemp(m, ps) {
|
||||
if (j >= 0) tryDo(() => ps.move(p.uid, { area: 'hand', idx: j }));
|
||||
}
|
||||
} else if (p.kind === 'item') {
|
||||
// 突变细胞 comes back after every transformation (昆图斯's strategy item): make room in the hand rather than lose it
|
||||
if (isMutationCell(m.gd, p.id) && makeHandRoom(m, ps)) {
|
||||
const j = freeSlot(ps.hand);
|
||||
if (j >= 0 && tryDo(() => ps.move(p.uid, { area: 'hand', idx: j }))) continue;
|
||||
}
|
||||
tryDo(() => ps.destroy(p.uid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,9 +179,10 @@ 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 (the official text
|
||||
// never says 销毁 — 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.
|
||||
// 初始干员(最高六阶)"); 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.
|
||||
char_chess_transformation_equip: {
|
||||
onBattleResult(ctx) {
|
||||
const { piece, holder } = ctx.source;
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
// of any tier (the text gives no tier cap); a bond without an available chess falls through to the next tied one.
|
||||
// * [ASSUMED] 松果: the "免费特殊招募" is a free pick-one offer of `rewardOffer.count` (3) chess of the pool's tier.
|
||||
// * 拉普兰德 SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT "若为本回合首次主动刷新": per copy — the first manual refresh this
|
||||
// operator witnesses in the round (players' report after 0.1.0); an elite merged this round keeps its copies'
|
||||
// count [ASSUMED]. "本回合每刷新过1次" (SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER, 阿罗玛 / 安洁莉娜 / 售出时)
|
||||
// operator witnesses in the round (players' report after 0.1.0); [ASSUMED] an elite merged this round keeps its
|
||||
// copies' count, and a copy bought after selling one this round is a new copy (fires on its own first refresh).
|
||||
// "本回合每刷新过1次" (SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER, 阿罗玛 / 安洁莉娜 / 售出时)
|
||||
// fires on another event and reads the player's refreshes of the round (roundStats), like 本回合每获得过 / 每花费.
|
||||
|
||||
import { metaBonds, frontPiece, behindPiece, distinctTiers } from '../support/meta.js';
|
||||
@@ -196,14 +197,18 @@ H.SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER = {
|
||||
// refreshes this copy witnessed this round (board or hand), so a 拉普兰德 bought after the round's first refresh still
|
||||
// fires on the next one (players' report after 0.1.0: "获得该干员后该回合的首次刷新" also stacks — the official behaviour;
|
||||
// read as each trait instance counting its own SERVER_REFRESH_SHOP triggers against bb.refresh_cnt). A re-triggered trait
|
||||
// (ev.trigger) is no manual refresh: it neither fires nor counts. The copies of an elite merged this round pass on their
|
||||
// count [ASSUMED] (PlayerState.pieceRoundCount); a new copy (bought — after a sale too —, granted) starts at 0.
|
||||
// (ev.trigger) is no manual refresh: it neither fires nor counts. A new copy (bought, granted) starts at 0. [ASSUMED]:
|
||||
// the copies of an elite merged this round pass on their highest count (PlayerState.pieceRoundCount — no second trigger
|
||||
// that round, conservative); a copy bought after selling one this round is a new copy — "获得该干员后" — and fires on its
|
||||
// own first refresh (the server cannot tell it from any other copy; each such +4 costs her price + a refresh − the
|
||||
// 1-fund refund, and needs her in the shop again).
|
||||
const REFRESH_CNT_KEY = 'garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes'; // per-piece counter (module-prefixed)
|
||||
H.SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT = {
|
||||
onRefresh(ctx, ev) {
|
||||
if (ev && ev.trigger) return;
|
||||
const { bb, bbStr, garrison, piece } = ctx.source;
|
||||
if (!piece || !Number.isInteger(piece.uid) || piece.uid <= 0) return;
|
||||
if (ctx.incPieceCounter(piece.uid, 'refreshes') !== num(bb.refresh_cnt, 1)) return;
|
||||
if (ctx.incPieceCounter(piece.uid, REFRESH_CNT_KEY) !== num(bb.refresh_cnt, 1)) return;
|
||||
addAll(ctx, ids(bbStr.bond), num(bb.layer), requireActiveOf(garrison));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ 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, cellTarget } from '../../server/match/bot.js';
|
||||
import { botPrep, botPrepEnd, cellTarget } from '../../server/match/bot.js';
|
||||
|
||||
const QUIET = { warn() {}, error() {}, info() {} };
|
||||
const REG = createRegistry({ log: QUIET });
|
||||
@@ -279,3 +279,73 @@ test('#4 strategy 昆图斯 end to end: the R3 cell survives its first transform
|
||||
assert.ok(!m2.gd.isGolden(carrierOf.id) && m2.gd.tierOf(carrierOf.id) < 6, 'never an elite or a 6阶');
|
||||
m2.dispose();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// #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
|
||||
|
||||
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 });
|
||||
const { m, ps } = s;
|
||||
forceVigil(m);
|
||||
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);
|
||||
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)');
|
||||
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 });
|
||||
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
|
||||
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);
|
||||
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');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#4 a bot never destroys the cell: left in temp with a full hand and nobody to inject, it gets a hand slot', () => {
|
||||
const s = setup({ seed: 33 });
|
||||
const { m, ps } = s;
|
||||
ps.isBot = true;
|
||||
const elites = [1, 2, 3, 4, 5, 6].flatMap((t) => plainOf(m, t)).map((id) => m.gd.goldenIdOf(id)).filter((g) => g && m.pool.left(m.gd.baseIdOf(g)) >= m.gd.goldenCopies);
|
||||
for (let i = 0; i < 8; i++) { const id = elites.shift(); give(m, ps, id, 'board', legalTileFor(m, ps, id)); }
|
||||
while (ps.hand.some((x) => x == null)) give(m, ps, elites.shift(), 'hand');
|
||||
giveItem(m, ps, CELL, 'temp');
|
||||
assert.equal(cellTarget(m, ps), null, 'elites only: nobody to inject');
|
||||
botPrepEnd(m, ps); // the end of the bot's prep: temp → hand / sell / destroy, a free hand slot, Ready
|
||||
assert.equal(ownedItems(ps, CELL).length, 1, 'the bot kept its strategy item (a hand chess was sold for the slot)');
|
||||
assert.ok(ps.tempEmpty, 'temp resolved');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
@@ -49,3 +49,8 @@ test('ItemDetail: 突变细胞 (both qualities) says it returns to the hand; an
|
||||
assert.equal(plain.note, null);
|
||||
assert.equal(rule(plain), undefined);
|
||||
});
|
||||
|
||||
test('the rule line has its own style (game-panels.css .dhint--rule), not only a test hook', () => {
|
||||
const css = readFileSync(path.join(ROOT, 'public/css/screens/game-panels.css'), 'utf8');
|
||||
assert.match(css, /\.dhint--rule\s*\{[^}]*color:/);
|
||||
});
|
||||
@@ -1360,9 +1360,10 @@ const SHOP_EXCLUDED_ITEMS = Object.freeze({
|
||||
* Rules the official item text leaves out, by normal item id (both qualities): `note` = a player-facing line shown under
|
||||
* the effect in the item card (items.json `note`), `implFormula` replaces research 04's formula.
|
||||
* 突变细胞 — not consumed (player feedback after 0.1.0): PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "生效时,原干员销毁,
|
||||
* 获得一名高一阶的随机初始干员(最高六阶)"; the text never says 销毁 for the cell (every consumable item's does), and
|
||||
* players re-inject it every round ("之后就是一直打针,扎到核心卡或者叠层手干员就换人扎", bilibili cv47000418;
|
||||
* BV1pEQ3BhEEi "如果四回合出了伊内丝,就考虑先不扎针") — the destroyed operator's equipment returns to the hand.
|
||||
* 获得一名高一阶的随机初始干员(最高六阶)", 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).
|
||||
*/
|
||||
const ITEM_RULES = Object.freeze({
|
||||
chess_item_5_08_e_a: {
|
||||
|
||||
Reference in new issue
Block a user