/** * SkinScene.js * Tank skin gallery — military-tech themed UI. * Players can preview, purchase, and equip cosmetic tank skins. */ const { SCREEN_WIDTH, SCREEN_HEIGHT, COLORS, SCENE, } = require('../base/GameGlobal'); const { t } = require('../i18n/I18n'); const { drawTankSkin } = require('../entities/TankSkinRenderer'); // ============================================================ // Layout // ============================================================ const COLS = 2; const CARD_GAP = 12; const SIDE_PAD = Math.max(16, (SCREEN_WIDTH - 320) / 2); const GRID_W = SCREEN_WIDTH - SIDE_PAD * 2; const CARD_W = (GRID_W - CARD_GAP) / COLS; const CARD_H = 108; const HEADER_H = SCREEN_HEIGHT * 0.15; const CARDS_START_Y = HEADER_H + 8; const BACK_BTN_W = 110; const BACK_BTN_H = 38; // ============================================================ // Colors & Style // ============================================================ const C = { BG_TOP: '#0b0e17', BG_BOT: '#141b2d', HEADER_LINE: '#e94560', CARD_BG: 'rgba(22, 33, 62, 0.85)', CARD_BORDER: '#1e3054', CARD_EQUIPPED_BG: 'rgba(46, 125, 50, 0.18)', CARD_EQUIPPED_BORDER: '#4CAF50', CARD_LOCKED_BG: 'rgba(15, 20, 35, 0.9)', CARD_LOCKED_BORDER: '#2a2a3a', GOLD: '#FFD700', GOLD_DIM: '#B8860B', TEXT_PRIMARY: '#E8E8E8', TEXT_SECONDARY: '#8899AA', TEXT_MUTED: '#556677', GREEN: '#4CAF50', RED: '#FF4444', ORANGE: '#FF9800', BACK_BTN: '#1a2744', BACK_BTN_BORDER: '#2a4060', TOAST_BG: 'rgba(0,0,0,0.82)', }; // ============================================================ // Scene // ============================================================ const SkinScene = { _buttons: {}, _skinCards: [], _message: '', _messageTimer: 0, _animTimer: 0, // Scroll state _scrollY: 0, _maxScrollY: 0, _touchStartY: 0, _touchLastY: 0, _isDragging: false, _scrollVelocity: 0, enter() { this._message = ''; this._messageTimer = 0; this._animTimer = 0; this._scrollY = 0; this._scrollVelocity = 0; this._isDragging = false; 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 = SIDE_PAD + 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 — fixed at bottom of screen (not scrollable) const BACK_BTN_BOTTOM_PAD = 12; this._buttons = { back: { x: (SCREEN_WIDTH - BACK_BTN_W) / 2, y: SCREEN_HEIGHT - BACK_BTN_H - BACK_BTN_BOTTOM_PAD, w: BACK_BTN_W, h: BACK_BTN_H, }, }; // Calculate max scroll: total content height vs visible area // Visible area ends where the fixed back button starts const visibleBottom = SCREEN_HEIGHT - BACK_BTN_H - BACK_BTN_BOTTOM_PAD - 8; const lastRow = Math.ceil(skins.length / COLS); const contentBottom = CARDS_START_Y + lastRow * (CARD_H + CARD_GAP); this._maxScrollY = Math.max(0, contentBottom - visibleBottom); // Clamp current scroll this._scrollY = Math.min(this._scrollY, this._maxScrollY); }, update(dt) { this._animTimer += dt; if (this._messageTimer > 0) { this._messageTimer -= dt; if (this._messageTimer <= 0) { this._message = ''; } } // Inertia scrolling if (!this._isDragging && Math.abs(this._scrollVelocity) > 0.5) { this._scrollY += this._scrollVelocity * dt; this._scrollVelocity *= 0.92; // friction // Clamp if (this._scrollY < 0) { this._scrollY = 0; this._scrollVelocity = 0; } else if (this._scrollY > this._maxScrollY) { this._scrollY = this._maxScrollY; this._scrollVelocity = 0; } } else if (!this._isDragging) { this._scrollVelocity = 0; } }, // ============================================================ // Render // ============================================================ render(ctx) { this._drawBackground(ctx); const sm = GameGlobal.skinManager; if (!sm) return; const equippedId = sm.getEquippedSkinId(); // Header (fixed, not affected by scroll) this._drawHeader(ctx); // Clip scrollable area (between header and back button) const clipTop = HEADER_H; const clipBottom = this._buttons.back.y - 4; ctx.save(); ctx.beginPath(); ctx.rect(0, clipTop, SCREEN_WIDTH, clipBottom - clipTop); ctx.clip(); // Apply scroll offset for cards ctx.save(); ctx.translate(0, -this._scrollY); // Cards for (const card of this._skinCards) { // Skip cards that are completely off-screen (optimization) const cardScreenY = card.rect.y - this._scrollY; if (cardScreenY + card.rect.h < clipTop - 10 || cardScreenY > clipBottom + 10) continue; this._renderSkinCard(ctx, card, sm.isUnlocked(card.id), equippedId === card.id); } ctx.restore(); // scroll translate ctx.restore(); // clip // Back button (fixed at bottom, not affected by scroll) this._drawBackButton(ctx); // Toast (fixed position, not affected by scroll) this._drawToast(ctx); }, // ---- Background ---- _drawBackground(ctx) { // Gradient background const grad = ctx.createLinearGradient(0, 0, 0, SCREEN_HEIGHT); grad.addColorStop(0, C.BG_TOP); grad.addColorStop(1, C.BG_BOT); ctx.fillStyle = grad; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); // Subtle horizontal scan-lines ctx.globalAlpha = 0.03; ctx.fillStyle = '#FFFFFF'; for (let sy = 0; sy < SCREEN_HEIGHT; sy += 4) { ctx.fillRect(0, sy, SCREEN_WIDTH, 1); } ctx.globalAlpha = 1; }, // ---- Header ---- _drawHeader(ctx) { // Accent line const lineGrad = ctx.createLinearGradient(0, 0, SCREEN_WIDTH, 0); lineGrad.addColorStop(0, 'transparent'); lineGrad.addColorStop(0.3, C.HEADER_LINE); lineGrad.addColorStop(0.7, C.HEADER_LINE); lineGrad.addColorStop(1, 'transparent'); ctx.fillStyle = lineGrad; ctx.fillRect(0, 0, SCREEN_WIDTH, 3); // Title ctx.fillStyle = C.GOLD; ctx.font = 'bold 20px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const titleY = HEADER_H * 0.38; ctx.fillText(t('skin.title') || 'Tank Skins', SCREEN_WIDTH / 2, titleY); // Decorative dashes beside title const tw = ctx.measureText(t('skin.title') || 'Tank Skins').width; ctx.strokeStyle = C.GOLD_DIM; ctx.lineWidth = 1; ctx.globalAlpha = 0.5; const dashW = 30; const dashY = titleY; // left ctx.beginPath(); ctx.moveTo(SCREEN_WIDTH / 2 - tw / 2 - 12, dashY); ctx.lineTo(SCREEN_WIDTH / 2 - tw / 2 - 12 - dashW, dashY); ctx.stroke(); // right ctx.beginPath(); ctx.moveTo(SCREEN_WIDTH / 2 + tw / 2 + 12, dashY); ctx.lineTo(SCREEN_WIDTH / 2 + tw / 2 + 12 + dashW, dashY); ctx.stroke(); ctx.globalAlpha = 1; // Gold balance — pill badge const gold = GameGlobal.currencyManager ? GameGlobal.currencyManager.getGold() : 0; const goldText = `🪙 ${gold}`; ctx.font = 'bold 13px Arial'; const gtw = ctx.measureText(goldText).width; const pillW = gtw + 20; const pillH = 22; const pillX = (SCREEN_WIDTH - pillW) / 2; const pillY = HEADER_H * 0.7 - pillH / 2; // Pill background ctx.fillStyle = 'rgba(255, 215, 0, 0.1)'; ctx.strokeStyle = 'rgba(255, 215, 0, 0.35)'; ctx.lineWidth = 1; this._roundRect(ctx, pillX, pillY, pillW, pillH, pillH / 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = C.GOLD; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(goldText, SCREEN_WIDTH / 2, HEADER_H * 0.7); }, // ---- Skin Card ---- _renderSkinCard(ctx, card, isUnlocked, isEquipped) { const { rect } = card; const r = 10; // Card shadow ctx.fillStyle = 'rgba(0,0,0,0.3)'; this._roundRect(ctx, rect.x + 2, rect.y + 2, rect.w, rect.h, r); ctx.fill(); // Card background if (isEquipped) { ctx.fillStyle = C.CARD_EQUIPPED_BG; ctx.strokeStyle = C.CARD_EQUIPPED_BORDER; ctx.lineWidth = 2; } else if (isUnlocked) { ctx.fillStyle = C.CARD_BG; ctx.strokeStyle = C.CARD_BORDER; ctx.lineWidth = 1.5; } else { ctx.fillStyle = C.CARD_LOCKED_BG; ctx.strokeStyle = C.CARD_LOCKED_BORDER; ctx.lineWidth = 1; } this._roundRect(ctx, rect.x, rect.y, rect.w, rect.h, r); ctx.fill(); ctx.stroke(); // Equipped glow pulse if (isEquipped) { const pulse = 0.12 + Math.sin(this._animTimer * 3) * 0.08; ctx.save(); ctx.shadowColor = C.GREEN; ctx.shadowBlur = 12; ctx.globalAlpha = pulse; ctx.strokeStyle = C.GREEN; ctx.lineWidth = 2; this._roundRect(ctx, rect.x, rect.y, rect.w, rect.h, r); ctx.stroke(); ctx.restore(); } const cx = rect.x + rect.w / 2; // Tank preview area — subtle dark circle backdrop const previewY = rect.y + 34; ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.arc(cx, previewY, 20, 0, Math.PI * 2); ctx.fill(); // Mini tank this._drawMiniTank(ctx, cx, previewY, card, isUnlocked); // Locked overlay icon if (!isUnlocked) { ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.arc(cx, previewY, 20, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = C.TEXT_MUTED; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('🔒', cx, previewY); } // Skin name ctx.fillStyle = isUnlocked ? C.TEXT_PRIMARY : C.TEXT_SECONDARY; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(t(card.nameKey) || card.id, cx, rect.y + 62); // Status / price tag const tagY = rect.y + rect.h - 18; if (isEquipped) { // Green badge const badgeText = t('skin.equipped') || '✓ Equipped'; ctx.font = 'bold 10px Arial'; const bw = ctx.measureText(badgeText).width + 14; const bh = 18; const bx = cx - bw / 2; ctx.fillStyle = 'rgba(76, 175, 80, 0.2)'; ctx.strokeStyle = C.GREEN; ctx.lineWidth = 1; this._roundRect(ctx, bx, tagY - bh / 2, bw, bh, bh / 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = C.GREEN; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(badgeText, cx, tagY); } else if (isUnlocked) { ctx.fillStyle = C.TEXT_MUTED; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(t('skin.owned') || 'Owned', cx, tagY); } else { // Price tag const canAfford = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(card.cost); const priceText = `🪙 ${card.cost}`; ctx.font = 'bold 11px Arial'; const pw = ctx.measureText(priceText).width + 14; const ph = 18; const px = cx - pw / 2; ctx.fillStyle = canAfford ? 'rgba(255, 215, 0, 0.12)' : 'rgba(255, 68, 68, 0.12)'; ctx.strokeStyle = canAfford ? C.GOLD_DIM : C.RED; ctx.lineWidth = 1; this._roundRect(ctx, px, tagY - ph / 2, pw, ph, ph / 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = canAfford ? C.GOLD : C.RED; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(priceText, cx, tagY); } }, // ---- Mini Tank (dispatcher) ---- // ★ Delegates to the shared TankSkinRenderer — SAME drawing code as the // in-game Tank.render() path, so preview and battle look identical. _drawMiniTank(ctx, cx, cy, card, isUnlocked) { ctx.save(); ctx.translate(cx, cy); if (!isUnlocked) ctx.globalAlpha = 0.35; drawTankSkin(ctx, card.id, card.colors, this._animTimer || 0); ctx.restore(); }, // ======== DEFAULT — Classic rounded tank ======== _tankDefault(ctx, bc, tc, kc) { const s = 12; // Tracks ctx.fillStyle = kc; this._roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill(); this._roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill(); // Body ctx.fillStyle = bc; this._roundRect(ctx, -s, -s, s * 2, s * 2, 3); ctx.fill(); // Highlight ctx.fillStyle = 'rgba(255,255,255,0.15)'; ctx.fillRect(-s + 2, -s + 2, s - 2, 3); // Turret base ctx.fillStyle = tc; ctx.beginPath(); ctx.arc(0, 0, s * 0.4, 0, Math.PI * 2); ctx.fill(); // Barrel ctx.fillStyle = tc; this._roundRect(ctx, -2.5, -s - 8, 5, s - 2, 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.fillRect(-1.5, -s - 8, 3, 3); }, // ======== ARCTIC — Ice-crystal armor with snowflake detail ======== _tankArctic(ctx, bc, tc, kc) { const s = 12; // Wide icy tracks with tread marks ctx.fillStyle = kc; ctx.fillRect(-s - 5, -s, 5, s * 2); ctx.fillRect(s, -s, 5, s * 2); // Tread lines ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 0.8; for (let ty = -s + 3; ty < s; ty += 4) { ctx.beginPath(); ctx.moveTo(-s - 5, ty); ctx.lineTo(-s, ty); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, ty); ctx.lineTo(s + 5, ty); ctx.stroke(); } // Body — hexagonal ice shape ctx.fillStyle = bc; ctx.beginPath(); ctx.moveTo(0, -s - 2); ctx.lineTo(s - 1, -s + 4); ctx.lineTo(s - 1, s - 4); ctx.lineTo(0, s + 2); ctx.lineTo(-s + 1, s - 4); ctx.lineTo(-s + 1, -s + 4); ctx.closePath(); ctx.fill(); // Ice shimmer ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.beginPath(); ctx.moveTo(-s + 3, -s + 4); ctx.lineTo(0, -s - 1); ctx.lineTo(2, -s + 5); ctx.closePath(); ctx.fill(); // Snowflake center detail (cross + diagonals) ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, -4); ctx.lineTo(0, 4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-4, 0); ctx.lineTo(4, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-3, -3); ctx.lineTo(3, 3); ctx.stroke(); ctx.beginPath(); ctx.moveTo(3, -3); ctx.lineTo(-3, 3); ctx.stroke(); // Turret — diamond shape ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(0, -5); ctx.lineTo(5, 0); ctx.lineTo(0, 5); ctx.lineTo(-5, 0); ctx.closePath(); ctx.fill(); // Barrel — tapered ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(-3, -s + 3); ctx.lineTo(3, -s + 3); ctx.lineTo(2, -s - 9); ctx.lineTo(-2, -s - 9); ctx.closePath(); ctx.fill(); // Ice tip glow ctx.fillStyle = 'rgba(176,224,230,0.6)'; ctx.beginPath(); ctx.arc(0, -s - 9, 2, 0, Math.PI * 2); ctx.fill(); }, // ======== INFERNO — Aggressive angular flame tank ======== _tankInferno(ctx, bc, tc, kc) { const s = 12; // Spiked tracks ctx.fillStyle = kc; ctx.fillRect(-s - 4, -s, 4, s * 2); ctx.fillRect(s, -s, 4, s * 2); // Track spikes for (let ty = -s + 2; ty < s - 2; ty += 5) { ctx.fillStyle = kc; ctx.beginPath(); ctx.moveTo(-s - 4, ty); ctx.lineTo(-s - 7, ty + 2.5); ctx.lineTo(-s - 4, ty + 5); ctx.fill(); ctx.beginPath(); ctx.moveTo(s + 4, ty); ctx.lineTo(s + 7, ty + 2.5); ctx.lineTo(s + 4, ty + 5); ctx.fill(); } // Body — angular/aggressive shape ctx.fillStyle = bc; ctx.beginPath(); ctx.moveTo(-s + 2, -s - 1); ctx.lineTo(s - 2, -s - 1); ctx.lineTo(s + 1, -s + 4); ctx.lineTo(s + 1, s - 2); ctx.lineTo(s - 3, s + 1); ctx.lineTo(-s + 3, s + 1); ctx.lineTo(-s - 1, s - 2); ctx.lineTo(-s - 1, -s + 4); ctx.closePath(); ctx.fill(); // Flame stripes on body ctx.fillStyle = 'rgba(255,165,0,0.5)'; ctx.beginPath(); ctx.moveTo(-s + 3, s); ctx.lineTo(-s + 6, 0); ctx.lineTo(-s + 3, -2); ctx.lineTo(-s + 1, s - 2); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(s - 3, s); ctx.lineTo(s - 6, 0); ctx.lineTo(s - 3, -2); ctx.lineTo(s + 1, s - 2); ctx.closePath(); ctx.fill(); // Hot glow center ctx.fillStyle = 'rgba(255,69,0,0.3)'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.fill(); // Turret — angular pentagon ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(0, -5); ctx.lineTo(5, -2); ctx.lineTo(4, 4); ctx.lineTo(-4, 4); ctx.lineTo(-5, -2); ctx.closePath(); ctx.fill(); // Barrel — dual cannon ctx.fillStyle = tc; this._roundRect(ctx, -4, -s - 10, 3, s - 1, 1); ctx.fill(); this._roundRect(ctx, 1, -s - 10, 3, s - 1, 1); ctx.fill(); // Muzzle flash dots ctx.fillStyle = 'rgba(255,100,0,0.7)'; ctx.beginPath(); ctx.arc(-2.5, -s - 10, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(2.5, -s - 10, 2, 0, Math.PI * 2); ctx.fill(); }, // ======== PHANTOM — Ghostly translucent stealth tank ======== _tankPhantom(ctx, bc, tc, kc) { const s = 12; const t = this._animTimer || 0; const flicker = 0.6 + Math.sin(t * 5) * 0.15; ctx.globalAlpha = ctx.globalAlpha * flicker; // Slim tracks ctx.fillStyle = kc; this._roundRect(ctx, -s - 3, -s + 2, 3, s * 2 - 4, 1.5); ctx.fill(); this._roundRect(ctx, s, -s + 2, 3, s * 2 - 4, 1.5); ctx.fill(); // Body — sleek elliptical ctx.fillStyle = bc; ctx.beginPath(); ctx.ellipse(0, 0, s - 1, s + 1, 0, 0, Math.PI * 2); ctx.fill(); // Ghost aura rings ctx.strokeStyle = 'rgba(147,112,219,0.3)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.ellipse(0, 0, s + 2, s + 4, 0, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = 'rgba(147,112,219,0.15)'; ctx.beginPath(); ctx.ellipse(0, 0, s + 5, s + 7, 0, 0, Math.PI * 2); ctx.stroke(); // Inner glow ctx.fillStyle = 'rgba(200,180,255,0.2)'; ctx.beginPath(); ctx.ellipse(0, -2, s * 0.5, s * 0.6, 0, 0, Math.PI * 2); ctx.fill(); // Turret — crescent shape ctx.fillStyle = tc; ctx.beginPath(); ctx.arc(0, 0, 5, Math.PI * 0.2, Math.PI * 1.8); ctx.closePath(); ctx.fill(); // Barrel — thin and long ctx.fillStyle = tc; ctx.fillRect(-1.5, -s - 10, 3, s); // Phantom tip — glowing orb ctx.fillStyle = 'rgba(147,112,219,0.8)'; ctx.beginPath(); ctx.arc(0, -s - 11, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.beginPath(); ctx.arc(0, -s - 11, 1, 0, Math.PI * 2); ctx.fill(); }, // ======== JUNGLE — Camo-patterned heavy tank with wide tracks ======== _tankJungle(ctx, bc, tc, kc) { const s = 13; // Extra-wide tracks with tread pattern ctx.fillStyle = kc; this._roundRect(ctx, -s - 6, -s, 6, s * 2, 2); ctx.fill(); this._roundRect(ctx, s, -s, 6, s * 2, 2); ctx.fill(); // Tread chevrons ctx.strokeStyle = 'rgba(0,100,0,0.4)'; ctx.lineWidth = 1; for (let ty = -s + 2; ty < s - 2; ty += 4) { ctx.beginPath(); ctx.moveTo(-s - 6, ty); ctx.lineTo(-s - 3, ty + 2); ctx.lineTo(-s - 6, ty + 4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 6, ty); ctx.lineTo(s + 3, ty + 2); ctx.lineTo(s + 6, ty + 4); ctx.stroke(); } // Body — boxy heavy tank ctx.fillStyle = bc; this._roundRect(ctx, -s, -s, s * 2, s * 2, 2); ctx.fill(); // Camo patches ctx.fillStyle = 'rgba(0,80,0,0.35)'; ctx.beginPath(); ctx.arc(-4, -4, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(34,60,20,0.3)'; ctx.beginPath(); ctx.arc(5, 3, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(0,50,0,0.25)'; ctx.beginPath(); ctx.arc(-2, 6, 3.5, 0, Math.PI * 2); ctx.fill(); // Leaf detail ctx.strokeStyle = 'rgba(0,100,0,0.4)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(6, -8); ctx.quadraticCurveTo(10, -6, 7, -3); ctx.stroke(); ctx.beginPath(); ctx.moveTo(8, -7); ctx.quadraticCurveTo(11, -8, 9, -4); ctx.stroke(); // Turret — square with rounded corners ctx.fillStyle = tc; this._roundRect(ctx, -5, -5, 10, 10, 3); ctx.fill(); // Turret camo dot ctx.fillStyle = 'rgba(0,50,0,0.3)'; ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill(); // Barrel — thick and short ctx.fillStyle = tc; this._roundRect(ctx, -3, -s - 7, 6, s - 3, 2); ctx.fill(); // Muzzle brake ctx.fillStyle = kc; ctx.fillRect(-4, -s - 7, 8, 2); }, // ======== NEON — Glowing wireframe sci-fi tank ======== _tankNeon(ctx, bc, tc, kc) { const s = 12; const t = this._animTimer || 0; const glow = 0.6 + Math.sin(t * 4) * 0.4; // Neon glow aura ctx.save(); ctx.shadowColor = bc; ctx.shadowBlur = 8 * glow; // Tracks — glowing lines ctx.strokeStyle = kc; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-s - 3, -s); ctx.lineTo(-s - 3, s); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 3, -s); ctx.lineTo(s + 3, s); ctx.stroke(); // Track end caps ctx.fillStyle = kc; ctx.beginPath(); ctx.arc(-s - 3, -s, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(-s - 3, s, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(s + 3, -s, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(s + 3, s, 2, 0, Math.PI * 2); ctx.fill(); // Body — wireframe rectangle with glow ctx.strokeStyle = bc; ctx.lineWidth = 1.5; ctx.strokeRect(-s, -s, s * 2, s * 2); // Diagonal cross wires ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(-s, -s); ctx.lineTo(s, s); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, -s); ctx.lineTo(-s, s); ctx.stroke(); // Inner fill (very subtle) ctx.fillStyle = 'rgba(0,255,127,0.08)'; ctx.fillRect(-s, -s, s * 2, s * 2); // Circuit lines ctx.strokeStyle = bc; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(-s, 0); ctx.lineTo(-3, 0); ctx.lineTo(-3, -5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, 0); ctx.lineTo(3, 0); ctx.lineTo(3, 5); ctx.stroke(); // Turret — glowing ring ctx.strokeStyle = tc; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.stroke(); // Center dot ctx.fillStyle = bc; ctx.beginPath(); ctx.arc(0, 0, 2, 0, Math.PI * 2); ctx.fill(); // Barrel — energy beam line ctx.strokeStyle = tc; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -5); ctx.lineTo(0, -s - 10); ctx.stroke(); // Barrel energy nodes ctx.fillStyle = bc; ctx.beginPath(); ctx.arc(0, -s - 2, 1.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(0, -s - 6, 1.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(0, -s - 10, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }, // ======== NEBULA — Cosmic purple tank with starfield and magenta energy ======== _tankNebula(ctx, bc, tc, kc) { const s = 12; const t = this._animTimer || 0; const pulse = 0.7 + Math.sin(t * 3) * 0.3; // Cosmic tracks with star dust ctx.fillStyle = kc; this._roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill(); this._roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill(); // Magenta trim on tracks ctx.strokeStyle = 'rgba(255,0,255,0.45)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(-s - 4, -s + 1); ctx.lineTo(-s - 4, s - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 4, -s + 1); ctx.lineTo(s + 4, s - 1); ctx.stroke(); // Body — rounded cosmic hull ctx.fillStyle = bc; this._roundRect(ctx, -s, -s, s * 2, s * 2, 5); ctx.fill(); // Nebula gradient overlay (top-left bright, bottom-right dark) ctx.fillStyle = 'rgba(180,0,255,0.15)'; ctx.beginPath(); ctx.moveTo(-s, -s); ctx.lineTo(s, -s); ctx.lineTo(-s, s); ctx.closePath(); ctx.fill(); // Starfield particles (twinkling) ctx.save(); const stars = [ { x: -7, y: -8, size: 1.2, phase: 0 }, { x: 5, y: -6, size: 0.9, phase: 1.2 }, { x: -4, y: 3, size: 1.0, phase: 2.4 }, { x: 8, y: 5, size: 0.8, phase: 3.6 }, { x: -2, y: -3, size: 1.1, phase: 4.8 }, { x: 6, y: -1, size: 0.7, phase: 0.8 }, { x: -8, y: 6, size: 0.9, phase: 2.0 }, { x: 3, y: 7, size: 1.0, phase: 3.2 }, ]; for (const star of stars) { const twinkle = 0.4 + Math.sin(t * 5 + star.phase) * 0.6; ctx.globalAlpha = Math.max(0, twinkle); ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2); ctx.fill(); // Star cross sparkle for larger stars if (star.size > 0.9 && twinkle > 0.7) { ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 0.4; ctx.beginPath(); ctx.moveTo(star.x - 2, star.y); ctx.lineTo(star.x + 2, star.y); ctx.stroke(); ctx.beginPath(); ctx.moveTo(star.x, star.y - 2); ctx.lineTo(star.x, star.y + 2); ctx.stroke(); } } ctx.restore(); // Cosmic swirl (subtle rotating nebula cloud) ctx.save(); ctx.globalAlpha = 0.2; ctx.strokeStyle = '#FF00FF'; ctx.lineWidth = 1; ctx.beginPath(); for (let a = 0; a < Math.PI * 4; a += 0.15) { const r = 2 + a * 1.2; const sx = Math.cos(a + t * 0.5) * r; const sy = Math.sin(a + t * 0.5) * r; if (a === 0) ctx.moveTo(sx, sy); else ctx.lineTo(sx, sy); if (r > s - 2) break; } ctx.stroke(); ctx.restore(); // Turret — glowing magenta orb ctx.save(); ctx.shadowColor = '#FF00FF'; ctx.shadowBlur = 6 * pulse; ctx.fillStyle = tc; ctx.beginPath(); ctx.arc(0, 0, 5.5, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // Turret inner ring ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.arc(0, 0, 3.5, 0, Math.PI * 2); ctx.stroke(); // Turret core — bright white-magenta ctx.fillStyle = `rgba(255,200,255,${0.5 + pulse * 0.3})`; ctx.beginPath(); ctx.arc(0, 0, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.beginPath(); ctx.arc(0, 0, 0.8, 0, Math.PI * 2); ctx.fill(); // Barrel — cosmic energy beam ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(-3, -5); ctx.lineTo(3, -5); ctx.lineTo(2, -s - 9); ctx.lineTo(-2, -s - 9); ctx.closePath(); ctx.fill(); // Barrel energy rings ctx.strokeStyle = `rgba(255,0,255,${pulse * 0.6})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-3.5, -s); ctx.lineTo(3.5, -s); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-3, -s - 4); ctx.lineTo(3, -s - 4); ctx.stroke(); // Barrel tip — bright cosmic flare ctx.save(); ctx.shadowColor = '#FF00FF'; ctx.shadowBlur = 8 * pulse; ctx.fillStyle = '#FF00FF'; ctx.beginPath(); ctx.arc(0, -s - 9, 2.8, 0, Math.PI * 2); ctx.fill(); ctx.restore(); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.beginPath(); ctx.arc(0, -s - 9, 1.2, 0, Math.PI * 2); ctx.fill(); }, // ======== ROYAL — Ornate golden tank with crown ======== _tankRoyal(ctx, bc, tc, kc) { const s = 12; // Ornate tracks with gold trim ctx.fillStyle = kc; this._roundRect(ctx, -s - 5, -s + 1, 5, s * 2 - 2, 2); ctx.fill(); this._roundRect(ctx, s, -s + 1, 5, s * 2 - 2, 2); ctx.fill(); // Gold trim lines on tracks ctx.strokeStyle = 'rgba(255,215,0,0.5)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(-s - 5, -s + 1); ctx.lineTo(-s - 5, s - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 5, -s + 1); ctx.lineTo(s + 5, s - 1); ctx.stroke(); // Body — rounded luxury shape ctx.fillStyle = bc; this._roundRect(ctx, -s, -s, s * 2, s * 2, 5); ctx.fill(); // Royal emblem — shield shape in center ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(-4, -5); ctx.lineTo(4, -5); ctx.lineTo(4, 1); ctx.quadraticCurveTo(4, 5, 0, 7); ctx.quadraticCurveTo(-4, 5, -4, 1); ctx.closePath(); ctx.fill(); // Emblem inner ctx.fillStyle = 'rgba(255,215,0,0.6)'; ctx.beginPath(); ctx.moveTo(-2, -3); ctx.lineTo(2, -3); ctx.lineTo(2, 0); ctx.quadraticCurveTo(2, 3, 0, 4); ctx.quadraticCurveTo(-2, 3, -2, 0); ctx.closePath(); ctx.fill(); // Crown on top of turret ctx.fillStyle = '#FFD700'; ctx.beginPath(); ctx.moveTo(-6, -s + 2); ctx.lineTo(-5, -s - 2); ctx.lineTo(-3, -s + 1); ctx.lineTo(0, -s - 4); ctx.lineTo(3, -s + 1); ctx.lineTo(5, -s - 2); ctx.lineTo(6, -s + 2); ctx.closePath(); ctx.fill(); // Crown jewels ctx.fillStyle = '#FF0000'; ctx.beginPath(); ctx.arc(0, -s - 2, 1.2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#0066FF'; ctx.beginPath(); ctx.arc(-4, -s, 0.8, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(4, -s, 0.8, 0, Math.PI * 2); ctx.fill(); // Barrel — ornate with rings ctx.fillStyle = tc; this._roundRect(ctx, -2.5, -s - 10, 5, s - 4, 2); ctx.fill(); // Barrel rings ctx.strokeStyle = '#FFD700'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-3, -s - 3); ctx.lineTo(3, -s - 3); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-3, -s - 7); ctx.lineTo(3, -s - 7); ctx.stroke(); // Royal tip ctx.fillStyle = '#FFD700'; ctx.beginPath(); ctx.arc(0, -s - 10, 2.5, 0, Math.PI * 2); ctx.fill(); }, // ======== SAKURA — Elegant cherry-blossom tank with petal details ======== _tankSakura(ctx, bc, tc, kc) { const s = 12; const t = this._animTimer || 0; // Soft rounded tracks with petal-pink trim ctx.fillStyle = kc; this._roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill(); this._roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill(); // Pink trim highlights ctx.strokeStyle = 'rgba(255,105,180,0.4)'; ctx.lineWidth = 0.7; ctx.beginPath(); ctx.moveTo(-s - 4, -s + 1); ctx.lineTo(-s - 4, s - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 4, -s + 1); ctx.lineTo(s + 4, s - 1); ctx.stroke(); // Body — soft rounded rectangle ctx.fillStyle = bc; this._roundRect(ctx, -s, -s, s * 2, s * 2, 6); ctx.fill(); // Petal shimmer overlay ctx.fillStyle = 'rgba(255,255,255,0.18)'; ctx.beginPath(); ctx.moveTo(-s + 2, -s); ctx.lineTo(s - 2, -s); ctx.lineTo(-s + 2, -s + 8); ctx.closePath(); ctx.fill(); // Floating cherry blossom petals (animated) ctx.save(); ctx.globalAlpha = 0.6; const petals = [ { ox: -6, oy: -7, phase: 0 }, { ox: 7, oy: -4, phase: 1.5 }, { ox: -3, oy: 6, phase: 3 }, { ox: 5, oy: 5, phase: 4.5 }, ]; for (const p of petals) { const drift = Math.sin(t * 2 + p.phase) * 1.5; const px = p.ox + drift; const py = p.oy; ctx.fillStyle = '#FF69B4'; ctx.beginPath(); // 5-petal flower shape for (let i = 0; i < 5; i++) { const angle = (i / 5) * Math.PI * 2 - Math.PI / 2; const pr = 2.2; const fx = px + Math.cos(angle) * pr; const fy = py + Math.sin(angle) * pr; ctx.moveTo(fx, fy); ctx.arc(fx, fy, 1.2, 0, Math.PI * 2); } ctx.fill(); // Flower center ctx.fillStyle = '#FFE4E1'; ctx.beginPath(); ctx.arc(px, py, 0.8, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); // Turret — circular with sakura emblem ctx.fillStyle = tc; ctx.beginPath(); ctx.arc(0, 0, 5.5, 0, Math.PI * 2); ctx.fill(); // Inner petal ring ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.arc(0, 0, 3.5, 0, Math.PI * 2); ctx.stroke(); // Center dot ctx.fillStyle = '#FFE4E1'; ctx.beginPath(); ctx.arc(0, 0, 1.5, 0, Math.PI * 2); ctx.fill(); // Barrel — elegant tapered with pink glow ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(-3, -5); ctx.lineTo(3, -5); ctx.lineTo(2, -s - 9); ctx.lineTo(-2, -s - 9); ctx.closePath(); ctx.fill(); // Barrel tip glow ctx.fillStyle = 'rgba(255,183,197,0.7)'; ctx.beginPath(); ctx.arc(0, -s - 9, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.beginPath(); ctx.arc(0, -s - 9, 1.2, 0, Math.PI * 2); ctx.fill(); }, // ======== THUNDER — Electric-blue lightning tank with arc effects ======== _tankThunder(ctx, bc, tc, kc) { const s = 12; const t = this._animTimer || 0; const flash = 0.7 + Math.sin(t * 8) * 0.3; // Heavy electrified tracks ctx.fillStyle = kc; this._roundRect(ctx, -s - 5, -s, 5, s * 2, 2); ctx.fill(); this._roundRect(ctx, s, -s, 5, s * 2, 2); ctx.fill(); // Electric sparks on tracks ctx.save(); ctx.globalAlpha = flash * 0.6; ctx.strokeStyle = '#00BFFF'; ctx.lineWidth = 0.8; for (let ty = -s + 2; ty < s - 2; ty += 5) { // Left track spark ctx.beginPath(); ctx.moveTo(-s - 5, ty); ctx.lineTo(-s - 3, ty + 1.5); ctx.lineTo(-s - 5, ty + 3); ctx.stroke(); // Right track spark ctx.beginPath(); ctx.moveTo(s + 5, ty); ctx.lineTo(s + 3, ty + 1.5); ctx.lineTo(s + 5, ty + 3); ctx.stroke(); } ctx.restore(); // Body — angular armored shape ctx.fillStyle = bc; ctx.beginPath(); ctx.moveTo(-s + 1, -s - 2); ctx.lineTo(s - 1, -s - 2); ctx.lineTo(s + 2, -s + 3); ctx.lineTo(s + 2, s - 3); ctx.lineTo(s - 1, s + 2); ctx.lineTo(-s + 1, s + 2); ctx.lineTo(-s - 2, s - 3); ctx.lineTo(-s - 2, -s + 3); ctx.closePath(); ctx.fill(); // Lightning bolt emblem on body ctx.fillStyle = 'rgba(0,191,255,0.5)'; ctx.beginPath(); ctx.moveTo(-1, -7); ctx.lineTo(3, -7); ctx.lineTo(0, -1); ctx.lineTo(4, -1); ctx.lineTo(-2, 8); ctx.lineTo(0, 2); ctx.lineTo(-3, 2); ctx.closePath(); ctx.fill(); // Electric aura glow (pulsing) ctx.save(); ctx.shadowColor = '#00BFFF'; ctx.shadowBlur = 6 * flash; // Body edge glow ctx.strokeStyle = 'rgba(0,191,255,0.35)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-s + 1, -s - 2); ctx.lineTo(s - 1, -s - 2); ctx.lineTo(s + 2, -s + 3); ctx.lineTo(s + 2, s - 3); ctx.lineTo(s - 1, s + 2); ctx.lineTo(-s + 1, s + 2); ctx.lineTo(-s - 2, s - 3); ctx.lineTo(-s - 2, -s + 3); ctx.closePath(); ctx.stroke(); // Turret — hexagonal with energy core ctx.fillStyle = tc; ctx.beginPath(); for (let i = 0; i < 6; i++) { const angle = (i / 6) * Math.PI * 2 - Math.PI / 6; const hx = Math.cos(angle) * 5.5; const hy = Math.sin(angle) * 5.5; if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy); } ctx.closePath(); ctx.fill(); // Energy core (pulsing bright center) ctx.fillStyle = `rgba(0,191,255,${0.4 + flash * 0.3})`; ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = `rgba(255,255,255,${0.5 + flash * 0.3})`; ctx.beginPath(); ctx.arc(0, 0, 1.5, 0, Math.PI * 2); ctx.fill(); // Barrel — energy cannon with lightning nodes ctx.fillStyle = tc; this._roundRect(ctx, -2.5, -s - 10, 5, s - 3, 2); ctx.fill(); // Lightning arcs along barrel ctx.strokeStyle = `rgba(0,191,255,${flash * 0.8})`; ctx.lineWidth = 1; // Left arc ctx.beginPath(); ctx.moveTo(-2.5, -s); ctx.lineTo(-5, -s - 3); ctx.lineTo(-2.5, -s - 5); ctx.lineTo(-5, -s - 8); ctx.stroke(); // Right arc ctx.beginPath(); ctx.moveTo(2.5, -s - 1); ctx.lineTo(5, -s - 4); ctx.lineTo(2.5, -s - 6); ctx.lineTo(5, -s - 9); ctx.stroke(); // Barrel tip — bright energy ball ctx.fillStyle = '#00BFFF'; ctx.beginPath(); ctx.arc(0, -s - 10, 3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.beginPath(); ctx.arc(0, -s - 10, 1.5, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }, // ======== DIAMOND — Ultra-luxurious brilliant-cut crystalline tank ======== // Design: Multi-layered prismatic facets with rainbow fire dispersion, // rotating spectral rays, breathing aurora glow, and diamond dust particles. // ======== DIAMOND 💎 — Elegant brilliant-cut gem tank ======== // Inspired by the 💎 emoji: clean geometric facets, ice-blue tones, // gentle luxurious glow — NOT frantic flashing. Slow, elegant, premium. _tankDiamond(ctx, bc, tc, kc) { const s = 12; const t = this._animTimer || 0; // ★ All animation speeds deliberately slow for elegance const breathe = 0.5 + Math.sin(t * 0.8) * 0.5; // very slow breathing (~8s cycle) const shimmer = 0.7 + Math.sin(t * 1.2) * 0.3; // gentle shimmer (~5s cycle) const rotPhase = t * 0.15; // ultra-slow rotation // ── SOFT OUTER GLOW (subtle ice-blue aura) ── ctx.save(); ctx.globalAlpha = 0.06 + breathe * 0.04; ctx.fillStyle = 'rgba(125, 249, 255, 0.2)'; ctx.beginPath(); ctx.arc(0, -2, 20, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(180, 220, 255, 0.15)'; ctx.beginPath(); ctx.arc(0, -2, 16, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // ── SLOW ROTATING PRISMATIC RAYS (very subtle, 6 rays) ── ctx.save(); ctx.globalAlpha = 0.04 + breathe * 0.03; const rayColors = ['#FF9AA2', '#FFD6A5', '#CAFFBF', '#9BF6FF', '#BDB2FF', '#FFC6FF']; for (let i = 0; i < 6; i++) { const angle = rotPhase + (i / 6) * Math.PI * 2; const rayLen = 15 + Math.sin(t * 0.5 + i * 1.0) * 3; ctx.strokeStyle = rayColors[i]; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(Math.cos(angle) * 4, Math.sin(angle) * 4 - 2); ctx.lineTo(Math.cos(angle) * rayLen, Math.sin(angle) * rayLen - 2); ctx.stroke(); } ctx.restore(); // ── CRYSTAL TRACKS ── ctx.fillStyle = kc; this._roundRect(ctx, -s - 5, -s + 1, 5, s * 2 - 2, 2); ctx.fill(); this._roundRect(ctx, s, -s + 1, 5, s * 2 - 2, 2); ctx.fill(); // Gentle edge highlight (steady ice-blue, barely pulsing) ctx.strokeStyle = `rgba(125, 249, 255, ${0.25 + breathe * 0.1})`; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(-s - 5, -s + 1); ctx.lineTo(-s - 5, s - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-s, -s + 1); ctx.lineTo(-s, s - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 5, -s + 1); ctx.lineTo(s + 5, s - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, -s + 1); ctx.lineTo(s, s - 1); ctx.stroke(); // Track crystal segments ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 0.5; for (let ty = -s + 4; ty < s - 2; ty += 4) { ctx.beginPath(); ctx.moveTo(-s - 5, ty); ctx.lineTo(-s, ty); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, ty); ctx.lineTo(s + 5, ty); ctx.stroke(); } // ── BODY — 💎 Brilliant-cut gem shape (12-sided) ── ctx.fillStyle = bc; ctx.beginPath(); ctx.moveTo(-s + 4, -s - 2); ctx.lineTo(s - 4, -s - 2); ctx.lineTo(s, -s + 2); ctx.lineTo(s + 2, -s + 6); ctx.lineTo(s + 2, s - 6); ctx.lineTo(s, s - 2); ctx.lineTo(s - 4, s + 2); ctx.lineTo(-s + 4, s + 2); ctx.lineTo(-s, s - 2); ctx.lineTo(-s - 2, s - 6); ctx.lineTo(-s - 2, -s + 6); ctx.lineTo(-s, -s + 2); ctx.closePath(); ctx.fill(); // ── FACET STRUCTURE (static crystal lines — no animation) ── ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 0.5; // Girdle line ctx.beginPath(); ctx.moveTo(-s - 2, 0); ctx.lineTo(s + 2, 0); ctx.stroke(); // Crown facets ctx.beginPath(); ctx.moveTo(-s + 4, -s - 2); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s - 4, -s - 2); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-s - 2, -s + 6); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 2, -s + 6); ctx.lineTo(0, 0); ctx.stroke(); // Pavilion facets ctx.beginPath(); ctx.moveTo(-s + 4, s + 2); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s - 4, s + 2); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-s - 2, s - 6); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s + 2, s - 6); ctx.lineTo(0, 0); ctx.stroke(); // Secondary facets ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.beginPath(); ctx.moveTo(-s, -s + 2); ctx.lineTo(0, -4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, -s + 2); ctx.lineTo(0, -4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-s, s - 2); ctx.lineTo(0, 4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(s, s - 2); ctx.lineTo(0, 4); ctx.stroke(); // ── FACET COLOR FILLS (very slow spectral drift — like real diamond fire) ── ctx.save(); const facetData = [ { pts: [[-s + 4, -s - 2], [s - 4, -s - 2], [0, 0]], hueBase: 200 }, { pts: [[-s + 4, -s - 2], [-s, -s + 2], [0, 0]], hueBase: 220 }, { pts: [[s - 4, -s - 2], [s, -s + 2], [0, 0]], hueBase: 180 }, { pts: [[-s, -s + 2], [-s - 2, -s + 6], [0, 0]], hueBase: 240 }, { pts: [[s, -s + 2], [s + 2, -s + 6], [0, 0]], hueBase: 160 }, { pts: [[-s + 4, s + 2], [s - 4, s + 2], [0, 0]], hueBase: 260 }, { pts: [[-s + 4, s + 2], [-s, s - 2], [0, 0]], hueBase: 280 }, { pts: [[s - 4, s + 2], [s, s - 2], [0, 0]], hueBase: 300 }, { pts: [[-s, s - 2], [-s - 2, s - 6], [0, 0]], hueBase: 320 }, { pts: [[s, s - 2], [s + 2, s - 6], [0, 0]], hueBase: 340 }, ]; for (const facet of facetData) { // ★ Very slow hue drift: t * 8 (was t * 40) const hue = (facet.hueBase + t * 8) % 360; // ★ Very slow alpha pulse: t * 0.6 (was t * 3) const facetAlpha = 0.05 + Math.sin(t * 0.6 + facet.hueBase * 0.01) * 0.03; ctx.globalAlpha = facetAlpha; ctx.fillStyle = `hsla(${hue}, 70%, 75%, 1)`; ctx.beginPath(); ctx.moveTo(facet.pts[0][0], facet.pts[0][1]); ctx.lineTo(facet.pts[1][0], facet.pts[1][1]); ctx.lineTo(facet.pts[2][0], facet.pts[2][1]); ctx.closePath(); ctx.fill(); } ctx.restore(); // ── TOP CROWN HIGHLIGHT (the "table" — gentle breathing) ── ctx.save(); ctx.globalAlpha = 0.15 + breathe * 0.1; ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.moveTo(-s + 4, -s - 2); ctx.lineTo(s - 4, -s - 2); ctx.lineTo(s * 0.3, -s * 0.3); ctx.lineTo(-s * 0.3, -s * 0.3); ctx.closePath(); ctx.fill(); ctx.restore(); // ── DIAMOND DUST PARTICLES (slow, sparse twinkle) ── ctx.save(); const dustParticles = [ { x: -8, y: -9, size: 0.7, speed: 1.5, phase: 0 }, { x: 9, y: -6, size: 0.5, speed: 1.2, phase: 1.5 }, { x: -6, y: 7, size: 0.6, speed: 1.0, phase: 3.0 }, { x: 7, y: 8, size: 0.5, speed: 1.3, phase: 4.5 }, { x: -10, y: 0, size: 0.5, speed: 0.8, phase: 6.0 }, { x: 10, y: -2, size: 0.6, speed: 1.1, phase: 2.0 }, ]; for (const d of dustParticles) { // ★ Slow twinkle with long visible duration const twinkle = Math.max(0, Math.sin(t * d.speed + d.phase)); if (twinkle > 0.4) { ctx.globalAlpha = (twinkle - 0.4) * 1.2; ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(d.x, d.y, d.size, 0, Math.PI * 2); ctx.fill(); } } ctx.restore(); // ── SPARKLE STARS (slow, elegant cross flares — only 4, staggered) ── ctx.save(); const flares = [ { x: -5, y: -7, phase: 0, len: 2.5 }, { x: 7, y: -4, phase: 2.5, len: 2 }, { x: -3, y: 6, phase: 5.0, len: 2.2 }, { x: 6, y: 5, phase: 7.5, len: 1.8 }, ]; for (const fl of flares) { // ★ Very slow twinkle: t * 0.8 (was t * 4) const twinkle = Math.max(0, Math.sin(t * 0.8 + fl.phase)); if (twinkle > 0.6) { const alpha = (twinkle - 0.6) * 2.5; const color = `rgba(200, 240, 255, ${alpha * 0.8})`; ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 0.5; // 4-point star cross ctx.beginPath(); ctx.moveTo(fl.x - fl.len, fl.y); ctx.lineTo(fl.x + fl.len, fl.y); ctx.stroke(); ctx.beginPath(); ctx.moveTo(fl.x, fl.y - fl.len); ctx.lineTo(fl.x, fl.y + fl.len); ctx.stroke(); // Center dot ctx.beginPath(); ctx.arc(fl.x, fl.y, 0.6, 0, Math.PI * 2); ctx.fill(); } } ctx.restore(); // ── BODY EDGE GLOW (gentle ice-blue border) ── ctx.save(); ctx.shadowColor = '#7DF9FF'; ctx.shadowBlur = 6 + breathe * 4; ctx.strokeStyle = `rgba(125, 249, 255, ${0.2 + breathe * 0.15})`; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.moveTo(-s + 4, -s - 2); ctx.lineTo(s - 4, -s - 2); ctx.lineTo(s, -s + 2); ctx.lineTo(s + 2, -s + 6); ctx.lineTo(s + 2, s - 6); ctx.lineTo(s, s - 2); ctx.lineTo(s - 4, s + 2); ctx.lineTo(-s + 4, s + 2); ctx.lineTo(-s, s - 2); ctx.lineTo(-s - 2, s - 6); ctx.lineTo(-s - 2, -s + 6); ctx.lineTo(-s, -s + 2); ctx.closePath(); ctx.stroke(); ctx.restore(); // Inner crisp edge ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 + shimmer * 0.05})`; ctx.lineWidth = 0.4; ctx.beginPath(); ctx.moveTo(-s + 4, -s - 2); ctx.lineTo(s - 4, -s - 2); ctx.lineTo(s, -s + 2); ctx.lineTo(s + 2, -s + 6); ctx.lineTo(s + 2, s - 6); ctx.lineTo(s, s - 2); ctx.lineTo(s - 4, s + 2); ctx.lineTo(-s + 4, s + 2); ctx.lineTo(-s, s - 2); ctx.lineTo(-s - 2, s - 6); ctx.lineTo(-s - 2, -s + 6); ctx.lineTo(-s, -s + 2); ctx.closePath(); ctx.stroke(); // ── TURRET — 💎 Gem top view (10-pointed star) ── ctx.save(); ctx.shadowColor = '#7DF9FF'; ctx.shadowBlur = 5 + breathe * 3; ctx.fillStyle = tc; ctx.beginPath(); for (let i = 0; i < 10; i++) { const angle = (i / 10) * Math.PI * 2 - Math.PI / 2; const r = i % 2 === 0 ? 7 : 4; const hx = Math.cos(angle) * r; const hy = Math.sin(angle) * r; if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy); } ctx.closePath(); ctx.fill(); ctx.restore(); // Turret facet lines ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 0.4; for (let i = 0; i < 10; i += 2) { const angle = (i / 10) * Math.PI * 2 - Math.PI / 2; const hx = Math.cos(angle) * 7; const hy = Math.sin(angle) * 7; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(hx, hy); ctx.stroke(); } // Turret edge — gentle ice-blue tint (steady, not rainbow) ctx.save(); ctx.strokeStyle = `rgba(125, 249, 255, ${0.2 + breathe * 0.1})`; ctx.lineWidth = 1; ctx.beginPath(); for (let i = 0; i < 10; i++) { const angle = (i / 10) * Math.PI * 2 - Math.PI / 2; const r = i % 2 === 0 ? 7 : 4; const hx = Math.cos(angle) * r; const hy = Math.sin(angle) * r; if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy); } ctx.closePath(); ctx.stroke(); ctx.restore(); // Turret center — white core ctx.save(); ctx.shadowColor = '#FFFFFF'; ctx.shadowBlur = 4 + breathe * 2; ctx.fillStyle = `rgba(255,255,255,${0.5 + breathe * 0.2})`; ctx.beginPath(); ctx.arc(0, 0, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.restore(); ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(0, 0, 1.2, 0, Math.PI * 2); ctx.fill(); // ── BARREL — Clean crystalline cannon ── ctx.fillStyle = tc; ctx.beginPath(); ctx.moveTo(-3.5, -7); ctx.lineTo(3.5, -7); ctx.lineTo(2.5, -s - 11); ctx.lineTo(-2.5, -s - 11); ctx.closePath(); ctx.fill(); // Barrel center line ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(0, -7); ctx.lineTo(0, -s - 11); ctx.stroke(); // Barrel rings (steady ice-blue, gentle pulse) const ringPositions = [-s + 1, -s - 3, -s - 7]; for (let ri = 0; ri < ringPositions.length; ri++) { const ry = ringPositions[ri]; // ★ Slow pulse: t * 1.0 (was t * 5) const ringAlpha = 0.3 + Math.sin(t * 1.0 + ri * 2.0) * 0.15; ctx.strokeStyle = `rgba(125, 249, 255, ${ringAlpha})`; ctx.lineWidth = 1; const halfW = 3.5 - ri * 0.3; ctx.beginPath(); ctx.moveTo(-halfW, ry); ctx.lineTo(halfW, ry); ctx.stroke(); } // ── BARREL TIP — 💎 Diamond shape muzzle ── ctx.save(); ctx.shadowColor = '#7DF9FF'; ctx.shadowBlur = 8 + breathe * 4; ctx.fillStyle = `rgba(125, 249, 255, ${0.6 + breathe * 0.2})`; ctx.beginPath(); ctx.moveTo(0, -s - 15); ctx.lineTo(4, -s - 11); ctx.lineTo(0, -s - 7); ctx.lineTo(-4, -s - 11); ctx.closePath(); ctx.fill(); ctx.restore(); // Tip facet lines ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 0.4; ctx.beginPath(); ctx.moveTo(0, -s - 15); ctx.lineTo(0, -s - 7); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-4, -s - 11); ctx.lineTo(4, -s - 11); ctx.stroke(); // Tip center — white core ctx.save(); ctx.shadowColor = '#FFFFFF'; ctx.shadowBlur = 4 + shimmer * 2; ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(0, -s - 11, 1.2, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // ── SUBTLE SPECTRAL BEAMS from tip (very faint) ── ctx.save(); ctx.globalAlpha = 0.06 + breathe * 0.04; const beamColors = ['#FF9AA2', '#CAFFBF', '#9BF6FF', '#BDB2FF', '#FFC6FF']; for (let bi = 0; bi < beamColors.length; bi++) { const spread = (bi - 2) * 2; ctx.strokeStyle = beamColors[bi]; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(0, -s - 15); ctx.lineTo(spread, -s - 19); ctx.stroke(); } ctx.restore(); }, // ---- Back Button ---- _drawBackButton(ctx) { const btn = this._buttons.back; if (!btn) return; const r = btn.h / 2; // Shadow ctx.fillStyle = 'rgba(0,0,0,0.3)'; this._roundRect(ctx, btn.x + 1, btn.y + 2, btn.w, btn.h, r); ctx.fill(); // Button body ctx.fillStyle = C.BACK_BTN; ctx.strokeStyle = C.BACK_BTN_BORDER; ctx.lineWidth = 1.5; this._roundRect(ctx, btn.x, btn.y, btn.w, btn.h, r); ctx.fill(); ctx.stroke(); // Label ctx.fillStyle = C.TEXT_SECONDARY; ctx.font = 'bold 13px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( t('common.back') || '← Back', btn.x + btn.w / 2, btn.y + btn.h / 2 ); }, // ---- Toast ---- _drawToast(ctx) { if (!this._message) return; // Fade calculation const alpha = Math.min(1, this._messageTimer / 0.3); ctx.globalAlpha = alpha; const msgW = 200; const msgH = 32; const msgX = (SCREEN_WIDTH - msgW) / 2; const msgY = SCREEN_HEIGHT * 0.9; // Toast background ctx.fillStyle = C.TOAST_BG; this._roundRect(ctx, msgX, msgY, msgW, msgH, msgH / 2); ctx.fill(); // Toast border ctx.strokeStyle = 'rgba(255,255,255,0.1)'; ctx.lineWidth = 1; this._roundRect(ctx, msgX, msgY, msgW, msgH, msgH / 2); ctx.stroke(); // Toast text ctx.fillStyle = '#FFFFFF'; ctx.font = '12px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(this._message, SCREEN_WIDTH / 2, msgY + msgH / 2); ctx.globalAlpha = 1; }, // ---- Utility: Rounded Rect ---- _roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); }, // ============================================================ // Touch // ============================================================ _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) { const sm = GameGlobal.skinManager; if (!sm) return; if (eventType === 'touchstart') { const touch = e.touches[0]; this._touchStartY = touch.clientY; this._touchLastY = touch.clientY; this._isDragging = false; this._scrollVelocity = 0; this._touchStartTime = Date.now(); return; } if (eventType === 'touchmove') { const touch = e.touches[0]; const dy = this._touchLastY - touch.clientY; const totalDy = Math.abs(touch.clientY - this._touchStartY); // Start dragging if moved more than 5px if (totalDy > 5) { this._isDragging = true; } if (this._isDragging) { this._scrollY += dy; // Clamp with elastic overscroll feel if (this._scrollY < 0) this._scrollY = 0; if (this._scrollY > this._maxScrollY) this._scrollY = this._maxScrollY; // Track velocity for inertia const now = Date.now(); const elapsed = now - (this._touchMoveTime || now); if (elapsed > 0) { this._scrollVelocity = (dy / elapsed) * 1000; // px per second } this._touchMoveTime = now; } this._touchLastY = touch.clientY; return; } if (eventType === 'touchend') { // If was dragging, just release with inertia if (this._isDragging) { this._isDragging = false; // Velocity is already set from touchmove return; } // Not dragging — treat as tap this._isDragging = false; const touch = e.changedTouches[0]; const tx = touch.clientX; const rawTy = touch.clientY; // Back button uses screen coordinates (fixed position) if (this._hitTest(tx, rawTy, this._buttons.back)) { GameGlobal.sceneManager.switchTo(SCENE.MENU); return; } // Adjust tap Y by scroll offset for scrollable content const ty = rawTy + this._scrollY; // 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!'); sm.equipSkin(card.id); } else if (result.error === 'Insufficient gold') { this._showMessage(t('currency.insufficient') || 'Insufficient Gold'); } } return; } } } }, }; module.exports = SkinScene;