feedback5 WE2: effects, reward pools and 机变 cards that grant a random operator also draw the player's 自选 pieces (#9)
「自选干员放入后模拟中的补给池随机范围也将被相应扩大」: player/diy.js diyStockEntries (every slotted piece with copies left; the roll's own tier rules, no 调度中心 gate) joins effectsMeta ctx.rollChess / ctx.rollPool (spDraft rollPool extra + chessOf: bonds through the player's view) and the 驰援 fallback in choices.js. Granted from the player's own stock (poolOf). Docs: META, PLAYING. Golden: no scenario moved.
This commit is contained in:
1 parent
7a3d53faf6
commit
7b83028139
7 files changed
+124
-11
No files matched your search
+4
-2
@@ -520,8 +520,10 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
picks, merges, promotions, sells, temp, elimination; `invariants.js` checks `left + held = cap` per player). The
|
||||
shop's chess slots and the reward offers' temporary refreshes roll it with the shared pool (`pool.roll({ extra })`,
|
||||
copy-weighted like any chess of its tier) once the 调度中心 reaches the slot's `shopLevel` (5 / 6) [ASSUMED for the
|
||||
reward offers]; effects that draw a random operator from the shared pool never draw it, and 信标 never sends one to a
|
||||
teammate [ASSUMED]. `battleInput` carries `diy` (the pick); `m.private.diy` / `diyBanned`; `prepFieldMeta` and
|
||||
reward offers]; an effect, reward pool or 机变 card that grants the player a random operator draws its stock too
|
||||
(effectsMeta `rollChess` / `rollPool`, the 驰援 fallback in choices.js: `ps.diyStockEntries()` — the roll's own tier
|
||||
rules, no 调度中心 gate, bonds through the player's view; 「自选干员放入后模拟中的补给池随机范围也将被相应扩大」, 0.2.0), and
|
||||
信标 never sends one to a teammate [ASSUMED]. `battleInput` carries `diy` (the pick); `m.private.diy` / `diyBanned`; `prepFieldMeta` and
|
||||
`m.result` lineups carry the pick (`diy`) so other players' cards compose the operator.
|
||||
* **Hand**: 10 slots filled right→left, 5 temp slots for passive overflow (merge results, grants, returned equipment);
|
||||
a full hand refuses every buy and reward pick — also one whose copy would complete a merge at once (PRTS
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——最左一列优先,同一列从上到下);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
|
||||
- **晋级奖励**:每次晋升后获得一次特殊招募:从 3 名 **高一阶**(最高 VI)、**互不相同**的随机干员中**免费**挑 1 名(该阶剩余干员不足 3 名时由低一阶补足)。它不能刷新、不能冻结、本回合结束失效。
|
||||
- **干员持有(补位)**:大厅、房间和「确认本局信息」里的「干员调配」按钮打开的界面有两个标签页:「干员调配」选择每名干员携带的技能和精锐的模组;「**干员持有**」可以把你没有的干员**下掉**(点一下卡片切换「持有 / 未持有」)。和官方一样只有 55 张「普通」棋子能下掉(界面按阶级列出本赛季商店里的 53 名,每张写着由谁替补、用几技能);74 张「预设」棋子(多是 1–2 阶的常见干员)无论是否持有都由本人上场,不在其中。默认全部持有。下掉的干员和官方一样**显示为替补干员**:商店和晋升奖励的卡片、整备区和临时整备区里的单位、作战区、结算界面的阵容,都是替补干员的头像、名字和模型(例如银灰显示为 Sharp),带一个小小的「替补」标记;详情卡显示替补干员的立绘、名字、技能、天赋和模组,名字下面写着「银灰的替补」。**盟约、特质、阶级、价格、合成仍按原干员**(卡片和详情卡上的盟约、特质就是原干员的),作战时的属性、技能、天赋、模组、攻击范围、近战位 / 远程位都是替补干员的(例如银灰 → Sharp 的 2 技能「亮剑」)。同一名替补干员可能替补好几名干员(Sharp 替补忍冬、银灰、隐德来希等),它们按各自的原干员分别合成:看阶级和盟约区分,点开详情卡能看到替补的是谁。同盟模拟中队友查看你的整备区和作战区、你的盟约成员时,看到的也是替补干员。替补的技能和模组由棋子固定,「干员调配」对它不生效;替补干员没有召唤物(下掉缪尔赛思就没有流形)。这是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;同盟模拟中只影响你自己的棋子,队友的棋子按队友的设置。没有官方的好友借用:想用本人上场,改回「持有」即可。「全部持有」一键恢复,「导出 / 导入」可以把设置带到别的浏览器;大厅的「干员调配」按钮上会显示「替补 N」提醒你有几名干员被下掉。
|
||||
- **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同,晋升奖励里也可能出现。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用)。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。
|
||||
- **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同,晋升奖励里也可能出现;效果、奖励或机变卡「获得一名随机干员」时,也可能抽到你自己的自选干员(按那个效果自己的阶级规则,不看调度中心等级)。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用)。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。
|
||||
- 每回合商店会自动刷新未冻结的格子。
|
||||
- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;同一回合内用已经生效过的拉普兰德合成的精锐,本回合不再生效。
|
||||
|
||||
|
||||
@@ -482,9 +482,13 @@ function applyDefault(m, ps, card) {
|
||||
handled = true;
|
||||
break;
|
||||
case 'single_special_choice_gain_bond_chess': {
|
||||
// the player's 自选 stock joins the draw, its bonds read through the player's view (player/diy.js diyStockEntries)
|
||||
const pgd = ps.gd || gd;
|
||||
const hasBond = (cid) => { const c = pgd.chess(cid); return !!(c && Array.isArray(c.bonds) && c.bonds.includes(bs.bond)); };
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = m.pool.roll(m.rngMeta, { maxTier: Math.max(1, ps.shop.level), filter: (cid) => { const c = gd.chess(cid); return !!(c && Array.isArray(c.bonds) && c.bonds.includes(bs.bond)); } })
|
||||
|| m.pool.roll(m.rngMeta, { maxTier: 6, filter: (cid) => { const c = gd.chess(cid); return !!(c && Array.isArray(c.bonds) && c.bonds.includes(bs.bond)); } });
|
||||
const extra = typeof ps.diyStockEntries === 'function' ? ps.diyStockEntries() : null;
|
||||
const id = m.pool.roll(m.rngMeta, { maxTier: Math.max(1, ps.shop.level), filter: hasBond, extra })
|
||||
|| m.pool.roll(m.rngMeta, { maxTier: 6, filter: hasBond, extra });
|
||||
if (id) ps.acquireChess(id, { source: 'choice' });
|
||||
}
|
||||
handled = true;
|
||||
|
||||
@@ -544,20 +544,30 @@ export function makeCtx(m, ps, source, hook, ev = null) {
|
||||
});
|
||||
return p ? view(p) : null;
|
||||
},
|
||||
/** Random chess id from the shared pool (copy-weighted). opts: { maxTier, tier, bond, filter(id) } */
|
||||
/**
|
||||
* Random chess id from the shared pool (copy-weighted) — and the player's own 自选 stock (player/diy.js
|
||||
* diyStockEntries, 0.2.0: 「自选干员放入后模拟中的补给池随机范围也将被相应扩大」). opts: { maxTier, tier, bond, filter(id) };
|
||||
* bonds are read through the player's data view (a slotted slot: its operator's).
|
||||
*/
|
||||
rollChess: (opts = {}) => {
|
||||
const f = (id) => {
|
||||
if (opts.bond) { const c = gd.chess(id); if (!c || !Array.isArray(c.bonds) || !c.bonds.includes(opts.bond)) return false; }
|
||||
return typeof opts.filter === 'function' ? !!opts.filter(id) : true;
|
||||
};
|
||||
return m.pool.roll(m.rngMeta, { maxTier: Number.isInteger(opts.maxTier) ? opts.maxTier : 6, tier: Number.isInteger(opts.tier) ? opts.tier : null, filter: f });
|
||||
const extra = typeof ps.diyStockEntries === 'function' ? ps.diyStockEntries() : null;
|
||||
return m.pool.roll(m.rngMeta, { maxTier: Number.isInteger(opts.maxTier) ? opts.maxTier : 6, tier: Number.isInteger(opts.tier) ? opts.tier : null, filter: f, extra });
|
||||
},
|
||||
rollItem: (opts = {}) => m.rollItemId(opts),
|
||||
/**
|
||||
* Roll a choices.json pool: equip pools → { kind: 'item', id }; chess pools (items / weighted / shopEligible with
|
||||
* tier, minTier, bond, golden) → { kind: 'chess', id, golden } (a pool chess needs a free copy). null when empty.
|
||||
*/
|
||||
rollPool: (poolId, opts = {}) => m.rollPool(poolId, { shopLevel: ps.shop.level, ...opts }),
|
||||
rollPool: (poolId, opts = {}) => m.rollPool(poolId, {
|
||||
shopLevel: ps.shop.level,
|
||||
// a shared-pool draw also takes the player's 自选 stock, its bonds read through the player's view (rollChess)
|
||||
extra: typeof ps.diyStockEntries === 'function' ? ps.diyStockEntries() : null, chessOf: (id) => gd.chess(id),
|
||||
...opts,
|
||||
}),
|
||||
/**
|
||||
* Run another owned chess's 特质 of `eventType` now (SERVER_GAIN / SERVER_PREP_START / SERVER_PREP_FIN /
|
||||
* SERVER_CHESS_SOLD / SERVER_REFRESH_SHOP). opts.asUid: run them as if they belonged to that piece. Returns the
|
||||
|
||||
@@ -152,9 +152,11 @@ export class MatchSpDraft {
|
||||
* Roll a choices.json pool (ctx.rollPool). Equip pools → rollItemId. Chess pools: an `items` (uniform) or `weighted`
|
||||
* list — only chess with a free pool copy (or outside the pool) qualify — else a copy-weighted draw from the shared
|
||||
* pool filtered by `tier` / `minTier` / `maxTier` (number or 'shopLevel') / `bond`; `golden: true` yields the elite id.
|
||||
* `extra` (the drawing player's 自选 stock, player/diy.js diyStockEntries) joins that shared-pool draw, `chessOf` reads the
|
||||
* bonds (the player's data view: a slotted slot's operator) — 0.2.0 WE2.
|
||||
* @returns {{ kind: 'item'|'chess', id: string, golden?: boolean } | null}
|
||||
*/
|
||||
rollPool(poolId, { shopLevel = 6 } = {}) {
|
||||
rollPool(poolId, { shopLevel = 6, extra = null, chessOf = null } = {}) {
|
||||
const pools = this.gd.choices.pools && typeof this.gd.choices.pools === 'object' ? this.gd.choices.pools : {};
|
||||
const p = typeof poolId === 'string' && Object.hasOwn(pools, poolId) ? pools[poolId] : null;
|
||||
if (!p || typeof p !== 'object') return null;
|
||||
@@ -181,10 +183,12 @@ export class MatchSpDraft {
|
||||
const maxTier = p.maxTier === 'shopLevel' ? lvl : Number.isInteger(p.maxTier) ? p.maxTier : 6;
|
||||
const minTier = Number.isInteger(p.minTier) ? p.minTier : 1;
|
||||
const bond = typeof p.bond === 'string' ? p.bond : null;
|
||||
const recOf = typeof chessOf === 'function' ? chessOf : (cid) => this.gd.chess(cid);
|
||||
id = this.pool.roll(rng, {
|
||||
tier: Number.isInteger(p.tier) ? p.tier : null,
|
||||
maxTier,
|
||||
filter: (cid, e) => e.tier >= minTier && (!bond || (Array.isArray(this.gd.chess(cid)?.bonds) && this.gd.chess(cid).bonds.includes(bond))),
|
||||
filter: (cid, e) => e.tier >= minTier && (!bond || (Array.isArray(recOf(cid)?.bonds) && recOf(cid).bonds.includes(bond))),
|
||||
extra,
|
||||
});
|
||||
}
|
||||
if (!id) return null;
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
// shop's chess slots and the reward offers' temporary refreshes [ASSUMED] — weighted like any chess of its tier, once
|
||||
// the 调度中心 has reached the slot's shopLevel (activity_table shopLevelDisplayDataDict lists the tier-5 slots at
|
||||
// level 5 and the tier-6 slots at level 6; PRTS 帮助 "仅在调度中心等级 ≥ 干员所在等阶"). The price is the slot's (any
|
||||
// chess of its tier: 4). Effects that grant a random operator from the shared pool never draw it.
|
||||
// chess of its tier: 4).
|
||||
// * random grants (diyStockEntries, 0.2.0 WE2 #9): an effect, reward or 机变 card that grants this player a random
|
||||
// operator from the pool draws its stock too — 「自选干员放入后模拟中的补给池随机范围也将被相应扩大」 (bilibili), "调度中心
|
||||
// 随机资源的范围将被扩大" (PRTS 新手教程) — under the roll's own tier rules (the effect's tier / maxTier: no 调度中心 gate,
|
||||
// as for a preset chess) and filters (bonds read through the player's data view).
|
||||
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
|
||||
// the player state).
|
||||
|
||||
@@ -204,6 +208,17 @@ export class PlayerDiy {
|
||||
return out.length ? out : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stock entries a random grant of this player draws besides the shared pool (see the header): every slotted piece
|
||||
* with copies left, whatever the 调度中心 level (the roll's own tier rules apply), or null (none).
|
||||
* @returns {Array<[string, { cap: number, left: number, tier: number, shopLevel: number }]> | null}
|
||||
*/
|
||||
diyStockEntries() {
|
||||
if (!this.diyStock || !this.diyStock.entries.size) return null;
|
||||
const out = [...this.diyStock.entries].filter(([, e]) => e.left > 0);
|
||||
return out.length ? out : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 自选 pick a chess id (normal or elite) of this player fields — its slot's — or null (not a slotted slot).
|
||||
* @param {string} id
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// test/match/feedback5-we2-diy-grants.test.js — 0.2.0 WE2 #9: an effect / reward / 机变 card that grants a player a random
|
||||
// operator draws that player's 自选 stock too (「自选干员放入后模拟中的补给池随机范围也将被相应扩大」; PRTS 新手教程 "调度中心随机资源
|
||||
// 的范围将被扩大"): effectsMeta ctx.rollChess / ctx.rollPool (player/diy.js diyStockEntries), choices.js 驰援 fallback —
|
||||
// the roll's own tier rules, bonds through the player's view; never another player's stock.
|
||||
// Run: node --test test/match/feedback5-we2-diy-grants.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeCtx } from '../../server/match/effectsMeta.js';
|
||||
import { applyCard, tacticCard } from '../../server/match/choices.js';
|
||||
import { makeMatch, checkInvariants } from './harness.js';
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const SIEGE = 'char_112_siege'; // 推进之王 (维多利亚)
|
||||
const PICK = { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' };
|
||||
|
||||
/** A co-op match: p_0 slots 推进之王 at T5A, p_1 slots nothing; first prep. */
|
||||
function prep(seed = 911) {
|
||||
const seats = [
|
||||
{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false, connected: true, diy: { [T5A]: PICK } },
|
||||
{ seat: 1, playerId: 'p_1', name: 'P1', isBot: false, connected: true },
|
||||
];
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', seats, seed }).start();
|
||||
h.toPrep(1);
|
||||
return { h, m: h.m, p0: h.ps('p_0'), p1: h.ps('p_1') };
|
||||
}
|
||||
/** Take every shared-pool copy of the chess `pred` admits (the draw then sees only what is left); m.restore() gives them back. */
|
||||
function drain(m, pred) {
|
||||
const back = (m.drained ??= []);
|
||||
for (const [id, e] of m.pool.entries) if (pred(id, e) && e.left > 0) back.push([id, m.pool.take(id, e.left)]);
|
||||
m.restore = () => { for (const [id, n] of back.splice(0)) m.pool.give(id, n); };
|
||||
}
|
||||
|
||||
test('#9 effect rolls (ctx.rollChess, any tier / bond filter) draw the player\'s 自选 stock — its own player only', () => {
|
||||
const { m, p0, p1 } = prep();
|
||||
assert.equal(p0.diyStock.left(T5A), 8);
|
||||
drain(m, (id, e) => e.tier === 5);
|
||||
const c0 = makeCtx(m, p0, { key: 'test' }, 'onTest');
|
||||
const c1 = makeCtx(m, p1, { key: 'test' }, 'onTest');
|
||||
assert.equal(c0.rollChess({ tier: 5 }), T5A, 'the only tier-5 copies left: her stock');
|
||||
assert.equal(c1.rollChess({ tier: 5 }), null, 'another player never draws it');
|
||||
// the 调度中心 level is not the shop's gate here: an effect's tier rule alone (p_0 is at level 1)
|
||||
assert.ok(p0.shop.level < 5);
|
||||
// a bond filter reads the operator: 推进之王 is 维多利亚
|
||||
drain(m, (id) => (m.gd.chess(id)?.bonds || []).includes('victoriaShip'));
|
||||
assert.equal(c0.rollChess({ bond: 'victoriaShip' }), T5A);
|
||||
assert.equal(c0.rollChess({ bond: 'yanShip', tier: 5 }), null, 'not a 炎 operator');
|
||||
// granting it takes one copy of her own stock
|
||||
assert.ok(c0.grantChess(T5A));
|
||||
assert.equal(p0.diyStock.left(T5A), 7);
|
||||
m.restore();
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#9 reward pools (ctx.rollPool: a shared-pool chess pool) and the 维多利亚驰援 card draw it too', () => {
|
||||
const { m, p0, p1 } = prep(910);
|
||||
drain(m, (id, e) => e.tier === 5);
|
||||
const c0 = makeCtx(m, p0, { key: 'test' }, 'onTest');
|
||||
const c1 = makeCtx(m, p1, { key: 'test' }, 'onTest');
|
||||
assert.deepEqual(c0.rollPool('pool_chess_shop_5_reward'), { kind: 'chess', id: T5A, golden: false });
|
||||
assert.equal(c1.rollPool('pool_chess_shop_5_reward'), null);
|
||||
// 维多利亚驰援 (allybuff_select_7_7, single_special_choice_gain_bond_chess) with every 维多利亚 copy of the pool taken:
|
||||
// the card grants her 推进之王 from her own stock
|
||||
drain(m, (id) => (m.gd.chess(id)?.bonds || []).includes('victoriaShip'));
|
||||
const card = tacticCard(m.gd.choices.cards.tactic.find((c) => c.effectId === 'allybuff_select_7_7'));
|
||||
const owned = (ps) => [...ps.board.values(), ...ps.hand, ...ps.temp].filter((x) => x && x.kind === 'chess' && x.id === T5A).length;
|
||||
const before = p0.diyStock.left(T5A);
|
||||
applyCard(m, p0, card);
|
||||
assert.equal(owned(p0), 1, 'she got her 自选 piece');
|
||||
assert.equal(p0.diyStock.left(T5A), before - 1, 'from her stock');
|
||||
const had1 = [...p1.hand, ...p1.temp].filter(Boolean).length;
|
||||
applyCard(m, p1, card);
|
||||
assert.equal([...p1.hand, ...p1.temp].filter(Boolean).length, had1, 'p_1 has no 自选 piece to draw: nothing');
|
||||
m.restore();
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
Reference in new issue
Block a user