feat(ui): 将召唤下一波按钮移至右侧面板HC下方,改为React DOM渲染,移除Phaser Canvas内按钮

This commit is contained in:
Cloud Bot
2026-03-24 08:36:38 +00:00
parent 653c54c06f
commit 1473542f65
2 changed files with 131 additions and 57 deletions

View File

@@ -116,15 +116,12 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
return ( return (
<div style={{ <div style={{
width: '240px',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.97)',
borderLeft: '2px solid #1e3a5f',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: '12px 10px', padding: '10px 10px 6px',
gap: '10px', gap: '8px',
overflow: 'hidden', overflow: 'hidden auto',
flex: 1,
}}> }}>
{/* 标题 */} {/* 标题 */}
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
@@ -397,7 +394,12 @@ export default function GamePage() {
const [hc, setHc] = useState(200) const [hc, setHc] = useState(200)
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null) const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false) const [gameReady, setGameReady] = useState(false)
const [waveStarted, setWaveStarted] = useState(false) // 第一波开始后才允许激励 const [waveStarted, setWaveStarted] = useState(false)
// 召唤按钮状态(由 HUD 通过 window.__gameSetWaveBtn 驱动)
const [waveBtn, setWaveBtn] = useState<{ text: string; disabled: boolean }>({
text: '▶ 召唤下一波',
disabled: false,
})
const selectedTowerRef = useRef<TowerType | null>(null) const selectedTowerRef = useRef<TowerType | null>(null)
const handleSelectTower = useCallback((type: TowerType) => { const handleSelectTower = useCallback((type: TowerType) => {
@@ -439,6 +441,11 @@ export default function GamePage() {
if (mounted) { selectedTowerRef.current = null; setSelectedTower(null) } if (mounted) { selectedTowerRef.current = null; setSelectedTower(null) }
} }
;(window as any).__gameReady = () => { if (mounted) setGameReady(true) } ;(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 // 轮询 __gameWaveStartedPhaser 设置后通知 React
const checkWaveStarted = setInterval(() => { const checkWaveStarted = setInterval(() => {
if ((window as any).__gameWaveStarted) { if ((window as any).__gameWaveStarted) {
@@ -458,7 +465,8 @@ export default function GamePage() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower', ;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
'__gameReady','__gameDifficulty','__gamePuaBuff', '__gameReady','__gameDifficulty','__gamePuaBuff',
'__gameGetHC','__gameSpendHC','__gameWaveStarted'].forEach(k => { '__gameGetHC','__gameSpendHC','__gameWaveStarted',
'__gameSetWaveBtn','__gameWaveBtnState','__gameOnWaveClick'].forEach(k => {
delete (window as any)[k] delete (window as any)[k]
}) })
} }
@@ -469,7 +477,7 @@ export default function GamePage() {
<div className="w-full h-screen flex flex-col overflow-hidden" <div className="w-full h-screen flex flex-col overflow-hidden"
style={{ backgroundColor: '#0A1628' }}> style={{ backgroundColor: '#0A1628' }}>
{/* 中间行:游戏画布 + PUA面板 */} {/* 中间行:游戏画布 + 右侧控制面板 */}
<div className="flex-1 min-h-0 flex flex-row overflow-hidden"> <div className="flex-1 min-h-0 flex flex-row overflow-hidden">
{/* 游戏画布 */} {/* 游戏画布 */}
<div <div
@@ -477,9 +485,82 @@ export default function GamePage() {
className="flex-1 min-w-0 min-h-0" className="flex-1 min-w-0 min-h-0"
style={{ backgroundColor: '#0A1628' }} style={{ backgroundColor: '#0A1628' }}
/> />
{/* PUA 激励台(右侧) */}
{/* 右侧面板HC + 召唤按钮 + PUA激励台 */}
<div style={{
width: '240px',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.97)',
borderLeft: '2px solid #1e3a5f',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}>
{/* HC 数量显示 */}
<div style={{
padding: '10px 12px 8px',
borderBottom: '1px solid #1e3a5f',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: '#64748B' }}>
</span>
<span style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '12px',
color: '#A78BFA',
letterSpacing: '1px',
}}>
{hc} HC
</span>
</div>
</div>
{/* 召唤下一波按钮 */}
<div style={{ padding: '10px 12px', borderBottom: '2px solid #1e3a5f' }}>
<button
onClick={() => {
if (typeof window !== 'undefined') {
(window as any).__gameOnWaveClick?.()
}
}}
disabled={waveBtn.disabled || !gameReady}
style={{
width: '100%',
padding: '10px 8px',
backgroundColor: waveBtn.disabled || !gameReady ? '#0F172A' : '#1e3a5f',
border: `2px solid ${waveBtn.disabled || !gameReady ? '#1e293b' : '#7C3AED'}`,
borderRadius: '8px',
color: waveBtn.disabled || !gameReady ? '#4B5563' : '#C4B5FD',
fontFamily: 'VT323, monospace',
fontSize: '20px',
cursor: waveBtn.disabled || !gameReady ? 'not-allowed' : 'pointer',
transition: 'all 0.15s ease',
letterSpacing: '1px',
lineHeight: 1.2,
boxShadow: waveBtn.disabled || !gameReady ? 'none' : '0 0 12px rgba(124,58,237,0.3)',
}}
onMouseEnter={e => {
if (!waveBtn.disabled && gameReady) {
(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) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = '#1e3a5f'
;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 12px rgba(124,58,237,0.3)'
}
}}
>
{waveBtn.text}
</button>
</div>
{/* PUA 激励台 */}
<PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted} /> <PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted} />
</div> </div>
</div>
{/* 底部塔选择面板 */} {/* 底部塔选择面板 */}
<div style={{ <div style={{

View File

@@ -5,74 +5,62 @@ import { showVictoryModal, showDefeatModal } from './EndScreenModal'
/** /**
* 游戏 HUD 辅助工具 * 游戏 HUD 辅助工具
* 负责管理"召唤下一波"按钮、波次提示横幅、胜利/失败结算弹窗 * 召唤下一波按钮已移至 React 右侧面板HUD 只负责横幅提示和结算弹窗
*/ */
export class HUD { export class HUD {
private scene: Phaser.Scene private scene: Phaser.Scene
private waveBtn: Phaser.GameObjects.Text | null = null
private waveBannerTimeout: (() => void) | null = null private waveBannerTimeout: (() => void) | null = null
private _onClick: (() => void) | null = null // 按钮状态通过 window 回调同步到 React
private _onWaveClick: (() => void) | null = null
constructor(scene: Phaser.Scene) { constructor(scene: Phaser.Scene) {
this.scene = scene this.scene = scene
} }
/** /**
* 创建"召唤下一波"按钮 * 注册"召唤下一波"逻辑,并把控制接口暴露给 React 层
* @param onClick 点击回调
*/ */
createWaveButton(onClick: () => void): void { createWaveButton(onClick: () => void): void {
if (this.waveBtn) this.waveBtn.destroy() this._onWaveClick = onClick
this._onClick = onClick if (typeof window === 'undefined') return
this.waveBtn = this.scene.add // React 层通过 window.__gameOnWaveClick() 触发
.text(GAME_WIDTH / 2, HUD_HEIGHT + 16, '▶ 召唤下一波', { ;(window as any).__gameOnWaveClick = () => {
fontFamily: 'VT323, monospace', this._onWaveClick?.()
fontSize: '26px', }
color: '#A78BFA', // 初始状态:可用
backgroundColor: '#1e3a5f', this._notifyReact('▶ 召唤下一波', false)
padding: { x: 20, y: 8 }, }
stroke: '#7C3AED',
strokeThickness: 1, private _notifyReact(text: string, disabled: boolean): void {
}) if (typeof window !== 'undefined') {
.setOrigin(0.5, 0) ;(window as any).__gameSetWaveBtn?.({ text, disabled })
.setDepth(20) }
.setInteractive({ useHandCursor: true })
this.waveBtn.on('pointerover', () => {
if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#2d5a8e', color: '#C4B5FD' })
})
this.waveBtn.on('pointerout', () => {
if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#1e3a5f', color: '#A78BFA' })
})
this.waveBtn.on('pointerdown', () => onClick())
} }
/** 更新按钮文字(如禁用状态) */
setWaveButtonText(text: string): void { setWaveButtonText(text: string): void {
this.waveBtn?.setText(text) // 保持当前 disabled 状态,只更新文字
if (typeof window !== 'undefined') {
const cur = (window as any).__gameWaveBtnState
const disabled = cur?.disabled ?? false
this._notifyReact(text, disabled)
}
} }
disableWaveButton(): void { disableWaveButton(): void {
if (!this.waveBtn) return this._notifyReact((typeof window !== 'undefined'
this.waveBtn.setStyle({ color: '#4B5563', backgroundColor: '#0F172A' }) ? (window as any).__gameWaveBtnState?.text ?? '波次进行中...'
this.waveBtn.removeAllListeners('pointerdown') : '波次进行中...'), true)
} }
enableWaveButton(): void { enableWaveButton(): void {
if (!this.waveBtn) return this._notifyReact((typeof window !== 'undefined'
this.waveBtn.setStyle({ color: '#A78BFA', backgroundColor: '#1e3a5f' }) ? (window as any).__gameWaveBtnState?.text ?? '▶ 召唤下一波'
// 重新绑定点击事件disableWaveButton 会 removeAllListeners : '▶ 召唤下一波'), false)
this.waveBtn.removeAllListeners('pointerdown')
if (this._onClick) {
this.waveBtn.on('pointerdown', () => this._onClick!())
}
} }
/** /**
* 显示波次开始横幅 * 显示波次开始横幅
* @param waveNumber 当前波次1-based
* @param totalWaves 总波次数
*/ */
showWaveBanner(waveNumber: number, totalWaves: number): void { showWaveBanner(waveNumber: number, totalWaves: number): void {
const isBoss = waveNumber === totalWaves const isBoss = waveNumber === totalWaves
@@ -128,7 +116,7 @@ export class HUD {
}) })
} }
/** 显示胜利画面(完整绩效评级弹窗,委托给 EndScreenModal */ /** 显示胜利画面 */
showVictory(): void { showVictory(): void {
const manager = GameManager.getInstance() const manager = GameManager.getInstance()
const kpi = manager.kpi const kpi = manager.kpi
@@ -159,13 +147,18 @@ export class HUD {
showVictoryModal({ kpi, hc, grade, gradeColor, gradeDesc }) showVictoryModal({ kpi, hc, grade, gradeColor, gradeDesc })
} }
/** 显示失败画面(仿钉钉退群通知,委托给 EndScreenModal */ /** 显示失败画面 */
showGameOver(): void { showGameOver(): void {
showDefeatModal() showDefeatModal()
} }
destroy(): void { destroy(): void {
this.waveBtn?.destroy()
if (this.waveBannerTimeout) this.waveBannerTimeout() if (this.waveBannerTimeout) this.waveBannerTimeout()
if (typeof window !== 'undefined') {
delete (window as any).__gameOnWaveClick
delete (window as any).__gameSetWaveBtn
delete (window as any).__gameWaveBtnState
} }
} }
}