Files
test1/app/game/page.tsx

439 lines
16 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
}
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是福报感恩公司给的机会',
'我们要有狼性精神,卷赢对手!',
'大家加油,相信自己!',
]
// ── PUA 输入面板 ─────────────────────────────────────────────────────────────
function PuaPanel({ gameReady }: { gameReady: boolean }) {
const [text, setText] = useState('')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<PuaResult | null>(null)
const [history, setHistory] = useState<PuaResult[]>([])
const textareaRef = useRef<HTMLTextAreaElement>(null)
const placeholder = PUA_PLACEHOLDERS[Math.floor(Math.random() * PUA_PLACEHOLDERS.length)]
const handleSubmit = useCallback(async () => {
if (!text.trim() || loading || !gameReady) return
setLoading(true)
setResult(null)
try {
const res = await fetch('/api/pua-score', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text.trim() }),
})
const data: PuaResult = await res.json()
setResult(data)
setHistory(prev => [data, ...prev].slice(0, 5))
// 通知游戏场景应用 buff
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])
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={{
width: '240px',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.97)',
borderLeft: '2px solid #1e3a5f',
display: 'flex',
flexDirection: 'column',
padding: '12px 10px',
gap: '10px',
overflow: 'hidden',
}}>
{/* 标题 */}
<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',
}}>
AI判断鸡血值
</div>
</div>
{/* 输入框 */}
<textarea
ref={textareaRef}
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={loading || !gameReady}
rows={4}
style={{
width: '100%',
backgroundColor: '#0F1B2D',
border: '1px solid #1e3a5f',
borderRadius: '6px',
color: '#E2E8F0',
fontFamily: 'VT323, monospace',
fontSize: '14px',
padding: '8px',
resize: 'none',
outline: 'none',
lineHeight: 1.4,
opacity: gameReady ? 1 : 0.5,
}}
/>
{/* 提交按钮 */}
<button
onClick={handleSubmit}
disabled={loading || !text.trim() || !gameReady}
style={{
width: '100%',
padding: '8px',
backgroundColor: loading ? '#1e3a5f' : '#7C3AED',
border: 'none',
borderRadius: '6px',
color: '#E2E8F0',
fontFamily: "'Press Start 2P', monospace",
fontSize: '8px',
cursor: loading || !text.trim() || !gameReady ? 'not-allowed' : 'pointer',
opacity: !text.trim() || !gameReady ? 0.5 : 1,
transition: 'all 0.15s',
letterSpacing: '0.5px',
}}
>
{loading ? '分析中...' : '发起激励 ▶'}
</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>
{/* 效果标签 */}
<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,
textAlign: 'center',
}}>
{em.label}
</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.4,
marginTop: 'auto',
borderTop: '1px solid #1e3a5f',
paddingTop: '8px',
}}>
Enter <br />
1-3分: 废话翻车<br />
4-6分: 小幅加成<br />
7-8分: 攻速暴增<br />
9-10分: 全场狂暴
</div>
</div>
)
}
// ── 主游戏页面 ────────────────────────────────────────────────────────────────
export default function GamePage() {
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
const [hc, setHc] = useState(200)
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false)
const selectedTowerRef = useRef<TowerType | null>(null)
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) }
}
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'].forEach(k => {
delete (window as any)[k]
})
}
}
}, [])
return (
<div className="w-full h-screen flex flex-col overflow-hidden"
style={{ backgroundColor: '#0A1628' }}>
{/* 中间行:游戏画布 + PUA面板 */}
<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' }}
/>
{/* PUA 激励台(右侧) */}
<PuaPanel gameReady={gameReady} />
</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>
)
}