431 lines
16 KiB
TypeScript
431 lines
16 KiB
TypeScript
'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 />外包程序员凑数堵路口,00后实习生建2-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 />转弯处密集建塔 > 直线段稀疏建;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 }
|
||
}
|