- Introduced `puzzleResidences.ts` to define puzzle residence configurations including properties like display name, position, and puzzle availability. - Added `reflectionProgress.ts` to manage badge unlocking logic based on place completion, including functions to determine unlocked badges and video sources for scenes.
258 lines
9.6 KiB
TypeScript
258 lines
9.6 KiB
TypeScript
import { Image, StudyGuideModal } from '@/components'
|
|
import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
|
import { useMemo } from 'react'
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
import { LOBBY_COLUMNS, LOBBY_LAYOUT, LOBBY_TIMELINE } from './lobby.config'
|
|
import { useNieerStore } from '@/store/nieerStore'
|
|
import { useDialogReducer } from './useDialogReducer'
|
|
import DialogLayer from './DialogLayer'
|
|
import {
|
|
BADGE_CONFIG,
|
|
getAllPlacesCompleted,
|
|
getBadgeUnlockedMap,
|
|
getGuideSceneId,
|
|
getPlaceCompletionMap,
|
|
getUnlockVideoSrcForScene,
|
|
getUnlockedByIdMap,
|
|
} from './reflectionProgress'
|
|
|
|
const starName = 'nieer'
|
|
|
|
const NieerReflection = () => {
|
|
const navigate = useNavigate()
|
|
const { moduleName } = useParams<{ moduleName: string }>()
|
|
const debugLobby = new URLSearchParams(window.location.search).get('debugLobby') === '1'
|
|
const visitedReflectionCursors = useNieerStore((state) => state.visitedReflectionCursors)
|
|
const markReflectionCursorVisited = useNieerStore((state) => state.markReflectionCursorVisited)
|
|
const setReflectionWork = useNieerStore((state) => state.setReflectionWork)
|
|
const hasSeenReflectionGuide = useNieerStore((state) => state.hasSeenReflectionGuide)
|
|
const markReflectionGuideSeen = useNieerStore((state) => state.markReflectionGuideSeen)
|
|
const showGuideModal = !hasSeenReflectionGuide
|
|
|
|
const {
|
|
state: dialogState,
|
|
openScene,
|
|
enterUnlock,
|
|
handleShare,
|
|
finishBadgeVideo,
|
|
close,
|
|
} = useDialogReducer(markReflectionCursorVisited, setReflectionWork)
|
|
|
|
const placeCompletion = useMemo(
|
|
() => getPlaceCompletionMap(visitedReflectionCursors),
|
|
[visitedReflectionCursors]
|
|
)
|
|
const unlockedById = useMemo(() => getUnlockedByIdMap(placeCompletion), [placeCompletion])
|
|
const badgeUnlocked = useMemo(() => getBadgeUnlockedMap(placeCompletion), [placeCompletion])
|
|
const guideSceneId = useMemo(() => getGuideSceneId(placeCompletion), [placeCompletion])
|
|
const allPlacesCompleted = useMemo(
|
|
() => getAllPlacesCompleted(placeCompletion),
|
|
[placeCompletion]
|
|
)
|
|
|
|
const handleResultShare = (sceneId: number, payload?: Record<string, unknown>) => {
|
|
handleShare({
|
|
payload,
|
|
unlockVideoSrc: getUnlockVideoSrcForScene(sceneId, visitedReflectionCursors),
|
|
})
|
|
}
|
|
|
|
if (!moduleName) return null
|
|
|
|
return (
|
|
<div
|
|
className="w-full h-full relative bg-cover bg-center bg-no-repeat overflow-hidden"
|
|
style={{ backgroundImage: getBackgroundImage(`${starName}/reflection/lobby/场景首页底图`) }}
|
|
>
|
|
<Image
|
|
className="absolute right-6 top-5 z-30"
|
|
src={getStaticImage('components/返回')}
|
|
button
|
|
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
'absolute h-full grid z-1',
|
|
LOBBY_LAYOUT.columnsTop,
|
|
LOBBY_LAYOUT.columnsLeft
|
|
)}
|
|
style={{
|
|
width: `${LOBBY_LAYOUT.columnsWidthPx}px`,
|
|
gridTemplateColumns: `repeat(${LOBBY_LAYOUT.columnCount}, minmax(0, 1fr))`,
|
|
}}
|
|
>
|
|
<img
|
|
className="absolute -left-65 top-0 z-2 w-[1651px] h-auto pointer-events-none"
|
|
src={getStaticImage(`${starName}/reflection/lobby/menu/琴弦`)}
|
|
alt="琴弦"
|
|
/>
|
|
{LOBBY_COLUMNS.map((scene) => {
|
|
const isUnlocked = unlockedById[scene.id as keyof typeof unlockedById]
|
|
const isCompleted = placeCompletion[scene.id as keyof typeof placeCompletion]
|
|
const iconSrc = isCompleted ? scene.icon : scene.iconBw
|
|
const isClickable = isUnlocked || scene.id === guideSceneId
|
|
const showGuideState = !allPlacesCompleted && scene.id === guideSceneId
|
|
|
|
return (
|
|
<button
|
|
key={scene.id}
|
|
type="button"
|
|
className={cn(
|
|
'relative w-[220px] h-full flex flex-col items-center',
|
|
!isClickable && 'pointer-events-none cursor-not-allowed',
|
|
showGuideState && 'border-l-6 border-r-6 border-white'
|
|
)}
|
|
onClick={() => {
|
|
if (!isClickable) return
|
|
openScene(scene.id, !!isUnlocked)
|
|
}}
|
|
>
|
|
<Image
|
|
className={cn('absolute inset-0 -z-1', !isUnlocked && 'brightness-50')}
|
|
src={getStaticImage(`${starName}/reflection/lobby/${scene.cityImage}`)}
|
|
alt={scene.city}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
'relative z-3',
|
|
'w-[233px] h-[233px] flex items-center justify-center -mt-13'
|
|
)}
|
|
>
|
|
<div className="relative w-[112px] h-[111px]">
|
|
{showGuideState && (
|
|
<span
|
|
className={cn(
|
|
' absolute -z-3 left-0 inline-flex h-full w-full rounded-full',
|
|
'bg-[#fcc419] opacity-100 animate-ping',
|
|
'border-2 border-white drop-shadow-2xl'
|
|
)}
|
|
/>
|
|
)}
|
|
<img
|
|
className="w-full"
|
|
src={getStaticImage(`${starName}/reflection/lobby/menu/${iconSrc}`)}
|
|
alt={scene.icon}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{showGuideState && (
|
|
<Image
|
|
className="animate-finger-bounce absolute bottom-[calc(50%-200px)] translate-y-1/2"
|
|
src={getStaticImage(`${starName}/reflection/lobby/icon/点击前往`)}
|
|
alt="点击前往"
|
|
/>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div
|
|
className={cn('absolute z-10 grid', LOBBY_LAYOUT.timelineTop, LOBBY_LAYOUT.columnsLeft)}
|
|
style={{
|
|
width: `${LOBBY_LAYOUT.columnsWidthPx}px`,
|
|
gridTemplateColumns: `repeat(${LOBBY_LAYOUT.columnCount}, minmax(0, 1fr))`,
|
|
}}
|
|
>
|
|
{LOBBY_TIMELINE.map((item) => {
|
|
if (item.type === 'single') {
|
|
return (
|
|
<div
|
|
key={`${item.type}-${item.col}-${item.label}`}
|
|
className="text-center text-4xl font-semibold text-white [text-shadow:0_0_20px_rgb(247_214_97_/0.8),0_0_20px_rgb(247_214_97/0.8)]"
|
|
style={{ gridColumn: `${item.col + 1} / span 1` }}
|
|
>
|
|
{item.label}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={`${item.type}-${item.colStart}-${item.colEnd}-${item.label}`}
|
|
className="flex items-center justify-center gap-3 text-4xl font-semibold text-white [text-shadow:0_0_20px_rgb(247_214_97_/0.8),0_0_20px_rgb(247_214_97/0.8)]"
|
|
style={{ gridColumn: `${item.colStart + 1} / ${item.colEnd + 2}` }}
|
|
>
|
|
{item.withLine && <span className="h-[2px] w-24 bg-[#f5e9c7]/80" />}
|
|
<span>{item.label}</span>
|
|
{item.withLine && <span className="h-[2px] w-24 bg-[#f5e9c7]/80" />}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div
|
|
className="absolute left-0 bottom-0 w-full h-[201px] z-20 flex justify-center items-center pt-20 gap-10"
|
|
style={{
|
|
backgroundImage: getBackgroundImage(`${starName}/reflection/lobby/footer`),
|
|
}}
|
|
>
|
|
<div className="relative w-340 h-full">
|
|
{BADGE_CONFIG.map((badge) => {
|
|
const isLit = badgeUnlocked[badge.id as keyof typeof badgeUnlocked]
|
|
return (
|
|
<div key={badge.id} className={cn('absolute w-[193px]', badge.className)}>
|
|
<Image
|
|
width={193}
|
|
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章底板`)}
|
|
alt="徽章底板"
|
|
/>
|
|
{isLit && (
|
|
<Image
|
|
className="absolute inset-0"
|
|
width={193}
|
|
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章${badge.id}`)}
|
|
alt={`徽章${badge.id}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<Image
|
|
button
|
|
src={getStaticImage(`${starName}/reflection/lobby/icon/旅行箱`)}
|
|
className="absolute right-0 bottom-0 z-30"
|
|
alt="旅行箱"
|
|
onClick={() => navigate(`/${moduleName}/${starName}/reflection/box`)}
|
|
/>
|
|
|
|
{debugLobby && (
|
|
<div
|
|
className="absolute inset-0 z-40 pointer-events-none"
|
|
style={{
|
|
background:
|
|
'linear-gradient(to right, rgba(255,0,0,0.25) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,255,255,0.18) 1px, transparent 1px)',
|
|
backgroundSize: '120px 100%, 100% 80px',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<DialogLayer
|
|
state={dialogState}
|
|
onEnterUnlock={enterUnlock}
|
|
onShare={handleResultShare}
|
|
onFinishBadgeVideo={finishBadgeVideo}
|
|
onClose={close}
|
|
/>
|
|
|
|
<StudyGuideModal
|
|
visible={showGuideModal}
|
|
firstLineIndent="2em"
|
|
onContinue={markReflectionGuideSeen}
|
|
audioSrc={getPublicPath('audio/nieer/mp3_nieer_28.mp3')}
|
|
>
|
|
{`同学你好,欢迎来到“跨越时空·体悟”学习模块!
|
|
请登陆“时空回溯・亲历时代”体验仓,下一站:1936年,上海。
|
|
请带上你的旅行包,穿越时空解锁6个国歌故事,集齐精神徽章,绘制属于你的国歌精神图谱吧~时空门已开,请出发吧!`}
|
|
</StudyGuideModal>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default NieerReflection
|