feedback1c WK: 变形同构体 wearers in the bond popup's members and on their card
Player report: with 变形同构体 and the matching bond item the wearer gets the bond's effect, but the bond's member count leaves the converted operator out. The count that activates the bond already included the wearer on every server path (bondsMeta.pieceBonds: prep count, views, battle input; support.unitBonds: the battle). The client did not: the bond popup's member list (成员 x/y) and the wearer's card read only the chess records' bonds, so the popup said 在场 3 but 成员 2/10 without the wearer, and the card had no chip for the bond. - gameLogic.grantedBonds / pieceBondIds: the server's 变形同构体 rule on the client; bondMembers lists wearers (tagged 同构, one row per operator, on the board or owned), so 成员 x/y agrees with 在场. - detailPanel: the card's chips include the granted bond (dashed, 同构). - Teammates: UnitInfo carries an ally operator's item ids (sim unitInfo, Match.prepFieldMeta); ownerBoard / runner.ownerOps keep them. - Tests: every pairing on real matches (prep, views, battle input, the real battle), bench vs board, half pairs, distinct members; the client rule against the server's; popup / card rendering.
This commit is contained in:
1 parent
7f134737ce
commit
0c898bb524
13 files changed
+464
-34
No files matched your search
@@ -0,0 +1,165 @@
|
||||
// 变形同构体 and the bond member count on the real match paths (player report after 0.1.0: "缪尔赛思的变形同构体配上对应
|
||||
// 职业转职的装备能使装备的干员吃到对应盟约的加成,但是那个盟约的人口数量没有把转职之后的干员算进去").
|
||||
// Official rule: band 变形者集群 "与特定装备一同装备时装备者将视为特定盟约成员"; the item's talent (character_table
|
||||
// trap_1073_acarm073) lists the 14 pairings ("“维式重锤”系列装备→【维多利亚】盟约" …); 缪尔赛思's 特质 garrison_76
|
||||
// "<获得时>获得1个“变形同构体”". The wearer is a member: it counts for the bond's activation like any member — on the
|
||||
// board only for these BOARD bonds, once per operator (distinct members, normal and elite alike).
|
||||
// The server already counted it (prep, battle input, views, the battle); what the player saw was the client's bond
|
||||
// popup and card (test/ui/morph-bonds.test.js). This suite pins the count on every path, and the item ids the
|
||||
// client now gets with a teammate's units (prep scouting UnitInfo, the battle's UnitInfo).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeMatch, give, giveItem, DATA, legalTileFor } from './harness.js';
|
||||
import { unitBonds, isMember } from '../../server/sim/content/support/index.js';
|
||||
import { unitInfo } from '../../server/sim/snapshot.js';
|
||||
|
||||
const ISO = 'chess_item_6_09_e_a';
|
||||
const ISO_B = 'chess_item_6_09_e_b';
|
||||
const HAMMER = 'chess_item_1_01_e_a';
|
||||
const GRANTS = Object.values(DATA.items).filter((r) => r.giveBondId).map((r) => r.id).sort();
|
||||
|
||||
/** A target chess outside `bond` (and not 调和: its +1 would blur the count). */
|
||||
const outsider = (bond) => Object.values(DATA.chess).find((c) => c.visible && !c.isGolden && c.tier <= 2 && !c.bonds.includes(bond) && !c.bonds.includes('maniShip')).chessId;
|
||||
const members = (bond) => Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && c.bonds.includes(bond) && !c.bonds.includes('maniShip')).map((c) => c.chessId).sort();
|
||||
|
||||
/** PREP R1 of a real match (all 23 bonds: 绝境), the player's pieces cleared. */
|
||||
function prep({ mode = 'solo', humans = 1, fake = false, seed = 21 } = {}) {
|
||||
const h = makeMatch({ mode, difficulty: 'HARD', humans, seed, fake }).start();
|
||||
h.toPrep(1);
|
||||
for (const ps of h.m.players.values()) {
|
||||
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;
|
||||
ps.funds = 50; ps.bandId = null;
|
||||
ps.recompute();
|
||||
}
|
||||
return h;
|
||||
}
|
||||
const ok = (r) => assert.deepEqual(r, { ok: true });
|
||||
/** A chess with `items` (equipped through g.equip, in this order), from the hand, then onto the board when `board`. */
|
||||
function wearer(h, pid, chessId, itemIds, { board = true } = {}) {
|
||||
const { m } = h; const ps = h.ps(pid);
|
||||
const p = give(m, ps, chessId, 'hand');
|
||||
for (const id of itemIds) ok(m.handle(pid, { t: 'g.equip', itemUid: giveItem(m, ps, id).uid, targetUid: p.uid }));
|
||||
if (board) {
|
||||
const at = legalTileFor(m, ps, chessId);
|
||||
ok(m.handle(pid, { t: 'g.move', uid: p.uid, to: { area: 'board', row: at[0], col: at[1] } }));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
const place = (h, pid, chessId) => {
|
||||
const { m } = h; const ps = h.ps(pid);
|
||||
const p = give(m, ps, chessId, 'hand');
|
||||
const at = legalTileFor(m, ps, chessId);
|
||||
ok(m.handle(pid, { t: 'g.move', uid: p.uid, to: { area: 'board', row: at[0], col: at[1] } }));
|
||||
return p;
|
||||
};
|
||||
const count = (ps, bond) => ps.bonds[bond]?.count ?? 0;
|
||||
|
||||
test('缪尔赛思 hands out 变形同构体 on purchase (garrison_76), the bought copy pairs with any bond item', () => {
|
||||
const h = prep();
|
||||
const { m } = h; const ps = h.ps('p_0');
|
||||
ps.shop.slots[0] = { kind: 'chess', id: 'chess_char_6_11_a', price: 6, basePrice: 6, sold: false, frozen: false };
|
||||
ok(m.handle('p_0', { t: 'g.buy', slot: 0 }));
|
||||
const iso = ps.hand.find((p) => p && p.id === ISO);
|
||||
assert.ok(iso, 'the 变形同构体 is in the hand');
|
||||
const t = give(m, ps, outsider('victoriaShip'), 'hand');
|
||||
ok(m.handle('p_0', { t: 'g.equip', itemUid: iso.uid, targetUid: t.uid }));
|
||||
ok(m.handle('p_0', { t: 'g.equip', itemUid: giveItem(m, ps, HAMMER).uid, targetUid: t.uid }));
|
||||
assert.equal(count(ps, 'victoriaShip'), 0, 'on the bench: BOARD bonds count the board only');
|
||||
const at = legalTileFor(m, ps, t.id);
|
||||
ok(m.handle('p_0', { t: 'g.move', uid: t.uid, to: { area: 'board', row: at[0], col: at[1] } }));
|
||||
assert.equal(count(ps, 'victoriaShip'), 1);
|
||||
h.invariants();
|
||||
});
|
||||
|
||||
test('every pairing (18 bond items × normal / golden, both 变形同构体): the wearer counts in prep, in the views, in the battle input and in the real battle', () => {
|
||||
const grants = Object.values(DATA.items).filter((r) => r.giveBondId);
|
||||
assert.equal(grants.length, 36);
|
||||
assert.equal(new Set(grants.map((r) => r.giveBondId)).size, 14, '14 bonds have a bond item (the official talent\'s list)');
|
||||
for (const r of grants) {
|
||||
const bond = r.giveBondId;
|
||||
const iso = r.isGolden ? ISO_B : ISO; // golden bond items with the golden 变形同构体, normal with normal
|
||||
const h = prep({ seed: 31 });
|
||||
const { m } = h; const ps = h.ps('p_0');
|
||||
const [a, b] = members(bond);
|
||||
place(h, 'p_0', a); place(h, 'p_0', b);
|
||||
assert.equal(count(ps, bond), 2, `${bond}: two members`);
|
||||
// the bond item first, then the 变形同构体 (the other order: the next test)
|
||||
const w = wearer(h, 'p_0', outsider(bond), [r.id, iso]);
|
||||
assert.equal(count(ps, bond), 3, `${r.id}: the wearer is the third member`);
|
||||
assert.equal(ps.bonds[bond].active, true, `${r.id}: the threshold of 3 (2 for add-on bonds) is reached with it`);
|
||||
assert.equal(ps.battleInput().bonds[bond].count, 3, `${r.id}: battle input`);
|
||||
assert.equal(ps.privateView().bonds.find((x) => x.bondId === bond).count, 3, `${r.id}: m.private`);
|
||||
let battle = null;
|
||||
const orig = m.newBattle.bind(m);
|
||||
m.newBattle = (o) => { const bt = orig(o); battle ??= bt; return bt; };
|
||||
assert.ok(h.drive(() => battle != null), 'combat starts');
|
||||
assert.equal(battle.getPlayer('p_0').bonds[bond].count, 3, `${r.id}: the battle's count`);
|
||||
const u = battle.allyUnits.find((x) => x.kind === 'op' && x.uid === w.uid);
|
||||
assert.ok(unitBonds(u).includes(bond) && isMember(battle, u, bond), `${r.id}: the battle's member`);
|
||||
}
|
||||
});
|
||||
|
||||
test('bench vs board, half pairs, two 变形同构体, the equip order, distinct members', () => {
|
||||
const h = prep({ fake: true });
|
||||
const { m } = h; const ps = h.ps('p_0');
|
||||
const bond = 'victoriaShip';
|
||||
const target = outsider(bond);
|
||||
// half pairs grant nothing
|
||||
wearer(h, 'p_0', target, [HAMMER]);
|
||||
assert.equal(count(ps, bond), 0, 'the bond item alone');
|
||||
const h2 = prep({ fake: true });
|
||||
wearer(h2, 'p_0', target, [ISO]);
|
||||
const other = Object.values(DATA.chess).find((c) => c.visible && !c.isGolden && c.tier <= 2 && !c.bonds.includes(bond) && c.chessId !== target).chessId;
|
||||
wearer(h2, 'p_0', other, [ISO, ISO_B]);
|
||||
assert.equal(count(h2.ps('p_0'), bond), 0, '变形同构体 alone / two of them');
|
||||
// order: 变形同构体 first
|
||||
const h3 = prep({ fake: true });
|
||||
const w = wearer(h3, 'p_0', target, [ISO, HAMMER], { board: false });
|
||||
assert.equal(count(h3.ps('p_0'), bond), 0, 'the pair on the bench');
|
||||
const at = legalTileFor(h3.m, h3.ps('p_0'), target);
|
||||
ok(h3.m.handle('p_0', { t: 'g.move', uid: w.uid, to: { area: 'board', row: at[0], col: at[1] } }));
|
||||
assert.equal(count(h3.ps('p_0'), bond), 1, 'deployed');
|
||||
ok(h3.m.handle('p_0', { t: 'g.move', uid: w.uid, to: { area: 'hand', idx: 9 } }));
|
||||
assert.equal(count(h3.ps('p_0'), bond), 0, 'back to the bench');
|
||||
// distinct members: two converted copies of one operator, or a converted copy beside a plain one, are one member
|
||||
const h4 = prep({ fake: true });
|
||||
const ps4 = h4.ps('p_0');
|
||||
wearer(h4, 'p_0', target, [ISO, HAMMER]);
|
||||
const plain = place(h4, 'p_0', target); // (a third copy would merge into the elite, the items back to the hand)
|
||||
assert.equal(count(ps4, bond), 1, 'converted + plain copy: one member');
|
||||
// (another 维式重锤 would merge with the equipped one into the golden hammer: the 战栗 one of the series)
|
||||
for (const id of [ISO_B, 'chess_item_2_03_e_a']) ok(h4.m.handle('p_0', { t: 'g.equip', itemUid: giveItem(h4.m, ps4, id).uid, targetUid: plain.uid }));
|
||||
assert.equal(plain.items.length, 2);
|
||||
assert.equal(count(ps4, bond), 1, 'two converted copies: one member');
|
||||
// and it never doubles a real member: a member wearing the pair still counts once
|
||||
const h5 = prep({ fake: true });
|
||||
wearer(h5, 'p_0', members(bond)[0], [ISO, HAMMER]);
|
||||
assert.equal(count(h5.ps('p_0'), bond), 1);
|
||||
for (const x of [h, h2, h3, h4, h5]) x.invariants();
|
||||
});
|
||||
|
||||
test('a teammate\'s views: m.public counts the wearer; the scouting UnitInfo and the battle\'s UnitInfo carry the items', () => {
|
||||
const h = prep({ mode: 'coop', humans: 2, seed: 41 });
|
||||
const { m } = h;
|
||||
const ps = h.ps('p_0');
|
||||
const bond = 'victoriaShip';
|
||||
const [a, b] = members(bond);
|
||||
place(h, 'p_0', a); place(h, 'p_0', b);
|
||||
const w = wearer(h, 'p_0', outsider(bond), [ISO, HAMMER]);
|
||||
const pub = m.publicView().players.find((p) => p.playerId === 'p_0');
|
||||
assert.equal(pub.bonds.find((x) => x.bondId === bond).count, 3, 'm.public players[].bonds');
|
||||
const meta = m.prepFieldMeta(ps);
|
||||
const u = meta.units.find((x) => x.uid === w.uid);
|
||||
assert.deepEqual(u.items, [ISO, HAMMER], 'prep scouting UnitInfo');
|
||||
assert.equal(meta.units.find((x) => x.defId === a).items, undefined, 'no items: no field');
|
||||
let battle = null;
|
||||
const orig = m.newBattle.bind(m);
|
||||
m.newBattle = (o) => { const bt = orig(o); if (o.players?.some((p) => p.playerId === 'p_0')) battle ??= bt; return bt; };
|
||||
assert.ok(h.drive(() => battle != null), 'combat starts');
|
||||
const su = battle.allyUnits.find((x) => x.kind === 'op' && x.uid === w.uid);
|
||||
assert.deepEqual(unitInfo(su).items, [ISO, HAMMER], 'the battle\'s UnitInfo (m.field / spawn events)');
|
||||
const plain = battle.allyUnits.find((x) => x.kind === 'op' && x.defId === a);
|
||||
assert.equal(unitInfo(plain).items, undefined, 'no items: no field');
|
||||
assert.equal(battle.enemies.every((e) => unitInfo(e).items === undefined), true, 'enemies: never');
|
||||
});
|
||||
@@ -444,6 +444,16 @@ test('live unit stats (user playtest #4 item 7): unitStats(id) reads the battle
|
||||
assert.equal(r.runner.unitIdOf(ally.uid, ally.ownerId, 'n:someone_else'), null, 'another field');
|
||||
assert.equal(r.runner.unitIdOf(null, ally.ownerId), null);
|
||||
assert.equal(r.runner.unitStats('x'), null);
|
||||
// a teammate's bond popup reads the owner's operators with their equipment (a 变形同构体 wearer is a member of the bond
|
||||
// it grants: test/ui/morph-bonds.test.js); an operator without items keeps the plain shape
|
||||
const saved = ally.items;
|
||||
ally.items = ['chess_item_6_09_e_a', 'chess_item_1_01_e_a'];
|
||||
const op = r.runner.ownerOps(ally.ownerId, start.fieldId).find((o) => o.defId === ally.defId);
|
||||
assert.deepEqual(op, { kind: 'op', ownerId: ally.ownerId, defId: ally.defId, items: ['chess_item_6_09_e_a', 'chess_item_1_01_e_a'] });
|
||||
ally.items = [];
|
||||
assert.deepEqual(r.runner.ownerOps(ally.ownerId, start.fieldId).find((o) => o.defId === ally.defId), { kind: 'op', ownerId: ally.ownerId, defId: ally.defId });
|
||||
ally.items = saved;
|
||||
assert.deepEqual(r.runner.ownerOps(ally.ownerId, 'n:someone_else'), [], 'another field');
|
||||
// enemies too, once one is out
|
||||
for (let i = 0; i < 60 && !e.battle.enemies.some((x) => x.alive); i++) r.advance(500, 50);
|
||||
const foe = e.battle.enemies.find((x) => x.alive);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// 变形同构体 on the client (player report after 0.1.0: "变形同构体配上对应装备能吃到盟约加成,但那个盟约的人口数量没有把
|
||||
// 转职之后的干员算进去"). The server counts the wearer (bondsMeta.pieceBonds; test/match/morph-bonds.test.js), so the
|
||||
// strip's "在场 n" was right — but the bond popup's member list (gameLogic bondMembers: "成员 x/y") and the wearer's own
|
||||
// card (detailPanel BondChips) only read the chess records' bonds, so the converted operator was missing from both.
|
||||
// Official rule: band 变形者集群 "与特定装备一同装备时装备者将视为特定盟约成员"; the item's talent (character_table
|
||||
// trap_1073_acarm073) lists the 14 pairings "“维式重锤”系列装备→【维多利亚】盟约 …".
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { bondMembers, grantedBonds, pieceBondIds } from '../../public/js/ui/gameLogic.js';
|
||||
import { ownerBoard } from '../../public/js/ui/watchBonds.js';
|
||||
import { pieceBonds } from '../../server/match/bondsMeta.js';
|
||||
import { GameData } from '../../server/match/gamedata.js';
|
||||
import { getData } from '../../server/data.js';
|
||||
import { GEO } from '../../shared/constants.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const load = (f) => JSON.parse(readFileSync(path.join(ROOT, 'data', f), 'utf8'));
|
||||
const chess = load('chess.json');
|
||||
const items = load('items.json');
|
||||
const bonds = load('bonds.json');
|
||||
const getChess = (id) => chess[id] || null;
|
||||
const getItem = (id) => items[id] || null;
|
||||
|
||||
const ISO = 'chess_item_6_09_e_a'; // 变形同构体
|
||||
const ISO_B = 'chess_item_6_09_e_b';
|
||||
const HAMMER = 'chess_item_1_01_e_a'; // 维式重锤 → 维多利亚
|
||||
const SHIELD = 'chess_item_1_02_e_a'; // 坚守盾牌 → 坚守
|
||||
const WEARER = 'chess_char_1_01_a'; // 隐现 (拉特兰 / 迅捷), not 维多利亚
|
||||
const VIC = Object.values(chess).filter((c) => c.visible && !c.isGolden && c.bonds.includes('victoriaShip')).map((c) => c.chessId).sort();
|
||||
|
||||
let uid = 0;
|
||||
const itemPiece = (id) => ({ uid: ++uid, id });
|
||||
const piece = (id, its = [], extra = {}) => ({ uid: ++uid, kind: 'chess', id, golden: !!chess[id]?.isGolden, tier: chess[id]?.tier ?? 1, items: its.map(itemPiece), ...extra });
|
||||
function privWith({ board = [], hand = [] } = {}) {
|
||||
const h = new Array(GEO.HAND_SIZE).fill(null);
|
||||
hand.forEach((p, i) => { h[i] = p; });
|
||||
return { board, hand: h, temp: new Array(GEO.TEMP_SIZE).fill(null) };
|
||||
}
|
||||
|
||||
describe('变形同构体 — the client rule', () => {
|
||||
test('grantedBonds: the other item\'s giveBondId while a 变形同构体 is worn; nothing otherwise', () => {
|
||||
assert.deepEqual(grantedBonds([itemPiece(ISO), itemPiece(HAMMER)], getItem), ['victoriaShip']);
|
||||
assert.deepEqual(grantedBonds([HAMMER, ISO_B], getItem), ['victoriaShip'], 'item ids, the golden 变形同构体, any order');
|
||||
assert.deepEqual(grantedBonds([itemPiece(HAMMER)], getItem), [], 'the bond item alone grants nothing');
|
||||
assert.deepEqual(grantedBonds([itemPiece(ISO)], getItem), []);
|
||||
assert.deepEqual(grantedBonds([ISO, ISO_B], getItem), [], 'two 变形同构体: no partner bond');
|
||||
assert.deepEqual(grantedBonds(null, getItem), []);
|
||||
assert.deepEqual(pieceBondIds(chess[WEARER], [ISO, SHIELD], getItem), ['lateranoShip', 'swiftShip', 'steadShip']);
|
||||
assert.deepEqual(pieceBondIds(chess[WEARER], [], getItem), ['lateranoShip', 'swiftShip']);
|
||||
});
|
||||
|
||||
test('the client rule equals the server\'s (bondsMeta.pieceBonds) for every bond item, normal and golden', () => {
|
||||
const gd = new GameData(getData({ log: { warn() {}, error() {}, info() {} } }), 'mode_multi_hard');
|
||||
const grants = Object.values(items).filter((r) => r.giveBondId);
|
||||
assert.equal(grants.length, 36, '18 bond items × normal / golden');
|
||||
for (const r of grants) {
|
||||
for (const iso of [ISO, ISO_B]) {
|
||||
const p = { id: WEARER, items: [{ id: r.id }, { id: iso }] };
|
||||
assert.deepEqual(pieceBondIds(chess[WEARER], p.items, getItem), pieceBonds(gd, p), `${r.id} + ${iso}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('变形同构体 — the bond popup\'s member list', () => {
|
||||
test('the wearer on the board is an on-board member of the granted bond (成员 x/y agrees with 在场)', () => {
|
||||
const b = bonds.victoriaShip;
|
||||
const priv = privWith({ board: [{ ...piece(VIC[0]), row: 9, col: 3 }, { ...piece(VIC[1]), row: 9, col: 4 }, { ...piece(WEARER, [ISO, HAMMER]), row: 9, col: 5 }] });
|
||||
const rows = bondMembers(b, priv, [], getChess, getItem);
|
||||
const on = rows.filter((r) => r.onBoard).map((r) => r.id).sort();
|
||||
assert.deepEqual(on, [VIC[0], VIC[1], WEARER].sort(), 'three in play, like the server\'s count');
|
||||
const w = rows.find((r) => r.id === WEARER);
|
||||
assert.equal(w.granted, true, 'marked as converted');
|
||||
assert.equal(w.owned, true);
|
||||
assert.equal(w.name, chess[WEARER].name);
|
||||
assert.equal(rows.length, b.visibleMembers.length + 1, 'the converted operator joins the list');
|
||||
assert.equal(rows.filter((r) => r.granted).length, 1);
|
||||
// not a member of an unrelated bond, and not with the bond item alone
|
||||
assert.equal(bondMembers(bonds.steadShip, priv, [], getChess, getItem).some((r) => r.id === WEARER), false);
|
||||
const plain = privWith({ board: [{ ...piece(WEARER, [HAMMER]), row: 9, col: 5 }] });
|
||||
assert.equal(bondMembers(b, plain, [], getChess, getItem).some((r) => r.id === WEARER), false);
|
||||
});
|
||||
|
||||
test('a wearer on the bench is owned, not in play; copies of one operator are one member (distinct members)', () => {
|
||||
const b = bonds.victoriaShip;
|
||||
const bench = privWith({ hand: [piece(WEARER, [ISO, HAMMER])] });
|
||||
const w = bondMembers(b, bench, [], getChess, getItem).find((r) => r.id === WEARER);
|
||||
assert.ok(w && w.granted && w.owned && !w.onBoard, 'bench: owned only (BOARD bonds count the board)');
|
||||
const two = privWith({
|
||||
board: [{ ...piece(WEARER, [ISO, HAMMER]), row: 9, col: 5 }, { ...piece(chess[WEARER].goldenId, [ISO_B, HAMMER]), row: 9, col: 6 }],
|
||||
hand: [piece(WEARER, [ISO, HAMMER])],
|
||||
});
|
||||
const rows = bondMembers(b, two, [], getChess, getItem).filter((r) => r.id === WEARER);
|
||||
assert.equal(rows.length, 1, 'one entry per operator (normal and elite share it)');
|
||||
assert.equal(rows[0].onBoard, true);
|
||||
});
|
||||
|
||||
test('a teammate\'s operators on the field (ownerBoard) carry their items, so their popup lists the wearer too', () => {
|
||||
const field = { units: [
|
||||
{ kind: 'op', ownerId: 'p2', defId: WEARER, items: [ISO, HAMMER] },
|
||||
{ kind: 'op', ownerId: 'p2', defId: VIC[0] },
|
||||
{ kind: 'op', ownerId: 'p1', defId: VIC[1], items: [ISO, SHIELD] },
|
||||
] };
|
||||
const board = ownerBoard(field, 'p2');
|
||||
const on = bondMembers(bonds.victoriaShip, board, [], getChess, getItem).filter((r) => r.onBoard).map((r) => r.id).sort();
|
||||
assert.deepEqual(on, [VIC[0], WEARER].sort());
|
||||
});
|
||||
});
|
||||
|
||||
// ---- the components (htm vnodes walked like test/ui/watch-bonds.test.js) -------------------------------------------
|
||||
// the browser data store reads the real data files from disk
|
||||
globalThis.fetch = async (url) => {
|
||||
const name = String(url).split('/').pop();
|
||||
try {
|
||||
const body = readFileSync(path.join(ROOT, 'data', name), 'utf8');
|
||||
return { ok: true, status: 200, json: async () => JSON.parse(body) };
|
||||
} catch {
|
||||
return { ok: false, status: 404, json: async () => ({}) };
|
||||
}
|
||||
};
|
||||
const { data } = await import('../../public/js/data.js');
|
||||
await data.loadAll('bonds', 'chess', 'items', 'assets');
|
||||
const { BondPopup } = await import('../../public/js/ui/bondStrip.js');
|
||||
const { BondChips, resolveDetail } = await import('../../public/js/ui/detailPanel.js');
|
||||
|
||||
function* walk(v) {
|
||||
if (Array.isArray(v)) { for (const x of v) yield* walk(x); return; }
|
||||
if (!v || typeof v !== 'object') return;
|
||||
yield v;
|
||||
yield* walk(v.props?.children);
|
||||
}
|
||||
const hasClass = (v, c) => typeof v?.props?.class === 'string' && v.props.class.split(/\s+/).includes(c);
|
||||
const textOf = (v) => {
|
||||
if (v == null || typeof v === 'boolean') return '';
|
||||
if (typeof v === 'string' || typeof v === 'number') return String(v);
|
||||
if (Array.isArray(v)) return v.map(textOf).join('');
|
||||
return textOf(v.props?.children);
|
||||
};
|
||||
|
||||
describe('变形同构体 — what the popup and the card show', () => {
|
||||
test('the popup: 在场 3 and 成员 3/11 — the wearer lit, tagged 同构, titled 变形同构体', () => {
|
||||
const priv = privWith({ board: [{ ...piece(VIC[0]), row: 9, col: 3 }, { ...piece(VIC[1]), row: 9, col: 4 }, { ...piece(WEARER, [ISO, HAMMER]), row: 9, col: 5 }] });
|
||||
const entry = { bondId: 'victoriaShip', count: 3, active: true, tier: 1, layers: 0, thresholds: [3, 6], countsHand: false };
|
||||
const v = BondPopup({ bondId: 'victoriaShip', entry, priv, onClose() {} });
|
||||
const head = [...walk(v)].filter((x) => x.type === 'h4').map(textOf).find((t) => t.startsWith('成员'));
|
||||
assert.equal(head.replace(/\s+/g, ''), `成员3/${bonds.victoriaShip.visibleMembers.length + 1}`);
|
||||
assert.match(textOf([...walk(v)].find((x) => hasClass(x, 'bpop__facts'))), /在场\s*3/);
|
||||
const iso = [...walk(v)].filter((x) => hasClass(x, 'bpop__member') && hasClass(x, 'is-granted'));
|
||||
assert.equal(iso.length, 1);
|
||||
assert.ok(hasClass(iso[0], 'is-on'));
|
||||
assert.match(iso[0].props.title, /变形同构体/);
|
||||
assert.ok([...walk(iso[0])].some((x) => hasClass(x, 'bpop__iso') && textOf(x) === '同构'));
|
||||
});
|
||||
|
||||
test('the card\'s bond chips: the granted bond with its count, dashed and tagged; resolveDetail hands a teammate unit\'s items on', () => {
|
||||
const ids = pieceBondIds(chess[WEARER], [ISO, HAMMER], getItem);
|
||||
const mine = [{ bondId: 'victoriaShip', count: 3, active: true, tier: 1, layers: 0, thresholds: [3, 6] }];
|
||||
const v = BondChips({ bondIds: ids, bonds: mine, granted: ['victoriaShip'], onBond() {} });
|
||||
const chips = [...walk(v)].filter((x) => hasClass(x, 'dbond'));
|
||||
assert.deepEqual(chips.map((x) => x.props['data-bond']), ['lateranoShip', 'swiftShip', 'victoriaShip']);
|
||||
const vic = chips[2];
|
||||
assert.ok(hasClass(vic, 'is-granted') && hasClass(vic, 'is-active'));
|
||||
assert.match(vic.props.title, /维多利亚(变形同构体):在场 3\/6/);
|
||||
assert.ok(!hasClass(chips[0], 'is-granted'));
|
||||
const d = resolveDetail({ kind: 'unit', unit: { id: 7, side: 'ally', ownerId: 'p2', defId: WEARER, items: [ISO, HAMMER] } }, new Map());
|
||||
assert.deepEqual(d.unitItems, [ISO, HAMMER]);
|
||||
assert.equal(resolveDetail({ kind: 'unit', unit: { id: 8, side: 'ally', ownerId: 'p2', defId: WEARER } }, new Map()).unitItems, null);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user