// Band draft — BAND_DRAFT "2/2 选择策略" (research 06 §4.2, D1): left = draft order (avatar, name, state: // … waiting / ⌛ 决策中 / chosen band ✓), current picker highlighted; centre = grid of every band allowed // for the mode type (icon, name, LP); a band a teammate already picked carries the picker's avatar and is marked // 队友已选 — it cannot be chosen again (research 09 §5, guidebook 策略与轮选; the server refuses it too); right = // detail pane (icon, 初始生命值, name, effect name + rich description) with 跳过 (co-op, once) and 确认选择. // A timeout assigns 「华法琳」, or — when a teammate already holds it — the first free strategy (timeoutBand; the tip // under the order list names the one I would get). Solo: free pick, no timer. import { useEffect, useMemo, 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 { actions } from '../ui/gameActions.js'; import { normalizeDraft, sortedPlayers, phaseTotalSeconds } from '../ui/gameLogic.js'; import { useStore } from '../store.js'; import { audio } from '../audio.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); /** * Bands selectable in a mode (modeTypeList contains the mode's type), sorted by sortId. * @param {any[]} bands * @param {string|null} modeType 'SINGLE'|'MULTI' */ export function allowedBands(bands, modeType) { const sid = (b) => (Number.isFinite(b.sortId) ? b.sortId : 99); return (Array.isArray(bands) ? bands : []) .filter((b) => b && (!modeType || !Array.isArray(b.modeTypeList) || b.modeTypeList.includes(modeType))) .sort((a, b) => sid(a) - sid(b) || (a.bandId < b.bandId ? -1 : a.bandId > b.bandId ? 1 : 0)); } /** The official default strategy of an automatic assignment (data/config.json bandDraft.timeoutBandId). */ export const DEFAULT_TIMEOUT_BAND = 'band_bldsk'; /** * The strategy the server assigns me when my turn times out (server/match/Match.js defaultBand): the official default * 「华法琳」 while no teammate holds it, else the first free strategy in draft order (sortId) — never one a teammate * already picked (队友已选). * @param {any[]} bands allowedBands(...) (sortId order) * @param {Map} taken teammateBands(...) * @param {string} [defaultId] * @returns {string|null} */ export function timeoutBand(bands, taken, defaultId = DEFAULT_TIMEOUT_BAND) { const list = Array.isArray(bands) ? bands : []; const has = (id) => !!(taken && typeof taken.has === 'function' && taken.has(id)); if (defaultId && !has(defaultId) && (!list.length || list.some((b) => b.bandId === defaultId))) return defaultId; return list.find((b) => !has(b.bandId))?.bandId || defaultId || null; } /** * Bands taken by teammates (队友已选): bandId → the picking players (never the viewer). * @param {Map} picks normalizeDraft(...).picks (playerId → bandId) * @param {string} myId */ export function teammateBands(picks, myId) { const out = new Map(); for (const [pid, bid] of picks instanceof Map ? picks : []) { if (pid === myId || typeof bid !== 'string') continue; if (!out.has(bid)) out.set(bid, []); out.get(bid).push(pid); } return out; } /** * The band the detail pane shows: the current one, else my pick, else the first band no teammate took. When it is my * turn and the shown band was taken meanwhile (队友已选), the first free one instead (confirm would be disabled). * @param {string|null} sel * @param {{ bands: any[], taken: Map, myPick: string|null, myTurn: boolean }} o */ export function draftSelection(sel, { bands, taken, myPick, myTurn }) { if (!Array.isArray(bands) || !bands.length) return sel; const free = (bands.find((b) => !taken.has(b.bandId)) || bands[0]).bandId; if (!sel) return myPick || free; if (!myPick && myTurn && taken.has(sel)) return free; return sel; } /** BAND_DRAFT screen. */ export function BandDraftScreen() { const pub = useStore((s) => s.match.public); const priv = useStore((s) => s.match.private); const myId = useStore((s) => s.me.playerId); const roomSolo = useStore((s) => s.room?.mode === 'solo'); const gd = useGameData(); const [sel, setSel] = useState(null); const [busy, setBusy] = useState(null); const [exit, setExit] = useState(false); const [skipped, setSkipped] = useState(false); const mode = gd.config?.modes?.[pub?.modeId]; 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); const draft = normalizeDraft(pub?.draft, players); const myPick = draft.picks.get(myId) || priv?.bandId || null; const myTurn = !myPick && (solo || draft.turnPid === myId); const skipsLeft = draft.skipsLeft.has(myId) ? draft.skipsLeft.get(myId) : (skipped ? 0 : 1); const canSkip = !solo && myTurn && skipsLeft > 0 && draft.order.length > 1; const taken = solo ? new Map() : teammateBands(draft.picks, myId); const pickers = new Map(); // bandId → players for (const [pid, bid] of draft.picks) { const p = players.find((x) => x.playerId === pid); if (!pickers.has(bid)) pickers.set(bid, []); pickers.get(bid).push(p || { playerId: pid, name: '?' }); } // default selection: my pick, else the first band nobody else took; when my turn comes while the selected band has // been taken by a teammate meanwhile (队友已选), move the selection to the first free one const takenKey = [...taken.keys()].sort().join(','); useEffect(() => { const next = draftSelection(sel, { bands, taken, myPick, myTurn }); if (next !== sel) setSel(next); }, [bands.length, myPick, myTurn, takenKey]); // "your turn" cue useEffect(() => { if (myTurn && !solo) audio.sfx('yourTurn'); }, [myTurn]); // what a timeout gives me (the server never assigns a strategy a teammate holds — Match.js defaultBand) const defaultId = gd.config?.bandDraft?.timeoutBandId || DEFAULT_TIMEOUT_BAND; const autoId = myPick ? null : timeoutBand(bands, taken, defaultId); const defaultName = gd.band(defaultId)?.name || '华法琳'; const autoName = (autoId && gd.band(autoId)?.name) || defaultName; const band = sel ? gd.band(sel) : null; const selTaken = !!band && taken.has(band.bandId); const confirm = async () => { if (!band || busy || !myTurn || selTaken) return; setBusy('pick'); await actions.band(band.bandId); setBusy(null); }; const skip = async () => { if (busy || !canSkip) return; setBusy('skip'); if (await actions.bandSkip()) setSkipped(true); setBusy(null); }; const turnName = players.find((p) => p.playerId === draft.turnPid)?.name; const turnDeadline = Number(pub?.draft?.turnDeadline) || 0; useTicker(!solo && turnDeadline > 0 ? 250 : 0); const turnSecs = !solo && turnDeadline > 0 ? secondsLeft(turnDeadline) : null; return html`
<${StepHeader} step=${2} of=${2} title="选择策略" micro="STRATEGY // BAND CHECK" pub=${pub} total=${phaseTotalSeconds(pub, gd.config)} onExit=${() => setExit(true)} />
${bands.map((b) => { const who = pickers.get(b.bandId) || []; const isTaken = taken.has(b.bandId); return html``; })}
<${ExitModal} open=${exit} onClose=${() => setExit(false)} solo=${solo} />
`; }