feat(ui): 实现底部塔选择面板(DOM覆盖层)和HUD辅助工具

This commit is contained in:
Cloud Bot
2026-03-21 08:04:20 +00:00
parent 45cf1e4642
commit ff3fb0536f
2 changed files with 411 additions and 0 deletions

235
game/ui/TowerPanel.ts Normal file
View 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()
}
}