feat(ui): 底部塔卡片添加悬浮信息弹窗——显示角色名言、属性数值、特殊技能、建议打法
This commit is contained in:
@@ -2,15 +2,89 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
|
|
||||||
const TOWER_META = [
|
// ── 塔的完整元数据(用于底部面板 + Tooltip) ──────────────────────────────
|
||||||
{ type: 'outsource', name: '外包程序员', cost: 30, desc: '近战 8伤', color: '#94A3B8', img: '/game-assets/tower-outsource.png', tip: '廉价但30%丢空,5%自伤' },
|
interface TowerInfo {
|
||||||
{ type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场:5%概率秒杀' },
|
type: string
|
||||||
{ type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助+20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' },
|
name: string
|
||||||
{ type: 'ops', name: '运营专员', cost: 90, desc: '溅射 18伤', color: '#8B5CF6', img: '/game-assets/tower-ops.png', tip: '增长黑客:20%双倍HC' },
|
cost: number
|
||||||
{ type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' },
|
color: string
|
||||||
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
|
img: string
|
||||||
{ type: 'pm', name: '产品经理', cost: 160, desc: '需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更:每4次把怪打回去' },
|
// Tooltip 内容
|
||||||
] as const
|
quote: string // 人物一句话名言/自我介绍
|
||||||
|
atk: string // 攻击类型
|
||||||
|
dmg: string // 伤害数值
|
||||||
|
range: string // 射程
|
||||||
|
speed: string // 攻速
|
||||||
|
skill: string // 特殊技能名
|
||||||
|
skillDesc: string // 技能描述
|
||||||
|
tactics: string // 建议打法
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOWER_META: TowerInfo[] = [
|
||||||
|
{
|
||||||
|
type: 'outsource', name: '外包程序员', cost: 30, color: '#94A3B8',
|
||||||
|
img: '/game-assets/tower-outsource.png',
|
||||||
|
quote: '"能跑就行,文档?没有!"',
|
||||||
|
atk: '近战', dmg: '8', range: '2格', speed: '0.7次/s',
|
||||||
|
skill: 'Bug自助餐',
|
||||||
|
skillDesc: '每次攻击30%概率丢空;5%概率Bug反弹,精力瞬间归零',
|
||||||
|
tactics: '前期凑数用,价格最便宜。不要指望他,放在路口堵一堵就够了。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'intern', name: '00后实习生', cost: 50, color: '#22C55E',
|
||||||
|
img: '/game-assets/tower-intern.png',
|
||||||
|
quote: '"准点下班,法律护身。"',
|
||||||
|
atk: '近战', dmg: '15', range: '1.5格', speed: '1.5次/s',
|
||||||
|
skill: '整顿职场',
|
||||||
|
skillDesc: '每次攻击5%概率直接秒杀普通怪物;每秒有1%概率自动"跑路",退还25HC',
|
||||||
|
tactics: '性价比最高的前排,攒够HC先多建几个。秒杀概率虽低但惊喜不断。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hrbp', name: 'HRBP', cost: 80, color: '#EC4899',
|
||||||
|
img: '/game-assets/tower-hrbp.png',
|
||||||
|
quote: '"公司是你家,福报靠大家。"',
|
||||||
|
atk: '辅助(无伤害)', dmg: '0', range: '1格光环', speed: '每0.5s触发',
|
||||||
|
skill: '打鸡血',
|
||||||
|
skillDesc: '持续给周围1格内所有塔附加+20%攻速BUFF;每次消耗自身5点精力',
|
||||||
|
tactics: '永远放在塔群中央!搭配P6或实习生使用效果翻倍,但别让她精力耗完。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ops', name: '运营专员', cost: 90, color: '#8B5CF6',
|
||||||
|
img: '/game-assets/tower-ops.png',
|
||||||
|
quote: '"数据不好看?A/B测!"',
|
||||||
|
atk: '远程 范围溅射', dmg: '18', range: '3格', speed: '1.2次/s',
|
||||||
|
skill: '增长黑客',
|
||||||
|
skillDesc: '每次命中20%概率触发"双倍HC",目标死亡时奖励翻倍;命中范围内相邻怪物也受溅射伤害',
|
||||||
|
tactics: '人多的时候最强,专门对付密集的应届生群体。顺手赚HC。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ppt', name: 'PPT大师', cost: 100, color: '#F59E0B',
|
||||||
|
img: '/game-assets/tower-ppt.png',
|
||||||
|
quote: '"底层逻辑要打通,顶层设计要对齐。"',
|
||||||
|
atk: 'AOE范围', dmg: '5(范围)', range: '3格圆形', speed: '1.5次/s',
|
||||||
|
skill: '黑话领域',
|
||||||
|
skillDesc: '攻击圆形范围内所有怪物并降低移速40%,持续2秒;越多怪物越划算',
|
||||||
|
tactics: '减速神器!放在路径转弯处效果最佳。搭配P6的DOT伤害组合拳秒杀老员工。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'senior', name: 'P6资深开发', cost: 120, color: '#3B82F6',
|
||||||
|
img: '/game-assets/tower-senior.png',
|
||||||
|
quote: '"这行代码谁写的?别问,是我。"',
|
||||||
|
atk: '远程直线', dmg: '30 + DOT 10/s×3s', range: '5格', speed: '1.0次/s',
|
||||||
|
skill: '代码屎山',
|
||||||
|
skillDesc: '子弹命中后附加DOT(每秒10伤,持续3秒);远程射程5格,直线穿透感',
|
||||||
|
tactics: '主力输出,射程超长。优先打护盾老员工和Boss,DOT能绕过护盾持续掉血。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'pm', name: '产品经理', cost: 160, color: '#06B6D4',
|
||||||
|
img: '/game-assets/tower-pm.png',
|
||||||
|
quote: '"这个需求很简单,下班前能上线吗?"',
|
||||||
|
atk: '远程曲线', dmg: '20', range: '4格', speed: '0.8次/s',
|
||||||
|
skill: '需求变更',
|
||||||
|
skillDesc: '每4次攻击触发"需求变更",将目标强制打回2个路径节点,大幅拖延到达时间',
|
||||||
|
tactics: '最贵但效果独特!把Boss和老员工不断打回去,配合其他输出塔可以让怪走不出去。',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm'
|
type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm'
|
||||||
|
|
||||||
@@ -44,6 +118,130 @@ const PUA_PLACEHOLDERS = [
|
|||||||
'大家加油,相信自己!',
|
'大家加油,相信自己!',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ── 塔的悬浮信息卡片 ─────────────────────────────────────────────────────────
|
||||||
|
function TowerTooltip({
|
||||||
|
tower,
|
||||||
|
pos,
|
||||||
|
canAfford,
|
||||||
|
}: {
|
||||||
|
tower: TowerInfo
|
||||||
|
pos: { left: number; bottom: number }
|
||||||
|
canAfford: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: pos.left,
|
||||||
|
bottom: pos.bottom,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
zIndex: 9999,
|
||||||
|
width: '220px',
|
||||||
|
backgroundColor: '#0a1628',
|
||||||
|
border: `2px solid ${tower.color}`,
|
||||||
|
borderRadius: '10px',
|
||||||
|
padding: '12px',
|
||||||
|
boxShadow: `0 0 24px ${tower.color}44, 0 8px 32px rgba(0,0,0,0.7)`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}>
|
||||||
|
{/* 小三角指示器 */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', bottom: '-8px', left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: 0, height: 0,
|
||||||
|
borderLeft: '7px solid transparent',
|
||||||
|
borderRight: '7px solid transparent',
|
||||||
|
borderTop: `7px solid ${tower.color}`,
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* 头部:头像 + 名字 + 费用 */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={tower.img} alt={tower.name}
|
||||||
|
style={{ width: '40px', height: '40px', objectFit: 'contain', flexShrink: 0 }} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '8px', color: tower.color, lineHeight: 1.4,
|
||||||
|
}}>
|
||||||
|
{tower.name}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '7px',
|
||||||
|
color: canAfford ? '#A78BFA' : '#EF4444',
|
||||||
|
marginTop: '3px',
|
||||||
|
}}>
|
||||||
|
{tower.cost} HC {!canAfford && '(不足)'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 名言 */}
|
||||||
|
<div style={{
|
||||||
|
fontFamily: 'VT323, monospace', fontSize: '13px',
|
||||||
|
color: '#94A3B8', fontStyle: 'italic',
|
||||||
|
borderLeft: `2px solid ${tower.color}66`,
|
||||||
|
paddingLeft: '8px', marginBottom: '8px', lineHeight: 1.3,
|
||||||
|
}}>
|
||||||
|
{tower.quote}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数值属性 */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid', gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: '3px 8px', marginBottom: '8px',
|
||||||
|
}}>
|
||||||
|
{[
|
||||||
|
['攻击', tower.atk],
|
||||||
|
['射程', tower.range],
|
||||||
|
['伤害', tower.dmg],
|
||||||
|
['攻速', tower.speed],
|
||||||
|
].map(([label, val]) => (
|
||||||
|
<div key={label} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<span style={{ fontFamily: 'VT323, monospace', fontSize: '10px', color: '#475569' }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: '#E2E8F0' }}>
|
||||||
|
{val}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 技能 */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: `${tower.color}15`,
|
||||||
|
border: `1px solid ${tower.color}40`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '6px 8px',
|
||||||
|
marginBottom: '7px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '7px', color: tower.color, marginBottom: '4px',
|
||||||
|
}}>
|
||||||
|
✦ {tower.skill}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontFamily: 'VT323, monospace', fontSize: '12px',
|
||||||
|
color: '#CBD5E1', lineHeight: 1.35,
|
||||||
|
}}>
|
||||||
|
{tower.skillDesc}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 打法建议 */}
|
||||||
|
<div style={{
|
||||||
|
fontFamily: 'VT323, monospace', fontSize: '12px',
|
||||||
|
color: '#64748B', lineHeight: 1.35,
|
||||||
|
borderTop: '1px solid #1e3a5f', paddingTop: '6px',
|
||||||
|
}}>
|
||||||
|
💡 {tower.tactics}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── 费用计算:基于当前HC的15%,最低20,最高200,取整到10的倍数 ────────────
|
// ── 费用计算:基于当前HC的15%,最低20,最高200,取整到10的倍数 ────────────
|
||||||
function calcPuaCost(hc: number): number {
|
function calcPuaCost(hc: number): number {
|
||||||
const raw = Math.ceil(hc * 0.15)
|
const raw = Math.ceil(hc * 0.15)
|
||||||
@@ -501,6 +699,10 @@ export default function GamePage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Tooltip 状态
|
||||||
|
const [hoveredTower, setHoveredTower] = useState<TowerInfo | null>(null)
|
||||||
|
const [tooltipPos, setTooltipPos] = useState<{ left: number; bottom: number } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
const initGame = async () => {
|
const initGame = async () => {
|
||||||
@@ -730,9 +932,20 @@ export default function GamePage() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={meta.type}
|
key={meta.type}
|
||||||
title={meta.tip}
|
onClick={() => canAfford && handleSelectTower(meta.type as TowerType)}
|
||||||
onClick={() => canAfford && handleSelectTower(meta.type)}
|
|
||||||
disabled={!canAfford}
|
disabled={!canAfford}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
const rect = (e.currentTarget as HTMLButtonElement).getBoundingClientRect()
|
||||||
|
setTooltipPos({
|
||||||
|
left: rect.left + rect.width / 2,
|
||||||
|
bottom: window.innerHeight - rect.top + 10,
|
||||||
|
})
|
||||||
|
setHoveredTower(meta)
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredTower(null)
|
||||||
|
setTooltipPos(null)
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
gap: '2px', padding: '5px 8px',
|
gap: '2px', padding: '5px 8px',
|
||||||
@@ -763,6 +976,15 @@ export default function GamePage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 塔 Tooltip 弹窗 */}
|
||||||
|
{hoveredTower && tooltipPos && (
|
||||||
|
<TowerTooltip
|
||||||
|
tower={hoveredTower}
|
||||||
|
pos={tooltipPos}
|
||||||
|
canAfford={hc >= hoveredTower.cost}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user