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

+90 -47
View File
@@ -5,26 +5,28 @@ import {
Image,
Title,
MatchQuizPanel,
SingleChoiceQuizPanel,
Video,
type VideoRef,
} from '@/components'
import { useNavigate, useParams } from 'react-router-dom'
import { getBackgroundImage, getStaticImage, getPublicPath } from '@/lib/utils'
import { useMemo, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import Cursor from '@/components/cursor'
import { useCursorController } from '@/hooks/useCursorController'
import SuccessAlert from '@/components/layout/alert/success-alert'
import ScaledPortal from '@/components/layout/ScaledPortal'
import { saveZhanglepingStore, useZhanglepingStore } from '@/store/zhanglepingStore'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
// starName 固定为 zhangleping
const starName = 'zhangleping'
// 展品 ID 与资源路径映射
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
1: 'zhangleping/place2/张乐平场景2展品1',
1: 'zhangleping/place2/张乐平场景2展品3',
2: 'zhangleping/place2/张乐平场景2展品2',
3: 'zhangleping/place2/张乐平场景2展品3',
3: 'zhangleping/place2/张乐平场景2展品1',
}
// 阶段二:抗战画家(3 个光标)
@@ -34,16 +36,13 @@ const ZhanglepingPlace2 = () => {
moduleName: string
}>()
const { triggerPuzzleAnimation } = usePuzzleAnimation()
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(null)
// 持久化接入
const markVisited = useZhanglepingStore((s) => s.markExhibitVisited)
const getVisited = useZhanglepingStore((s) => s.getVisitedExhibits)
const setLastVisitedPlace = useZhanglepingStore((s) => s.setLastVisitedPlace)
const persistedVisited = useMemo(
() => new Set(getVisited('place2') as (1 | 2 | 3)[]),
[getVisited]
)
const [visited, setVisited] = useState<Set<1 | 2 | 3>>(() => new Set(persistedVisited))
const place = 'place2'
const { markExhibitVisited, getVisitedExhibits, setLastVisitedPlace } = useZhanglepingStore()
const visitedExhibits = getVisitedExhibits(place) as (1 | 2 | 3)[]
const visited = new Set<1 | 2 | 3>(visitedExhibits)
const [guideVisible1, setGuideVisible1] = useState(false)
const [guideStep1, setGuideStep1] = useState(0)
@@ -63,6 +62,8 @@ const ZhanglepingPlace2 = () => {
// 展品1:关闭后播放的视频
const [showVideo1, setShowVideo1] = useState(false)
const video1Ref = useRef<VideoRef>(null)
const [quizVisible1, setQuizVisible1] = useState(false)
const [successAlert1Visible, setSuccessAlert1Visible] = useState(false)
// 展品2:关闭后播放的视频
const [showVideo2, setShowVideo2] = useState(false)
@@ -121,7 +122,7 @@ const ZhanglepingPlace2 = () => {
button
onClick={() => {
if (moduleName && starName) {
navigate(`/${moduleName}/${starName}/home`)
navigate(`/${moduleName}/${starName}/place1`)
}
}}
/>
@@ -228,6 +229,17 @@ const ZhanglepingPlace2 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 3) {
triggerPuzzleAnimation(3)
}
if (activeExhibitId === 2) {
triggerPuzzleAnimation(4)
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(5)
}
}}
onClose={() => {
if (activeExhibitId !== null) {
// 展品1:关闭弹窗时(对话可能还在进行),先不关闭弹窗,等待对话完全结束
@@ -251,22 +263,9 @@ const ZhanglepingPlace2 = () => {
}
// 展品3:需在完成连线题后才标记为已访问
if (activeExhibitId === 3) {
if (quizPassed3) {
setVisited((prev) => {
const next = new Set(prev)
next.add(3)
markVisited('place2', 3)
return next
})
}
if (activeExhibitId === 3 && quizPassed3) {
markExhibitVisited(place, 3)
}
// setVisited((prev) => {
// const next = new Set(prev)
// next.add(activeExhibitId)
// markVisited('place2', activeExhibitId)
// return next
// })
}
setActiveExhibitId(null)
}}
@@ -283,36 +282,84 @@ const ZhanglepingPlace2 = () => {
showSkipButton
onEnded={() => {
setShowVideo1(false)
setVisited((prev) => {
const next = new Set(prev)
next.add(1)
markVisited('place2', 1)
return next
})
setQuizVisible1(true)
markExhibitVisited(place, 1)
}}
onSkip={() => {
setShowVideo1(false)
setQuizVisible1(true)
}}
/>
)}
<ScaledPortal enabled={quizVisible1 || successAlert1Visible} zIndex={10050}>
<>
<SingleChoiceQuizPanel
visible={quizVisible1}
zIndex={10000}
title="【单选】抗战时期,张乐平先生的画笔是“刀枪”,他的作品主要传达了以下哪种精神?"
options={[
{
value: 'A',
label: 'A.逃避现实,只描绘美好的幻想世界,让大家暂时忘却痛苦',
},
{
value: 'B',
label: 'B.用幽默搞笑的方式逗大家开心,没有更深层次的意义',
},
{
value: 'C',
label: 'C.坚定地揭露日寇暴行,激发人们的爱国热情和抗争精神',
},
{
value: 'D',
label: 'D.只关注个人的艺术创作,不关心社会和国家的命运',
},
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === 'A') {
return '逃避现实与他的实际行动不符。请你再想想吧!'
}
if (value === 'B') {
return '只强调幽默搞笑,忽略了作品的深刻意义。请你再想想吧!'
}
if (value === 'D') {
return '不关心社会和国家命运与张乐平先生的抗战宣传行为相悖。请你再想想吧!'
}
return '请你再想想吧!'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuizVisible1(false)
setSuccessAlert1Visible(true)
}}
/>
<SuccessAlert
visible={successAlert1Visible}
title="恭喜你,选择正确!"
content="在抗战时期,张乐平先生以抗日救亡漫画宣传队副领队身份北上宣传抗日,带领宣传队足迹遍及多地,他的漫画被译成多国文字,印成传单,空投敌占区,宣传正义,呼唤良知。这充分体现出他用画笔坚定地揭露日寇暴行,激发人们的爱国热情和抗争精神。"
onContinue={() => {
setSuccessAlert1Visible(false)
markExhibitVisited(place, 1)
}}
/>
</>
</ScaledPortal>
{/* 展品2关闭后的讲解视频 */}
{showVideo2 && (
<Video
ref={video2Ref}
src={getPublicPath('video/sunzhongshan/sunzhongshan_03.mp4')}
src={getPublicPath('video/zhangleping/zhangleping_00.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo2(false)
setVisited((prev) => {
const next = new Set(prev)
next.add(2)
markVisited('place2', 2)
return next
})
markExhibitVisited(place, 2)
}}
onSkip={() => {
setShowVideo2(false)
@@ -365,12 +412,7 @@ const ZhanglepingPlace2 = () => {
setQuizVisible3(false)
setQuizPassed3(true)
setSuccessAlert3Visible(true)
// 答题成功后标记展品3为已访问
setVisited((prev) => {
const next = new Set(prev)
next.add(3)
return next
})
markExhibitVisited(place, 3)
}
}}
zIndex={10000}
@@ -378,6 +420,7 @@ const ZhanglepingPlace2 = () => {
<SuccessAlert
visible={successAlert3Visible}
title="恭喜你,匹配正确!"
onContinue={() => {
setSuccessAlert3Visible(false)
}}