import { puzzleDataConfig } from './puzzleData' import { PUZZLE_RESIDENCES } from './puzzleResidences' export type PuzzleProgress = Record> const getResidenceIndex = (residence: string) => PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence) const getConfiguredPieceIds = (residence: string) => { const pieces = puzzleDataConfig[residence] if (!pieces) return [] return Object.keys(pieces).map(Number) } const arePiecesUnlocked = (residence: string, puzzles: PuzzleProgress, pieceIds: number[]) => { if (pieceIds.length === 0) return false const unlockedPieces = puzzles[residence] if (!unlockedPieces) return false return pieceIds.every((pieceId) => unlockedPieces.has(pieceId)) } export const isResidenceStarted = (residence: string, puzzles: PuzzleProgress) => { return (puzzles[residence]?.size ?? 0) > 0 } export const isResidenceComplete = (residence: string, puzzles: PuzzleProgress) => { return arePiecesUnlocked(residence, puzzles, getConfiguredPieceIds(residence)) } export const isResidenceUnlocked = (residence: string, puzzles: PuzzleProgress) => { const index = getResidenceIndex(residence) if (index === 0) return true if (index < 0) return false const previousResidence = PUZZLE_RESIDENCES[index - 1] if (!previousResidence) return false return isResidenceComplete(previousResidence.residence, puzzles) } export const isMenuModuleUnlocked = (moduleName: string, puzzles: PuzzleProgress) => { return PUZZLE_RESIDENCES.some( (item) => item.moduleName === moduleName && isResidenceUnlocked(item.residence, puzzles) ) } export const isMenuModuleStarted = (moduleName: string, puzzles: PuzzleProgress) => { return PUZZLE_RESIDENCES.some( (item) => item.moduleName === moduleName && isResidenceStarted(item.residence, puzzles) ) } export const isMenuModuleComplete = (moduleName: string, puzzles: PuzzleProgress) => { const residences = PUZZLE_RESIDENCES.filter((item) => item.moduleName === moduleName) if (residences.length === 0) return false return residences.every((item) => isResidenceComplete(item.residence, puzzles)) } const getPieceIdsByRoute = (residence: string, routePrefix: string) => { const pieces = puzzleDataConfig[residence] if (!pieces) return [] return Object.entries(pieces) .filter(([, piece]) => piece.route.startsWith(routePrefix)) .map(([pieceId]) => Number(pieceId)) } export const getResidenceStageUnlocks = (residence: string, puzzles: PuzzleProgress) => { const residenceUnlocked = isResidenceUnlocked(residence, puzzles) return { dialogueUnlocked: residenceUnlocked, reflectionUnlocked: residenceUnlocked && arePiecesUnlocked(residence, puzzles, getPieceIdsByRoute(residence, 'place')), internalizationUnlocked: residenceUnlocked && arePiecesUnlocked(residence, puzzles, getPieceIdsByRoute(residence, 'reflection')), } } export interface ResidenceStageCompletion { dialogueComplete: boolean reflectionComplete: boolean internalizationComplete: boolean residenceComplete: boolean } /** * 基于拼图碎片统一判定每个故居三阶段是否完成。 * 每个故居 16 块碎片天然按 route 分为 place* / reflection* / internalization 三段, * 收齐对应碎片即视为该阶段完成。 */ export const getResidenceStageCompletion = ( residence: string, puzzles: PuzzleProgress ): ResidenceStageCompletion => { const dialogueComplete = arePiecesUnlocked( residence, puzzles, getPieceIdsByRoute(residence, 'place') ) const reflectionComplete = arePiecesUnlocked( residence, puzzles, getPieceIdsByRoute(residence, 'reflection') ) const internalizationComplete = arePiecesUnlocked( residence, puzzles, getPieceIdsByRoute(residence, 'internalization') ) return { dialogueComplete, reflectionComplete, internalizationComplete, residenceComplete: dialogueComplete && reflectionComplete && internalizationComplete, } } export interface ResidenceContinueInput { completion: ResidenceStageCompletion /** 对话模块断点(place1~placeN),各故居 store 均有 */ lastVisitedPlace?: string | null /** 体悟模块断点路由,仅部分多场景故居(孙中山/鲁迅/黄炎培)有 */ reflectionRoute?: string | null } /** * 计算「继续学习」应跳转到的目标路由——取第一个未完成的阶段。 * - 对话未完成 → lastVisitedPlace || 'place1' * - 体悟未完成 → reflectionRoute || 'reflection' * - 内化未完成 → 'internalization' * - 全部完成 → null(无需续学) * * 这些路由都是各故居 home/场景现有逻辑已经在用的跳转目标, * 单场景体悟(毛泽东/陈云/聂耳/荣德生)统一落到 'reflection' 由页面自管进度。 */ export const resolveResidenceContinueTarget = ({ completion, lastVisitedPlace, reflectionRoute, }: ResidenceContinueInput): string | null => { if (!completion.dialogueComplete) { return lastVisitedPlace || 'place1' } if (!completion.reflectionComplete) { return reflectionRoute || 'reflection' } if (!completion.internalizationComplete) { return 'internalization' } return null }