diff --git a/public/css/screens/guide.css b/public/css/screens/guide.css index 665d53f1..21e7dbd7 100644 --- a/public/css/screens/guide.css +++ b/public/css/screens/guide.css @@ -61,8 +61,6 @@ .guide__page img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; user-select: none; opacity: 0; transition: opacity 160ms ease; } .guide__page.is-loaded img { opacity: 1; } .guide__loading { position: absolute; inset: 0; display: grid; place-items: center; } -.guide__missing { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: .1rem; color: var(--text-lo); font-size: .2rem; } -.guide__missing .icon { width: .3rem; height: .3rem; } .guide__nav { position: absolute; top: 0; bottom: 0; z-index: 2; width: 1rem; display: grid; place-items: center; color: rgba(242, 242, 242, .7); background: transparent; border: 0; @@ -93,7 +91,7 @@ .guide__dot.is-on { width: .42rem; background: var(--mint-500); box-shadow: 0 0 .08rem rgba(23, 249, 183, .5); } .guide__count { justify-self: end; font-size: .24rem; color: var(--text-md); letter-spacing: .06em; } -/* text fallback (no local tutorial art) */ +/* text fallback: the official tips when no tutorial page can be shown (none listed, or none of a page's copies loads) */ .guide__stage--text { width: min(92vw, 10rem); max-height: calc(100vh - 2rem); overflow: auto; padding: .1rem .06rem; } .guide__tips ol { margin: .14rem 0 0; padding-left: .34rem; display: flex; flex-direction: column; gap: .08rem; } .guide__tips li { font-size: .18rem; line-height: 1.6; color: var(--text-md); } diff --git a/public/js/ui/guide.js b/public/js/ui/guide.js index 156b5827..abffa869 100644 --- a/public/js/ui/guide.js +++ b/public/js/ui/guide.js @@ -4,7 +4,10 @@ // whose picture fails to load tries its next URL. The pages are stored squashed to 1024² (both copies) 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. -// With neither copy the viewer shows the official loading-screen tips (config.tips) instead. +// With no page listed the viewer shows the official loading-screen tips (config.tips) instead, and so does a page none +// of whose copies loads (`guideStage`: data/assets.json lists the downloaded pages but the files are not on disk — a +// `git pull` and restart without setup, an older asset folder, a failed download the shrink guard kept); the chapter +// tabs, the page dots and the keys still reach the other pages. // // 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). @@ -49,6 +52,19 @@ export function guidePages() { return out; } +/** + * What the viewer's stage shows for a page: `{ kind: 'image', src }` — the first of its URLs that has not failed to + * load (the local file, then the downloaded copy) — or `{ kind: 'tips' }`, the official tips text, when none is left + * (no page, or every copy failed to load). + * @param {{ urls?: string[] }|null|undefined} page an entry of guidePages() + * @param {Set} [failed] URLs whose image fired an error + * @returns {{ kind: 'image', src: string } | { kind: 'tips' }} + */ +export function guideStage(page, failed) { + const src = page ? nextArtUrl(page.urls, failed) : null; + return src ? { kind: 'image', src } : { kind: 'tips' }; +} + /** Open/closed + current page. */ export const guideStore = createStore({ open: false, page: 0 }); @@ -75,7 +91,7 @@ function preload(url) { img.src = url; } -/** Fallback body when the tutorial art is not installed: the official tips as a numbered list. */ +/** Fallback body when no tutorial page can be shown: 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`
@@ -119,14 +135,15 @@ export function GuideHost() { useEffect(() => { if (!open || !n) return; - preload(pages[(i + 1) % n]?.url); - preload(pages[(i + n - 1) % n]?.url); - }, [open, i, n]); + preload(nextArtUrl(pages[(i + 1) % n]?.urls, failed)); + preload(nextArtUrl(pages[(i + n - 1) % n]?.urls, failed)); + }, [open, i, n, failed]); if (!open) return null; const chapter = cur ? GUIDE_CHAPTERS[cur.chapter] : null; const firstOf = (ci) => pages.findIndex((p) => p.chapter === ci); - const src = cur ? nextArtUrl(cur.urls, failed) : null; // the next copy when one fails to load + const stage = guideStage(cur, failed); // the next copy when one fails to load; the tips when none is left + const src = stage.kind === 'image' ? stage.src : null; const isLoaded = !!src && loaded.has(src); return html`