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 {