feat: add progression logic for puzzle residences

- Implemented functions to manage puzzle progress including checking if residences are started, complete, and unlocked.
- Added utility functions to retrieve piece IDs based on residence and route.
- Introduced a new TypeScript file for progression management.
- Integrated puzzle data configuration and residence data for seamless progression tracking.
This commit is contained in:
zhongyidian committed 2026-05-01 21:19:25 +08:00
1 parent 5a1c3cc368
commit 284302341c
72 files changed
+1104 -517

No files matched your search

+34 -6
View File
@@ -1,5 +1,8 @@
import { useEffect } from 'react'
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
import { Image } from '@/components'
import { usePuzzleStore } from '@/store/puzzleStore'
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
import { useNavigate, useParams } from 'react-router-dom'
import { useRongdeshengStore } from '@/store/rongdeshengStore'
@@ -11,6 +14,17 @@ const RongdeshengHome = () => {
const starName = 'rongdesheng'
const lastVisitedPlace = useRongdeshengStore((s) => s.lastVisitedPlace)
const puzzles = usePuzzleStore((state) => state.puzzles)
const isLoaded = usePuzzleStore((state) => state.isLoaded)
const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } =
getResidenceStageUnlocks(starName, puzzles)
useEffect(() => {
if (isLoaded && !isResidenceUnlocked(starName, puzzles)) {
navigate('/menu', { replace: true })
}
}, [isLoaded, navigate, puzzles, starName])
if (!moduleName) return null
return (
@@ -30,23 +44,37 @@ const RongdeshengHome = () => {
<div className="absolute right-20 top-75 flex flex-col space-y-10">
{/* 对话 */}
<Image
button
button={dialogueUnlocked}
className={dialogueUnlocked ? undefined : 'opacity-50 grayscale cursor-not-allowed'}
src={getStaticImage('star/一-解锁')}
onClick={() => navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)}
onClick={() => {
if (!dialogueUnlocked) return
navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)
}}
/>
{/* 体悟(占位) */}
<Image
button
button={reflectionUnlocked}
className={reflectionUnlocked ? undefined : 'opacity-50 grayscale cursor-not-allowed'}
src={getStaticImage('star/二-解锁')}
onClick={() => navigate(`/${moduleName}/${starName}/reflection`)}
onClick={() => {
if (!reflectionUnlocked) return
navigate(`/${moduleName}/${starName}/reflection`)
}}
/>
{/* 内化(占位) */}
<Image
button
button={internalizationUnlocked}
className={
internalizationUnlocked ? undefined : 'opacity-50 grayscale cursor-not-allowed'
}
src={getStaticImage('star/三-解锁')}
onClick={() => navigate(`/${moduleName}/${starName}/internalization`)}
onClick={() => {
if (!internalizationUnlocked) return
navigate(`/${moduleName}/${starName}/internalization`)
}}
/>
</div>
</div>
+1
View File
@@ -272,6 +272,7 @@ const RongdeshengPlace1 = () => {
visible={showAfterDialogGuide}
onContinue={() => setShowAfterDialogGuide(false)}
audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_01.mp3')}
firstLineIndent="2em"
>
{` 同学你好,欢迎来到“走进故居·对话”学习模块!
此刻你正伫立在荣德生旧居的石库门廊下,接下来请循着实业的足迹,一同探寻先生传奇人生里的4段壮阔征程:金融启蒙·资本筑基、实业拓疆·双业兴邦、山河危难·气节如钢、风雨坚守·初心不改。准备好共赴这场面粉与棉纱交织的民族工业史诗了吗?`}
+2 -2
View File
@@ -222,7 +222,7 @@ const RongdeshengPlace2 = () => {
)}
{showVideo && videoForExhibitId === 2 && (
<Video
src={getPublicPath('video/rongdesheng/rongdesheng_3.mp4')}
src={getPublicPath('video/rongdesheng/rongdesheng_4.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
@@ -233,7 +233,7 @@ const RongdeshengPlace2 = () => {
)}
{showVideo && videoForExhibitId === 4 && (
<Video
src={getPublicPath('video/rongdesheng/rongdesheng_3.mp4')}
src={getPublicPath('video/rongdesheng/rongdesheng_5.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
+19 -1
View File
@@ -48,6 +48,7 @@ const RongdeshengPlace4 = () => {
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>(null)
const [showVideo, setShowVideo] = useState(false)
const [showEndingVideo, setShowEndingVideo] = useState(false)
const [guide1Visible, setGuide1Visible] = useState(false)
const [guide1Step, setGuide1Step] = useState(0)
const [guide1AfterVideoVisible, setGuide1AfterVideoVisible] = useState(false)
@@ -95,6 +96,11 @@ const RongdeshengPlace4 = () => {
setGuide1AfterVideoVisible(true)
}
const handleEndingVideoEnded = () => {
setShowEndingVideo(false)
setEndingGuideVisible(true)
}
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
@@ -174,7 +180,7 @@ const RongdeshengPlace4 = () => {
})
if (closingId === 4) {
setEndingGuideVisible(true)
setShowEndingVideo(true)
}
}
@@ -196,6 +202,18 @@ const RongdeshengPlace4 = () => {
/>
)}
{showEndingVideo && (
<Video
src={getPublicPath('video/rongdesheng/rongdesheng_11.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={handleEndingVideoEnded}
onSkip={handleEndingVideoEnded}
/>
)}
<GuideSteps
visible={guide1Visible}
onVisibleChange={setGuide1Visible}
+1 -1
View File
@@ -350,8 +350,8 @@ const RongdeshengReflection = () => {
setShowGuide(false)
markReflectionGuideShown()
}}
firstLineIndent="2em"
audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_31.mp3')}
firstLineIndent="2em"
>
同学你好,欢迎来到“跨越时空·体悟”学习模块!{'\n'}
请登陆“时空回溯・亲历时代”体验仓,下一站:1915年,上海。{'\n'}
@@ -171,27 +171,31 @@ export const MODULES: Module[] = [
id: 'opt5-1',
text: 'A.仿制外国先进设备,改良生产工艺',
isCorrect: false,
videoResourceId: 'video5-1',
},
{
id: 'opt5-2',
text: 'B.设立职员养成所,培养本土技术人才',
isCorrect: true,
videoResourceId: 'video5-2',
videoResourceId: 'video5-1',
},
{
id: 'opt5-3',
text: 'C.降价促销,以低价抢占市场',
videoResourceId: 'video5-2',
isCorrect: true,
},
{ id: 'opt5-3', text: 'C.降价促销,以低价抢占市场', isCorrect: true },
],
videoResources: [
{
id: 'video5-1',
thumbnail: 'rongdesheng/reflection/视频5-1',
videoSrc: '/video/rongdesheng/rongdesheng_2_9.mp4',
videoSrc: '/video/rongdesheng/rongdesheng_2_10.mp4',
position: { top: '25%', left: '42%' },
},
{
id: 'video5-2',
thumbnail: 'rongdesheng/reflection/视频5-2',
videoSrc: '/video/rongdesheng/rongdesheng_2_10.mp4',
videoSrc: '/video/rongdesheng/rongdesheng_2_9.mp4',
position: { bottom: '50%', left: '55%' },
flipY: true,
},