- 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.
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { useEffect, useState } from 'react'
|
|
import { Image, Video } from '@/components'
|
|
import {
|
|
BeijingUnlockInteraction,
|
|
InputBg,
|
|
PianoUnlockInteraction,
|
|
PostcardUnlockInteraction,
|
|
SelectBg,
|
|
} from '../components'
|
|
import { getStaticImage } from '@/lib/utils'
|
|
import { LOBBY_COLUMNS, SCENE_TITLES, UNLOCK_TITLES } from './lobby.config'
|
|
import ContentDialog from './ContentDialog'
|
|
import UnlockHintDialog from './UnlockHintDialog'
|
|
import type { DialogState } from './useDialogReducer'
|
|
|
|
interface SharePayload {
|
|
sceneId?: number
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface DialogLayerProps {
|
|
state: DialogState
|
|
onEnterUnlock: () => void
|
|
onShare: (sceneId: number, payload?: SharePayload) => void
|
|
onFinishBadgeVideo: () => void
|
|
onClose: () => void
|
|
}
|
|
|
|
interface DialogLocalState {
|
|
selectedLyric?: string
|
|
guizhouInput: string
|
|
pianoFinished: boolean
|
|
selectedQuote?: string
|
|
postcardFinished: boolean
|
|
beijingFinished: boolean
|
|
postcardState: {
|
|
hasPlane: boolean
|
|
hasText: boolean
|
|
postcardImagePath: string
|
|
}
|
|
beijingSelection: {
|
|
paradeId?: string
|
|
paradeName?: string
|
|
paradeImagePath?: string
|
|
sloganId?: string
|
|
sloganImagePath?: string
|
|
previewImageDataUrl?: string
|
|
}
|
|
}
|
|
|
|
const starName = 'nieer'
|
|
const DEFAULT_POSTCARD_IMAGE_PATH = 'nieer/reflection/lobby/content/场景5待交互'
|
|
|
|
const ANTHEM_LYRICS = [
|
|
{ value: '1', label: '起来!不愿做奴隶的人们!' },
|
|
{ value: '2', label: '把我们的血肉,筑成我们新的长城!' },
|
|
{ value: '3', label: '中华民族到了最危险的时候' },
|
|
{ value: '4', label: '每个人被迫着发出最后的吼声' },
|
|
{ value: '5', label: '起来!起来!起来!' },
|
|
{ value: '6', label: '我们万众一心' },
|
|
{ value: '7', label: '冒着敌人的炮火,前进!' },
|
|
{ value: '8', label: '前进!前进!进!' },
|
|
]
|
|
|
|
const SMEDLEY_QUOTES = [
|
|
{ value: '1', label: '建设一个强大的和自由的中国' },
|
|
{ value: '2', label: '为我的遗体唱一首歌,中国的国歌……"起来"' },
|
|
{ value: '3', label: '我的心灵在这个世界上除了中国的任何地方都未能找到安宁' },
|
|
]
|
|
|
|
const getCanShare = (sceneId: number, localState: DialogLocalState): boolean => {
|
|
switch (sceneId) {
|
|
case 1:
|
|
return !!localState.selectedLyric
|
|
case 2:
|
|
return localState.guizhouInput.trim().length > 0
|
|
case 3:
|
|
return localState.pianoFinished
|
|
case 4:
|
|
return !!localState.selectedQuote
|
|
case 5:
|
|
return localState.postcardFinished
|
|
case 6:
|
|
return (
|
|
localState.beijingFinished &&
|
|
typeof localState.beijingSelection.previewImageDataUrl === 'string'
|
|
)
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
const buildSharePayload = (
|
|
sceneId: number,
|
|
localState: DialogLocalState
|
|
): SharePayload | undefined => {
|
|
switch (sceneId) {
|
|
case 1: {
|
|
const selected = ANTHEM_LYRICS.find((item) => item.value === localState.selectedLyric)
|
|
return {
|
|
sceneId: 1,
|
|
lyricValue: localState.selectedLyric ?? '',
|
|
lyricLabel: selected?.label ?? '',
|
|
}
|
|
}
|
|
case 2:
|
|
return {
|
|
sceneId: 2,
|
|
reflectionText: localState.guizhouInput.trim(),
|
|
}
|
|
case 3:
|
|
return {
|
|
sceneId: 3,
|
|
pianoCompleted: localState.pianoFinished,
|
|
pianoImagePath: 'nieer/reflection/lobby/box/音符',
|
|
}
|
|
case 4: {
|
|
const selected = SMEDLEY_QUOTES.find((item) => item.value === localState.selectedQuote)
|
|
return {
|
|
sceneId: 4,
|
|
quoteValue: localState.selectedQuote ?? '',
|
|
quoteLabel: selected?.label ?? '',
|
|
}
|
|
}
|
|
case 5:
|
|
return {
|
|
sceneId: 5,
|
|
...localState.postcardState,
|
|
}
|
|
case 6:
|
|
return {
|
|
sceneId: 6,
|
|
...localState.beijingSelection,
|
|
}
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
interface UnlockContentHandlers {
|
|
onSelectLyric: (value: string) => void
|
|
onChangeGuizhouInput: (value: string) => void
|
|
onFinishPiano: () => void
|
|
onSelectQuote: (value: string) => void
|
|
onFinishPostcard: () => void
|
|
onChangePostcardState: (payload: DialogLocalState['postcardState']) => void
|
|
onFinishBeijing: () => void
|
|
onChangeBeijingSelection: (payload: DialogLocalState['beijingSelection']) => void
|
|
}
|
|
|
|
const renderUnlockContent = (
|
|
sceneId: number,
|
|
localState: DialogLocalState,
|
|
handlers: UnlockContentHandlers
|
|
) => {
|
|
switch (sceneId) {
|
|
case 1:
|
|
return (
|
|
<div className="flex flex-col items-center justify-center mt-12">
|
|
<Image
|
|
className="mb-20"
|
|
src={getStaticImage(`${starName}/reflection/lobby/content/场景1交互`)}
|
|
alt="场景1交互"
|
|
/>
|
|
<SelectBg
|
|
options={ANTHEM_LYRICS}
|
|
value={localState.selectedLyric || ''}
|
|
onChange={handlers.onSelectLyric}
|
|
placeholder="请选择一句国歌歌词"
|
|
/>
|
|
</div>
|
|
)
|
|
case 2:
|
|
return (
|
|
<div className="flex flex-col items-center justify-center mt-12">
|
|
<Image
|
|
className="mb-20"
|
|
src={getStaticImage(`${starName}/reflection/lobby/content/场景2交互`)}
|
|
alt="场景2交互"
|
|
/>
|
|
<InputBg
|
|
className="w-[875px]"
|
|
value={localState.guizhouInput}
|
|
onChange={handlers.onChangeGuizhouInput}
|
|
placeholder="请输入你的爱国感悟(10字以内)"
|
|
maxLength={10}
|
|
/>
|
|
</div>
|
|
)
|
|
case 3:
|
|
return (
|
|
<div className="flex flex-col items-center justify-center mt-12">
|
|
<PianoUnlockInteraction onFinished={handlers.onFinishPiano} />
|
|
</div>
|
|
)
|
|
case 4:
|
|
return (
|
|
<div className="flex flex-col items-center justify-center mt-12">
|
|
<Image
|
|
className="mb-20"
|
|
src={getStaticImage(`${starName}/reflection/lobby/content/场景4交互`)}
|
|
alt="场景4交互"
|
|
/>
|
|
<SelectBg
|
|
options={SMEDLEY_QUOTES}
|
|
value={localState.selectedQuote || ''}
|
|
onChange={handlers.onSelectQuote}
|
|
placeholder="请选择一句最触动你的遗言"
|
|
/>
|
|
</div>
|
|
)
|
|
case 5:
|
|
return (
|
|
<PostcardUnlockInteraction
|
|
onFinished={handlers.onFinishPostcard}
|
|
onStateChange={handlers.onChangePostcardState}
|
|
/>
|
|
)
|
|
case 6:
|
|
return (
|
|
<BeijingUnlockInteraction
|
|
onFinished={handlers.onFinishBeijing}
|
|
onSelectionChange={handlers.onChangeBeijingSelection}
|
|
/>
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const createUnlockHandlers = (
|
|
setSelectedLyric: (value: string) => void,
|
|
setGuizhouInput: (value: string) => void,
|
|
setPianoFinished: (value: boolean) => void,
|
|
setSelectedQuote: (value: string) => void,
|
|
setPostcardFinished: (value: boolean) => void,
|
|
setPostcardState: (payload: DialogLocalState['postcardState']) => void,
|
|
setBeijingFinished: (value: boolean) => void,
|
|
setBeijingSelection: (payload: DialogLocalState['beijingSelection']) => void
|
|
): UnlockContentHandlers => ({
|
|
onSelectLyric: setSelectedLyric,
|
|
onChangeGuizhouInput: (value) => setGuizhouInput(value.slice(0, 10)),
|
|
onFinishPiano: () => setPianoFinished(true),
|
|
onSelectQuote: setSelectedQuote,
|
|
onFinishPostcard: () => setPostcardFinished(true),
|
|
onChangePostcardState: setPostcardState,
|
|
onFinishBeijing: () => setBeijingFinished(true),
|
|
onChangeBeijingSelection: setBeijingSelection,
|
|
})
|
|
|
|
const DialogLayer = ({
|
|
state,
|
|
onEnterUnlock,
|
|
onShare,
|
|
onFinishBadgeVideo,
|
|
onClose,
|
|
}: DialogLayerProps) => {
|
|
const [selectedLyric, setSelectedLyric] = useState<string>()
|
|
const [guizhouInput, setGuizhouInput] = useState('')
|
|
const [pianoFinished, setPianoFinished] = useState(false)
|
|
const [selectedQuote, setSelectedQuote] = useState<string>()
|
|
const [postcardFinished, setPostcardFinished] = useState(false)
|
|
const [beijingFinished, setBeijingFinished] = useState(false)
|
|
const [postcardState, setPostcardState] = useState({
|
|
hasPlane: false,
|
|
hasText: false,
|
|
postcardImagePath: DEFAULT_POSTCARD_IMAGE_PATH,
|
|
})
|
|
const [beijingSelection, setBeijingSelection] = useState<DialogLocalState['beijingSelection']>({})
|
|
|
|
const dialogNonce = state.mode === 'idle' ? 0 : state.nonce
|
|
|
|
useEffect(() => {
|
|
setSelectedLyric(undefined)
|
|
setGuizhouInput('')
|
|
setPianoFinished(false)
|
|
setSelectedQuote(undefined)
|
|
setPostcardFinished(false)
|
|
setBeijingFinished(false)
|
|
setPostcardState({
|
|
hasPlane: false,
|
|
hasText: false,
|
|
postcardImagePath: DEFAULT_POSTCARD_IMAGE_PATH,
|
|
})
|
|
setBeijingSelection({})
|
|
}, [dialogNonce])
|
|
|
|
if (state.mode === 'idle') return <AnimatePresence mode="sync" />
|
|
|
|
const scene = LOBBY_COLUMNS.find((item) => item.id === state.sceneId)
|
|
if (!scene) return null
|
|
|
|
const localState: DialogLocalState = {
|
|
selectedLyric,
|
|
guizhouInput,
|
|
pianoFinished,
|
|
selectedQuote,
|
|
postcardFinished,
|
|
beijingFinished,
|
|
postcardState,
|
|
beijingSelection,
|
|
}
|
|
|
|
const canShare = getCanShare(state.sceneId, localState)
|
|
const unlockHandlers = createUnlockHandlers(
|
|
setSelectedLyric,
|
|
setGuizhouInput,
|
|
setPianoFinished,
|
|
setSelectedQuote,
|
|
setPostcardFinished,
|
|
setPostcardState,
|
|
setBeijingFinished,
|
|
setBeijingSelection
|
|
)
|
|
|
|
const handleShareClick = () => {
|
|
if (state.mode !== 'unlock') return
|
|
onShare(state.sceneId, buildSharePayload(state.sceneId, localState))
|
|
}
|
|
|
|
return (
|
|
<AnimatePresence mode="sync">
|
|
{state.mode === 'result' && (
|
|
<motion.div
|
|
key={`result-${state.sceneId}-${state.nonce}`}
|
|
className="absolute w-full h-full z-99"
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
|
>
|
|
<div className="relative w-full h-full">
|
|
<Image
|
|
src={getStaticImage(
|
|
`${starName}/reflection/lobby/result/聂耳结束场景${state.sceneId}`
|
|
)}
|
|
alt="结果页"
|
|
/>
|
|
{state.videoSrc && (
|
|
<div className="absolute inset-0 z-20 bg-black">
|
|
<Video
|
|
key={state.videoSrc}
|
|
src={state.videoSrc}
|
|
autoPlay
|
|
playsInline
|
|
className="w-full h-full"
|
|
onSkip={onFinishBadgeVideo}
|
|
onEnded={onFinishBadgeVideo}
|
|
/>
|
|
</div>
|
|
)}
|
|
<Image
|
|
button
|
|
src={getStaticImage('components/关闭')}
|
|
className="absolute right-6 top-5 z-30"
|
|
alt="关闭"
|
|
onClick={onClose}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{state.mode === 'unlock' && (
|
|
<motion.div
|
|
key={`unlock-${state.sceneId}-${state.hasShare}-${state.nonce}`}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
|
className="absolute inset-0 z-40"
|
|
>
|
|
<UnlockHintDialog
|
|
starName={starName}
|
|
title={UNLOCK_TITLES[state.sceneId]}
|
|
visible
|
|
onClose={onClose}
|
|
onShare={state.hasShare && canShare ? handleShareClick : undefined}
|
|
>
|
|
{renderUnlockContent(state.sceneId, localState, unlockHandlers)}
|
|
</UnlockHintDialog>
|
|
</motion.div>
|
|
)}
|
|
|
|
{state.mode === 'content' && (
|
|
<ContentDialog
|
|
key={`content-${state.sceneId}-${state.nonce}`}
|
|
starName={starName}
|
|
title={SCENE_TITLES[state.sceneId]}
|
|
scene={scene}
|
|
onClose={onClose}
|
|
onEnterScene={onEnterUnlock}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
export default DialogLayer
|