Bonds of 自选 pieces checked everywhere membership is decided (the owner's reminder of 2026-10-05): the match's counts through the player's data view (煌 in 炎 and 维多利亚, one member per slot, 绝技 elites), the sim's bond effects through def.bonds (叙拉古 on deployment, 协防 ×1.2 / elite ×1.4), the client's strip, popups, cards and detail card. Golden unchanged. Tests: test/ui/diy-ui.test.js (15), test/match/diy-bonds.test.js (4), test/i18n.test.js pins screens/diy.js; test/ui/diy.e2e.test.js (SP_E2E=1, run once: pass).
341 lines
22 KiB
JavaScript
341 lines
22 KiB
JavaScript
// 0.2.0 自选编队 — client side (research 0.2.0 §2; the owner's decisions of 2026-10-05): the 自选编队 tab's model (storage,
|
|
// the slots, the picker's operators with the kit list, skill / module choices without 集成战略 modules, export / import)
|
|
// and its server sync (room.diy; welcome.diyKitted), the tab's components (four slots, the picker, 确认), and the match
|
|
// UI of a 自选 piece — the shop / reward card and the detail card show the operator (name, art, class, derived bonds,
|
|
// the pick's skill and module) with a 「自选」 badge, the own pieces get a tag, placement reads the operator's position, and
|
|
// the bond popup lists the 自选 member (own and a teammate's).
|
|
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';
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
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 M = await import('../../public/js/ui/diyModel.js');
|
|
const { installDiySync, SYNC_DEBOUNCE_MS } = await import('../../public/js/ui/loadoutSync.js');
|
|
const { createStore } = await import('../../public/js/store.js');
|
|
const G = await import('../../public/js/ui/gameLogic.js');
|
|
const { ChessCard } = await import('../../public/js/ui/shopBar.js');
|
|
const { ChessDetail, resolveDetail } = await import('../../public/js/ui/detailPanel.js');
|
|
const { BondPopup } = await import('../../public/js/ui/bondStrip.js');
|
|
const { ownerBoard } = await import('../../public/js/ui/watchBonds.js');
|
|
const { diyPieces } = await import('../../public/js/screens/game/standInTags.js');
|
|
const { DiyPanelView, DiyPickerView } = await import('../../public/js/screens/diy.js');
|
|
const { data } = await import('../../public/js/data.js');
|
|
const { DATA } = await import('../match/harness.js');
|
|
const { KITTED_CHARS } = await import('../../server/sim/content/kits/index.js');
|
|
|
|
await data.loadAll('chess', 'bonds', 'assets', 'garrisons', 'items', 'backups');
|
|
|
|
const D = { chess: DATA.chess, backups: DATA.backups };
|
|
const getChess = (id) => (Object.hasOwn(DATA.chess, id) ? DATA.chess[id] : null);
|
|
const T5A = 'chess_char_5_diy1_a';
|
|
const T5B = 'chess_char_5_diy2_a';
|
|
const T6A = 'chess_char_6_diy1_a';
|
|
const T6B = 'chess_char_6_diy2_a';
|
|
const SIEGE = 'char_112_siege';
|
|
const SHARP = 'char_609_acguad';
|
|
const KALTS = 'char_003_kalts'; // 凯尔希: an ISW-A module at both elite stages
|
|
const SIEGE_PICK = { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' };
|
|
const KIT = [...KITTED_CHARS];
|
|
|
|
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);
|
|
/** Render a component tree one level of function components deep at a time (htm vnodes with function types). */
|
|
function expand(v, depth = 6) {
|
|
if (Array.isArray(v)) return v.map((x) => expand(x, depth));
|
|
if (!v || typeof v !== 'object') return v;
|
|
if (typeof v.type === 'function' && depth > 0) {
|
|
try { return expand(v.type(v.props || {}), depth - 1); } catch { return v; }
|
|
}
|
|
const children = v.props?.children;
|
|
return children === undefined ? v : { ...v, props: { ...v.props, children: expand(children, depth) } };
|
|
}
|
|
const textOf = (v) => [...walk(v)].flatMap((n) => (Array.isArray(n.props?.children) ? n.props.children : [n.props?.children])).filter((x) => typeof x === 'string' || typeof x === 'number').join('');
|
|
|
|
describe('自选编队 model (ui/diyModel.js)', () => {
|
|
test('storage: tolerant parse, junk and hostile keys dropped, round trip', () => {
|
|
assert.deepEqual(M.parseStoredDiy(null), {});
|
|
const raw = { v: 1, picks: { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP }, __proto__x: 1, bad: { charId: 'a b' }, [T6B]: { charId: SIEGE, skillIndex: 99 } } };
|
|
assert.deepEqual(M.parseStoredDiy(raw), { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP }, [T6B]: { charId: SIEGE } });
|
|
assert.deepEqual(M.toStoredDiy({ [T5A]: SIEGE_PICK }), { v: M.DIY_VERSION, picks: { [T5A]: SIEGE_PICK } });
|
|
assert.equal(M.diyCount({ [T5A]: SIEGE_PICK, [T6A]: null }), 1);
|
|
assert.deepEqual(M.setPick({ [T5A]: SIEGE_PICK }, T5A, null), {});
|
|
assert.deepEqual(M.setPick({}, T6A, { charId: SHARP }), { [T6A]: { charId: SHARP } });
|
|
});
|
|
|
|
test('the four slots by tier, with the 调度中心 level that sells them', () => {
|
|
assert.deepEqual(M.diySlotList(D).map((s) => [s.slotId, s.tier, s.shopLevel]), [[T5A, 5, 5], [T5B, 5, 5], [T6A, 6, 6], [T6B, 6, 6]]);
|
|
});
|
|
|
|
test('the picker lists only kitted operators (prototypes first), marks one another slot holds', () => {
|
|
const opts = M.pickOptions(T5A, {}, D, KIT);
|
|
const ids = opts.map((o) => o.charId);
|
|
assert.ok(ids.includes(SIEGE) && ids.includes(SHARP) && ids.includes('char_601_cguard'));
|
|
assert.ok(ids.every((id) => KIT.includes(id)), 'no operator without a kit');
|
|
assert.ok(ids.indexOf(SHARP) < ids.indexOf(SIEGE), 'prototypes first');
|
|
assert.deepEqual(opts.find((o) => o.charId === SIEGE).bonds, ['victoriaShip']);
|
|
assert.equal(opts.find((o) => o.charId === SHARP).proto, true);
|
|
assert.ok(!M.pickOptions(T6A, {}, D, KIT).some((o) => o.charId === 'char_601_cguard'), 'a 4★ reserve is a tier-5 pick only');
|
|
// taken: an owned operator in any other slot; a prototype in the other slot of its tier (not across tiers)
|
|
const roster = { [T5B]: { charId: SHARP }, [T6A]: SIEGE_PICK };
|
|
const at5 = M.pickOptions(T5A, roster, D, KIT);
|
|
assert.equal(at5.find((o) => o.charId === SIEGE).taken, T6A);
|
|
assert.equal(at5.find((o) => o.charId === SHARP).taken, T5B);
|
|
assert.equal(M.pickOptions(T6B, roster, D, KIT).find((o) => o.charId === SHARP).taken, null, 'a prototype may fill a tier-5 and a tier-6 slot');
|
|
assert.deepEqual(M.pickOptions(T5A, {}, D, []), [], 'no kit list (not connected): nothing to pick');
|
|
});
|
|
|
|
test('choices: three skills, the elite form\'s modules without the 集成战略 (ISW-A) ones; a prototype\'s locked selection', () => {
|
|
const ch = M.pickChoices(SIEGE, T6A, D);
|
|
assert.deepEqual(ch.skills.map((s) => s.index), [0, 1, 2]);
|
|
assert.deepEqual(ch.modules.map((x) => x.uniEquipId), ['uniequip_002_siege', 'uniequip_003_siege']);
|
|
assert.equal(ch.stage, 3);
|
|
assert.equal(M.pickChoices(SIEGE, T5A, D).stage, 1);
|
|
const kalts = M.pickChoices(KALTS, T6A, D);
|
|
assert.ok(kalts.elite.modules.some((x) => x.typeName === 'ISW-A'), 'the form has one');
|
|
assert.ok(!kalts.modules.some((x) => x.rec.typeName === 'ISW-A'), 'never offered');
|
|
const proto = M.pickChoices(SHARP, T5A, D);
|
|
assert.equal(proto.proto, true);
|
|
assert.deepEqual(proto.locked, { skillIndex: 2, uniEquipId: 'uniequip_002_acguad' });
|
|
assert.deepEqual(M.defaultPick(SIEGE, T5A, D), { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' }, 'S3 and the first module');
|
|
assert.deepEqual(M.defaultPick(SHARP, T5A, D), { charId: SHARP });
|
|
});
|
|
|
|
test('sanitize keeps what the server keeps; export / import envelopes', () => {
|
|
assert.deepEqual(M.sanitizeDiyPicks({ [T5A]: SIEGE_PICK, [T6A]: { charId: SIEGE, skillIndex: 0 } }, D, KIT), { [T5A]: SIEGE_PICK });
|
|
const text = M.serializeDiy({ [T5A]: SIEGE_PICK }, { now: 0 });
|
|
const env = JSON.parse(text);
|
|
assert.deepEqual([env.kind, env.v, env.count, env.picks], [M.DIY_EXPORT_KIND, 1, 1, { [T5A]: SIEGE_PICK }]);
|
|
assert.deepEqual(M.parseDiyImport(text), { ok: true, picks: { [T5A]: SIEGE_PICK } });
|
|
assert.deepEqual(M.parseDiyImport({ v: 1, picks: {} }), { ok: true, picks: {} });
|
|
for (const bad of ['', '{nope', { v: 9, picks: {} }, { kind: 'stronghold.ownership', v: 1, notOwned: [] }, 'x'.repeat(M.DIY_IMPORT_MAX_BYTES + 1), [1]]) {
|
|
assert.equal(M.parseDiyImport(bad).ok, false, JSON.stringify(bad).slice(0, 30));
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---- sync -------------------------------------------------------------------------------------------------------------
|
|
|
|
function fakeNet() {
|
|
const listeners = new Map();
|
|
const net = {
|
|
status: 'online', sent: [], replies: [],
|
|
on(t, fn) { if (!listeners.has(t)) listeners.set(t, new Set()); listeners.get(t).add(fn); return () => listeners.get(t).delete(fn); },
|
|
emit(t, msg) { for (const fn of listeners.get(t) || []) fn(msg); },
|
|
request(t, fields) {
|
|
net.sent.push({ t, ...fields });
|
|
const r = net.replies.shift();
|
|
return r && r.error ? Promise.reject(Object.assign(new Error(r.error), { code: r.error })) : Promise.resolve({ t: 'ok' });
|
|
},
|
|
};
|
|
return net;
|
|
}
|
|
function fakeTimers() {
|
|
let now = 0;
|
|
let seq = 0;
|
|
const q = new Map();
|
|
return {
|
|
setTimeout: (fn, ms) => { const id = ++seq; q.set(id, { at: now + ms, fn }); return id; },
|
|
clearTimeout: (id) => q.delete(id),
|
|
async advance(ms) {
|
|
const end = now + ms;
|
|
for (;;) {
|
|
const next = [...q.entries()].filter(([, x]) => x.at <= end).sort((a, b) => a[1].at - b[1].at)[0];
|
|
if (!next) break;
|
|
q.delete(next[0]);
|
|
now = next[1].at;
|
|
next[1].fn();
|
|
for (let i = 0; i < 5; i++) await Promise.resolve();
|
|
}
|
|
now = end;
|
|
for (let i = 0; i < 5; i++) await Promise.resolve();
|
|
},
|
|
};
|
|
}
|
|
|
|
test('diy sync: welcome brings the kit list and sends room.diy; edits are debounced; in a match stored for the next one (told once)', async () => {
|
|
const net = fakeNet();
|
|
const T = fakeTimers();
|
|
const told = [];
|
|
const target = createStore({ diy: { [T5A]: SIEGE_PICK }, diyKitted: null, open: false, diySync: 'idle' });
|
|
const s = installDiySync({ net, timers: T, target, notify: (x) => told.push(x) });
|
|
net.emit('welcome', { diyKitted: [SIEGE, SHARP, 7] });
|
|
assert.deepEqual(target.get().diyKitted, [SIEGE, SHARP]);
|
|
await T.advance(100);
|
|
assert.deepEqual(net.sent, [{ t: 'room.diy', picks: { [T5A]: SIEGE_PICK } }]);
|
|
assert.equal(target.get().diySync, 'synced');
|
|
target.set({ diy: { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP } } });
|
|
await T.advance(SYNC_DEBOUNCE_MS + 10);
|
|
assert.deepEqual(net.sent[1], { t: 'room.diy', picks: { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP } } });
|
|
net.replies.push({ error: 'ROOM_STARTED' });
|
|
target.set({ diy: {} });
|
|
await T.advance(SYNC_DEBOUNCE_MS + 10);
|
|
assert.equal(target.get().diySync, 'locked');
|
|
assert.deepEqual(told, ['自选编队是局外设置,修改将在下一局生效']);
|
|
net.emit('room.state', { inMatch: false });
|
|
await T.advance(SYNC_DEBOUNCE_MS + 10);
|
|
assert.deepEqual(net.sent.at(-1), { t: 'room.diy', picks: {} });
|
|
s.dispose();
|
|
});
|
|
|
|
// ---- the tab --------------------------------------------------------------------------------------------------------------
|
|
|
|
describe('自选编队 tab (screens/diy.js)', () => {
|
|
test('four slots by tier: an empty slot to fill, a filled one shows the operator, its tag, skill and module', () => {
|
|
const tree = expand(DiyPanelView({ m: data.get('assets'), picks: { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP } }, legal: { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP } }, kitted: KIT, onSet() {}, picking: null, onPicking() {} }));
|
|
const slots = [...walk(tree)].filter((v) => hasClass(v, 'diy-slot'));
|
|
assert.deepEqual(slots.map((v) => v.props['data-slot']), [T5A, T5B, T6A, T6B]);
|
|
const siege = slots[0];
|
|
assert.equal(siege.props['data-char'], SIEGE);
|
|
const txt = textOf(siege);
|
|
assert.ok(txt.includes(DATA.backups.units[SIEGE].name) && txt.includes('已持有') && txt.includes('S3') && txt.includes('SOL-X'), txt);
|
|
assert.ok(textOf(slots[2]).includes('原型'));
|
|
assert.ok(hasClass(slots[1], 'is-empty') && textOf(slots[1]).includes('选择干员'));
|
|
});
|
|
|
|
test('the picker: the slot\'s operators; choosing one and its skill / module confirms the pick', () => {
|
|
const done = [];
|
|
let draft = { ...SIEGE_PICK };
|
|
const view = () => expand(DiyPickerView({ m: null, slot: { slotId: T5A, tier: 5, shopLevel: 5 }, picks: { [T5A]: SIEGE_PICK }, kitted: KIT, onDone: (p) => done.push(p), onClose() {},
|
|
filter: 'all', query: '', draft, onFilter() {}, onQuery() {}, onDraft: (d) => { draft = d; } }));
|
|
let tree = view();
|
|
const opts = [...walk(tree)].filter((v) => hasClass(v, 'diy-opt'));
|
|
assert.ok(opts.some((v) => v.props['data-char'] === SIEGE) && opts.some((v) => v.props['data-char'] === SHARP));
|
|
// the current pick is shown with its three skills and its modules (none + X + Y)
|
|
const skills = [...walk(tree)].filter((v) => v.props?.role === 'radio' && v.props['data-skill'] != null);
|
|
assert.deepEqual(skills.map((v) => v.props['data-skill']), [0, 1, 2]);
|
|
assert.equal(skills.find((v) => v.props['aria-checked'] === 'true').props['data-skill'], 2);
|
|
const mods = [...walk(tree)].filter((v) => v.props?.role === 'radio' && v.props['data-module'] != null);
|
|
assert.deepEqual(mods.map((v) => v.props['data-module']), ['none', 'uniequip_002_siege', 'uniequip_003_siege']);
|
|
// another skill and no module, then 确认
|
|
skills.find((v) => v.props['data-skill'] === 0).props.onClick();
|
|
tree = view();
|
|
[...walk(tree)].find((v) => v.props?.role === 'radio' && v.props['data-module'] === 'none').props.onClick();
|
|
tree = view();
|
|
[...walk(tree)].find((v) => v.props?.['data-testid'] === 'diy-confirm').props.onClick();
|
|
assert.deepEqual(done, [{ charId: SIEGE, skillIndex: 0, uniEquipId: null }]);
|
|
// a prototype: its locked skill only; 确认 sends just the operator (the server completes the locked selection)
|
|
[...walk(tree)].find((v) => hasClass(v, 'diy-opt') && v.props['data-char'] === SHARP).props.onClick();
|
|
tree = view();
|
|
assert.deepEqual([...walk(tree)].filter((v) => v.props?.role === 'radio' && v.props['data-skill'] != null).map((v) => [v.props['data-skill'], v.props.disabled]), [[2, true]]);
|
|
[...walk(tree)].find((v) => v.props?.['data-testid'] === 'diy-confirm').props.onClick();
|
|
assert.deepEqual(done.at(-1), { charId: SHARP });
|
|
});
|
|
});
|
|
|
|
// ---- match UI -------------------------------------------------------------------------------------------------------------
|
|
|
|
describe('match UI of a 自选 piece', () => {
|
|
const priv = { diy: { [T5A]: SIEGE_PICK, [T6A]: { charId: SHARP, skillIndex: 2, uniEquipId: 'uniequip_002_acguad' } }, loadout: {}, standIns: [], board: [], hand: [], temp: [] };
|
|
test('gameLogic: the own pick of a slot (normal + elite), the composed record, the own getter', () => {
|
|
assert.deepEqual(G.ownDiyPick(priv, getChess(T5A)), SIEGE_PICK);
|
|
assert.deepEqual(G.ownDiyPick(priv, getChess('chess_char_5_diy1_b')), SIEGE_PICK, 'the elite too');
|
|
assert.equal(G.ownDiyPick(priv, getChess(T5B)), null, 'a slot not filled');
|
|
assert.equal(G.ownDiyPick(priv, getChess('chess_char_4_22_a')), null);
|
|
const rec = G.ownDiyRecord(getChess(T5A), priv, D);
|
|
assert.deepEqual([rec.name, rec.charId, rec.diyFor, rec.bonds, rec.tier, rec.price], [DATA.backups.units[SIEGE].name, SIEGE, T5A, ['victoriaShip'], 5, 4]);
|
|
assert.equal(G.ownDiyRecord(getChess(T5A), priv, D), rec, 'cached');
|
|
const get = G.diyGetter(getChess, priv, D);
|
|
assert.equal(get(T5A), rec);
|
|
assert.equal(get('chess_char_4_22_a'), getChess('chess_char_4_22_a'));
|
|
assert.equal(G.chessLoadout(get('chess_char_5_diy1_b'), priv.loadout, get).skill.skillId, 'skchr_siege_3', 'the pick\'s skill');
|
|
assert.equal(G.deployedModuleId(get('chess_char_5_diy1_b'), priv, get, D.backups), 'uniequip_002_siege', 'the pick\'s module on the elite');
|
|
assert.equal(G.cardDiy(getChess(T5A), { unit: { diy: { charId: SHARP, skillIndex: 2, uniEquipId: 'uniequip_002_acguad' } }, data: D }).charId, SHARP, 'a teammate\'s unit says it itself');
|
|
});
|
|
|
|
test('placement reads the operator\'s position (推进之王 melee) through the own getter', () => {
|
|
const get = G.diyGetter(getChess, priv, D);
|
|
const ctx = G.placementContext({ priv, stage: null, editable: true, getChess: get, backups: D.backups });
|
|
assert.equal(G.piecePosition(ctx, { kind: 'chess', id: T5A }), 'MELEE');
|
|
});
|
|
|
|
test('shop / reward card: the operator\'s name and bonds, the 「自选」 badge, the pick\'s skill', () => {
|
|
const card = ChessCard({ slot: { kind: 'chess', id: T5A, price: 4, basePrice: 4 }, idx: 0, priv, onBuy() {}, onDetail() {} });
|
|
const nodes = [...walk(card)];
|
|
const badge = nodes.find((v) => hasClass(v, 'scard__diy'));
|
|
assert.ok(badge);
|
|
assert.equal(badge.props['data-diy'], SIEGE);
|
|
assert.equal(badge.props.children, '自选');
|
|
assert.equal(nodes.find((v) => hasClass(v, 'scard__name')).props.children, DATA.backups.units[SIEGE].name);
|
|
assert.ok(nodes.some((v) => v.props?.bondId === 'victoriaShip'), 'the derived bond');
|
|
assert.equal(nodes.find((v) => v?.type && v.props?.lo).props.lo.skill.skillId, 'skchr_siege_3');
|
|
const other = ChessCard({ slot: { kind: 'chess', id: T5A, price: 4, basePrice: 4 }, idx: 0, priv: { diy: {} }, onBuy() {}, onDetail() {} });
|
|
assert.equal([...walk(other)].find((v) => hasClass(v, 'scard__diy')), undefined);
|
|
});
|
|
|
|
test('detail card: own piece / card / a unit carrying diy → the operator, the 「自选」 tag, the pick\'s skill and module', () => {
|
|
const piece = { uid: 7, kind: 'chess', id: 'chess_char_5_diy1_b', golden: true, items: [] };
|
|
const pieces = new Map([[7, { piece, area: 'board', row: 10, col: 4 }]]);
|
|
const r = resolveDetail({ kind: 'piece', uid: 7 }, pieces, { priv, backups: D.backups });
|
|
assert.equal(r.chess.charId, SIEGE);
|
|
assert.deepEqual(r.diy, SIEGE_PICK);
|
|
assert.equal(resolveDetail({ kind: 'chess', id: T5A }, new Map(), { priv, backups: D.backups }).chess.charId, SIEGE, 'a shop card');
|
|
assert.equal(resolveDetail({ kind: 'chess', id: T5A, foreign: true }, new Map(), { priv, backups: D.backups }).diy, undefined, 'the banned list / another player\'s card');
|
|
const u = resolveDetail({ kind: 'unit', unit: { id: 3, uid: 33, side: 'ally', kind: 'op', defId: T6A, diy: { charId: SHARP, skillIndex: 2, uniEquipId: 'uniequip_002_acguad' } } }, new Map(), { priv: null, backups: D.backups });
|
|
assert.equal(u.chess.charId, SHARP);
|
|
const blocks = ChessDetail({ chess: r.chess, piece, editable: false, bonds: [], loadout: priv.loadout, diy: r.diy });
|
|
const head = blocks.find((b) => b.key === 'head');
|
|
assert.equal([...walk(head)].find((v) => hasClass(v, 'dtag-diy'))?.props.children, '自选');
|
|
assert.equal([...walk(head)].find((v) => hasClass(v, 'dhead__name')).props.children, DATA.backups.units[SIEGE].name);
|
|
const skill = blocks.find((b) => b.key === 'skill');
|
|
assert.ok(JSON.stringify(skill.props).includes('skchr_siege_3'));
|
|
const mod = blocks.find((b) => b.key === 'module');
|
|
assert.ok(JSON.stringify(mod.props).includes('uniequip_002_siege'), 'the pick\'s module (elite)');
|
|
assert.ok(!blocks.some((b) => b.key === 'garrison'), 'no 特质');
|
|
});
|
|
|
|
test('own prep pieces of a filled slot get the 「自选」 tag', () => {
|
|
const p = { ...priv, hand: [{ uid: 1, kind: 'chess', id: T5A }, { uid: 2, kind: 'chess', id: 'chess_char_4_22_a' }], temp: [], board: [{ uid: 4, kind: 'chess', id: 'chess_char_6_diy1_b', row: 10, col: 4 }] };
|
|
assert.deepEqual(diyPieces(p, getChess, D).map((x) => [x.uid, x.area, x.label, x.name]), [[1, 'hand', '自选', DATA.backups.units[SIEGE].name], [4, 'board', '自选', DATA.backups.units[SHARP].name]]);
|
|
assert.deepEqual(diyPieces({ ...p, diy: {} }, getChess, D), []);
|
|
});
|
|
|
|
test('bonds: the popup lists the own 自选 member (煌-style several bonds count each) and a teammate\'s by its unit\'s pick', () => {
|
|
const get = G.diyGetter(getChess, priv, D);
|
|
const own = { ...priv, board: [{ uid: 4, kind: 'chess', id: T5A, row: 10, col: 4 }], hand: [{ uid: 5, kind: 'chess', id: T6A }] };
|
|
const vic = G.bondMembers(DATA.bonds.victoriaShip, own, [], get);
|
|
const row = vic.find((x) => x.diy);
|
|
assert.deepEqual([row.id, row.name, row.onBoard, row.owned, row.rec.charId], [T5A, DATA.backups.units[SIEGE].name, true, true, SIEGE]);
|
|
const empty = G.bondMembers(DATA.bonds.emptyShip, own, [], get);
|
|
assert.deepEqual(empty.filter((x) => x.diy).map((x) => [x.id, x.inHand]), [[T6A, true]], 'the prototype in the hand counts for 协防');
|
|
assert.equal(G.memberHeadCount(vic.filter((x) => x.diy)), 1);
|
|
// a watched teammate: its board from the field units, their picks carried along (watchBonds ownerBoard)
|
|
const field = { units: [{ kind: 'op', ownerId: 'p_2', defId: T5B, diy: { charId: 'char_017_huang', skillIndex: 0, uniEquipId: null } }] };
|
|
const mate = ownerBoard(field, 'p_2');
|
|
assert.deepEqual(mate.board[0].diy, { charId: 'char_017_huang', skillIndex: 0, uniEquipId: null });
|
|
const pieceRecord = (p) => (p.diy ? G.diyRecordFor(getChess(p.id), p.diy, D) : null) || getChess(p.id);
|
|
for (const b of ['yanShip', 'victoriaShip']) {
|
|
const rows = G.bondMembers(DATA.bonds[b], mate, [], getChess, () => null, pieceRecord);
|
|
assert.ok(rows.some((x) => x.diy && x.rec.charId === 'char_017_huang'), `煌 is a ${b} member`);
|
|
}
|
|
// the popup itself renders the 自选 row with its tag
|
|
const pop = expand(BondPopup({ bondId: 'victoriaShip', entry: { count: 1, active: false, tier: 0, layers: 0 }, priv: own, onClose() {} }));
|
|
const mrow = [...walk(pop)].find((v) => hasClass(v, 'bpop__member') && textOf(v).includes(DATA.backups.units[SIEGE].name));
|
|
assert.ok(mrow, 'the 自选 member row');
|
|
assert.ok([...walk(mrow)].some((v) => hasClass(v, 'bpop__diy')), 'tagged 自选');
|
|
});
|
|
|
|
test('the renderer draws the operator (pieceInfo reads m.private.diy; UnitInfo diy is kept)', () => {
|
|
const src = readFileSync(path.join(ROOT, 'public/js/render/app.js'), 'utf8');
|
|
assert.match(src, /const pick = chess && chess\.isDiy \? diyPicks\[chess\.baseId \|\| chess\.chessId\] : null;/);
|
|
assert.match(src, /diyPicks = src\.diy && typeof src\.diy === 'object' \? src\.diy : \{\};/);
|
|
assert.match(readFileSync(path.join(ROOT, 'public/js/render/app/info.js'), 'utf8'), /diy: u\.diy && typeof u\.diy === 'object'/);
|
|
});
|
|
});
|