feat(ui): 添加首次游玩教程弹窗——5步引导含故事背景/部署操作/波次机制/开会激励/兵力建议,localStorage记录已读

This commit is contained in:
Cloud Bot
2026-03-24 09:33:47 +00:00
parent de17de46e1
commit dc9904b3f9
2 changed files with 438 additions and 1 deletions

430
app/game/TutorialModal.tsx Normal file
View File

@@ -0,0 +1,430 @@
'use client'
import { useState, useEffect } from 'react'
const STORAGE_KEY = 'dachang-tutorial-seen-v1'
// ── 教程步骤数据 ─────────────────────────────────────────────────────────────
const STEPS = [
{
id: 'story',
icon: '🏢',
title: '故事背景',
subtitle: '2026年某大厂总部',
content: (
<div style={{ lineHeight: 1.7 }}>
<p>
"组织架构调整"<span style={{ color: '#FBBF24' }}>VP</span>
"职场怪物"<b></b><b></b>
</p>
<p style={{ marginTop: '10px' }}>
<span style={{ color: '#EF4444' }}>KPI归零</span>
"毕业"
</p>
<p style={{ marginTop: '10px' }}>
<br />
<span style={{ color: '#A78BFA' }}>HC</span>线<br />
KPI
</p>
</div>
),
},
{
id: 'deploy',
icon: '🗺️',
title: '如何部署防线',
subtitle: '拖拽?不,点击就够了',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#A78BFA' }}></b> = </span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#22C55E' }}></b></span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#F59E0B' }}></b> 10 HC</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#EC4899' }}></b></span>
</div>
<div style={{
marginTop: '6px',
backgroundColor: 'rgba(167,139,250,0.1)',
border: '1px solid rgba(167,139,250,0.3)',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '13px',
color: '#CBD5E1',
}}>
💡 <b></b>
</div>
</div>
</div>
),
},
{
id: 'wave',
icon: '⚔️',
title: '战斗与波次',
subtitle: '召唤敌人才能赚HC',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#C4B5FD' }}></b> HC</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🕐</span>
<span><b style={{ color: '#FBBF24' }}>2</b></span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>📋</span>
<span>3<b style={{ color: '#FCD34D' }}></b>3</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>👑</span>
<span>5<b style={{ color: '#FBBF24' }}>BOSS波</b>VP会随机摧毁你的防御塔</span>
</div>
<div style={{
marginTop: '2px',
backgroundColor: 'rgba(239,68,68,0.1)',
border: '1px solid rgba(239,68,68,0.25)',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '13px',
color: '#FCA5A5',
}}>
<b>3 × 5</b> HC
</div>
</div>
</div>
),
},
{
id: 'meeting',
icon: '📋',
title: '开会 & 发激励',
subtitle: '暂停游戏PUA员工',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🟢</span>
<span><b style={{ color: '#4ADE80' }}>📋 </b><b></b></span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#EC4899' }}>PUA激励台</b>//...AI会评分并给出游戏加成</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🎯</span>
<span>
<span style={{ color: '#22C55E' }}> 4-6</span>
<span style={{ color: '#FBBF24' }}> 7-8</span>
<span style={{ color: '#FF4E00' }}> 9-10</span>
</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span> = HC × 15%<b style={{ color: '#F97316' }}></b>HC</span>
</div>
<div style={{
marginTop: '2px',
backgroundColor: 'rgba(74,222,128,0.08)',
border: '1px solid rgba(74,222,128,0.25)',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '13px',
color: '#86EFAC',
}}>
💡 <b></b>
</div>
</div>
</div>
),
},
{
id: 'tactics',
icon: '🧠',
title: '兵力部署建议',
subtitle: '老板传授的排兵布阵',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '9px' }}>
<div style={{
backgroundColor: 'rgba(34,197,94,0.1)',
border: '1px solid rgba(34,197,94,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#22C55E' }}>1-2</b>
<br />002-3
</div>
<div style={{
backgroundColor: 'rgba(59,130,246,0.1)',
border: '1px solid rgba(59,130,246,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#3B82F6' }}>3-4</b>
<br />PPT大师减速P6资深开发远程输出HRBP放中央提速
</div>
<div style={{
backgroundColor: 'rgba(239,68,68,0.1)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#EF4444' }}>BOSS波5</b>
<br />BOSS路径上
</div>
<div style={{
backgroundColor: 'rgba(167,139,250,0.1)',
border: '1px solid rgba(167,139,250,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#A78BFA' }}></b>
<br /> &gt; 线HRBP永远放在其他塔的中间
</div>
</div>
</div>
),
},
]
// ── 教程弹窗组件 ─────────────────────────────────────────────────────────────
interface TutorialModalProps {
onClose: () => void
}
export function TutorialModal({ onClose }: TutorialModalProps) {
const [step, setStep] = useState(0)
const current = STEPS[step]
const isLast = step === STEPS.length - 1
const isFirst = step === 0
const handleClose = () => {
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, '1')
}
onClose()
}
return (
<div style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.82)',
zIndex: 10000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '16px',
backdropFilter: 'blur(4px)',
}}>
<div style={{
backgroundColor: '#0a1628',
border: '2px solid #7C3AED',
borderRadius: '16px',
width: '100%',
maxWidth: '520px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 0 60px rgba(124,58,237,0.5), 0 24px 64px rgba(0,0,0,0.8)',
overflow: 'hidden',
}}>
{/* 顶部:进度条 */}
<div style={{
height: '3px',
backgroundColor: '#1e3a5f',
flexShrink: 0,
}}>
<div style={{
height: '100%',
width: `${((step + 1) / STEPS.length) * 100}%`,
background: 'linear-gradient(90deg, #7C3AED, #EC4899)',
transition: 'width 0.3s ease',
borderRadius: '2px',
}} />
</div>
{/* 步骤指示点 */}
<div style={{
display: 'flex',
justifyContent: 'center',
gap: '6px',
padding: '12px 24px 0',
flexShrink: 0,
}}>
{STEPS.map((s, i) => (
<button
key={s.id}
onClick={() => setStep(i)}
style={{
width: i === step ? '20px' : '8px',
height: '8px',
borderRadius: '4px',
border: 'none',
backgroundColor: i === step ? '#7C3AED' : i < step ? '#4C1D95' : '#1e3a5f',
cursor: 'pointer',
transition: 'all 0.2s ease',
padding: 0,
}}
/>
))}
</div>
{/* 内容区 */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '20px 28px',
}}>
{/* 图标 + 标题 */}
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<div style={{ fontSize: '40px', marginBottom: '8px', lineHeight: 1 }}>
{current.icon}
</div>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '11px',
color: '#A78BFA',
marginBottom: '6px',
letterSpacing: '0.5px',
}}>
{current.title}
</div>
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '16px',
color: '#64748B',
}}>
{current.subtitle}
</div>
</div>
{/* 分割线 */}
<div style={{
height: '1px',
background: 'linear-gradient(90deg, transparent, #1e3a5f, transparent)',
marginBottom: '16px',
}} />
{/* 正文 */}
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '15px',
color: '#CBD5E1',
}}>
{current.content}
</div>
</div>
{/* 底部导航 */}
<div style={{
borderTop: '1px solid #1e3a5f',
padding: '14px 24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.5)',
}}>
{/* 步骤文字 */}
<span style={{
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#334155',
}}>
{step + 1} / {STEPS.length}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
{/* 上一步 */}
{!isFirst && (
<button
onClick={() => setStep(s => s - 1)}
style={{
padding: '8px 16px',
backgroundColor: '#0F1B2D',
border: '1px solid #1e3a5f',
borderRadius: '8px',
color: '#94A3B8',
fontFamily: 'VT323, monospace',
fontSize: '16px',
cursor: 'pointer',
}}
>
</button>
)}
{/* 跳过(仅前几步) */}
{!isLast && (
<button
onClick={handleClose}
style={{
padding: '8px 16px',
backgroundColor: 'transparent',
border: '1px solid #1e293b',
borderRadius: '8px',
color: '#475569',
fontFamily: 'VT323, monospace',
fontSize: '16px',
cursor: 'pointer',
}}
>
</button>
)}
{/* 下一步 / 开始战斗 */}
<button
onClick={isLast ? handleClose : () => setStep(s => s + 1)}
style={{
padding: '8px 20px',
backgroundColor: isLast ? '#7C3AED' : '#1e3a5f',
border: `2px solid ${isLast ? '#A78BFA' : '#7C3AED'}`,
borderRadius: '8px',
color: isLast ? '#E2E8F0' : '#C4B5FD',
fontFamily: isLast ? "'Press Start 2P', monospace" : 'VT323, monospace',
fontSize: isLast ? '9px' : '16px',
cursor: 'pointer',
boxShadow: isLast ? '0 0 16px rgba(124,58,237,0.5)' : 'none',
letterSpacing: isLast ? '0.5px' : 'normal',
transition: 'all 0.15s',
}}
>
{isLast ? '开始战斗 ▶' : '下一步 →'}
</button>
</div>
</div>
</div>
</div>
)
}
// ── Hook判断是否需要显示教程 ────────────────────────────────────────────────
export function useTutorial() {
const [show, setShow] = useState(false)
useEffect(() => {
// 避免 SSR 问题,在客户端检查
if (typeof window === 'undefined') return
const seen = localStorage.getItem(STORAGE_KEY)
if (!seen) setShow(true)
}, [])
const dismiss = () => {
localStorage.setItem(STORAGE_KEY, '1')
setShow(false)
}
return { show, dismiss }
}

View File

@@ -1,6 +1,7 @@
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { TutorialModal, useTutorial } from './TutorialModal'
// ── 塔的完整元数据(用于底部面板 + Tooltip ──────────────────────────────
interface TowerInfo {
@@ -672,12 +673,13 @@ export default function GamePage() {
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false)
const [waveStarted, setWaveStarted] = useState(false)
const [inMeeting, setInMeeting] = useState(false) // 开会=游戏暂停
const [inMeeting, setInMeeting] = useState(false)
const [waveBtn, setWaveBtn] = useState<{ text: string; disabled: boolean }>({
text: '▶ 召唤下一波',
disabled: false,
})
const selectedTowerRef = useRef<TowerType | null>(null)
const { show: showTutorial, dismiss: dismissTutorial } = useTutorial()
const handleMeeting = useCallback(() => {
if (!gameReady) return
@@ -985,6 +987,11 @@ export default function GamePage() {
canAfford={hc >= hoveredTower.cost}
/>
)}
{/* 首次游玩教程弹窗 */}
{showTutorial && (
<TutorialModal onClose={dismissTutorial} />
)}
</div>
)
}