add skin manager
This commit is contained in:
@@ -15,6 +15,7 @@ const CurrencyManager = require('./js/managers/CurrencyManager');
|
||||
const PaymentManager = require('./js/managers/PaymentManager');
|
||||
const ComplianceManager = require('./js/managers/ComplianceManager');
|
||||
const BuffManager = require('./js/managers/BuffManager');
|
||||
const SkinManager = require('./js/managers/SkinManager');
|
||||
const EventBus = require('./js/base/EventBus');
|
||||
const {
|
||||
SCREEN_WIDTH,
|
||||
@@ -62,12 +63,14 @@ const currencyManager = new CurrencyManager();
|
||||
const paymentManager = new PaymentManager();
|
||||
const complianceManager = new ComplianceManager();
|
||||
const buffManager = new BuffManager();
|
||||
const skinManager = new SkinManager();
|
||||
GameGlobal.adManager = adManager;
|
||||
GameGlobal.shareManager = shareManager;
|
||||
GameGlobal.currencyManager = currencyManager;
|
||||
GameGlobal.paymentManager = paymentManager;
|
||||
GameGlobal.complianceManager = complianceManager;
|
||||
GameGlobal.buffManager = buffManager;
|
||||
GameGlobal.skinManager = skinManager;
|
||||
|
||||
// ============================================================
|
||||
// Game State
|
||||
|
||||
@@ -164,6 +164,7 @@ const SCENE = {
|
||||
RANKING: 'ranking',
|
||||
SETTINGS: 'settings',
|
||||
SHOP: 'shop',
|
||||
SKIN: 'skin',
|
||||
BUFF_SELECT: 'buff_select',
|
||||
PVP_ROOM: 'pvp_room',
|
||||
PVP_GAME: 'pvp_game',
|
||||
|
||||
+19
-1
@@ -26,6 +26,7 @@ module.exports = {
|
||||
'menu.pvp': 'PVP',
|
||||
'menu.team3v3': '3v3 Battle',
|
||||
'menu.shop': 'Shop',
|
||||
'menu.skin': 'Skins',
|
||||
'menu.ranking': 'Ranking',
|
||||
'menu.settings': 'Settings',
|
||||
|
||||
@@ -229,7 +230,7 @@ module.exports = {
|
||||
'ad.reviveTitle': 'Revive Chance',
|
||||
'ad.reviveDesc': 'Choose how to revive and continue',
|
||||
'ad.watchAd': '📺 Watch Ad (Free)',
|
||||
'ad.goldRevive': '🪙 Gold Revive (200)',
|
||||
'ad.goldRevive': '🪙 Gold Revive',
|
||||
'ad.giveUp': 'Give Up',
|
||||
'ad.doubleReward': '🎬 Watch Ad for 2x Reward',
|
||||
'ad.unavailable': 'Ad temporarily unavailable',
|
||||
@@ -269,4 +270,21 @@ module.exports = {
|
||||
'dailyGold.remaining': '{remaining}/3',
|
||||
'dailyGold.exhausted': 'Come back tomorrow',
|
||||
'dailyGold.reward': '+100 Gold!',
|
||||
|
||||
// ============================================================
|
||||
// Skin System
|
||||
// ============================================================
|
||||
'skin.title': 'Tank Skins',
|
||||
'skin.default': 'Classic',
|
||||
'skin.arctic': 'Arctic',
|
||||
'skin.inferno': 'Inferno',
|
||||
'skin.phantom': 'Phantom',
|
||||
'skin.jungle': 'Jungle',
|
||||
'skin.neon': 'Neon',
|
||||
'skin.shadow': 'Shadow',
|
||||
'skin.royal': 'Royal',
|
||||
'skin.equipped': '✓ Equipped',
|
||||
'skin.owned': 'Owned',
|
||||
'skin.equipSuccess': '✓ Skin equipped!',
|
||||
'skin.purchaseSuccess': '✓ Skin unlocked!',
|
||||
};
|
||||
|
||||
+19
-1
@@ -26,6 +26,7 @@ module.exports = {
|
||||
'menu.pvp': '双人对战',
|
||||
'menu.team3v3': '3v3 对战',
|
||||
'menu.shop': '商店',
|
||||
'menu.skin': '皮肤',
|
||||
'menu.ranking': '排行榜',
|
||||
'menu.settings': '设置',
|
||||
|
||||
@@ -229,7 +230,7 @@ module.exports = {
|
||||
'ad.reviveTitle': '复活机会',
|
||||
'ad.reviveDesc': '选择复活方式继续游戏',
|
||||
'ad.watchAd': '📺 观看广告(免费)',
|
||||
'ad.goldRevive': '🪙 金币复活(200)',
|
||||
'ad.goldRevive': '🪙 金币复活',
|
||||
'ad.giveUp': '放弃',
|
||||
'ad.doubleReward': '🎬 看广告双倍奖励',
|
||||
'ad.unavailable': '广告暂时不可用',
|
||||
@@ -269,4 +270,21 @@ module.exports = {
|
||||
'dailyGold.remaining': '{remaining}/3',
|
||||
'dailyGold.exhausted': '明日再来',
|
||||
'dailyGold.reward': '+100 金币!',
|
||||
|
||||
// ============================================================
|
||||
// Skin System
|
||||
// ============================================================
|
||||
'skin.title': '坦克皮肤',
|
||||
'skin.default': '经典',
|
||||
'skin.arctic': '极地',
|
||||
'skin.inferno': '烈焰',
|
||||
'skin.phantom': '幻影',
|
||||
'skin.jungle': '丛林',
|
||||
'skin.neon': '霓虹',
|
||||
'skin.shadow': '暗影',
|
||||
'skin.royal': '皇家',
|
||||
'skin.equipped': '✓ 使用中',
|
||||
'skin.owned': '已拥有',
|
||||
'skin.equipSuccess': '✓ 已装备!',
|
||||
'skin.purchaseSuccess': '✓ 已解锁!',
|
||||
};
|
||||
|
||||
+31
-15
@@ -79,6 +79,7 @@ const GameScene = {
|
||||
|
||||
// Revive ad state
|
||||
_reviveAdUsed: false,
|
||||
_reviveCount: 0, // Track revive count for escalating cost
|
||||
_showingReviveDialog: false,
|
||||
_reviveDialogButtons: null,
|
||||
|
||||
@@ -97,6 +98,7 @@ const GameScene = {
|
||||
this._gameOverDelay = 0;
|
||||
this._cachedBasePos = null;
|
||||
this._reviveAdUsed = false;
|
||||
this._reviveCount = 0;
|
||||
this._showingReviveDialog = false;
|
||||
this._reviveDialogButtons = null;
|
||||
|
||||
@@ -138,6 +140,11 @@ const GameScene = {
|
||||
});
|
||||
this._playerTank.activateShield(3000); // spawn protection
|
||||
|
||||
// Apply equipped skin colors to player tank
|
||||
if (GameGlobal.skinManager) {
|
||||
this._playerTank._skinColors = GameGlobal.skinManager.getCurrentSkinColors();
|
||||
}
|
||||
|
||||
// Safety: ensure player spawn area is clear of blocking terrain
|
||||
this._clearSpawnArea(levelData.playerSpawn.col, levelData.playerSpawn.row);
|
||||
|
||||
@@ -471,14 +478,15 @@ const GameScene = {
|
||||
ctx.fillText(t('ad.watchAd') || '📺 Watch Ad (Free)', btns.watchAd.x + btns.watchAd.w / 2, btns.watchAd.y + btns.watchAd.h / 2);
|
||||
}
|
||||
|
||||
// Gold Revive button (orange)
|
||||
// Gold Revive button (orange) - show escalating cost
|
||||
if (btns.goldRevive) {
|
||||
const hasGold = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(200);
|
||||
const reviveCost = this._getReviveCost();
|
||||
const hasGold = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(reviveCost);
|
||||
ctx.fillStyle = hasGold ? '#FF9800' : '#555555';
|
||||
ctx.fillRect(btns.goldRevive.x, btns.goldRevive.y, btns.goldRevive.w, btns.goldRevive.h);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = 'bold 13px Arial';
|
||||
ctx.fillText(t('ad.goldRevive') || '🪙 Gold Revive (200)', btns.goldRevive.x + btns.goldRevive.w / 2, btns.goldRevive.y + btns.goldRevive.h / 2);
|
||||
ctx.fillText(`🪙 ${t('ad.goldRevive') || 'Gold Revive'} (${reviveCost})`, btns.goldRevive.x + btns.goldRevive.w / 2, btns.goldRevive.y + btns.goldRevive.h / 2);
|
||||
}
|
||||
|
||||
// Give Up button (gray)
|
||||
@@ -680,13 +688,8 @@ const GameScene = {
|
||||
|
||||
const hasLives = this._playerTank.die();
|
||||
if (!hasLives) {
|
||||
// Check if revive ad is available and not yet used this level
|
||||
if (!this._reviveAdUsed) {
|
||||
// Always show revive dialog (with ad and/or gold options)
|
||||
this._showReviveAdDialog();
|
||||
} else {
|
||||
this._triggerGameOver();
|
||||
}
|
||||
// Always show revive dialog (escalating cost each time)
|
||||
this._showReviveAdDialog();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -701,6 +704,18 @@ const GameScene = {
|
||||
return GameGlobal.adManager.canShowScene(AdManager.AD_SCENE.REVIVE);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current revive gold cost based on revive count (escalating).
|
||||
* 1st revive: 200, 2nd: 400, 3rd+: 800
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
_getReviveCost() {
|
||||
if (this._reviveCount === 0) return 200;
|
||||
if (this._reviveCount === 1) return 400;
|
||||
return 800;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the revive dialog overlay with dual options.
|
||||
* Pauses the game and presents watch-ad / gold-revive / give-up options.
|
||||
@@ -736,17 +751,18 @@ const GameScene = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the player choosing to revive with gold (200 gold).
|
||||
* Handle the player choosing to revive with gold (escalating cost).
|
||||
* @private
|
||||
*/
|
||||
_onGoldRevive() {
|
||||
const cost = this._getReviveCost();
|
||||
const cm = GameGlobal.currencyManager;
|
||||
if (cm && cm.spendGold(200)) {
|
||||
if (cm && cm.spendGold(cost)) {
|
||||
this._showingReviveDialog = false;
|
||||
this._reviveDialogButtons = null;
|
||||
this._reviveAdUsed = true;
|
||||
this._reviveCount++;
|
||||
this._revivePlayer();
|
||||
console.log('[GameScene] Player revived via gold (200)');
|
||||
console.log(`[GameScene] Player revived via gold (${cost}), revive #${this._reviveCount}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -762,7 +778,7 @@ const GameScene = {
|
||||
this._showingReviveDialog = false;
|
||||
this._reviveDialogButtons = null;
|
||||
if (completed) {
|
||||
this._reviveAdUsed = true;
|
||||
this._reviveCount++;
|
||||
this._revivePlayer();
|
||||
} else {
|
||||
this._triggerGameOver();
|
||||
|
||||
+32
-12
@@ -22,8 +22,10 @@ const BTN_GAP = Math.min(8, SCREEN_HEIGHT * 0.015);
|
||||
const BTN_START_Y = SCREEN_HEIGHT * 0.38;
|
||||
const BTN_X = (SCREEN_WIDTH - BTN_WIDTH) / 2;
|
||||
|
||||
// Half-width buttons for the utility row (shop, battle pass, ranking, settings)
|
||||
// Half-width buttons for the utility row
|
||||
const HALF_BTN_WIDTH = (BTN_WIDTH - BTN_GAP) / 2;
|
||||
// Third-width buttons for 3-column row
|
||||
const THIRD_BTN_WIDTH = (BTN_WIDTH - BTN_GAP * 2) / 3;
|
||||
|
||||
// Main game mode buttons (full width, vertical)
|
||||
const MAIN_BUTTONS = [
|
||||
@@ -33,10 +35,11 @@ const MAIN_BUTTONS = [
|
||||
{ labelKey: 'menu.team3v3', mode: GAME_MODE.TEAM_3V3, scene: SCENE.TEAM_ROOM },
|
||||
];
|
||||
|
||||
// Utility buttons: shop, daily gold, ranking, settings (2x2 grid)
|
||||
// Utility buttons: shop, daily gold, skin, ranking, settings (grid)
|
||||
const UTIL_BUTTONS = [
|
||||
{ labelKey: 'menu.shop', mode: null, scene: SCENE.SHOP },
|
||||
{ labelKey: 'dailyGold.btn', mode: null, scene: 'DAILY_GOLD' },
|
||||
{ labelKey: 'menu.skin', mode: null, scene: SCENE.SKIN },
|
||||
{ labelKey: 'menu.ranking', mode: null, scene: SCENE.RANKING },
|
||||
{ labelKey: 'menu.settings', mode: null, scene: SCENE.SETTINGS },
|
||||
];
|
||||
@@ -50,18 +53,29 @@ const mainBtnRects = MAIN_BUTTONS.map((btn, i) => ({
|
||||
...btn,
|
||||
}));
|
||||
|
||||
// Pre-calculate button rects for utility buttons (2x2 grid)
|
||||
// Pre-calculate button rects for utility buttons (row1: 3 cols, row2: 2 cols centered)
|
||||
const utilStartY = BTN_START_Y + MAIN_BUTTONS.length * (BTN_HEIGHT + BTN_GAP) + BTN_GAP;
|
||||
const utilBtnRects = UTIL_BUTTONS.map((btn, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
return {
|
||||
x: BTN_X + col * (HALF_BTN_WIDTH + BTN_GAP),
|
||||
y: utilStartY + row * (BTN_HEIGHT + BTN_GAP),
|
||||
w: HALF_BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
...btn,
|
||||
};
|
||||
if (i < 3) {
|
||||
// First row: 3 buttons
|
||||
return {
|
||||
x: BTN_X + i * (THIRD_BTN_WIDTH + BTN_GAP),
|
||||
y: utilStartY,
|
||||
w: THIRD_BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
...btn,
|
||||
};
|
||||
} else {
|
||||
// Second row: 2 buttons centered
|
||||
const col = i - 3;
|
||||
return {
|
||||
x: BTN_X + col * (HALF_BTN_WIDTH + BTN_GAP),
|
||||
y: utilStartY + (BTN_HEIGHT + BTN_GAP),
|
||||
w: HALF_BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
...btn,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Combined list for unified iteration
|
||||
@@ -289,6 +303,12 @@ const MenuScene = {
|
||||
sm.register(SCENE.SHOP, ShopScene);
|
||||
}
|
||||
sm.switchTo(SCENE.SHOP);
|
||||
} else if (btn.scene === SCENE.SKIN) {
|
||||
if (!sm._scenes.has(SCENE.SKIN)) {
|
||||
const SkinScene = require('./SkinScene');
|
||||
sm.register(SCENE.SKIN, SkinScene);
|
||||
}
|
||||
sm.switchTo(SCENE.SKIN);
|
||||
} else if (btn.scene === SCENE.SETTINGS) {
|
||||
if (!sm._scenes.has(SCENE.SETTINGS)) {
|
||||
const SettingsScene = require('./SettingsScene');
|
||||
|
||||
+12
-12
@@ -82,27 +82,27 @@ const ResultScene = {
|
||||
*/
|
||||
_calculateGoldReward() {
|
||||
const stats = this._stats;
|
||||
let gold = 50; // Base reward per requirements
|
||||
let gold = 30; // Base reward (reduced from 50)
|
||||
|
||||
// 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;
|
||||
// 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
|
||||
// Victory bonus (reduced from 50)
|
||||
if (this._victory) {
|
||||
gold += 50;
|
||||
gold += 30;
|
||||
}
|
||||
|
||||
// Time bonus (faster = more gold, max 30 gold for under 60s)
|
||||
// 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) / 10));
|
||||
gold += Math.max(0, Math.floor((300 - stats.timeElapsed) / 15));
|
||||
}
|
||||
|
||||
// Base alive bonus
|
||||
// Base alive bonus (reduced from 20)
|
||||
if (stats.baseAlive) {
|
||||
gold += 20;
|
||||
gold += 10;
|
||||
}
|
||||
|
||||
return gold;
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* SkinScene.js
|
||||
* Tank skin gallery scene where players can preview, purchase, and equip skins.
|
||||
*/
|
||||
|
||||
const {
|
||||
SCREEN_WIDTH,
|
||||
SCREEN_HEIGHT,
|
||||
COLORS,
|
||||
SCENE,
|
||||
} = require('../base/GameGlobal');
|
||||
const { t } = require('../i18n/I18n');
|
||||
|
||||
// Layout constants
|
||||
const COLS = 2;
|
||||
const CARD_GAP = 10;
|
||||
const CARD_W = Math.min((SCREEN_WIDTH * 0.85 - CARD_GAP) / COLS, 150);
|
||||
const CARD_H = 100;
|
||||
const GRID_W = COLS * CARD_W + (COLS - 1) * CARD_GAP;
|
||||
const GRID_LEFT = (SCREEN_WIDTH - GRID_W) / 2;
|
||||
const CARDS_START_Y = SCREEN_HEIGHT * 0.18;
|
||||
|
||||
const SkinScene = {
|
||||
_buttons: {},
|
||||
_skinCards: [],
|
||||
_message: '',
|
||||
_messageTimer: 0,
|
||||
_scrollY: 0,
|
||||
|
||||
enter() {
|
||||
this._message = '';
|
||||
this._messageTimer = 0;
|
||||
this._scrollY = 0;
|
||||
this._calculateLayout();
|
||||
},
|
||||
|
||||
exit() {},
|
||||
|
||||
_calculateLayout() {
|
||||
const sm = GameGlobal.skinManager;
|
||||
if (!sm) return;
|
||||
|
||||
const skins = sm.getAllSkins();
|
||||
this._skinCards = [];
|
||||
|
||||
for (let i = 0; i < skins.length; i++) {
|
||||
const col = i % COLS;
|
||||
const row = Math.floor(i / COLS);
|
||||
const x = GRID_LEFT + col * (CARD_W + CARD_GAP);
|
||||
const y = CARDS_START_Y + row * (CARD_H + CARD_GAP);
|
||||
|
||||
this._skinCards.push({
|
||||
...skins[i],
|
||||
rect: { x, y, w: CARD_W, h: CARD_H },
|
||||
});
|
||||
}
|
||||
|
||||
// Back button
|
||||
const backW = 100;
|
||||
const backH = 36;
|
||||
const lastRow = Math.ceil(skins.length / COLS);
|
||||
const backY = CARDS_START_Y + lastRow * (CARD_H + CARD_GAP) + 10;
|
||||
this._buttons = {
|
||||
back: { x: (SCREEN_WIDTH - backW) / 2, y: backY, w: backW, h: backH },
|
||||
};
|
||||
},
|
||||
|
||||
update(dt) {
|
||||
if (this._messageTimer > 0) {
|
||||
this._messageTimer -= dt;
|
||||
if (this._messageTimer <= 0) {
|
||||
this._message = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render(ctx) {
|
||||
// Background
|
||||
ctx.fillStyle = COLORS.MENU_BG;
|
||||
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
|
||||
// Title
|
||||
ctx.fillStyle = COLORS.MENU_TITLE;
|
||||
ctx.font = 'bold 22px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(t('skin.title') || 'Tank Skins', SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.06);
|
||||
|
||||
// Gold balance
|
||||
const gold = GameGlobal.currencyManager ? GameGlobal.currencyManager.getGold() : 0;
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.font = 'bold 14px Arial';
|
||||
ctx.fillText(`🪙 ${gold}`, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.12);
|
||||
|
||||
const sm = GameGlobal.skinManager;
|
||||
if (!sm) return;
|
||||
|
||||
const equippedId = sm.getEquippedSkinId();
|
||||
|
||||
// Render skin cards
|
||||
for (const card of this._skinCards) {
|
||||
this._renderSkinCard(ctx, card, sm.isUnlocked(card.id), equippedId === card.id);
|
||||
}
|
||||
|
||||
// Back button
|
||||
this._renderButton(ctx, this._buttons.back, t('common.back') || '← Back', '#666666');
|
||||
|
||||
// Message toast
|
||||
if (this._message) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
||||
const msgW = 200;
|
||||
const msgH = 30;
|
||||
const msgX = (SCREEN_WIDTH - msgW) / 2;
|
||||
const msgY = SCREEN_HEIGHT * 0.92;
|
||||
ctx.fillRect(msgX, msgY, msgW, msgH);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = '13px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(this._message, SCREEN_WIDTH / 2, msgY + msgH / 2);
|
||||
}
|
||||
},
|
||||
|
||||
_renderSkinCard(ctx, card, isUnlocked, isEquipped) {
|
||||
const { rect } = card;
|
||||
|
||||
// Card background
|
||||
if (isEquipped) {
|
||||
ctx.fillStyle = 'rgba(76, 175, 80, 0.3)';
|
||||
ctx.strokeStyle = '#4CAF50';
|
||||
} else if (isUnlocked) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.08)';
|
||||
ctx.strokeStyle = '#666666';
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.03)';
|
||||
ctx.strokeStyle = '#444444';
|
||||
}
|
||||
ctx.lineWidth = isEquipped ? 2.5 : 1.5;
|
||||
|
||||
// Rounded rect
|
||||
const r = 8;
|
||||
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();
|
||||
|
||||
const cx = rect.x + rect.w / 2;
|
||||
|
||||
// Tank preview (mini tank icon)
|
||||
this._drawMiniTank(ctx, cx, rect.y + 28, card);
|
||||
|
||||
// Skin name
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = 'bold 12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(t(card.nameKey) || card.id, cx, rect.y + 58);
|
||||
|
||||
// Status / price
|
||||
if (isEquipped) {
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.font = 'bold 11px Arial';
|
||||
ctx.fillText(t('skin.equipped') || '✓ Equipped', cx, rect.y + rect.h - 16);
|
||||
} else if (isUnlocked) {
|
||||
ctx.fillStyle = '#AAAAAA';
|
||||
ctx.font = '11px Arial';
|
||||
ctx.fillText(t('skin.owned') || 'Owned', cx, rect.y + rect.h - 16);
|
||||
} else {
|
||||
const canAfford = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(card.cost);
|
||||
ctx.fillStyle = canAfford ? '#FFD700' : '#FF4444';
|
||||
ctx.font = 'bold 11px Arial';
|
||||
ctx.fillText(`🪙 ${card.cost}`, cx, rect.y + rect.h - 16);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw a mini tank preview with skin colors.
|
||||
*/
|
||||
_drawMiniTank(ctx, cx, cy, card) {
|
||||
const size = 14;
|
||||
const bodyColor = card.colors ? card.colors.body : '#FFD700';
|
||||
const turretColor = card.colors ? card.colors.turret : '#B8860B';
|
||||
const trackColor = card.colors ? card.colors.track : '#8B6914';
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
|
||||
// Body
|
||||
ctx.fillStyle = bodyColor;
|
||||
ctx.fillRect(-size, -size / 2, size * 2, size);
|
||||
|
||||
// Turret (barrel)
|
||||
ctx.fillStyle = turretColor;
|
||||
ctx.fillRect(-2, -size / 2 - 10, 4, 10);
|
||||
|
||||
// Center detail
|
||||
ctx.fillStyle = turretColor;
|
||||
ctx.fillRect(-size * 0.3, -size * 0.3, size * 0.6, size * 0.6);
|
||||
|
||||
// Tracks
|
||||
ctx.fillStyle = trackColor;
|
||||
ctx.fillRect(-size - 3, -size / 2, 3, size);
|
||||
ctx.fillRect(size, -size / 2, 3, size);
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
_renderButton(ctx, rect, label, color) {
|
||||
if (!rect) return;
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
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 = '#FFFFFF';
|
||||
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;
|
||||
},
|
||||
|
||||
_showMessage(msg) {
|
||||
this._message = msg;
|
||||
this._messageTimer = 2;
|
||||
},
|
||||
|
||||
handleTouch(eventType, e) {
|
||||
if (eventType !== 'touchstart') return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const tx = touch.clientX;
|
||||
const ty = touch.clientY;
|
||||
const sm = GameGlobal.skinManager;
|
||||
if (!sm) return;
|
||||
|
||||
// Check skin cards
|
||||
for (const card of this._skinCards) {
|
||||
if (this._hitTest(tx, ty, card.rect)) {
|
||||
if (sm.isUnlocked(card.id)) {
|
||||
// Equip
|
||||
if (sm.getEquippedSkinId() !== card.id) {
|
||||
const result = sm.equipSkin(card.id);
|
||||
if (result.success) {
|
||||
this._showMessage(t('skin.equipSuccess') || '✓ Skin equipped!');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Purchase
|
||||
const result = sm.purchaseSkin(card.id);
|
||||
if (result.success) {
|
||||
this._showMessage(t('skin.purchaseSuccess') || '✓ Skin unlocked!');
|
||||
// Auto-equip after purchase
|
||||
sm.equipSkin(card.id);
|
||||
} else if (result.error === 'Insufficient gold') {
|
||||
this._showMessage(t('currency.insufficient') || 'Insufficient Gold');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Back button
|
||||
if (this._hitTest(tx, ty, this._buttons.back)) {
|
||||
GameGlobal.sceneManager.switchTo(SCENE.MENU);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = SkinScene;
|
||||
@@ -151,19 +151,19 @@ const TeamResultScene = {
|
||||
* @private
|
||||
*/
|
||||
_calculateAndAwardGold() {
|
||||
let gold = 50; // Base reward per requirements
|
||||
let gold = 30; // Base reward (reduced from 50)
|
||||
|
||||
// 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;
|
||||
gold += (stats.kills || 0) * 5;
|
||||
gold += (stats.assists || 0) * 3;
|
||||
}
|
||||
|
||||
// Victory bonus
|
||||
// Victory bonus (reduced from 50)
|
||||
if (this._didWin) {
|
||||
gold += 50;
|
||||
gold += 30;
|
||||
}
|
||||
|
||||
this._goldReward = gold;
|
||||
|
||||
Reference in New Issue
Block a user