diff --git a/game/ui/EndScreenModal.ts b/game/ui/EndScreenModal.ts
new file mode 100644
index 0000000..9d322df
--- /dev/null
+++ b/game/ui/EndScreenModal.ts
@@ -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 = `
+
绩效评级公示
+
+
+ 最终KPI${data.kpi}%
+
+
+ 剩余HC${data.hc}
+
+
+
+ ${data.grade}
+
+ ${data.gradeDesc}
+ `
+ 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 = `
+ 📋
+
+ 鉴于您近期的表现未能达成业务闭环,
+ 经公司管理层研究决定,
+ 对您进行「毕业」处理。
+ 请于5分钟内归还工牌,
+ 不要带走办公文具。
+
+ `
+
+ 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)
+}
diff --git a/game/ui/HUD.ts b/game/ui/HUD.ts
index da12a53..fd7491c 100644
--- a/game/ui/HUD.ts
+++ b/game/ui/HUD.ts
@@ -1,9 +1,11 @@
import type Phaser from 'phaser'
import { GAME_WIDTH, HUD_HEIGHT } from '../constants'
+import { GameManager } from '../GameManager'
+import { showVictoryModal, showDefeatModal } from './EndScreenModal'
/**
* 游戏 HUD 辅助工具
- * 负责管理"召唤下一波"按钮和波次提示横幅
+ * 负责管理"召唤下一波"按钮、波次提示横幅、胜利/失败结算弹窗
*/
export class HUD {
private scene: Phaser.Scene
@@ -34,18 +36,12 @@ export class HUD {
.setInteractive({ useHandCursor: true })
this.waveBtn.on('pointerover', () => {
- if (this.waveBtn) {
- this.waveBtn.setStyle({ backgroundColor: '#2d5a8e' })
- }
+ 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()
+ if (this.waveBtn) this.waveBtn.setStyle({ backgroundColor: '#1e3a5f' })
})
+ this.waveBtn.on('pointerdown', () => onClick())
}
/** 更新按钮文字(如禁用状态) */
@@ -67,6 +63,7 @@ export class HUD {
/**
* 显示波次开始横幅
* @param waveNumber 当前波次(1-based)
+ * @param totalWaves 总波次数
*/
showWaveBanner(waveNumber: number, totalWaves: number): void {
const isBoss = waveNumber === totalWaves
@@ -98,10 +95,10 @@ export class HUD {
})
}
- /** 显示周报触发提示 */
+ /** 显示周报触发提示横幅 */
showWeeklyReportAlert(): void {
const banner = this.scene.add
- .text(GAME_WIDTH / 2, HUD_HEIGHT + 120, '📋 季度周报截止!效率翻倍!', {
+ .text(GAME_WIDTH / 2, HUD_HEIGHT + 120, '周报时间到!请选择正确黑话!', {
fontFamily: "'Press Start 2P', monospace",
fontSize: '11px',
color: '#FCD34D',
@@ -122,51 +119,40 @@ export class HUD {
})
}
- /** 显示胜利画面 */
+ /** 显示胜利画面(完整绩效评级弹窗,委托给 EndScreenModal) */
showVictory(): void {
- const overlay = this.scene.add.graphics()
- overlay.fillStyle(0x000000, 0.6)
- overlay.fillRect(0, 0, GAME_WIDTH, 720)
- overlay.setDepth(50)
+ const manager = GameManager.getInstance()
+ const kpi = manager.kpi
+ const hc = manager.hc
- 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)
+ let grade: string
+ let gradeColor: string
+ let gradeDesc: string
- this.scene.add
- .text(GAME_WIDTH / 2, 380, 'KPI 绩效已发放!', {
- fontFamily: 'VT323, monospace',
- fontSize: '24px',
- color: '#A78BFA',
- })
- .setOrigin(0.5, 0.5)
- .setDepth(55)
+ if (kpi >= 80) {
+ grade = 'S级 "超出预期"'
+ gradeColor = '#fbbf24'
+ gradeDesc = '恭喜晋升为高级打工人!福报已在路上...'
+ } else if (kpi >= 60) {
+ grade = 'A级 "达成预期"'
+ gradeColor = '#34d399'
+ 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 {
- 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)
+ showDefeatModal()
}
destroy(): void {