/** * ResultScene.js * Post-game result/settlement screen showing stats, score, and navigation options. */ const { SCREEN_WIDTH, SCREEN_HEIGHT, COLORS, SCENE, GAME_MODE, TANK_TYPE, } = require('../base/GameGlobal'); const { t } = require('../i18n/I18n'); const ResultScene = { _level: 1, _mode: GAME_MODE.CLASSIC, _victory: false, _stats: null, _animTimer: 0, _showButtons: false, _isNewHighScore: false, _adWatched: false, enter(params) { this._level = params.level || 1; this._mode = params.mode || GAME_MODE.CLASSIC; this._victory = params.victory || false; this._stats = params.stats || { kills: { normal: 0, fast: 0, armor: 0, boss: 0 }, totalKills: 0, score: 0, timeElapsed: 0, baseAlive: true, }; this._animTimer = 0; this._showButtons = false; this._isNewHighScore = false; this._adWatched = false; this._buttons = {}; // Save score and progress this._saveResults(); // Interstitial ad is shown when player exits (next/retry/menu), not on enter console.log(`[ResultScene] ${this._victory ? 'Victory' : 'Defeat'} - Score: ${this._stats.score}`); }, _saveResults() { const sm = GameGlobal.storageManager; if (!sm) return; // Update high score this._isNewHighScore = sm.updateHighScore(this._mode, this._stats.score); // Update highest level if (this._victory) { sm.updateHighestLevel(this._level); } // Update open data for friend ranking if (GameGlobal.shareManager) { GameGlobal.shareManager.updateOpenData(this._stats.score, this._level); } // Calculate and award gold coins this._goldReward = this._calculateGoldReward(); if (this._goldReward > 0 && GameGlobal.currencyManager) { GameGlobal.currencyManager.addGold(this._goldReward); } }, /** * Calculate gold reward based on game performance. * @returns {number} * @private */ _calculateGoldReward() { const stats = this._stats; let gold = 30; // Base reward (reduced from 50) // Bonus per kill type (reduced) gold += (stats.kills.normal || 0) * 3; gold += (stats.kills.fast || 0) * 5; gold += (stats.kills.armor || 0) * 8; gold += (stats.kills.boss || 0) * 15; // Victory bonus (reduced from 50) if (this._victory) { gold += 30; } // Time bonus (faster = more gold, max 20 gold for under 60s, reduced from 30) if (this._victory && stats.timeElapsed < 300) { gold += Math.max(0, Math.floor((300 - stats.timeElapsed) / 15)); } // Base alive bonus (reduced from 20) if (stats.baseAlive) { gold += 10; } return gold; }, exit() {}, update(dt) { this._animTimer += dt; if (this._animTimer > 1.5 && !this._showButtons) { this._showButtons = true; } }, render(ctx) { // Background ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); const cx = SCREEN_WIDTH / 2; let y = 35; // Title ctx.fillStyle = this._victory ? '#00FF00' : '#FF4444'; ctx.font = 'bold 24px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( this._victory ? t('result.victory') : t('result.defeat'), cx, y ); y += 30; // Level info ctx.fillStyle = '#AAAAAA'; ctx.font = '14px Arial'; ctx.fillText(t('result.level', { level: this._level }), cx, y); y += 35; // Kill statistics - horizontal table layout ctx.fillStyle = COLORS.HUD_TEXT; ctx.font = 'bold 14px Arial'; ctx.fillText(t('result.killStats'), cx, y); y += 20; const killData = [ { label: t('result.tankNormal'), count: this._stats.kills.normal, score: 100, color: '#AAAAAA' }, { label: t('result.tankFast'), count: this._stats.kills.fast, score: 200, color: '#FF6347' }, { label: t('result.tankArmor'), count: this._stats.kills.armor, score: 300, color: '#228B22' }, { label: t('result.tankBoss'), count: this._stats.kills.boss, score: 500, color: '#8B0000' }, { label: t('result.totalLabel'), count: this._stats.totalKills, score: null, total: this._stats.score, color: '#FFD700' }, ]; // Table layout: first column for row labels, then 5 data columns const colCount = killData.length; const rowLabelWidth = 30; // Width for row label column const tableWidth = SCREEN_WIDTH * 0.55; const tableLeft = (SCREEN_WIDTH - tableWidth) / 2; const dataColWidth = (tableWidth - rowLabelWidth) / colCount; const dataLeft = tableLeft + rowLabelWidth; // Row 1: Column headers (blank first col + tank type names + total) ctx.font = 'bold 11px Arial'; ctx.textBaseline = 'middle'; for (let i = 0; i < colCount; i++) { const showDelay = i * 0.3; if (this._animTimer < showDelay) continue; const colCx = dataLeft + dataColWidth * i + dataColWidth / 2; ctx.textAlign = 'center'; ctx.fillStyle = killData[i].color; ctx.fillText(killData[i].label, colCx, y); } y += 16; // Row 2: Kill counts (first col = "击杀") ctx.font = '12px Arial'; ctx.textAlign = 'left'; ctx.fillStyle = '#AAAAAA'; ctx.fillText(t('result.rowKills'), tableLeft, y); for (let i = 0; i < colCount; i++) { const showDelay = i * 0.3; if (this._animTimer < showDelay) continue; const colCx = dataLeft + dataColWidth * i + dataColWidth / 2; ctx.textAlign = 'center'; ctx.fillStyle = COLORS.HUD_TEXT; ctx.fillText(`×${killData[i].count}`, colCx, y); } y += 16; // Row 3: Scores (first col = "得分") ctx.font = '12px Arial'; ctx.textAlign = 'left'; ctx.fillStyle = '#AAAAAA'; ctx.fillText(t('result.rowScore'), tableLeft, y); for (let i = 0; i < colCount; i++) { const showDelay = i * 0.3; if (this._animTimer < showDelay) continue; const colCx = dataLeft + dataColWidth * i + dataColWidth / 2; ctx.textAlign = 'center'; ctx.fillStyle = '#FFD700'; const scoreVal = killData[i].total != null ? killData[i].total : killData[i].count * killData[i].score; ctx.fillText(`${scoreVal}`, colCx, y); } y += 10; // Divider ctx.strokeStyle = '#333333'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx - 120, y); ctx.lineTo(cx + 120, y); ctx.stroke(); y += 18; // Time ctx.fillStyle = '#AAAAAA'; ctx.font = '12px Arial'; const minutes = Math.floor(this._stats.timeElapsed / 60); const seconds = Math.floor(this._stats.timeElapsed % 60); ctx.fillText( t('result.time', { minutes, seconds: seconds.toString().padStart(2, '0') }), cx, y ); y += 18; // Base status ctx.fillText( this._stats.baseAlive ? t('result.baseAlive') : t('result.baseDestroyed'), cx, y ); // New high score indicator if (this._isNewHighScore) { y += 20; ctx.fillStyle = '#FF69B4'; ctx.font = 'bold 13px Arial'; ctx.fillText(t('result.newRecord'), cx, y); } // Gold reward display if (this._goldReward > 0) { y += 22; ctx.fillStyle = '#FFD700'; ctx.font = 'bold 14px Arial'; const goldLabel = this._adWatched ? `🪙 +${this._goldReward} (${t('result.doubled') || '2x!'})` : `🪙 +${this._goldReward}`; ctx.fillText(goldLabel, cx, y); } // Buttons (shown after animation) if (this._showButtons) { // Calculate how many buttons will be shown let btnCount = 2; // retry + menu always present if (this._victory) btnCount += 2; // share + next if (!this._adWatched) btnCount += 1; // ad_double const btnSpacing = 38; const totalBtnHeight = btnCount * btnSpacing; // Start buttons so they end 15px above screen bottom y = SCREEN_HEIGHT - totalBtnHeight - 15; // Share challenge button if (this._victory) { this._drawButton(ctx, cx, y, t('result.share'), 'share'); y += btnSpacing; } // Double score ad button (if not watched) if (!this._adWatched) { this._drawButton(ctx, cx, y, t('result.adDouble'), 'ad_double'); y += btnSpacing; } if (this._victory) { this._drawButton(ctx, cx, y, t('result.nextLevel'), 'next'); y += btnSpacing; } // Retry button this._drawButton(ctx, cx, y, t('result.retry'), 'retry'); y += btnSpacing; // Menu button this._drawButton(ctx, cx, y, t('result.backMenu'), 'menu'); } }, _drawButton(ctx, cx, y, label, id) { const w = SCREEN_WIDTH * 0.55; const h = 36; const x = cx - w / 2; // Store button rect for touch detection if (!this._buttons) this._buttons = {}; this._buttons[id] = { x, y: y - h / 2, w, h }; ctx.fillStyle = COLORS.MENU_BTN; ctx.strokeStyle = COLORS.MENU_BTN_BORDER; ctx.lineWidth = 2; // Rounded rect ctx.beginPath(); const r = 6; ctx.moveTo(x + r, y - h / 2); ctx.lineTo(x + w - r, y - h / 2); ctx.arcTo(x + w, y - h / 2, x + w, y - h / 2 + r, r); ctx.lineTo(x + w, y + h / 2 - r); ctx.arcTo(x + w, y + h / 2, x + w - r, y + h / 2, r); ctx.lineTo(x + r, y + h / 2); ctx.arcTo(x, y + h / 2, x, y + h / 2 - r, r); ctx.lineTo(x, y - h / 2 + r); ctx.arcTo(x, y - h / 2, x + r, y - h / 2, r); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = COLORS.MENU_BTN_TEXT; ctx.font = 'bold 14px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, cx, y); }, handleTouch(eventType, e) { if (eventType !== 'touchstart' || !this._showButtons) return; const touch = e.touches[0]; const tx = touch.clientX; const ty = touch.clientY; if (!this._buttons) return; for (const [id, rect] of Object.entries(this._buttons)) { if (tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h) { this._handleButtonPress(id); break; } } }, _handleButtonPress(id) { const sm = GameGlobal.sceneManager; switch (id) { case 'next': sm.switchTo(SCENE.GAME, { mode: this._mode, level: this._level + 1, }); break; case 'retry': sm.switchTo(SCENE.GAME, { mode: this._mode, level: this._level, }); break; case 'menu': // Show interstitial ad when leaving result screen if (GameGlobal.adManager) { GameGlobal.adManager.showInterstitial(); } sm.switchTo(SCENE.MENU); break; case 'share': if (GameGlobal.shareManager) { GameGlobal.shareManager.shareChallenge(this._level, this._stats.score); } break; case 'ad_double': { const AdManager = require('../managers/AdManager'); if (GameGlobal.adManager && GameGlobal.adManager.canShowScene(AdManager.AD_SCENE.DOUBLE_REWARD)) { GameGlobal.adManager.showRewardedVideoForScene( AdManager.AD_SCENE.DOUBLE_REWARD, (completed) => { if (completed) { this._stats.score *= 2; this._adWatched = true; // Re-save with doubled score if (GameGlobal.storageManager) { GameGlobal.storageManager.updateHighScore(this._mode, this._stats.score); } // Award bonus gold (double the original reward) if (this._goldReward && GameGlobal.currencyManager) { GameGlobal.currencyManager.addGold(this._goldReward); this._goldReward *= 2; // Update display } } } ); } else { console.warn('[ResultScene] Double reward ad not available'); } break; } } }, }; module.exports = ResultScene;