Files
tankwar_proj/js/scenes/TeamResultScene.js
T
2026-04-10 22:59:39 +08:00

589 lines
18 KiB
JavaScript

/**
* 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;