feat(game): PUA激励台增加HC消耗机制,费用=当前HC×15%,余额不足时禁用,防止无限激励破坏平衡

This commit is contained in:
Cloud Bot
2026-03-24 08:22:21 +00:00
parent adda7ae57c
commit d7253c45be
2 changed files with 137 additions and 28 deletions

View File

@@ -21,6 +21,7 @@ interface PuaResult {
title: string title: string
desc: string desc: string
effect: EffectType effect: EffectType
cost?: number // 实际扣除的 HC前端填写
} }
const EFFECT_META: Record<EffectType, { label: string; color: string; icon: string }> = { const EFFECT_META: Record<EffectType, { label: string; color: string; icon: string }> = {
@@ -39,26 +40,57 @@ const PUA_PLACEHOLDERS = [
'大家加油,相信自己!', '大家加油,相信自己!',
] ]
// ── 费用计算基于当前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 输入面板 ───────────────────────────────────────────────────────────── // ── PUA 输入面板 ─────────────────────────────────────────────────────────────
function PuaPanel({ gameReady }: { gameReady: boolean }) { function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) {
const [text, setText] = useState('') const [text, setText] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [result, setResult] = useState<PuaResult | null>(null) const [result, setResult] = useState<PuaResult | null>(null)
const [history, setHistory] = useState<PuaResult[]>([]) const [history, setHistory] = useState<PuaResult[]>([])
const [insufficient, setInsufficient] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const placeholder = PUA_PLACEHOLDERS[Math.floor(Math.random() * PUA_PLACEHOLDERS.length)] 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 () => { const handleSubmit = useCallback(async () => {
if (!text.trim() || loading || !gameReady) return if (!text.trim() || loading || !gameReady) return
// 先从游戏扣除 HC扣不到则拒绝
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) setLoading(true)
setResult(null) setResult(null)
setInsufficient(false)
try { try {
const res = await fetch('/api/pua-score', { const res = await fetch('/api/pua-score', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text.trim() }), body: JSON.stringify({ text: text.trim() }),
}) })
const data: PuaResult = await res.json() const data: PuaResult & { cost?: number } = await res.json()
data.cost = actualCost
setResult(data) setResult(data)
setHistory(prev => [data, ...prev].slice(0, 5)) setHistory(prev => [data, ...prev].slice(0, 5))
// 通知游戏场景应用 buff // 通知游戏场景应用 buff
@@ -70,7 +102,7 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [text, loading, gameReady]) }, [text, loading, gameReady, hc])
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
@@ -115,6 +147,62 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
</div> </div>
</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',
animation: 'none',
}}>
HC
</div>
)}
{/* 输入框 */} {/* 输入框 */}
<textarea <textarea
ref={textareaRef} ref={textareaRef}
@@ -127,7 +215,7 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
style={{ style={{
width: '100%', width: '100%',
backgroundColor: '#0F1B2D', backgroundColor: '#0F1B2D',
border: '1px solid #1e3a5f', border: `1px solid ${canAfford ? '#1e3a5f' : '#7F1D1D'}`,
borderRadius: '6px', borderRadius: '6px',
color: '#E2E8F0', color: '#E2E8F0',
fontFamily: 'VT323, monospace', fontFamily: 'VT323, monospace',
@@ -137,29 +225,30 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
outline: 'none', outline: 'none',
lineHeight: 1.4, lineHeight: 1.4,
opacity: gameReady ? 1 : 0.5, opacity: gameReady ? 1 : 0.5,
transition: 'border-color 0.2s',
}} }}
/> />
{/* 提交按钮 */} {/* 提交按钮 */}
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading || !text.trim() || !gameReady} disabled={loading || !text.trim() || !gameReady || !canAfford}
style={{ style={{
width: '100%', width: '100%',
padding: '8px', padding: '8px',
backgroundColor: loading ? '#1e3a5f' : '#7C3AED', backgroundColor: loading ? '#1e3a5f' : canAfford ? '#7C3AED' : '#4C1D95',
border: 'none', border: 'none',
borderRadius: '6px', borderRadius: '6px',
color: '#E2E8F0', color: '#E2E8F0',
fontFamily: "'Press Start 2P', monospace", fontFamily: "'Press Start 2P', monospace",
fontSize: '8px', fontSize: '8px',
cursor: loading || !text.trim() || !gameReady ? 'not-allowed' : 'pointer', cursor: loading || !text.trim() || !gameReady || !canAfford ? 'not-allowed' : 'pointer',
opacity: !text.trim() || !gameReady ? 0.5 : 1, opacity: !text.trim() || !gameReady || !canAfford ? 0.45 : 1,
transition: 'all 0.15s', transition: 'all 0.15s',
letterSpacing: '0.5px', letterSpacing: '0.5px',
}} }}
> >
{loading ? '分析中...' : '发起激励 ▶'} {loading ? '分析中...' : !canAfford ? 'HC不足' : `发起激励 -${cost}HC`}
</button> </button>
{/* 当前结果 */} {/* 当前结果 */}
@@ -209,18 +298,28 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
}}> }}>
{result.desc} {result.desc}
</div> </div>
{/* 效果标签 */} {/* 效果标签 + 花费 */}
<div style={{ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
backgroundColor: `${em.color}22`, <div style={{
border: `1px solid ${em.color}55`, backgroundColor: `${em.color}22`,
borderRadius: '4px', border: `1px solid ${em.color}55`,
padding: '3px 6px', borderRadius: '4px',
fontFamily: 'VT323, monospace', padding: '3px 6px',
fontSize: '12px', fontFamily: 'VT323, monospace',
color: em.color, fontSize: '12px',
textAlign: 'center', color: em.color,
}}> }}>
{em.label} {em.label}
</div>
{result.cost && (
<span style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px',
color: '#475569',
}}>
-{result.cost}HC
</span>
)}
</div> </div>
</div> </div>
) )
@@ -274,14 +373,17 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
fontFamily: 'VT323, monospace', fontFamily: 'VT323, monospace',
fontSize: '11px', fontSize: '11px',
color: '#334155', color: '#334155',
lineHeight: 1.4, lineHeight: 1.5,
marginTop: 'auto', marginTop: 'auto',
borderTop: '1px solid #1e3a5f', borderTop: '1px solid #1e3a5f',
paddingTop: '8px', paddingTop: '8px',
}}> }}>
<span style={{ color: '#475569' }}> = HC × 15%</span><br />
<span style={{ color: '#334155' }}>20 200</span><br />
<br />
Enter <br /> Enter <br />
1-3: 废话翻车<br /> 1-2: 废话翻车<br />
4-6分: 小幅加成<br /> 4-6分: 攻击+HC<br />
7-8分: 攻速暴增<br /> 7-8分: 攻速暴增<br />
9-10分: 全场狂暴 9-10分: 全场狂暴
</div> </div>
@@ -347,7 +449,8 @@ export default function GamePage() {
gameRef.current = null gameRef.current = null
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower', ;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
'__gameReady','__gameDifficulty','__gamePuaBuff'].forEach(k => { '__gameReady','__gameDifficulty','__gamePuaBuff',
'__gameGetHC','__gameSpendHC'].forEach(k => {
delete (window as any)[k] delete (window as any)[k]
}) })
} }
@@ -367,7 +470,7 @@ export default function GamePage() {
style={{ backgroundColor: '#0A1628' }} style={{ backgroundColor: '#0A1628' }}
/> />
{/* PUA 激励台(右侧) */} {/* PUA 激励台(右侧) */}
<PuaPanel gameReady={gameReady} /> <PuaPanel gameReady={gameReady} hc={hc} />
</div> </div>
{/* 底部塔选择面板 */} {/* 底部塔选择面板 */}

View File

@@ -124,13 +124,19 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
audio.startBGM() audio.startBGM()
}) })
// 注册 PUA buff 接口供 React 层调用 // 注册 PUA buff 接口 + HC 查询/扣除接口供 React 层调用
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
;(window as any).__gamePuaBuff = ( ;(window as any).__gamePuaBuff = (
effect: string, score: number, title: string effect: string, score: number, title: string
) => { ) => {
this.applyPuaBuff(effect, score, title) this.applyPuaBuff(effect, score, title)
} }
// 查询当前 HC
;(window as any).__gameGetHC = () => this.manager.hc
// 尝试扣除 HC成功返回 true不足返回 false
;(window as any).__gameSpendHC = (amount: number): boolean => {
return this.manager.spendHC(amount)
}
} }
this.setupInteraction() this.setupInteraction()