diff --git a/public/css/screens/briefing.css b/public/css/screens/briefing.css index 90a00a4e..b1ec678a 100644 --- a/public/css/screens/briefing.css +++ b/public/css/screens/briefing.css @@ -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 ---- */ diff --git a/public/css/screens/draft.css b/public/css/screens/draft.css index 261f9402..318a5f62 100644 --- a/public/css/screens/draft.css +++ b/public/css/screens/draft.css @@ -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); } diff --git a/public/js/screens/bandDraft.js b/public/js/screens/bandDraft.js index 99378a71..0d3393e3 100644 --- a/public/js/screens/bandDraft.js +++ b/public/js/screens/bandDraft.js @@ -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`
@@ -226,6 +256,8 @@ export function BandDraftScreen() {
`; })} + <${Button} variant="secondary" icon="search" block=${true} class="draft-order__info" data-testid="match-info-open" + aria-haspopup="dialog" onClick=${() => setInfoOpen(true)}>查看禁用盟约与干员 ${!solo ? html`

${draftTip({ timed, turnSeconds: turnLen, autoName: myPick ? null : autoName, selected: autoId === sel })}

` : null} @@ -270,5 +302,9 @@ export function BandDraftScreen() { <${ExitModal} open=${exit} onClose=${() => setExit(false)} solo=${solo} /> + <${MatchInfoDialog} open=${infoOpen} onClose=${() => setInfoOpen(false)} model=${info} + status=${infoStatus ? html` + <${Icon} name=${infoStatus.tone === 'mint' ? 'check' : 'hourglass'} />${infoStatus.text}${infoStatus.secs != null ? html`${infoStatus.secs}s` : null} + ` : null} /> `; } diff --git a/public/js/screens/briefing.js b/public/js/screens/briefing.js index 8c5af7ba..1819692a 100644 --- a/public/js/screens/briefing.js +++ b/public/js/screens/briefing.js @@ -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`
-

${title}<${MicroLabel}>${micro}

-
- ${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}> -
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`<${Icon} name="user" />${bannedN}` : null} -
- `; - })} -
-
`; -} - /** 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() {
- <${BondRow} title="核心盟约" micro="CORE BONDS" bonds=${core} sets=${sets} perBond=${perBond} /> - <${BondRow} title="附加盟约" micro="ADD-ON BONDS" bonds=${addon} sets=${sets} perBond=${perBond} /> -

灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)${sets.off.size ? ',或本模式禁用' : ''} · <${Icon} name="user" />该盟约中无法出现的干员数

-
-

本局禁用干员<${MicroLabel}>BANNED OPERATORS${banned.length}

- ${banned.length ? html`
- ${banned.map((id) => html`<${UnitThumb} key=${id} kind="chess" id=${id} size="sm" dim=${true} />`)} -
` : html`

本局没有禁用干员

`} -
+ <${MatchInfo} model=${info} />