feedback1 gaps: the strategy draft marks a band built around a switched-off bond 本局禁用

A band whose bondIds meet the mode's inactive bonds (gameLogic bandOffBonds,
the same field and rule as the bot) gets a 本局禁用 tag on its card and, in
the detail pane, "本局禁用【拉特兰】盟约,此策略效果可能无法发挥" with the bond
struck through. It stays selectable. In 标准 that is 潘格尼尼, 克莱门莎 and
玛恩纳; no band is marked in the other modes. A test pins the client mark to
the server's exclusion in every mode; the headless check covers the draft
at 1920x1080 and 844x390. DESIGN §21.26, META and the CHANGELOG say so.
This commit is contained in:
sganggs committed 2026-10-03 15:56:50 +08:00
1 parent 1f29b8a3d0
commit da7960261a
9 files changed
+177 -12

No files matched your search

+57 -2
View File
@@ -8,8 +8,10 @@
// At 1920×1080 and on phones (844×390, 756×366, touch): the 变形同构体 card from the shop lists its 14 pairings (5 marked 本局禁用),
// the wearer's card highlights 维多利亚 (生效中), and the popup of a core bond holding 调和's +1 says 在场 n(含调和 +1) with the
// 调和 row naming 缪尔赛思 — every line inside its panel (the detail card without sideways scrolling), text ≥ 9 px on the
// phones. Screenshots:
// fb1-gaps-{morph-shop,morph-wearer,harmony}-<w>x<h>.png. Unit counterparts: test/ui/feedback1-gaps.test.js.
// phones. The strategy draft of a 标准 match (?phase=BAND_DRAFT&variant=funny, 1920×1080 and 844×390): 潘格尼尼, 克莱门莎 and
// 玛恩纳 — and no other — read 本局禁用 on their card, the detail pane says "本局禁用【拉特兰】盟约,此策略效果可能无法发挥", and
// 潘格尼尼 can still be picked. Screenshots:
// fb1-gaps-{morph-shop,morph-wearer,harmony,draft}-<w>x<h>.png. Unit counterparts: test/ui/feedback1-gaps.test.js.
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdirSync } from 'node:fs';
@@ -129,4 +131,57 @@ describe('0.1.1 gaps: 变形同构体 pairings and 调和\'s +1 (mock harness, h
await page.close();
});
}
// the strategy draft of a 标准 match (?phase=BAND_DRAFT&variant=funny; my turn): the strategies built around a bond 标准
// switches off read 本局禁用 on their card and in the detail pane — and can still be picked
for (const [name, w, h, touch] of [['1920x1080', 1920, 1080, false], ['844x390', 844, 390, true]]) {
test(`${name}: the 标准 strategy draft marks 潘格尼尼 / 克莱门莎 / 玛恩纳 本局禁用, and 潘格尼尼 can still be picked`, { timeout: 120000 }, async () => {
const page = await browser.newPage();
await page.setViewport({ width: w, height: h, deviceScaleFactor: 1, isMobile: touch, hasTouch: touch });
const problems = [];
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
await page.goto(`${base}/dev/game-mock.html?shot=1&phase=BAND_DRAFT&variant=funny`, { waitUntil: 'networkidle0' });
await page.waitForSelector('.draft-grid .dband[data-band]', { timeout: 20000 });
await sleep(500);
const tap = async (sel) => { const el = await page.$(sel); assert.ok(el, sel); await el.scrollIntoView(); if (touch) await el.tap(); else await el.click(); await sleep(300); };
const cards = await page.$$eval('.draft-grid .dband[data-band]', (els) => els.map((c) => {
const t = c.querySelector('.dband__off');
const r = c.getBoundingClientRect();
const q = t ? t.getBoundingClientRect() : null;
return { id: c.getAttribute('data-band'), off: c.classList.contains('is-off'), tag: t ? t.textContent : null, title: c.title || null,
font: t ? parseFloat(getComputedStyle(t).fontSize) : null, inside: !q || (q.left >= r.left - 0.5 && q.right <= r.right + 0.5 && q.bottom <= r.bottom + 0.5) };
}));
assert.ok(cards.length >= 30, `${cards.length} strategies on offer`);
assert.deepEqual(cards.filter((c) => c.tag).map((c) => c.id), ['band_paganini', 'band_clementia', 'band_mlynar'], 'only the three');
assert.ok(cards.filter((c) => c.tag).every((c) => c.off && c.tag === '本局禁用' && c.inside && c.font >= (touch ? 8 : 11.5)), JSON.stringify(cards.filter((c) => c.tag)));
assert.equal(cards.find((c) => c.id === 'band_paganini').title, '本局禁用【拉特兰】盟约,此策略效果可能无法发挥');
await page.screenshot({ path: path.join(OUT, `fb1-gaps-draft-${name}.png`) });
// its detail pane: the note, inside the pane; 确认选择 stays enabled (information only)
await tap('.draft-grid .dband[data-band="band_paganini"]');
await page.waitForSelector('.draft-detail .draft-detail__off', { timeout: 5000 });
const note = await page.evaluate(() => {
const n = document.querySelector('.draft-detail .draft-detail__off');
const p = document.querySelector('.draft-detail').getBoundingClientRect();
const r = n.getBoundingClientRect();
const btn = [...document.querySelectorAll('.draft-detail__btns button')].pop();
return { text: n.textContent.trim(), struck: n.querySelector('.draft-detail__offname')?.textContent, font: parseFloat(getComputedStyle(n).fontSize),
inside: r.left >= p.left - 0.5 && r.right <= p.right + 0.5, confirm: btn ? { text: btn.textContent.trim(), disabled: btn.disabled } : null };
});
assert.equal(note.text, '本局禁用【拉特兰】盟约,此策略效果可能无法发挥');
assert.equal(note.struck, '拉特兰');
assert.ok(note.inside && note.font >= (touch ? 9 : 13.5), JSON.stringify(note));
assert.deepEqual(note.confirm, { text: '确认选择', disabled: false }, 'still selectable');
await page.screenshot({ path: path.join(OUT, `fb1-gaps-draft-${name}-detail.png`) });
// a strategy tied to no switched-off bond: no note
await tap('.draft-grid .dband[data-band="band_bldsk"]');
assert.equal(await page.$('.draft-detail .draft-detail__off'), null);
// picking 潘格尼尼 works like any strategy
await tap('.draft-grid .dband[data-band="band_paganini"]');
await tap('.draft-detail__btns button:last-child');
await page.waitForFunction(() => /已选择「潘格尼尼」/.test(document.querySelector('.draft-detail__status')?.textContent || ''), { timeout: 5000 });
assert.deepEqual(problems, []);
await page.close();
});
}
});
+57 -1
View File
@@ -8,6 +8,10 @@
// 调和's +1 (`harmony` in m.private bonds / m.public players[].bonds — test/match/feedback1-gaps.test.js); the popup
// says 在场 n(含调和 +1) and a 调和 row heads the member list naming the 调和 operators on that board — own and a
// teammate's (ui/watchBonds.js popupView over the scouted field), the chip's and the strip's titles too.
// 4. The strategy draft marks a strategy built around a bond the mode switches off 本局禁用 (bands.json `bondIds`, built
// by shared/bandBonds.js — the field the bot reads too: gameLogic bandOffBonds = GameData.bandBondIds ∩ the mode's
// inactive bonds), on its card and in the detail pane (screens/bandDraft.js BandOffTag / BandOffNote); still selectable.
// (3, the bot's pick, is server-side: test/match/feedback1-gaps.test.js.)
// On screen (headless Chrome, opt-in): test/ui/feedback1-gaps.e2e.test.js.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
@@ -29,7 +33,7 @@ globalThis.fetch = async (url) => {
}
};
const { morphPairings, modeOffBonds, grantedBonds, harmonyMembers, HARMONY_BOND } = await import('../../public/js/ui/gameLogic.js');
const { morphPairings, modeOffBonds, grantedBonds, harmonyMembers, HARMONY_BOND, bandOffBonds, bandOffLine } = await import('../../public/js/ui/gameLogic.js');
const { data, getMode } = await import('../../public/js/data.js');
await data.loadAll('bonds', 'chess', 'items', 'assets', 'garrisons', 'config');
const { ItemDetail, ChessDetail, MorphPairings, MorphGrantLine, BondChips } = await import('../../public/js/ui/detailPanel.js');
@@ -308,3 +312,55 @@ describe('§21.26 2 — 调和\'s +1 in the bond popup', () => {
assert.equal(noPlus.props.title, '炎:在场 3/6,已激活 1 阶');
});
});
describe('§21.26 4 — the strategy draft marks a strategy built around a bond the mode switches off (本局禁用)', () => {
const bands = load('bands.json');
const modeIds = Object.keys(config.modes);
const markedIn = (modeId) => Object.values(bands).map((b) => [b.bandId, bandOffBonds(b, modeOffBonds(getMode(modeId)))]).filter(([, ids]) => ids.length);
test('标准: exactly 潘格尼尼 (拉特兰), 克莱门莎 (阿戈尔) and 玛恩纳 (卡西米尔); 险境 / 绝境 / 终极: none; a band tied to no bond: never', () => {
for (const id of ['mode_single_funny', 'mode_multi_funny']) {
assert.deepEqual(markedIn(id), [['band_paganini', ['lateranoShip']], ['band_clementia', ['egirShip']], ['band_mlynar', ['kazimierzShip']]], id);
}
for (const id of ['mode_single_normal', 'mode_multi_normal', 'mode_single_hard', 'mode_multi_hard', 'mode_single_abyss', 'mode_multi_abyss']) assert.deepEqual(markedIn(id), [], id);
for (const b of Object.values(bands).filter((x) => !x.bondIds.length)) {
for (const id of modeIds) assert.deepEqual(bandOffBonds(b, modeOffBonds(getMode(id))), [], `${b.name} in ${id}`);
}
// a tied band whose bond stays on (杜遥夜 炎) is not marked in 标准 either
assert.deepEqual(bandOffBonds(bands.band_duyaoy, modeOffBonds(getMode('mode_multi_funny'))), []);
assert.deepEqual(bandOffBonds(bands.band_paganini, null), []);
assert.deepEqual(bandOffBonds({ bandId: 'old' }, modeOffBonds(getMode('mode_multi_funny'))), [], 'data without bondIds: no mark');
});
test('one source with the server: the bot\'s exclusion (GameData.bandBondIds ∩ modeInactiveBonds) is the draft\'s mark, in every mode', async () => {
const { GameData } = await import('../../server/match/gamedata.js');
const { getData } = await import('../../server/data.js');
const DATA = getData({ log: { warn() {}, error() {}, info() {} } });
for (const modeId of modeIds) {
const gd = new GameData(DATA, modeId);
for (const id of Object.keys(bands)) {
assert.deepEqual([...gd.bandBondIds(id)], bands[id].bondIds, `${id}: the same field`);
const server = gd.bandBondIds(id).filter((b) => gd.modeInactiveBonds.has(b));
assert.deepEqual(server, bandOffBonds(bands[id], modeOffBonds(getMode(modeId))), `${modeId} ${id}`);
}
}
});
test('the card\'s tag and the detail pane\'s note: "本局禁用【拉特兰】盟约,此策略效果可能无法发挥", the bond struck through; nothing without one', async () => {
const { BandOffTag, BandOffNote } = await import('../../public/js/screens/bandDraft.js');
assert.equal(bandOffLine(['拉特兰']), '本局禁用【拉特兰】盟约,此策略效果可能无法发挥');
assert.equal(bandOffLine(['拉特兰', '阿戈尔']), '本局禁用【拉特兰】【阿戈尔】盟约,此策略效果可能无法发挥');
assert.equal(bandOffLine([]), '');
const tag = BandOffTag({ names: ['拉特兰'] });
assert.ok(hasClass(tag, 'dband__off'));
assert.equal(textOf(tag), '本局禁用');
assert.equal(tag.props.title, '本局禁用【拉特兰】盟约,此策略效果可能无法发挥');
const note = BandOffNote({ names: ['拉特兰'] });
assert.ok(hasClass(note, 'draft-detail__off'));
assert.equal(note.props.role, 'note');
assert.equal(textOf(note), '本局禁用【拉特兰】盟约,此策略效果可能无法发挥');
assert.deepEqual(byClass(note, 'draft-detail__offname').map(textOf), ['拉特兰'], 'the bond name struck through (§21.7)');
assert.equal(BandOffTag({ names: [] }), null);
assert.equal(BandOffNote({ names: [] }), null);
});
});