feat(game): 添加开会暂停系统——点击开会按钮暂停游戏,可安心发激励,结束开会后恢复

This commit is contained in:
Cloud Bot
2026-03-24 09:13:14 +00:00
parent 6843d2b74c
commit b8ba572ffb
4 changed files with 157 additions and 33 deletions

View File

@@ -474,13 +474,24 @@ export default function GamePage() {
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false)
const [waveStarted, setWaveStarted] = useState(false)
// 召唤按钮状态(由 HUD 通过 window.__gameSetWaveBtn 驱动)
const [inMeeting, setInMeeting] = useState(false) // 开会=游戏暂停
const [waveBtn, setWaveBtn] = useState<{ text: string; disabled: boolean }>({
text: '▶ 召唤下一波',
disabled: false,
})
const selectedTowerRef = useRef<TowerType | null>(null)
const handleMeeting = useCallback(() => {
if (!gameReady) return
if (!inMeeting) {
const ok = typeof window !== 'undefined' && (window as any).__gamePause?.()
if (ok) setInMeeting(true)
} else {
const ok = typeof window !== 'undefined' && (window as any).__gameResume?.()
if (ok) setInMeeting(false)
}
}, [gameReady, inMeeting])
const handleSelectTower = useCallback((type: TowerType) => {
const next = selectedTowerRef.current === type ? null : type
selectedTowerRef.current = next
@@ -545,7 +556,8 @@ export default function GamePage() {
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
'__gameReady','__gameDifficulty','__gamePuaBuff',
'__gameGetHC','__gameSpendHC','__gameWaveStarted',
'__gameSetWaveBtn','__gameWaveBtnState','__gameOnWaveClick'].forEach(k => {
'__gameSetWaveBtn','__gameWaveBtnState','__gameOnWaveClick',
'__gamePause','__gameResume','__gameIsPaused'].forEach(k => {
delete (window as any)[k]
})
}
@@ -565,79 +577,126 @@ export default function GamePage() {
style={{ backgroundColor: '#0A1628' }}
/>
{/* 右侧面板HC + 召唤按钮 + PUA激励台 */}
{/* 右侧面板HC + 开会按钮 + 召唤按钮 + PUA激励台 */}
<div style={{
width: '240px',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.97)',
borderLeft: '2px solid #1e3a5f',
borderLeft: `2px solid ${inMeeting ? '#22C55E' : '#1e3a5f'}`,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
transition: 'border-color 0.2s',
boxShadow: inMeeting ? 'inset 0 0 20px rgba(34,197,94,0.08)' : 'none',
}}>
{/* HC 数量显示 */}
{/* HC 数量 + 开会按钮(同一行) */}
<div style={{
padding: '10px 12px 8px',
padding: '8px 12px',
borderBottom: '1px solid #1e3a5f',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: '#64748B' }}>
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#475569', marginBottom: '1px' }}>
</span>
<span style={{
</div>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '12px',
fontSize: '11px',
color: '#A78BFA',
letterSpacing: '1px',
}}>
{hc} HC
</span>
</div>
</div>
{/* 开会按钮 */}
<button
onClick={handleMeeting}
disabled={!gameReady || !waveStarted}
title={inMeeting ? '结束开会,恢复游戏' : '开会暂停,发起激励'}
style={{
flexShrink: 0,
padding: '6px 10px',
backgroundColor: inMeeting ? '#14532D' : '#0F1B2D',
border: `2px solid ${inMeeting ? '#22C55E' : '#1e3a5f'}`,
borderRadius: '8px',
color: inMeeting ? '#4ADE80' : (!gameReady || !waveStarted ? '#334155' : '#94A3B8'),
fontFamily: 'VT323, monospace',
fontSize: '14px',
cursor: !gameReady || !waveStarted ? 'not-allowed' : 'pointer',
transition: 'all 0.15s',
whiteSpace: 'nowrap',
lineHeight: 1.2,
boxShadow: inMeeting ? '0 0 10px rgba(34,197,94,0.3)' : 'none',
}}
>
{inMeeting ? '📋 结束开会' : '📋 开会'}
</button>
</div>
{/* 召唤下一波按钮 */}
<div style={{ padding: '10px 12px', borderBottom: '2px solid #1e3a5f' }}>
{/* 开会中提示条 */}
{inMeeting && (
<div style={{
backgroundColor: '#14532D',
borderBottom: '1px solid #22C55E',
padding: '5px 12px',
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#86EFAC',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<span style={{ animation: 'pulse 1.5s ease-in-out infinite' }}></span>
</div>
)}
{/* 召唤下一波按钮(开会时禁用) */}
<div style={{ padding: '8px 12px', borderBottom: '2px solid #1e3a5f' }}>
<button
onClick={() => {
if (inMeeting) return
if (typeof window !== 'undefined') {
(window as any).__gameOnWaveClick?.()
}
}}
disabled={waveBtn.disabled || !gameReady}
disabled={waveBtn.disabled || !gameReady || inMeeting}
style={{
width: '100%',
padding: '10px 8px',
backgroundColor: waveBtn.disabled || !gameReady ? '#0F172A' : '#1e3a5f',
border: `2px solid ${waveBtn.disabled || !gameReady ? '#1e293b' : '#7C3AED'}`,
backgroundColor: waveBtn.disabled || !gameReady || inMeeting ? '#0F172A' : '#1e3a5f',
border: `2px solid ${waveBtn.disabled || !gameReady || inMeeting ? '#1e293b' : '#7C3AED'}`,
borderRadius: '8px',
color: waveBtn.disabled || !gameReady ? '#4B5563' : '#C4B5FD',
color: waveBtn.disabled || !gameReady || inMeeting ? '#4B5563' : '#C4B5FD',
fontFamily: 'VT323, monospace',
fontSize: '20px',
cursor: waveBtn.disabled || !gameReady ? 'not-allowed' : 'pointer',
cursor: waveBtn.disabled || !gameReady || inMeeting ? '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)',
boxShadow: waveBtn.disabled || !gameReady || inMeeting ? 'none' : '0 0 12px rgba(124,58,237,0.3)',
}}
onMouseEnter={e => {
if (!waveBtn.disabled && gameReady) {
if (!waveBtn.disabled && gameReady && !inMeeting) {
(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) {
if (!waveBtn.disabled && gameReady && !inMeeting) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = '#1e3a5f'
;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 12px rgba(124,58,237,0.3)'
}
}}
>
{waveBtn.text}
{inMeeting ? '开会中...' : waveBtn.text}
</button>
</div>
{/* PUA 激励台 */}
<PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted} />
<PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted || inMeeting} />
</div>
</div>

View File

@@ -725,3 +725,8 @@
/* 深色背景(终端/代码块保留不覆盖bg-slate-900 bg-slate-950 */
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}

View File

@@ -102,6 +102,22 @@ export class GameManager {
this.onKPIChange.forEach(cb => cb(this.kpi))
}
/** 暂停游戏(仅 playing 状态下有效) */
pause(): boolean {
if (this.gameState !== 'playing') return false
this.gameState = 'paused'
return true
}
/** 恢复游戏(仅 paused 状态下有效) */
resume(): boolean {
if (this.gameState !== 'paused') return false
this.gameState = 'playing'
return true
}
get isPaused(): boolean { return this.gameState === 'paused' }
/** 触发胜利 */
triggerVictory(): void {
if (this.gameState === 'playing') {

View File

@@ -64,6 +64,10 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
private bgObject: Phaser.GameObjects.Image | null = null
private mapInTransition: boolean = false
// 暂停遮罩
private pauseOverlay: Phaser.GameObjects.Graphics | null = null
private pauseText: Phaser.GameObjects.Text | null = null
constructor() { super({ key: 'GameScene' }) }
preload(): void {
@@ -124,19 +128,34 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
audio.startBGM()
})
// 注册 PUA buff 接口 + HC 查询/扣除接口供 React 层调用
// 注册 PUA buff 接口 + HC 查询/扣除接口 + 暂停/恢复接口
if (typeof window !== 'undefined') {
;(window as any).__gamePuaBuff = (
effect: string, score: number, title: string
) => {
this.applyPuaBuff(effect, score, title)
}
// 查询当前 HC
) => { this.applyPuaBuff(effect, score, title) }
;(window as any).__gameGetHC = () => this.manager.hc
// 尝试扣除 HC成功返回 true不足返回 false
;(window as any).__gameSpendHC = (amount: number): boolean => {
return this.manager.spendHC(amount)
;(window as any).__gameSpendHC = (amount: number): boolean =>
this.manager.spendHC(amount)
;(window as any).__gamePause = (): boolean => {
if (!this.manager.pause()) return false
this.showPauseOverlay()
// 暂停 Phaser 物理与 Tween
this.physics.pause()
this.tweens.pauseAll()
return true
}
;(window as any).__gameResume = (): boolean => {
if (!this.manager.resume()) return false
this.hidePauseOverlay()
this.physics.resume()
this.tweens.resumeAll()
return true
}
;(window as any).__gameIsPaused = (): boolean => this.manager.isPaused
}
this.setupInteraction()
@@ -145,10 +164,34 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
}
private createMuteButton(_audio: AudioEngine): void {
// 静音按钮已移除,音乐默认开启
void _audio
}
private showPauseOverlay(): void {
const { width, height } = this.scale
// 半透明深色遮罩
this.pauseOverlay = this.add.graphics().setDepth(90)
this.pauseOverlay.fillStyle(0x000000, 0.6)
this.pauseOverlay.fillRect(0, 0, width, height)
// "开会中" 提示卡片
this.pauseText = this.add.text(width / 2, height / 2, '📋 开会中...\n游戏已暂停', {
fontFamily: 'VT323, monospace',
fontSize: '32px',
color: '#FCD34D',
backgroundColor: '#1e3a5f',
padding: { x: 32, y: 18 },
align: 'center',
}).setOrigin(0.5, 0.5).setDepth(91)
}
private hidePauseOverlay(): void {
this.pauseOverlay?.destroy()
this.pauseOverlay = null
this.pauseText?.destroy()
this.pauseText = null
}
/**
* 应用 PUA buff 效果到游戏
* effect: attack_boost | speed_boost | money_rain | rage_mode | backfire
@@ -324,6 +367,7 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
private readonly AUTO_WAVE_DELAY = 2000 // 自动开始等待3s→2s缩短喘息时间
update(_time: number, delta: number): void {
if (this.manager.gameState === 'paused') return // 暂停时完全跳过
if (this.manager.gameState !== 'playing' && this.manager.gameState !== 'idle') return
if (this.mapInTransition) return