first commit
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user