feat(ui): 实现底部塔选择面板(DOM覆盖层)和HUD辅助工具
This commit is contained in:
176
game/ui/HUD.ts
Normal file
176
game/ui/HUD.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { GAME_WIDTH, HUD_HEIGHT } from '../constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏 HUD 辅助工具
|
||||||
|
* 负责管理"召唤下一波"按钮和波次提示横幅
|
||||||
|
*/
|
||||||
|
export class HUD {
|
||||||
|
private scene: Phaser.Scene
|
||||||
|
private waveBtn: Phaser.GameObjects.Text | null = null
|
||||||
|
private waveBannerTimeout: (() => void) | null = null
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene) {
|
||||||
|
this.scene = scene
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建"召唤下一波"按钮
|
||||||
|
* @param onClick 点击回调
|
||||||
|
*/
|
||||||
|
createWaveButton(onClick: () => void): void {
|
||||||
|
if (this.waveBtn) this.waveBtn.destroy()
|
||||||
|
|
||||||
|
this.waveBtn = this.scene.add
|
||||||
|
.text(GAME_WIDTH - 160, HUD_HEIGHT + 20, '▶ 召唤下一波', {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '9px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
backgroundColor: '#1e3a5f',
|
||||||
|
padding: { x: 10, y: 6 },
|
||||||
|
})
|
||||||
|
.setOrigin(0, 0)
|
||||||
|
.setDepth(20)
|
||||||
|
.setInteractive({ useHandCursor: true })
|
||||||
|
|
||||||
|
this.waveBtn.on('pointerover', () => {
|
||||||
|
if (this.waveBtn) {
|
||||||
|
this.waveBtn.setStyle({ backgroundColor: '#2d5a8e' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.waveBtn.on('pointerout', () => {
|
||||||
|
if (this.waveBtn) {
|
||||||
|
this.waveBtn.setStyle({ backgroundColor: '#1e3a5f' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.waveBtn.on('pointerdown', () => {
|
||||||
|
onClick()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新按钮文字(如禁用状态) */
|
||||||
|
setWaveButtonText(text: string): void {
|
||||||
|
this.waveBtn?.setText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
disableWaveButton(): void {
|
||||||
|
if (!this.waveBtn) return
|
||||||
|
this.waveBtn.setStyle({ color: '#4B5563', backgroundColor: '#0F172A' })
|
||||||
|
this.waveBtn.removeAllListeners('pointerdown')
|
||||||
|
}
|
||||||
|
|
||||||
|
enableWaveButton(): void {
|
||||||
|
if (!this.waveBtn) return
|
||||||
|
this.waveBtn.setStyle({ color: '#A78BFA', backgroundColor: '#1e3a5f' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示波次开始横幅
|
||||||
|
* @param waveNumber 当前波次(1-based)
|
||||||
|
*/
|
||||||
|
showWaveBanner(waveNumber: number, totalWaves: number): void {
|
||||||
|
const isBoss = waveNumber === totalWaves
|
||||||
|
const label = isBoss
|
||||||
|
? `!! 第${waveNumber}波:空降VP来袭 !!`
|
||||||
|
: `第 ${waveNumber} 波来袭!`
|
||||||
|
const color = isBoss ? '#FBBF24' : '#F43F5E'
|
||||||
|
const bg = isBoss ? '#7C2D12' : '#0A1628'
|
||||||
|
|
||||||
|
const banner = this.scene.add
|
||||||
|
.text(GAME_WIDTH / 2, HUD_HEIGHT + 60, label, {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: isBoss ? '16px' : '14px',
|
||||||
|
color,
|
||||||
|
backgroundColor: bg,
|
||||||
|
padding: { x: 20, y: 10 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(40)
|
||||||
|
.setAlpha(0)
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: banner,
|
||||||
|
alpha: 1,
|
||||||
|
duration: 300,
|
||||||
|
yoyo: true,
|
||||||
|
hold: 1800,
|
||||||
|
onComplete: () => banner.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示周报触发提示 */
|
||||||
|
showWeeklyReportAlert(): void {
|
||||||
|
const banner = this.scene.add
|
||||||
|
.text(GAME_WIDTH / 2, HUD_HEIGHT + 120, '📋 季度周报截止!效率翻倍!', {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#FCD34D',
|
||||||
|
backgroundColor: '#78350F',
|
||||||
|
padding: { x: 16, y: 8 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(40)
|
||||||
|
.setAlpha(0)
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: banner,
|
||||||
|
alpha: 1,
|
||||||
|
duration: 400,
|
||||||
|
yoyo: true,
|
||||||
|
hold: 2500,
|
||||||
|
onComplete: () => banner.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示胜利画面 */
|
||||||
|
showVictory(): void {
|
||||||
|
const overlay = this.scene.add.graphics()
|
||||||
|
overlay.fillStyle(0x000000, 0.6)
|
||||||
|
overlay.fillRect(0, 0, GAME_WIDTH, 720)
|
||||||
|
overlay.setDepth(50)
|
||||||
|
|
||||||
|
this.scene.add
|
||||||
|
.text(GAME_WIDTH / 2, 300, '🎉 大厂保卫成功!', {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '22px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
backgroundColor: '#0A1628',
|
||||||
|
padding: { x: 24, y: 12 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(55)
|
||||||
|
|
||||||
|
this.scene.add
|
||||||
|
.text(GAME_WIDTH / 2, 380, 'KPI 绩效已发放!', {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(55)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示失败画面 */
|
||||||
|
showGameOver(): void {
|
||||||
|
const overlay = this.scene.add.graphics()
|
||||||
|
overlay.fillStyle(0x000000, 0.7)
|
||||||
|
overlay.fillRect(0, 0, GAME_WIDTH, 720)
|
||||||
|
overlay.setDepth(50)
|
||||||
|
|
||||||
|
this.scene.add
|
||||||
|
.text(GAME_WIDTH / 2, 300, 'KPI 归零!被裁了!', {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#EF4444',
|
||||||
|
backgroundColor: '#0A1628',
|
||||||
|
padding: { x: 24, y: 12 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(55)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.waveBtn?.destroy()
|
||||||
|
if (this.waveBannerTimeout) this.waveBannerTimeout()
|
||||||
|
}
|
||||||
|
}
|
||||||
235
game/ui/TowerPanel.ts
Normal file
235
game/ui/TowerPanel.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { TOWER_COSTS, type TowerType } from '../towers/TowerManager'
|
||||||
|
import { GameManager } from '../GameManager'
|
||||||
|
|
||||||
|
const TOWER_META: {
|
||||||
|
type: TowerType
|
||||||
|
name: string
|
||||||
|
desc: string
|
||||||
|
color: string
|
||||||
|
icon: string
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
type: 'intern',
|
||||||
|
name: '00后实习生',
|
||||||
|
desc: '近战 15伤 1.5/s',
|
||||||
|
color: '#22C55E',
|
||||||
|
icon: '🧑💻',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'senior',
|
||||||
|
name: 'P6资深开发',
|
||||||
|
desc: '远程 30伤 5格',
|
||||||
|
color: '#3B82F6',
|
||||||
|
icon: '</>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ppt',
|
||||||
|
name: 'PPT大师',
|
||||||
|
desc: 'AOE 5伤 减速',
|
||||||
|
color: '#F59E0B',
|
||||||
|
icon: '📊',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hrbp',
|
||||||
|
name: 'HRBP',
|
||||||
|
desc: '辅助 +攻速',
|
||||||
|
color: '#EC4899',
|
||||||
|
icon: '❤',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export class TowerPanel {
|
||||||
|
private container: HTMLElement
|
||||||
|
private cards: Map<TowerType, HTMLElement> = new Map()
|
||||||
|
private selectedType: TowerType | null = null
|
||||||
|
private onSelectCallback?: (type: TowerType | null) => void
|
||||||
|
|
||||||
|
constructor(parentId: string) {
|
||||||
|
this.container = document.createElement('div')
|
||||||
|
this.container.id = 'tower-panel'
|
||||||
|
this.applyContainerStyles()
|
||||||
|
|
||||||
|
this.buildCards()
|
||||||
|
|
||||||
|
const parent = document.getElementById(parentId)
|
||||||
|
if (parent) {
|
||||||
|
parent.style.position = 'relative'
|
||||||
|
parent.appendChild(this.container)
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(this.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 HC 变化更新可用状态
|
||||||
|
GameManager.getInstance().onHCChange.push(() => this.refreshCardStates())
|
||||||
|
this.refreshCardStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyContainerStyles(): void {
|
||||||
|
Object.assign(this.container.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '0',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'rgba(10,22,40,0.92)',
|
||||||
|
borderTop: '2px solid #1e3a5f',
|
||||||
|
zIndex: '100',
|
||||||
|
borderRadius: '8px 8px 0 0',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCards(): void {
|
||||||
|
for (const meta of TOWER_META) {
|
||||||
|
const card = document.createElement('div')
|
||||||
|
card.id = `tower-card-${meta.type}`
|
||||||
|
this.applyCardStyles(card, meta.color)
|
||||||
|
|
||||||
|
const icon = document.createElement('div')
|
||||||
|
icon.textContent = meta.icon
|
||||||
|
icon.style.fontSize = '20px'
|
||||||
|
icon.style.textAlign = 'center'
|
||||||
|
|
||||||
|
const name = document.createElement('div')
|
||||||
|
name.textContent = meta.name
|
||||||
|
name.style.fontSize = '11px'
|
||||||
|
name.style.fontWeight = 'bold'
|
||||||
|
name.style.color = meta.color
|
||||||
|
name.style.fontFamily = 'VT323, monospace'
|
||||||
|
name.style.textAlign = 'center'
|
||||||
|
|
||||||
|
const cost = document.createElement('div')
|
||||||
|
cost.id = `tower-cost-${meta.type}`
|
||||||
|
cost.textContent = `${TOWER_COSTS[meta.type]} HC`
|
||||||
|
cost.style.fontSize = '10px'
|
||||||
|
cost.style.color = '#A78BFA'
|
||||||
|
cost.style.textAlign = 'center'
|
||||||
|
|
||||||
|
const desc = document.createElement('div')
|
||||||
|
desc.textContent = meta.desc
|
||||||
|
desc.style.fontSize = '10px'
|
||||||
|
desc.style.color = '#94A3B8'
|
||||||
|
desc.style.textAlign = 'center'
|
||||||
|
|
||||||
|
card.appendChild(icon)
|
||||||
|
card.appendChild(name)
|
||||||
|
card.appendChild(cost)
|
||||||
|
card.appendChild(desc)
|
||||||
|
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.onCardClick(meta.type)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.container.appendChild(card)
|
||||||
|
this.cards.set(meta.type, card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyCardStyles(card: HTMLElement, accentColor: string): void {
|
||||||
|
Object.assign(card.style, {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '2px',
|
||||||
|
padding: '6px 10px',
|
||||||
|
background: '#0F1B2D',
|
||||||
|
border: `2px solid #1e3a5f`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
minWidth: '80px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
userSelect: 'none',
|
||||||
|
})
|
||||||
|
card.setAttribute('data-accent', accentColor)
|
||||||
|
|
||||||
|
card.addEventListener('mouseenter', () => {
|
||||||
|
if (!card.classList.contains('disabled') && !card.classList.contains('selected')) {
|
||||||
|
card.style.background = '#1e3a5f'
|
||||||
|
card.style.transform = 'translateY(-2px)'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
|
if (!card.classList.contains('selected')) {
|
||||||
|
card.style.background = '#0F1B2D'
|
||||||
|
card.style.transform = 'translateY(0)'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private onCardClick(type: TowerType): void {
|
||||||
|
const card = this.cards.get(type)
|
||||||
|
if (!card || card.classList.contains('disabled')) return
|
||||||
|
|
||||||
|
if (this.selectedType === type) {
|
||||||
|
this.clearSelection()
|
||||||
|
} else {
|
||||||
|
this.setSelected(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSelected(type: TowerType): void {
|
||||||
|
this.clearSelection()
|
||||||
|
this.selectedType = type
|
||||||
|
const card = this.cards.get(type)
|
||||||
|
if (card) {
|
||||||
|
const accent = card.getAttribute('data-accent') ?? '#7C3AED'
|
||||||
|
card.classList.add('selected')
|
||||||
|
card.style.border = `2px solid ${accent}`
|
||||||
|
card.style.background = '#1e3a5f'
|
||||||
|
card.style.boxShadow = `0 0 8px ${accent}88`
|
||||||
|
}
|
||||||
|
this.onSelectCallback?.(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSelection(): void {
|
||||||
|
if (this.selectedType) {
|
||||||
|
const card = this.cards.get(this.selectedType)
|
||||||
|
if (card) {
|
||||||
|
card.classList.remove('selected')
|
||||||
|
card.style.border = '2px solid #1e3a5f'
|
||||||
|
card.style.background = '#0F1B2D'
|
||||||
|
card.style.boxShadow = ''
|
||||||
|
card.style.transform = 'translateY(0)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectedType = null
|
||||||
|
this.onSelectCallback?.(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCardStates(): void {
|
||||||
|
const hc = GameManager.getInstance().hc
|
||||||
|
for (const meta of TOWER_META) {
|
||||||
|
const card = this.cards.get(meta.type)
|
||||||
|
if (!card) continue
|
||||||
|
const affordable = hc >= TOWER_COSTS[meta.type]
|
||||||
|
if (!affordable) {
|
||||||
|
card.classList.add('disabled')
|
||||||
|
card.style.opacity = '0.4'
|
||||||
|
card.style.cursor = 'not-allowed'
|
||||||
|
} else {
|
||||||
|
card.classList.remove('disabled')
|
||||||
|
card.style.opacity = '1'
|
||||||
|
card.style.cursor = 'pointer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(cb: (type: TowerType | null) => void): void {
|
||||||
|
this.onSelectCallback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedType(): TowerType | null {
|
||||||
|
return this.selectedType
|
||||||
|
}
|
||||||
|
|
||||||
|
deselect(): void {
|
||||||
|
this.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.container.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user