feat(game): PUA激励台增加HC消耗机制,费用=当前HC×15%,余额不足时禁用,防止无限激励破坏平衡
This commit is contained in:
@@ -21,6 +21,7 @@ interface PuaResult {
|
||||
title: string
|
||||
desc: string
|
||||
effect: EffectType
|
||||
cost?: number // 实际扣除的 HC(前端填写)
|
||||
}
|
||||
|
||||
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 输入面板 ─────────────────────────────────────────────────────────────
|
||||
function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) {
|
||||
const [text, setText] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<PuaResult | null>(null)
|
||||
const [history, setHistory] = useState<PuaResult[]>([])
|
||||
const [insufficient, setInsufficient] = useState(false)
|
||||
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 () => {
|
||||
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)
|
||||
setResult(null)
|
||||
setInsufficient(false)
|
||||
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()
|
||||
const data: PuaResult & { cost?: number } = await res.json()
|
||||
data.cost = actualCost
|
||||
setResult(data)
|
||||
setHistory(prev => [data, ...prev].slice(0, 5))
|
||||
// 通知游戏场景应用 buff
|
||||
@@ -70,7 +102,7 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [text, loading, gameReady])
|
||||
}, [text, loading, gameReady, hc])
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
@@ -115,6 +147,62 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
</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
|
||||
ref={textareaRef}
|
||||
@@ -127,7 +215,7 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: '#0F1B2D',
|
||||
border: '1px solid #1e3a5f',
|
||||
border: `1px solid ${canAfford ? '#1e3a5f' : '#7F1D1D'}`,
|
||||
borderRadius: '6px',
|
||||
color: '#E2E8F0',
|
||||
fontFamily: 'VT323, monospace',
|
||||
@@ -137,29 +225,30 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
outline: 'none',
|
||||
lineHeight: 1.4,
|
||||
opacity: gameReady ? 1 : 0.5,
|
||||
transition: 'border-color 0.2s',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !text.trim() || !gameReady}
|
||||
disabled={loading || !text.trim() || !gameReady || !canAfford}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
backgroundColor: loading ? '#1e3a5f' : '#7C3AED',
|
||||
backgroundColor: loading ? '#1e3a5f' : canAfford ? '#7C3AED' : '#4C1D95',
|
||||
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,
|
||||
cursor: loading || !text.trim() || !gameReady || !canAfford ? 'not-allowed' : 'pointer',
|
||||
opacity: !text.trim() || !gameReady || !canAfford ? 0.45 : 1,
|
||||
transition: 'all 0.15s',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
{loading ? '分析中...' : '发起激励 ▶'}
|
||||
{loading ? '分析中...' : !canAfford ? 'HC不足' : `发起激励 -${cost}HC`}
|
||||
</button>
|
||||
|
||||
{/* 当前结果 */}
|
||||
@@ -209,18 +298,28 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
}}>
|
||||
{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 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>
|
||||
)
|
||||
@@ -274,14 +373,17 @@ function PuaPanel({ gameReady }: { gameReady: boolean }) {
|
||||
fontFamily: 'VT323, monospace',
|
||||
fontSize: '11px',
|
||||
color: '#334155',
|
||||
lineHeight: 1.4,
|
||||
lineHeight: 1.5,
|
||||
marginTop: 'auto',
|
||||
borderTop: '1px solid #1e3a5f',
|
||||
paddingTop: '8px',
|
||||
}}>
|
||||
<span style={{ color: '#475569' }}>费用 = 当前HC × 15%</span><br />
|
||||
<span style={{ color: '#334155' }}>越富越贵,最低20 最高200</span><br />
|
||||
──────────<br />
|
||||
Enter 快速提交<br />
|
||||
1-3分: 废话翻车<br />
|
||||
4-6分: 小幅加成<br />
|
||||
1-2分: 废话翻车<br />
|
||||
4-6分: 攻击+HC<br />
|
||||
7-8分: 攻速暴增<br />
|
||||
9-10分: 全场狂暴
|
||||
</div>
|
||||
@@ -347,7 +449,8 @@ export default function GamePage() {
|
||||
gameRef.current = null
|
||||
if (typeof window !== 'undefined') {
|
||||
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
|
||||
'__gameReady','__gameDifficulty','__gamePuaBuff'].forEach(k => {
|
||||
'__gameReady','__gameDifficulty','__gamePuaBuff',
|
||||
'__gameGetHC','__gameSpendHC'].forEach(k => {
|
||||
delete (window as any)[k]
|
||||
})
|
||||
}
|
||||
@@ -367,7 +470,7 @@ export default function GamePage() {
|
||||
style={{ backgroundColor: '#0A1628' }}
|
||||
/>
|
||||
{/* PUA 激励台(右侧) */}
|
||||
<PuaPanel gameReady={gameReady} />
|
||||
<PuaPanel gameReady={gameReady} hc={hc} />
|
||||
</div>
|
||||
|
||||
{/* 底部塔选择面板 */}
|
||||
|
||||
Reference in New Issue
Block a user