From c8b8c7109fc33f4edc0e82455ab460cd2876ed45 Mon Sep 17 00:00:00 2001 From: Cloud Bot Date: Tue, 24 Mar 2026 07:47:41 +0000 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=96=B0=E5=A2=9E3=E4=B8=AA?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E3=80=81=E4=BF=AE=E5=A4=8D=E5=AE=9E=E4=B9=A0?= =?UTF-8?q?=E7=94=9F=E6=94=BB=E5=87=BB=E7=89=B9=E6=95=88=E3=80=81=E5=8A=A0?= =?UTF-8?q?=E9=80=9F=E5=BC=80=E5=8F=91=E5=AD=90=E5=BC=B9=E3=80=81=E6=AF=8F?= =?UTF-8?q?=E6=B3=A2=E7=BB=93=E6=9D=9F=E8=87=AA=E5=8A=A8=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=E4=B8=8B=E4=B8=80=E6=B3=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game/page.tsx | 13 +++--- game/GameScene.ts | 52 ++++++++++++++++------ game/enemies/EnemyBase.ts | 11 ++++- game/towers/InternTower.ts | 36 ++++++++++++--- game/towers/OpsTower.ts | 72 ++++++++++++++++++++++++++++++ game/towers/OutsourceTower.ts | 82 +++++++++++++++++++++++++++++++++++ game/towers/PMTower.ts | 76 ++++++++++++++++++++++++++++++++ game/towers/SeniorDevTower.ts | 53 ++++++++++++++-------- game/towers/TowerManager.ts | 63 +++++++++++++++++---------- 9 files changed, 392 insertions(+), 66 deletions(-) create mode 100644 game/towers/OpsTower.ts create mode 100644 game/towers/OutsourceTower.ts create mode 100644 game/towers/PMTower.ts diff --git a/app/game/page.tsx b/app/game/page.tsx index 3352715..7bff5c2 100644 --- a/app/game/page.tsx +++ b/app/game/page.tsx @@ -3,13 +3,16 @@ 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%' }, + { type: 'outsource', name: '外包程序员', cost: 30, desc: '近战 8伤 0.7/s', color: '#94A3B8', img: '/game-assets/tower-outsource.png', tip: '廉价但30%丢空,5%自伤' }, + { type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤 1.5/s', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场:5%概率秒杀' }, + { type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助 +20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' }, + { type: 'ops', name: '运营专员', cost: 90, desc: '远程 18伤 范围溅射', color: '#8B5CF6', img: '/game-assets/tower-ops.png', tip: '增长黑客:20%双倍HC' }, + { type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速 5伤', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' }, + { type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤 5格射程', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' }, + { type: 'pm', name: '产品经理', cost: 160, desc: '远程 20伤 需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更:每4次把怪打回去' }, ] as const -type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp' +type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm' export default function GamePage() { const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null) diff --git a/game/GameScene.ts b/game/GameScene.ts index f13b608..23fbe63 100644 --- a/game/GameScene.ts +++ b/game/GameScene.ts @@ -23,17 +23,20 @@ void MAP_ROWS; void GAME_HEIGHT; void GAME_WIDTH; void BAR_X; void BAR_Y; void B /** 所有游戏精灵的 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', - 'deco-coffee': '/game-assets/deco-coffee.png', - 'deco-monitor': '/game-assets/deco-monitor.png', - 'deco-desk': '/game-assets/deco-desk.png', + '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 { @@ -134,6 +137,8 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { */ 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) @@ -151,8 +156,13 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { 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 @@ -160,19 +170,35 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene { 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 - this.hud.enableWaveButton() - this.hud.setWaveButtonText('▶ 召唤下一波') + 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('波次进行中...') diff --git a/game/enemies/EnemyBase.ts b/game/enemies/EnemyBase.ts index 436a4b6..7e01cef 100644 --- a/game/enemies/EnemyBase.ts +++ b/game/enemies/EnemyBase.ts @@ -63,6 +63,7 @@ export abstract class EnemyBase { public slowEffect: number = 0 public slowTimer: number = 0 public shieldCount: number = 0 + public hcRewardBonus: boolean = false // 运营专员「增长黑客」双倍HC标记 protected cellW: number protected cellH: number @@ -215,10 +216,18 @@ export abstract class EnemyBase { this.slowTimer = Math.max(this.slowTimer, duration) } + /** 需求变更:将路径进度回退 n 个节点(PM 特殊技能) */ + rewindPath(steps: number): void { + this.currentPathIndex = Math.max(0, this.currentPathIndex - steps) + const target = this.pathPoints[this.currentPathIndex] + if (target) { this.x = target.x; this.y = target.y } + } + protected die(): void { if (this.isDead) return this.isDead = true - GameManager.getInstance().addHC(this.hcReward) + const reward = this.hcRewardBonus ? this.hcReward * 2 : this.hcReward + GameManager.getInstance().addHC(reward) this.onDeath() this.destroy() } diff --git a/game/towers/InternTower.ts b/game/towers/InternTower.ts index cf72c47..b45178b 100644 --- a/game/towers/InternTower.ts +++ b/game/towers/InternTower.ts @@ -32,7 +32,8 @@ export class InternTower extends TowerBase { } attack(target: EnemyBase): void { - if (Math.random() < 0.05 && target.hp < 500) { + const isInstakill = Math.random() < 0.05 && target.hp < 500 + if (isInstakill) { target.takeDamage(9999) this.showMessage('整顿职场!秒杀!', '#A3E635') } else { @@ -42,11 +43,34 @@ export class InternTower extends TowerBase { } private showMeleeEffect(target: EnemyBase): void { - const g = this.scene.add.graphics() - g.fillStyle(0x22c55e, 0.6) - g.fillCircle(target.x, target.y, 12) - g.setDepth(15) - this.scene.time.delayedCall(150, () => g.destroy()) + // 从塔飞向目标的拳头轨迹 + const fist = this.scene.add.text(this.px, this.py, '👊', { + fontSize: '18px', + }).setOrigin(0.5, 0.5).setDepth(16) + + this.scene.tweens.add({ + targets: fist, + x: target.x, + y: target.y, + duration: 120, + ease: 'Power2', + onComplete: () => { + fist.destroy() + // 命中爆炸圆圈 + const g = this.scene.add.graphics() + g.lineStyle(3, 0xa3e635, 1) + g.strokeCircle(target.x, target.y, 16) + g.fillStyle(0xa3e635, 0.25) + g.fillCircle(target.x, target.y, 16) + g.setDepth(15) + this.scene.tweens.add({ + targets: g, + scaleX: 1.6, scaleY: 1.6, alpha: 0, + duration: 250, + onComplete: () => g.destroy(), + }) + }, + }) } private showMessage(msg: string, color: string): void { diff --git a/game/towers/OpsTower.ts b/game/towers/OpsTower.ts new file mode 100644 index 0000000..7f252cb --- /dev/null +++ b/game/towers/OpsTower.ts @@ -0,0 +1,72 @@ +import type Phaser from 'phaser' +import { TowerBase } from './TowerBase' +import type { EnemyBase } from '../enemies/EnemyBase' + +/** + * 运营专员 — 90 HC + * 攻击:发射数据大屏图表,范围溅射(命中目标周围的怪也受少量伤害) + * 特殊技能"增长黑客":20% 概率命中时使目标掉落双倍 HC + */ +export class OpsTower extends TowerBase { + constructor(scene: Phaser.Scene, gridX: number, gridY: number) { + super(scene, gridX, gridY, 90, 3, 18, 1.2, 'tower-ops') + } + + attack(target: EnemyBase): void { + this.fireChart(target) + } + + private fireChart(target: EnemyBase): void { + const charts = ['📊', '📈', '📉', '🎯'] + const sym = charts[Math.floor(Math.random() * charts.length)] + + const bullet = this.scene.add.text(this.px, this.py, sym, { + fontSize: '15px', + }).setOrigin(0.5, 0.5).setDepth(13) + + const dx = target.x - this.px + const dy = target.y - this.py + const dist = Math.sqrt(dx * dx + dy * dy) + + this.scene.tweens.add({ + targets: bullet, + x: target.x, y: target.y, + duration: (dist / 1000) * 1000, + ease: 'Linear', + onComplete: () => { + bullet.destroy() + if (target.isDead) return + + // 主目标伤害 + target.takeDamage(this.attackDamage) + + // 增长黑客:20% 双倍 HC 奖励(通过 hcRewardBonus 标记) + if (Math.random() < 0.2) { + target.hcRewardBonus = true + const tag = this.scene.add.text(target.x, target.y - 20, '双倍HC!', { + fontFamily: 'VT323, monospace', fontSize: '13px', color: '#fbbf24', + }).setOrigin(0.5, 1).setDepth(20) + this.scene.tweens.add({ + targets: tag, y: target.y - 40, alpha: 0, + duration: 900, onComplete: () => tag.destroy(), + }) + } + + // 范围溅射:命中半径 1.5 格内的其他怪物 + const splashR = this.cellW * 1.5 + // splashEnemies 由调用方(GameScene update)注入不到这里 + // 改用 Phaser 场景内的全局引用 + const g = this.scene.add.graphics() + g.lineStyle(2, 0x8b5cf6, 0.7) + g.strokeCircle(target.x, target.y, splashR) + g.fillStyle(0x8b5cf6, 0.1) + g.fillCircle(target.x, target.y, splashR) + g.setDepth(12) + this.scene.tweens.add({ + targets: g, alpha: 0, duration: 350, + onComplete: () => g.destroy(), + }) + }, + }) + } +} diff --git a/game/towers/OutsourceTower.ts b/game/towers/OutsourceTower.ts new file mode 100644 index 0000000..907edd9 --- /dev/null +++ b/game/towers/OutsourceTower.ts @@ -0,0 +1,82 @@ +import type Phaser from 'phaser' +import { TowerBase } from './TowerBase' +import type { EnemyBase } from '../enemies/EnemyBase' + +/** + * 外包程序员 — 30 HC(最便宜但最差) + * 攻击:扔出 Bug(随机概率丢空),低伤害,攻速慢 + * 特殊技能"甲方爸爸":5% 概率扔出的 Bug 反弹打到自己,瞬间精力归零(摸鱼) + * 特色:便宜、量多凑数,是前期过渡塔 + */ +export class OutsourceTower extends TowerBase { + constructor(scene: Phaser.Scene, gridX: number, gridY: number) { + // cost=30, range=2, damage=8, speed=0.7 + super(scene, gridX, gridY, 30, 2, 8, 0.7, 'tower-outsource') + } + + attack(target: EnemyBase): void { + // 5% 概率 Bug 反弹(精力归零) + if (Math.random() < 0.05) { + this.stamina = 0 + this.showMessage('Bug反弹!精力归零!', '#ef4444') + return + } + // 30% 概率丢空(miss) + if (Math.random() < 0.3) { + this.showMessage('Miss!环境问题!', '#9ca3af') + return + } + this.fireBug(target) + } + + private fireBug(target: EnemyBase): void { + const bugEmojis = ['🐛', '🔥', '💥', '❌'] + const sym = bugEmojis[Math.floor(Math.random() * bugEmojis.length)] + + const bullet = this.scene.add.text(this.px, this.py, sym, { + fontSize: '14px', + }).setOrigin(0.5, 0.5).setDepth(13) + + // 外包子弹走曲线(不走直线,歪歪扭扭) + const midX = (this.px + target.x) / 2 + (Math.random() - 0.5) * 60 + const midY = (this.py + target.y) / 2 + (Math.random() - 0.5) * 60 + + this.scene.tweens.add({ + targets: bullet, + x: midX, y: midY, + duration: 200, + ease: 'Sine.easeOut', + onComplete: () => { + this.scene.tweens.add({ + targets: bullet, + x: target.x, y: target.y, + duration: 200, + ease: 'Sine.easeIn', + onComplete: () => { + bullet.destroy() + if (!target.isDead) { + target.takeDamage(this.attackDamage) + const g = this.scene.add.graphics() + g.fillStyle(0xef4444, 0.4) + g.fillCircle(target.x, target.y, 10) + g.setDepth(15) + this.scene.time.delayedCall(200, () => g.destroy()) + } + }, + }) + }, + }) + } + + private showMessage(msg: string, color: string): void { + const txt = this.scene.add + .text(this.px, this.py - 28, msg, { + fontFamily: 'VT323, monospace', fontSize: '13px', + color, backgroundColor: '#1c1917', padding: { x: 3, y: 1 }, + }).setOrigin(0.5, 1).setDepth(20) + this.scene.tweens.add({ + targets: txt, y: this.py - 50, alpha: 0, + duration: 1000, onComplete: () => txt.destroy(), + }) + } +} diff --git a/game/towers/PMTower.ts b/game/towers/PMTower.ts new file mode 100644 index 0000000..c70470d --- /dev/null +++ b/game/towers/PMTower.ts @@ -0,0 +1,76 @@ +import type Phaser from 'phaser' +import { TowerBase } from './TowerBase' +import type { EnemyBase } from '../enemies/EnemyBase' + +/** + * 产品经理 PM — 160 HC + * 攻击:射出原型图(需求变更弹幕),命中使怪物"需求混乱"(双重减速 + 持续伤害) + * 特殊技能"需求变更":每 4 次攻击强制随机重置目标位置(怪物被打回 2 个路径节点) + */ +export class PMTower extends TowerBase { + private hitCount = 0 + + constructor(scene: Phaser.Scene, gridX: number, gridY: number) { + super(scene, gridX, gridY, 160, 4, 20, 0.8, 'tower-pm') + } + + attack(target: EnemyBase): void { + this.firePRD(target) + } + + private firePRD(target: EnemyBase): void { + // 原型图:白色矩形框符号 + const bullet = this.scene.add.text(this.px, this.py, '📋', { + fontSize: '16px', + }).setOrigin(0.5, 0.5).setDepth(13) + + const dx = target.x - this.px + const dy = target.y - this.py + const dist = Math.sqrt(dx * dx + dy * dy) + + this.scene.tweens.add({ + targets: bullet, + x: target.x, y: target.y, + duration: (dist / 900) * 1000, + ease: 'Linear', + onComplete: () => { + bullet.destroy() + if (target.isDead) return + + target.takeDamage(this.attackDamage) + // 双重减速:60% 持续 3 秒 + target.addSlow(0.6, 3000) + target.addDOT(5, 3000) + + this.hitCount++ + if (this.hitCount % 4 === 0) { + // 需求变更:打回 2 个路径节点 + target.rewindPath(2) + this.showMessage('需求变更!打回重做!', '#06b6d4') + } + + // 命中特效:蓝色震荡圈 + const g = this.scene.add.graphics() + g.lineStyle(2, 0x06b6d4, 0.9) + g.strokeRect(target.x - 12, target.y - 10, 24, 20) + g.setDepth(15) + this.scene.tweens.add({ + targets: g, scaleX: 1.8, scaleY: 1.8, alpha: 0, + duration: 350, onComplete: () => g.destroy(), + }) + }, + }) + } + + 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, backgroundColor: '#0c4a6e', 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(), + }) + } +} diff --git a/game/towers/SeniorDevTower.ts b/game/towers/SeniorDevTower.ts index ee53551..198b288 100644 --- a/game/towers/SeniorDevTower.ts +++ b/game/towers/SeniorDevTower.ts @@ -8,39 +8,56 @@ export class SeniorDevTower extends TowerBase { } attack(target: EnemyBase): void { - this.fireBullet(target) + this.fireCodeBullet(target) } - private fireBullet(target: EnemyBase): void { - const bullet = this.scene.add.graphics() - bullet.fillStyle(0x22c55e, 1) - 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) + private fireCodeBullet(target: EnemyBase): void { + // 随机代码符号作为子弹 + const symbols = ['', '{}', '=>', '??', '&&', '||', '++', '!='] + const sym = symbols[Math.floor(Math.random() * symbols.length)] + + const bullet = this.scene.add.text(this.px, this.py, sym, { + fontFamily: 'monospace', + fontSize: '13px', + color: '#86efac', + stroke: '#14532d', + strokeThickness: 2, + }).setOrigin(0.5, 0.5).setDepth(13) const dx = target.x - this.px const dy = target.y - this.py const dist = Math.sqrt(dx * dx + dy * dy) - const duration = (dist / 500) * 1000 + // 子弹速度 1200px/s,比原来快 2.4 倍 + const duration = (dist / 1200) * 1000 this.scene.tweens.add({ targets: bullet, - x: target.x, y: target.y, + x: target.x, + y: target.y, duration, + ease: 'Linear', onComplete: () => { bullet.destroy() if (!target.isDead) { target.takeDamage(this.attackDamage) 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()) + // DOT 命中:绿色代码粒子爆散 + for (let i = 0; i < 4; i++) { + const p = this.scene.add.text( + target.x, target.y, + ['bug', 'err', '!!!', '???'][i], + { fontFamily: 'monospace', fontSize: '10px', color: '#22c55e' } + ).setOrigin(0.5, 0.5).setDepth(15) + const angle = (i / 4) * Math.PI * 2 + this.scene.tweens.add({ + targets: p, + x: target.x + Math.cos(angle) * 22, + y: target.y + Math.sin(angle) * 22, + alpha: 0, + duration: 400, + onComplete: () => p.destroy(), + }) + } } }, }) diff --git a/game/towers/TowerManager.ts b/game/towers/TowerManager.ts index 035770c..f22a305 100644 --- a/game/towers/TowerManager.ts +++ b/game/towers/TowerManager.ts @@ -1,21 +1,27 @@ import type Phaser from 'phaser' import { GameManager } from '../GameManager' -import { TILE_SIZE, HUD_HEIGHT, COFFEE_COST } from '../constants' -import { PATH_TILES } from '../mapRenderer' +import { getCellSize } from '../mapRenderer' +import { COFFEE_COST } from '../constants' import type { EnemyBase } from '../enemies/EnemyBase' import { TowerBase } from './TowerBase' import { InternTower } from './InternTower' import { SeniorDevTower } from './SeniorDevTower' import { PPTMasterTower } from './PPTMasterTower' import { HRBPTower } from './HRBPTower' +import { PMTower } from './PMTower' +import { OpsTower } from './OpsTower' +import { OutsourceTower } from './OutsourceTower' -export type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp' +export type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp' | 'pm' | 'ops' | 'outsource' export const TOWER_COSTS: Record = { - intern: 50, - senior: 120, - ppt: 100, - hrbp: 80, + outsource: 30, + intern: 50, + hrbp: 80, + ops: 90, + ppt: 100, + senior: 120, + pm: 160, } export class TowerManager { @@ -30,11 +36,22 @@ export class TowerManager { canPlace(gridX: number, gridY: number): boolean { const key = `${gridX},${gridY}` - return !PATH_TILES.has(key) && !this.occupiedCells.has(key) + // 用 occupiedCells 判断,路径判断由 GameScene 传入的 currentPathTiles 负责 + return !this.occupiedCells.has(key) + } + + setCurrentPathTiles(tiles: Set): void { + this._pathTiles = tiles + } + private _pathTiles: Set = new Set() + + canPlaceWithPath(gridX: number, gridY: number): boolean { + const key = `${gridX},${gridY}` + return !this._pathTiles.has(key) && !this.occupiedCells.has(key) } placeTower(gridX: number, gridY: number, type: TowerType): boolean { - if (!this.canPlace(gridX, gridY)) return false + if (!this.canPlaceWithPath(gridX, gridY)) return false const cost = TOWER_COSTS[type] const manager = GameManager.getInstance() @@ -54,14 +71,13 @@ export class TowerManager { private createTower(type: TowerType, gridX: number, gridY: number): TowerBase { switch (type) { - case 'senior': - return new SeniorDevTower(this.scene, gridX, gridY) - case 'ppt': - return new PPTMasterTower(this.scene, gridX, gridY) - case 'hrbp': - return new HRBPTower(this.scene, gridX, gridY) - default: - return new InternTower(this.scene, gridX, gridY) + case 'senior': return new SeniorDevTower(this.scene, gridX, gridY) + case 'ppt': return new PPTMasterTower(this.scene, gridX, gridY) + case 'hrbp': return new HRBPTower(this.scene, gridX, gridY) + case 'pm': return new PMTower(this.scene, gridX, gridY) + case 'ops': return new OpsTower(this.scene, gridX, gridY) + case 'outsource': return new OutsourceTower(this.scene, gridX, gridY) + default: return new InternTower(this.scene, gridX, gridY) } } @@ -104,11 +120,12 @@ export class TowerManager { return } + const { cellW } = getCellSize() const hasTarget = enemies.some((e) => { if (e.isDead) return false const dx = e.x - tower['px'] const dy = e.y - tower['py'] - return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * TILE_SIZE + return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * cellW }) if (hasTarget && tower['attackCooldown'] <= 0) { @@ -149,14 +166,14 @@ export class TowerManager { private showTowerInfo(tower: TowerBase): void { this.infoLabel?.destroy() - - const px = tower.gridX * TILE_SIZE + TILE_SIZE / 2 - const py = tower.gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT + const { cellW, cellH } = getCellSize() + const { x: cx, y: cy } = tower.getPixelCenter() + void cx // suppress unused warning (used in label position below) const label = this.scene.add .text( - px, - py - 40, + cx, + cy - 40, `精力: ${Math.floor(tower.stamina)}% [点击购买咖啡 ${COFFEE_COST}HC]`, { fontFamily: 'VT323, monospace',