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

1026 lines
37 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* TankSkinRenderer.js
* Shared tank-skin drawing primitives used by BOTH the SkinScene preview
* and the in-game Tank.render() path. This is the SINGLE SOURCE OF TRUTH
* for how each skin looks.
*
* Coordinate contract for every _tankXxx(ctx, bc, tc, kc, t) function:
* • ctx is already translated to the tank center and rotated so that
* the barrel points toward negative-Y (i.e. "up" on screen).
* • All drawings use a DESIGN UNIT of s ≈ 12 pixels (matches the
* SkinScene preview). In-game callers should call ctx.scale(k, k)
* beforehand where k = this.halfSize / DESIGN_HALF_SIZE to make the
* skin match the actual collision box.
* • bc / tc / kc are body / turret / track colors.
* • t is a seconds-based animation timer (optional for static skins).
*/
const DESIGN_HALF_SIZE = 12; // matches the "s" used in most _tankXxx funcs
// ---------------------------------------------------------------
// Rounded-rect helper (local copy so we don't depend on any scene)
// ---------------------------------------------------------------
function _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();
}
// ======== DEFAULT — Classic rounded tank ========
function _tankDefault(ctx, bc, tc, kc /* , t */) {
const s = 12;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
_roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
ctx.fillStyle = bc;
_roundRect(ctx, -s, -s, s * 2, s * 2, 3); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(-s + 2, -s + 2, s - 2, 3);
ctx.fillStyle = tc;
ctx.beginPath(); ctx.arc(0, 0, s * 0.4, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = tc;
_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 ========
function _tankArctic(ctx, bc, tc, kc /* , t */) {
const s = 12;
ctx.fillStyle = kc;
ctx.fillRect(-s - 5, -s, 5, s * 2);
ctx.fillRect(s, -s, 5, s * 2);
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();
}
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();
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();
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();
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();
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();
ctx.fillStyle = 'rgba(176,224,230,0.6)';
ctx.beginPath(); ctx.arc(0, -s - 9, 2, 0, Math.PI * 2); ctx.fill();
}
// ======== INFERNO ========
function _tankInferno(ctx, bc, tc, kc /* , t */) {
const s = 12;
ctx.fillStyle = kc;
ctx.fillRect(-s - 4, -s, 4, s * 2);
ctx.fillRect(s, -s, 4, s * 2);
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();
}
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();
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();
ctx.fillStyle = 'rgba(255,69,0,0.3)';
ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.fill();
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();
ctx.fillStyle = tc;
_roundRect(ctx, -4, -s - 10, 3, s - 1, 1); ctx.fill();
_roundRect(ctx, 1, -s - 10, 3, s - 1, 1); ctx.fill();
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 ========
function _tankPhantom(ctx, bc, tc, kc, t) {
const s = 12;
const tt = t || 0;
const flicker = 0.6 + Math.sin(tt * 5) * 0.15;
ctx.save();
ctx.globalAlpha = ctx.globalAlpha * flicker;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 3, -s + 2, 3, s * 2 - 4, 1.5); ctx.fill();
_roundRect(ctx, s, -s + 2, 3, s * 2 - 4, 1.5); ctx.fill();
ctx.fillStyle = bc;
ctx.beginPath();
ctx.ellipse(0, 0, s - 1, s + 1, 0, 0, Math.PI * 2);
ctx.fill();
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();
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();
ctx.fillStyle = tc;
ctx.beginPath();
ctx.arc(0, 0, 5, Math.PI * 0.2, Math.PI * 1.8);
ctx.closePath();
ctx.fill();
ctx.fillStyle = tc;
ctx.fillRect(-1.5, -s - 10, 3, s);
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();
ctx.restore();
}
// ======== JUNGLE ========
function _tankJungle(ctx, bc, tc, kc /* , t */) {
const s = 13;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 6, -s, 6, s * 2, 2); ctx.fill();
_roundRect(ctx, s, -s, 6, s * 2, 2); ctx.fill();
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();
}
ctx.fillStyle = bc;
_roundRect(ctx, -s, -s, s * 2, s * 2, 2); ctx.fill();
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();
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();
ctx.fillStyle = tc;
_roundRect(ctx, -5, -5, 10, 10, 3); ctx.fill();
ctx.fillStyle = 'rgba(0,50,0,0.3)';
ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = tc;
_roundRect(ctx, -3, -s - 7, 6, s - 3, 2); ctx.fill();
ctx.fillStyle = kc;
ctx.fillRect(-4, -s - 7, 8, 2);
}
// ======== NEON ========
function _tankNeon(ctx, bc, tc, kc, t) {
const s = 12;
const tt = t || 0;
const glow = 0.6 + Math.sin(tt * 4) * 0.4;
ctx.save();
ctx.shadowColor = bc;
ctx.shadowBlur = 8 * glow;
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();
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();
ctx.strokeStyle = bc;
ctx.lineWidth = 1.5;
ctx.strokeRect(-s, -s, s * 2, s * 2);
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();
ctx.fillStyle = 'rgba(255,20,147,0.08)';
ctx.fillRect(-s, -s, s * 2, s * 2);
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();
ctx.strokeStyle = tc;
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = bc;
ctx.beginPath(); ctx.arc(0, 0, 2, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = tc;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, -5); ctx.lineTo(0, -s - 10); ctx.stroke();
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 ========
function _tankNebula(ctx, bc, tc, kc, t) {
const s = 12;
const tt = t || 0;
const pulse = 0.7 + Math.sin(tt * 3) * 0.3;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
_roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
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();
ctx.fillStyle = bc;
_roundRect(ctx, -s, -s, s * 2, s * 2, 5); ctx.fill();
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();
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(tt * 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();
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();
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 + tt * 0.5) * r;
const sy = Math.sin(a + tt * 0.5) * r;
if (a === 0) ctx.moveTo(sx, sy);
else ctx.lineTo(sx, sy);
if (r > s - 2) break;
}
ctx.stroke();
ctx.restore();
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();
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();
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();
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();
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();
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 ========
function _tankRoyal(ctx, bc, tc, kc /* , t */) {
const s = 12;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 5, -s + 1, 5, s * 2 - 2, 2); ctx.fill();
_roundRect(ctx, s, -s + 1, 5, s * 2 - 2, 2); ctx.fill();
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();
ctx.fillStyle = bc;
_roundRect(ctx, -s, -s, s * 2, s * 2, 5); ctx.fill();
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();
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();
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();
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();
ctx.fillStyle = tc;
_roundRect(ctx, -2.5, -s - 10, 5, s - 4, 2); ctx.fill();
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();
ctx.fillStyle = '#FFD700';
ctx.beginPath(); ctx.arc(0, -s - 10, 2.5, 0, Math.PI * 2); ctx.fill();
}
// ======== SAKURA ========
function _tankSakura(ctx, bc, tc, kc, t) {
const s = 12;
const tt = t || 0;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
_roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
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();
ctx.fillStyle = bc;
_roundRect(ctx, -s, -s, s * 2, s * 2, 6); ctx.fill();
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();
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(tt * 2 + p.phase) * 1.5;
const px = p.ox + drift;
const py = p.oy;
ctx.fillStyle = '#FF69B4';
ctx.beginPath();
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();
ctx.fillStyle = '#FFE4E1';
ctx.beginPath(); ctx.arc(px, py, 0.8, 0, Math.PI * 2); ctx.fill();
}
ctx.restore();
ctx.fillStyle = tc;
ctx.beginPath(); ctx.arc(0, 0, 5.5, 0, Math.PI * 2); ctx.fill();
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();
ctx.fillStyle = '#FFE4E1';
ctx.beginPath(); ctx.arc(0, 0, 1.5, 0, Math.PI * 2); ctx.fill();
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();
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 ========
function _tankThunder(ctx, bc, tc, kc, t) {
const s = 12;
const tt = t || 0;
const flash = 0.7 + Math.sin(tt * 8) * 0.3;
ctx.fillStyle = kc;
_roundRect(ctx, -s - 5, -s, 5, s * 2, 2); ctx.fill();
_roundRect(ctx, s, -s, 5, s * 2, 2); ctx.fill();
ctx.save();
ctx.globalAlpha = flash * 0.6;
ctx.strokeStyle = '#00BFFF';
ctx.lineWidth = 0.8;
for (let ty = -s + 2; ty < s - 2; ty += 5) {
ctx.beginPath();
ctx.moveTo(-s - 5, ty);
ctx.lineTo(-s - 3, ty + 1.5);
ctx.lineTo(-s - 5, ty + 3);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(s + 5, ty);
ctx.lineTo(s + 3, ty + 1.5);
ctx.lineTo(s + 5, ty + 3);
ctx.stroke();
}
ctx.restore();
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();
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();
ctx.save();
ctx.shadowColor = '#00BFFF';
ctx.shadowBlur = 6 * flash;
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();
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();
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();
ctx.fillStyle = tc;
_roundRect(ctx, -2.5, -s - 10, 5, s - 3, 2); ctx.fill();
ctx.strokeStyle = `rgba(0,191,255,${flash * 0.8})`;
ctx.lineWidth = 1;
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();
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();
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 💎 ========
// Full crystalline tank — all-diamond body, gem-encrusted tracks,
// hexagonal faceted turret, jewel barrel. Stays within the standard
// [s, +s] design footprint so it matches the size of every other skin.
// Reference: clean icy-blue gem tank, faceted triangular surfaces,
// sapphire track wheels, centered diamond rivet.
function _tankDiamond(ctx, bc, tc, kc, t) {
const s = 12;
const tt = t || 0;
const breathe = 0.5 + Math.sin(tt * 0.5) * 0.5; // very slow breathing
const shimmer = 0.5 + Math.sin(tt * 0.7) * 0.5; // slow shimmer
// Ice-blue palette (reference image):
// light = crystal highlight, mid = surface, deep = shadow / outline
const light = '#EAF6FF';
const midA = '#BCDFFF';
const midB = '#7DC4FF';
const deep = '#1E5B9E';
const edge = '#7DF9FF';
// ── 0. Very faint outer aura (doesn't enlarge footprint) ──
ctx.save();
ctx.globalAlpha = 0.10 + breathe * 0.06;
ctx.fillStyle = edge;
ctx.beginPath();
ctx.arc(0, 0, s + 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// ═══════════════════════════════════════════════════════════
// TRACKS (left & right) — sapphire-bead chains with hub wheels
// ═══════════════════════════════════════════════════════════
const tW = 4; // track half-width
const tx = -s - tW * 0.5; // left track center x
const rx = s + tW * 0.5; // right track center x
const trackTop = -s + 1;
const trackBot = s - 1;
// Track outer shell (dark steel)
ctx.fillStyle = '#0E3562';
ctx.fillRect(tx - tW, trackTop - 0.5, tW * 2, trackBot - trackTop + 1);
ctx.fillRect(rx - tW, trackTop - 0.5, tW * 2, trackBot - trackTop + 1);
// Track top/bottom rounded caps
ctx.beginPath(); ctx.arc(tx, trackTop, tW, Math.PI, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(tx, trackBot, tW, 0, Math.PI); ctx.fill();
ctx.beginPath(); ctx.arc(rx, trackTop, tW, Math.PI, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(rx, trackBot, tW, 0, Math.PI); ctx.fill();
// Gem beads along each track (small blue sapphires)
const beadCount = 5;
for (let i = 0; i < beadCount; i++) {
const by = trackTop + 3 + (trackBot - trackTop - 6) * (i / (beadCount - 1));
for (const cx of [tx, rx]) {
// Bead body (rhombus-ish)
ctx.fillStyle = midB;
ctx.beginPath();
ctx.moveTo(cx, by - 1.6);
ctx.lineTo(cx + 2.2, by);
ctx.lineTo(cx, by + 1.6);
ctx.lineTo(cx - 2.2, by);
ctx.closePath();
ctx.fill();
// Highlight
ctx.fillStyle = light;
ctx.beginPath();
ctx.moveTo(cx, by - 1.6);
ctx.lineTo(cx + 0.8, by - 0.4);
ctx.lineTo(cx - 0.8, by - 0.4);
ctx.closePath();
ctx.fill();
// Dark edge bottom
ctx.fillStyle = deep;
ctx.beginPath();
ctx.moveTo(cx + 2.2, by);
ctx.lineTo(cx, by + 1.6);
ctx.lineTo(cx + 0.4, by + 0.2);
ctx.closePath();
ctx.fill();
}
}
// Hub wheels (big sapphire wheels at each end)
const hubR = 2.6;
for (const cx of [tx, rx]) {
for (const hy of [trackTop, trackBot]) {
// Outer ring
ctx.fillStyle = deep;
ctx.beginPath(); ctx.arc(cx, hy, hubR + 0.6, 0, Math.PI * 2); ctx.fill();
// Gem core
ctx.fillStyle = midB;
ctx.beginPath(); ctx.arc(cx, hy, hubR, 0, Math.PI * 2); ctx.fill();
// Star-cut facets (4-point)
ctx.strokeStyle = 'rgba(255,255,255,0.65)';
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(cx - hubR, hy); ctx.lineTo(cx + hubR, hy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx, hy - hubR); ctx.lineTo(cx, hy + hubR); ctx.stroke();
// Inner highlight
ctx.fillStyle = light;
ctx.beginPath(); ctx.arc(cx - 0.6, hy - 0.6, 0.8, 0, Math.PI * 2); ctx.fill();
}
}
// ═══════════════════════════════════════════════════════════
// BODY — teardrop/bullet silhouette, faceted diamond surface
// Width stays within ±s; height uses the full ±s footprint.
// ═══════════════════════════════════════════════════════════
// Main body outline (rear wider, front slightly narrower — classic tank nose)
const body = [
[-s + 2, -s + 1], // front-left
[ s - 2, -s + 1], // front-right
[ s, -s + 4],
[ s, s - 4],
[ s - 2, s - 1],
[-s + 2, s - 1],
[-s, s - 4],
[-s, -s + 4],
];
// Base fill — soft ice-blue gradient approximation (radial-ish)
ctx.fillStyle = midA;
ctx.beginPath();
ctx.moveTo(body[0][0], body[0][1]);
for (let i = 1; i < body.length; i++) ctx.lineTo(body[i][0], body[i][1]);
ctx.closePath();
ctx.fill();
// Faceted triangles — all emanating from a single "highlight point" so the
// crystal looks like it has a single light source, mimicking the reference.
const hp = [-s + 4, -s + 3]; // highlight point (upper-left region)
const facetRim = [
[-s + 2, -s + 1], [ s - 2, -s + 1], [ s, -s + 4], [ s, s - 4],
[ s - 2, s - 1], [-s + 2, s - 1], [-s, s - 4], [-s, -s + 4],
];
// Triangles from highlight point to each rim edge
for (let i = 0; i < facetRim.length; i++) {
const a = facetRim[i];
const b = facetRim[(i + 1) % facetRim.length];
// Shade based on angle away from light point
const mx = (a[0] + b[0]) * 0.5;
const my = (a[1] + b[1]) * 0.5;
const dx = mx - hp[0];
const dy = my - hp[1];
const dist = Math.sqrt(dx * dx + dy * dy);
// Nearer to hp → brighter, farther → deeper blue
const tBright = Math.max(0, 1 - dist / 22);
const colA = tBright > 0.6 ? light : tBright > 0.3 ? midA : midB;
ctx.fillStyle = colA;
ctx.globalAlpha = 0.55;
ctx.beginPath();
ctx.moveTo(hp[0], hp[1]);
ctx.lineTo(a[0], a[1]);
ctx.lineTo(b[0], b[1]);
ctx.closePath();
ctx.fill();
}
ctx.globalAlpha = 1;
// Secondary facet lines — subtle geometric net
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.lineWidth = 0.4;
// From highlight point to every rim vertex
for (const p of facetRim) {
ctx.beginPath(); ctx.moveTo(hp[0], hp[1]); ctx.lineTo(p[0], p[1]); ctx.stroke();
}
// Rim outline (crisp inner)
ctx.strokeStyle = 'rgba(255,255,255,0.55)';
ctx.lineWidth = 0.6;
ctx.beginPath();
ctx.moveTo(body[0][0], body[0][1]);
for (let i = 1; i < body.length; i++) ctx.lineTo(body[i][0], body[i][1]);
ctx.closePath();
ctx.stroke();
// Deep blue body outline (crisp edge)
ctx.strokeStyle = deep;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo(body[0][0], body[0][1]);
for (let i = 1; i < body.length; i++) ctx.lineTo(body[i][0], body[i][1]);
ctx.closePath();
ctx.stroke();
// ── Centered "diamond rivet" — the signature small gem on the hull ──
ctx.save();
ctx.fillStyle = midB;
ctx.beginPath();
ctx.moveTo(0, -2.8);
ctx.lineTo(2.4, 0);
ctx.lineTo(0, 2.8);
ctx.lineTo(-2.4, 0);
ctx.closePath();
ctx.fill();
// Rivet facet lines
ctx.strokeStyle = 'rgba(255,255,255,0.7)';
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(0, -2.8); ctx.lineTo(0, 2.8); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-2.4, 0); ctx.lineTo(2.4, 0); ctx.stroke();
// Rivet highlight
ctx.fillStyle = light;
ctx.beginPath();
ctx.moveTo(0, -2.8); ctx.lineTo(0.9, -1); ctx.lineTo(-0.9, -1);
ctx.closePath();
ctx.fill();
// Rivet dark outline
ctx.strokeStyle = deep;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, -2.8); ctx.lineTo(2.4, 0); ctx.lineTo(0, 2.8);
ctx.lineTo(-2.4, 0); ctx.closePath();
ctx.stroke();
ctx.restore();
// ═══════════════════════════════════════════════════════════
// TURRET — hexagonal gem prism (top view) sitting on the hull
// ═══════════════════════════════════════════════════════════
const turretR = 6.5;
const turretRy = 5.5; // slightly squashed vertically (perspective feel)
// Turret base ring (darker blue outline for contrast with body)
ctx.fillStyle = deep;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const ang = (i / 6) * Math.PI * 2 - Math.PI / 2;
const hx = Math.cos(ang) * (turretR + 0.6);
const hy = Math.sin(ang) * (turretRy + 0.6) - 1;
if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
}
ctx.closePath();
ctx.fill();
// Turret body (6-sided gem)
const turretPts = [];
for (let i = 0; i < 6; i++) {
const ang = (i / 6) * Math.PI * 2 - Math.PI / 2;
turretPts.push([Math.cos(ang) * turretR, Math.sin(ang) * turretRy - 1]);
}
ctx.fillStyle = midA;
ctx.beginPath();
ctx.moveTo(turretPts[0][0], turretPts[0][1]);
for (let i = 1; i < 6; i++) ctx.lineTo(turretPts[i][0], turretPts[i][1]);
ctx.closePath();
ctx.fill();
// Turret facets from center
const tHp = [-1.8, -2.5];
for (let i = 0; i < 6; i++) {
const a = turretPts[i];
const b = turretPts[(i + 1) % 6];
const mx = (a[0] + b[0]) * 0.5;
const my = (a[1] + b[1]) * 0.5;
const d = Math.sqrt((mx - tHp[0]) ** 2 + (my - tHp[1]) ** 2);
const tBright = Math.max(0, 1 - d / 10);
ctx.fillStyle = tBright > 0.55 ? light : tBright > 0.25 ? midA : midB;
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.moveTo(tHp[0], tHp[1]);
ctx.lineTo(a[0], a[1]);
ctx.lineTo(b[0], b[1]);
ctx.closePath();
ctx.fill();
}
ctx.globalAlpha = 1;
// Turret facet lines
ctx.strokeStyle = 'rgba(255,255,255,0.55)';
ctx.lineWidth = 0.5;
for (const p of turretPts) {
ctx.beginPath(); ctx.moveTo(tHp[0], tHp[1]); ctx.lineTo(p[0], p[1]); ctx.stroke();
}
// Turret crisp outline
ctx.strokeStyle = deep;
ctx.lineWidth = 0.7;
ctx.beginPath();
ctx.moveTo(turretPts[0][0], turretPts[0][1]);
for (let i = 1; i < 6; i++) ctx.lineTo(turretPts[i][0], turretPts[i][1]);
ctx.closePath();
ctx.stroke();
// Turret crown highlight band
ctx.fillStyle = `rgba(255,255,255,${0.25 + breathe * 0.12})`;
ctx.beginPath();
ctx.ellipse(-1, -3.8, 3.2, 1.3, -0.3, 0, Math.PI * 2);
ctx.fill();
// Turret center (small cut-gem apex)
ctx.fillStyle = midB;
ctx.beginPath();
ctx.arc(tHp[0], tHp[1], 1.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.arc(tHp[0] - 0.3, tHp[1] - 0.3, 0.5, 0, Math.PI * 2);
ctx.fill();
// ═══════════════════════════════════════════════════════════
// BARREL — hex-prism gem cannon with metal band + glowing muzzle
// ═══════════════════════════════════════════════════════════
const barrelBase = -3; // at turret edge
const barrelTip = -s - 4; // doesn't exceed footprint much
const barrelHalfW = 2.2;
// Barrel body — faceted
ctx.fillStyle = midA;
ctx.beginPath();
ctx.moveTo(-barrelHalfW, barrelBase);
ctx.lineTo( barrelHalfW, barrelBase);
ctx.lineTo( barrelHalfW * 0.8, barrelTip);
ctx.lineTo(-barrelHalfW * 0.8, barrelTip);
ctx.closePath();
ctx.fill();
// Barrel left/right facet shade
ctx.fillStyle = midB;
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.moveTo( barrelHalfW, barrelBase);
ctx.lineTo( barrelHalfW * 0.8, barrelTip);
ctx.lineTo( 0, barrelTip);
ctx.lineTo( 0, barrelBase);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
// Barrel center highlight stripe
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(-0.6, barrelBase);
ctx.lineTo(-0.5, barrelTip);
ctx.stroke();
// Barrel outline
ctx.strokeStyle = deep;
ctx.lineWidth = 0.7;
ctx.beginPath();
ctx.moveTo(-barrelHalfW, barrelBase);
ctx.lineTo( barrelHalfW, barrelBase);
ctx.lineTo( barrelHalfW * 0.8, barrelTip);
ctx.lineTo(-barrelHalfW * 0.8, barrelTip);
ctx.closePath();
ctx.stroke();
// Metal collar near turret (dark band)
ctx.fillStyle = deep;
ctx.fillRect(-barrelHalfW - 0.4, barrelBase - 0.4, (barrelHalfW + 0.4) * 2, 1.4);
ctx.fillStyle = '#3E88C8';
ctx.fillRect(-barrelHalfW - 0.4, barrelBase - 0.1, (barrelHalfW + 0.4) * 2, 0.4);
// Muzzle ring
ctx.fillStyle = deep;
ctx.fillRect(-barrelHalfW * 0.9, barrelTip - 0.8, barrelHalfW * 1.8, 1.2);
// Glowing gem muzzle
ctx.save();
ctx.shadowColor = edge;
ctx.shadowBlur = 5 + breathe * 3;
ctx.fillStyle = light;
ctx.beginPath();
ctx.arc(0, barrelTip - 0.4, 1.4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.arc(0, barrelTip - 0.4, 0.7, 0, Math.PI * 2);
ctx.fill();
// ═══════════════════════════════════════════════════════════
// SPARKLES — 4 sparse 4-point stars, very slow twinkle
// ═══════════════════════════════════════════════════════════
ctx.save();
const flares = [
{ x: -s + 3, y: -s + 2, phase: 0, len: 2.2 },
{ x: s - 4, y: -4, phase: 2.0, len: 1.8 },
{ x: -4, y: s - 3, phase: 4.0, len: 2.0 },
{ x: s - 2, y: s - 6, phase: 6.0, len: 1.6 },
];
for (const fl of flares) {
const tw = Math.max(0, Math.sin(tt * 0.6 + fl.phase));
if (tw > 0.65) {
const a = (tw - 0.65) * 3;
const col = `rgba(255,255,255,${Math.min(1, a)})`;
ctx.strokeStyle = col;
ctx.fillStyle = col;
ctx.lineWidth = 0.5;
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();
ctx.beginPath(); ctx.arc(fl.x, fl.y, 0.6, 0, Math.PI * 2); ctx.fill();
}
}
ctx.restore();
// ── Very subtle shimmer band sliding across body (crystal gleam) ──
ctx.save();
ctx.globalAlpha = 0.12 * shimmer;
ctx.fillStyle = '#FFFFFF';
const gleamY = -s + (s * 2) * ((tt * 0.25) % 1);
ctx.beginPath();
ctx.moveTo(-s + 2, gleamY);
ctx.lineTo( s - 2, gleamY - 3);
ctx.lineTo( s - 2, gleamY - 1);
ctx.lineTo(-s + 2, gleamY + 2);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// ---------------------------------------------------------------
// Dispatcher — the ONE function every caller should use.
// @param {CanvasRenderingContext2D} ctx — already translated+rotated to tank
// @param {string} skinId — 'default', 'arctic', ... 'diamond'
// @param {object|null} colors — { body, turret, track } (null = default)
// @param {number} t — animation timer in seconds
// ---------------------------------------------------------------
const FALLBACK_COLORS = { body: '#FFD700', turret: '#B8860B', track: '#8B6914' };
function drawTankSkin(ctx, skinId, colors, t) {
const c = colors || FALLBACK_COLORS;
const bc = c.body || FALLBACK_COLORS.body;
const tc = c.turret || FALLBACK_COLORS.turret;
const kc = c.track || FALLBACK_COLORS.track;
const tt = t || 0;
switch (skinId) {
case 'arctic': return _tankArctic(ctx, bc, tc, kc, tt);
case 'inferno': return _tankInferno(ctx, bc, tc, kc, tt);
case 'phantom': return _tankPhantom(ctx, bc, tc, kc, tt);
case 'jungle': return _tankJungle(ctx, bc, tc, kc, tt);
case 'neon': return _tankNeon(ctx, bc, tc, kc, tt);
case 'nebula': return _tankNebula(ctx, bc, tc, kc, tt);
case 'royal': return _tankRoyal(ctx, bc, tc, kc, tt);
case 'sakura': return _tankSakura(ctx, bc, tc, kc, tt);
case 'thunder': return _tankThunder(ctx, bc, tc, kc, tt);
case 'diamond': return _tankDiamond(ctx, bc, tc, kc, tt);
case 'default':
default: return _tankDefault(ctx, bc, tc, kc, tt);
}
}
module.exports = {
drawTankSkin,
DESIGN_HALF_SIZE,
};