Files
test1/app/game/page.tsx

769 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
const TOWER_META = [
{ type: 'outsource', name: '外包程序员', cost: 30, desc: '近战 8伤', color: '#94A3B8', img: '/game-assets/tower-outsource.png', tip: '廉价但30%丢空5%自伤' },
{ type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场5%概率秒杀' },
{ type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助+20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' },
{ type: 'ops', name: '运营专员', cost: 90, desc: '溅射 18伤', color: '#8B5CF6', img: '/game-assets/tower-ops.png', tip: '增长黑客20%双倍HC' },
{ type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域减速40%' },
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
{ type: 'pm', name: '产品经理', cost: 160, desc: '需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更每4次把怪打回去' },
] as const
type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm'
type EffectType = 'attack_boost' | 'speed_boost' | 'money_rain' | 'rage_mode' | 'backfire'
interface PuaResult {
score: number
title: string
desc: string
effect: EffectType
cost?: number
similarity?: number
similarTo?: string
isDuplicate?: boolean
penaltyMultiplier?: number
}
const EFFECT_META: Record<EffectType, { label: string; color: string; icon: string }> = {
attack_boost: { label: '攻击力提升', color: '#22C55E', icon: '⚔️' },
speed_boost: { label: '攻速暴增', color: '#FBBF24', icon: '⚡' },
money_rain: { label: 'HC暴击', color: '#A78BFA', icon: '💰' },
rage_mode: { label: '全场狂暴', color: '#FF4E00', icon: '🔥' },
backfire: { label: '废话翻车', color: '#6B7280', icon: '💀' },
}
const PUA_PLACEHOLDERS = [
'今天是本季度最关键的一天,大家冲!',
'不拼搏,对不起父母的养育之恩...',
'996是福报感恩公司给的机会',
'我们要有狼性精神,卷赢对手!',
'大家加油,相信自己!',
]
// ── 费用计算基于当前HC的15%最低20最高200取整到10的倍数 ────────────
function calcPuaCost(hc: number): number {
const raw = Math.ceil(hc * 0.15)
const rounded = Math.ceil(raw / 10) * 10 // 向上取到整十
return Math.max(20, Math.min(200, rounded))
}
// ── PUA 输入面板 ─────────────────────────────────────────────────────────────
function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: number; waveStarted: boolean }) {
const [text, setText] = useState('')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<PuaResult | null>(null)
const [history, setHistory] = useState<PuaResult[]>([])
// 历史原文列表(用于发给服务端做相似度检测)
const historyTexts = useRef<string[]>([])
const [insufficient, setInsufficient] = useState(false)
const [dupWarning, setDupWarning] = useState<string | null>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const placeholder = useRef(PUA_PLACEHOLDERS[Math.floor(Math.random() * PUA_PLACEHOLDERS.length)]).current
const cost = calcPuaCost(hc)
const canAfford = hc >= cost
const handleSubmit = useCallback(async () => {
if (!text.trim() || loading || !gameReady || !waveStarted) return
const spendHC: ((n: number) => boolean) | undefined =
typeof window !== 'undefined' ? (window as any).__gameSpendHC : undefined
const currentHC: number =
typeof window !== 'undefined'
? ((window as any).__gameGetHC?.() ?? hc)
: hc
const actualCost = calcPuaCost(currentHC)
// 扣除基础费用
if (!spendHC?.(actualCost)) {
setInsufficient(true)
setTimeout(() => setInsufficient(false), 2000)
return
}
setLoading(true)
setResult(null)
setDupWarning(null)
setInsufficient(false)
try {
const res = await fetch('/api/pua-score', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text.trim(),
history: historyTexts.current,
}),
})
const data: PuaResult = await res.json()
data.cost = actualCost
// 处理重复惩罚:额外扣 HC
const penalty = data.penaltyMultiplier ?? 0
if (penalty > 0 && data.isDuplicate) {
const extraCost = Math.round(actualCost * penalty)
const penaltyPaid = spendHC?.(extraCost) ?? false
if (penaltyPaid) {
data.cost = actualCost + extraCost
// 显示重复警告
const sim = data.similarity ?? 0
const level = sim >= 0.8 ? '严重重复' : '内容相似'
setDupWarning(
`检测到${level}!额外扣除 ${extraCost} HC` +
(data.similarTo ? `\n与"${data.similarTo}"相似)` : '')
)
}
}
setResult(data)
// 更新历史原文列表最近8条
historyTexts.current = [text.trim(), ...historyTexts.current].slice(0, 8)
setHistory(prev => [data, ...prev].slice(0, 5))
if (typeof window !== 'undefined') {
;(window as any).__gamePuaBuff?.(data.effect, data.score, data.title)
}
} catch {
setResult({ score: 1, title: '网络故障', desc: 'AI开小差了', effect: 'backfire' })
} finally {
setLoading(false)
}
}, [text, loading, gameReady, hc, waveStarted])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}
const scoreColor = (s: number) =>
s >= 9 ? '#FF4E00' : s >= 7 ? '#FBBF24' : s >= 4 ? '#22C55E' : '#6B7280'
return (
<div style={{
display: 'flex',
flexDirection: 'column',
padding: '10px 10px 6px',
gap: '8px',
overflow: 'hidden auto',
flex: 1,
}}>
{/* 标题 */}
<div style={{ textAlign: 'center' }}>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px',
color: '#EC4899',
letterSpacing: '0.5px',
lineHeight: 1.6,
}}>
PUA
</div>
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '12px',
color: '#475569',
marginTop: '2px',
}}>
{!waveStarted ? '⚠ 召唤第一波后才能激励' : '输入打鸡血的话AI判断鸡血值'}
</div>
</div>
{/* 费用提示 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#0a1628',
borderRadius: '6px',
padding: '6px 8px',
border: `1px solid ${insufficient ? '#EF4444' : canAfford ? '#1e3a5f' : '#7F1D1D'}`,
transition: 'border-color 0.2s',
}}>
<span style={{
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#64748B',
}}>
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '9px',
color: canAfford ? '#A78BFA' : '#EF4444',
transition: 'color 0.2s',
}}>
-{cost} HC
</span>
{!canAfford && (
<span style={{
fontFamily: 'VT323, monospace',
fontSize: '11px',
color: '#EF4444',
}}>
</span>
)}
</div>
</div>
{/* HC 不足提示 */}
{insufficient && (
<div style={{
backgroundColor: '#7F1D1D',
border: '1px solid #EF4444',
borderRadius: '6px',
padding: '6px 8px',
fontFamily: 'VT323, monospace',
fontSize: '14px',
color: '#FCA5A5',
textAlign: 'center',
}}>
HC
</div>
)}
{/* 重复惩罚警告 */}
{dupWarning && (
<div style={{
backgroundColor: '#431407',
border: '1px solid #F97316',
borderRadius: '6px',
padding: '7px 8px',
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#FED7AA',
lineHeight: 1.4,
whiteSpace: 'pre-line',
}}>
{dupWarning}
</div>
)}
{/* 输入框 */}
<textarea
ref={textareaRef}
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={loading || !gameReady || !waveStarted}
rows={4}
style={{
width: '100%',
backgroundColor: '#0F1B2D',
border: `1px solid ${!waveStarted ? '#1e3a5f' : canAfford ? '#1e3a5f' : '#7F1D1D'}`,
borderRadius: '6px',
color: '#E2E8F0',
fontFamily: 'VT323, monospace',
fontSize: '14px',
padding: '8px',
resize: 'none',
outline: 'none',
lineHeight: 1.4,
opacity: gameReady && waveStarted ? 1 : 0.4,
transition: 'border-color 0.2s',
}}
/>
{/* 提交按钮 */}
<button
onClick={handleSubmit}
disabled={loading || !text.trim() || !gameReady || !canAfford || !waveStarted}
style={{
width: '100%',
padding: '8px',
backgroundColor: loading ? '#1e3a5f' : !waveStarted ? '#1e3a5f' : canAfford ? '#7C3AED' : '#4C1D95',
border: 'none',
borderRadius: '6px',
color: '#E2E8F0',
fontFamily: "'Press Start 2P', monospace",
fontSize: '8px',
cursor: loading || !text.trim() || !gameReady || !canAfford || !waveStarted ? 'not-allowed' : 'pointer',
opacity: !text.trim() || !gameReady || !canAfford || !waveStarted ? 0.45 : 1,
transition: 'all 0.15s',
letterSpacing: '0.5px',
}}
>
{loading ? '分析中...' : !waveStarted ? '战斗开始后可用' : !canAfford ? 'HC不足' : `发起激励 -${cost}HC`}
</button>
{/* 当前结果 */}
{result && (() => {
const em = EFFECT_META[result.effect]
return (
<div style={{
backgroundColor: '#0F1B2D',
border: `1px solid ${em.color}44`,
borderRadius: '8px',
padding: '10px',
display: 'flex',
flexDirection: 'column',
gap: '6px',
}}>
{/* 分数 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: '#94A3B8' }}>
</span>
<div style={{ display: 'flex', gap: '2px' }}>
{Array.from({ length: 10 }).map((_, i) => (
<div key={i} style={{
width: '8px', height: '8px', borderRadius: '1px',
backgroundColor: i < result.score ? scoreColor(result.score) : '#1e3a5f',
}} />
))}
</div>
</div>
{/* 效果名 */}
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '9px',
color: em.color,
textAlign: 'center',
padding: '4px 0',
}}>
{em.icon} {result.title}
</div>
{/* 点评 */}
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#94A3B8',
textAlign: 'center',
lineHeight: 1.3,
}}>
{result.desc}
</div>
{/* 相似度指示(有重复时显示) */}
{(result.similarity ?? 0) >= 0.3 && (
<div style={{
backgroundColor: '#431407',
border: '1px solid #F97316',
borderRadius: '4px',
padding: '4px 6px',
fontFamily: 'VT323, monospace',
fontSize: '12px',
color: '#FED7AA',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
<span></span>
<span style={{ color: (result.similarity ?? 0) >= 0.8 ? '#EF4444' : '#F97316' }}>
{Math.round((result.similarity ?? 0) * 100)}%
</span>
</div>
{/* 相似度进度条 */}
<div style={{ height: '3px', backgroundColor: '#1c0a04', borderRadius: '2px' }}>
<div style={{
height: '100%',
width: `${(result.similarity ?? 0) * 100}%`,
backgroundColor: (result.similarity ?? 0) >= 0.8 ? '#EF4444' : '#F97316',
borderRadius: '2px',
transition: 'width 0.3s ease',
}} />
</div>
</div>
)}
{/* 效果标签 + 花费 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{
backgroundColor: `${em.color}22`,
border: `1px solid ${em.color}55`,
borderRadius: '4px',
padding: '3px 6px',
fontFamily: 'VT323, monospace',
fontSize: '12px',
color: em.color,
}}>
{em.label}
</div>
{result.cost && (
<span style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px',
color: '#475569',
}}>
-{result.cost}HC
</span>
)}
</div>
</div>
)
})()}
{/* 历史记录 */}
{history.length > 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '6px',
color: '#334155',
marginBottom: '2px',
}}>
HISTORY
</div>
{history.slice(1).map((h, i) => {
const em = EFFECT_META[h.effect]
return (
<div key={i} style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#0a1628',
borderRadius: '4px',
padding: '4px 6px',
}}>
<span style={{
fontFamily: 'VT323, monospace', fontSize: '12px',
color: '#475569', flex: 1,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{em.icon} {h.title}
</span>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px',
color: scoreColor(h.score),
marginLeft: '4px',
}}>
{h.score}
</div>
</div>
)
})}
</div>
)}
{/* 说明 */}
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '11px',
color: '#334155',
lineHeight: 1.5,
marginTop: 'auto',
borderTop: '1px solid #1e3a5f',
paddingTop: '8px',
}}>
<span style={{ color: '#475569' }}> = HC × 15%</span><br />
<br />
60%: <br />
80%: <br />
HC<br />
<br />
9-10分: 全场狂暴<br />
7-8分: 攻速暴增<br />
4-6分: 攻击+HC<br />
1-2分: 废话翻车
</div>
</div>
)
}
// ── 主游戏页面 ────────────────────────────────────────────────────────────────
export default function GamePage() {
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
const [hc, setHc] = useState(170)
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false)
const [waveStarted, setWaveStarted] = 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 handleMeeting = useCallback(() => {
if (!gameReady) return
if (!inMeeting) {
const ok = typeof window !== 'undefined' && (window as any).__gamePause?.()
if (ok) setInMeeting(true)
} else {
const ok = typeof window !== 'undefined' && (window as any).__gameResume?.()
if (ok) setInMeeting(false)
}
}, [gameReady, inMeeting])
const handleSelectTower = useCallback((type: TowerType) => {
const next = selectedTowerRef.current === type ? null : type
selectedTowerRef.current = next
setSelectedTower(next)
if (typeof window !== 'undefined') {
(window as any).__gameSelectTower?.(next)
}
}, [])
useEffect(() => {
let mounted = true
const initGame = async () => {
const Phaser = (await import('phaser')).default
const { createGameConfig } = await import('@/game/config')
const { createGameScene } = await import('@/game/GameScene')
if (!mounted) return
if (typeof window !== 'undefined') {
const storedDifficulty = localStorage.getItem('game-difficulty')
;(window as any).__gameDifficulty =
storedDifficulty === 'easy' || storedDifficulty === 'hard'
? storedDifficulty : 'normal'
}
const GameScene = createGameScene(Phaser)
const config = createGameConfig('game-canvas-container')
config.scene = [GameScene]
if (config.scale) {
config.scale.mode = Phaser.Scale.FIT
config.scale.autoCenter = Phaser.Scale.CENTER_BOTH
}
config.type = Phaser.AUTO
if (typeof window !== 'undefined') {
(window as any).__gameOnHCChange = (val: number) => { if (mounted) setHc(val) }
(window as any).__gameOnTowerDeselect = () => {
if (mounted) { selectedTowerRef.current = null; setSelectedTower(null) }
}
;(window as any).__gameReady = () => { if (mounted) setGameReady(true) }
// HUD 通过此回调更新召唤按钮状态
;(window as any).__gameSetWaveBtn = (s: { text: string; disabled: boolean }) => {
;(window as any).__gameWaveBtnState = s
if (mounted) setWaveBtn({ ...s })
}
// 轮询 __gameWaveStartedPhaser 设置后通知 React
const checkWaveStarted = setInterval(() => {
if ((window as any).__gameWaveStarted) {
if (mounted) setWaveStarted(true)
clearInterval(checkWaveStarted)
}
}, 200)
}
gameRef.current = new Phaser.Game(config)
}
initGame().catch(console.error)
return () => {
mounted = false
gameRef.current?.destroy(true)
gameRef.current = null
if (typeof window !== 'undefined') {
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
'__gameReady','__gameDifficulty','__gamePuaBuff',
'__gameGetHC','__gameSpendHC','__gameWaveStarted',
'__gameSetWaveBtn','__gameWaveBtnState','__gameOnWaveClick',
'__gamePause','__gameResume','__gameIsPaused'].forEach(k => {
delete (window as any)[k]
})
}
}
}, [])
return (
<div className="w-full h-screen flex flex-col overflow-hidden"
style={{ backgroundColor: '#0A1628' }}>
{/* 中间行:游戏画布 + 右侧控制面板 */}
<div className="flex-1 min-h-0 flex flex-row overflow-hidden">
{/* 游戏画布 */}
<div
id="game-canvas-container"
className="flex-1 min-w-0 min-h-0"
style={{ backgroundColor: '#0A1628' }}
/>
{/* 右侧面板HC + 开会按钮 + 召唤按钮 + PUA激励台 */}
<div style={{
width: '240px',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.97)',
borderLeft: `2px solid ${inMeeting ? '#22C55E' : '#1e3a5f'}`,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
transition: 'border-color 0.2s',
boxShadow: inMeeting ? 'inset 0 0 20px rgba(34,197,94,0.08)' : 'none',
}}>
{/* HC 数量 + 开会按钮(同一行) */}
<div style={{
padding: '8px 12px',
borderBottom: '1px solid #1e3a5f',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#475569', marginBottom: '1px' }}>
</div>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '11px',
color: '#A78BFA',
letterSpacing: '1px',
}}>
{hc} HC
</div>
</div>
{/* 开会按钮 */}
<button
onClick={handleMeeting}
disabled={!gameReady || !waveStarted}
title={inMeeting ? '结束开会,恢复游戏' : '开会暂停,发起激励'}
style={{
flexShrink: 0,
padding: '6px 10px',
backgroundColor: inMeeting ? '#14532D' : '#0F1B2D',
border: `2px solid ${inMeeting ? '#22C55E' : '#1e3a5f'}`,
borderRadius: '8px',
color: inMeeting ? '#4ADE80' : (!gameReady || !waveStarted ? '#334155' : '#94A3B8'),
fontFamily: 'VT323, monospace',
fontSize: '14px',
cursor: !gameReady || !waveStarted ? 'not-allowed' : 'pointer',
transition: 'all 0.15s',
whiteSpace: 'nowrap',
lineHeight: 1.2,
boxShadow: inMeeting ? '0 0 10px rgba(34,197,94,0.3)' : 'none',
}}
>
{inMeeting ? '📋 结束开会' : '📋 开会'}
</button>
</div>
{/* 开会中提示条 */}
{inMeeting && (
<div style={{
backgroundColor: '#14532D',
borderBottom: '1px solid #22C55E',
padding: '5px 12px',
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#86EFAC',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<span style={{ animation: 'pulse 1.5s ease-in-out infinite' }}></span>
</div>
)}
{/* 召唤下一波按钮(开会时禁用) */}
<div style={{ padding: '8px 12px', borderBottom: '2px solid #1e3a5f' }}>
<button
onClick={() => {
if (inMeeting) return
if (typeof window !== 'undefined') {
(window as any).__gameOnWaveClick?.()
}
}}
disabled={waveBtn.disabled || !gameReady || inMeeting}
style={{
width: '100%',
padding: '10px 8px',
backgroundColor: waveBtn.disabled || !gameReady || inMeeting ? '#0F172A' : '#1e3a5f',
border: `2px solid ${waveBtn.disabled || !gameReady || inMeeting ? '#1e293b' : '#7C3AED'}`,
borderRadius: '8px',
color: waveBtn.disabled || !gameReady || inMeeting ? '#4B5563' : '#C4B5FD',
fontFamily: 'VT323, monospace',
fontSize: '20px',
cursor: waveBtn.disabled || !gameReady || inMeeting ? 'not-allowed' : 'pointer',
transition: 'all 0.15s ease',
letterSpacing: '1px',
lineHeight: 1.2,
boxShadow: waveBtn.disabled || !gameReady || inMeeting ? 'none' : '0 0 12px rgba(124,58,237,0.3)',
}}
onMouseEnter={e => {
if (!waveBtn.disabled && gameReady && !inMeeting) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = '#2d3a5e'
;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 18px rgba(124,58,237,0.5)'
}
}}
onMouseLeave={e => {
if (!waveBtn.disabled && gameReady && !inMeeting) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = '#1e3a5f'
;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 12px rgba(124,58,237,0.3)'
}
}}
>
{inMeeting ? '开会中...' : waveBtn.text}
</button>
</div>
{/* PUA 激励台 */}
<PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted || inMeeting} />
</div>
</div>
{/* 底部塔选择面板 */}
<div style={{
backgroundColor: 'rgba(10,18,40,0.97)',
borderTop: '2px solid #1e3a5f',
padding: '6px 12px',
display: 'flex',
gap: '8px',
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0,
zIndex: 50,
boxShadow: '0 -4px 20px rgba(124,58,237,0.15)',
overflowX: 'auto',
}}>
{/* 选塔提示 */}
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px', color: '#7C3AED',
marginRight: '6px', whiteSpace: 'nowrap',
opacity: gameReady ? 1 : 0.3,
}}>
{selectedTower ? '点击格子建造' : '选择塔 ▼'}
</div>
{TOWER_META.map((meta) => {
const canAfford = hc >= meta.cost
const isSelected = selectedTower === meta.type
return (
<button
key={meta.type}
title={meta.tip}
onClick={() => canAfford && handleSelectTower(meta.type)}
disabled={!canAfford}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: '2px', padding: '5px 8px',
backgroundColor: isSelected ? '#1e3a5f' : '#0F1B2D',
border: `2px solid ${isSelected ? meta.color : '#1e3a5f'}`,
borderRadius: '8px',
cursor: canAfford ? 'pointer' : 'not-allowed',
minWidth: '76px',
opacity: canAfford ? 1 : 0.4,
transition: 'all 0.15s ease',
boxShadow: isSelected ? `0 0 10px ${meta.color}66` : 'none',
transform: isSelected ? 'translateY(-2px)' : 'none',
flexShrink: 0,
}}
>
<div style={{ width: '40px', height: '40px', flexShrink: 0 }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={meta.img} alt={meta.name}
style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
</div>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '12px', color: meta.color, textAlign: 'center', lineHeight: 1 }}>
{meta.name}
</span>
<span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '7px', color: '#A78BFA' }}>
{meta.cost}HC
</span>
</button>
)
})}
</div>
</div>
)
}