fix(game): 修复底部塔面板不可见问题,改用React层实现,并用AI素材替换Graphics像素块

This commit is contained in:
Cloud Bot
2026-03-21 08:32:23 +00:00
parent 66b7776f32
commit 085aa0a407
14 changed files with 405 additions and 469 deletions

View File

@@ -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()
}