feat(ui): 添加首次游玩教程弹窗——5步引导含故事背景/部署操作/波次机制/开会激励/兵力建议,localStorage记录已读
This commit is contained in:
430
app/game/TutorialModal.tsx
Normal file
430
app/game/TutorialModal.tsx
Normal 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 />外包程序员凑数堵路口,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 }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
|
import { TutorialModal, useTutorial } from './TutorialModal'
|
||||||
|
|
||||||
// ── 塔的完整元数据(用于底部面板 + Tooltip) ──────────────────────────────
|
// ── 塔的完整元数据(用于底部面板 + Tooltip) ──────────────────────────────
|
||||||
interface TowerInfo {
|
interface TowerInfo {
|
||||||
@@ -672,12 +673,13 @@ export default function GamePage() {
|
|||||||
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
|
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
|
||||||
const [gameReady, setGameReady] = useState(false)
|
const [gameReady, setGameReady] = useState(false)
|
||||||
const [waveStarted, setWaveStarted] = 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 }>({
|
const [waveBtn, setWaveBtn] = useState<{ text: string; disabled: boolean }>({
|
||||||
text: '▶ 召唤下一波',
|
text: '▶ 召唤下一波',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})
|
})
|
||||||
const selectedTowerRef = useRef<TowerType | null>(null)
|
const selectedTowerRef = useRef<TowerType | null>(null)
|
||||||
|
const { show: showTutorial, dismiss: dismissTutorial } = useTutorial()
|
||||||
|
|
||||||
const handleMeeting = useCallback(() => {
|
const handleMeeting = useCallback(() => {
|
||||||
if (!gameReady) return
|
if (!gameReady) return
|
||||||
@@ -985,6 +987,11 @@ export default function GamePage() {
|
|||||||
canAfford={hc >= hoveredTower.cost}
|
canAfford={hc >= hoveredTower.cost}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 首次游玩教程弹窗 */}
|
||||||
|
{showTutorial && (
|
||||||
|
<TutorialModal onClose={dismissTutorial} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user