From da7960261a2de8eba1e6fb74fc67ceb63d354ffc Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 15:56:50 +0800 Subject: [PATCH] =?UTF-8?q?feedback1=20gaps:=20the=20strategy=20draft=20ma?= =?UTF-8?q?rks=20a=20band=20built=20around=20a=20switched-off=20bond=20?= =?UTF-8?q?=E6=9C=AC=E5=B1=80=E7=A6=81=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 1 + docs/DESIGN.md | 5 ++- docs/META.md | 7 ++-- public/css/screens/draft.css | 8 ++++ public/js/screens/bandDraft.js | 25 +++++++++++-- public/js/ui/gameLogic.js | 21 +++++++++++ test/match/feedback1-gaps.test.js | 5 ++- test/ui/feedback1-gaps.e2e.test.js | 59 +++++++++++++++++++++++++++++- test/ui/feedback1-gaps.test.js | 58 ++++++++++++++++++++++++++++- 9 files changed, 177 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aefbb802..f2c37665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ - 灼热 / 炽焰源石虫:整合包附带官方模型;从源码运行且没有提取本机客户端素材时,显示染成橙色 / 红橙色的源石虫。 - 变形同构体的详情卡列出天赋栏里的对应关系:每个盟约由哪些装备搭配,本局禁用的盟约会标出;装在干员身上时高亮正在生效的那一组,各盟约装备的详情也写明与变形同构体一同装备时携带者视为哪个盟约的成员。 - 调和让核心盟约人数 +1 时,盟约详情写明「含调和 +1」,成员列表上方列出带来这 1 人的调和干员(如缪尔赛思);查看队友的盟约时也一样。 +- 选择策略时,围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)标出「本局禁用」并在详情里说明,仍可选择。 ### 人机 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a53536e5..a0bb7300 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1407,11 +1407,12 @@ Not done (noted, no primary source or out of scope): whether 华法琳 S2's 流 ### 21.26 Gaps found while triaging GitHub issues #1 / #8 (v0.1.1) -Where: `ui/gameLogic.js morphPairings / HARMONY_BOND / harmonyMembers`, `ui/detailPanel.js MorphPairings / MorphGrantLine / ItemDetail / ChessDetail / BondChips`, `ui/bondStrip.js BondPopup / BondStrip`, `match/bondsMeta.js computeBonds / bondList`, `match/gamedata.js bandBondIds`, `match/bot.js botPickBand`, `tools/botbench.mjs` (`--band`), `match/fields.js layerAllowanceOf`, `public/dev/game-mock.js` (variants `funny` / `morph` / `harmony`). Three gaps the user decided to close in 0.1.1 (no rule changes), and a false rejection of honest client results found on the way. +Where: `ui/gameLogic.js morphPairings / HARMONY_BOND / harmonyMembers`, `ui/detailPanel.js MorphPairings / MorphGrantLine / ItemDetail / ChessDetail / BondChips`, `ui/bondStrip.js BondPopup / BondStrip`, `match/bondsMeta.js computeBonds / bondList`, `shared/bandBonds.js` (→ bands.json `bondIds`), `match/gamedata.js bandBondIds`, `match/bot.js botPickBand`, `screens/bandDraft.js BandOffTag / BandOffNote`, `ui/gameLogic.js bandOffBonds`, `tools/botbench.mjs` (`--band`), `match/fields.js layerAllowanceOf`, `public/dev/game-mock.js` (variants `funny` / `morph` / `harmony`). Three gaps the user decided to close in 0.1.1 (no rule changes), and a false rejection of honest client results found on the way. - **变形同构体 shows its pairings** (issue #1: players read the "转职球" as doing nothing). Its text says "具体对应关系可在模拟中查看本装备天赋栏", and no screen showed that 天赋栏. **Now** the item card (shop, reward, hand / temp: `ItemDetail`) has a 天赋 section: "搭配以下装备时,携带者视为对应盟约的成员:", then one line per bond in bonds.json order — 【bond】 and the items whose `giveBondId` is it (one name per item family, by tier): the 14 pairings of the official talent (character_table trap_1073_acarm073, "“炎国短刀”→【炎】盟约 …"; a test checks the two against each other), its "“维式重锤”系列装备" as the 5 hammers — 18 items, built from the same `giveBondId` the server counts with (`morphPairings`; `pieceBonds` / `grantedBonds`). A bond the mode never activates is struck through with 本局禁用 (`modeOffBonds`, §21.7; 标准: 拉特兰 阿戈尔 卡西米尔 奥术 突袭). On a wearer's card (its 装备 rows; also a teammate's unit and the card a 同构 row opens) the pairing it wears is highlighted — 生效中, or 已搭配 when that bond is off — and a wearer without one reads 暂未生效. A bond item's card and row say "与变形同构体一同装备时,携带者视为【X】成员" (`MorphGrantLine`). The 机密商店 cards have no detail view and keep the official text. Phones: the list keeps ≥ 9 px (px floors in `game-panels.css`). - **调和's +1 explained** (issue #1: "一些不考虑整备区的盟约计算人数时也会把整备区的干员算进去" — the +1 read as a miscount; §21.11 had left "调和's +1 raises 在场 for core bonds without a member row"). The rule is unchanged (§6.3; one +1 total, research 02 §2.1 [ASSUMED]). **Now** a bond state whose count holds the +1 carries `harmony: 1` (`computeBonds`) and both views send it — m.private `bonds[]` and m.public `players[].bonds[]` entries `{ …, harmony? }` (absent otherwise; not in the battle input, `bondSnapshot`) — so the client never re-derives the rule. The popup says 在场 n(含调和 +1) and a 调和 row heads its member list, naming the 调和 operators on that board (`harmonyMembers`: 缪尔赛思, Pith's 盟约·辅助干员; a tap opens the first one's card; with that board not on screen "调和已激活:核心盟约激活人数 +1") — for your own strip and a teammate's (§20.15: the entry from m.public, the operators from the field on screen). The strip disc's and the card chip's titles add (含调和 +1). -- **Bots skip strategies built around a switched-off bond**: in 标准 the bot took 潘格尼尼 (【定制铳械】 an elite <拉特兰>), 克莱门莎 (<阿戈尔>) or 玛恩纳 (<卡西米尔>) in 5 of 200 solo and 23 of 800 co-op picks (seeds 1–200). `GameData.bandBondIds` reads a band's tie from its own data — the bond names in <…> of its text (the official note "在部分干员缺席时体验可能不完整" among them; a bracketed item or enemy adds nothing) and the bond ids / bond pools of its blackboards (`bond`, `bond_id`; `pool` → choices.json `pools[pool].bond`, 潘格尼尼's pool_char_later) — nine bands are tied (杜遥夜 炎, 佩佩 / 娜仁图亚 萨尔贡, 哈洛德 维多利亚, 休露丝 谢拉格, 潘格尼尼 拉特兰, 克莱门莎 阿戈尔, 玛恩纳 卡西米尔, 贾维 叙拉古). `botPickBand` weighs a band tied to a bond of `gd.modeInactiveBonds` 0 — one rng draw per pick, the default band when every offered band is excluded: 0 of 200 and 0 of 800 in 标准, while the other modes keep exactly the earlier picks (绝境: 5 / 200, 23 / 800). A human may still pick those bands (open: a 本局禁用 mark on them in the strategy draft); `tools/botbench.mjs --band` still forces any band. +- **Bots skip strategies built around a switched-off bond**: in 标准 the bot took 潘格尼尼 (【定制铳械】 an elite <拉特兰>), 克莱门莎 (<阿戈尔>) or 玛恩纳 (<卡西米尔>) in 5 of 200 solo and 23 of 800 co-op picks (seeds 1–200). bands.json `bondIds` holds a band's tie, read from its own data at build time (`tools/build-data.mjs` through `shared/bandBonds.js`; `GameData.bandBondIds` and the client read the field) — the bond names in <…> of its text (the official note "在部分干员缺席时体验可能不完整" among them; a bracketed item or enemy adds nothing) and the bond ids / bond pools of its blackboards (`bond`, `bond_id`; `pool` → choices.json `pools[pool].bond`, 潘格尼尼's pool_char_later) — nine bands are tied (杜遥夜 炎, 佩佩 / 娜仁图亚 萨尔贡, 哈洛德 维多利亚, 休露丝 谢拉格, 潘格尼尼 拉特兰, 克莱门莎 阿戈尔, 玛恩纳 卡西米尔, 贾维 叙拉古). `botPickBand` weighs a band tied to a bond of `gd.modeInactiveBonds` 0 — one rng draw per pick, the default band when every offered band is excluded: 0 of 200 and 0 of 800 in 标准, while the other modes keep exactly the earlier picks (绝境: 5 / 200, 23 / 800). `tools/botbench.mjs --band` still forces any band. +- **The strategy draft marks them 本局禁用** for human players (the owner's follow-up): a band whose `bondIds` meet the mode's inactive bonds (`gameLogic.bandOffBonds`, the same field and rule as the bot — a test pins the two in every mode) carries a 本局禁用 tag on its card and, in the detail pane, "本局禁用【拉特兰】盟约,此策略效果可能无法发挥" with the bond struck through (§21.7's look); it stays selectable — the official game lets you pick it. - **Found on the way — the client-result layer bound** (`match/fields.js layerAllowanceOf / validateClientResult`): with the bots' new picks the full-match suite's coop/FUNNY/107 (4 humans on AI 托管) reached a 6-谢拉格 freeze board at R12 (初雪 + 银灰: garrison_28, 50 % +1 per freeze in range, no per-battle cap in the data; 凛御银灰 hands out a 60 % +2) whose honest +112 谢拉格 layers passed the flat 60 + 4·12 = 108, so the server rejected the client and re-simulated (SP_VERIFY=all agrees with the client on all 57 battles of that match). Uncapped traits are common (菲莱 / 百炼嘉维尔 per skill, 斯卡蒂 per 2 kills, 莫斯提马 per 6 bullets …) and capped ones reach 100–200 (史尔特尔, 荒芜拉普兰德), so the flat bound now adds, per bond, what the player's IN_BATTLE layer 特质 can add to it — each trait of its units and each one their ADD_BOND traits hand out (once per operator), on the bonds it names, up to the cap the sim applies (`max_add_count_per_battle`, the handed-out 华法琳 trait's `GRANTED_CAP_OVERRIDE`); a trait without a cap — or a 魔王, whose +extra counts toward none — leaves its bonds bounded only by the 999 room. A forged gain on any other bond meets the flat bound as before. - Normative lines rewritten with it: §6.3 (`harmony` in the views), §6.6 (the band pick), §8.2 / §8.3 (`bonds[]` entries `harmony?`), §14 / §20.12 (the layer bound), §21.11 (its 调和 note). - Tests: `test/ui/feedback1-gaps.test.js`, `test/match/feedback1-gaps.test.js`; opt-in `test/ui/feedback1-gaps.e2e.test.js` (`SP_E2E=1`; 1920×1080, 844×390, 756×366; `SP_E2E_OUT` moves the screenshots). diff --git a/docs/META.md b/docs/META.md index a497430f..70431a5a 100644 --- a/docs/META.md +++ b/docs/META.md @@ -213,9 +213,10 @@ The rest of this section is the legacy server-run mode ### 1.5 AI player (bot.js) **Strategy** (`botPickBand`): weighted by starting LP among the offered bands (alone, 老鲤's withheld first-round funds -× 0.02); a band whose mechanic rides on a bond the mode switches off weighs 0 — `GameData.bandBondIds` (the bond names -in <…> of its text, the bond ids / bond pools of its blackboards) ∩ `modeInactiveBonds`: 标准's 潘格尼尼, 克莱门莎, 玛恩纳 — -and with every offered band excluded it takes the default band; one rng draw per pick (DESIGN §21.26). +× 0.02); a band whose mechanic rides on a bond the mode switches off weighs 0 — `GameData.bandBondIds` (bands.json +`bondIds`, built by shared/bandBonds.js: the bond names in <…> of its text, the bond ids / bond pools of its blackboards) +∩ `modeInactiveBonds`: 标准's 潘格尼尼, 克莱门莎, 玛恩纳 — and with every offered band excluded it takes the default band; +one rng draw per pick (DESIGN §21.26). The strategy draft marks the same bands 本局禁用 for humans (still selectable). Buys toward a full board first (the cap is 8 from R1; leftover funds are lost), completes the merges it can afford, then levels on a curve (L2 ≈ R3, L3 ≈ R5, L4 ≈ R7, L5 ≈ R10, L6 ≈ R12 — the competent curve of docs/BALANCE.md; free levels always), then spends the rest (a band that keeps its leftover funds — 坎诺特 利滚利 — holds its interest capital diff --git a/public/css/screens/draft.css b/public/css/screens/draft.css index 261f9402..57751172 100644 --- a/public/css/screens/draft.css +++ b/public/css/screens/draft.css @@ -75,3 +75,11 @@ box-shadow: 0 .03rem .1rem rgba(0, 0, 0, .6); } .draft-detail__status--taken { color: var(--amber); } +/* a strategy built around a bond this mode switches off (bandDraft.js BandOffTag / BandOffNote, DESIGN §21.26 — the + §21.7 本局禁用 look: dim, dashed, the bond struck through): information only, it stays selectable. Px floors keep it + readable on phones (1rem = 40 px). */ +.dband__off { font-size: max(.12rem, 8px); line-height: 1.5; padding: 0 .05rem; color: var(--text-dim); border: 1px dashed rgba(255, 255, 255, .28); white-space: nowrap; } +.draft-detail__off { align-self: stretch; display: flex; align-items: flex-start; gap: .06rem; margin: 0; padding: .06rem .1rem; + font-size: max(.14rem, 9px); line-height: 1.5; color: var(--text-lo); background: rgba(255, 255, 255, .03); border-left: 2px solid rgba(255, 255, 255, .28); } +.draft-detail__off .icon { width: max(.16rem, 10px); height: max(.16rem, 10px); flex: none; margin-top: .02rem; } +.draft-detail__offname { color: var(--text-dim); text-decoration: line-through; text-decoration-color: rgba(255, 84, 84, .6); } diff --git a/public/js/screens/bandDraft.js b/public/js/screens/bandDraft.js index 99378a71..4f2c24b8 100644 --- a/public/js/screens/bandDraft.js +++ b/public/js/screens/bandDraft.js @@ -9,6 +9,9 @@ // is reported (g.bandFocus) and the server assigns it while it is free, else 「华法琳」, else the first free strategy // (timeoutBand). It starts on that default, so the tip under the order list always names what a timeout gives. // Solo, and a co-op match with a single human (the server's soloUntimed: draft.untimed): no clock at all. +// A strategy built around a bond the mode switches off (bands.json bondIds ∩ the mode's inactive bonds — 标准: 潘格尼尼 +// 拉特兰, 克莱门莎 阿戈尔, 玛恩纳 卡西米尔; the bot never picks one) reads 本局禁用 on its card and in the detail pane +// (BandOffTag / BandOffNote, DESIGN §21.26, §21.7's look); it stays selectable — information only. import { useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.js'; import { html, Button, Icon, MicroLabel, useTicker, secondsLeft } from '../ui/components.js'; @@ -18,9 +21,21 @@ import { actions, act } from '../ui/gameActions.js'; import { normalizeDraft, sortedPlayers } from '../ui/gameLogic.js'; import { useStore } from '../store.js'; import { audio } from '../audio.js'; +import { modeOffBonds, bandOffBonds, bandOffLine } from '../ui/gameLogic.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); +/** 本局禁用 on a strategy card whose bonds `names` the mode switches off (bandOffBonds); nothing otherwise. */ +export function BandOffTag({ names = [] }) { + return names.length ? html`本局禁用` : null; +} + +/** The detail pane's note for such a strategy: "本局禁用【拉特兰】盟约,此策略效果可能无法发挥" (the bond names struck through). */ +export function BandOffNote({ names = [] }) { + if (!names.length) return null; + return html`

