feat(game): 添加PUA激励台——AI评分打鸡血,触发攻击/攻速/HC/狂暴/翻车等游戏效果;移除静音按钮BGM默认开启
This commit is contained in:
71
app/api/pua-score/route.ts
Normal file
71
app/api/pua-score/route.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { llmClient } from '@/app/api/llm-client'
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `你是一个专门分析互联网大厂老板PUA话术的AI裁判。
|
||||||
|
用户输入一段老板/领导对员工说的"打鸡血"或"PUA"的话,你需要分析这段话并给出评分和游戏效果。
|
||||||
|
|
||||||
|
评分标准(1-10分):
|
||||||
|
1-3分:废话/没营养("大家加油"之类)→ 轻微效果
|
||||||
|
4-6分:标准大厂黑话("对齐""闭环""狼性")→ 中等效果
|
||||||
|
7-8分:强力PUA("996是福报""不拼搏对不起父母")→ 强力效果
|
||||||
|
9-10分:终极毒鸡汤(极限施压/情感绑架)→ 全场爆发
|
||||||
|
|
||||||
|
只返回如下JSON,不要其他内容:
|
||||||
|
{
|
||||||
|
"score": 数字(1-10),
|
||||||
|
"title": "效果名称(2-6个汉字,有创意)",
|
||||||
|
"desc": "对话语的一句话点评(15字以内,带点讽刺)",
|
||||||
|
"effect": "attack_boost" | "speed_boost" | "money_rain" | "rage_mode" | "backfire"
|
||||||
|
}
|
||||||
|
|
||||||
|
effect说明:
|
||||||
|
- attack_boost: 攻击力提升(score 4-6)
|
||||||
|
- speed_boost: 攻击速度提升(score 7-8)
|
||||||
|
- money_rain: HC暴增(score 5-7,话语中强调利益)
|
||||||
|
- rage_mode: 全场狂暴(score 9-10)
|
||||||
|
- backfire: 话太废了反而debuff(score 1-2)`
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { text } = await req.json()
|
||||||
|
if (!text || typeof text !== 'string' || text.trim().length < 2) {
|
||||||
|
return NextResponse.json({ error: '输入内容太短了' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await llmClient.chat({
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: SYSTEM_PROMPT },
|
||||||
|
{ role: 'user', content: text.slice(0, 200) },
|
||||||
|
],
|
||||||
|
maxTokens: 150,
|
||||||
|
temperature: 0.7,
|
||||||
|
})
|
||||||
|
|
||||||
|
const raw = resp.choices?.[0]?.message?.content ?? ''
|
||||||
|
|
||||||
|
// 提取 JSON(防止 LLM 多输出文字)
|
||||||
|
const match = raw.match(/\{[\s\S]*\}/)
|
||||||
|
if (!match) throw new Error('LLM 返回格式异常')
|
||||||
|
|
||||||
|
const result = JSON.parse(match[0])
|
||||||
|
const score = Math.max(1, Math.min(10, Number(result.score) || 5))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
score,
|
||||||
|
title: result.title || '打鸡血',
|
||||||
|
desc: result.desc || '还行吧',
|
||||||
|
effect: result.effect || 'attack_boost',
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[pua-score]', e)
|
||||||
|
// 降级:随机给个分数
|
||||||
|
const score = Math.floor(Math.random() * 7) + 3
|
||||||
|
return NextResponse.json({
|
||||||
|
score,
|
||||||
|
title: '随机鸡血',
|
||||||
|
desc: 'AI开小差了,随机发力',
|
||||||
|
effect: 'attack_boost',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,293 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
|
|
||||||
const TOWER_META = [
|
const TOWER_META = [
|
||||||
{ type: 'outsource', name: '外包程序员', cost: 30, desc: '近战 8伤 0.7/s', color: '#94A3B8', img: '/game-assets/tower-outsource.png', tip: '廉价但30%丢空,5%自伤' },
|
{ 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伤 1.5/s', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场: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: '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: '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减速 5伤', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' },
|
{ 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伤 5格射程', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
|
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
|
||||||
{ type: 'pm', name: '产品经理', cost: 160, desc: '远程 20伤 需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更:每4次把怪打回去' },
|
{ type: 'pm', name: '产品经理', cost: 160, desc: '需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更:每4次把怪打回去' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm'
|
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() {
|
export default function GamePage() {
|
||||||
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
|
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
|
||||||
const [hc, setHc] = useState(200)
|
const [hc, setHc] = useState(200)
|
||||||
@@ -32,28 +308,22 @@ export default function GamePage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
|
|
||||||
const initGame = async () => {
|
const initGame = async () => {
|
||||||
const Phaser = (await import('phaser')).default
|
const Phaser = (await import('phaser')).default
|
||||||
const { createGameConfig } = await import('@/game/config')
|
const { createGameConfig } = await import('@/game/config')
|
||||||
const { createGameScene } = await import('@/game/GameScene')
|
const { createGameScene } = await import('@/game/GameScene')
|
||||||
|
|
||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
|
|
||||||
// 从 localStorage 读取难度,通过 window 变量传给场景
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const storedDifficulty = localStorage.getItem('game-difficulty')
|
const storedDifficulty = localStorage.getItem('game-difficulty')
|
||||||
if (storedDifficulty === 'easy' || storedDifficulty === 'normal' || storedDifficulty === 'hard') {
|
;(window as any).__gameDifficulty =
|
||||||
;(window as any).__gameDifficulty = storedDifficulty
|
storedDifficulty === 'easy' || storedDifficulty === 'hard'
|
||||||
} else {
|
? storedDifficulty : 'normal'
|
||||||
;(window as any).__gameDifficulty = 'normal'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameScene = createGameScene(Phaser)
|
const GameScene = createGameScene(Phaser)
|
||||||
const config = createGameConfig('game-canvas-container')
|
const config = createGameConfig('game-canvas-container')
|
||||||
config.scene = [GameScene]
|
config.scene = [GameScene]
|
||||||
|
|
||||||
if (config.scale) {
|
if (config.scale) {
|
||||||
config.scale.mode = Phaser.Scale.FIT
|
config.scale.mode = Phaser.Scale.FIT
|
||||||
config.scale.autoCenter = Phaser.Scale.CENTER_BOTH
|
config.scale.autoCenter = Phaser.Scale.CENTER_BOTH
|
||||||
@@ -61,68 +331,66 @@ export default function GamePage() {
|
|||||||
config.type = Phaser.AUTO
|
config.type = Phaser.AUTO
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
(window as any).__gameOnHCChange = (val: number) => {
|
(window as any).__gameOnHCChange = (val: number) => { if (mounted) setHc(val) }
|
||||||
if (mounted) setHc(val)
|
|
||||||
}
|
|
||||||
(window as any).__gameOnTowerDeselect = () => {
|
(window as any).__gameOnTowerDeselect = () => {
|
||||||
if (mounted) {
|
if (mounted) { selectedTowerRef.current = null; setSelectedTower(null) }
|
||||||
selectedTowerRef.current = null
|
|
||||||
setSelectedTower(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
;(window as any).__gameReady = () => {
|
|
||||||
if (mounted) setGameReady(true)
|
|
||||||
}
|
}
|
||||||
|
;(window as any).__gameReady = () => { if (mounted) setGameReady(true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
gameRef.current = new Phaser.Game(config)
|
gameRef.current = new Phaser.Game(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
initGame().catch(console.error)
|
initGame().catch(console.error)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false
|
mounted = false
|
||||||
gameRef.current?.destroy(true)
|
gameRef.current?.destroy(true)
|
||||||
gameRef.current = null
|
gameRef.current = null
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
delete (window as any).__gameOnHCChange
|
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
|
||||||
delete (window as any).__gameOnTowerDeselect
|
'__gameReady','__gameDifficulty','__gamePuaBuff'].forEach(k => {
|
||||||
delete (window as any).__gameSelectTower
|
delete (window as any)[k]
|
||||||
delete (window as any).__gameReady
|
})
|
||||||
delete (window as any).__gameDifficulty
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full h-screen flex flex-col overflow-hidden"
|
||||||
className="w-full h-screen flex flex-col overflow-hidden"
|
style={{ backgroundColor: '#0A1628' }}>
|
||||||
style={{ backgroundColor: '#0A1628' }}
|
|
||||||
>
|
{/* 中间行:游戏画布 + PUA面板 */}
|
||||||
{/* Phaser canvas 区域 */}
|
<div className="flex-1 min-h-0 flex flex-row overflow-hidden">
|
||||||
<div
|
{/* 游戏画布 */}
|
||||||
id="game-canvas-container"
|
<div
|
||||||
className="flex-1 min-h-0 w-full"
|
id="game-canvas-container"
|
||||||
style={{ backgroundColor: '#0A1628' }}
|
className="flex-1 min-w-0 min-h-0"
|
||||||
/>
|
style={{ backgroundColor: '#0A1628' }}
|
||||||
|
/>
|
||||||
|
{/* PUA 激励台(右侧) */}
|
||||||
|
<PuaPanel gameReady={gameReady} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 底部塔选择面板 */}
|
{/* 底部塔选择面板 */}
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
backgroundColor: 'rgba(10,18,40,0.97)',
|
||||||
backgroundColor: 'rgba(10,18,40,0.97)',
|
borderTop: '2px solid #1e3a5f',
|
||||||
borderTop: '2px solid #1e3a5f',
|
padding: '6px 12px',
|
||||||
padding: '8px 16px',
|
display: 'flex',
|
||||||
display: 'flex',
|
gap: '8px',
|
||||||
gap: '10px',
|
justifyContent: 'center',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
flexShrink: 0,
|
||||||
flexShrink: 0,
|
zIndex: 50,
|
||||||
zIndex: 50,
|
boxShadow: '0 -4px 20px rgba(124,58,237,0.15)',
|
||||||
boxShadow: '0 -4px 20px rgba(124,58,237,0.15)',
|
overflowX: 'auto',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{/* 选塔提示 */}
|
{/* 选塔提示 */}
|
||||||
<div style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#7C3AED', marginRight: '8px', whiteSpace: 'nowrap', opacity: gameReady ? 1 : 0.3 }}>
|
<div style={{
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '7px', color: '#7C3AED',
|
||||||
|
marginRight: '6px', whiteSpace: 'nowrap',
|
||||||
|
opacity: gameReady ? 1 : 0.3,
|
||||||
|
}}>
|
||||||
{selectedTower ? '点击格子建造' : '选择塔 ▼'}
|
{selectedTower ? '点击格子建造' : '选择塔 ▼'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,38 +404,30 @@ export default function GamePage() {
|
|||||||
onClick={() => canAfford && handleSelectTower(meta.type)}
|
onClick={() => canAfford && handleSelectTower(meta.type)}
|
||||||
disabled={!canAfford}
|
disabled={!canAfford}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
flexDirection: 'column',
|
gap: '2px', padding: '5px 8px',
|
||||||
alignItems: 'center',
|
|
||||||
gap: '3px',
|
|
||||||
padding: '6px 10px',
|
|
||||||
backgroundColor: isSelected ? '#1e3a5f' : '#0F1B2D',
|
backgroundColor: isSelected ? '#1e3a5f' : '#0F1B2D',
|
||||||
border: `2px solid ${isSelected ? meta.color : '#1e3a5f'}`,
|
border: `2px solid ${isSelected ? meta.color : '#1e3a5f'}`,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
cursor: canAfford ? 'pointer' : 'not-allowed',
|
cursor: canAfford ? 'pointer' : 'not-allowed',
|
||||||
minWidth: '90px',
|
minWidth: '76px',
|
||||||
opacity: canAfford ? 1 : 0.4,
|
opacity: canAfford ? 1 : 0.4,
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
boxShadow: isSelected ? `0 0 12px ${meta.color}66` : 'none',
|
boxShadow: isSelected ? `0 0 10px ${meta.color}66` : 'none',
|
||||||
transform: isSelected ? 'translateY(-3px)' : 'none',
|
transform: isSelected ? 'translateY(-2px)' : 'none',
|
||||||
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ width: '48px', height: '48px', position: 'relative', flexShrink: 0 }}>
|
<div style={{ width: '40px', height: '40px', flexShrink: 0 }}>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img src={meta.img} alt={meta.name}
|
||||||
src={meta.img}
|
style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||||
alt={meta.name}
|
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'contain', imageRendering: 'auto' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: meta.color, textAlign: 'center', lineHeight: 1.1 }}>
|
<span style={{ fontFamily: 'VT323, monospace', fontSize: '12px', color: meta.color, textAlign: 'center', lineHeight: 1 }}>
|
||||||
{meta.name}
|
{meta.name}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#A78BFA' }}>
|
<span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '7px', color: '#A78BFA' }}>
|
||||||
{meta.cost} HC
|
{meta.cost}HC
|
||||||
</span>
|
|
||||||
<span style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#64748B', textAlign: 'center' }}>
|
|
||||||
{meta.desc}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -117,34 +117,158 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化音效引擎(首次点击后激活 AudioContext)
|
// 初始化音效引擎(首次点击后激活 AudioContext,默认开启)
|
||||||
const audio = AudioEngine.getInstance()
|
const audio = AudioEngine.getInstance()
|
||||||
this.input.once('pointerdown', () => {
|
this.input.once('pointerdown', () => {
|
||||||
audio.init()
|
audio.init()
|
||||||
audio.startBGM()
|
audio.startBGM()
|
||||||
})
|
})
|
||||||
// 添加静音切换按钮(右上角)
|
|
||||||
this.createMuteButton(audio)
|
// 注册 PUA buff 接口供 React 层调用
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
;(window as any).__gamePuaBuff = (
|
||||||
|
effect: string, score: number, title: string
|
||||||
|
) => {
|
||||||
|
this.applyPuaBuff(effect, score, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setupInteraction()
|
this.setupInteraction()
|
||||||
this.setupManagerCallbacks()
|
this.setupManagerCallbacks()
|
||||||
if (typeof window !== 'undefined') { ;(window as any).__gameReady?.() }
|
if (typeof window !== 'undefined') { ;(window as any).__gameReady?.() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMuteButton(audio: AudioEngine): void {
|
private createMuteButton(_audio: AudioEngine): void {
|
||||||
const btn = this.add.text(GAME_WIDTH - 10, 8, '🔊', {
|
// 静音按钮已移除,音乐默认开启
|
||||||
fontSize: '16px', backgroundColor: 'rgba(0,0,0,0.35)',
|
void _audio
|
||||||
padding: { x: 6, y: 3 },
|
}
|
||||||
}).setOrigin(1, 0).setDepth(50).setInteractive({ useHandCursor: true })
|
|
||||||
btn.on('pointerdown', () => {
|
/**
|
||||||
const nowMuted = !audio.isMuted()
|
* 应用 PUA buff 效果到游戏
|
||||||
audio.setMuted(nowMuted)
|
* effect: attack_boost | speed_boost | money_rain | rage_mode | backfire
|
||||||
btn.setText(nowMuted ? '🔇' : '🔊')
|
*/
|
||||||
if (!nowMuted) audio.startBGM()
|
private applyPuaBuff(effect: string, score: number, title: string): void {
|
||||||
else audio.stopBGM()
|
const towers = this.towerManager.getAllTowers()
|
||||||
|
const audio = AudioEngine.getInstance()
|
||||||
|
|
||||||
|
// 全屏效果提示横幅
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
rage_mode: '#FF4E00',
|
||||||
|
speed_boost: '#FBBF24',
|
||||||
|
attack_boost: '#22C55E',
|
||||||
|
money_rain: '#A78BFA',
|
||||||
|
backfire: '#6B7280',
|
||||||
|
}
|
||||||
|
const color = colors[effect] ?? '#E2E8F0'
|
||||||
|
this.showPuaBanner(title, score, color)
|
||||||
|
|
||||||
|
const dur = Math.max(8000, score * 1500) // buff 持续时间(ms)
|
||||||
|
|
||||||
|
switch (effect) {
|
||||||
|
case 'rage_mode': {
|
||||||
|
// 9-10分:全场狂暴 — 攻速×2,全图红色相机震动
|
||||||
|
audio.playBossAppear()
|
||||||
|
this.cameras.main.flash(600, 255, 60, 0, false)
|
||||||
|
this.cameras.main.shake(400, 0.008)
|
||||||
|
towers.forEach(t => { t['_puaSpeedMult'] = 2.0 })
|
||||||
|
this.manager.addHC(score * 15)
|
||||||
|
this.time.delayedCall(dur, () => {
|
||||||
|
towers.forEach(t => { t['_puaSpeedMult'] = 1.0 })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'speed_boost': {
|
||||||
|
// 7-8分:攻速 ×1.5
|
||||||
|
audio.playWaveStart()
|
||||||
|
towers.forEach(t => { t['_puaSpeedMult'] = 1.5 })
|
||||||
|
this.time.delayedCall(dur, () => {
|
||||||
|
towers.forEach(t => { t['_puaSpeedMult'] = 1.0 })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'attack_boost': {
|
||||||
|
// 4-6分:伤害 ×(1 + score*0.08)
|
||||||
|
audio.playDingTalk()
|
||||||
|
const mult = 1 + score * 0.08
|
||||||
|
towers.forEach(t => { t['_puaDmgMult'] = mult })
|
||||||
|
this.time.delayedCall(dur, () => {
|
||||||
|
towers.forEach(t => { t['_puaDmgMult'] = 1.0 })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'money_rain': {
|
||||||
|
// 5-7分:立即获得 HC,屏幕绿色雨
|
||||||
|
const hcBonus = score * 20
|
||||||
|
this.manager.addHC(hcBonus)
|
||||||
|
audio.playOpsAttack()
|
||||||
|
this.showMoneyRain(hcBonus)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'backfire': {
|
||||||
|
// 1-2分:废话反效果 — 全场塔禁锢 2 秒
|
||||||
|
audio.playEnemyDeath()
|
||||||
|
this.cameras.main.shake(300, 0.005)
|
||||||
|
this.freezeAllTowers(2000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示 PUA buff 横幅 */
|
||||||
|
private showPuaBanner(title: string, score: number, color: string): void {
|
||||||
|
const stars = '★'.repeat(Math.min(score, 10))
|
||||||
|
const banner = this.add.text(
|
||||||
|
GAME_WIDTH / 2, HUD_HEIGHT + 80,
|
||||||
|
`${title} ${stars}`,
|
||||||
|
{
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '28px',
|
||||||
|
color,
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
shadow: { offsetX: 0, offsetY: 0, color, blur: 16, stroke: true, fill: true },
|
||||||
|
}
|
||||||
|
).setOrigin(0.5, 0.5).setDepth(60).setAlpha(0)
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: banner,
|
||||||
|
alpha: 1,
|
||||||
|
y: HUD_HEIGHT + 60,
|
||||||
|
duration: 300,
|
||||||
|
yoyo: false,
|
||||||
|
onComplete: () => {
|
||||||
|
this.time.delayedCall(2000, () => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: banner, alpha: 0, y: HUD_HEIGHT + 40,
|
||||||
|
duration: 400, onComplete: () => banner.destroy(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 金币雨效果(money_rain) */
|
||||||
|
private showMoneyRain(amount: number): void {
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const x = Math.random() * GAME_WIDTH
|
||||||
|
const coin = this.add.text(x, HUD_HEIGHT + 20, `+${Math.floor(amount / 12)}HC`, {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
stroke: '#000',
|
||||||
|
strokeThickness: 2,
|
||||||
|
}).setOrigin(0.5, 0).setDepth(55).setAlpha(0.9)
|
||||||
|
this.tweens.add({
|
||||||
|
targets: coin,
|
||||||
|
y: coin.y + 120 + Math.random() * 80,
|
||||||
|
alpha: 0,
|
||||||
|
delay: i * 80,
|
||||||
|
duration: 900,
|
||||||
|
onComplete: () => coin.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setupManagerCallbacks(): void {
|
private setupManagerCallbacks(): void {
|
||||||
this.manager.onHCChange.push((hc: number) => {
|
this.manager.onHCChange.push((hc: number) => {
|
||||||
this.hcText.setText(`HC: ${hc}`)
|
this.hcText.setText(`HC: ${hc}`)
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ export abstract class TowerBase {
|
|||||||
|
|
||||||
public readonly cost: number
|
public readonly cost: number
|
||||||
public readonly attackRange: number
|
public readonly attackRange: number
|
||||||
public readonly attackDamage: number
|
private readonly _attackDamage: number
|
||||||
|
// 实际伤害 = 基础伤害 × PUA倍率
|
||||||
|
get attackDamage(): number {
|
||||||
|
return this._attackDamage * this._puaDmgMult
|
||||||
|
}
|
||||||
public readonly attackSpeed: number
|
public readonly attackSpeed: number
|
||||||
public readonly maxStamina: number = STAMINA_MAX
|
public readonly maxStamina: number = STAMINA_MAX
|
||||||
public stamina: number = STAMINA_MAX
|
public stamina: number = STAMINA_MAX
|
||||||
public isFrozen: boolean = false
|
public isFrozen: boolean = false
|
||||||
|
// PUA buff 动态倍率(由 GameScene.applyPuaBuff 设置)
|
||||||
|
public _puaSpeedMult: number = 1.0 // 攻速倍率
|
||||||
|
public _puaDmgMult: number = 1.0 // 伤害倍率
|
||||||
|
|
||||||
protected attackCooldown: number = 0
|
protected attackCooldown: number = 0
|
||||||
protected staminaRegen: number = STAMINA_REGEN
|
protected staminaRegen: number = STAMINA_REGEN
|
||||||
@@ -47,7 +54,7 @@ export abstract class TowerBase {
|
|||||||
this.gridY = gridY
|
this.gridY = gridY
|
||||||
this.cost = cost
|
this.cost = cost
|
||||||
this.attackRange = attackRange
|
this.attackRange = attackRange
|
||||||
this.attackDamage = attackDamage
|
this._attackDamage = attackDamage
|
||||||
this.attackSpeed = attackSpeed
|
this.attackSpeed = attackSpeed
|
||||||
this.spriteKey = spriteKey
|
this.spriteKey = spriteKey
|
||||||
|
|
||||||
@@ -92,7 +99,8 @@ export abstract class TowerBase {
|
|||||||
if (target && this.attackCooldown <= 0) {
|
if (target && this.attackCooldown <= 0) {
|
||||||
this.attack(target)
|
this.attack(target)
|
||||||
this.stamina -= 5
|
this.stamina -= 5
|
||||||
this.attackCooldown = 1000 / this.attackSpeed
|
// 应用 PUA 攻速倍率(倍率越高冷却越短)
|
||||||
|
this.attackCooldown = 1000 / (this.attackSpeed * this._puaSpeedMult)
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
} else if (!target) {
|
} else if (!target) {
|
||||||
this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000)
|
this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000)
|
||||||
|
|||||||
Reference in New Issue
Block a user