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:
zhongyidian committed 2026-04-27 11:49:30 +08:00
1 parent e0fa47d21a
commit f5f36f75b5
153 files changed
+3846 -2352

No files matched your search

+264 -165
View File
@@ -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>
)}