// 玩法说明 (How to play): viewer for the 19 official tutorial pages of the mode (local-client art, DESIGN §13: // data/local-assets.json → guide/autochess_{home 1–9, shop 1–6, handbook 1–4}). The pages are stored squashed to // 1024² and are displayed stretched back to 16:9. Three chapters (基础规则 / 调度手册 / 进阶图鉴), ‹ › buttons, // ←/→ (A/D) keys, page dots, thumbnails; Esc or the backdrop closes. Adjacent pages are preloaded. // Without the local art the viewer shows the official loading-screen tips (config.tips) instead. // // Global & imperative so every screen can open it: `openGuide(page?)`; is mounted once by main.js // (and by the dev mock harness); is the standard trigger (title, lobby, room, in-match menu). import { useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.js'; import { html, Icon, MicroLabel, Button, Spinner } from './components.js'; import { createStore, useStore } from '../store.js'; import { data, useData, localAsset } from '../data.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); /** Page catalogue in reading order (titles transcribed from the pages). */ export const GUIDE_CHAPTERS = [ { id: 'home', name: '基础规则', micro: 'BASICS', pages: [ ['autochess_home_1', '卫戍协议已运行'], ['autochess_home_2', '攻防战'], ['autochess_home_3', '休整期 · 区域'], ['autochess_home_4', '休整期 · 资金与调度'], ['autochess_home_5', '机变阶段'], ['autochess_home_6', '限时战斗'], ['autochess_home_7', '作战期'], ['autochess_home_8', '协同战斗'], ['autochess_home_9', '盟约'], ] }, { id: 'shop', name: '调度手册', micro: 'HANDBOOK', pages: [ ['autochess_shop_1', '调度手册'], ['autochess_shop_2', '干员晋级'], ['autochess_shop_3', '加成情况'], ['autochess_shop_4', '卫戍能力'], ['autochess_shop_5', '盟约'], ['autochess_shop_6', '助战及自选编队'], ] }, { id: 'handbook', name: '进阶图鉴', micro: 'ADVANCED', pages: [ ['autochess_handbook_1', '敌人类型'], ['autochess_handbook_2', '盟约激活与叠加'], ['autochess_handbook_3', '追加盟约'], ['autochess_handbook_4', '策略与轮选'], ] }, ]; /** * Flat page list with URLs (only pages the local manifest lists). * @returns {Array<{ key: string, title: string, chapter: number, url: string }>} */ export function guidePages() { const out = []; GUIDE_CHAPTERS.forEach((ch, ci) => { for (const [key, title] of ch.pages) { const url = localAsset('guide', key); if (url) out.push({ key, title, chapter: ci, url }); } }); return out; } /** Open/closed + current page. */ export const guideStore = createStore({ open: false, page: 0 }); /** Open the viewer (optionally at a page index). */ export function openGuide(page = 0) { data.load('local'); data.load('config'); guideStore.set({ open: true, page: Math.max(0, page | 0) }); } export const closeGuide = () => guideStore.set({ open: false }); /** Standard 玩法说明 trigger button. */ export function GuideButton({ class: cls, size = 'sm', variant = 'ghost', label = '玩法说明', square = false }) { return html`<${Button} variant=${variant} size=${size} icon="book" square=${square} class=${cx('guide-btn', cls)} onClick=${() => openGuide(0)} title="玩法说明" aria-label="玩法说明">${square ? null : label}`; } function preload(url) { if (!url || typeof Image === 'undefined') return; const img = new Image(); img.decoding = 'async'; img.src = url; } /** Fallback body when the tutorial art is not installed: the official tips as a numbered list. */ function TipsFallback() { const tips = (Array.isArray(data.get('config')?.tips) ? data.get('config').tips : []).map((t) => t?.tip).filter(Boolean); return html`
<${MicroLabel} tone="mint">TIPS // 模拟要点
    ${tips.map((t, i) => html`
  1. ${t}
  2. `)}
${!tips.length ? html`

暂无说明内容

` : null}
`; } /** The viewer (mounted once near the root). */ export function GuideHost() { const { open, page } = useStore((s) => s, Object.is, guideStore); const ready = useData('local', 'config'); const pages = useMemo(() => (ready ? guidePages() : []), [ready]); const [loaded, setLoaded] = useState(() => new Set()); const [failed, setFailed] = useState(() => new Set()); const boxRef = useRef(null); const n = pages.length; const i = n ? Math.min(Math.max(0, page), n - 1) : 0; const cur = pages[i] || null; const go = (k) => { if (n) guideStore.set({ open: true, page: ((k % n) + n) % n }); }; useEffect(() => { if (!open) return undefined; const onKey = (e) => { if (e.ctrlKey || e.metaKey || e.altKey) return; const k = e.key; let handled = true; if (k === 'Escape') closeGuide(); else if (k === 'ArrowRight' || k === 'd' || k === 'D' || k === 'PageDown') go(guideStore.get().page + 1); else if (k === 'ArrowLeft' || k === 'a' || k === 'A' || k === 'PageUp') go(guideStore.get().page - 1); else if (k === 'Home') go(0); else if (k === 'End') go(n - 1); else handled = (!!k && k.length === 1) || k === ' '; // swallow game shortcuts (R/F/D/Space) while open if (handled) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } }; window.addEventListener('keydown', onKey, true); const t = setTimeout(() => boxRef.current?.focus?.(), 30); return () => { window.removeEventListener('keydown', onKey, true); clearTimeout(t); }; }, [open, n]); useEffect(() => { if (!open || !n) return; preload(pages[(i + 1) % n]?.url); preload(pages[(i + n - 1) % n]?.url); }, [open, i, n]); if (!open) return null; const chapter = cur ? GUIDE_CHAPTERS[cur.chapter] : null; const firstOf = (ci) => pages.findIndex((p) => p.chapter === ci); const isLoaded = cur && loaded.has(cur.url); return html``; }