diff --git a/app/game/TutorialModal.tsx b/app/game/TutorialModal.tsx new file mode 100644 index 0000000..674736f --- /dev/null +++ b/app/game/TutorialModal.tsx @@ -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: ( +
+

+ 公司宣布"组织架构调整",一批空降VP + 和各路"职场怪物"正从面试间财务室蜂拥而来—— +

+

+ 他们的目标只有一个:让公司的KPI归零, + 宣布你"毕业"。 +

+

+ 而你,是大厂最后的打工人。
+ 用HC(人才储备)雇佣防御阵线,
+ 保住 KPI,守住财务室,活下去。 +

+
+ ), + }, + { + id: 'deploy', + icon: '🗺️', + title: '如何部署防线', + subtitle: '拖拽?不,点击就够了', + content: ( +
+
+
+ + 在屏幕底部面板点击一个角色卡片(变色高亮 = 已选中) +
+
+ + 在地图上点击任意空白格子即可放置(路径格无法建塔) +
+
+ + 点击已放置的塔可购买咖啡恢复精力(消耗 10 HC) +
+
+ + 精力耗尽的塔会暂时罢工摸鱼,及时补咖啡! +
+
+ 💡 鼠标悬停角色卡片可查看详细技能介绍和建议打法 +
+
+
+ ), + }, + { + id: 'wave', + icon: '⚔️', + title: '战斗与波次', + subtitle: '召唤敌人,才能赚HC', + content: ( +
+
+
+ + 点击右侧「召唤下一波」按钮开始战斗,每波消灭敌人可获得 HC +
+
+ 🕐 + 波次结束后自动倒计时2秒进入下一波,也可手动提前开始 +
+
+ 📋 + 每3波结束触发「周报结算」——选对大厂黑话获得奖励,选错全场禁锢3秒 +
+
+ 👑 + 第5波是BOSS波,空降VP会随机摧毁你的防御塔,做好准备! +
+
+ ⚠️ 每关共 3张地图 × 5波,通关后 HC 重置,难度逐关提升 +
+
+
+ ), + }, + { + id: 'meeting', + icon: '📋', + title: '开会 & 发激励', + subtitle: '暂停游戏,PUA员工', + content: ( +
+
+
+ 🟢 + 点击右侧「📋 开会」按钮,游戏立即暂停,怪物和塔全部冻结 +
+
+ ✍️ + PUA激励台输入一段打鸡血的话(福报/狼性/闭环...),AI会评分并给出游戏加成 +
+
+ 🎯 + + 分数越高效果越猛: + 4-6分攻击↑ + 7-8分攻速↑↑ + 9-10分全场狂暴 + +
+
+ ⚠️ + 激励费用 = 当前 HC × 15%;重复或相似内容会被识破并额外扣HC +
+
+ 💡 激励发完后点「结束开会」继续战斗,效果在整场战斗中持续生效 +
+
+
+ ), + }, + { + id: 'tactics', + icon: '🧠', + title: '兵力部署建议', + subtitle: '老板传授的排兵布阵', + content: ( +
+
+
+ 前期(第1-2波) +
外包程序员凑数堵路口,00后实习生建2-3个做主力,先用廉价兵抗住 +
+
+ 中期(第3-4波) +
在路径转弯处放PPT大师减速,用P6资深开发远程输出;HRBP放中央提速 +
+
+ BOSS波(第5波) +
先召开紧急会议发激励!产品经理放在BOSS路径上,不断把他打回去 +
+
+ 核心原则 +
转弯处密集建塔 > 直线段稀疏建;HRBP永远放在其他塔的中间 +
+
+
+ ), + }, +] + +// ── 教程弹窗组件 ───────────────────────────────────────────────────────────── +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 ( +
+
+ {/* 顶部:进度条 */} +
+
+
+ + {/* 步骤指示点 */} +
+ {STEPS.map((s, i) => ( +
+ + {/* 内容区 */} +
+ {/* 图标 + 标题 */} +
+
+ {current.icon} +
+
+ {current.title} +
+
+ {current.subtitle} +
+
+ + {/* 分割线 */} +
+ + {/* 正文 */} +
+ {current.content} +
+
+ + {/* 底部导航 */} +
+ {/* 步骤文字 */} + + {step + 1} / {STEPS.length} + + +
+ {/* 上一步 */} + {!isFirst && ( + + )} + + {/* 跳过(仅前几步) */} + {!isLast && ( + + )} + + {/* 下一步 / 开始战斗 */} + +
+
+
+
+ ) +} + +// ── 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 } +} diff --git a/app/game/page.tsx b/app/game/page.tsx index 010f276..85c00cb 100644 --- a/app/game/page.tsx +++ b/app/game/page.tsx @@ -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(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(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 && ( + + )}
) }