<${Icon} name="info" />本局禁用${names.map((n, i) => html`【${n}】`)}盟约,此策略效果可能无法发挥

`; +} + /** * Bands selectable in a mode (modeTypeList contains the mode's type), sorted by sortId. * @param {any[]} bands @@ -137,6 +152,7 @@ export function BandDraftScreen() { const [skipped, setSkipped] = useState(false); const mode = gd.config?.modes?.[pub?.modeId]; + const offBonds = modeOffBonds(mode); // the bonds this mode never activates (标准: 10 of 23) const solo = roomSolo || mode?.type === 'SINGLE' || String(pub?.modeId || '').includes('single'); const bands = useMemo(() => allowedBands(gd.list('bands'), mode?.type || (solo ? 'SINGLE' : 'MULTI')), [gd.ready, mode?.type, solo]); const players = sortedPlayers(pub); @@ -233,12 +249,14 @@ export function BandDraftScreen() { ${bands.map((b) => { const who = pickers.get(b.bandId) || []; const isTaken = taken.has(b.bandId); - return html``; @@ -252,6 +270,7 @@ export function BandDraftScreen() {
初始生命值<${LpTower} value=${band.totalHp} size="lg" />

${band.name}

+ <${BandOffNote} names=${bandOffBonds(band, offBonds).map((id) => gd.bond(id)?.name || id)} />
<${MicroLabel} tone="mint">EFFECT ${band.effectName || ''} diff --git a/public/js/ui/gameLogic.js b/public/js/ui/gameLogic.js index b5f08f50..9ca6c3e3 100644 --- a/public/js/ui/gameLogic.js +++ b/public/js/ui/gameLogic.js @@ -582,6 +582,27 @@ export function modeOffBonds(mode) { return new Set(list.filter((b) => typeof b === 'string')); } +/** + * The bonds a strategy is built around that this mode switches off: bands.json `bondIds` (shared/bandBonds.js at build + * time — the field the server's bot reads too, GameData.bandBondIds) ∩ `off` (modeOffBonds). 标准: 潘格尼尼 → 拉特兰, + * 克莱门莎 → 阿戈尔, 玛恩纳 → 卡西米尔; the strategy draft marks such a band 本局禁用 (still selectable, DESIGN §21.26). + * @param {any} band bands.json record @param {Set|null} off + * @returns {string[]} + */ +export function bandOffBonds(band, off) { + if (!(off instanceof Set) || !off.size || !Array.isArray(band?.bondIds)) return []; + return band.bondIds.filter((b) => typeof b === 'string' && off.has(b)); +} + +/** + * The strategy draft's note for such a band: "本局禁用【拉特兰】盟约,此策略效果可能无法发挥". + * @param {string[]} names the switched-off bonds' names + */ +export function bandOffLine(names) { + const list = (Array.isArray(names) ? names : []).filter((n) => typeof n === 'string' && n); + return list.length ? `本局禁用${list.map((n) => `【${n}】`).join('')}盟约,此策略效果可能无法发挥` : ''; +} + // ---- shop --------------------------------------------------------------------------------------------- /** diff --git a/test/match/feedback1-gaps.test.js b/test/match/feedback1-gaps.test.js index 909422d1..d474c8cc 100644 --- a/test/match/feedback1-gaps.test.js +++ b/test/match/feedback1-gaps.test.js @@ -260,7 +260,8 @@ test('§21.26 docs: DESIGN (the subsection and the normative lines), META, PLAYI const at = DESIGN.indexOf('### 21.26 Gaps found while triaging GitHub issues #1 / #8 (v0.1.1)'); assert.ok(at > 0, 'the subsection'); const s = DESIGN.slice(at); - for (const k of ['morphPairings', 'harmonyMembers', '`harmony: 1`', 'bandBondIds', 'botPickBand', '0 of 200 and 0 of 800', '5 of 200 solo and 23 of 800 co-op']) assert.ok(s.includes(k), k); + for (const k of ['morphPairings', 'harmonyMembers', '`harmony: 1`', 'bandBondIds', 'botPickBand', '0 of 200 and 0 of 800', '5 of 200 solo and 23 of 800 co-op', + 'shared/bandBonds.js', 'bandOffBonds', 'BandOffTag', '本局禁用【拉特兰】盟约,此策略效果可能无法发挥', 'it stays selectable']) assert.ok(s.includes(k), k); assert.match(doc('docs/DATA.md'), /\| `bondIds` \| `\["lateranoShip"\]` \(潘格尼尼\) \/ `\[\]` \|/, 'DATA.md bands.json bondIds'); assert.match(DESIGN, /bonds:\[\{bondId,count,active,tier,layers,harmony\? \/\* 调和's \+1 is in count, §21\.26 \*\/\}\]/, '§8.2'); assert.match(DESIGN, /bonds: \[ \{ bondId, count, active, tier, layers, harmony\? \/\* 调和's \+1 is in count, §21\.26 \*\/, thresholds, countsHand \} \]/, '§8.3'); @@ -274,4 +275,6 @@ test('§21.26 docs: DESIGN (the subsection and the normative lines), META, PLAYI assert.match(v011, /变形同构体的详情卡列出天赋栏里的对应关系/); assert.match(v011, /盟约详情写明「含调和 \+1」/); assert.match(v011, /人机不再选择围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)/); + assert.match(v011, /选择策略时,围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)标出「本局禁用」并在详情里说明,仍可选择/); + assert.match(META, /The strategy draft marks the same bands 本局禁用 for humans \(still selectable\)/); }); diff --git a/test/ui/feedback1-gaps.e2e.test.js b/test/ui/feedback1-gaps.e2e.test.js index 8dcb00c4..83df82ea 100644 --- a/test/ui/feedback1-gaps.e2e.test.js +++ b/test/ui/feedback1-gaps.e2e.test.js @@ -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}-x.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}-x.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(); + }); + } }); diff --git a/test/ui/feedback1-gaps.test.js b/test/ui/feedback1-gaps.test.js index 22b2a442..0cbb61f5 100644 --- a/test/ui/feedback1-gaps.test.js +++ b/test/ui/feedback1-gaps.test.js @@ -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); + }); +});