Files
test1/game/GameScene.ts

355 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string> = {
'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<string> = 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()
})
// 添加静音切换按钮(右上角)
this.createMuteButton(audio)
this.setupInteraction()
this.setupManagerCallbacks()
if (typeof window !== 'undefined') { ;(window as any).__gameReady?.() }
}
private createMuteButton(audio: AudioEngine): void {
const btn = this.add.text(GAME_WIDTH - 10, 8, '🔊', {
fontSize: '16px', backgroundColor: 'rgba(0,0,0,0.35)',
padding: { x: 6, y: 3 },
}).setOrigin(1, 0).setDepth(50).setInteractive({ useHandCursor: true })
btn.on('pointerdown', () => {
const nowMuted = !audio.isMuted()
audio.setMuted(nowMuted)
btn.setText(nowMuted ? '🔇' : '🔊')
if (!nowMuted) audio.startBGM()
else audio.stopBGM()
})
}
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()
}
private onWeeklyReport(): void {
this.hud.showWeeklyReportAlert()
this.time.delayedCall(600, () => { this.weeklyModal.show() })
}
private onMapCleared(): void {
this.mapInTransition = true
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.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
}