Files
test1/game/GameScene.ts

494 lines
18 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()
})
// 注册 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<string, string> = {
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
}