feat: add puzzle residence configuration and reflection progress logic
- 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.
This commit is contained in:
1 parent
e0fa47d21a
commit
f5f36f75b5
153 files changed
+3846
-2352
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Image } from '@/components'
|
||||
import { Image, Video } from '@/components'
|
||||
import {
|
||||
BeijingUnlockInteraction,
|
||||
InputBg,
|
||||
@@ -14,7 +14,43 @@ 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: '起来!不愿做奴隶的人们!' },
|
||||
@@ -33,41 +69,208 @@ const SMEDLEY_QUOTES = [
|
||||
{ value: '3', label: '我的心灵在这个世界上除了中国的任何地方都未能找到安宁' },
|
||||
]
|
||||
|
||||
interface DialogLayerProps {
|
||||
state: DialogState
|
||||
onEnterUnlock: () => void
|
||||
onShare: (payload?: Record<string, unknown>) => void
|
||||
onClose: () => void
|
||||
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 DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProps) => {
|
||||
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<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: boolean
|
||||
hasText: boolean
|
||||
postcardImagePath: string
|
||||
}>({
|
||||
const [postcardState, setPostcardState] = useState({
|
||||
hasPlane: false,
|
||||
hasText: false,
|
||||
postcardImagePath: 'nieer/reflection/lobby/content/场景5待交互',
|
||||
postcardImagePath: DEFAULT_POSTCARD_IMAGE_PATH,
|
||||
})
|
||||
const [beijingSelection, setBeijingSelection] = useState<{
|
||||
paradeId?: string
|
||||
paradeName?: string
|
||||
paradeImagePath?: string
|
||||
sloganId?: string
|
||||
sloganImagePath?: string
|
||||
previewImageDataUrl?: string
|
||||
}>({})
|
||||
const [beijingSelection, setBeijingSelection] = useState<DialogLocalState['beijingSelection']>({})
|
||||
|
||||
const dialogNonce = state.mode === 'idle' ? 0 : state.nonce
|
||||
|
||||
// 每次点击卡片(nonce 变化)时,重置 unlock 交互状态
|
||||
useEffect(() => {
|
||||
setSelectedLyric(undefined)
|
||||
setGuizhouInput('')
|
||||
@@ -78,84 +281,42 @@ const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProp
|
||||
setPostcardState({
|
||||
hasPlane: false,
|
||||
hasText: false,
|
||||
postcardImagePath: 'nieer/reflection/lobby/content/场景5待交互',
|
||||
postcardImagePath: DEFAULT_POSTCARD_IMAGE_PATH,
|
||||
})
|
||||
setBeijingSelection({})
|
||||
}, [dialogNonce])
|
||||
|
||||
if (state.mode === 'idle') return <AnimatePresence mode="sync" />
|
||||
|
||||
const scene = LOBBY_COLUMNS.find((s) => s.id === state.sceneId)
|
||||
const scene = LOBBY_COLUMNS.find((item) => item.id === state.sceneId)
|
||||
if (!scene) return null
|
||||
|
||||
let canShare = true
|
||||
if (state.sceneId === 1) {
|
||||
canShare = !!selectedLyric
|
||||
} else if (state.sceneId === 2) {
|
||||
canShare = guizhouInput.trim().length > 0
|
||||
} else if (state.sceneId === 3) {
|
||||
// 欧洲·亚洲:钢琴交互播放完成后才允许分享
|
||||
canShare = pianoFinished
|
||||
} else if (state.sceneId === 4) {
|
||||
canShare = !!selectedQuote
|
||||
} else if (state.sceneId === 5) {
|
||||
canShare = postcardFinished
|
||||
} else if (state.sceneId === 6) {
|
||||
canShare = beijingFinished && typeof beijingSelection.previewImageDataUrl === 'string'
|
||||
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
|
||||
|
||||
if (state.sceneId === 1) {
|
||||
const selected = ANTHEM_LYRICS.find((item) => item.value === selectedLyric)
|
||||
onShare({
|
||||
sceneId: 1,
|
||||
lyricValue: selectedLyric ?? '',
|
||||
lyricLabel: selected?.label ?? '',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (state.sceneId === 2) {
|
||||
onShare({
|
||||
sceneId: 2,
|
||||
reflectionText: guizhouInput.trim(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (state.sceneId === 3) {
|
||||
onShare({
|
||||
sceneId: 3,
|
||||
pianoCompleted: pianoFinished,
|
||||
pianoImagePath: 'nieer/reflection/lobby/box/音符',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (state.sceneId === 4) {
|
||||
const selected = SMEDLEY_QUOTES.find((item) => item.value === selectedQuote)
|
||||
onShare({
|
||||
sceneId: 4,
|
||||
quoteValue: selectedQuote ?? '',
|
||||
quoteLabel: selected?.label ?? '',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (state.sceneId === 5) {
|
||||
onShare({
|
||||
sceneId: 5,
|
||||
...postcardState,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (state.sceneId === 6) {
|
||||
onShare({
|
||||
sceneId: 6,
|
||||
...beijingSelection,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
onShare()
|
||||
onShare(state.sceneId, buildSharePayload(state.sceneId, localState))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -176,6 +337,19 @@ const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProp
|
||||
)}
|
||||
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/关闭')}
|
||||
@@ -203,82 +377,7 @@ const DialogLayer = ({ state, onEnterUnlock, onShare, onClose }: DialogLayerProp
|
||||
onClose={onClose}
|
||||
onShare={state.hasShare && canShare ? handleShareClick : undefined}
|
||||
>
|
||||
{state.sceneId === 1 && (
|
||||
<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={selectedLyric || ''}
|
||||
onChange={setSelectedLyric}
|
||||
placeholder="请选择一句国歌歌词"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.sceneId === 2 && (
|
||||
<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={guizhouInput}
|
||||
onChange={(value) => setGuizhouInput(value.slice(0, 10))}
|
||||
placeholder="请输入你的爱国感悟(10字以内)"
|
||||
maxLength={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.sceneId === 3 && (
|
||||
<div className="flex flex-col items-center justify-center mt-12">
|
||||
<PianoUnlockInteraction
|
||||
onFinished={() => {
|
||||
setPianoFinished(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.sceneId === 4 && (
|
||||
<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={selectedQuote || ''}
|
||||
onChange={setSelectedQuote}
|
||||
placeholder="请选择一句最触动你的遗言"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.sceneId === 5 && (
|
||||
<PostcardUnlockInteraction
|
||||
onFinished={() => {
|
||||
setPostcardFinished(true)
|
||||
}}
|
||||
onStateChange={setPostcardState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state.sceneId === 6 && (
|
||||
<BeijingUnlockInteraction
|
||||
onFinished={() => {
|
||||
setBeijingFinished(true)
|
||||
}}
|
||||
onSelectionChange={setBeijingSelection}
|
||||
/>
|
||||
)}
|
||||
{renderUnlockContent(state.sceneId, localState, unlockHandlers)}
|
||||
</UnlockHintDialog>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user