/** * 초보자 도움 (docs/07 §5): "30초 규칙 요약" cards (first game of a kind on this device) and the * in-game [?] sheet ("지금 할 수 있는 것" + full rules + summary again). Content lives in each game's chunk. */ import { useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import type { PublicUser, RoomView } from '@bg/shared/lite'; import { Sheet } from './ui'; import { useRoom } from '../net/socket'; import type { GameUI, QuickCard } from '../games/types'; const seenKey = (gameId: string) => `bg:qs:${gameId}`; export function quickStartSeen(gameId: string): boolean { try { return localStorage.getItem(seenKey(gameId)) === '1'; } catch { return true; // storage blocked: don't nag every game } } function markQuickStartSeen(gameId: string) { try { localStorage.setItem(seenKey(gameId), '1'); } catch { /* ignore */ } } /** The 3 summary cards with [다음] / [건너뛰기] / [시작하기]. */ export function QuickStartCards({ title, cards, onClose, finish = '시작하기' }: { title: string; cards: QuickCard[]; onClose: () => void; finish?: string }) { const [i, setI] = useState(0); const card = cards[i]!; const last = i === cards.length - 1; return ( {title} 30초 규칙 요약 · {i + 1}/{cards.length} {card.pic} {card.title} {card.text} {cards.map((_, k) => ( ))} {last ? ( {finish} ) : ( 건너뛰기 setI(i + 1)}> 다음 → )} ); } /** "30초 요약 보기" button for places outside a game (rules page). */ export function QuickStartButton({ title, cards }: { title: string; cards: QuickCard[] | undefined }) { const [open, setOpen] = useState(false); const close = useCallback(() => setOpen(false), []); if (!cards?.length) return null; return ( <> setOpen(true)}> 🃏 30초 요약 보기 {open && } > ); } /** * [?] button next to the turn status while a game is on screen. Also opens the summary cards by itself * the first time this device sees a running game of this kind. */ export function GameHelp({ room, ui, title, me, players }: { room: RoomView; ui: GameUI; title: string; me: string; players: Record }) { const view = useRoom((s) => s.game?.view); const [open, setOpen] = useState(false); const [rules, setRules] = useState(false); const [cards, setCards] = useState(false); const playing = room.status === 'playing'; const gameId = room.gameId; useEffect(() => { if (playing && ui.quickStart?.length && !quickStartSeen(gameId)) setCards(true); }, [playing, gameId, ui]); // Stable callbacks: Sheet re-runs its focus effect when onClose changes (every game update otherwise). const closeCards = useCallback(() => { markQuickStartSeen(gameId); setCards(false); }, [gameId]); const closeHelp = useCallback(() => setOpen(false), []); const seated = room.seats.some((s) => s?.user.id === me); const now = room.status === 'paused' ? '오류로 게임이 멈췄어요. 방장이 [게임 끝내기] 버튼으로 끝낼 수 있어요.' : view === undefined ? '' : (ui.helpNow ?? ui.statusText)(view, me, players); return ( <> (setRules(false), setOpen(true))} aria-label="도움말: 지금 할 수 있는 것" aria-haspopup="dialog"> ? 도움 {open && createPortal( 지금 할 수 있는 것 {!seated && 지금은 구경하고 있어요. 게임에는 손댈 수 없어요.} {now} setRules(!rules)}> 📖 {rules ? '규칙 접기' : '전체 규칙 보기'} {rules && ( )} {!!ui.quickStart?.length && ( (setOpen(false), setCards(true))}> 🃏 30초 요약 다시 보기 )} setOpen(false)}> 닫기 , document.body, )} {cards && ui.quickStart && createPortal(, document.body)} > ); }
{title} 30초 규칙 요약 · {i + 1}/{cards.length}
{card.text}
지금은 구경하고 있어요. 게임에는 손댈 수 없어요.
{now}