feedback3 WH: mode-off bonds the player has members of show as grey 本局禁用 discs, and hand members count in that header
This commit is contained in:
1 parent
e444bc3c3f
commit
c0cc1d6118
12 files changed
+305
-69
No files matched your search
@@ -0,0 +1,91 @@
|
||||
// Mode-off bonds stay out of the battle (computeBonds) and show in the views when the player has members
|
||||
// (offBondCounts → bondList `off: true`, the strip's grey 本局禁用 discs). 标准模拟's inactiveBondIdList
|
||||
// (activity_table mode_single_funny): 投资人 and 奇迹 count the hand (BOARD_AND_DECK); 突袭 counts the board.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { GameData } from '../../server/match/gamedata.js';
|
||||
import { computeBonds, bondList, bondSnapshot, offBondCounts } from '../../server/match/bondsMeta.js';
|
||||
import { DATA } from './harness.js';
|
||||
|
||||
const funny = new GameData(DATA, 'mode_single_funny');
|
||||
const open = new GameData(DATA, 'mode_multi_normal');
|
||||
let uid = 1;
|
||||
const piece = (id) => ({ uid: uid++, kind: 'chess', id, items: [] });
|
||||
const state = ({ board = [], hand = [], layers = {}, bondCountBonus } = {}) => {
|
||||
const b = new Map();
|
||||
board.forEach((p, i) => b.set(`${9 + (i % 4)},${2 + Math.floor(i / 4)}`, p));
|
||||
const h = new Array(10).fill(null);
|
||||
hand.forEach((p, i) => { h[9 - i] = p; });
|
||||
return { board: b, hand: h, layers, bondCountBonus };
|
||||
};
|
||||
const members = (bond, n) => Object.values(DATA.chess)
|
||||
.filter((c) => c.visible && !c.isGolden && c.bonds.includes(bond))
|
||||
.slice(0, n)
|
||||
.map((c) => c.chessId);
|
||||
|
||||
const INVESTOR = members('investShip', 2);
|
||||
const RAIDER = members('raidShip', 1);
|
||||
const YAN = members('yanShip', 1);
|
||||
|
||||
test('标准模拟: an 投资人 member in hand and a 突袭 member on the board are off entries, never in computeBonds', () => {
|
||||
assert.equal(INVESTOR.length, 2);
|
||||
assert.equal(RAIDER.length, 1);
|
||||
const ps = state({
|
||||
board: [piece(RAIDER[0]), piece(YAN[0])],
|
||||
hand: [piece(INVESTOR[0])],
|
||||
layers: { investShip: 12, raidShip: 4, lateranoShip: 9 },
|
||||
});
|
||||
const bonds = computeBonds(funny, ps);
|
||||
assert.ok(!('investShip' in bonds), '投资人 stays out of the battle input');
|
||||
assert.ok(!('raidShip' in bonds), '突袭 stays out of the battle input');
|
||||
assert.ok(!('lateranoShip' in bonds));
|
||||
assert.equal(bondSnapshot(bonds).investShip, undefined);
|
||||
assert.ok(bonds.yanShip, 'an enabled bond is unchanged');
|
||||
|
||||
const off = offBondCounts(funny, ps);
|
||||
assert.equal(off.investShip.count, 1, 'a hand member counts for 投资人');
|
||||
assert.equal(off.investShip.layers, 12);
|
||||
assert.equal(off.raidShip.count, 1);
|
||||
assert.equal(off.raidShip.layers, 4);
|
||||
assert.ok(!('lateranoShip' in off), 'an inactive bond with zero members is omitted, layers or not');
|
||||
|
||||
const list = bondList(funny, bonds, { full: true, off });
|
||||
const invest = list.find((x) => x.bondId === 'investShip');
|
||||
const raid = list.find((x) => x.bondId === 'raidShip');
|
||||
assert.deepEqual(
|
||||
{ count: invest.count, active: invest.active, tier: invest.tier, layers: invest.layers, off: invest.off, countsHand: invest.countsHand },
|
||||
{ count: 1, active: false, tier: 0, layers: 12, off: true, countsHand: true },
|
||||
);
|
||||
assert.equal(raid.off, true);
|
||||
assert.equal(raid.active, false);
|
||||
assert.equal(raid.tier, 0);
|
||||
assert.equal(raid.countsHand, false);
|
||||
assert.ok(list.findIndex((x) => x.off) > list.findIndex((x) => x.bondId === 'yanShip'), 'off entries follow the live bonds');
|
||||
assert.ok(list.slice(list.findIndex((x) => x.off)).every((x) => x.off), 'the off entries are a suffix');
|
||||
const pub = bondList(funny, bonds, { off });
|
||||
assert.equal(pub.find((x) => x.bondId === 'investShip').off, true);
|
||||
assert.equal('countsHand' in pub.find((x) => x.bondId === 'investShip'), false, 'm.public omits countsHand');
|
||||
});
|
||||
|
||||
test('a BOARD_AND_DECK off bond counts the hand, not the temporary slots, and not twice', () => {
|
||||
const [a, b] = INVESTOR;
|
||||
const handOnly = offBondCounts(funny, state({ hand: [piece(a), piece(b)] }));
|
||||
assert.equal(handOnly.investShip.count, 2);
|
||||
assert.ok(!('raidShip' in (handOnly || {})), '突袭 in nobody\'s hand: a board bond with no board member is omitted');
|
||||
const raidHand = offBondCounts(funny, state({ hand: [piece(RAIDER[0])] }));
|
||||
assert.equal(raidHand, null, '突袭 does not count a hand member');
|
||||
const both = offBondCounts(funny, state({ board: [piece(a)], hand: [piece(a), piece(DATA.chess[a].goldenId)] }));
|
||||
assert.equal(both.investShip.count, 1, 'normal and elite of one operator, board and hand, count once');
|
||||
const tempOnly = { board: new Map(), hand: new Array(10).fill(null), layers: {}, temp: [piece(a)] };
|
||||
assert.equal(offBondCounts(funny, tempOnly), null, 'the 5 temporary slots never count');
|
||||
});
|
||||
|
||||
test('a mode with an empty inactive set returns null, and those bonds stay in computeBonds', () => {
|
||||
assert.equal(open.modeInactiveBonds.size, 0);
|
||||
const ps = state({ board: [piece(RAIDER[0])], hand: [piece(INVESTOR[0])] });
|
||||
assert.equal(offBondCounts(open, ps), null);
|
||||
const bonds = computeBonds(open, ps);
|
||||
assert.equal(bonds.investShip.count, 1);
|
||||
assert.equal(bonds.raidShip.count, 1);
|
||||
assert.equal(bondList(open, bonds).some((x) => x.off), false);
|
||||
});
|
||||
@@ -3,8 +3,9 @@
|
||||
// of the modes whose official modeDataDict inactiveBondIdList switches 奥术 off (with 拉特兰 阿戈尔 卡西米尔 灵巧 奇迹 投资人
|
||||
// 突袭 独行 绝技), yet 深靛 洛洛 阿罗玛 夕 圣聆初雪 stay in the pool through their other bond (server pool.js) and their cards
|
||||
// showed 奥术 like any bond: the detail card's chip read "奥术 0/2 未激活" with three 奥术 operators deployed (the server
|
||||
// leaves an inactive bond out of m.private.bonds) and the bond popup "在场 0/2 未激活". The cards, chips and popup now say
|
||||
// 本局禁用 for a bond the mode never activates (gameLogic modeOffBonds over config.modes[modeId].inactiveBondIds).
|
||||
// used to leave an inactive bond out of m.private.bonds) and the bond popup "在场 0/2 未激活". The cards, chips and popup
|
||||
// say 本局禁用 for a bond the mode never activates (gameLogic modeOffBonds over config.modes[modeId].inactiveBondIds);
|
||||
// since 0.1.3 the view also lists it (`off: true`) so the strip can draw the grey disc — still never in ps.bonds.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -63,13 +64,15 @@ test('a real 标准 match: 奥术 operators are buyable, three on the board neve
|
||||
const used = new Set();
|
||||
for (const id of members.slice(0, 3)) { const t = legalTileFor(m, ps, id, used); used.add(tileKey(t[0], t[1])); give(m, ps, id, 'board', t); }
|
||||
ps.recompute();
|
||||
assert.equal(ps.bonds.arcaneShip, undefined, 'official: the mode never activates 奥术');
|
||||
assert.equal(ps.bonds.arcaneShip, undefined, 'official: the mode never activates 奥术 — not in the battle state');
|
||||
const priv = ps.privateView();
|
||||
assert.equal(priv.bonds.find((b) => b.bondId === 'arcaneShip'), undefined, 'no 奥术 entry in m.private.bonds');
|
||||
const arc = priv.bonds.find((b) => b.bondId === 'arcaneShip');
|
||||
assert.ok(arc && arc.off === true && arc.active === false && arc.tier === 0 && arc.count === 3, 'the view lists the members as a grey 本局禁用 disc');
|
||||
assert.ok(priv.bonds.slice(priv.bonds.findIndex((b) => b.off)).every((b) => b.off), 'off entries come last');
|
||||
const off = modeOffBonds(getMode(m.modeId));
|
||||
// before: the chip computed 0 members from the missing entry — "奥术:在场 0/2,未激活" with 3 deployed
|
||||
const before = chipOf(BondChips({ bondIds: DATA.chess[members[0]].bonds, bonds: priv.bonds }), 'arcaneShip');
|
||||
assert.match(before.props.title, /在场 0\/2,未激活/);
|
||||
// without the mode's off set the chip would read the new entry as an ordinary inactive bond (在场 3/3,未激活)
|
||||
const bare = chipOf(BondChips({ bondIds: DATA.chess[members[0]].bonds, bonds: priv.bonds }), 'arcaneShip');
|
||||
assert.match(bare.props.title, /在场 3\/3,未激活/);
|
||||
const chip = chipOf(BondChips({ bondIds: DATA.chess[members[0]].bonds, bonds: priv.bonds, off }), 'arcaneShip');
|
||||
assert.ok(hasClass(chip, 'is-off') && !hasClass(chip, 'is-active'));
|
||||
assert.equal(chip.props.title, '奥术:本局禁用(该盟约不会激活)');
|
||||
|
||||
@@ -9,7 +9,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
phaseMode, phaseBanner, isCombatPhase, isBossPhase, countdownState, phaseTotalSeconds, sortBonds, bondTier, nextThreshold,
|
||||
bondMembers, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
|
||||
bondMembers, memberHeadCount, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
|
||||
boardTargets, dropIntent, normalizeDraft, normalizeSp, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
|
||||
rangeGridBox, shortcutFor, sanitizeSettings, DEFAULT_SETTINGS, normalizeResult, cycleField, fieldLabel, homeFieldId,
|
||||
activeBubbles, sortedPlayers, tileKey, prepCapsuleLabel, prepCamera, dropFailureReason,
|
||||
@@ -138,9 +138,46 @@ describe('bonds', () => {
|
||||
assert.equal(byId.get(m2).owned, true, 'golden in hand counts as owned base');
|
||||
assert.equal(byId.get(m2).onBoard, false);
|
||||
assert.equal(byId.get(m3).banned, true);
|
||||
assert.equal(byId.get(m2).inHand, true, 'a golden copy in the hand marks the base in hand');
|
||||
assert.equal(rows[0].id, m1, 'on-board first');
|
||||
assert.equal(rows.length, b.visibleMembers.length);
|
||||
});
|
||||
test('memberHeadCount: the hand counts only for a bond that counts the hand, once per row', () => {
|
||||
const rows = [
|
||||
{ id: 'a', onBoard: true, inHand: true },
|
||||
{ id: 'b', onBoard: false, inHand: true },
|
||||
{ id: 'c', onBoard: false, inHand: false },
|
||||
null,
|
||||
];
|
||||
assert.equal(memberHeadCount(rows, false), 1, 'board only');
|
||||
assert.equal(memberHeadCount(rows, true), 2, 'board or hand, one row once');
|
||||
assert.equal(memberHeadCount(null, true), 0);
|
||||
assert.equal(memberHeadCount(undefined), 0);
|
||||
const invest = bonds.investShip;
|
||||
assert.equal(invest.countsHand, true);
|
||||
const [m1, m2] = invest.visibleMembers;
|
||||
const priv = privWith({
|
||||
board: [{ ...piece(m1), row: 9, col: 3 }],
|
||||
hand: [piece(m2)],
|
||||
temp: [piece(invest.visibleMembers[2] || m2)],
|
||||
});
|
||||
const got = bondMembers(invest, priv, [], getChess);
|
||||
const byId = new Map(got.map((r) => [r.id, r]));
|
||||
assert.equal(byId.get(m1).onBoard, true);
|
||||
assert.equal(byId.get(m2).inHand, true);
|
||||
assert.equal(byId.get(m2).onBoard, false);
|
||||
if (invest.visibleMembers[2]) assert.equal(byId.get(invest.visibleMembers[2]).inHand, false, 'a temporary-slot copy is not in hand');
|
||||
assert.equal(memberHeadCount(got, true), 2, 'the board member and the hand member, not the temporary slot');
|
||||
assert.equal(memberHeadCount(got, false), 1, 'without countsHand the hand member is left out');
|
||||
});
|
||||
test('sortBonds puts a mode-off bond after every live bond', () => {
|
||||
const sorted = sortBonds([
|
||||
{ bondId: 'investShip', off: true, active: false, layers: 99, count: 3, tier: 0 },
|
||||
{ bondId: 'yanShip', active: true, layers: 1, count: 3, tier: 1 },
|
||||
{ bondId: 'raidShip', off: true, layers: 0, count: 1 },
|
||||
], (id) => bonds[id]).map((b) => b.bondId);
|
||||
assert.deepEqual(sorted, ['yanShip', 'investShip', 'raidShip']);
|
||||
});
|
||||
test('bannedPerBond counts banned visible members', () => {
|
||||
const b = bonds.deputShip;
|
||||
const per = bannedPerBond(Object.values(bonds), [b.visibleMembers[0], b.visibleMembers[1], 'nope']);
|
||||
|
||||
Reference in new issue
Block a user