feat(ui): 将召唤下一波按钮移至右侧面板HC下方,改为React DOM渲染,移除Phaser Canvas内按钮
This commit is contained in:
@@ -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 })
|
||||||
|
}
|
||||||
// 轮询 __gameWaveStarted(Phaser 设置后通知 React)
|
// 轮询 __gameWaveStarted(Phaser 设置后通知 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={{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user