/** * 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;