feat(game/ui): 升级胜利/失败结算界面,胜利弹窗显示绩效等级评定,失败弹窗仿钉钉风格

This commit is contained in:
Cloud Bot
2026-03-21 08:15:08 +00:00
parent 4737fd3cb1
commit 26b01422db
2 changed files with 208 additions and 51 deletions

171
game/ui/EndScreenModal.ts Normal file
View File

@@ -0,0 +1,171 @@
/**
* 游戏结束模态弹窗(胜利 / 失败)
* DOM 层实现z-index: 9998覆盖 canvas
*/
export interface VictoryData {
kpi: number
hc: number
grade: string
gradeColor: string
gradeDesc: string
}
function injectEndStyles(): void {
if (document.getElementById('end-screen-styles')) return
const style = document.createElement('style')
style.id = 'end-screen-styles'
style.textContent = `
@keyframes endOverlayIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes endCardIn { from { transform: scale(0.8); opacity: 0 } to { transform: scale(1); opacity: 1 } }
`
document.head.appendChild(style)
}
function buildOverlay(): HTMLElement {
const overlay = document.createElement('div')
overlay.id = 'end-screen-overlay'
overlay.style.cssText = `
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
font-family: 'VT323', monospace;
animation: endOverlayIn 0.4s ease;
`
return overlay
}
function buildVictoryCard(data: VictoryData): HTMLElement {
const card = document.createElement('div')
card.style.cssText = `
background: #0f0c29;
border: 2px solid #d97706;
border-radius: 14px;
padding: 36px 48px;
max-width: 480px;
width: 90%;
text-align: center;
box-shadow: 0 0 60px rgba(217,119,6,0.4);
animation: endCardIn 0.4s ease;
`
card.innerHTML = `
<div style="font-size:32px;color:#fbbf24;letter-spacing:4px;margin-bottom:24px;">绩效评级公示</div>
<div style="background:rgba(255,255,255,0.04);border-radius:8px;padding:16px;margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;font-size:20px;color:#9ca3af;margin-bottom:6px;">
<span>最终KPI</span><span style="color:#e2e8f0">${data.kpi}%</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:20px;color:#9ca3af;">
<span>剩余HC</span><span style="color:#86efac">${data.hc}</span>
</div>
</div>
<div style="font-size:28px;color:${data.gradeColor};margin-bottom:10px;text-shadow:0 0 12px ${data.gradeColor}88;">
${data.grade}
</div>
<div style="font-size:16px;color:#9ca3af;margin-bottom:28px;">${data.gradeDesc}</div>
`
const btnWrap = document.createElement('div')
btnWrap.style.cssText = 'display:flex;gap:16px;justify-content:center;'
const restartBtn = document.createElement('button')
restartBtn.textContent = '再战一局'
restartBtn.style.cssText = `
padding:12px 28px;background:#7c3aed;color:#fff;border:2px solid #7c3aed;
border-radius:8px;font-family:'VT323',monospace;font-size:20px;cursor:pointer;
`
restartBtn.addEventListener('click', () => window.location.reload())
const homeBtn = document.createElement('button')
homeBtn.textContent = '见好就收'
homeBtn.style.cssText = `
padding:12px 28px;background:#374151;color:#d1d5db;border:1px solid #4b5563;
border-radius:8px;font-family:'VT323',monospace;font-size:20px;cursor:pointer;
`
homeBtn.addEventListener('click', () => { window.location.href = '/' })
btnWrap.appendChild(restartBtn)
btnWrap.appendChild(homeBtn)
card.appendChild(btnWrap)
return card
}
function buildDefeatCard(): HTMLElement {
const card = document.createElement('div')
card.style.cssText = `
background: #ffffff;
border-radius: 14px;
max-width: 420px;
width: 90%;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
animation: endCardIn 0.4s ease;
`
const topBar = document.createElement('div')
topBar.textContent = '系统通知 — 人力资源管理系统'
topBar.style.cssText = `
background: #1677ff;
color: #fff;
font-family: 'VT323', monospace;
font-size: 18px;
padding: 10px 20px;
letter-spacing: 2px;
`
card.appendChild(topBar)
const body = document.createElement('div')
body.style.cssText = 'padding: 28px 32px 24px;'
body.innerHTML = `
<div style="font-size:48px;text-align:center;margin-bottom:16px;">📋</div>
<div style="font-family:'VT323',monospace;font-size:18px;color:#333;line-height:1.7;margin-bottom:24px;text-align:center;">
鉴于您近期的表现未能达成业务闭环,<br/>
经公司管理层研究决定,<br/>
对您进行<strong style="color:#ef4444">「毕业」</strong>处理。<br/>
请于5分钟内归还工牌<br/>
不要带走办公文具。
</div>
`
const btnWrap = document.createElement('div')
btnWrap.style.cssText = 'display:flex;gap:12px;justify-content:center;'
const restartBtn = document.createElement('button')
restartBtn.textContent = '领取 N+1'
restartBtn.style.cssText = `
padding:10px 24px;background:#1677ff;color:#fff;border:none;
border-radius:6px;font-family:'VT323',monospace;font-size:20px;cursor:pointer;
`
restartBtn.addEventListener('click', () => window.location.reload())
const homeBtn = document.createElement('button')
homeBtn.textContent = '申请仲裁'
homeBtn.style.cssText = `
padding:10px 24px;background:#f3f4f6;color:#374151;border:1px solid #d1d5db;
border-radius:6px;font-family:'VT323',monospace;font-size:20px;cursor:pointer;
`
homeBtn.addEventListener('click', () => { window.location.href = '/' })
btnWrap.appendChild(restartBtn)
btnWrap.appendChild(homeBtn)
body.appendChild(btnWrap)
card.appendChild(body)
return card
}
/** 显示胜利结算弹窗 */
export function showVictoryModal(data: VictoryData): void {
injectEndStyles()
const overlay = buildOverlay()
overlay.appendChild(buildVictoryCard(data))
document.body.appendChild(overlay)
}
/** 显示失败结算弹窗 */
export function showDefeatModal(): void {
injectEndStyles()
const overlay = buildOverlay()
overlay.appendChild(buildDefeatCard())
document.body.appendChild(overlay)
}

