fix(game): 修复底部塔面板不可见问题,改用React层实现,并用AI素材替换Graphics像素块
This commit is contained in:
@@ -1,19 +1,38 @@
|
|||||||
'use client'
|
'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() {
|
export default function GamePage() {
|
||||||
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
|
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
|
||||||
|
const [hc, setHc] = useState(200)
|
||||||
|
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
|
||||||
|
const [gameReady, setGameReady] = useState(false)
|
||||||
|
// expose setter to game scene via window
|
||||||
|
const selectedTowerRef = useRef<TowerType | null>(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(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
|
|
||||||
const initGame = async () => {
|
const initGame = async () => {
|
||||||
// 动态导入 Phaser,避免 SSR 环境报错
|
|
||||||
const Phaser = (await import('phaser')).default
|
const Phaser = (await import('phaser')).default
|
||||||
const { createGameConfig } = await import('@/game/config')
|
const { createGameConfig } = await import('@/game/config')
|
||||||
const { createGameScene } = await import('@/game/GameScene')
|
const { createGameScene } = await import('@/game/GameScene')
|
||||||
@@ -21,20 +40,31 @@ export default function GamePage() {
|
|||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
|
|
||||||
const GameScene = createGameScene(Phaser)
|
const GameScene = createGameScene(Phaser)
|
||||||
const config = createGameConfig('game-container')
|
const config = createGameConfig('game-canvas-container')
|
||||||
|
|
||||||
// 注入场景
|
|
||||||
config.scene = [GameScene]
|
config.scene = [GameScene]
|
||||||
|
|
||||||
// 手动设置 scale mode(避免 SSR 时引用 Phaser.Scale 常量报错)
|
|
||||||
if (config.scale) {
|
if (config.scale) {
|
||||||
config.scale.mode = Phaser.Scale.FIT
|
config.scale.mode = Phaser.Scale.FIT
|
||||||
config.scale.autoCenter = Phaser.Scale.CENTER_BOTH
|
config.scale.autoCenter = Phaser.Scale.CENTER_BOTH
|
||||||
}
|
}
|
||||||
|
|
||||||
// type 设置
|
|
||||||
config.type = Phaser.AUTO
|
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)
|
gameRef.current = new Phaser.Game(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +74,98 @@ export default function GamePage() {
|
|||||||
mounted = false
|
mounted = false
|
||||||
gameRef.current?.destroy(true)
|
gameRef.current?.destroy(true)
|
||||||
gameRef.current = null
|
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 (
|
return (
|
||||||
<div className="w-full h-screen overflow-hidden" style={{ backgroundColor: '#0F0F23' }}>
|
<div
|
||||||
<div id="game-container" className="w-full h-full" />
|
className="w-full h-screen flex flex-col overflow-hidden"
|
||||||
|
style={{ backgroundColor: '#0A1628' }}
|
||||||
|
>
|
||||||
|
{/* Phaser canvas 区域,flex-1 填满剩余高度 */}
|
||||||
|
<div
|
||||||
|
id="game-canvas-container"
|
||||||
|
className="flex-1 min-h-0 w-full"
|
||||||
|
style={{ backgroundColor: '#0A1628' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 底部塔选择面板 — 纯 React DOM,不被 Canvas 遮挡 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(10,18,40,0.97)',
|
||||||
|
borderTop: '2px solid #1e3a5f',
|
||||||
|
padding: '8px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
zIndex: 50,
|
||||||
|
boxShadow: '0 -4px 20px rgba(124,58,237,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 选塔提示 */}
|
||||||
|
<div style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#7C3AED', marginRight: '8px', whiteSpace: 'nowrap', opacity: gameReady ? 1 : 0.3 }}>
|
||||||
|
{selectedTower ? '点击格子建造' : '选择塔 ▼'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{TOWER_META.map((meta) => {
|
||||||
|
const canAfford = hc >= meta.cost
|
||||||
|
const isSelected = selectedTower === meta.type
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={meta.type}
|
||||||
|
title={meta.tip}
|
||||||
|
onClick={() => canAfford && handleSelectTower(meta.type)}
|
||||||
|
disabled={!canAfford}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '3px',
|
||||||
|
padding: '6px 10px',
|
||||||
|
backgroundColor: isSelected ? '#1e3a5f' : '#0F1B2D',
|
||||||
|
border: `2px solid ${isSelected ? meta.color : '#1e3a5f'}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: canAfford ? 'pointer' : 'not-allowed',
|
||||||
|
minWidth: '90px',
|
||||||
|
opacity: canAfford ? 1 : 0.4,
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
boxShadow: isSelected ? `0 0 12px ${meta.color}66` : 'none',
|
||||||
|
transform: isSelected ? 'translateY(-3px)' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 角色图片 */}
|
||||||
|
<div style={{ width: '48px', height: '48px', position: 'relative', flexShrink: 0 }}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={meta.img}
|
||||||
|
alt={meta.name}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'contain', imageRendering: 'auto' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 名称 */}
|
||||||
|
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: meta.color, textAlign: 'center', lineHeight: 1.1 }}>
|
||||||
|
{meta.name}
|
||||||
|
</span>
|
||||||
|
{/* 费用 */}
|
||||||
|
<span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#A78BFA' }}>
|
||||||
|
{meta.cost} HC
|
||||||
|
</span>
|
||||||
|
{/* 描述 */}
|
||||||
|
<span style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#64748B', textAlign: 'center' }}>
|
||||||
|
{meta.desc}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
10
app/page.tsx
10
app/page.tsx
@@ -15,8 +15,16 @@ export default function GameCover() {
|
|||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="crt-overlay relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden"
|
className="crt-overlay relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden"
|
||||||
style={{ backgroundColor: '#0F0F23' }}
|
style={{
|
||||||
|
backgroundColor: '#0F0F23',
|
||||||
|
backgroundImage: 'url(/game-assets/cover-bg.png)',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 深色叠加层,确保文字可读 */}
|
||||||
|
<div className="absolute inset-0" style={{ backgroundColor: 'rgba(10,10,30,0.72)' }} />
|
||||||
|
|
||||||
{/* CRT 流动扫描线 */}
|
{/* CRT 流动扫描线 */}
|
||||||
<div className="game-cover-scanline" />
|
<div className="game-cover-scanline" />
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from './mapRenderer'
|
} from './mapRenderer'
|
||||||
import { TowerManager, type TowerType } from './towers/TowerManager'
|
import { TowerManager, type TowerType } from './towers/TowerManager'
|
||||||
import { WaveManager } from './enemies/WaveManager'
|
import { WaveManager } from './enemies/WaveManager'
|
||||||
import { TowerPanel } from './ui/TowerPanel'
|
|
||||||
import { HUD } from './ui/HUD'
|
import { HUD } from './ui/HUD'
|
||||||
import { WeeklyReportModal } from './ui/WeeklyReportModal'
|
import { WeeklyReportModal } from './ui/WeeklyReportModal'
|
||||||
|
|
||||||
@@ -20,6 +19,18 @@ import { WeeklyReportModal } from './ui/WeeklyReportModal'
|
|||||||
void MAP_ROWS; void GAME_HEIGHT; void GAME_WIDTH
|
void MAP_ROWS; void GAME_HEIGHT; void GAME_WIDTH
|
||||||
void BAR_X; void BAR_Y; void BAR_W; void BAR_H
|
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',
|
||||||
|
'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 {
|
export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
||||||
class GameScene extends PhaserLib.Scene {
|
class GameScene extends PhaserLib.Scene {
|
||||||
private manager!: GameManager
|
private manager!: GameManager
|
||||||
@@ -30,7 +41,6 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
|||||||
private tileGraphics!: Phaser.GameObjects.Graphics
|
private tileGraphics!: Phaser.GameObjects.Graphics
|
||||||
private towerManager!: TowerManager
|
private towerManager!: TowerManager
|
||||||
private waveManager!: WaveManager
|
private waveManager!: WaveManager
|
||||||
private towerPanel!: TowerPanel
|
|
||||||
private hud!: HUD
|
private hud!: HUD
|
||||||
private weeklyModal!: WeeklyReportModal
|
private weeklyModal!: WeeklyReportModal
|
||||||
private selectedTowerType: TowerType | null = null
|
private selectedTowerType: TowerType | null = null
|
||||||
@@ -39,6 +49,13 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
|||||||
|
|
||||||
constructor() { super({ key: 'GameScene' }) }
|
constructor() { super({ key: 'GameScene' }) }
|
||||||
|
|
||||||
|
preload(): void {
|
||||||
|
// 加载所有 AI 生成的角色图片
|
||||||
|
for (const [key, path] of Object.entries(SPRITE_ASSETS)) {
|
||||||
|
this.load.image(key, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
this.manager = GameManager.getInstance()
|
this.manager = GameManager.getInstance()
|
||||||
this.manager.reset()
|
this.manager.reset()
|
||||||
@@ -70,30 +87,39 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
|||||||
this.hud = new HUD(this)
|
this.hud = new HUD(this)
|
||||||
this.hud.createWaveButton(() => this.onWaveButtonClick())
|
this.hud.createWaveButton(() => this.onWaveButtonClick())
|
||||||
|
|
||||||
this.towerPanel = new TowerPanel('game-container')
|
// 接收 React 层传来的选塔事件
|
||||||
this.towerPanel.onSelect((type) => {
|
if (typeof window !== 'undefined') {
|
||||||
|
;(window as any).__gameSelectTower = (type: TowerType | null) => {
|
||||||
this.selectedTowerType = type
|
this.selectedTowerType = type
|
||||||
if (!type) this.buildModeGraphics.clear()
|
if (!type) this.buildModeGraphics.clear()
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setupInteraction()
|
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) => {
|
this.manager.onKPIChange.push((kpi: number) => {
|
||||||
updateKPIBar(this.kpiBar, kpi)
|
updateKPIBar(this.kpiBar, kpi)
|
||||||
this.kpiText.setText(`${kpi}%`)
|
this.kpiText.setText(`${kpi}%`)
|
||||||
})
|
})
|
||||||
this.manager.onHCChange.push((hc: number) => {
|
|
||||||
this.hcText.setText(`HC: ${hc}`)
|
|
||||||
this.towerPanel.refreshCardStates()
|
|
||||||
})
|
|
||||||
this.manager.onGameOver.push(() => {
|
this.manager.onGameOver.push(() => {
|
||||||
this.hud.showGameOver()
|
this.hud.showGameOver()
|
||||||
this.towerPanel.destroy()
|
|
||||||
})
|
})
|
||||||
this.manager.onVictory.push(() => {
|
this.manager.onVictory.push(() => {
|
||||||
this.hud.showVictory()
|
this.hud.showVictory()
|
||||||
this.towerPanel.destroy()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 通知 React 层游戏已就绪
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
;(window as any).__gameReady?.()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(_time: number, delta: number): void {
|
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 => {
|
this.towerManager.getAllTowers().forEach(tower => {
|
||||||
tower.isFrozen = true
|
tower.isFrozen = true
|
||||||
setTimeout(() => { tower.isFrozen = false }, duration)
|
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 => {
|
this.towerManager.getAllTowers().forEach(tower => {
|
||||||
tower.stamina = tower.maxStamina
|
tower.stamina = tower.maxStamina
|
||||||
tower['isActive'] = true
|
tower['isActive'] = true
|
||||||
@@ -183,6 +209,9 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
|||||||
const y = HUD_HEIGHT + row * cellH
|
const y = HUD_HEIGHT + row * cellH
|
||||||
this.buildModeGraphics.lineStyle(2, 0xa78bfa, 0.9)
|
this.buildModeGraphics.lineStyle(2, 0xa78bfa, 0.9)
|
||||||
this.buildModeGraphics.strokeRect(x + 2, y + 2, cellW - 4, cellH - 4)
|
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 {
|
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)
|
const placed = this.towerManager.placeTower(col, row, this.selectedTowerType)
|
||||||
if (placed) {
|
if (placed) {
|
||||||
this.buildModeGraphics.clear()
|
this.buildModeGraphics.clear()
|
||||||
this.towerPanel.deselect()
|
|
||||||
this.selectedTowerType = null
|
this.selectedTowerType = null
|
||||||
|
// 通知 React 取消选中状态
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
;(window as any).__gameOnTowerDeselect?.()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showTip(col, row, cellW, cellH, 'HC不足或格子已占用', '#EF4444')
|
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)
|
const hasTower = this.towerManager.handleTileClick(col, row)
|
||||||
if (!hasTower && !PATH_TILES.has(`${col},${row}`)) {
|
if (!hasTower && !PATH_TILES.has(`${col},${row}`)) {
|
||||||
this.showTip(col, row, cellW, cellH, '从底部面板选择塔', '#A78BFA')
|
this.showTip(col, row, cellW, cellH, '请先从底部选择塔', '#A78BFA')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,104 +5,74 @@ import { getRandomQuote } from '../data/quotes'
|
|||||||
export class BossVP extends EnemyBase {
|
export class BossVP extends EnemyBase {
|
||||||
private skillTimer: number = 20000
|
private skillTimer: number = 20000
|
||||||
private onDestroyTower?: () => void
|
private onDestroyTower?: () => void
|
||||||
|
private bossLabel!: Phaser.GameObjects.Text
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
scene: Phaser.Scene,
|
scene: Phaser.Scene,
|
||||||
pathPoints: PathPoint[],
|
pathPoints: PathPoint[],
|
||||||
onDestroyTower?: () => void
|
onDestroyTower?: () => void
|
||||||
) {
|
) {
|
||||||
super(scene, pathPoints, 800, 40, 30, 150)
|
super(scene, pathPoints, 800, 40, 30, 150, 'enemy-boss')
|
||||||
this.onDestroyTower = onDestroyTower
|
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)
|
scene.cameras.main.flash(800, 255, 0, 0, false)
|
||||||
this.showBossAlert()
|
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 {
|
private showBossAlert(): void {
|
||||||
const alert = this.scene.add
|
const alert = this.scene.add
|
||||||
.text(640, 360, '⚠ 空降VP来袭!⚠', {
|
.text(this.scene.scale.width / 2, this.scene.scale.height / 2, '⚠ 空降VP来袭 ⚠', {
|
||||||
fontFamily: 'VT323, monospace',
|
fontFamily: 'VT323, monospace', fontSize: '36px',
|
||||||
fontSize: '36px',
|
color: '#FBBF24', backgroundColor: '#7F1D1D', padding: { x: 16, y: 8 },
|
||||||
color: '#FBBF24',
|
}).setOrigin(0.5, 0.5).setDepth(50)
|
||||||
backgroundColor: '#7F1D1D',
|
|
||||||
padding: { x: 16, y: 8 },
|
|
||||||
})
|
|
||||||
.setOrigin(0.5, 0.5)
|
|
||||||
.setDepth(50)
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: alert,
|
targets: alert, alpha: 0, duration: 2500, delay: 500,
|
||||||
alpha: 0,
|
|
||||||
duration: 2500,
|
|
||||||
delay: 500,
|
|
||||||
onComplete: () => alert.destroy(),
|
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 {
|
override update(delta: number): void {
|
||||||
if (this.isDead) return
|
if (this.isDead) return
|
||||||
super.update(delta)
|
super.update(delta)
|
||||||
|
|
||||||
this.skillTimer -= delta
|
this.skillTimer -= delta
|
||||||
if (this.skillTimer <= 0) {
|
if (this.skillTimer <= 0) {
|
||||||
this.skillTimer = 20000
|
this.skillTimer = 20000
|
||||||
this.triggerOrgRestructure()
|
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 {
|
private triggerOrgRestructure(): void {
|
||||||
// 组织架构调整:随机摧毁一个防御塔
|
this.onDestroyTower?.()
|
||||||
if (this.onDestroyTower) {
|
|
||||||
this.onDestroyTower()
|
|
||||||
}
|
|
||||||
const txt = this.scene.add
|
const txt = this.scene.add
|
||||||
.text(this.x, this.y - 40, '组织架构调整!', {
|
.text(this.x, this.y - 40, '组织架构调整!', {
|
||||||
fontFamily: 'VT323, monospace',
|
fontFamily: 'VT323, monospace', fontSize: '18px',
|
||||||
fontSize: '18px',
|
color: '#FBBF24', backgroundColor: '#7C2D12', padding: { x: 6, y: 3 },
|
||||||
color: '#FBBF24',
|
}).setOrigin(0.5, 1).setDepth(25)
|
||||||
backgroundColor: '#7C2D12',
|
|
||||||
padding: { x: 6, y: 3 },
|
|
||||||
})
|
|
||||||
.setOrigin(0.5, 1)
|
|
||||||
.setDepth(25)
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: txt,
|
targets: txt, y: this.y - 70, alpha: 0,
|
||||||
y: this.y - 70,
|
duration: 2000, onComplete: () => txt.destroy(),
|
||||||
alpha: 0,
|
|
||||||
duration: 2000,
|
|
||||||
onComplete: () => txt.destroy(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuote(): string {
|
override destroy(): void {
|
||||||
return getRandomQuote('BossVP')
|
this.bossLabel?.destroy()
|
||||||
|
super.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQuote(): string { return getRandomQuote('BossVP') }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import type Phaser from 'phaser'
|
import type Phaser from 'phaser'
|
||||||
import { GameManager } from '../GameManager'
|
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 {
|
export interface PathPoint { x: number; y: number }
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将格子坐标转换为像素坐标(格子中心) */
|
function gridToPixel(gx: number, gy: number, cellW: number, cellH: number): PathPoint {
|
||||||
function gridToPixel(gx: number, gy: number): PathPoint {
|
|
||||||
return {
|
return {
|
||||||
x: gx * TILE_SIZE + TILE_SIZE / 2,
|
x: gx * cellW + cellW / 2,
|
||||||
y: gy * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT,
|
y: gy * cellH + cellH / 2 + HUD_HEIGHT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将 PATH_WAYPOINTS 扩展为完整转折路径(去重相邻相同点) */
|
|
||||||
export function buildFullPath(): PathPoint[] {
|
export function buildFullPath(): PathPoint[] {
|
||||||
|
const { cellW, cellH } = getCellSize()
|
||||||
const points: PathPoint[] = []
|
const points: PathPoint[] = []
|
||||||
for (let i = 0; i < PATH_WAYPOINTS.length - 1; i++) {
|
for (let i = 0; i < PATH_WAYPOINTS.length - 1; i++) {
|
||||||
const from = gridToPixel(PATH_WAYPOINTS[i].x, PATH_WAYPOINTS[i].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)
|
const to = gridToPixel(PATH_WAYPOINTS[i + 1].x, PATH_WAYPOINTS[i + 1].y, cellW, cellH)
|
||||||
points.push(from)
|
points.push(from)
|
||||||
points.push(to)
|
points.push(to)
|
||||||
}
|
}
|
||||||
@@ -29,15 +26,14 @@ export function buildFullPath(): PathPoint[] {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DotEffect {
|
export interface DotEffect { damage: number; duration: number; timer: number }
|
||||||
damage: number
|
|
||||||
duration: number
|
|
||||||
timer: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class EnemyBase {
|
export abstract class EnemyBase {
|
||||||
protected scene: Phaser.Scene
|
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 healthBar!: Phaser.GameObjects.Graphics
|
||||||
protected quoteText!: Phaser.GameObjects.Text
|
protected quoteText!: Phaser.GameObjects.Text
|
||||||
|
|
||||||
@@ -46,30 +42,32 @@ export abstract class EnemyBase {
|
|||||||
public speed: number
|
public speed: number
|
||||||
public readonly kpiDamage: number
|
public readonly kpiDamage: number
|
||||||
public readonly hcReward: number
|
public readonly hcReward: number
|
||||||
|
protected spriteKey: string
|
||||||
|
|
||||||
protected pathPoints: PathPoint[]
|
protected pathPoints: PathPoint[]
|
||||||
protected currentPathIndex: number = 0
|
protected currentPathIndex: number = 0
|
||||||
protected x: number = 0
|
|
||||||
protected y: number = 0
|
|
||||||
|
|
||||||
public isDead: boolean = false
|
public isDead: boolean = false
|
||||||
public isActive: boolean = true
|
public isActive: boolean = true
|
||||||
|
|
||||||
get pathProgress(): number {
|
get pathProgress(): number { return this.currentPathIndex }
|
||||||
return this.currentPathIndex
|
|
||||||
}
|
|
||||||
public dotEffects: DotEffect[] = []
|
public dotEffects: DotEffect[] = []
|
||||||
public slowEffect: number = 0
|
public slowEffect: number = 0
|
||||||
public slowTimer: number = 0
|
public slowTimer: number = 0
|
||||||
public shieldCount: number = 0
|
public shieldCount: number = 0
|
||||||
|
|
||||||
|
protected cellW: number
|
||||||
|
protected cellH: number
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
scene: Phaser.Scene,
|
scene: Phaser.Scene,
|
||||||
pathPoints: PathPoint[],
|
pathPoints: PathPoint[],
|
||||||
maxHp: number,
|
maxHp: number,
|
||||||
speed: number,
|
speed: number,
|
||||||
kpiDamage: number,
|
kpiDamage: number,
|
||||||
hcReward: number
|
hcReward: number,
|
||||||
|
spriteKey: string
|
||||||
) {
|
) {
|
||||||
this.scene = scene
|
this.scene = scene
|
||||||
this.pathPoints = pathPoints
|
this.pathPoints = pathPoints
|
||||||
@@ -78,28 +76,40 @@ export abstract class EnemyBase {
|
|||||||
this.speed = speed
|
this.speed = speed
|
||||||
this.kpiDamage = kpiDamage
|
this.kpiDamage = kpiDamage
|
||||||
this.hcReward = hcReward
|
this.hcReward = hcReward
|
||||||
|
this.spriteKey = spriteKey
|
||||||
|
|
||||||
|
const { cellW, cellH } = getCellSize()
|
||||||
|
this.cellW = cellW
|
||||||
|
this.cellH = cellH
|
||||||
|
|
||||||
if (pathPoints.length > 0) {
|
if (pathPoints.length > 0) {
|
||||||
this.x = pathPoints[0].x
|
this.x = pathPoints[0].x
|
||||||
this.y = pathPoints[0].y
|
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.healthBar = scene.add.graphics()
|
||||||
|
|
||||||
this.quoteText = scene.add
|
this.quoteText = scene.add
|
||||||
.text(this.x, this.y - 30, this.getQuote(), {
|
.text(this.x, this.y - 30, this.getQuote(), {
|
||||||
fontFamily: 'VT323, monospace',
|
fontFamily: 'VT323, monospace',
|
||||||
fontSize: '12px',
|
fontSize: '13px',
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
padding: { x: 3, y: 2 },
|
padding: { x: 4, y: 2 },
|
||||||
})
|
})
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setDepth(15)
|
.setDepth(15)
|
||||||
.setAlpha(0)
|
.setAlpha(0)
|
||||||
|
|
||||||
this.drawSprite()
|
|
||||||
this.drawHealthBar()
|
this.drawHealthBar()
|
||||||
|
// 出生后 0.5s 显示语录
|
||||||
|
scene.time.delayedCall(500, () => { if (!this.isDead) this.showQuote() })
|
||||||
}
|
}
|
||||||
|
|
||||||
update(delta: number): void {
|
update(delta: number): void {
|
||||||
@@ -110,11 +120,10 @@ export abstract class EnemyBase {
|
|||||||
if (this.slowTimer <= 0) this.slowEffect = 0
|
if (this.slowTimer <= 0) this.slowEffect = 0
|
||||||
}
|
}
|
||||||
this.moveAlongPath(delta)
|
this.moveAlongPath(delta)
|
||||||
|
this.imageSprite.setPosition(this.x, this.y)
|
||||||
this.drawHealthBar()
|
this.drawHealthBar()
|
||||||
this.sprite.setPosition(this.x, this.y)
|
if (this.quoteText?.alpha > 0) {
|
||||||
// 语录文字跟随怪物移动
|
this.quoteText.setPosition(this.x, this.y - (this.cellH * 0.45))
|
||||||
if (this.quoteText && this.quoteText.alpha > 0) {
|
|
||||||
this.quoteText.setPosition(this.x, this.y - 30)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,51 +131,46 @@ export abstract class EnemyBase {
|
|||||||
for (let i = this.dotEffects.length - 1; i >= 0; i--) {
|
for (let i = this.dotEffects.length - 1; i >= 0; i--) {
|
||||||
const dot = this.dotEffects[i]
|
const dot = this.dotEffects[i]
|
||||||
dot.timer -= delta
|
dot.timer -= delta
|
||||||
const tickDamage = (dot.damage / 1000) * delta
|
this.hp -= (dot.damage / 1000) * delta
|
||||||
this.hp -= tickDamage
|
if (this.hp <= 0) { this.die(); return }
|
||||||
if (this.hp <= 0) {
|
if (dot.timer <= 0) this.dotEffects.splice(i, 1)
|
||||||
this.die()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dot.timer <= 0) {
|
|
||||||
this.dotEffects.splice(i, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected moveAlongPath(delta: number): void {
|
protected moveAlongPath(delta: number): void {
|
||||||
if (this.currentPathIndex >= this.pathPoints.length - 1) {
|
if (this.currentPathIndex >= this.pathPoints.length - 1) {
|
||||||
this.reachEnd()
|
this.reachEnd(); return
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const target = this.pathPoints[this.currentPathIndex + 1]
|
const target = this.pathPoints[this.currentPathIndex + 1]
|
||||||
const currentSpeed = this.speed * (1 - this.slowEffect)
|
const currentSpeed = this.speed * (1 - this.slowEffect)
|
||||||
const distance = (currentSpeed * delta) / 1000
|
const distance = (currentSpeed * delta) / 1000
|
||||||
|
|
||||||
const dx = target.x - this.x
|
const dx = target.x - this.x
|
||||||
const dy = target.y - this.y
|
const dy = target.y - this.y
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
if (dist <= distance) {
|
if (dist <= distance) {
|
||||||
this.x = target.x
|
this.x = target.x; this.y = target.y
|
||||||
this.y = target.y
|
|
||||||
this.currentPathIndex++
|
this.currentPathIndex++
|
||||||
} else {
|
} else {
|
||||||
this.x += (dx / dist) * distance
|
this.x += (dx / dist) * distance
|
||||||
this.y += (dy / dist) * distance
|
this.y += (dy / dist) * distance
|
||||||
}
|
}
|
||||||
|
// 减速时图片变色
|
||||||
|
if (this.slowEffect > 0) {
|
||||||
|
this.imageSprite.setTint(0x93c5fd)
|
||||||
|
} else {
|
||||||
|
this.imageSprite.clearTint()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected drawHealthBar(): void {
|
protected drawHealthBar(): void {
|
||||||
this.healthBar.clear()
|
this.healthBar.clear()
|
||||||
const bw = 30
|
const bw = this.cellW * 0.5
|
||||||
const bh = 4
|
const bh = 5
|
||||||
const bx = this.x - bw / 2
|
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 ratio = Math.max(0, this.hp / this.maxHp)
|
||||||
const color = ratio > 0.5 ? 0x22c55e : ratio > 0.25 ? 0xf59e0b : 0xef4444
|
const color = ratio > 0.5 ? 0x22c55e : ratio > 0.25 ? 0xf59e0b : 0xef4444
|
||||||
|
this.healthBar.fillStyle(0x1f2937, 1)
|
||||||
this.healthBar.fillStyle(0x374151, 1)
|
|
||||||
this.healthBar.fillRect(bx, by, bw, bh)
|
this.healthBar.fillRect(bx, by, bw, bh)
|
||||||
this.healthBar.fillStyle(color, 1)
|
this.healthBar.fillStyle(color, 1)
|
||||||
this.healthBar.fillRect(bx, by, bw * ratio, bh)
|
this.healthBar.fillRect(bx, by, bw * ratio, bh)
|
||||||
@@ -182,25 +186,16 @@ export abstract class EnemyBase {
|
|||||||
}
|
}
|
||||||
this.hp -= damage
|
this.hp -= damage
|
||||||
this.drawHealthBar()
|
this.drawHealthBar()
|
||||||
if (this.hp <= 0) {
|
if (this.hp <= 0) this.die()
|
||||||
this.die()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private showShieldBlock(): void {
|
private showShieldBlock(): void {
|
||||||
const txt = this.scene.add
|
const txt = this.scene.add
|
||||||
.text(this.x, this.y - 35, '护盾!', {
|
.text(this.x, this.y - 35, '护盾!', {
|
||||||
fontFamily: 'VT323, monospace',
|
fontFamily: 'VT323, monospace', fontSize: '14px', color: '#93C5FD',
|
||||||
fontSize: '14px',
|
}).setOrigin(0.5, 1).setDepth(20)
|
||||||
color: '#93C5FD',
|
|
||||||
})
|
|
||||||
.setOrigin(0.5, 1)
|
|
||||||
.setDepth(20)
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: txt,
|
targets: txt, y: this.y - 55, alpha: 0, duration: 800,
|
||||||
y: this.y - 55,
|
|
||||||
alpha: 0,
|
|
||||||
duration: 800,
|
|
||||||
onComplete: () => txt.destroy(),
|
onComplete: () => txt.destroy(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -217,43 +212,35 @@ export abstract class EnemyBase {
|
|||||||
protected die(): void {
|
protected die(): void {
|
||||||
if (this.isDead) return
|
if (this.isDead) return
|
||||||
this.isDead = true
|
this.isDead = true
|
||||||
const manager = GameManager.getInstance()
|
GameManager.getInstance().addHC(this.hcReward)
|
||||||
manager.addHC(this.hcReward)
|
|
||||||
this.onDeath()
|
this.onDeath()
|
||||||
this.destroy()
|
this.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reachEnd(): void {
|
protected reachEnd(): void {
|
||||||
if (this.isDead) return
|
if (this.isDead) return
|
||||||
const manager = GameManager.getInstance()
|
GameManager.getInstance().reduceKPI(this.kpiDamage)
|
||||||
manager.reduceKPI(this.kpiDamage)
|
|
||||||
this.isDead = true
|
this.isDead = true
|
||||||
this.destroy()
|
this.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDeath(): void {}
|
protected onDeath(): void {}
|
||||||
|
|
||||||
/** 显示头顶语录(出生后随机触发,3秒后淡出) */
|
|
||||||
showQuote(): void {
|
showQuote(): void {
|
||||||
if (this.isDead || !this.quoteText) return
|
if (this.isDead || !this.quoteText) return
|
||||||
this.quoteText.setText(this.getQuote())
|
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)
|
this.quoteText.setAlpha(1)
|
||||||
// 3秒后淡出动画
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: this.quoteText,
|
targets: this.quoteText, alpha: 0,
|
||||||
alpha: 0,
|
duration: 800, delay: 2200, ease: 'Linear',
|
||||||
duration: 800,
|
|
||||||
delay: 2200,
|
|
||||||
ease: 'Linear',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract drawSprite(): void
|
|
||||||
abstract getQuote(): string
|
abstract getQuote(): string
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.sprite?.destroy()
|
this.imageSprite?.destroy()
|
||||||
this.healthBar?.destroy()
|
this.healthBar?.destroy()
|
||||||
this.quoteText?.destroy()
|
this.quoteText?.destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,7 @@ import { getRandomQuote } from '../data/quotes'
|
|||||||
|
|
||||||
export class FreshGraduate extends EnemyBase {
|
export class FreshGraduate extends EnemyBase {
|
||||||
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
||||||
super(scene, pathPoints, 30, 120, 2, 10)
|
super(scene, pathPoints, 30, 120, 2, 10, 'enemy-fresh')
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
getQuote(): string { return getRandomQuote('FreshGraduate') }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,35 +4,12 @@ import { getRandomQuote } from '../data/quotes'
|
|||||||
|
|
||||||
export class OldEmployee extends EnemyBase {
|
export class OldEmployee extends EnemyBase {
|
||||||
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
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.shieldCount = 3
|
||||||
this.drawSprite()
|
// 老员工图片稍大
|
||||||
|
const scale = Math.min(this.cellW, this.cellH) / 128 * 0.9
|
||||||
|
this.imageSprite.setScale(scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSprite(): void {
|
getQuote(): string { return getRandomQuote('OldEmployee') }
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,49 +5,21 @@ import { getRandomQuote } from '../data/quotes'
|
|||||||
|
|
||||||
export class TroubleMaker extends EnemyBase {
|
export class TroubleMaker extends EnemyBase {
|
||||||
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
||||||
super(scene, pathPoints, 80, 80, 5, 20)
|
super(scene, pathPoints, 80, 80, 5, 20, 'enemy-trouble')
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override onDeath(): void {
|
protected override onDeath(): void {
|
||||||
// 劳动仲裁:死亡时扣玩家 20 HC
|
GameManager.getInstance().spendHC(20)
|
||||||
const manager = GameManager.getInstance()
|
|
||||||
manager.spendHC(20)
|
|
||||||
// 显示提示文字
|
|
||||||
const txt = this.scene.add
|
const txt = this.scene.add
|
||||||
.text(this.x, this.y - 20, '劳动仲裁! -20HC', {
|
.text(this.x, this.y - 20, '劳动仲裁! -20HC', {
|
||||||
fontFamily: 'VT323, monospace',
|
fontFamily: 'VT323, monospace', fontSize: '16px',
|
||||||
fontSize: '16px',
|
color: '#FCA5A5', backgroundColor: '#7F1D1D', padding: { x: 4, y: 2 },
|
||||||
color: '#FCA5A5',
|
}).setOrigin(0.5, 1).setDepth(25)
|
||||||
backgroundColor: '#7F1D1D',
|
|
||||||
padding: { x: 4, y: 2 },
|
|
||||||
})
|
|
||||||
.setOrigin(0.5, 1)
|
|
||||||
.setDepth(25)
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: txt,
|
targets: txt, y: this.y - 50, alpha: 0,
|
||||||
y: this.y - 50,
|
duration: 1500, onComplete: () => txt.destroy(),
|
||||||
alpha: 0,
|
|
||||||
duration: 1500,
|
|
||||||
onComplete: () => txt.destroy(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuote(): string {
|
getQuote(): string { return getRandomQuote('TroubleMaker') }
|
||||||
return getRandomQuote('TroubleMaker')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,24 @@
|
|||||||
import type Phaser from 'phaser'
|
import type Phaser from 'phaser'
|
||||||
import { TowerBase } from './TowerBase'
|
import { TowerBase } from './TowerBase'
|
||||||
import type { EnemyBase } from '../enemies/EnemyBase'
|
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 {
|
export class HRBPTower extends TowerBase {
|
||||||
private buffCooldown: number = 0
|
private buffCooldown: number = 0
|
||||||
private readonly BUFF_INTERVAL = 500
|
private readonly BUFF_INTERVAL = 500
|
||||||
private nearbyTowersBuff: Set<TowerBaseType> = new Set()
|
private nearbyTowersBuff: Set<TowerBase> = new Set()
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
super(scene, gridX, gridY, 80, 1, 0, 0)
|
super(scene, gridX, gridY, 80, 1, 0, 0, 'tower-hrbp')
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override update(delta: number, enemies: EnemyBase[]): void {
|
override update(delta: number, enemies: EnemyBase[]): void {
|
||||||
// HRBP 没有攻击逻辑,只做 BUFF
|
|
||||||
void enemies
|
void enemies
|
||||||
|
|
||||||
if (!this.isActive) {
|
if (!this.isActive) {
|
||||||
this.stamina = Math.min(
|
this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000)
|
||||||
this.maxStamina,
|
|
||||||
this.stamina + (this.staminaRegen * delta) / 1000
|
|
||||||
)
|
|
||||||
if (this.stamina > 20) this.isActive = true
|
if (this.stamina > 20) this.isActive = true
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buffCooldown -= delta
|
this.buffCooldown -= delta
|
||||||
if (this.buffCooldown <= 0) {
|
if (this.buffCooldown <= 0) {
|
||||||
this.buffCooldown = this.BUFF_INTERVAL
|
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)
|
this.nearbyTowersBuff = new Set(towers)
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyBuffToNearby(): void {
|
private applyBuffToNearby(): void {
|
||||||
if (this.nearbyTowersBuff.size === 0) return
|
if (this.nearbyTowersBuff.size === 0) return
|
||||||
if (this.stamina < 5) {
|
if (this.stamina < 5) { this.isActive = false; return }
|
||||||
this.isActive = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.stamina -= 5
|
this.stamina -= 5
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
// BUFF 效果通过 attackSpeedMultiplier 外部读取
|
|
||||||
// 这里显示一个粉色光圈效果
|
|
||||||
this.showBuffEffect()
|
this.showBuffEffect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private showBuffEffect(): void {
|
private showBuffEffect(): void {
|
||||||
const g = this.scene.add.graphics()
|
const g = this.scene.add.graphics()
|
||||||
g.lineStyle(2, 0xec4899, 0.6)
|
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)
|
g.setDepth(8)
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: g,
|
targets: g, alpha: 0, duration: 400,
|
||||||
alpha: 0,
|
|
||||||
duration: 400,
|
|
||||||
onComplete: () => g.destroy(),
|
onComplete: () => g.destroy(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getBuffedTowers(): Set<TowerBaseType> {
|
getBuffedTowers(): Set<TowerBase> { return this.nearbyTowersBuff }
|
||||||
return this.nearbyTowersBuff
|
getAttackSpeedBonus(): number { return 0.2 }
|
||||||
}
|
|
||||||
|
|
||||||
getAttackSpeedBonus(): number {
|
|
||||||
return BUFF_ATTACK_SPEED_BONUS
|
|
||||||
}
|
|
||||||
|
|
||||||
// HRBP 无直接攻击
|
|
||||||
attack(_target: EnemyBase): void {}
|
attack(_target: EnemyBase): void {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,37 +10,18 @@ export class InternTower extends TowerBase {
|
|||||||
public onSelfDestroy?: (tower: InternTower) => void
|
public onSelfDestroy?: (tower: InternTower) => void
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
super(scene, gridX, gridY, 50, 1, 15, 1.5)
|
super(scene, gridX, gridY, 50, 1, 15, 1.5, 'tower-intern')
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override update(delta: number, enemies: EnemyBase[]): void {
|
override update(delta: number, enemies: EnemyBase[]): void {
|
||||||
if (this.destroyed) return
|
if (this.destroyed) return
|
||||||
|
|
||||||
super.update(delta, enemies)
|
super.update(delta, enemies)
|
||||||
|
|
||||||
// 被动:每秒1%概率离场
|
|
||||||
this.selfDestroyTimer += delta
|
this.selfDestroyTimer += delta
|
||||||
if (this.selfDestroyTimer >= this.SELF_DESTROY_INTERVAL) {
|
if (this.selfDestroyTimer >= this.SELF_DESTROY_INTERVAL) {
|
||||||
this.selfDestroyTimer -= this.SELF_DESTROY_INTERVAL
|
this.selfDestroyTimer -= this.SELF_DESTROY_INTERVAL
|
||||||
if (Math.random() < 0.01) {
|
if (Math.random() < 0.01) {
|
||||||
// 退还 25 HC
|
|
||||||
GameManager.getInstance().addHC(25)
|
GameManager.getInstance().addHC(25)
|
||||||
this.showMessage('实习生跑路!+25HC')
|
this.showMessage('实习生跑路!+25HC', '#22C55E')
|
||||||
this.destroyed = true
|
this.destroyed = true
|
||||||
this.onSelfDestroy?.(this)
|
this.onSelfDestroy?.(this)
|
||||||
this.destroy()
|
this.destroy()
|
||||||
@@ -50,42 +31,32 @@ export class InternTower extends TowerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attack(target: EnemyBase): void {
|
attack(target: EnemyBase): void {
|
||||||
// 整顿职场:5% 概率秒杀 HP < 500 的怪物
|
|
||||||
if (Math.random() < 0.05 && target.hp < 500) {
|
if (Math.random() < 0.05 && target.hp < 500) {
|
||||||
target.takeDamage(9999)
|
target.takeDamage(9999)
|
||||||
this.showMessage('整顿职场!秒杀!')
|
this.showMessage('整顿职场!秒杀!', '#A3E635')
|
||||||
} else {
|
} else {
|
||||||
target.takeDamage(this.attackDamage)
|
target.takeDamage(this.attackDamage)
|
||||||
}
|
}
|
||||||
// 近战效果(闪光)
|
|
||||||
this.showMeleeEffect(target)
|
this.showMeleeEffect(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
private showMeleeEffect(target: EnemyBase): void {
|
private showMeleeEffect(target: EnemyBase): void {
|
||||||
const g = this.scene.add.graphics()
|
const g = this.scene.add.graphics()
|
||||||
g.fillStyle(0x22c55e, 0.7)
|
g.fillStyle(0x22c55e, 0.6)
|
||||||
g.fillCircle(target.sprite.x, target.sprite.y, 10)
|
g.fillCircle(target.x, target.y, 12)
|
||||||
g.setDepth(15)
|
g.setDepth(15)
|
||||||
this.scene.time.delayedCall(150, () => g.destroy())
|
this.scene.time.delayedCall(150, () => g.destroy())
|
||||||
}
|
}
|
||||||
|
|
||||||
private showMessage(msg: string): void {
|
private showMessage(msg: string, color: string): void {
|
||||||
const txt = this.scene.add
|
const txt = this.scene.add
|
||||||
.text(this.px, this.py - 30, msg, {
|
.text(this.px, this.py - 30, msg, {
|
||||||
fontFamily: 'VT323, monospace',
|
fontFamily: 'VT323, monospace', fontSize: '14px',
|
||||||
fontSize: '14px',
|
color, backgroundColor: '#052e16', padding: { x: 4, y: 2 },
|
||||||
color: '#22C55E',
|
}).setOrigin(0.5, 1).setDepth(20)
|
||||||
backgroundColor: '#14532D',
|
|
||||||
padding: { x: 4, y: 2 },
|
|
||||||
})
|
|
||||||
.setOrigin(0.5, 1)
|
|
||||||
.setDepth(20)
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: txt,
|
targets: txt, y: this.py - 55, alpha: 0,
|
||||||
y: this.py - 55,
|
duration: 1200, onComplete: () => txt.destroy(),
|
||||||
alpha: 0,
|
|
||||||
duration: 1200,
|
|
||||||
onComplete: () => txt.destroy(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,57 +4,37 @@ import type { EnemyBase } from '../enemies/EnemyBase'
|
|||||||
|
|
||||||
export class PPTMasterTower extends TowerBase {
|
export class PPTMasterTower extends TowerBase {
|
||||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
super(scene, gridX, gridY, 100, 3, 5, 1.5)
|
super(scene, gridX, gridY, 100, 3, 5, 1.5, 'tower-ppt')
|
||||||
this.drawSprite()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSprite(): void {
|
attack(_target: EnemyBase): void {
|
||||||
if (!this.sprite) return
|
// AOE 攻击由 TowerManager.updatePPTTower 直接调用 attackAoe
|
||||||
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 攻击:对射程内所有怪物造成伤害 + 减速
|
|
||||||
this.showAoeEffect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 该方法由 TowerManager 调用,传入全体敌人 */
|
|
||||||
attackAoe(enemies: EnemyBase[]): void {
|
attackAoe(enemies: EnemyBase[]): void {
|
||||||
const rangePx = this.attackRange * 80 // TILE_SIZE
|
const rangePx = this.attackRange * this.cellW
|
||||||
this.showAoeEffect()
|
this.showAoeEffect(rangePx)
|
||||||
for (const e of enemies) {
|
for (const e of enemies) {
|
||||||
if (e.isDead) continue
|
if (e.isDead) continue
|
||||||
const dx = e.sprite.x - this.px
|
const dx = e.x - this.px
|
||||||
const dy = e.sprite.y - this.py
|
const dy = e.y - this.py
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
if (Math.sqrt(dx * dx + dy * dy) <= rangePx) {
|
||||||
if (dist <= rangePx) {
|
|
||||||
e.takeDamage(this.attackDamage)
|
e.takeDamage(this.attackDamage)
|
||||||
// 黑话领域:减速40%持续2秒
|
|
||||||
e.addSlow(0.4, 2000)
|
e.addSlow(0.4, 2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private showAoeEffect(): void {
|
private showAoeEffect(rangePx: number): void {
|
||||||
const g = this.scene.add.graphics()
|
const g = this.scene.add.graphics()
|
||||||
g.lineStyle(2, 0xf59e0b, 0.8)
|
g.lineStyle(2, 0xf59e0b, 0.7)
|
||||||
g.strokeCircle(this.px, this.py, this.attackRange * 80)
|
g.strokeCircle(this.px, this.py, rangePx)
|
||||||
g.setDepth(12)
|
g.fillStyle(0xf59e0b, 0.07)
|
||||||
|
g.fillCircle(this.px, this.py, rangePx)
|
||||||
|
g.setDepth(8)
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: g,
|
targets: g, alpha: 0, scaleX: 1.15, scaleY: 1.15,
|
||||||
alpha: 0,
|
duration: 450, onComplete: () => g.destroy(),
|
||||||
scaleX: 1.2,
|
|
||||||
scaleY: 1.2,
|
|
||||||
duration: 400,
|
|
||||||
onComplete: () => g.destroy(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,71 +4,43 @@ import type { EnemyBase } from '../enemies/EnemyBase'
|
|||||||
|
|
||||||
export class SeniorDevTower extends TowerBase {
|
export class SeniorDevTower extends TowerBase {
|
||||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
super(scene, gridX, gridY, 120, 5, 30, 1.0)
|
super(scene, gridX, gridY, 120, 5, 30, 1.0, 'tower-senior')
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attack(target: EnemyBase): void {
|
attack(target: EnemyBase): void {
|
||||||
// 发射绿色代码块子弹
|
|
||||||
this.fireBullet(target)
|
this.fireBullet(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fireBullet(target: EnemyBase): void {
|
private fireBullet(target: EnemyBase): void {
|
||||||
const bullet = this.scene.add.graphics()
|
const bullet = this.scene.add.graphics()
|
||||||
bullet.fillStyle(0x22c55e, 1)
|
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.setPosition(this.px, this.py)
|
||||||
bullet.setDepth(13)
|
bullet.setDepth(13)
|
||||||
|
|
||||||
const startX = this.px
|
const dx = target.x - this.px
|
||||||
const startY = this.py
|
const dy = target.y - this.py
|
||||||
const targetX = target.sprite.x
|
|
||||||
const targetY = target.sprite.y
|
|
||||||
|
|
||||||
const dx = targetX - startX
|
|
||||||
const dy = targetY - startY
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
const duration = (dist / 400) * 1000
|
const duration = (dist / 500) * 1000
|
||||||
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: bullet,
|
targets: bullet,
|
||||||
x: targetX,
|
x: target.x, y: target.y,
|
||||||
y: targetY,
|
|
||||||
duration,
|
duration,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
bullet.destroy()
|
bullet.destroy()
|
||||||
if (!target.isDead) {
|
if (!target.isDead) {
|
||||||
target.takeDamage(this.attackDamage)
|
target.takeDamage(this.attackDamage)
|
||||||
// 代码屎山:附加 DOT
|
|
||||||
target.addDOT(10, 3000)
|
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())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import type Phaser from 'phaser'
|
import type Phaser from 'phaser'
|
||||||
import { GameManager } from '../GameManager'
|
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'
|
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||||
|
|
||||||
export abstract class TowerBase {
|
export abstract class TowerBase {
|
||||||
protected scene: Phaser.Scene
|
protected scene: Phaser.Scene
|
||||||
public gridX: number
|
public gridX: number
|
||||||
public gridY: number
|
public gridY: number
|
||||||
protected sprite!: Phaser.GameObjects.Graphics
|
protected imageSprite!: Phaser.GameObjects.Image
|
||||||
|
protected spriteKey: string
|
||||||
protected staminaBar!: Phaser.GameObjects.Graphics
|
protected staminaBar!: Phaser.GameObjects.Graphics
|
||||||
private frozenOverlay!: Phaser.GameObjects.Graphics
|
private frozenOverlay!: Phaser.GameObjects.Graphics
|
||||||
|
|
||||||
@@ -23,9 +25,11 @@ export abstract class TowerBase {
|
|||||||
protected staminaRegen: number = STAMINA_REGEN
|
protected staminaRegen: number = STAMINA_REGEN
|
||||||
protected isActive: boolean = true
|
protected isActive: boolean = true
|
||||||
|
|
||||||
// Pixel center position
|
// Pixel center position (computed from actual cell size)
|
||||||
protected px: number
|
protected px: number
|
||||||
protected py: number
|
protected py: number
|
||||||
|
protected cellW: number
|
||||||
|
protected cellH: number
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
scene: Phaser.Scene,
|
scene: Phaser.Scene,
|
||||||
@@ -34,7 +38,8 @@ export abstract class TowerBase {
|
|||||||
cost: number,
|
cost: number,
|
||||||
attackRange: number,
|
attackRange: number,
|
||||||
attackDamage: number,
|
attackDamage: number,
|
||||||
attackSpeed: number
|
attackSpeed: number,
|
||||||
|
spriteKey: string
|
||||||
) {
|
) {
|
||||||
this.scene = scene
|
this.scene = scene
|
||||||
this.gridX = gridX
|
this.gridX = gridX
|
||||||
@@ -43,41 +48,44 @@ export abstract class TowerBase {
|
|||||||
this.attackRange = attackRange
|
this.attackRange = attackRange
|
||||||
this.attackDamage = attackDamage
|
this.attackDamage = attackDamage
|
||||||
this.attackSpeed = attackSpeed
|
this.attackSpeed = attackSpeed
|
||||||
|
this.spriteKey = spriteKey
|
||||||
|
|
||||||
this.px = gridX * TILE_SIZE + TILE_SIZE / 2
|
const { cellW, cellH } = getCellSize()
|
||||||
this.py = gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT
|
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.staminaBar = scene.add.graphics()
|
||||||
this.frozenOverlay = scene.add.graphics()
|
this.frozenOverlay = scene.add.graphics()
|
||||||
|
|
||||||
this.drawSprite()
|
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
update(delta: number, enemies: EnemyBase[]): void {
|
update(delta: number, enemies: EnemyBase[]): void {
|
||||||
// 禁锢状态:跳过攻击,显示灰色覆盖
|
|
||||||
if (this.isFrozen) {
|
if (this.isFrozen) {
|
||||||
this.drawFrozenOverlay()
|
this.drawFrozenOverlay()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 解除禁锢时清除覆盖层
|
|
||||||
this.clearFrozenOverlay()
|
this.clearFrozenOverlay()
|
||||||
this.attackCooldown -= delta
|
this.attackCooldown -= delta
|
||||||
|
|
||||||
if (this.stamina <= 0) {
|
if (this.stamina <= 0) this.isActive = false
|
||||||
this.isActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isActive) {
|
if (!this.isActive) {
|
||||||
this.stamina = Math.min(
|
this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000)
|
||||||
this.maxStamina,
|
|
||||||
this.stamina + (this.staminaRegen * delta) / 1000
|
|
||||||
)
|
|
||||||
if (this.stamina > 20) this.isActive = true
|
if (this.stamina > 20) this.isActive = true
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
|
// 摸鱼时图片半透明
|
||||||
|
this.imageSprite.setAlpha(0.5)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.imageSprite.setAlpha(1)
|
||||||
|
|
||||||
const target = this.findTarget(enemies)
|
const target = this.findTarget(enemies)
|
||||||
if (target && this.attackCooldown <= 0) {
|
if (target && this.attackCooldown <= 0) {
|
||||||
@@ -86,29 +94,23 @@ export abstract class TowerBase {
|
|||||||
this.attackCooldown = 1000 / this.attackSpeed
|
this.attackCooldown = 1000 / this.attackSpeed
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
} else if (!target) {
|
} else if (!target) {
|
||||||
this.stamina = Math.min(
|
this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000)
|
||||||
this.maxStamina,
|
|
||||||
this.stamina + (this.staminaRegen * delta) / 1000
|
|
||||||
)
|
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected findTarget(enemies: EnemyBase[]): EnemyBase | null {
|
protected findTarget(enemies: EnemyBase[]): EnemyBase | null {
|
||||||
const rangePx = this.attackRange * TILE_SIZE
|
const rangePx = this.attackRange * this.cellW
|
||||||
let best: EnemyBase | null = null
|
let best: EnemyBase | null = null
|
||||||
let bestProgress = -1
|
let bestProgress = -1
|
||||||
|
|
||||||
for (const e of enemies) {
|
for (const e of enemies) {
|
||||||
if (e.isDead) continue
|
if (e.isDead) continue
|
||||||
const dx = e.sprite.x - this.px
|
const dx = e.x - this.px
|
||||||
const dy = e.sprite.y - this.py
|
const dy = e.y - this.py
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
if (dist <= rangePx) {
|
if (dist <= rangePx) {
|
||||||
// 优先选取路径进度最深的(模拟血量最多/威胁最大)
|
if (e.pathProgress > bestProgress) {
|
||||||
const progress = e.pathProgress
|
bestProgress = e.pathProgress
|
||||||
if (progress > bestProgress) {
|
|
||||||
bestProgress = progress
|
|
||||||
best = e
|
best = e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,15 +120,16 @@ export abstract class TowerBase {
|
|||||||
|
|
||||||
protected updateStaminaBar(): void {
|
protected updateStaminaBar(): void {
|
||||||
this.staminaBar.clear()
|
this.staminaBar.clear()
|
||||||
const bw = 40
|
const bw = this.cellW * 0.7
|
||||||
const bh = 4
|
const bh = 5
|
||||||
const bx = this.px - bw / 2
|
const bx = this.px - bw / 2
|
||||||
const by = this.py + TILE_SIZE / 2 - 8
|
const by = this.py + this.cellH / 2 - 10
|
||||||
|
this.staminaBar.fillStyle(0x1f2937, 1)
|
||||||
this.staminaBar.fillStyle(0x374151, 1)
|
|
||||||
this.staminaBar.fillRect(bx, by, bw, bh)
|
this.staminaBar.fillRect(bx, by, bw, bh)
|
||||||
this.staminaBar.fillStyle(0xf59e0b, 1)
|
const ratio = this.stamina / this.maxStamina
|
||||||
this.staminaBar.fillRect(bx, by, bw * (this.stamina / this.maxStamina), bh)
|
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)
|
this.staminaBar.setDepth(11)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +138,7 @@ export abstract class TowerBase {
|
|||||||
if (manager.spendHC(COFFEE_COST)) {
|
if (manager.spendHC(COFFEE_COST)) {
|
||||||
this.stamina = this.maxStamina
|
this.stamina = this.maxStamina
|
||||||
this.isActive = true
|
this.isActive = true
|
||||||
|
this.imageSprite.setAlpha(1)
|
||||||
this.updateStaminaBar()
|
this.updateStaminaBar()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -145,30 +149,25 @@ export abstract class TowerBase {
|
|||||||
return { x: this.px, y: this.py }
|
return { x: this.px, y: this.py }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绘制禁锢状态灰色覆盖层 */
|
|
||||||
protected drawFrozenOverlay(): void {
|
protected drawFrozenOverlay(): void {
|
||||||
this.frozenOverlay.clear()
|
this.frozenOverlay.clear()
|
||||||
const half = TILE_SIZE / 2
|
const hw = this.cellW / 2
|
||||||
this.frozenOverlay.fillStyle(0x6b7280, 0.6)
|
const hh = this.cellH / 2
|
||||||
this.frozenOverlay.fillRect(
|
this.frozenOverlay.fillStyle(0x6b7280, 0.55)
|
||||||
this.px - half,
|
this.frozenOverlay.fillRect(this.px - hw, this.py - hh, this.cellW, this.cellH)
|
||||||
this.py - half,
|
|
||||||
TILE_SIZE,
|
|
||||||
TILE_SIZE
|
|
||||||
)
|
|
||||||
this.frozenOverlay.setDepth(13)
|
this.frozenOverlay.setDepth(13)
|
||||||
|
this.imageSprite.setTint(0x9ca3af)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清除禁锢覆盖层 */
|
|
||||||
protected clearFrozenOverlay(): void {
|
protected clearFrozenOverlay(): void {
|
||||||
this.frozenOverlay.clear()
|
this.frozenOverlay.clear()
|
||||||
|
this.imageSprite.clearTint()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract attack(target: EnemyBase): void
|
abstract attack(target: EnemyBase): void
|
||||||
abstract drawSprite(): void
|
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.sprite?.destroy()
|
this.imageSprite?.destroy()
|
||||||
this.staminaBar?.destroy()
|
this.staminaBar?.destroy()
|
||||||
this.frozenOverlay?.destroy()
|
this.frozenOverlay?.destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ export class TowerManager {
|
|||||||
|
|
||||||
const hasTarget = enemies.some((e) => {
|
const hasTarget = enemies.some((e) => {
|
||||||
if (e.isDead) return false
|
if (e.isDead) return false
|
||||||
const dx = e.sprite.x - tower['px']
|
const dx = e.x - tower['px']
|
||||||
const dy = e.sprite.y - tower['py']
|
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 * TILE_SIZE
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user