feedback1 draft-info: the strategy draft shows the briefing's disabled bonds and banned operators again (GitHub issue #8 item 1)
One component, public/js/ui/matchInfo.js: matchInfoModel (the drawn set D and the mode's inactive bonds via disabledBondSets, the briefing's bond order, banned operators by tier, bannedPerBond) and the briefing's bond rows, legend and banned-operator grid. The briefing renders it; the band draft opens it read-only in a dialog (查看禁用盟约与干员 under the order list; 关闭, a tap outside or Esc close it; its footer repeats the turn and the countdown; a turn change closes it, the end of the draft unmounts it); the in-game 本局信息 tab reads the same model, so its banned operators are listed by tier too. In the dialog every size has a px floor, so phones keep readable text and a scrollable body. Tests: test/ui/match-info.test.js; opt-in test/ui/match-info.e2e.test.js (SP_E2E=1: solo 标准 on desktop, co-op 绝境 with a desktop and a phone).
This commit is contained in:
1 parent
842e1e13c4
commit
7188e0af89
8 files changed
+847
-56
No files matched your search
@@ -1,6 +1,8 @@
|
||||
/* ==========================================================================================
|
||||
screens/briefing.css — INFO_CHECK briefing (js/screens/briefing.js) + the pre-game step header
|
||||
shared with the band draft (js/ui/matchChrome.js StepHeader). Research 06 §4.1 / §11.6.
|
||||
The bond rows, the legend and the banned operators (.brief-bonds / .brief-legend / .brief-banned) are
|
||||
js/ui/matchInfo.js MatchInfo — also shown by the band draft's 本局信息 dialog (css/screens/draft.css .minfo-dlg).
|
||||
========================================================================================== */
|
||||
|
||||
/* ---- shared pre-game backdrop + header ---- */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* ==========================================================================================
|
||||
screens/draft.css — BAND_DRAFT strategy pick (js/screens/bandDraft.js). Research 06 §4.2:
|
||||
draft order on the left, portrait grid, detail pane with 初始生命值 and 确认选择 / 跳过.
|
||||
draft order on the left, portrait grid, detail pane with 初始生命值 and 确认选择 / 跳过; the 本局信息 entry under
|
||||
the order list and its dialog (js/ui/matchInfo.js MatchInfoDialog, GitHub issue #8 item 1).
|
||||
========================================================================================== */
|
||||
|
||||
.draft__main { position: relative; z-index: 1; flex: 1; min-height: 0; display: grid; grid-template-columns: 3.9rem 1fr 5rem; gap: .3rem; padding: .3rem .42rem .36rem; }
|
||||
@@ -75,3 +76,50 @@
|
||||
box-shadow: 0 .03rem .1rem rgba(0, 0, 0, .6);
|
||||
}
|
||||
.draft-detail__status--taken { color: var(--amber); }
|
||||
|
||||
/* ---- 本局信息 (GitHub issue #8 item 1): the entry under the order list and the read-only dialog ----------------------
|
||||
The dialog shows the briefing's own blocks (.brief-bonds / .brief-legend / .brief-banned, css/screens/briefing.css)
|
||||
at the briefing's size on desktop; every size is max(design rem, px floor), so on a phone (1rem = 40 px) the text
|
||||
stays at ≥ 9–15 px and the dialog's body scrolls inside the screen (756×366, 844×390) while the header and the
|
||||
footer (the turn's countdown, 关闭) stay in view. */
|
||||
.draft-order__info {
|
||||
margin-top: .04rem; justify-content: flex-start; padding: 0 .16rem;
|
||||
height: max(.5rem, 28px); font-size: max(.18rem, 10px);
|
||||
--btn-bd: var(--mint-800); --btn-bg: rgba(78, 216, 175, .06); --btn-bg-hover: rgba(78, 216, 175, .14);
|
||||
}
|
||||
.draft-order__info .btn__icon { color: var(--mint-400); }
|
||||
.modal__box.minfo-dlg {
|
||||
width: min(max(12rem, 600px), calc(100vw - .6rem - var(--sa-l, 0px) - var(--sa-r, 0px)));
|
||||
max-height: calc(100vh - .6rem);
|
||||
max-height: calc(100svh - .6rem);
|
||||
}
|
||||
.minfo-dlg .modal__head { padding: .16rem .3rem .02rem; }
|
||||
.minfo-dlg .modal__head .micro { font-size: max(.11rem, 8px); }
|
||||
.minfo-dlg .modal__title { font-size: max(.26rem, 15px); }
|
||||
.minfo-dlg .modal__body { display: flex; flex-direction: column; gap: max(.16rem, 6px); padding: .1rem .3rem .2rem; }
|
||||
.minfo-dlg .modal__actions { align-items: center; padding: .12rem .3rem; }
|
||||
.minfo-dlg .modal__actions .btn { height: max(.5rem, 30px); font-size: max(.18rem, 12px); }
|
||||
.minfo-dlg__status { margin-right: auto; min-width: 0; }
|
||||
.minfo-dlg__turn { display: inline-flex; align-items: center; gap: .06rem; font-size: max(.17rem, 11px); font-weight: 700; color: var(--gold); white-space: nowrap; }
|
||||
.minfo-dlg__turn .icon { width: max(.18rem, 12px); height: max(.18rem, 12px); }
|
||||
.minfo-dlg__turn b { margin-left: .04rem; font-size: max(.22rem, 14px); }
|
||||
.minfo-dlg__turn.is-warn { color: var(--orange-hi); }
|
||||
.minfo-dlg__turn.is-mint { color: var(--mint-400); }
|
||||
.minfo-dlg__turn.is-dim { color: var(--text-lo); }
|
||||
.minfo-dlg .brief-bonds, .minfo-dlg .brief-banned { padding: max(.16rem, 6px) max(.2rem, 8px) max(.2rem, 8px); }
|
||||
.minfo-dlg .brief-h { font-size: max(.21rem, 12px); margin-bottom: max(.12rem, 5px); }
|
||||
.minfo-dlg .brief-h::before { width: max(.06rem, 3px); height: max(.2rem, 11px); }
|
||||
.minfo-dlg .brief-bonds__row { gap: max(.16rem, 6px) max(.1rem, 4px); }
|
||||
.minfo-dlg .brief-bond { width: max(1rem, 50px); }
|
||||
.minfo-dlg .brief-bond .bond { --disc: max(.66rem, 32px); }
|
||||
.minfo-dlg .brief-bond .bond__name { font-size: max(.16rem, 10px); }
|
||||
.minfo-dlg .brief-bond__ban { font-size: max(.12rem, 9px); padding: 0 max(.05rem, 2px); }
|
||||
.minfo-dlg .brief-bond__ban .icon { width: max(.12rem, 9px); height: max(.12rem, 9px); }
|
||||
.minfo-dlg .brief-legend { margin-top: 0; font-size: max(.14rem, 10px); }
|
||||
.minfo-dlg .brief-legend__off { width: max(.16rem, 9px); height: max(.16rem, 9px); }
|
||||
.minfo-dlg .brief-legend__ban { width: max(.2rem, 12px); height: max(.18rem, 11px); }
|
||||
.minfo-dlg .brief-legend__ban .icon { width: max(.12rem, 8px); height: max(.12rem, 8px); }
|
||||
.minfo-dlg .brief-banned__n { font-size: max(.22rem, 13px); }
|
||||
.minfo-dlg .brief-banned__grid { grid-template-columns: repeat(auto-fill, minmax(max(.6rem, 34px), 1fr)); gap: max(.08rem, 3px); }
|
||||
.minfo-dlg .brief-banned__grid .uthumb { --ut: max(.6rem, 34px); }
|
||||
.minfo-dlg .t-dim { font-size: max(.16rem, 10px); }
|
||||
@@ -9,11 +9,17 @@
|
||||
// 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.
|
||||
// 本局信息 (GitHub issue #8 item 1, "选策略时没法返回查看禁用的干员和盟约"): 查看禁用盟约与干员 under the order list opens the
|
||||
// briefing's bond rows, legend and 本局禁用干员 again, read-only (ui/matchInfo.js MatchInfoDialog — the very blocks of the
|
||||
// briefing). The draft runs on underneath: its status line repeats the current turn and the countdown (draftInfoStatus),
|
||||
// a turn change (a pick, a skip, a turn that runs out, an AI pick) closes it, the end of the draft unmounts it, and it
|
||||
// never touches the highlighted band or the buttons.
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.js';
|
||||
import { html, Button, Icon, MicroLabel, useTicker, secondsLeft } from '../ui/components.js';
|
||||
import { useGameData, BandIcon, RichText, PlayerAvatar, LpTower, Sprite } from '../ui/gameComponents.js';
|
||||
import { StepHeader, ExitModal } from '../ui/matchChrome.js';
|
||||
import { MatchInfoDialog, matchInfoModel } from '../ui/matchInfo.js';
|
||||
import { actions, act } from '../ui/gameActions.js';
|
||||
import { normalizeDraft, sortedPlayers } from '../ui/gameLogic.js';
|
||||
import { useStore } from '../store.js';
|
||||
@@ -124,6 +130,21 @@ export function draftClock(pub) {
|
||||
return { deadline, total: Number(d.turnSeconds) > 0 ? Number(d.turnSeconds) : null };
|
||||
}
|
||||
|
||||
/**
|
||||
* The status line of the 本局信息 dialog: what the draft does while the dialog covers it — my pick, else whose turn it is
|
||||
* with the turn's seconds left (the step header's number; none when untimed), warning at ≤ 10 s like the countdown.
|
||||
* @param {{ myPick?: string|null, pickName?: string|null, myTurn: boolean, turnName?: string|null, secs?: number|null,
|
||||
* waiting?: boolean }} o waiting: teammates still have to pick after my pick
|
||||
* @returns {{ text: string, secs: number|null, tone: 'mint'|'gold'|'warn'|'dim' }}
|
||||
*/
|
||||
export function draftInfoStatus({ myPick = null, pickName = null, myTurn, turnName = null, secs = null, waiting = false }) {
|
||||
if (myPick) return { text: `已选择「${pickName || ''}」${waiting ? ',等待其他博士' : ''}`, secs: null, tone: 'mint' };
|
||||
const s = Number.isFinite(secs) ? Math.max(0, Math.round(secs)) : null;
|
||||
const tone = s != null && s <= 10 ? 'warn' : 'gold';
|
||||
if (myTurn) return { text: '轮到你决策', secs: s, tone };
|
||||
return { text: turnName ? `${turnName} 决策中` : '等待轮到你', secs: s, tone: s != null ? tone : 'dim' };
|
||||
}
|
||||
|
||||
/** BAND_DRAFT screen. */
|
||||
export function BandDraftScreen() {
|
||||
const pub = useStore((s) => s.match.public);
|
||||
@@ -135,6 +156,7 @@ export function BandDraftScreen() {
|
||||
const [busy, setBusy] = useState(null);
|
||||
const [exit, setExit] = useState(false);
|
||||
const [skipped, setSkipped] = useState(false);
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
|
||||
const mode = gd.config?.modes?.[pub?.modeId];
|
||||
const solo = roomSolo || mode?.type === 'SINGLE' || String(pub?.modeId || '').includes('single');
|
||||
@@ -163,6 +185,10 @@ export function BandDraftScreen() {
|
||||
}, [bands.length, myPick, myTurn, takenKey]);
|
||||
// "your turn" cue
|
||||
useEffect(() => { if (myTurn && !solo) audio.sfx('yourTurn'); }, [myTurn]);
|
||||
// the 本局信息 dialog never outlives the turn it was opened in: a turn change (a pick, a skip, a turn that ran out, an
|
||||
// AI pick) or my pick closes it, so whoever's turn begins sees the draft
|
||||
const turnKey = `${draft.turnPid || ''}|${myPick || ''}`;
|
||||
useEffect(() => { setInfoOpen(false); }, [turnKey]);
|
||||
|
||||
// one countdown (user playtest #4 item 4): the current turn's — m.public.deadline, the same clock as the picker's row
|
||||
const clock = solo ? null : draftClock(pub);
|
||||
@@ -198,6 +224,10 @@ export function BandDraftScreen() {
|
||||
// the picker's row shows the step header's number (both read the one turn deadline)
|
||||
const turnSecs = clock ? secondsLeft(clock.deadline) : null;
|
||||
const turnLen = Number(pub?.draft?.turnSeconds) > 0 ? Math.round(pub.draft.turnSeconds) : null;
|
||||
// 本局信息: the briefing's blocks (built only while the dialog is open) and the draft's state under it
|
||||
const info = infoOpen ? matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode }) : null;
|
||||
const infoStatus = infoOpen ? draftInfoStatus({ myPick, pickName: myPick ? gd.band(myPick)?.name : null, myTurn, turnName, secs: turnSecs,
|
||||
waiting: !solo && !draft.done }) : null;
|
||||
|
||||
return html`<div class="screen draft">
|
||||
<div class="brief__bg" aria-hidden="true"></div>
|
||||
@@ -226,6 +256,8 @@ export function BandDraftScreen() {
|
||||
</span>
|
||||
</div>`;
|
||||
})}
|
||||
<${Button} variant="secondary" icon="search" block=${true} class="draft-order__info" data-testid="match-info-open"
|
||||
aria-haspopup="dialog" onClick=${() => setInfoOpen(true)}>查看禁用盟约与干员<//>
|
||||
${!solo ? html`<p class="draft-order__tip" data-testid="draft-tip">${draftTip({ timed, turnSeconds: turnLen, autoName: myPick ? null : autoName, selected: autoId === sel })}</p>` : null}
|
||||
</aside>
|
||||
|
||||
@@ -270,5 +302,9 @@ export function BandDraftScreen() {
|
||||
</aside>
|
||||
</main>
|
||||
<${ExitModal} open=${exit} onClose=${() => setExit(false)} solo=${solo} />
|
||||
<${MatchInfoDialog} open=${infoOpen} onClose=${() => setInfoOpen(false)} model=${info}
|
||||
status=${infoStatus ? html`<span class=${cx('minfo-dlg__turn', `is-${infoStatus.tone}`)}>
|
||||
<${Icon} name=${infoStatus.tone === 'mint' ? 'check' : 'hourglass'} />${infoStatus.text}${infoStatus.secs != null ? html`<b class="num">${infoStatus.secs}s</b>` : null}
|
||||
</span>` : null} />
|
||||
</div>`;
|
||||
}
|
||||
@@ -1,44 +1,22 @@
|
||||
// Briefing — INFO_CHECK "1/2 确认本局信息" (research 06 §4.1, D1): enemy leader (silhouette, name,
|
||||
// abilities), stage (+ its pool: 战场固定 / 战场随机(共N张)), 特训敌人 factions (icon, name, description), 核心盟约 / 附加盟约 rows of bond discs
|
||||
// (disabled bonds greyed with the banned-member badge), 本局禁用干员 avatars, difficulty tag, the ready
|
||||
// count x/N with person pips, the 准备就绪 button (g.infoReady) and the countdown.
|
||||
// abilities), stage (+ its pool: 战场固定 / 战场随机(共N张)), 特训敌人 factions (icon, name, description), the difficulty
|
||||
// tag, the ready count x/N with person pips, the 准备就绪 button (g.infoReady) and the countdown. The right column — 核心盟约
|
||||
// / 附加盟约 rows of bond discs (disabled bonds greyed with the banned-member badge), the legend and the 本局禁用干员
|
||||
// avatars — is ui/matchInfo.js MatchInfo, the same blocks the strategy draft's 本局信息 dialog shows (screens/bandDraft.js).
|
||||
|
||||
import { useState } from '../../vendor/hooks.module.js';
|
||||
import { html, Button, Icon, MicroLabel, BondDisc, Tooltip } from '../ui/components.js';
|
||||
import { useGameData, Img, UnitThumb, RichText } from '../ui/gameComponents.js';
|
||||
import { html, Button, Icon, MicroLabel } from '../ui/components.js';
|
||||
import { useGameData, Img, RichText } from '../ui/gameComponents.js';
|
||||
import { StepHeader, ExitModal } from '../ui/matchChrome.js';
|
||||
import { MatchInfo, matchInfoModel } from '../ui/matchInfo.js';
|
||||
import { LoadoutButton } from './loadout.js';
|
||||
import { actions } from '../ui/gameActions.js';
|
||||
import { factionTypes, bannedPerBond, sortedPlayers, phaseTotalSeconds, disabledBondSets, briefingBondTip } from '../ui/gameLogic.js';
|
||||
import { bondIconUrl, enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js';
|
||||
import { factionTypes, sortedPlayers, phaseTotalSeconds } from '../ui/gameLogic.js';
|
||||
import { enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js';
|
||||
import { useStore } from '../store.js';
|
||||
import { data } from '../data.js';
|
||||
|
||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||
|
||||
function BondRow({ title, micro, bonds, sets, perBond }) {
|
||||
const m = data.get('assets');
|
||||
return html`<div class="brief-bonds">
|
||||
<h3 class="brief-h"><span>${title}</span><${MicroLabel}>${micro}</${MicroLabel}></h3>
|
||||
<div class="brief-bonds__row">
|
||||
${bonds.map((b) => {
|
||||
// greyed: the drawn set D (still activatable, roster incomplete) and the mode's static inactive bonds
|
||||
const state = sets.off.has(b.bondId) ? 'off' : sets.drawn.has(b.bondId) ? 'drawn' : null;
|
||||
const off = !!state;
|
||||
const bannedN = perBond.get(b.bondId) || 0;
|
||||
const tip = briefingBondTip(b.name, state, bannedN);
|
||||
return html`<${Tooltip} key=${b.bondId} text=${tip}>
|
||||
<div class=${cx('brief-bond', off && 'is-off', state === 'drawn' && 'is-incomplete', !off && bannedN > 0 && 'is-partial')}>
|
||||
<${BondDisc} name=${b.name} icon=${bondIconUrl(m, b.bondId)} active=${!off} disabled=${off} tier=${off ? 0 : (b.thresholds?.length || 1)}
|
||||
maxTier=${Math.max(1, b.thresholds?.length || 1)} size="md" />
|
||||
${bannedN > 0 ? html`<span class="brief-bond__ban num"><${Icon} name="user" />${bannedN}</span>` : null}
|
||||
</div>
|
||||
<//>`;
|
||||
})}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** INFO_CHECK screen. */
|
||||
export function BriefingScreen() {
|
||||
const pub = useStore((s) => s.match.public);
|
||||
@@ -53,13 +31,7 @@ export function BriefingScreen() {
|
||||
const me = players.find((p) => p.playerId === myId);
|
||||
const readyN = players.filter((p) => p.ready).length;
|
||||
const mode = gd.config?.modes?.[pub.modeId];
|
||||
const sets = disabledBondSets(pub, mode?.inactiveBondIds);
|
||||
const bonds = gd.list('bonds').sort((a, b) => (a.bondOrder ?? 0) - (b.bondOrder ?? 0) || (a.identifier ?? 0) - (b.identifier ?? 0));
|
||||
const core = bonds.filter((b) => b.isCore);
|
||||
const addon = bonds.filter((b) => !b.isCore);
|
||||
const banned = (Array.isArray(pub.bannedChess) ? pub.bannedChess : []).filter((id) => gd.chess(id));
|
||||
banned.sort((a, b) => (gd.chess(a)?.tier ?? 0) - (gd.chess(b)?.tier ?? 0));
|
||||
const perBond = bannedPerBond(bonds, banned);
|
||||
const info = matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode });
|
||||
const boss = pub.bossId ? gd.boss(pub.bossId) : null;
|
||||
const bossEnemy = boss ? gd.enemy(boss.enemyKey) : null;
|
||||
const stage = pub.stageId ? gd.stage(pub.stageId) : null;
|
||||
@@ -111,15 +83,7 @@ export function BriefingScreen() {
|
||||
</div>
|
||||
</section>
|
||||
<section class="brief__right">
|
||||
<${BondRow} title="核心盟约" micro="CORE BONDS" bonds=${core} sets=${sets} perBond=${perBond} />
|
||||
<${BondRow} title="附加盟约" micro="ADD-ON BONDS" bonds=${addon} sets=${sets} perBond=${perBond} />
|
||||
<p class="brief-legend"><span class="brief-legend__off"></span>灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)${sets.off.size ? ',或本模式禁用' : ''} · <span class="brief-legend__ban"><${Icon} name="user" /></span>该盟约中无法出现的干员数</p>
|
||||
<div class="brief-banned">
|
||||
<h3 class="brief-h"><span>本局禁用干员</span><${MicroLabel}>BANNED OPERATORS</${MicroLabel}><b class="num brief-banned__n">${banned.length}</b></h3>
|
||||
${banned.length ? html`<div class="brief-banned__grid">
|
||||
${banned.map((id) => html`<${UnitThumb} key=${id} kind="chess" id=${id} size="sm" dim=${true} />`)}
|
||||
</div>` : html`<p class="t-dim">本局没有禁用干员</p>`}
|
||||
</div>
|
||||
<${MatchInfo} model=${info} />
|
||||
</section>
|
||||
</main>
|
||||
<footer class="brief__foot">
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// 本局信息 dialog under the top bar (opened by the left 🔍, official btn_check_player / autochess_hud_player_info_dialog,
|
||||
// research 09 §2.1) with two tabs:
|
||||
// 本局信息 — own strategy, stage, disabled bonds with banned-member counts and the banned operator list (default);
|
||||
// 本局信息 — own strategy, stage, disabled bonds with banned-member counts and the banned operator list (default) —
|
||||
// the same bonds, states and banned operators (by tier) as the briefing and the strategy draft's 本局信息
|
||||
// dialog: ui/matchInfo.js matchInfoModel, drawn here as compact chips and avatars;
|
||||
// 敌方情报 — the secondary enemy list: m.private.nextEnemies (icons, names, counts, tags, 特训 faction tags) + this
|
||||
// match's factions and the enemy leader on boss rounds. The primary enemy preview is the pen on the board
|
||||
// (the right 🔍▶▶ pans the camera there; tapping an enemy in the pen opens its detail card) — research 09 §6.2.
|
||||
|
||||
import { html, Icon, Tabs, MicroLabel } from './components.js';
|
||||
import { Img, UnitThumb, BondGlyph, BandIcon, RichText, GIcon } from './gameComponents.js';
|
||||
import { groupEnemies, factionTypes, bannedPerBond, disabledBondSets, briefingBondTip } from './gameLogic.js';
|
||||
import { groupEnemies, factionTypes, briefingBondTip } from './gameLogic.js';
|
||||
import { matchInfoModel } from './matchInfo.js';
|
||||
import { factionIconUrl } from './assetUrls.js';
|
||||
import { data } from '../data.js';
|
||||
|
||||
@@ -57,12 +60,10 @@ function EnemiesTab({ pub, priv, onEnemy }) {
|
||||
}
|
||||
|
||||
function InfoTab({ pub, priv, onChess }) {
|
||||
const sets = disabledBondSets(pub, data.get('config')?.modes?.[pub?.modeId]?.inactiveBondIds);
|
||||
const disabled = new Set([...sets.drawn, ...sets.off]);
|
||||
const stateOf = (id) => (sets.off.has(id) ? 'off' : sets.drawn.has(id) ? 'drawn' : null);
|
||||
const bonds = data.list('bonds');
|
||||
const banned = Array.isArray(pub?.bannedChess) ? pub.bannedChess : [];
|
||||
const perBond = bannedPerBond(bonds, banned);
|
||||
const { bonds, banned, perBond, stateOf } = matchInfoModel(pub, {
|
||||
bonds: data.list('bonds'), chess: (id) => data.lookup('chess', id), mode: data.get('config')?.modes?.[pub?.modeId],
|
||||
});
|
||||
const disabled = new Set(bonds.filter((b) => stateOf(b.bondId)).map((b) => b.bondId));
|
||||
const band = priv?.bandId ? data.lookup('bands', priv.bandId) : null;
|
||||
const stage = pub?.stageId ? data.lookup('stages', pub.stageId) : null;
|
||||
const withBans = bonds.filter((b) => disabled.has(b.bondId) || (perBond.get(b.bondId) || 0) > 0)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Match info — the bonds and banned operators of a match (research 06 §4.1 "1/2 确认本局信息", research 01 A2 / 06
|
||||
// addendum D), one implementation for every place that shows them, so they never drift:
|
||||
// * the INFO_CHECK briefing's right column (screens/briefing.js);
|
||||
// * the strategy draft's 本局信息 dialog (screens/bandDraft.js; GitHub issue #8 item 1 "选策略时没法返回查看禁用的干员和盟约":
|
||||
// players choose a strategy partly by the bonds and operators the match leaves them);
|
||||
// * the data of the in-game 本局信息 tab (ui/enemyDrawer.js InfoTab, its own compact chips and avatars).
|
||||
// matchInfoModel (pure) derives everything from m.public and the static data: the two kinds of greyed bonds
|
||||
// (gameLogic disabledBondSets over config.modes[modeId].inactiveBondIds — the drawn set D "部分盟约所含干员阵容不完整",
|
||||
// still activatable through other bonds' operators or items, and the mode's static inactive bonds "本局禁用", e.g.
|
||||
// 标准模拟's 10), every bond in the briefing order (bondOrder, then identifier) split into 核心 / 附加, the banned
|
||||
// operators (m.public bannedChess, known chess only) sorted by tier, and the banned-member count per bond (gameLogic
|
||||
// bannedPerBond). The blocks are hookless (unit-tested by calling them): the 核心盟约 / 附加盟约 rows of bond discs —
|
||||
// greyed with the ✕, the red banned-member badge, the briefingBondTip tooltip — the legend and the 本局禁用干员 grid.
|
||||
// MatchInfoDialog shows the same blocks read-only in a components.js Modal (关闭, a tap outside or Esc close it) with a
|
||||
// status line from its caller (the draft's turn and countdown, so the running clock stays in view).
|
||||
// Styles: css/screens/briefing.css (.brief-*), css/screens/draft.css (.minfo-dlg).
|
||||
|
||||
import { html, Button, Icon, MicroLabel, BondDisc, Tooltip, Modal } from './components.js';
|
||||
import { UnitThumb } from './gameComponents.js';
|
||||
import { bannedPerBond, disabledBondSets, briefingBondTip } from './gameLogic.js';
|
||||
import { bondIconUrl } from './assetUrls.js';
|
||||
import { data } from '../data.js';
|
||||
|
||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||
|
||||
/**
|
||||
* @typedef {{ sets: { drawn: Set<string>, off: Set<string> }, stateOf: (bondId: string) => 'off'|'drawn'|null,
|
||||
* bonds: any[], core: any[], addon: any[], banned: string[], perBond: Map<string, number> }} MatchInfoModel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Everything the match-info blocks show.
|
||||
* @param {any} pub m.public (drawnDisabledBonds / disabledBonds, bannedChess)
|
||||
* @param {{ bonds?: any[], chess?: (id: string) => any, mode?: any }} [src]
|
||||
* bonds: bonds.json records (data.list('bonds')); chess: the chess lookup; mode: config.json modes[pub.modeId]
|
||||
* @returns {MatchInfoModel}
|
||||
* stateOf: 'off' = the mode never activates the bond (本局禁用), 'drawn' = in the drawn set D (阵容不完整), null = normal;
|
||||
* banned: the banned chess ids the data knows, by tier (ties keep the server's order); perBond: bondId → banned members
|
||||
*/
|
||||
export function matchInfoModel(pub, { bonds = [], chess = () => null, mode = null } = {}) {
|
||||
const sets = disabledBondSets(pub, mode?.inactiveBondIds);
|
||||
const stateOf = (id) => (sets.off.has(id) ? 'off' : sets.drawn.has(id) ? 'drawn' : null);
|
||||
const list = (Array.isArray(bonds) ? bonds : []).filter((b) => !!b && typeof b === 'object' && typeof b.bondId === 'string')
|
||||
.sort((a, b) => (a.bondOrder ?? 0) - (b.bondOrder ?? 0) || (a.identifier ?? 0) - (b.identifier ?? 0));
|
||||
const tierOf = (id) => chess(id)?.tier ?? 0;
|
||||
const banned = (Array.isArray(pub?.bannedChess) ? pub.bannedChess : []).filter((id) => typeof id === 'string' && !!chess(id))
|
||||
.sort((a, b) => tierOf(a) - tierOf(b));
|
||||
return {
|
||||
sets, stateOf, bonds: list, core: list.filter((b) => b.isCore), addon: list.filter((b) => !b.isCore),
|
||||
banned, perBond: bannedPerBond(list, banned),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One row of bond discs (核心盟约 / 附加盟约): a greyed disc (✕) for a bond in the drawn set D or one the mode never
|
||||
* activates, the red badge with its banned members, a red ring on an enabled bond that lost members; the tooltip says
|
||||
* which (briefingBondTip).
|
||||
* @param {{ title: string, micro: string, bonds: any[], model: MatchInfoModel }} props
|
||||
*/
|
||||
export function MatchBondRow({ title, micro, bonds, model }) {
|
||||
const m = data.get('assets');
|
||||
return html`<div class="brief-bonds">
|
||||
<h3 class="brief-h"><span>${title}</span><${MicroLabel}>${micro}</${MicroLabel}></h3>
|
||||
<div class="brief-bonds__row">
|
||||
${bonds.map((b) => {
|
||||
const state = model.stateOf(b.bondId);
|
||||
const off = !!state;
|
||||
const bannedN = model.perBond.get(b.bondId) || 0;
|
||||
return html`<${Tooltip} key=${b.bondId} text=${briefingBondTip(b.name, state, bannedN)}>
|
||||
<div class=${cx('brief-bond', off && 'is-off', state === 'drawn' && 'is-incomplete', !off && bannedN > 0 && 'is-partial')} data-bond=${b.bondId}>
|
||||
<${BondDisc} name=${b.name} icon=${bondIconUrl(m, b.bondId)} active=${!off} disabled=${off} tier=${off ? 0 : (b.thresholds?.length || 1)}
|
||||
maxTier=${Math.max(1, b.thresholds?.length || 1)} size="md" />
|
||||
${bannedN > 0 ? html`<span class="brief-bond__ban num"><${Icon} name="user" />${bannedN}</span>` : null}
|
||||
</div>
|
||||
<//>`;
|
||||
})}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The legend under the bond rows ("或本模式禁用" only when the mode switches bonds off).
|
||||
* @param {{ model: MatchInfoModel }} props
|
||||
*/
|
||||
export function MatchLegend({ model }) {
|
||||
return html`<p class="brief-legend"><span class="brief-legend__off"></span>灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)${model.sets.off.size ? ',或本模式禁用' : ''} · <span class="brief-legend__ban"><${Icon} name="user" /></span>该盟约中无法出现的干员数</p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本局禁用干员: the count and the greyed avatars by tier.
|
||||
* @param {{ model: MatchInfoModel }} props
|
||||
*/
|
||||
export function BannedOperators({ model }) {
|
||||
const { banned } = model;
|
||||
return html`<div class="brief-banned">
|
||||
<h3 class="brief-h"><span>本局禁用干员</span><${MicroLabel}>BANNED OPERATORS</${MicroLabel}><b class="num brief-banned__n">${banned.length}</b></h3>
|
||||
${banned.length ? html`<div class="brief-banned__grid">
|
||||
${banned.map((id) => html`<${UnitThumb} key=${id} kind="chess" id=${id} size="sm" dim=${true} />`)}
|
||||
</div>` : html`<p class="t-dim">本局没有禁用干员</p>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The briefing's bonds and banned operators: 核心盟约, 附加盟约, the legend, 本局禁用干员.
|
||||
* @param {{ model: MatchInfoModel }} props
|
||||
*/
|
||||
export function MatchInfo({ model }) {
|
||||
return html`<${MatchBondRow} title="核心盟约" micro="CORE BONDS" bonds=${model.core} model=${model} />
|
||||
<${MatchBondRow} title="附加盟约" micro="ADD-ON BONDS" bonds=${model.addon} model=${model} />
|
||||
<${MatchLegend} model=${model} />
|
||||
<${BannedOperators} model=${model} />`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The read-only match-info dialog (the strategy draft's 本局信息): MatchInfo in a Modal that 关闭, a tap outside or Esc
|
||||
* closes. `status` (the caller's line, e.g. the draft's turn and countdown) sits left of 关闭.
|
||||
* @param {{ open: boolean, onClose: Function, model: MatchInfoModel|null, status?: any }} props
|
||||
*/
|
||||
export function MatchInfoDialog({ open, onClose, model, status = null }) {
|
||||
return html`<${Modal} open=${open && !!model} onClose=${onClose} title="本局信息" micro="MATCH INFO // BONDS & BANNED OPERATORS" class="minfo-dlg"
|
||||
actions=${html`${status ? html`<div class="minfo-dlg__status" data-testid="match-info-status">${status}</div>` : null}
|
||||
<${Button} variant="secondary" icon="close" data-autofocus data-testid="match-info-close" onClick=${onClose}>关闭<//>`}>
|
||||
${open && model ? html`<${MatchInfo} model=${model} />` : null}
|
||||
<//>`;
|
||||
}
|
||||
Reference in new issue
Block a user