View File

@@ -1,9 +1,11 @@
import type Phaser from 'phaser' import type Phaser from 'phaser'
import { GAME_WIDTH, HUD_HEIGHT } from '../constants' import { GAME_WIDTH, HUD_HEIGHT } from '../constants'
import { GameManager } from '../GameManager'
import { showVictoryModal, showDefeatModal } from './EndScreenModal'
/** /**
* 游戏 HUD 辅助工具 * 游戏 HUD 辅助工具
* 负责管理"召唤下一波"按钮波次提示横幅 * 负责管理"召唤下一波"按钮波次提示横幅、胜利/失败结算弹窗
*/ */
export class HUD { export class HUD {
private scene: Phaser.Scene private scene: Phaser.Scene
@@ -34,18 +36,12 @@ export class HUD {
.setInteractive({ useHandCursor: true }) .setInteractive({ useHandCursor: true })
this.waveBtn.on('pointerover', () => { this.waveBtn.on('pointerover', () => {
if (this.waveBtn) { if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#2d5a8e' })
this.waveBtn.setStyle({ backgroundColor: '#2d5a8e' })
}
}) })
this.waveBtn.on('pointerout', () => { this.waveBtn.on('pointerout', () => {
if (this.waveBtn) { if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#1e3a5f' })
this.waveBtn.setStyle({ backgroundColor: '#1e3a5f' })
}
})
this.waveBtn.on('pointerdown', () => {
onClick()
}) })
this.waveBtn.on('pointerdown', () => onClick())
} }
/** 更新按钮文字(如禁用状态) */ /** 更新按钮文字(如禁用状态) */
@@ -67,6 +63,7 @@ export class HUD {
/** /**
* 显示波次开始横幅 * 显示波次开始横幅
* @param waveNumber 当前波次1-based * @param waveNumber 当前波次1-based
* @param totalWaves 总波次数
*/ */
showWaveBanner(waveNumber: number, totalWaves: number): void { showWaveBanner(waveNumber: number, totalWaves: number): void {
const isBoss = waveNumber === totalWaves const isBoss = waveNumber === totalWaves
@@ -98,10 +95,10 @@ export class HUD {
}) })
} }
/** 显示周报触发提示 */ /** 显示周报触发提示横幅 */
showWeeklyReportAlert(): void { showWeeklyReportAlert(): void {
const banner = this.scene.add const banner = this.scene.add
.text(GAME_WIDTH / 2, HUD_HEIGHT + 120, '📋 季度周报截止!效率翻倍', { .text(GAME_WIDTH / 2, HUD_HEIGHT + 120, '周报时间到!请选择正确黑话', {
fontFamily: "'Press Start 2P', monospace", fontFamily: "'Press Start 2P', monospace",
fontSize: '11px', fontSize: '11px',
color: '#FCD34D', color: '#FCD34D',
@@ -122,51 +119,40 @@ export class HUD {
}) })
} }
/** 显示胜利画面 */ /** 显示胜利画面(完整绩效评级弹窗,委托给 EndScreenModal */
showVictory(): void { showVictory(): void {
const overlay = this.scene.add.graphics() const manager = GameManager.getInstance()
overlay.fillStyle(0x000000, 0.6) const kpi = manager.kpi
overlay.fillRect(0, 0, GAME_WIDTH, 720) const hc = manager.hc
overlay.setDepth(50)
this.scene.add let grade: string
.text(GAME_WIDTH / 2, 300, '🎉 大厂保卫成功!', { let gradeColor: string
fontFamily: "'Press Start 2P', monospace", let gradeDesc: string
fontSize: '22px',
color: '#A78BFA',
backgroundColor: '#0A1628',
padding: { x: 24, y: 12 },
})
.setOrigin(0.5, 0.5)
.setDepth(55)
this.scene.add if (kpi >= 80) {
.text(GAME_WIDTH / 2, 380, 'KPI 绩效已发放!', { grade = 'S级 "超出预期"'
fontFamily: 'VT323, monospace', gradeColor = '#fbbf24'
fontSize: '24px', gradeDesc = '恭喜晋升为高级打工人!福报已在路上...'
color: '#A78BFA', } else if (kpi >= 60) {
}) grade = 'A级 "达成预期"'
.setOrigin(0.5, 0.5) gradeColor = '#34d399'
.setDepth(55) gradeDesc = '表现良好明年涨薪3%(扣去通胀后-2%'
} else if (kpi >= 40) {
grade = 'B级 "基本达成"'
gradeColor = '#60a5fa'
gradeDesc = '还需努力下月开始996'
} else {
grade = 'C级 "未达预期"'
gradeColor = '#f87171'
gradeDesc = '已被纳入待优化名单'
}
showVictoryModal({ kpi, hc, grade, gradeColor, gradeDesc })
} }
/** 显示失败画面 */ /** 显示失败画面(仿钉钉退群通知,委托给 EndScreenModal */
showGameOver(): void { showGameOver(): void {
const overlay = this.scene.add.graphics() showDefeatModal()
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 { destroy(): void {