Files
tankwar_proj/js/scenes/SkinScene.js
2026-05-02 13:50:52 +08:00

1618 lines
54 KiB
JavaScript

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