first commit

This commit is contained in:
jakciehan
2026-04-10 22:59:39 +08:00
commit cc2e7b9bb0
89 changed files with 23631 additions and 0 deletions
+420
View File
@@ -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;