- 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.
254 lines
8.1 KiB
TypeScript
254 lines
8.1 KiB
TypeScript
import { useMemo, useState } from 'react'
|
||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||
import { Image } from '@/components'
|
||
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||
|
||
/**
|
||
* 六边形进度条组件
|
||
* 根据进度值显示不同颜色:100%绿色,50%-99%橙色,0%-49%灰色
|
||
*/
|
||
interface HexagonProgressBarProps {
|
||
progress: number // 0-100
|
||
}
|
||
|
||
const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||
const progressPercent = Math.min(100, Math.max(0, progress))
|
||
const progressText =
|
||
progressPercent.toFixed(2) === '100.00' ? '进度100%' : `进度${Math.round(progressPercent)}%`
|
||
|
||
// 根据进度值确定填充颜色
|
||
const getProgressColor = () => {
|
||
if (progressPercent >= 100) {
|
||
return '#90B86B' // 浅橄榄绿色(100%完成)
|
||
} else if (progressPercent >= 50) {
|
||
return '#EEB362' // 橙色(50%-99%)
|
||
} else {
|
||
return '#E0E0E0' // 浅灰色(0%-49%)
|
||
}
|
||
}
|
||
|
||
const fillColor = getProgressColor()
|
||
// 六边形路径,细长的水平六边形,左右两端是尖角
|
||
// 正确的六边形路径:从左顶点开始,顺时针绘制
|
||
// 左顶点(0%, 50%) -> 左上角(3%, 0%) -> 右上角(97%, 0%) -> 右顶点(100%, 50%) -> 右下角(97%, 100%) -> 左下角(3%, 100%)
|
||
// 外层路径(包含边框)
|
||
const outerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||
// 内层路径(边框内部)
|
||
const innerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||
const borderWidth = 4 // 增加边框宽度从2px到3px
|
||
|
||
return (
|
||
<div className="relative" style={{ width: '260px', height: '50px' }}>
|
||
{/* 外层:白色边框(使用 clip-path 裁剪形状) */}
|
||
<div
|
||
className="absolute inset-0"
|
||
style={{
|
||
clipPath: outerHexagonPath,
|
||
backgroundColor: 'white',
|
||
}}
|
||
/>
|
||
{/* 内容容器(边框内部区域) */}
|
||
<div
|
||
className="absolute"
|
||
style={{
|
||
left: `${borderWidth}px`,
|
||
top: `${borderWidth}px`,
|
||
right: `${borderWidth}px`,
|
||
bottom: `${borderWidth}px`,
|
||
clipPath: innerHexagonPath,
|
||
}}
|
||
>
|
||
{/* 背景层:深灰色背景(未填充部分) */}
|
||
<div
|
||
className="absolute inset-0"
|
||
style={{
|
||
backgroundColor: '#757575',
|
||
}}
|
||
/>
|
||
{/* 填充层(根据进度百分比从左侧填充) */}
|
||
{progressPercent > 0 && (
|
||
<div
|
||
className="absolute left-0 top-0 bottom-0 overflow-hidden"
|
||
style={{
|
||
width: `${progressPercent}%`,
|
||
}}
|
||
>
|
||
<div
|
||
className="absolute inset-0"
|
||
style={{
|
||
backgroundColor: fillColor,
|
||
transition: 'width 0.3s ease, background-color 0.3s ease',
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{/* 文字(白色,居中,始终显示在最上层,带阴影效果) */}
|
||
<div
|
||
className="absolute inset-0 flex items-center justify-center text-white text-2xl font-bold pointer-events-none"
|
||
style={{
|
||
textShadow: '0 2px 4px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.8)',
|
||
zIndex: 10,
|
||
}}
|
||
>
|
||
{progressText}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export interface MenuPuzzleCenterProps {
|
||
/** 是否可见 */
|
||
visible: boolean
|
||
/** 关闭回调 */
|
||
onClose: () => void
|
||
/** 选择某个故居后的回调(进入对应的碎片系统) */
|
||
onSelectResidence: (residence: string) => void
|
||
/** 是否允许从管理中心进入具体拼图 */
|
||
allowPuzzleEntry?: boolean
|
||
}
|
||
|
||
const MenuPuzzleCenter = ({
|
||
visible,
|
||
onClose,
|
||
onSelectResidence,
|
||
allowPuzzleEntry = true,
|
||
}: MenuPuzzleCenterProps) => {
|
||
const { getPuzzleCollectProgress } = usePuzzleStore()
|
||
const [activeMapImage, setActiveMapImage] = useState<string | null>(null)
|
||
|
||
const residenceProgress = useMemo(
|
||
() =>
|
||
PUZZLE_RESIDENCES.map((item) => ({
|
||
...item,
|
||
// 只有已开放拼图的故居才计算收集进度
|
||
progress: item.hasPuzzle ? getPuzzleCollectProgress(item.residence) : 0,
|
||
})),
|
||
[getPuzzleCollectProgress]
|
||
)
|
||
|
||
if (!visible) return null
|
||
|
||
return (
|
||
<div
|
||
className="fixed inset-0 z-90 flex items-center justify-center"
|
||
style={{ background: 'rgba(0,0,0,0.8)' }}
|
||
onClick={(e) => {
|
||
if (e.target === e.currentTarget) {
|
||
onClose()
|
||
}
|
||
}}
|
||
>
|
||
<div
|
||
className="relative w-full h-full bg-cover bg-center bg-no-repeat"
|
||
style={{ backgroundImage: getBackgroundImage('puzzle/home/底图') }}
|
||
>
|
||
{/* 箭头指示底图 */}
|
||
<img
|
||
className="absolute top-84 left-55"
|
||
src={getStaticImage('puzzle/home/箭头指引')}
|
||
alt=""
|
||
/>
|
||
|
||
{/* 关闭按钮 */}
|
||
<Image
|
||
className="absolute right-8 top-8 z-10"
|
||
src={getStaticImage('components/关闭')}
|
||
alt="关闭"
|
||
button
|
||
onClick={onClose}
|
||
/>
|
||
|
||
{/* 故居列表:按照设计图进行绝对定位 */}
|
||
<div className="absolute inset-0">
|
||
{residenceProgress.map(
|
||
({
|
||
residence,
|
||
displayName,
|
||
left,
|
||
top,
|
||
image,
|
||
hasPuzzle,
|
||
progress,
|
||
hasMap,
|
||
mapImage,
|
||
}) => (
|
||
<div
|
||
key={residence}
|
||
className={`absolute -translate-x-1/2 -translate-y-1/2`}
|
||
style={{ left, top }}
|
||
>
|
||
{/* 故居图片(资源在 puzzle/home 下,可根据实际名称调整) */}
|
||
<Image src={getStaticImage(`puzzle/home/${image}`)} alt={displayName} />
|
||
|
||
{/* 进度展示,仅对已开放拼图的故居显示 */}
|
||
{hasPuzzle && (
|
||
<div className="mb-4 flex justify-center">
|
||
<HexagonProgressBar progress={progress} />
|
||
</div>
|
||
)}
|
||
|
||
{/* 图标 */}
|
||
<div className="flex justify-center gap-6">
|
||
{allowPuzzleEntry && (
|
||
<Image
|
||
src={getStaticImage(`puzzle/home/拼图`)}
|
||
alt="拼图"
|
||
button
|
||
onClick={(e) => {
|
||
if (!hasPuzzle) return
|
||
e.stopPropagation()
|
||
onSelectResidence(residence)
|
||
}}
|
||
/>
|
||
)}
|
||
<Image
|
||
src={getStaticImage(`puzzle/home/追光入口`)}
|
||
alt="导图"
|
||
button
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
if (!hasMap || !mapImage) return
|
||
setActiveMapImage(mapImage)
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
)}
|
||
</div>
|
||
|
||
{/* 导图弹窗 */}
|
||
{activeMapImage && (
|
||
<div
|
||
className="fixed inset-0 z-110 flex items-center justify-center"
|
||
style={{ background: 'rgba(0,0,0,0.75)' }}
|
||
onClick={(e) => {
|
||
if (e.target === e.currentTarget) {
|
||
setActiveMapImage(null)
|
||
}
|
||
}}
|
||
>
|
||
<div className="relative w-full h-full">
|
||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||
<Image
|
||
className="absolute -right-4 -top-8 z-10"
|
||
src={getStaticImage('components/close')}
|
||
alt="关闭"
|
||
button
|
||
onClick={() => setActiveMapImage(null)}
|
||
/>
|
||
|
||
<img src={getStaticImage(`puzzle/home/map/${activeMapImage}`)} alt="导图" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default MenuPuzzleCenter
|