diff --git a/app/game/page.tsx b/app/game/page.tsx index 52f2cb5..b066c46 100644 --- a/app/game/page.tsx +++ b/app/game/page.tsx @@ -1,19 +1,38 @@ 'use client' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState, useCallback } from 'react' + +const TOWER_META = [ + { type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤 1.5/s', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场:5%概率秒杀' }, + { type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤 5格射程', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' }, + { type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速 5伤', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' }, + { type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助 +20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' }, +] as const + +type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp' -/** - * 游戏页面 - * Phaser 通过动态 import 加载,确保 SSR 安全(不在服务端执行) - */ export default function GamePage() { const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null) + const [hc, setHc] = useState(200) + const [selectedTower, setSelectedTower] = useState(null) + const [gameReady, setGameReady] = useState(false) + // expose setter to game scene via window + const selectedTowerRef = useRef(null) + + const handleSelectTower = useCallback((type: TowerType) => { + const next = selectedTowerRef.current === type ? null : type + selectedTowerRef.current = next + setSelectedTower(next) + // notify scene + if (typeof window !== 'undefined') { + (window as any).__gameSelectTower?.(next) + } + }, []) useEffect(() => { let mounted = true const initGame = async () => { - // 动态导入 Phaser,避免 SSR 环境报错 const Phaser = (await import('phaser')).default const { createGameConfig } = await import('@/game/config') const { createGameScene } = await import('@/game/GameScene') @@ -21,20 +40,31 @@ export default function GamePage() { if (!mounted) return const GameScene = createGameScene(Phaser) - const config = createGameConfig('game-container') - - // 注入场景 + const config = createGameConfig('game-canvas-container') config.scene = [GameScene] - // 手动设置 scale mode(避免 SSR 时引用 Phaser.Scale 常量报错) if (config.scale) { config.scale.mode = Phaser.Scale.FIT config.scale.autoCenter = Phaser.Scale.CENTER_BOTH } - - // type 设置 config.type = Phaser.AUTO + // expose HC update to React + if (typeof window !== 'undefined') { + (window as any).__gameOnHCChange = (val: number) => { + if (mounted) setHc(val) + } + (window as any).__gameOnTowerDeselect = () => { + if (mounted) { + selectedTowerRef.current = null + setSelectedTower(null) + } + } + ;(window as any).__gameReady = () => { + if (mounted) setGameReady(true) + } + } + gameRef.current = new Phaser.Game(config) } @@ -44,12 +74,98 @@ export default function GamePage() { mounted = false gameRef.current?.destroy(true) gameRef.current = null + if (typeof window !== 'undefined') { + delete (window as any).__gameOnHCChange + delete (window as any).__gameOnTowerDeselect + delete (window as any).__gameSelectTower + delete (window as any).__gameReady + } } }, []) return ( -
-
+
+ {/* Phaser canvas 区域,flex-1 填满剩余高度 */} +
+ + {/* 底部塔选择面板 — 纯 React DOM,不被 Canvas 遮挡 */} +
+ {/* 选塔提示 */} +
+ {selectedTower ? '点击格子建造' : '选择塔 ▼'} +
+ + {TOWER_META.map((meta) => { + const canAfford = hc >= meta.cost + const isSelected = selectedTower === meta.type + return ( + + ) + })} +
) } diff --git a/app/page.tsx b/app/page.tsx index a4d2d69..bd63cf8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -15,8 +15,16 @@ export default function GameCover() { return (
+ {/* 深色叠加层,确保文字可读 */} +
+ {/* CRT 流动扫描线 */}
diff --git a/game/GameScene.ts b/game/GameScene.ts index 35ca9c4..7216ffe 100644 --- a/game/GameScene.ts +++ b/game/GameScene.ts @@ -12,7 +12,6 @@ import { } from './mapRenderer' import { TowerManager, type TowerType } from './towers/TowerManager' import { WaveManager } from './enemies/WaveManager' -import { TowerPanel } from './ui/TowerPanel' import { HUD } from './ui/HUD' import { WeeklyReportModal } from './ui/WeeklyReportModal' @@ -20,6 +19,18 @@ import { WeeklyReportModal } from './ui/WeeklyReportModal' 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', + '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', +} + export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { class GameScene extends PhaserLib.Scene { private manager!: GameManager @@ -30,7 +41,6 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { private tileGraphics!: Phaser.GameObjects.Graphics private towerManager!: TowerManager private waveManager!: WaveManager - private towerPanel!: TowerPanel private hud!: HUD private weeklyModal!: WeeklyReportModal private selectedTowerType: TowerType | null = null @@ -39,6 +49,13 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { constructor() { super({ key: 'GameScene' }) } + preload(): void { + // 加载所有 AI 生成的角色图片 + for (const [key, path] of Object.entries(SPRITE_ASSETS)) { + this.load.image(key, path) + } + } + create(): void { this.manager = GameManager.getInstance() this.manager.reset() @@ -70,30 +87,39 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { this.hud = new HUD(this) this.hud.createWaveButton(() => this.onWaveButtonClick()) - this.towerPanel = new TowerPanel('game-container') - this.towerPanel.onSelect((type) => { - this.selectedTowerType = type - if (!type) this.buildModeGraphics.clear() - }) + // 接收 React 层传来的选塔事件 + if (typeof window !== 'undefined') { + ;(window as any).__gameSelectTower = (type: TowerType | null) => { + this.selectedTowerType = type + if (!type) this.buildModeGraphics.clear() + } + } this.setupInteraction() + // HC/KPI 变化时同步到 React HUD + this.manager.onHCChange.push((hc: number) => { + this.hcText.setText(`HC: ${hc}`) + // 通知 React 层更新塔面板可用状态 + if (typeof window !== 'undefined') { + ;(window as any).__gameOnHCChange?.(hc) + } + }) this.manager.onKPIChange.push((kpi: number) => { updateKPIBar(this.kpiBar, kpi) this.kpiText.setText(`${kpi}%`) }) - this.manager.onHCChange.push((hc: number) => { - this.hcText.setText(`HC: ${hc}`) - this.towerPanel.refreshCardStates() - }) this.manager.onGameOver.push(() => { this.hud.showGameOver() - this.towerPanel.destroy() }) this.manager.onVictory.push(() => { this.hud.showVictory() - this.towerPanel.destroy() }) + + // 通知 React 层游戏已就绪 + if (typeof window !== 'undefined') { + ;(window as any).__gameReady?.() + } } update(_time: number, delta: number): void { @@ -128,7 +154,7 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { } /** 禁锢全场塔(老板视察效果) */ - private freezeAllTowers(duration: number = 3000): void { + freezeAllTowers(duration: number = 3000): void { this.towerManager.getAllTowers().forEach(tower => { tower.isFrozen = true setTimeout(() => { tower.isFrozen = false }, duration) @@ -136,7 +162,7 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { } /** 补满全场塔精力 */ - private refillAllStamina(): void { + refillAllStamina(): void { this.towerManager.getAllTowers().forEach(tower => { tower.stamina = tower.maxStamina tower['isActive'] = true @@ -183,6 +209,9 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { 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 { @@ -191,8 +220,11 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { const placed = this.towerManager.placeTower(col, row, this.selectedTowerType) if (placed) { this.buildModeGraphics.clear() - this.towerPanel.deselect() this.selectedTowerType = null + // 通知 React 取消选中状态 + if (typeof window !== 'undefined') { + ;(window as any).__gameOnTowerDeselect?.() + } } else { this.showTip(col, row, cellW, cellH, 'HC不足或格子已占用', '#EF4444') } @@ -200,7 +232,7 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { } const hasTower = this.towerManager.handleTileClick(col, row) if (!hasTower && !PATH_TILES.has(`${col},${row}`)) { - this.showTip(col, row, cellW, cellH, '从底部面板选择塔', '#A78BFA') + this.showTip(col, row, cellW, cellH, '请先从底部选择塔', '#A78BFA') } } diff --git a/game/enemies/BossVP.ts b/game/enemies/BossVP.ts index e1bfb0b..4a16965 100644 --- a/game/enemies/BossVP.ts +++ b/game/enemies/BossVP.ts @@ -5,104 +5,74 @@ import { getRandomQuote } from '../data/quotes' export class BossVP extends EnemyBase { private skillTimer: number = 20000 private onDestroyTower?: () => void + private bossLabel!: Phaser.GameObjects.Text constructor( scene: Phaser.Scene, pathPoints: PathPoint[], onDestroyTower?: () => void ) { - super(scene, pathPoints, 800, 40, 30, 150) + super(scene, pathPoints, 800, 40, 30, 150, 'enemy-boss') this.onDestroyTower = onDestroyTower - this.drawSprite() - // BOSS 出现时全屏红色闪光 + // 放大 BOSS + const bossScale = Math.min(this.cellW, this.cellH) / 128 * 1.3 + this.imageSprite.setScale(bossScale) + this.imageSprite.setDepth(12) + // BOSS 出现特效 scene.cameras.main.flash(800, 255, 0, 0, false) this.showBossAlert() + // BOSS 名字标签 + this.bossLabel = scene.add.text(this.x, this.y + this.cellH * 0.5, '空降VP', { + fontFamily: 'VT323, monospace', fontSize: '14px', + color: '#FBBF24', backgroundColor: '#7c2d12', padding: { x: 4, y: 1 }, + }).setOrigin(0.5, 0).setDepth(15) } private showBossAlert(): void { const alert = this.scene.add - .text(640, 360, '⚠ 空降VP来袭!⚠', { - fontFamily: 'VT323, monospace', - fontSize: '36px', - color: '#FBBF24', - backgroundColor: '#7F1D1D', - padding: { x: 16, y: 8 }, - }) - .setOrigin(0.5, 0.5) - .setDepth(50) + .text(this.scene.scale.width / 2, this.scene.scale.height / 2, '⚠ 空降VP来袭 ⚠', { + fontFamily: 'VT323, monospace', fontSize: '36px', + color: '#FBBF24', backgroundColor: '#7F1D1D', padding: { x: 16, y: 8 }, + }).setOrigin(0.5, 0.5).setDepth(50) this.scene.tweens.add({ - targets: alert, - alpha: 0, - duration: 2500, - delay: 500, + targets: alert, alpha: 0, duration: 2500, delay: 500, onComplete: () => alert.destroy(), }) } - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 金色六边形 - this.sprite.fillStyle(0xfbbf24, 1) - const r = 22 - this.sprite.fillPoints(this.hexPoints(r), true) - // 金色外框 - this.sprite.lineStyle(3, 0xf59e0b, 1) - this.sprite.strokePoints(this.hexPoints(r + 4), false) - // 内部颜色 - this.sprite.fillStyle(0xd97706, 1) - this.sprite.fillCircle(0, 0, 8) - this.sprite.setDepth(12) - this.sprite.setPosition(this.x, this.y) - } - - private hexPoints(r: number): Phaser.Types.Math.Vector2Like[] { - const pts: Phaser.Types.Math.Vector2Like[] = [] - for (let i = 0; i < 6; i++) { - const angle = (Math.PI / 3) * i - Math.PI / 6 - pts.push({ x: Math.cos(angle) * r, y: Math.sin(angle) * r }) - } - return pts - } - override update(delta: number): void { if (this.isDead) return super.update(delta) - this.skillTimer -= delta if (this.skillTimer <= 0) { this.skillTimer = 20000 this.triggerOrgRestructure() } - // 重绘六边形到新位置 - this.drawSprite() + // 更新名字标签位置 + if (this.bossLabel) { + this.bossLabel.setPosition(this.x, this.y + this.cellH * 0.5) + } + // BOSS 金色发光边框 + this.imageSprite.setTint(this.skillTimer < 3000 ? 0xff6600 : 0xfbbf24) } private triggerOrgRestructure(): void { - // 组织架构调整:随机摧毁一个防御塔 - if (this.onDestroyTower) { - this.onDestroyTower() - } + this.onDestroyTower?.() const txt = this.scene.add .text(this.x, this.y - 40, '组织架构调整!', { - fontFamily: 'VT323, monospace', - fontSize: '18px', - color: '#FBBF24', - backgroundColor: '#7C2D12', - padding: { x: 6, y: 3 }, - }) - .setOrigin(0.5, 1) - .setDepth(25) + fontFamily: 'VT323, monospace', fontSize: '18px', + color: '#FBBF24', backgroundColor: '#7C2D12', padding: { x: 6, y: 3 }, + }).setOrigin(0.5, 1).setDepth(25) this.scene.tweens.add({ - targets: txt, - y: this.y - 70, - alpha: 0, - duration: 2000, - onComplete: () => txt.destroy(), + targets: txt, y: this.y - 70, alpha: 0, + duration: 2000, onComplete: () => txt.destroy(), }) } - getQuote(): string { - return getRandomQuote('BossVP') + override destroy(): void { + this.bossLabel?.destroy() + super.destroy() } + + getQuote(): string { return getRandomQuote('BossVP') } } diff --git a/game/enemies/EnemyBase.ts b/game/enemies/EnemyBase.ts index c33fb91..78747bb 100644 --- a/game/enemies/EnemyBase.ts +++ b/game/enemies/EnemyBase.ts @@ -1,26 +1,23 @@ import type Phaser from 'phaser' import { GameManager } from '../GameManager' -import { TILE_SIZE, HUD_HEIGHT, PATH_WAYPOINTS } from '../constants' +import { HUD_HEIGHT, PATH_WAYPOINTS } from '../constants' +import { getCellSize } from '../mapRenderer' -export interface PathPoint { - x: number - y: number -} +export interface PathPoint { x: number; y: number } -/** 将格子坐标转换为像素坐标(格子中心) */ -function gridToPixel(gx: number, gy: number): PathPoint { +function gridToPixel(gx: number, gy: number, cellW: number, cellH: number): PathPoint { return { - x: gx * TILE_SIZE + TILE_SIZE / 2, - y: gy * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT, + x: gx * cellW + cellW / 2, + y: gy * cellH + cellH / 2 + HUD_HEIGHT, } } -/** 将 PATH_WAYPOINTS 扩展为完整转折路径(去重相邻相同点) */ export function buildFullPath(): PathPoint[] { + const { cellW, cellH } = getCellSize() const points: PathPoint[] = [] for (let i = 0; i < PATH_WAYPOINTS.length - 1; i++) { - const from = gridToPixel(PATH_WAYPOINTS[i].x, PATH_WAYPOINTS[i].y) - const to = gridToPixel(PATH_WAYPOINTS[i + 1].x, PATH_WAYPOINTS[i + 1].y) + const from = gridToPixel(PATH_WAYPOINTS[i].x, PATH_WAYPOINTS[i].y, cellW, cellH) + const to = gridToPixel(PATH_WAYPOINTS[i + 1].x, PATH_WAYPOINTS[i + 1].y, cellW, cellH) points.push(from) points.push(to) } @@ -29,15 +26,14 @@ export function buildFullPath(): PathPoint[] { ) } -export interface DotEffect { - damage: number - duration: number - timer: number -} +export interface DotEffect { damage: number; duration: number; timer: number } export abstract class EnemyBase { protected scene: Phaser.Scene - public sprite!: Phaser.GameObjects.Graphics + protected imageSprite!: Phaser.GameObjects.Image + // expose for tower targeting & health bar + public x: number = 0 + public y: number = 0 protected healthBar!: Phaser.GameObjects.Graphics protected quoteText!: Phaser.GameObjects.Text @@ -46,30 +42,32 @@ export abstract class EnemyBase { public speed: number public readonly kpiDamage: number public readonly hcReward: number + protected spriteKey: string protected pathPoints: PathPoint[] protected currentPathIndex: number = 0 - protected x: number = 0 - protected y: number = 0 public isDead: boolean = false public isActive: boolean = true - get pathProgress(): number { - return this.currentPathIndex - } + get pathProgress(): number { return this.currentPathIndex } + public dotEffects: DotEffect[] = [] public slowEffect: number = 0 public slowTimer: number = 0 public shieldCount: number = 0 + protected cellW: number + protected cellH: number + constructor( scene: Phaser.Scene, pathPoints: PathPoint[], maxHp: number, speed: number, kpiDamage: number, - hcReward: number + hcReward: number, + spriteKey: string ) { this.scene = scene this.pathPoints = pathPoints @@ -78,28 +76,40 @@ export abstract class EnemyBase { this.speed = speed this.kpiDamage = kpiDamage this.hcReward = hcReward + this.spriteKey = spriteKey + + const { cellW, cellH } = getCellSize() + this.cellW = cellW + this.cellH = cellH if (pathPoints.length > 0) { this.x = pathPoints[0].x this.y = pathPoints[0].y } - this.sprite = scene.add.graphics() + // 用 AI 图片精灵,尺寸按格子缩放 + this.imageSprite = scene.add.image(this.x, this.y, spriteKey) + const scale = Math.min(cellW, cellH) / 128 * 0.75 + this.imageSprite.setScale(scale) + this.imageSprite.setDepth(10) + this.healthBar = scene.add.graphics() + this.quoteText = scene.add .text(this.x, this.y - 30, this.getQuote(), { fontFamily: 'VT323, monospace', - fontSize: '12px', + fontSize: '13px', color: '#FFFFFF', - backgroundColor: 'rgba(0,0,0,0.6)', - padding: { x: 3, y: 2 }, + backgroundColor: 'rgba(0,0,0,0.7)', + padding: { x: 4, y: 2 }, }) .setOrigin(0.5, 1) .setDepth(15) .setAlpha(0) - this.drawSprite() this.drawHealthBar() + // 出生后 0.5s 显示语录 + scene.time.delayedCall(500, () => { if (!this.isDead) this.showQuote() }) } update(delta: number): void { @@ -110,11 +120,10 @@ export abstract class EnemyBase { if (this.slowTimer <= 0) this.slowEffect = 0 } this.moveAlongPath(delta) + this.imageSprite.setPosition(this.x, this.y) this.drawHealthBar() - this.sprite.setPosition(this.x, this.y) - // 语录文字跟随怪物移动 - if (this.quoteText && this.quoteText.alpha > 0) { - this.quoteText.setPosition(this.x, this.y - 30) + if (this.quoteText?.alpha > 0) { + this.quoteText.setPosition(this.x, this.y - (this.cellH * 0.45)) } } @@ -122,51 +131,46 @@ export abstract class EnemyBase { for (let i = this.dotEffects.length - 1; i >= 0; i--) { const dot = this.dotEffects[i] dot.timer -= delta - const tickDamage = (dot.damage / 1000) * delta - this.hp -= tickDamage - if (this.hp <= 0) { - this.die() - return - } - if (dot.timer <= 0) { - this.dotEffects.splice(i, 1) - } + this.hp -= (dot.damage / 1000) * delta + if (this.hp <= 0) { this.die(); return } + if (dot.timer <= 0) this.dotEffects.splice(i, 1) } } protected moveAlongPath(delta: number): void { if (this.currentPathIndex >= this.pathPoints.length - 1) { - this.reachEnd() - return + this.reachEnd(); return } const target = this.pathPoints[this.currentPathIndex + 1] const currentSpeed = this.speed * (1 - this.slowEffect) const distance = (currentSpeed * delta) / 1000 - const dx = target.x - this.x const dy = target.y - this.y const dist = Math.sqrt(dx * dx + dy * dy) - if (dist <= distance) { - this.x = target.x - this.y = target.y + this.x = target.x; this.y = target.y this.currentPathIndex++ } else { this.x += (dx / dist) * distance this.y += (dy / dist) * distance } + // 减速时图片变色 + if (this.slowEffect > 0) { + this.imageSprite.setTint(0x93c5fd) + } else { + this.imageSprite.clearTint() + } } protected drawHealthBar(): void { this.healthBar.clear() - const bw = 30 - const bh = 4 + const bw = this.cellW * 0.5 + const bh = 5 const bx = this.x - bw / 2 - const by = this.y - 20 + const by = this.y - this.cellH * 0.45 const ratio = Math.max(0, this.hp / this.maxHp) const color = ratio > 0.5 ? 0x22c55e : ratio > 0.25 ? 0xf59e0b : 0xef4444 - - this.healthBar.fillStyle(0x374151, 1) + this.healthBar.fillStyle(0x1f2937, 1) this.healthBar.fillRect(bx, by, bw, bh) this.healthBar.fillStyle(color, 1) this.healthBar.fillRect(bx, by, bw * ratio, bh) @@ -182,25 +186,16 @@ export abstract class EnemyBase { } this.hp -= damage this.drawHealthBar() - if (this.hp <= 0) { - this.die() - } + if (this.hp <= 0) this.die() } private showShieldBlock(): void { const txt = this.scene.add .text(this.x, this.y - 35, '护盾!', { - fontFamily: 'VT323, monospace', - fontSize: '14px', - color: '#93C5FD', - }) - .setOrigin(0.5, 1) - .setDepth(20) + fontFamily: 'VT323, monospace', fontSize: '14px', color: '#93C5FD', + }).setOrigin(0.5, 1).setDepth(20) this.scene.tweens.add({ - targets: txt, - y: this.y - 55, - alpha: 0, - duration: 800, + targets: txt, y: this.y - 55, alpha: 0, duration: 800, onComplete: () => txt.destroy(), }) } @@ -217,43 +212,35 @@ export abstract class EnemyBase { protected die(): void { if (this.isDead) return this.isDead = true - const manager = GameManager.getInstance() - manager.addHC(this.hcReward) + GameManager.getInstance().addHC(this.hcReward) this.onDeath() this.destroy() } protected reachEnd(): void { if (this.isDead) return - const manager = GameManager.getInstance() - manager.reduceKPI(this.kpiDamage) + GameManager.getInstance().reduceKPI(this.kpiDamage) this.isDead = true this.destroy() } protected onDeath(): void {} - /** 显示头顶语录(出生后随机触发,3秒后淡出) */ showQuote(): void { if (this.isDead || !this.quoteText) return this.quoteText.setText(this.getQuote()) - this.quoteText.setPosition(this.x, this.y - 30) + this.quoteText.setPosition(this.x, this.y - this.cellH * 0.45) this.quoteText.setAlpha(1) - // 3秒后淡出动画 this.scene.tweens.add({ - targets: this.quoteText, - alpha: 0, - duration: 800, - delay: 2200, - ease: 'Linear', + targets: this.quoteText, alpha: 0, + duration: 800, delay: 2200, ease: 'Linear', }) } - abstract drawSprite(): void abstract getQuote(): string destroy(): void { - this.sprite?.destroy() + this.imageSprite?.destroy() this.healthBar?.destroy() this.quoteText?.destroy() } diff --git a/game/enemies/FreshGraduate.ts b/game/enemies/FreshGraduate.ts index 805f185..de59131 100644 --- a/game/enemies/FreshGraduate.ts +++ b/game/enemies/FreshGraduate.ts @@ -4,20 +4,7 @@ import { getRandomQuote } from '../data/quotes' export class FreshGraduate extends EnemyBase { constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) { - super(scene, pathPoints, 30, 120, 2, 10) - this.drawSprite() - } - - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - this.sprite.fillStyle(0x86efac, 1) - this.sprite.fillCircle(0, 0, 8) - this.sprite.setDepth(10) - this.sprite.setPosition(this.x, this.y) - } - - getQuote(): string { - return getRandomQuote('FreshGraduate') + super(scene, pathPoints, 30, 120, 2, 10, 'enemy-fresh') } + getQuote(): string { return getRandomQuote('FreshGraduate') } } diff --git a/game/enemies/OldEmployee.ts b/game/enemies/OldEmployee.ts index 3e99e84..fccb464 100644 --- a/game/enemies/OldEmployee.ts +++ b/game/enemies/OldEmployee.ts @@ -4,35 +4,12 @@ import { getRandomQuote } from '../data/quotes' export class OldEmployee extends EnemyBase { constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) { - super(scene, pathPoints, 150, 50, 8, 30) + super(scene, pathPoints, 150, 50, 8, 30, 'enemy-old') this.shieldCount = 3 - this.drawSprite() + // 老员工图片稍大 + const scale = Math.min(this.cellW, this.cellH) / 128 * 0.9 + this.imageSprite.setScale(scale) } - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 大蓝方块 - this.sprite.fillStyle(0x93c5fd, 1) - this.sprite.fillRect(-10, -10, 20, 20) - // 护盾外框(金色) - this.sprite.lineStyle(2, 0xfbbf24, 0.8) - this.sprite.strokeRect(-12, -12, 24, 24) - this.sprite.setDepth(10) - this.sprite.setPosition(this.x, this.y) - } - - override drawHealthBar(): void { - super.drawHealthBar() - // 绘制护盾数量标记 - if (!this.healthBar) return - for (let i = 0; i < this.shieldCount; i++) { - this.healthBar.fillStyle(0xfbbf24, 1) - this.healthBar.fillRect(this.x - 15 + i * 11, this.y - 28, 8, 4) - } - } - - getQuote(): string { - return getRandomQuote('OldEmployee') - } + getQuote(): string { return getRandomQuote('OldEmployee') } } diff --git a/game/enemies/TroubleMaker.ts b/game/enemies/TroubleMaker.ts index cbbe6bc..31f03e1 100644 --- a/game/enemies/TroubleMaker.ts +++ b/game/enemies/TroubleMaker.ts @@ -5,49 +5,21 @@ import { getRandomQuote } from '../data/quotes' export class TroubleMaker extends EnemyBase { constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) { - super(scene, pathPoints, 80, 80, 5, 20) - this.drawSprite() - } - - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 三角形(叹号形状) - this.sprite.fillStyle(0xfca5a5, 1) - this.sprite.fillTriangle(0, -14, -12, 10, 12, 10) - // 叹号 - this.sprite.fillStyle(0x7f1d1d, 1) - this.sprite.fillRect(-2, -6, 4, 8) - this.sprite.fillRect(-2, 6, 4, 4) - this.sprite.setDepth(10) - this.sprite.setPosition(this.x, this.y) + super(scene, pathPoints, 80, 80, 5, 20, 'enemy-trouble') } protected override onDeath(): void { - // 劳动仲裁:死亡时扣玩家 20 HC - const manager = GameManager.getInstance() - manager.spendHC(20) - // 显示提示文字 + GameManager.getInstance().spendHC(20) const txt = this.scene.add .text(this.x, this.y - 20, '劳动仲裁! -20HC', { - fontFamily: 'VT323, monospace', - fontSize: '16px', - color: '#FCA5A5', - backgroundColor: '#7F1D1D', - padding: { x: 4, y: 2 }, - }) - .setOrigin(0.5, 1) - .setDepth(25) + fontFamily: 'VT323, monospace', fontSize: '16px', + color: '#FCA5A5', backgroundColor: '#7F1D1D', padding: { x: 4, y: 2 }, + }).setOrigin(0.5, 1).setDepth(25) this.scene.tweens.add({ - targets: txt, - y: this.y - 50, - alpha: 0, - duration: 1500, - onComplete: () => txt.destroy(), + targets: txt, y: this.y - 50, alpha: 0, + duration: 1500, onComplete: () => txt.destroy(), }) } - getQuote(): string { - return getRandomQuote('TroubleMaker') - } + getQuote(): string { return getRandomQuote('TroubleMaker') } } diff --git a/game/towers/HRBPTower.ts b/game/towers/HRBPTower.ts index 14b435e..38fe105 100644 --- a/game/towers/HRBPTower.ts +++ b/game/towers/HRBPTower.ts @@ -1,45 +1,24 @@ import type Phaser from 'phaser' import { TowerBase } from './TowerBase' import type { EnemyBase } from '../enemies/EnemyBase' -import type { TowerBase as TowerBaseType } from './TowerBase' - -const BUFF_ATTACK_SPEED_BONUS = 0.2 export class HRBPTower extends TowerBase { private buffCooldown: number = 0 private readonly BUFF_INTERVAL = 500 - private nearbyTowersBuff: Set = new Set() + private nearbyTowersBuff: Set = new Set() constructor(scene: Phaser.Scene, gridX: number, gridY: number) { - super(scene, gridX, gridY, 80, 1, 0, 0) - this.drawSprite() - } - - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 菱形(粉色) - this.sprite.fillStyle(0xec4899, 1) - this.sprite.fillTriangle(0, -16, 16, 0, 0, 16) - this.sprite.fillTriangle(0, -16, -16, 0, 0, 16) - this.sprite.setPosition(this.px, this.py) - this.sprite.setDepth(10) + super(scene, gridX, gridY, 80, 1, 0, 0, 'tower-hrbp') } override update(delta: number, enemies: EnemyBase[]): void { - // HRBP 没有攻击逻辑,只做 BUFF void enemies - if (!this.isActive) { - this.stamina = Math.min( - this.maxStamina, - this.stamina + (this.staminaRegen * delta) / 1000 - ) + this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000) if (this.stamina > 20) this.isActive = true this.updateStaminaBar() return } - this.buffCooldown -= delta if (this.buffCooldown <= 0) { this.buffCooldown = this.BUFF_INTERVAL @@ -47,44 +26,30 @@ export class HRBPTower extends TowerBase { } } - setNearbyTowers(towers: TowerBaseType[]): void { + setNearbyTowers(towers: TowerBase[]): void { this.nearbyTowersBuff = new Set(towers) } private applyBuffToNearby(): void { if (this.nearbyTowersBuff.size === 0) return - if (this.stamina < 5) { - this.isActive = false - return - } + if (this.stamina < 5) { this.isActive = false; return } this.stamina -= 5 this.updateStaminaBar() - // BUFF 效果通过 attackSpeedMultiplier 外部读取 - // 这里显示一个粉色光圈效果 this.showBuffEffect() } private showBuffEffect(): void { const g = this.scene.add.graphics() g.lineStyle(2, 0xec4899, 0.6) - g.strokeCircle(this.px, this.py, 90) + g.strokeCircle(this.px, this.py, this.cellW * 1.5) g.setDepth(8) this.scene.tweens.add({ - targets: g, - alpha: 0, - duration: 400, + targets: g, alpha: 0, duration: 400, onComplete: () => g.destroy(), }) } - getBuffedTowers(): Set { - return this.nearbyTowersBuff - } - - getAttackSpeedBonus(): number { - return BUFF_ATTACK_SPEED_BONUS - } - - // HRBP 无直接攻击 + getBuffedTowers(): Set { return this.nearbyTowersBuff } + getAttackSpeedBonus(): number { return 0.2 } attack(_target: EnemyBase): void {} } diff --git a/game/towers/InternTower.ts b/game/towers/InternTower.ts index 2710ad4..798cf7b 100644 --- a/game/towers/InternTower.ts +++ b/game/towers/InternTower.ts @@ -10,37 +10,18 @@ export class InternTower extends TowerBase { public onSelfDestroy?: (tower: InternTower) => void constructor(scene: Phaser.Scene, gridX: number, gridY: number) { - super(scene, gridX, gridY, 50, 1, 15, 1.5) - this.drawSprite() - } - - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 绿色小人(圆头+十字身体) - this.sprite.fillStyle(0x22c55e, 1) - this.sprite.fillCircle(0, -12, 8) - // 身体 - this.sprite.fillRect(-3, -4, 6, 14) - // 手臂 - this.sprite.fillRect(-12, -2, 24, 4) - this.sprite.setPosition(this.px, this.py) - this.sprite.setDepth(10) + super(scene, gridX, gridY, 50, 1, 15, 1.5, 'tower-intern') } override update(delta: number, enemies: EnemyBase[]): void { if (this.destroyed) return - super.update(delta, enemies) - - // 被动:每秒1%概率离场 this.selfDestroyTimer += delta if (this.selfDestroyTimer >= this.SELF_DESTROY_INTERVAL) { this.selfDestroyTimer -= this.SELF_DESTROY_INTERVAL if (Math.random() < 0.01) { - // 退还 25 HC GameManager.getInstance().addHC(25) - this.showMessage('实习生跑路!+25HC') + this.showMessage('实习生跑路!+25HC', '#22C55E') this.destroyed = true this.onSelfDestroy?.(this) this.destroy() @@ -50,42 +31,32 @@ export class InternTower extends TowerBase { } attack(target: EnemyBase): void { - // 整顿职场:5% 概率秒杀 HP < 500 的怪物 if (Math.random() < 0.05 && target.hp < 500) { target.takeDamage(9999) - this.showMessage('整顿职场!秒杀!') + this.showMessage('整顿职场!秒杀!', '#A3E635') } else { target.takeDamage(this.attackDamage) } - // 近战效果(闪光) this.showMeleeEffect(target) } private showMeleeEffect(target: EnemyBase): void { const g = this.scene.add.graphics() - g.fillStyle(0x22c55e, 0.7) - g.fillCircle(target.sprite.x, target.sprite.y, 10) + g.fillStyle(0x22c55e, 0.6) + g.fillCircle(target.x, target.y, 12) g.setDepth(15) this.scene.time.delayedCall(150, () => g.destroy()) } - private showMessage(msg: string): void { + private showMessage(msg: string, color: string): void { const txt = this.scene.add .text(this.px, this.py - 30, msg, { - fontFamily: 'VT323, monospace', - fontSize: '14px', - color: '#22C55E', - backgroundColor: '#14532D', - padding: { x: 4, y: 2 }, - }) - .setOrigin(0.5, 1) - .setDepth(20) + fontFamily: 'VT323, monospace', fontSize: '14px', + color, backgroundColor: '#052e16', padding: { x: 4, y: 2 }, + }).setOrigin(0.5, 1).setDepth(20) this.scene.tweens.add({ - targets: txt, - y: this.py - 55, - alpha: 0, - duration: 1200, - onComplete: () => txt.destroy(), + targets: txt, y: this.py - 55, alpha: 0, + duration: 1200, onComplete: () => txt.destroy(), }) } } diff --git a/game/towers/PPTMasterTower.ts b/game/towers/PPTMasterTower.ts index 9411dfe..66345cb 100644 --- a/game/towers/PPTMasterTower.ts +++ b/game/towers/PPTMasterTower.ts @@ -4,57 +4,37 @@ import type { EnemyBase } from '../enemies/EnemyBase' export class PPTMasterTower extends TowerBase { constructor(scene: Phaser.Scene, gridX: number, gridY: number) { - super(scene, gridX, gridY, 100, 3, 5, 1.5) - this.drawSprite() + super(scene, gridX, gridY, 100, 3, 5, 1.5, 'tower-ppt') } - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 橙色圆形 - this.sprite.fillStyle(0xf59e0b, 1) - this.sprite.fillCircle(0, 0, 16) - // 圆心白点 - this.sprite.fillStyle(0xffffff, 0.9) - this.sprite.fillCircle(0, 0, 5) - this.sprite.setPosition(this.px, this.py) - this.sprite.setDepth(10) + attack(_target: EnemyBase): void { + // AOE 攻击由 TowerManager.updatePPTTower 直接调用 attackAoe } - attack(target: EnemyBase): void { - // AOE 攻击:对射程内所有怪物造成伤害 + 减速 - this.showAoeEffect() - } - - /** 该方法由 TowerManager 调用,传入全体敌人 */ attackAoe(enemies: EnemyBase[]): void { - const rangePx = this.attackRange * 80 // TILE_SIZE - this.showAoeEffect() + const rangePx = this.attackRange * this.cellW + this.showAoeEffect(rangePx) for (const e of enemies) { if (e.isDead) continue - const dx = e.sprite.x - this.px - const dy = e.sprite.y - this.py - const dist = Math.sqrt(dx * dx + dy * dy) - if (dist <= rangePx) { + const dx = e.x - this.px + const dy = e.y - this.py + if (Math.sqrt(dx * dx + dy * dy) <= rangePx) { e.takeDamage(this.attackDamage) - // 黑话领域:减速40%持续2秒 e.addSlow(0.4, 2000) } } } - private showAoeEffect(): void { + private showAoeEffect(rangePx: number): void { const g = this.scene.add.graphics() - g.lineStyle(2, 0xf59e0b, 0.8) - g.strokeCircle(this.px, this.py, this.attackRange * 80) - g.setDepth(12) + g.lineStyle(2, 0xf59e0b, 0.7) + g.strokeCircle(this.px, this.py, rangePx) + g.fillStyle(0xf59e0b, 0.07) + g.fillCircle(this.px, this.py, rangePx) + g.setDepth(8) this.scene.tweens.add({ - targets: g, - alpha: 0, - scaleX: 1.2, - scaleY: 1.2, - duration: 400, - onComplete: () => g.destroy(), + targets: g, alpha: 0, scaleX: 1.15, scaleY: 1.15, + duration: 450, onComplete: () => g.destroy(), }) } } diff --git a/game/towers/SeniorDevTower.ts b/game/towers/SeniorDevTower.ts index 8484447..ee53551 100644 --- a/game/towers/SeniorDevTower.ts +++ b/game/towers/SeniorDevTower.ts @@ -4,71 +4,43 @@ import type { EnemyBase } from '../enemies/EnemyBase' export class SeniorDevTower extends TowerBase { constructor(scene: Phaser.Scene, gridX: number, gridY: number) { - super(scene, gridX, gridY, 120, 5, 30, 1.0) - this.drawSprite() - } - - drawSprite(): void { - if (!this.sprite) return - this.sprite.clear() - // 深蓝色方块 - this.sprite.fillStyle(0x3b82f6, 1) - this.sprite.fillRect(-12, -12, 24, 24) - this.sprite.lineStyle(1, 0x93c5fd, 1) - this.sprite.strokeRect(-12, -12, 24, 24) - this.sprite.setPosition(this.px, this.py) - this.sprite.setDepth(10) - - // 顶部 符号文字 - if (this.scene) { - const existing = this.scene.children.getByName(`dev_label_${this.gridX}_${this.gridY}`) - if (!existing) { - this.scene.add - .text(this.px, this.py, '', { - fontFamily: 'monospace', - fontSize: '10px', - color: '#DBEAFE', - }) - .setOrigin(0.5, 0.5) - .setDepth(11) - .setName(`dev_label_${this.gridX}_${this.gridY}`) - } - } + super(scene, gridX, gridY, 120, 5, 30, 1.0, 'tower-senior') } attack(target: EnemyBase): void { - // 发射绿色代码块子弹 this.fireBullet(target) } private fireBullet(target: EnemyBase): void { const bullet = this.scene.add.graphics() bullet.fillStyle(0x22c55e, 1) - bullet.fillRect(-4, -4, 8, 8) + bullet.fillRoundedRect(-5, -5, 10, 10, 2) + // 添加绿色发光 + bullet.lineStyle(1, 0x86efac, 0.8) + bullet.strokeRoundedRect(-6, -6, 12, 12, 3) bullet.setPosition(this.px, this.py) bullet.setDepth(13) - const startX = this.px - const startY = this.py - const targetX = target.sprite.x - const targetY = target.sprite.y - - const dx = targetX - startX - const dy = targetY - startY + const dx = target.x - this.px + const dy = target.y - this.py const dist = Math.sqrt(dx * dx + dy * dy) - const duration = (dist / 400) * 1000 + const duration = (dist / 500) * 1000 this.scene.tweens.add({ targets: bullet, - x: targetX, - y: targetY, + x: target.x, y: target.y, duration, onComplete: () => { bullet.destroy() if (!target.isDead) { target.takeDamage(this.attackDamage) - // 代码屎山:附加 DOT target.addDOT(10, 3000) + // DOT 命中效果 + const fx = this.scene.add.graphics() + fx.lineStyle(2, 0x22c55e, 0.8) + fx.strokeCircle(target.x, target.y, 14) + fx.setDepth(15) + this.scene.time.delayedCall(300, () => fx.destroy()) } }, }) diff --git a/game/towers/TowerBase.ts b/game/towers/TowerBase.ts index 3e30e31..4e7af0a 100644 --- a/game/towers/TowerBase.ts +++ b/game/towers/TowerBase.ts @@ -1,13 +1,15 @@ import type Phaser from 'phaser' import { GameManager } from '../GameManager' -import { TILE_SIZE, HUD_HEIGHT, COFFEE_COST, STAMINA_MAX, STAMINA_REGEN } from '../constants' +import { HUD_HEIGHT, COFFEE_COST, STAMINA_MAX, STAMINA_REGEN } from '../constants' +import { getCellSize } from '../mapRenderer' import type { EnemyBase } from '../enemies/EnemyBase' export abstract class TowerBase { protected scene: Phaser.Scene public gridX: number public gridY: number - protected sprite!: Phaser.GameObjects.Graphics + protected imageSprite!: Phaser.GameObjects.Image + protected spriteKey: string protected staminaBar!: Phaser.GameObjects.Graphics private frozenOverlay!: Phaser.GameObjects.Graphics @@ -23,9 +25,11 @@ export abstract class TowerBase { protected staminaRegen: number = STAMINA_REGEN protected isActive: boolean = true - // Pixel center position + // Pixel center position (computed from actual cell size) protected px: number protected py: number + protected cellW: number + protected cellH: number constructor( scene: Phaser.Scene, @@ -34,7 +38,8 @@ export abstract class TowerBase { cost: number, attackRange: number, attackDamage: number, - attackSpeed: number + attackSpeed: number, + spriteKey: string ) { this.scene = scene this.gridX = gridX @@ -43,41 +48,44 @@ export abstract class TowerBase { this.attackRange = attackRange this.attackDamage = attackDamage this.attackSpeed = attackSpeed + this.spriteKey = spriteKey - this.px = gridX * TILE_SIZE + TILE_SIZE / 2 - this.py = gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT + const { cellW, cellH } = getCellSize() + this.cellW = cellW + this.cellH = cellH + this.px = gridX * cellW + cellW / 2 + this.py = HUD_HEIGHT + gridY * cellH + cellH / 2 + + // 用 AI 图片作为精灵 + this.imageSprite = scene.add.image(this.px, this.py, spriteKey) + const scale = Math.min(cellW, cellH) / 128 * 0.85 + this.imageSprite.setScale(scale) + this.imageSprite.setDepth(10) - this.sprite = scene.add.graphics() this.staminaBar = scene.add.graphics() this.frozenOverlay = scene.add.graphics() - - this.drawSprite() this.updateStaminaBar() } update(delta: number, enemies: EnemyBase[]): void { - // 禁锢状态:跳过攻击,显示灰色覆盖 if (this.isFrozen) { this.drawFrozenOverlay() return } - // 解除禁锢时清除覆盖层 this.clearFrozenOverlay() this.attackCooldown -= delta - if (this.stamina <= 0) { - this.isActive = false - } + if (this.stamina <= 0) this.isActive = false if (!this.isActive) { - this.stamina = Math.min( - this.maxStamina, - this.stamina + (this.staminaRegen * delta) / 1000 - ) + this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000) if (this.stamina > 20) this.isActive = true this.updateStaminaBar() + // 摸鱼时图片半透明 + this.imageSprite.setAlpha(0.5) return } + this.imageSprite.setAlpha(1) const target = this.findTarget(enemies) if (target && this.attackCooldown <= 0) { @@ -86,29 +94,23 @@ export abstract class TowerBase { this.attackCooldown = 1000 / this.attackSpeed this.updateStaminaBar() } else if (!target) { - this.stamina = Math.min( - this.maxStamina, - this.stamina + (this.staminaRegen * delta) / 1000 - ) + this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000) this.updateStaminaBar() } } protected findTarget(enemies: EnemyBase[]): EnemyBase | null { - const rangePx = this.attackRange * TILE_SIZE + const rangePx = this.attackRange * this.cellW let best: EnemyBase | null = null let bestProgress = -1 - for (const e of enemies) { if (e.isDead) continue - const dx = e.sprite.x - this.px - const dy = e.sprite.y - this.py + const dx = e.x - this.px + const dy = e.y - this.py const dist = Math.sqrt(dx * dx + dy * dy) if (dist <= rangePx) { - // 优先选取路径进度最深的(模拟血量最多/威胁最大) - const progress = e.pathProgress - if (progress > bestProgress) { - bestProgress = progress + if (e.pathProgress > bestProgress) { + bestProgress = e.pathProgress best = e } } @@ -118,15 +120,16 @@ export abstract class TowerBase { protected updateStaminaBar(): void { this.staminaBar.clear() - const bw = 40 - const bh = 4 + const bw = this.cellW * 0.7 + const bh = 5 const bx = this.px - bw / 2 - const by = this.py + TILE_SIZE / 2 - 8 - - this.staminaBar.fillStyle(0x374151, 1) + const by = this.py + this.cellH / 2 - 10 + this.staminaBar.fillStyle(0x1f2937, 1) this.staminaBar.fillRect(bx, by, bw, bh) - this.staminaBar.fillStyle(0xf59e0b, 1) - this.staminaBar.fillRect(bx, by, bw * (this.stamina / this.maxStamina), bh) + const ratio = this.stamina / this.maxStamina + const color = ratio > 0.5 ? 0xf59e0b : ratio > 0.25 ? 0xfb923c : 0xef4444 + this.staminaBar.fillStyle(color, 1) + this.staminaBar.fillRect(bx, by, bw * ratio, bh) this.staminaBar.setDepth(11) } @@ -135,6 +138,7 @@ export abstract class TowerBase { if (manager.spendHC(COFFEE_COST)) { this.stamina = this.maxStamina this.isActive = true + this.imageSprite.setAlpha(1) this.updateStaminaBar() return true } @@ -145,30 +149,25 @@ export abstract class TowerBase { return { x: this.px, y: this.py } } - /** 绘制禁锢状态灰色覆盖层 */ protected drawFrozenOverlay(): void { this.frozenOverlay.clear() - const half = TILE_SIZE / 2 - this.frozenOverlay.fillStyle(0x6b7280, 0.6) - this.frozenOverlay.fillRect( - this.px - half, - this.py - half, - TILE_SIZE, - TILE_SIZE - ) + const hw = this.cellW / 2 + const hh = this.cellH / 2 + this.frozenOverlay.fillStyle(0x6b7280, 0.55) + this.frozenOverlay.fillRect(this.px - hw, this.py - hh, this.cellW, this.cellH) this.frozenOverlay.setDepth(13) + this.imageSprite.setTint(0x9ca3af) } - /** 清除禁锢覆盖层 */ protected clearFrozenOverlay(): void { this.frozenOverlay.clear() + this.imageSprite.clearTint() } abstract attack(target: EnemyBase): void - abstract drawSprite(): void destroy(): void { - this.sprite?.destroy() + this.imageSprite?.destroy() this.staminaBar?.destroy() this.frozenOverlay?.destroy() } diff --git a/game/towers/TowerManager.ts b/game/towers/TowerManager.ts index 5832010..035770c 100644 --- a/game/towers/TowerManager.ts +++ b/game/towers/TowerManager.ts @@ -106,8 +106,8 @@ export class TowerManager { const hasTarget = enemies.some((e) => { if (e.isDead) return false - const dx = e.sprite.x - tower['px'] - const dy = e.sprite.y - tower['py'] + const dx = e.x - tower['px'] + const dy = e.y - tower['py'] return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * TILE_SIZE })