feedback5: merge feedback4 — PR #129, the lobby ERR import
This commit is contained in:
commit
8ab6d021da
15 files changed
+521
-20
No files matched your search
@@ -0,0 +1,138 @@
|
||||
// 观战队友的整备区(游玩记录 #2 item 1「观战队友时看不到队友的未上场的干员」,建议见 GitHub #44
|
||||
// 「看不见队友的待战栏位」):prep 侦察的 m.field(`Match.prepFieldMeta`)把被侦察玩家的手牌当作
|
||||
// 单位放进手牌行(row 7,col = 手牌槽位,道具是 kind 'item')——客户端按自己整备区的同一渲染路径
|
||||
// 画出这一行(app.js battleView / ItemView)。手牌变化(部署 / 购入 / 装备)跟棋盘变化一样触发
|
||||
// 推送(`_prepScoutSig`);观战席副本与玩家完全一致(test/match/spectator.test.js)。客户端:
|
||||
// test/ui/prep-bench.test.js。
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { GEO, PHASE } from '../../shared/constants.js';
|
||||
import { makeMatch, give, giveItem, legalTileFor, chessOfTier } from './harness.js';
|
||||
|
||||
const MELEE = (c) => c.position === 'MELEE' && c.profession === 'TANK';
|
||||
|
||||
test('#44 prep scout: held pieces are units on the hand row; deploying moves them onto the field', () => {
|
||||
const S = 's_spec';
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 44, fake: true, spectators: [S] }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m;
|
||||
assert.equal(m.phase, PHASE.PREP);
|
||||
const b = h.ps('p_1');
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.ready', ready: false }), { ok: true });
|
||||
for (const p of [...b.board.values(), ...b.hand.filter(Boolean), ...b.temp.filter(Boolean)]) {
|
||||
if (p.kind === 'chess') b.returnCopies(p);
|
||||
}
|
||||
b.board.clear();
|
||||
b.hand.fill(null);
|
||||
b.temp.fill(null);
|
||||
b.recompute();
|
||||
const id = chessOfTier(1, MELEE).find((x) => m.pool.has(x));
|
||||
const piece = give(m, b, id); // into the hand
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||||
assert.deepEqual(m.handle(S, { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||||
|
||||
for (const pid of ['p_0', S]) {
|
||||
const first = h.lastTo(pid, 'm.field');
|
||||
assert.equal(first.prep, true);
|
||||
const u = first.units.find((x) => x.uid === piece.uid);
|
||||
assert.ok(u, `${pid}: the held operator is a unit of the scout`);
|
||||
assert.deepEqual([u.x, u.y], [0, GEO.HAND_ROW], `${pid}: standing on the first hand slot`);
|
||||
assert.equal(u.kind, 'op');
|
||||
assert.equal(u.items, undefined, 'no items equipped yet');
|
||||
}
|
||||
assert.deepEqual(h.lastTo('p_0', 'm.field').effects, h.ps('p_1').effectsView(), 'the scouted effects column rides the meta');
|
||||
|
||||
// deploying it is a hand AND a board change: one new push with the piece on the field and off the hand row
|
||||
const [r, c] = legalTileFor(m, b, id);
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.move', uid: piece.uid, to: { area: 'board', row: r, col: c }, dir: 'LEFT' }), { ok: true });
|
||||
const last = h.lastTo('p_0', 'm.field');
|
||||
const u = last.units.find((x) => x.uid === piece.uid);
|
||||
assert.deepEqual([u.x, u.y], [c, r], 'deployed onto the watched board');
|
||||
assert.equal(last.units.some((x) => x.uid === piece.uid && x.y === GEO.HAND_ROW), false, 'and gone from the hand row');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#44 prep scout: a held item is a kind-item unit on the hand row; equipped items ride their operator', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 46, fake: true }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m;
|
||||
const b = h.ps('p_1');
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.ready', ready: false }), { ok: true });
|
||||
for (const p of [...b.board.values(), ...b.hand.filter(Boolean), ...b.temp.filter(Boolean)]) {
|
||||
if (p.kind === 'chess') b.returnCopies(p);
|
||||
}
|
||||
b.board.clear();
|
||||
b.hand.fill(null);
|
||||
b.temp.fill(null);
|
||||
b.recompute();
|
||||
const id = chessOfTier(1, MELEE).find((x) => m.pool.has(x));
|
||||
const op = give(m, b, id);
|
||||
const item = giveItem(m, b, 'chess_item_1_01_e_a'); // 维式重锤, into the hand
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||||
const first = h.lastTo('p_0', 'm.field');
|
||||
const iu = first.units.find((x) => x.uid === item.uid);
|
||||
assert.ok(iu, 'the held item is a unit of the scout');
|
||||
assert.equal(iu.kind, 'item');
|
||||
assert.equal(iu.y, GEO.HAND_ROW);
|
||||
// equip the item onto the operator (still in the hand): the operator's unit carries the item id
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.equip', itemUid: item.uid, targetUid: op.uid }), { ok: true });
|
||||
const after = h.lastTo('p_0', 'm.field');
|
||||
assert.equal(after.units.some((x) => x.uid === item.uid), false, 'the equipped item left the hand row');
|
||||
const ou = after.units.find((x) => x.uid === op.uid);
|
||||
assert.deepEqual(ou.items, ['chess_item_1_01_e_a'], 'the operator scouts with its equipment');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test("PR #129 review: moving a hand piece to another slot is a scout change (x follows the slot), and the temp row rides along", () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 47, fake: true }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m;
|
||||
const b = h.ps('p_1');
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.ready', ready: false }), { ok: true });
|
||||
for (const p of [...b.board.values(), ...b.hand.filter(Boolean), ...b.temp.filter(Boolean)]) {
|
||||
if (p.kind === 'chess') b.returnCopies(p);
|
||||
}
|
||||
b.board.clear();
|
||||
b.hand.fill(null);
|
||||
b.temp.fill(null);
|
||||
b.recompute();
|
||||
const id = chessOfTier(1, MELEE).find((x) => m.pool.has(x));
|
||||
const a = give(m, b, id, 'hand', 0);
|
||||
const c = give(m, b, id, 'temp', 0); // 临时整备区: PRTS 帮助把它算进手牌区,一起侦察
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||||
|
||||
const first = h.lastTo('p_0', 'm.field');
|
||||
const tu = first.units.find((x) => x.uid === c.uid);
|
||||
assert.deepEqual([tu.x, tu.y], [GEO.TEMP_C0, GEO.TEMP_ROW], 'the temp piece scouts on the temp row (first slot, col 4)');
|
||||
assert.equal(first.units.find((x) => x.uid === a.uid).x, 0);
|
||||
|
||||
// a hand piece moved to another slot keeps uid and id — the slot must be in the signature for the push to fire
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.move', uid: a.uid, to: { area: 'hand', idx: 3 } }), { ok: true });
|
||||
const last = h.lastTo('p_0', 'm.field');
|
||||
assert.ok(last, 'a new scout push arrived');
|
||||
assert.equal(last.units.find((x) => x.uid === a.uid).x, 3, 'the piece stands on its new hand slot');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#44 prep scout: an empty hand adds no units and never an m.private to a spectator seat', () => {
|
||||
const S = 's_spec';
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 45, fake: true, spectators: [S] }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m;
|
||||
const b = h.ps('p_1');
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.ready', ready: false }), { ok: true });
|
||||
for (const p of [...b.board.values(), ...b.hand.filter(Boolean), ...b.temp.filter(Boolean)]) {
|
||||
if (p.kind === 'chess') b.returnCopies(p);
|
||||
}
|
||||
b.board.clear();
|
||||
b.hand.fill(null);
|
||||
b.temp.fill(null);
|
||||
b.recompute();
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||||
assert.deepEqual(m.handle(S, { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||||
const first = h.lastTo('p_0', 'm.field');
|
||||
assert.equal(first.units.length, 0, 'nothing held, nothing deployed: no units');
|
||||
assert.equal(first.bench, undefined, 'no separate bench field (the hand rides units)');
|
||||
assert.equal(h.allTo(S, 'm.private').length, 0, 'still no private state to a scout');
|
||||
m.dispose();
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// GitHub #87: watching another board during prep follows its moves. A spectator uses the same prep scout.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { PHASE } from '../../shared/constants.js';
|
||||
import { GEO, PHASE } from '../../shared/constants.js';
|
||||
import { makeMatch, give, legalTileFor, chessOfTier } from './harness.js';
|
||||
|
||||
const MELEE = (c) => c.position === 'MELEE' && c.profession === 'TANK';
|
||||
@@ -31,7 +31,9 @@ test('#87 prep watch: seat B moves during prep and seat A\'s view of B changes b
|
||||
const first = h.lastTo(pid, 'm.field');
|
||||
assert.equal(first.fieldId, 'n:p_1');
|
||||
assert.equal(first.prep, true);
|
||||
assert.equal(first.units.some((u) => u.uid === piece.uid), false, `${pid}: the piece is still in the hand`);
|
||||
const hu = first.units.find((u) => u.uid === piece.uid);
|
||||
assert.ok(hu, `${pid}: the piece is held — it scouts as a unit on the hand row`);
|
||||
assert.equal(hu.y, GEO.HAND_ROW, `${pid}: on the bench (row 7), not on the field`);
|
||||
}
|
||||
assert.equal(h.allTo(S, 'm.private').length, 0);
|
||||
const beforeA = h.allTo('p_0', 'm.field').length;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// 点击 / 观战队友单位时详情卡显示装备(游玩记录 #2:「观战队友时看不到干员的装备」)。DESIGN §16
|
||||
// 承诺 "a teammate's unit shows its owner's loadout in the detail card":服务器把 an ally operator 的
|
||||
// 装备 id 放进战斗快照与侦察 meta 的 UnitInfo.items(server/sim/snapshot.js、Match.prepFieldMeta),
|
||||
// detailPanel 的 unit 分支也从 `u.items` 取 `unitItems` —— 但 renderInfo(public/js/render/app.js)
|
||||
// 构建 info 白名单时把 items 丢了,队友单位点开的卡永远没有 装备 区,变形同构体的「同构」pairing
|
||||
// chips 对队友单位同样失效。renderInfo 在 app.js 里(模块顶层有 canvas 副作用,node 无法 import),
|
||||
// 按docs-consistency / feedback1d 的惯例对源码断言;消费端行为已由 morph-bonds.test.js 锁住,
|
||||
// 这里再走一遍 unit → resolveDetail → ChessDetail 的完整用户可见结果。
|
||||
import { test } 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)), '../..');
|
||||
const load = (f) => JSON.parse(readFileSync(path.join(ROOT, 'data', f), 'utf8'));
|
||||
const chess = load('chess.json');
|
||||
const WEARER = 'chess_char_1_01_a'; // 隐现
|
||||
const ISO = 'chess_item_6_09_e_a'; // 变形同构体
|
||||
const HAMMER = 'chess_item_1_01_e_a'; // 维式重锤 → 维多利亚
|
||||
|
||||
// ---- renderInfo: the snapshot UnitInfo reaches the client info object ---------------------------------------------
|
||||
|
||||
test('renderInfo passes UnitInfo.items through (the teammate-unit equipment into the detail card)', () => {
|
||||
const app = readFileSync(path.join(ROOT, 'public/js/render/app.js'), 'utf8');
|
||||
const start = app.indexOf('export function renderInfo(');
|
||||
assert.ok(start > 0, 'renderInfo exists');
|
||||
const body = app.slice(start, app.indexOf('\n}', start));
|
||||
assert.match(
|
||||
body,
|
||||
/items: Array\.isArray\(u\.items\) \? u\.items\.filter\(\(x\) => typeof x === 'string'\) : undefined/,
|
||||
'the equipped item ids must survive the info whitelist (DESIGN §16: a teammate\'s unit shows its owner\'s loadout)',
|
||||
);
|
||||
});
|
||||
|
||||
// ---- the user-visible end of the chain: a teammate unit's card with its equipment ----------------------------------
|
||||
|
||||
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', 'garrisons');
|
||||
const { ChessDetail, resolveDetail } = await import('../../public/js/ui/detailPanel.js');
|
||||
const { BondChips } = await import('../../public/js/ui/detailPanel.js');
|
||||
|
||||
/** Every vnode of a preact tree (htm output), depth first. */
|
||||
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);
|
||||
|
||||
test('a teammate unit carrying items opens with a read-only 装备 section and the worn 同构 pairing', () => {
|
||||
// the info object exactly as the battle view holds it after renderInfo (unit: info in pieceClick)
|
||||
const unit = { id: 7, uid: null, kind: 'op', side: 'ally', ownerId: 'p2', defId: WEARER, items: [ISO, HAMMER] };
|
||||
const d = resolveDetail({ kind: 'unit', unit }, new Map());
|
||||
assert.equal(d.type, 'chess');
|
||||
assert.deepEqual(d.unitItems, [ISO, HAMMER], 'no own piece for a teammate: the unit\'s items become unitItems');
|
||||
|
||||
const mine = [{ bondId: 'victoriaShip', count: 3, active: true, tier: 1, layers: 0, thresholds: [3, 6] }];
|
||||
const blocks = ChessDetail({ chess: d.chess, piece: null, editable: false, bonds: mine, loadout: null, unitItems: d.unitItems });
|
||||
const equip = blocks.find((b) => b?.key === 'equip');
|
||||
assert.ok(equip, 'the 装备 section renders without an own piece');
|
||||
assert.deepEqual([...walk(equip)].filter((x) => x.props?.itemId).map((x) => x.props.itemId), [ISO, HAMMER]);
|
||||
const chips = [...walk(blocks)].find((x) => x.type === BondChips);
|
||||
assert.ok(chips, 'the card has bond chips');
|
||||
const vic = [...walk(BondChips(chips.props))].find((x) => hasClass(x, 'dbond') && x.props['data-bond'] === 'victoriaShip');
|
||||
assert.ok(vic && hasClass(vic, 'is-granted'), '变形同构体 marks the wearer as a 维多利亚 member on a teammate\'s card too');
|
||||
});
|
||||
|
||||
test('a unit without items (enemies, summons, plain allies) still opens without a 装备 section', () => {
|
||||
const d = resolveDetail({ kind: 'unit', unit: { id: 8, kind: 'op', side: 'ally', ownerId: 'p2', defId: WEARER } }, new Map());
|
||||
assert.equal(d.unitItems, null, 'renderInfo leaves items undefined; resolveDetail hands null on');
|
||||
const mine = [{ bondId: 'victoriaShip', count: 3, active: true, tier: 1, layers: 0, thresholds: [3, 6] }];
|
||||
const blocks = ChessDetail({ chess: d.chess, piece: null, editable: false, bonds: mine, loadout: null, unitItems: d.unitItems });
|
||||
assert.equal(blocks.find((b) => b?.key === 'equip'), undefined, 'no empty 装备 section');
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// 观战整备区的客户端(游玩记录 #2 item 1):被侦察玩家的手牌是 m.field(prep:true).units 里手牌行
|
||||
// (row 7)上的单位——干员站立、道具 kind 'item'(浮牌),和自己整备区同一渲染路径;观战棋盘用
|
||||
// prep 相机(收起商店形态,bench 行进画面)。battleView 为 kind 'item' 建 ItemView(图标/颜色客户端
|
||||
// 解析,同 pieceInfo),prep 面上给带装备的队友干员挂 item pips。服务器侧:test/match/prep-bench.test.js。
|
||||
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)), '../..');
|
||||
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
||||
|
||||
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 { resolveDetail } = await import('../../public/js/ui/detailPanel.js');
|
||||
|
||||
describe('the scouted prep board renders the hand like the own bench', () => {
|
||||
test('the game screen frames it with the prep camera in its shop-folded form (source)', () => {
|
||||
const game = read('public/js/screens/game.js');
|
||||
assert.match(game, /field\.prep \? 'prep' : \(field\.kind === 'hidden' \? 'boss' : field\.kind \|\| 'normal'\)/);
|
||||
assert.match(game, /field\.prep \? \{ shop: false \} : \{\}/);
|
||||
assert.match(game, /const scoutPid = watchingOther && field\?\.prep/);
|
||||
assert.ok(!game.includes('ScoutedBench'), 'the floating strip is gone — the hand rides the board');
|
||||
assert.ok(!read('public/css/screens/game.css').includes('.sbench'), 'its styles too');
|
||||
});
|
||||
|
||||
test('battleView builds an ItemView for a hand item and item pips for a teammate operator (source)', () => {
|
||||
const app = read('public/js/render/app.js');
|
||||
assert.match(app, /info\.kind === 'item' \? new ItemView\(ctx, scoutItemInfo\(info\)\)/);
|
||||
assert.match(app, /battleMeta\?\.prep && Array\.isArray\(info\.items\) && info\.items\.length/);
|
||||
assert.match(app, /function scoutItemInfo\(info\)/);
|
||||
});
|
||||
|
||||
test('tapping a held item on the scouted board opens its card (resolveDetail unit → item)', () => {
|
||||
const IT = 'chess_item_1_01_e_a';
|
||||
const d = resolveDetail({ kind: 'unit', unit: { id: 3, kind: 'item', side: 'ally', ownerId: 'p2', defId: IT } }, new Map());
|
||||
assert.equal(d.type, 'item');
|
||||
assert.equal(d.item.itemId || d.item.id, IT);
|
||||
assert.equal(resolveDetail({ kind: 'unit', unit: { id: 4, kind: 'item', side: 'ally', ownerId: 'p2', defId: 'nope' } }, new Map()), null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// 观战对象的效果列(游玩记录 #2 追加反馈:「观战队友时右侧的 effect 列也改成观战对象的」):
|
||||
// * 整备期侦察:m.field 直接带被侦察玩家的 effectsView(server Match.prepFieldMeta,现成显示形状);
|
||||
// * 战斗观战(显示复刻):runner 从 spec 的原始 playerEffects 客户端还原显示条目
|
||||
// (battle/observe.js spectateEffects — 策略效果经 bands.json 的 effectId,其余经 effects.json);
|
||||
// * 联防 / boss 对半场与服务器运行场的 meta 无 effects —— 列留空,不再显示自己的。
|
||||
// 服务器侧:test/match/prep-bench.test.js。
|
||||
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)), '../..');
|
||||
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
||||
|
||||
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('effects', 'bands', 'assets');
|
||||
const { spectateEffects } = await import('../../public/js/battle/observe.js');
|
||||
|
||||
describe('spectateEffects — the watched battle\'s effects column from the spec', () => {
|
||||
const spec = { players: [
|
||||
{ playerId: 'p1', playerEffects: [
|
||||
{ id: 'aceffect_band_21', source: 'band' }, // 华法琳【重点监护】
|
||||
{ id: 'aceffect_char_1', source: 'choice', counter: 2 },
|
||||
{ id: 'effect_not_in_data', source: 'team' },
|
||||
] },
|
||||
{ playerId: 'p2', playerEffects: [] },
|
||||
] };
|
||||
|
||||
test('the single member\'s playerEffects resolved: band via bands.json, the rest via effects.json', () => {
|
||||
const out = spectateEffects(spec, ['p1']);
|
||||
assert.ok(Array.isArray(out) && out.length === 3);
|
||||
assert.deepEqual([out[0].name, out[0].iconKind, out[0].iconId], ['重点监护', 'band', 'icon_bldsk']);
|
||||
assert.match(out[0].desc, /开始作战时/);
|
||||
assert.deepEqual([out[1].name, out[1].iconKind, out[1].counter], ['未精英化', 'choice', 2]);
|
||||
assert.equal(out[2].name, 'effect_not_in_data', 'an unknown id falls back to the id');
|
||||
assert.equal(out[2].iconKind, 'team');
|
||||
});
|
||||
|
||||
test('undefined where whose column would be ambiguous or missing', () => {
|
||||
assert.equal(spectateEffects(spec, ['p1', 'p2']), undefined, '联防 / boss pairs');
|
||||
assert.equal(spectateEffects({ players: [{ playerId: 'p1' }] }, ['p1']), undefined, 'no playerEffects on the spec');
|
||||
assert.equal(spectateEffects(null, ['p1']), undefined);
|
||||
assert.deepEqual(spectateEffects(spec, ['p2']), [], 'an empty list stays a list (empty column)');
|
||||
});
|
||||
|
||||
test('wiring: the runner derives it for replica fields, the game screen prefers the field\'s list (source)', () => {
|
||||
assert.match(read('public/js/battle/runner.js'), /effects: spectateEffects\(e\.spec, e\.members\)/);
|
||||
assert.match(read('public/js/battle/runner.js'), /import \{ spectateEffects \} from '\.\/observe\.js'/);
|
||||
assert.match(read('public/js/screens/game.js'), /watchingOther && field \? \(field\.effects \?\? null\) : priv\?\.effects/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// 队友的策略(游玩记录 #2 item 2:「观战队友时看不到队友的策略详情」):观战队友的整备区棋盘时,
|
||||
// 「本局信息」抽屉的策略块从自己的 priv.bandId 切换成被观看玩家的 m.public players[].bandId,标签
|
||||
// 从「我的策略」变为「XX 的策略」(ui/enemyDrawer.js InfoTab 的 bandId / bandOwner 覆盖;game.js
|
||||
// scoutPid / scoutBandId)。不进详情卡——策略和自己的查看位置一致。
|
||||
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)), '../..');
|
||||
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
||||
|
||||
import { ownerBandId } from '../../public/js/ui/gameLogic.js';
|
||||
|
||||
describe('ownerBandId — the picked 策略 of a player', () => {
|
||||
const pub = { players: [
|
||||
{ playerId: 'p1', name: '博士一', bandId: 'band_bldsk' },
|
||||
{ playerId: 'p2', name: '博士二' }, // not picked yet (draft running)
|
||||
] };
|
||||
test('the bandId of the owner, null for anyone without one or an unknown id', () => {
|
||||
assert.equal(ownerBandId(pub, 'p1'), 'band_bldsk');
|
||||
assert.equal(ownerBandId(pub, 'p2'), null);
|
||||
assert.equal(ownerBandId(pub, 'pX'), null);
|
||||
assert.equal(ownerBandId(null, 'p1'), null);
|
||||
assert.equal(ownerBandId({ players: [] }, 'p1'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the drawer shows the watched player\'s 策略 while scouting', () => {
|
||||
test('game.js derives the scouted player and hands bandId / bandOwner to the drawer (source)', () => {
|
||||
const game = read('public/js/screens/game.js');
|
||||
assert.match(game, /const scoutPid = watchingOther && field\?\.prep && typeof field\.fieldId === 'string' && field\.fieldId\.startsWith\('n:'\) \? field\.fieldId\.slice\(2\) : null/);
|
||||
assert.match(game, /const scoutBandId = scoutPid \? ownerBandId\(pub, scoutPid\) : null/);
|
||||
assert.match(game, /bandId=\$\{scoutBandId\} bandOwner=\$\{scoutBandOwner\}/);
|
||||
});
|
||||
|
||||
test('InfoTab overrides the band and labels it with the owner (source); the detail card has no 策略 section', () => {
|
||||
const drawer = read('public/js/ui/enemyDrawer.js');
|
||||
assert.match(drawer, /const band = \(bandId \|\| priv\?\.bandId\) \? data\.lookup\('bands', bandId \|\| priv\.bandId\) : null/);
|
||||
assert.match(drawer, /bandOwner \? `\$\{bandOwner\} 的策略` : '我的策略'/);
|
||||
const panel = read('public/js/ui/detailPanel.js');
|
||||
assert.doesNotMatch(panel, /key="band"/, 'no 策略 section on the detail card');
|
||||
assert.doesNotMatch(panel, /'band'/, "CHESS_SECTIONS has no 'band' block");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user