import type Phaser from 'phaser' import { MAP_COLS, MAP_ROWS, HUD_HEIGHT, GAME_HEIGHT, GAME_WIDTH } from './constants' import { GameManager } from './GameManager' import { buildPathTiles, getCellSize, drawAllTiles, renderMapLabels, renderMapBackground, renderDecorations, renderHUD, updateKPIBar, BAR_X, BAR_Y, BAR_W, BAR_H, } from './mapRenderer' import { TowerManager, type TowerType } from './towers/TowerManager' import { WaveManager } from './enemies/WaveManager' import { HUD } from './ui/HUD' import { WeeklyReportModal } from './ui/WeeklyReportModal' import { MapTransitionModal } from './ui/MapTransitionModal' import { ALL_MAPS, type MapConfig } from './data/mapConfigs' import { AudioEngine } from './AudioEngine' void MAP_ROWS; void GAME_HEIGHT; void GAME_WIDTH; void BAR_X; void BAR_Y; void BAR_W; void BAR_H /** 所有游戏精灵的 key → public path 映射 */ const SPRITE_ASSETS: Record = { 'tower-intern': '/game-assets/tower-intern.png', 'tower-senior': '/game-assets/tower-senior.png', 'tower-ppt': '/game-assets/tower-ppt.png', 'tower-hrbp': '/game-assets/tower-hrbp.png', 'tower-pm': '/game-assets/tower-pm.png', 'tower-ops': '/game-assets/tower-ops.png', 'tower-outsource': '/game-assets/tower-outsource.png', 'enemy-fresh': '/game-assets/enemy-fresh.png', 'enemy-old': '/game-assets/enemy-old.png', 'enemy-trouble': '/game-assets/enemy-trouble.png', 'enemy-boss': '/game-assets/enemy-boss.png', 'deco-coffee': '/game-assets/deco-coffee.png', 'deco-monitor': '/game-assets/deco-monitor.png', 'deco-desk': '/game-assets/deco-desk.png', } export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { class GameScene extends PhaserLib.Scene { private manager!: GameManager private kpiBar!: Phaser.GameObjects.Graphics private kpiText!: Phaser.GameObjects.Text private hcText!: Phaser.GameObjects.Text private hoveredTile: { col: number; row: number } | null = null private tileGraphics!: Phaser.GameObjects.Graphics private towerManager!: TowerManager private waveManager!: WaveManager private hud!: HUD private weeklyModal!: WeeklyReportModal private mapTransitionModal!: MapTransitionModal private selectedTowerType: TowerType | null = null private buildModeGraphics!: Phaser.GameObjects.Graphics private isWaveRunning: boolean = false // 多地图状态 private currentPathTiles: Set = new Set() private decorationObjects: Phaser.GameObjects.Image[] = [] private labelObjects: Phaser.GameObjects.Text[] = [] private bgObject: Phaser.GameObjects.Image | null = null private mapInTransition: boolean = false constructor() { super({ key: 'GameScene' }) } preload(): void { for (const [key, path] of Object.entries(SPRITE_ASSETS)) { this.load.image(key, path) } // 预加载所有地图背景 for (const map of ALL_MAPS) { if (!this.textures.exists(map.bgKey)) { this.load.image(map.bgKey, map.bgPath) } } } create(): void { this.manager = GameManager.getInstance() this.manager.reset() // 读取难度 if (typeof window !== 'undefined') { const diff = (window as any).__gameDifficulty if (diff === 'easy' || diff === 'normal' || diff === 'hard') { this.manager.setDifficulty(diff) } } this.manager.gameState = 'playing' this.tileGraphics = this.add.graphics() this.buildModeGraphics = this.add.graphics().setDepth(5) const hudObjs = renderHUD(this) this.kpiBar = hudObjs.kpiBar this.kpiText = hudObjs.kpiText this.hcText = hudObjs.hcText this.towerManager = new TowerManager(this) this.weeklyModal = new WeeklyReportModal({ onBossInspection: () => this.freezeAllTowers(3000), onFullStamina: () => this.refillAllStamina(), }) this.mapTransitionModal = new MapTransitionModal() this.hud = new HUD(this) this.hud.createWaveButton(() => this.onWaveButtonClick()) this.loadMap(ALL_MAPS[0]) if (typeof window !== 'undefined') { ;(window as any).__gameSelectTower = (type: TowerType | null) => { this.selectedTowerType = type if (!type) this.buildModeGraphics.clear() } } // 初始化音效引擎(首次点击后激活 AudioContext,默认开启) const audio = AudioEngine.getInstance() this.input.once('pointerdown', () => { audio.init() audio.startBGM() }) // 注册 PUA buff 接口 + HC 查询/扣除接口供 React 层调用 if (typeof window !== 'undefined') { ;(window as any).__gamePuaBuff = ( effect: string, score: number, title: string ) => { this.applyPuaBuff(effect, score, title) } // 查询当前 HC ;(window as any).__gameGetHC = () => this.manager.hc // 尝试扣除 HC,成功返回 true,不足返回 false ;(window as any).__gameSpendHC = (amount: number): boolean => { return this.manager.spendHC(amount) } } this.setupInteraction() this.setupManagerCallbacks() if (typeof window !== 'undefined') { ;(window as any).__gameReady?.() } } private createMuteButton(_audio: AudioEngine): void { // 静音按钮已移除,音乐默认开启 void _audio } /** * 应用 PUA buff 效果到游戏 * effect: attack_boost | speed_boost | money_rain | rage_mode | backfire */ private applyPuaBuff(effect: string, score: number, title: string): void { const towers = this.towerManager.getAllTowers() const audio = AudioEngine.getInstance() // 全屏效果提示横幅 const colors: Record = { rage_mode: '#FF4E00', speed_boost: '#FBBF24', attack_boost: '#22C55E', money_rain: '#A78BFA', backfire: '#6B7280', } const color = colors[effect] ?? '#E2E8F0' this.showPuaBanner(title, score, color) const dur = Math.max(8000, score * 1500) // buff 持续时间(ms) switch (effect) { case 'rage_mode': { // 9-10分:全场狂暴 — 攻速×2,全图红色相机震动 audio.playBossAppear() this.cameras.main.flash(600, 255, 60, 0, false) this.cameras.main.shake(400, 0.008) towers.forEach(t => { t['_puaSpeedMult'] = 2.0 }) this.manager.addHC(score * 15) this.time.delayedCall(dur, () => { towers.forEach(t => { t['_puaSpeedMult'] = 1.0 }) }) break } case 'speed_boost': { // 7-8分:攻速 ×1.5 audio.playWaveStart() towers.forEach(t => { t['_puaSpeedMult'] = 1.5 }) this.time.delayedCall(dur, () => { towers.forEach(t => { t['_puaSpeedMult'] = 1.0 }) }) break } case 'attack_boost': { // 4-6分:伤害 ×(1 + score*0.08) audio.playDingTalk() const mult = 1 + score * 0.08 towers.forEach(t => { t['_puaDmgMult'] = mult }) this.time.delayedCall(dur, () => { towers.forEach(t => { t['_puaDmgMult'] = 1.0 }) }) break } case 'money_rain': { // 5-7分:立即获得 HC,屏幕绿色雨 const hcBonus = score * 20 this.manager.addHC(hcBonus) audio.playOpsAttack() this.showMoneyRain(hcBonus) break } case 'backfire': { // 1-2分:废话反效果 — 全场塔禁锢 2 秒 audio.playEnemyDeath() this.cameras.main.shake(300, 0.005) this.freezeAllTowers(2000) break } } } /** 显示 PUA buff 横幅 */ private showPuaBanner(title: string, score: number, color: string): void { const stars = '★'.repeat(Math.min(score, 10)) const banner = this.add.text( GAME_WIDTH / 2, HUD_HEIGHT + 80, `${title} ${stars}`, { fontFamily: 'VT323, monospace', fontSize: '28px', color, stroke: '#000000', strokeThickness: 4, shadow: { offsetX: 0, offsetY: 0, color, blur: 16, stroke: true, fill: true }, } ).setOrigin(0.5, 0.5).setDepth(60).setAlpha(0) this.tweens.add({ targets: banner, alpha: 1, y: HUD_HEIGHT + 60, duration: 300, yoyo: false, onComplete: () => { this.time.delayedCall(2000, () => { this.tweens.add({ targets: banner, alpha: 0, y: HUD_HEIGHT + 40, duration: 400, onComplete: () => banner.destroy(), }) }) }, }) } /** 金币雨效果(money_rain) */ private showMoneyRain(amount: number): void { for (let i = 0; i < 12; i++) { const x = Math.random() * GAME_WIDTH const coin = this.add.text(x, HUD_HEIGHT + 20, `+${Math.floor(amount / 12)}HC`, { fontFamily: 'VT323, monospace', fontSize: '16px', color: '#A78BFA', stroke: '#000', strokeThickness: 2, }).setOrigin(0.5, 0).setDepth(55).setAlpha(0.9) this.tweens.add({ targets: coin, y: coin.y + 120 + Math.random() * 80, alpha: 0, delay: i * 80, duration: 900, onComplete: () => coin.destroy(), }) } } private setupManagerCallbacks(): void { this.manager.onHCChange.push((hc: number) => { this.hcText.setText(`HC: ${hc}`) if (typeof window !== 'undefined') { ;(window as any).__gameOnHCChange?.(hc) } }) this.manager.onKPIChange.push((kpi: number) => { updateKPIBar(this.kpiBar, kpi) this.kpiText.setText(`${kpi}%`) // KPI 危险时播放警告音(每次低于 30% 时触发一次) if (kpi <= 30 && kpi > 0) { AudioEngine.getInstance().playKPIWarning() } }) this.manager.onGameOver.push(() => { this.hud.showGameOver() }) this.manager.onVictory.push(() => { this.hud.showVictory() }) } /** * 加载指定地图:渲染背景、路径、装饰物,重置 WaveManager */ private loadMap(mapConfig: MapConfig): void { this.currentPathTiles = buildPathTiles(mapConfig.waypoints) // 同步路径格给 TowerManager,用于建塔合法性判断 this.towerManager.setCurrentPathTiles(this.currentPathTiles) this.bgObject?.destroy() this.bgObject = renderMapBackground(this, mapConfig.bgKey) drawAllTiles(this.tileGraphics, null, this.currentPathTiles, mapConfig) this.labelObjects.forEach(l => l.destroy()) this.labelObjects = renderMapLabels(this, mapConfig.labels) this.decorationObjects.forEach(d => d.destroy()) this.decorationObjects = renderDecorations(this, mapConfig.decorations, this.currentPathTiles) this.waveManager = new WaveManager( this, mapConfig.waves, this.manager.difficulty, { onWaveComplete: () => this.onWeeklyReport(), onAllWavesComplete: () => this.onMapCleared(), onDestroyRandomTower: () => this.towerManager.removeRandomTower(), }, mapConfig.waypoints ) this.mapInTransition = false this.isWaveRunning = false } // 自动下一波倒计时(ms),-1 表示未激活 private autoNextWaveTimer: number = -1 private readonly AUTO_WAVE_DELAY = 3000 // 3 秒后自动开始 update(_time: number, delta: number): void { if (this.manager.gameState !== 'playing' && this.manager.gameState !== 'idle') return if (this.mapInTransition) return this.towerManager.update(delta, this.waveManager.getAllActiveEnemies()) this.waveManager.update(delta) // 当前波次怪物全灭且还有下一波时,启动自动倒计时 if ( this.isWaveRunning && this.waveManager.getAllActiveEnemies().length === 0 && this.waveManager.hasMoreWaves() ) { this.isWaveRunning = false if (this.autoNextWaveTimer < 0) { this.autoNextWaveTimer = this.AUTO_WAVE_DELAY this.hud.setWaveButtonText('3s 后自动开始...') this.hud.enableWaveButton() } } // 自动倒计时 if (this.autoNextWaveTimer > 0) { this.autoNextWaveTimer -= delta const sec = Math.ceil(this.autoNextWaveTimer / 1000) this.hud.setWaveButtonText(`${sec}s 后自动开始...`) if (this.autoNextWaveTimer <= 0) { this.autoNextWaveTimer = -1 this.onWaveButtonClick() } } } private onWaveButtonClick(): void { if (!this.waveManager.hasMoreWaves() || this.isWaveRunning || this.mapInTransition) return this.autoNextWaveTimer = -1 this.isWaveRunning = true this.hud.disableWaveButton() this.hud.setWaveButtonText('波次进行中...') 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 { this.hud.showWeeklyReportAlert() this.time.delayedCall(600, () => { this.weeklyModal.show() }) } private onMapCleared(): void { // 若游戏已失败,不触发地图切换 if (this.manager.gameState === 'defeat') return this.mapInTransition = true this.autoNextWaveTimer = -1 this.isWaveRunning = false this.hud.disableWaveButton() this.hud.setWaveButtonText('关卡完成!') const currentMap = ALL_MAPS[this.manager.currentMapIndex] const nextIndex = this.manager.currentMapIndex + 1 if (nextIndex >= ALL_MAPS.length) { this.manager.totalWaveCleared += currentMap.waveCount this.mapTransitionModal.show(currentMap, () => { this.manager.triggerVictory() }) return } this.mapTransitionModal.show(currentMap, () => { 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('▶ 召唤下一波') }) } freezeAllTowers(duration: number = 3000): void { this.towerManager.getAllTowers().forEach(tower => { tower.isFrozen = true setTimeout(() => { tower.isFrozen = false }, duration) }) } refillAllStamina(): void { this.towerManager.getAllTowers().forEach(tower => { tower.stamina = tower.maxStamina tower['isActive'] = true }) } private setupInteraction(): void { const { cellW, cellH } = getCellSize() this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => { const col = Math.floor(pointer.x / cellW) const row = Math.floor((pointer.y - HUD_HEIGHT) / cellH) const inBounds = col >= 0 && col < MAP_COLS && row >= 0 && row < MAP_ROWS if (inBounds && !this.currentPathTiles.has(`${col},${row}`)) { if (!this.hoveredTile || this.hoveredTile.col !== col || this.hoveredTile.row !== row) { this.hoveredTile = { col, row } drawAllTiles(this.tileGraphics, this.hoveredTile, this.currentPathTiles, ALL_MAPS[this.manager.currentMapIndex]) } if (this.selectedTowerType) this.drawBuildPreview(col, row, cellW, cellH) return } if (this.hoveredTile !== null) { this.hoveredTile = null drawAllTiles(this.tileGraphics, null, this.currentPathTiles, ALL_MAPS[this.manager.currentMapIndex]) this.buildModeGraphics.clear() } }) this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { const col = Math.floor(pointer.x / cellW) const row = Math.floor((pointer.y - HUD_HEIGHT) / cellH) if (col >= 0 && col < MAP_COLS && row >= 0 && row < MAP_ROWS) { this.handleTileClick(col, row, cellW, cellH) } }) } private drawBuildPreview(col: number, row: number, cellW: number, cellH: number): void { this.buildModeGraphics.clear() if (!this.towerManager.canPlace(col, row)) return const x = col * cellW const y = HUD_HEIGHT + row * cellH this.buildModeGraphics.lineStyle(2, 0xa78bfa, 0.9) this.buildModeGraphics.strokeRect(x + 2, y + 2, cellW - 4, cellH - 4) this.buildModeGraphics.fillStyle(0xa78bfa, 0.15) this.buildModeGraphics.fillRect(x + 2, y + 2, cellW - 4, cellH - 4) } private handleTileClick(col: number, row: number, cellW: number, cellH: number): void { if (this.selectedTowerType) { if (this.currentPathTiles.has(`${col},${row}`)) return const placed = this.towerManager.placeTower(col, row, this.selectedTowerType) if (placed) { this.buildModeGraphics.clear() this.selectedTowerType = null if (typeof window !== 'undefined') { ;(window as any).__gameOnTowerDeselect?.() } } else { this.showTip(col, row, cellW, cellH, 'HC不足或格子已占用', '#EF4444') } return } const hasTower = this.towerManager.handleTileClick(col, row) if (!hasTower && !this.currentPathTiles.has(`${col},${row}`)) { this.showTip(col, row, cellW, cellH, '请先从底部选择塔', '#A78BFA') } } private showTip(col: number, row: number, cellW: number, cellH: number, msg: string, color: string): void { const x = col * cellW + cellW / 2 const y = HUD_HEIGHT + row * cellH + cellH / 2 const tip = this.add.text(x, y - 20, msg, { fontFamily: 'VT323, monospace', fontSize: '16px', color, backgroundColor: '#0a1628', padding: { x: 8, y: 4 }, }).setOrigin(0.5, 1).setDepth(20) this.time.delayedCall(1200, () => tip.destroy()) } } return GameScene }