/** * TeamResultScene.js * 3v3 Team match result screen. * Shows winner, per-player stats (kills/deaths/assists/base damage), * base HP summary, and options to rematch or return to menu. */ const { SCREEN_WIDTH, SCREEN_HEIGHT, COLORS, SCENE, NET_MSG, } = require('../base/GameGlobal'); const { t } = require('../i18n/I18n'); // Layout const BTN_WIDTH = Math.min(SCREEN_WIDTH * 0.35, 180); const BTN_HEIGHT = Math.min(36, SCREEN_HEIGHT * 0.07); const BTN_GAP = 14; const CENTER_X = SCREEN_WIDTH / 2; // Team colors const TEAM_A_COLOR = '#4A90D9'; const TEAM_B_COLOR = '#E94560'; const TeamResultScene = { _winner: '', _winReason: '', _myTeam: '', _didWin: false, _teamABaseHp: 0, _teamBBaseHp: 0, _stats: {}, _players: [], _elapsedTime: 0, _teamId: '', _animTimer: 0, _battleMode: '3v3', // '1v1' or '3v3' // Rematch state _rematchRequested: false, _rematchReadyCount: 0, _rematchTotalCount: 0, _networkManager: null, _unsubscribers: [], // Button rects _rematchBtnRect: null, _menuBtnRect: null, _adDoubleBtnRect: null, // Ad state _adWatched: false, _goldReward: 0, // Scroll state for player list _scrollY: 0, enter(params) { this._winner = (params && params.winner) || ''; this._winReason = (params && params.winReason) || 'base_destroyed'; this._myTeam = (params && params.myTeam) || 'A'; this._didWin = (params && params.didWin) || false; this._teamABaseHp = (params && params.teamABaseHp) || 0; this._teamBBaseHp = (params && params.teamBBaseHp) || 0; this._stats = (params && params.stats) || {}; this._players = (params && params.players) || []; this._elapsedTime = (params && params.elapsedTime) || 0; this._teamId = (params && params.teamId) || ''; this._animTimer = 0; this._scrollY = 0; this._battleMode = (params && params.battleMode) || '3v3'; this._rematchRequested = false; this._rematchReadyCount = 0; this._rematchTotalCount = 0; this._networkManager = GameGlobal.networkManager; this._adWatched = false; this._goldReward = 0; // Calculate and award gold this._calculateAndAwardGold(); this._setupNetworkEvents(); const btnY = SCREEN_HEIGHT * 0.88; this._rematchBtnRect = { x: CENTER_X - BTN_WIDTH - BTN_GAP / 2, y: btnY, w: BTN_WIDTH, h: BTN_HEIGHT, }; this._menuBtnRect = { x: CENTER_X + BTN_GAP / 2, y: btnY, w: BTN_WIDTH, h: BTN_HEIGHT, }; // Double reward ad button (above rematch/menu buttons) const adBtnY = btnY - BTN_HEIGHT - BTN_GAP; this._adDoubleBtnRect = { x: CENTER_X - BTN_WIDTH * 0.75, y: adBtnY, w: BTN_WIDTH * 1.5, h: BTN_HEIGHT, }; }, exit() { this._cleanupNetworkEvents(); }, _setupNetworkEvents() { this._cleanupNetworkEvents(); const nm = this._networkManager; if (!nm) return; const unsubs = []; // Listen for rematch ready updates unsubs.push(nm.on(NET_MSG.REMATCH_READY, (data) => { console.log('[TeamResultScene] REMATCH_READY received:', JSON.stringify(data)); this._rematchReadyCount = data.readyCount || 0; this._rematchTotalCount = data.totalCount || 0; })); // Listen for game start (rematch accepted, new game starting) unsubs.push(nm.on(NET_MSG.GAME_START, (data) => { console.log('[TeamResultScene] GAME_START received for rematch'); this._startRematchGame(data); })); unsubs.push(nm.on(NET_MSG.TEAM_GAME_START, (data) => { console.log('[TeamResultScene] TEAM_GAME_START received for rematch'); this._startRematchGame(data); })); this._unsubscribers = unsubs; }, _cleanupNetworkEvents() { for (const unsub of this._unsubscribers) { if (typeof unsub === 'function') unsub(); } this._unsubscribers = []; }, /** * Calculate and award gold for team match. * @private */ _calculateAndAwardGold() { let gold = 50; // Base reward per requirements // Find local player stats const localPlayer = this._players.find(p => p.isLocal); if (localPlayer) { const stats = this._stats[localPlayer.playerId] || {}; gold += (stats.kills || 0) * 10; gold += (stats.assists || 0) * 5; } // Victory bonus if (this._didWin) { gold += 50; } this._goldReward = gold; // Award gold if (gold > 0 && GameGlobal.currencyManager) { GameGlobal.currencyManager.addGold(gold); } }, _startRematchGame(data) { const sm = GameGlobal.sceneManager; if (!sm._scenes.has(SCENE.TEAM_GAME)) { const TeamGameScene = require('./TeamGameScene'); sm.register(SCENE.TEAM_GAME, TeamGameScene); } const myPlayerId = this._networkManager ? this._networkManager.playerId : 'local'; sm.switchTo(SCENE.TEAM_GAME, { teamId: data.roomId || this._teamId, roomId: data.roomId || this._teamId, mapId: data.mapId || null, teamA: data.teamA || [], teamB: data.teamB || [], teamABaseHp: data.teamABaseHp, teamBBaseHp: data.teamBBaseHp, myPlayerId, battleMode: data.battleMode || this._battleMode, }); }, update(dt) { this._animTimer += dt; }, render(ctx) { // Background ctx.fillStyle = COLORS.MENU_BG; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); // Top accent bar const gradient = ctx.createLinearGradient(0, 0, SCREEN_WIDTH, 0); gradient.addColorStop(0, '#0f3460'); gradient.addColorStop(0.5, '#e94560'); gradient.addColorStop(1, '#0f3460'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, SCREEN_WIDTH, 4); // Title ctx.font = 'bold 16px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#AAAAAA'; ctx.fillText(t('teamResult.title'), CENTER_X, SCREEN_HEIGHT * 0.05); // Winner announcement with pulsing effect let resultText, resultColor; if (this._didWin) { resultText = t('teamResult.victory'); resultColor = '#00FF00'; } else { resultText = t('teamResult.defeat'); resultColor = '#FF4444'; } const scale = 1 + Math.sin(this._animTimer * 3) * 0.05; ctx.save(); ctx.translate(CENTER_X, SCREEN_HEIGHT * 0.13); ctx.scale(scale, scale); ctx.fillStyle = resultColor; ctx.font = 'bold 28px Arial'; ctx.fillText(resultText, 0, 0); ctx.restore(); // Base HP summary const hpY = SCREEN_HEIGHT * 0.2; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'center'; ctx.fillStyle = TEAM_A_COLOR; ctx.fillText(t('teamResult.teamAHp', { hp: this._teamABaseHp }), CENTER_X - 70, hpY); ctx.fillStyle = '#AAAAAA'; ctx.fillText('vs', CENTER_X, hpY); ctx.fillStyle = TEAM_B_COLOR; ctx.fillText(t('teamResult.teamBHp', { hp: this._teamBBaseHp }), CENTER_X + 70, hpY); // Win reason let reasonText = t('teamResult.baseDestroyed'); if (this._winReason === 'disconnected') { reasonText = t('teamResult.disconnectedReason'); } ctx.fillStyle = '#888888'; ctx.font = '10px Arial'; ctx.fillText(reasonText, CENTER_X, hpY + 16); // Player stats table this._renderStatsTable(ctx); // Double reward ad button if (!this._adWatched) { this._drawButton(ctx, this._adDoubleBtnRect, t('result.adDouble') || '📺 Double Rewards'); } // Gold reward display if (this._goldReward > 0) { const goldY = this._adDoubleBtnRect ? this._adDoubleBtnRect.y - 20 : SCREEN_HEIGHT * 0.82; ctx.fillStyle = '#FFD700'; ctx.font = 'bold 14px Arial'; ctx.textAlign = 'center'; const goldLabel = this._adWatched ? `🪙 +${this._goldReward} (${t('result.doubled') || '2x!'})` : `🪙 +${this._goldReward}`; ctx.fillText(goldLabel, CENTER_X, goldY); } // Buttons if (this._rematchRequested) { // Show waiting state on rematch button const dots = '.'.repeat(Math.floor(this._animTimer * 3) % 4); this._drawButton(ctx, this._rematchBtnRect, t('teamResult.rematchWaiting', { ready: this._rematchReadyCount, total: this._rematchTotalCount }) + dots, true); } else { this._drawButton(ctx, this._rematchBtnRect, t('teamResult.rematch')); } this._drawButton(ctx, this._menuBtnRect, t('teamResult.backMenu')); }, _renderStatsTable(ctx) { const tableY = SCREEN_HEIGHT * 0.28; const tableW = Math.min(SCREEN_WIDTH * 0.92, 400); const tableX = CENTER_X - tableW / 2; const rowH = 18; const headerH = 22; // Sort players: Team A first, then Team B; within team sort by kills desc const teamAPlayers = this._players.filter(p => p.team === 'A'); const teamBPlayers = this._players.filter(p => p.team === 'B'); const sortByKills = (a, b) => { const sa = this._stats[a.playerId] || {}; const sb = this._stats[b.playerId] || {}; return (sb.kills || 0) - (sa.kills || 0); }; teamAPlayers.sort(sortByKills); teamBPlayers.sort(sortByKills); // Column positions const cols = { name: tableX + tableW * 0.22, kills: tableX + tableW * 0.48, deaths: tableX + tableW * 0.60, assists: tableX + tableW * 0.72, baseDmg: tableX + tableW * 0.88, }; // Render Team A section let y = tableY; // Team A header ctx.fillStyle = 'rgba(74, 144, 217, 0.15)'; ctx.fillRect(tableX, y, tableW, headerH); ctx.fillStyle = TEAM_A_COLOR; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(t('teamResult.teamAHeader') + (this._myTeam === 'A' ? t('teamResult.myTeamSuffix') : ''), tableX + 6, y + headerH / 2); // Column headers ctx.fillStyle = '#AAAAAA'; ctx.font = '9px Arial'; ctx.textAlign = 'center'; ctx.fillText(t('teamResult.player'), cols.name, y + headerH / 2); ctx.fillText(t('teamResult.k'), cols.kills, y + headerH / 2); ctx.fillText(t('teamResult.d'), cols.deaths, y + headerH / 2); ctx.fillText(t('teamResult.a'), cols.assists, y + headerH / 2); ctx.fillText(t('teamResult.dmg'), cols.baseDmg, y + headerH / 2); y += headerH; // Team A players for (const player of teamAPlayers) { const stats = this._stats[player.playerId] || {}; const isLocal = player.isLocal; ctx.fillStyle = isLocal ? 'rgba(255, 215, 0, 0.1)' : 'rgba(255,255,255,0.02)'; ctx.fillRect(tableX, y, tableW, rowH); // Player name ctx.fillStyle = isLocal ? '#FFD700' : '#CCCCCC'; ctx.font = isLocal ? 'bold 10px Arial' : '10px Arial'; ctx.textAlign = 'center'; const name = player.isBot ? t('teamResult.bot') : (player.playerId.length > 10 ? player.playerId.substring(0, 10) + '..' : player.playerId); ctx.fillText(name + (isLocal ? ' ★' : ''), cols.name, y + rowH / 2); // Stats ctx.fillStyle = '#FFFFFF'; ctx.font = '10px Arial'; ctx.fillText(String(stats.kills || 0), cols.kills, y + rowH / 2); ctx.fillText(String(stats.deaths || 0), cols.deaths, y + rowH / 2); ctx.fillText(String(stats.assists || 0), cols.assists, y + rowH / 2); ctx.fillText(String(stats.baseDamage || 0), cols.baseDmg, y + rowH / 2); y += rowH; } // Separator y += 4; // Team B header ctx.fillStyle = 'rgba(233, 69, 96, 0.15)'; ctx.fillRect(tableX, y, tableW, headerH); ctx.fillStyle = TEAM_B_COLOR; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'left'; ctx.fillText(t('teamResult.teamBHeader') + (this._myTeam === 'B' ? t('teamResult.myTeamSuffix') : ''), tableX + 6, y + headerH / 2); // Column headers ctx.fillStyle = '#AAAAAA'; ctx.font = '9px Arial'; ctx.textAlign = 'center'; ctx.fillText(t('teamResult.player'), cols.name, y + headerH / 2); ctx.fillText(t('teamResult.k'), cols.kills, y + headerH / 2); ctx.fillText(t('teamResult.d'), cols.deaths, y + headerH / 2); ctx.fillText(t('teamResult.a'), cols.assists, y + headerH / 2); ctx.fillText(t('teamResult.dmg'), cols.baseDmg, y + headerH / 2); y += headerH; // Team B players for (const player of teamBPlayers) { const stats = this._stats[player.playerId] || {}; const isLocal = player.isLocal; ctx.fillStyle = isLocal ? 'rgba(255, 215, 0, 0.1)' : 'rgba(255,255,255,0.02)'; ctx.fillRect(tableX, y, tableW, rowH); // Player name ctx.fillStyle = isLocal ? '#FFD700' : '#CCCCCC'; ctx.font = isLocal ? 'bold 10px Arial' : '10px Arial'; ctx.textAlign = 'center'; const name = player.isBot ? t('teamResult.bot') : (player.playerId.length > 10 ? player.playerId.substring(0, 10) + '..' : player.playerId); ctx.fillText(name + (isLocal ? ' ★' : ''), cols.name, y + rowH / 2); // Stats ctx.fillStyle = '#FFFFFF'; ctx.font = '10px Arial'; ctx.fillText(String(stats.kills || 0), cols.kills, y + rowH / 2); ctx.fillText(String(stats.deaths || 0), cols.deaths, y + rowH / 2); ctx.fillText(String(stats.assists || 0), cols.assists, y + rowH / 2); ctx.fillText(String(stats.baseDamage || 0), cols.baseDmg, y + rowH / 2); y += rowH; } // Elapsed time display if (this._elapsedTime > 0) { y += 8; const minutes = Math.floor(this._elapsedTime / 60); const seconds = this._elapsedTime % 60; ctx.fillStyle = '#666666'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.fillText( t('teamResult.duration', { time: `${minutes}:${seconds.toString().padStart(2, '0')}` }), CENTER_X, y ); } // MVP highlight (player with most kills) const allPlayers = [...teamAPlayers, ...teamBPlayers]; let mvp = null; let maxKills = 0; for (const p of allPlayers) { const s = this._stats[p.playerId] || {}; if ((s.kills || 0) > maxKills) { maxKills = s.kills || 0; mvp = p; } } if (mvp && maxKills > 0) { y += 16; ctx.fillStyle = '#FFD700'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; const mvpName = mvp.isBot ? t('teamResult.bot').replace('🤖 ', '') : mvp.playerId; ctx.fillText(t('teamResult.mvp', { name: mvpName, kills: maxKills }), CENTER_X, y); } // Rank points change y += 18; const basePoints = 20; const mvpBonus = 5; if (this._didWin) { const isMvp = mvp && mvp.isLocal; const points = basePoints + (isMvp ? mvpBonus : 0); ctx.fillStyle = '#00FF00'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(t('teamResult.rankUp', { points }) + (isMvp ? t('teamResult.mvpBonus') : ''), CENTER_X, y); } else { ctx.fillStyle = '#FF6347'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(t('teamResult.rankDown', { points: basePoints }), CENTER_X, y); } }, _drawButton(ctx, rect, label) { if (!rect) return; ctx.fillStyle = COLORS.MENU_BTN; ctx.strokeStyle = COLORS.MENU_BTN_BORDER; ctx.lineWidth = 2; const r = 6; ctx.beginPath(); ctx.moveTo(rect.x + r, rect.y); ctx.lineTo(rect.x + rect.w - r, rect.y); ctx.arcTo(rect.x + rect.w, rect.y, rect.x + rect.w, rect.y + r, r); ctx.lineTo(rect.x + rect.w, rect.y + rect.h - r); ctx.arcTo(rect.x + rect.w, rect.y + rect.h, rect.x + rect.w - r, rect.y + rect.h, r); ctx.lineTo(rect.x + r, rect.y + rect.h); ctx.arcTo(rect.x, rect.y + rect.h, rect.x, rect.y + rect.h - r, r); ctx.lineTo(rect.x, rect.y + r); ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, 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, rect.x + rect.w / 2, rect.y + rect.h / 2); }, _hitTest(tx, ty, rect) { if (!rect) return false; return tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h; }, handleTouch(eventType, e) { if (eventType !== 'touchstart') return; const touch = e.touches[0]; const tx = touch.clientX; const ty = touch.clientY; // Double reward ad button if (!this._adWatched && this._hitTest(tx, ty, this._adDoubleBtnRect)) { 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._adWatched = true; // Award bonus gold (double the original reward) if (this._goldReward > 0 && GameGlobal.currencyManager) { GameGlobal.currencyManager.addGold(this._goldReward); this._goldReward *= 2; // Update display } console.log('[TeamResultScene] Double reward ad completed'); } } ); } return; } // Rematch button -> send rematch request to server (reuse room) if (this._hitTest(tx, ty, this._rematchBtnRect)) { if (!this._rematchRequested) { this._rematchRequested = true; const nm = this._networkManager; console.log(`[TeamResultScene] Rematch clicked. nm=${!!nm}, connected=${nm ? nm.connected : 'N/A'}, teamId=${this._teamId}`); if (nm && nm.connected) { nm.send(NET_MSG.REMATCH, { teamId: this._teamId }); console.log('[TeamResultScene] Rematch request sent'); } else { // Not connected, fall back to creating a new room this._rematchRequested = false; const sm = GameGlobal.sceneManager; if (this._battleMode === '1v1') { if (!sm._scenes.has(SCENE.PVP_ROOM)) { const RoomScene = require('./RoomScene'); sm.register(SCENE.PVP_ROOM, RoomScene); } sm.switchTo(SCENE.PVP_ROOM); } else { if (!sm._scenes.has(SCENE.TEAM_ROOM)) { const TeamRoomScene = require('./TeamRoomScene'); sm.register(SCENE.TEAM_ROOM, TeamRoomScene); } sm.switchTo(SCENE.TEAM_ROOM); } } } return; } // Menu button -> disconnect and go to menu if (this._hitTest(tx, ty, this._menuBtnRect)) { // Show interstitial ad when leaving if (GameGlobal.adManager) { GameGlobal.adManager.showInterstitial(); } if (GameGlobal.networkManager && GameGlobal.networkManager.connected) { GameGlobal.networkManager.disconnect(); } const sm = GameGlobal.sceneManager; sm.switchTo(SCENE.MENU); return; } }, }; module.exports = TeamResultScene;