diff --git a/app/game/page.tsx b/app/game/page.tsx index 808568b..9597d37 100644 --- a/app/game/page.tsx +++ b/app/game/page.tsx @@ -48,7 +48,7 @@ function calcPuaCost(hc: number): number { } // ── PUA 输入面板 ───────────────────────────────────────────────────────────── -function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { +function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: number; waveStarted: boolean }) { const [text, setText] = useState('') const [loading, setLoading] = useState(false) const [result, setResult] = useState(null) @@ -61,7 +61,7 @@ function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { const canAfford = hc >= cost const handleSubmit = useCallback(async () => { - if (!text.trim() || loading || !gameReady) return + if (!text.trim() || loading || !gameReady || !waveStarted) return // 先从游戏扣除 HC(扣不到则拒绝) const spendHC: ((n: number) => boolean) | undefined = @@ -102,7 +102,7 @@ function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { } finally { setLoading(false) } - }, [text, loading, gameReady, hc]) + }, [text, loading, gameReady, hc, waveStarted]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -143,7 +143,7 @@ function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { color: '#475569', marginTop: '2px', }}> - 输入打鸡血的话,AI判断鸡血值 + {!waveStarted ? '⚠ 召唤第一波后才能激励' : '输入打鸡血的话,AI判断鸡血值'} @@ -210,12 +210,12 @@ function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { onChange={e => setText(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder} - disabled={loading || !gameReady} + disabled={loading || !gameReady || !waveStarted} rows={4} style={{ width: '100%', backgroundColor: '#0F1B2D', - border: `1px solid ${canAfford ? '#1e3a5f' : '#7F1D1D'}`, + border: `1px solid ${!waveStarted ? '#1e3a5f' : canAfford ? '#1e3a5f' : '#7F1D1D'}`, borderRadius: '6px', color: '#E2E8F0', fontFamily: 'VT323, monospace', @@ -224,7 +224,7 @@ function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { resize: 'none', outline: 'none', lineHeight: 1.4, - opacity: gameReady ? 1 : 0.5, + opacity: gameReady && waveStarted ? 1 : 0.4, transition: 'border-color 0.2s', }} /> @@ -232,23 +232,23 @@ function PuaPanel({ gameReady, hc }: { gameReady: boolean; hc: number }) { {/* 提交按钮 */} {/* 当前结果 */} @@ -397,6 +397,7 @@ export default function GamePage() { const [hc, setHc] = useState(200) const [selectedTower, setSelectedTower] = useState(null) const [gameReady, setGameReady] = useState(false) + const [waveStarted, setWaveStarted] = useState(false) // 第一波开始后才允许激励 const selectedTowerRef = useRef(null) const handleSelectTower = useCallback((type: TowerType) => { @@ -438,6 +439,13 @@ export default function GamePage() { if (mounted) { selectedTowerRef.current = null; setSelectedTower(null) } } ;(window as any).__gameReady = () => { if (mounted) setGameReady(true) } + // 轮询 __gameWaveStarted(Phaser 设置后通知 React) + const checkWaveStarted = setInterval(() => { + if ((window as any).__gameWaveStarted) { + if (mounted) setWaveStarted(true) + clearInterval(checkWaveStarted) + } + }, 200) } gameRef.current = new Phaser.Game(config) @@ -450,7 +458,7 @@ export default function GamePage() { if (typeof window !== 'undefined') { ;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower', '__gameReady','__gameDifficulty','__gamePuaBuff', - '__gameGetHC','__gameSpendHC'].forEach(k => { + '__gameGetHC','__gameSpendHC','__gameWaveStarted'].forEach(k => { delete (window as any)[k] }) } @@ -470,7 +478,7 @@ export default function GamePage() { style={{ backgroundColor: '#0A1628' }} /> {/* PUA 激励台(右侧) */} - + {/* 底部塔选择面板 */} diff --git a/game/GameScene.ts b/game/GameScene.ts index 8d48e5a..a29790b 100644 --- a/game/GameScene.ts +++ b/game/GameScene.ts @@ -365,8 +365,11 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { this.waveManager.startNextWave() const waveNum = this.waveManager.getCurrentWaveNumber() this.hud.showWaveBanner(waveNum, this.waveManager.totalWaves) - // 波次开始音效 AudioEngine.getInstance().playWaveStart() + // 通知 React 层:战斗已开始,允许发起激励 + if (typeof window !== 'undefined') { + ;(window as any).__gameWaveStarted = true + } } private onWeeklyReport(): void { @@ -375,7 +378,11 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { } private onMapCleared(): void { + // 若游戏已失败,不触发地图切换 + if (this.manager.gameState === 'defeat') return + this.mapInTransition = true + this.autoNextWaveTimer = -1 this.isWaveRunning = false this.hud.disableWaveButton() this.hud.setWaveButtonText('关卡完成!') @@ -390,6 +397,8 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { this.manager.totalWaveCleared += currentMap.waveCount this.manager.currentMapIndex = nextIndex this.waveManager.clearAllEnemies() + // 清除上一关所有防御塔 + this.towerManager.clearAllTowers() this.loadMap(ALL_MAPS[nextIndex]) this.hud.enableWaveButton() this.hud.setWaveButtonText('▶ 召唤下一波') diff --git a/game/towers/TowerManager.ts b/game/towers/TowerManager.ts index 96ddb12..5210e55 100644 --- a/game/towers/TowerManager.ts +++ b/game/towers/TowerManager.ts @@ -258,6 +258,17 @@ export class TowerManager { return this.towers } + /** 切换地图时清除所有防御塔(静默销毁,不播放音效) */ + clearAllTowers(): void { + for (const tower of this.towers) { + tower.destroy() + } + this.towers = [] + this.occupiedCells.clear() + this.infoLabel?.destroy() + this.infoLabel = null + } + hasTowerAt(gridX: number, gridY: number): boolean { return this.occupiedCells.has(`${gridX},${gridY}`) } diff --git a/game/ui/HUD.ts b/game/ui/HUD.ts index ab8dc41..e68824d 100644 --- a/game/ui/HUD.ts +++ b/game/ui/HUD.ts @@ -23,25 +23,27 @@ export class HUD { */ createWaveButton(onClick: () => void): void { if (this.waveBtn) this.waveBtn.destroy() - this._onClick = onClick // 保存引用,供 enableWaveButton 重新绑定 + this._onClick = onClick this.waveBtn = this.scene.add - .text(GAME_WIDTH - 160, HUD_HEIGHT + 20, '▶ 召唤下一波', { - fontFamily: "'Press Start 2P', monospace", - fontSize: '9px', + .text(GAME_WIDTH / 2, HUD_HEIGHT + 16, '▶ 召唤下一波', { + fontFamily: 'VT323, monospace', + fontSize: '26px', color: '#A78BFA', backgroundColor: '#1e3a5f', - padding: { x: 10, y: 6 }, + padding: { x: 20, y: 8 }, + stroke: '#7C3AED', + strokeThickness: 1, }) - .setOrigin(0, 0) + .setOrigin(0.5, 0) .setDepth(20) .setInteractive({ useHandCursor: true }) this.waveBtn.on('pointerover', () => { - if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#2d5a8e' }) + if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#2d5a8e', color: '#C4B5FD' }) }) this.waveBtn.on('pointerout', () => { - if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#1e3a5f' }) + if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#1e3a5f', color: '#A78BFA' }) }) this.waveBtn.on('pointerdown', () => onClick()) }