Files
tankwar_proj/js/scenes/ResultScene.js
2026-04-10 23:05:26 +08:00

421 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;