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
@@ -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)
|
||||
}}
|
||||
|
||||
Reference in new issue
Block a user