421 lines
12 KiB
JavaScript
421 lines
12 KiB
JavaScript
/**
|
||
* 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 = 50; // Base reward per requirements
|
||
|
||
// Bonus per kill type
|
||
gold += (stats.kills.normal || 0) * 5;
|
||
gold += (stats.kills.fast || 0) * 10;
|
||
gold += (stats.kills.armor || 0) * 15;
|
||
gold += (stats.kills.boss || 0) * 25;
|
||
|
||
// Victory bonus
|
||
if (this._victory) {
|
||
gold += 50;
|
||
}
|
||
|
||
// Time bonus (faster = more gold, max 30 gold for under 60s)
|
||
if (this._victory && stats.timeElapsed < 300) {
|
||
gold += Math.max(0, Math.floor((300 - stats.timeElapsed) / 10));
|
||
}
|
||
|
||
// Base alive bonus
|
||
if (stats.baseAlive) {
|
||
gold += 20;
|
||
}
|
||
|
||
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;
|