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,11 +1,20 @@
|
||||
import { Image } from '@/components'
|
||||
import { cn, getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||||
import { Image, StudyGuideModal } from '@/components'
|
||||
import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { LOBBY_COLUMNS, LOBBY_LAYOUT, LOBBY_TIMELINE, PLACE_REQUIRED_COUNTS } from './lobby.config'
|
||||
import { LOBBY_COLUMNS, LOBBY_LAYOUT, LOBBY_TIMELINE } from './lobby.config'
|
||||
import { useNieerStore } from '@/store/nieerStore'
|
||||
import { useDialogReducer } from './useDialogReducer'
|
||||
import DialogLayer from './DialogLayer'
|
||||
import {
|
||||
BADGE_CONFIG,
|
||||
getAllPlacesCompleted,
|
||||
getBadgeUnlockedMap,
|
||||
getGuideSceneId,
|
||||
getPlaceCompletionMap,
|
||||
getUnlockVideoSrcForScene,
|
||||
getUnlockedByIdMap,
|
||||
} from './reflectionProgress'
|
||||
|
||||
const starName = 'nieer'
|
||||
|
||||
@@ -16,61 +25,38 @@ const NieerReflection = () => {
|
||||
const visitedReflectionCursors = useNieerStore((state) => state.visitedReflectionCursors)
|
||||
const markReflectionCursorVisited = useNieerStore((state) => state.markReflectionCursorVisited)
|
||||
const setReflectionWork = useNieerStore((state) => state.setReflectionWork)
|
||||
const hasSeenReflectionGuide = useNieerStore((state) => state.hasSeenReflectionGuide)
|
||||
const markReflectionGuideSeen = useNieerStore((state) => state.markReflectionGuideSeen)
|
||||
const showGuideModal = !hasSeenReflectionGuide
|
||||
|
||||
const {
|
||||
state: dialogState,
|
||||
openScene,
|
||||
enterUnlock,
|
||||
handleShare,
|
||||
finishBadgeVideo,
|
||||
close,
|
||||
} = useDialogReducer(markReflectionCursorVisited, setReflectionWork)
|
||||
|
||||
const placeCompletion = useMemo(() => {
|
||||
const getVisitedCount = (placeName: string): number =>
|
||||
visitedReflectionCursors[placeName]?.size ?? 0
|
||||
return {
|
||||
1: getVisitedCount('place1') >= (PLACE_REQUIRED_COUNTS[1] ?? 0),
|
||||
2: getVisitedCount('place2') >= (PLACE_REQUIRED_COUNTS[2] ?? 0),
|
||||
3: getVisitedCount('place3') >= (PLACE_REQUIRED_COUNTS[3] ?? 0),
|
||||
4: getVisitedCount('place4') >= (PLACE_REQUIRED_COUNTS[4] ?? 0),
|
||||
5: getVisitedCount('place5') >= (PLACE_REQUIRED_COUNTS[5] ?? 0),
|
||||
6: getVisitedCount('place6') >= (PLACE_REQUIRED_COUNTS[6] ?? 0),
|
||||
} as const
|
||||
}, [visitedReflectionCursors])
|
||||
|
||||
const unlockedById = useMemo(
|
||||
() =>
|
||||
({
|
||||
1: true,
|
||||
2: placeCompletion[1],
|
||||
3: placeCompletion[2],
|
||||
4: placeCompletion[3],
|
||||
5: placeCompletion[4],
|
||||
6: placeCompletion[5],
|
||||
}) as const,
|
||||
[placeCompletion]
|
||||
const placeCompletion = useMemo(
|
||||
() => getPlaceCompletionMap(visitedReflectionCursors),
|
||||
[visitedReflectionCursors]
|
||||
)
|
||||
|
||||
const badgeUnlocked = useMemo(
|
||||
() => ({
|
||||
1: placeCompletion[1] && placeCompletion[2],
|
||||
2: placeCompletion[3] && placeCompletion[4],
|
||||
3: placeCompletion[5] && placeCompletion[6],
|
||||
}),
|
||||
[placeCompletion]
|
||||
)
|
||||
|
||||
const guideSceneId = useMemo(() => {
|
||||
const nextScene = LOBBY_COLUMNS.find(
|
||||
(scene) => !placeCompletion[scene.id as keyof typeof placeCompletion]
|
||||
)
|
||||
return nextScene?.id ?? 6
|
||||
}, [placeCompletion])
|
||||
const unlockedById = useMemo(() => getUnlockedByIdMap(placeCompletion), [placeCompletion])
|
||||
const badgeUnlocked = useMemo(() => getBadgeUnlockedMap(placeCompletion), [placeCompletion])
|
||||
const guideSceneId = useMemo(() => getGuideSceneId(placeCompletion), [placeCompletion])
|
||||
const allPlacesCompleted = useMemo(
|
||||
() => LOBBY_COLUMNS.every((scene) => placeCompletion[scene.id as keyof typeof placeCompletion]),
|
||||
() => getAllPlacesCompleted(placeCompletion),
|
||||
[placeCompletion]
|
||||
)
|
||||
|
||||
const handleResultShare = (sceneId: number, payload?: Record<string, unknown>) => {
|
||||
handleShare({
|
||||
payload,
|
||||
unlockVideoSrc: getUnlockVideoSrcForScene(sceneId, visitedReflectionCursors),
|
||||
})
|
||||
}
|
||||
|
||||
if (!moduleName) return null
|
||||
|
||||
return (
|
||||
@@ -78,7 +64,6 @@ const NieerReflection = () => {
|
||||
className="w-full h-full relative bg-cover bg-center bg-no-repeat overflow-hidden"
|
||||
style={{ backgroundImage: getBackgroundImage(`${starName}/reflection/lobby/场景首页底图`) }}
|
||||
>
|
||||
{/* 返回按钮 */}
|
||||
<Image
|
||||
className="absolute right-6 top-5 z-30"
|
||||
src={getStaticImage('components/返回')}
|
||||
@@ -86,7 +71,6 @@ const NieerReflection = () => {
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
|
||||
/>
|
||||
|
||||
{/* 六列时间轴内容 */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute h-full grid z-1',
|
||||
@@ -98,7 +82,6 @@ const NieerReflection = () => {
|
||||
gridTemplateColumns: `repeat(${LOBBY_LAYOUT.columnCount}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{/* 琴弦层:在背景图之上、在顶部图标之下 */}
|
||||
<img
|
||||
className="absolute -left-65 top-0 z-2 w-[1651px] h-auto pointer-events-none"
|
||||
src={getStaticImage(`${starName}/reflection/lobby/menu/琴弦`)}
|
||||
@@ -106,7 +89,6 @@ const NieerReflection = () => {
|
||||
/>
|
||||
{LOBBY_COLUMNS.map((scene) => {
|
||||
const isUnlocked = unlockedById[scene.id as keyof typeof unlockedById]
|
||||
// 顶部图标只在“该模块已完成学习任务”时亮起;未完成(即当前进度)保持黑白
|
||||
const isCompleted = placeCompletion[scene.id as keyof typeof placeCompletion]
|
||||
const iconSrc = isCompleted ? scene.icon : scene.iconBw
|
||||
const isClickable = isUnlocked || scene.id === guideSceneId
|
||||
@@ -131,7 +113,6 @@ const NieerReflection = () => {
|
||||
src={getStaticImage(`${starName}/reflection/lobby/${scene.cityImage}`)}
|
||||
alt={scene.city}
|
||||
/>
|
||||
{/* 顶部图标 */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-3',
|
||||
@@ -156,22 +137,18 @@ const NieerReflection = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 点击前往(当前引导场景) */}
|
||||
{showGuideState && (
|
||||
<>
|
||||
<Image
|
||||
className="animate-finger-bounce absolute bottom-[calc(50%-200px)] translate-y-1/2"
|
||||
src={getStaticImage(`${starName}/reflection/lobby/icon/点击前往`)}
|
||||
alt="点击前往"
|
||||
/>
|
||||
</>
|
||||
<Image
|
||||
className="animate-finger-bounce absolute bottom-[calc(50%-200px)] translate-y-1/2"
|
||||
src={getStaticImage(`${starName}/reflection/lobby/icon/点击前往`)}
|
||||
alt="点击前往"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 横跨年份层:只负责 1937 跨列展示 */}
|
||||
<div
|
||||
className={cn('absolute z-10 grid', LOBBY_LAYOUT.timelineTop, LOBBY_LAYOUT.columnsLeft)}
|
||||
style={{
|
||||
@@ -206,7 +183,6 @@ const NieerReflection = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 底部 footer + 徽章区 */}
|
||||
<div
|
||||
className="absolute left-0 bottom-0 w-full h-[201px] z-20 flex justify-center items-center pt-20 gap-10"
|
||||
style={{
|
||||
@@ -214,23 +190,10 @@ const NieerReflection = () => {
|
||||
}}
|
||||
>
|
||||
<div className="relative w-340 h-full">
|
||||
{[
|
||||
{
|
||||
id: 1,
|
||||
className: 'top-0 left-36',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
className: 'top-0 left-146',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
className: 'top-0 right-36',
|
||||
},
|
||||
].map((i) => {
|
||||
const isLit = badgeUnlocked[i.id as keyof typeof badgeUnlocked]
|
||||
{BADGE_CONFIG.map((badge) => {
|
||||
const isLit = badgeUnlocked[badge.id as keyof typeof badgeUnlocked]
|
||||
return (
|
||||
<div key={i.id} className={cn('absolute w-[193px]', i.className)}>
|
||||
<div key={badge.id} className={cn('absolute w-[193px]', badge.className)}>
|
||||
<Image
|
||||
width={193}
|
||||
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章底板`)}
|
||||
@@ -240,8 +203,8 @@ const NieerReflection = () => {
|
||||
<Image
|
||||
className="absolute inset-0"
|
||||
width={193}
|
||||
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章${i.id}`)}
|
||||
alt={`徽章${i.id}`}
|
||||
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章${badge.id}`)}
|
||||
alt={`徽章${badge.id}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -250,7 +213,6 @@ const NieerReflection = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右下角:旅行箱 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage(`${starName}/reflection/lobby/icon/旅行箱`)}
|
||||
@@ -259,7 +221,6 @@ const NieerReflection = () => {
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/reflection/box`)}
|
||||
/>
|
||||
|
||||
{/* debug 参考线 */}
|
||||
{debugLobby && (
|
||||
<div
|
||||
className="absolute inset-0 z-40 pointer-events-none"
|
||||
@@ -274,9 +235,21 @@ const NieerReflection = () => {
|
||||
<DialogLayer
|
||||
state={dialogState}
|
||||
onEnterUnlock={enterUnlock}
|
||||
onShare={handleShare}
|
||||
onShare={handleResultShare}
|
||||
onFinishBadgeVideo={finishBadgeVideo}
|
||||
onClose={close}
|
||||
/>
|
||||
|
||||
<StudyGuideModal
|
||||
visible={showGuideModal}
|
||||
firstLineIndent="2em"
|
||||
onContinue={markReflectionGuideSeen}
|
||||
audioSrc={getPublicPath('audio/nieer/mp3_nieer_28.mp3')}
|
||||
>
|
||||
{`同学你好,欢迎来到“跨越时空·体悟”学习模块!
|
||||
请登陆“时空回溯・亲历时代”体验仓,下一站:1936年,上海。
|
||||
请带上你的旅行包,穿越时空解锁6个国歌故事,集齐精神徽章,绘制属于你的国歌精神图谱吧~时空门已开,请出发吧!`}
|
||||
</StudyGuideModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user