/* ============================================================ * Diablo2D — sprites.js : procedural baked art (tiles, props, icons) * ============================================================ */ 'use strict'; window.D2 = window.D2 || {}; (function (D2) { const TW = 64, TH = 32; // iso tile diamond size const WALL_H = 46; const cache = { themes: {}, // themeId -> {floors[], wallBlock} props: {}, // key -> canvas skills: {}, // iconId -> canvas }; function mkCanvas(w, h) { const c = document.createElement('canvas'); c.width = w; c.height = h; return c; } /* ---------------- iso helpers ---------------- */ function diamondPath(ctx, cx, cy, hw, hh) { ctx.beginPath(); ctx.moveTo(cx, cy - hh); ctx.lineTo(cx + hw, cy); ctx.lineTo(cx, cy + hh); ctx.lineTo(cx - hw, cy); ctx.closePath(); } /* ---------------- floor tiles ---------------- */ function bakeFloorTile(theme, colA, variant) { const c = mkCanvas(TW, TH); const ctx = c.getContext('2d'); const shade = [1, 0.92, 1.06, 0.85][variant]; const base = D2.util.shade(colA, shade); diamondPath(ctx, TW / 2, TH / 2, TW / 2, TH / 2); ctx.fillStyle = base; ctx.fill(); /* subtle inner texture */ ctx.save(); diamondPath(ctx, TW / 2, TH / 2, TW / 2, TH / 2); ctx.clip(); const rnd = new D2.util.RNG(variant * 7919 + colA.length * 131); ctx.globalAlpha = 0.14; for (let i = 0; i < 10; i++) { const x = rnd.range(6, TW - 6), y = rnd.range(4, TH - 4); ctx.fillStyle = i % 2 ? '#000' : '#fff'; ctx.fillRect(x, y, rnd.range(3, 10), 1); } /* grout lines */ ctx.globalAlpha = 0.25; ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(TW / 2, 0); ctx.lineTo(TW, TH / 2); ctx.moveTo(TW / 2, TH); ctx.lineTo(0, TH / 2); ctx.stroke(); /* cracks on some variants */ if (variant === 2 || variant === 3) { ctx.globalAlpha = 0.3; ctx.beginPath(); let x = TW * 0.3, y = TH * 0.5; ctx.moveTo(x, y); for (let i = 0; i < 4; i++) { x += rnd.range(-8, 12); y += rnd.range(-6, 6); ctx.lineTo(x, y); } ctx.stroke(); } ctx.restore(); return c; } function bakeWallBlock(theme) { const c = mkCanvas(TW + 4, WALL_H + TH + 4); const ctx = c.getContext('2d'); const ox = 2, oy = WALL_H + 2; /* front-left face (SW) */ ctx.fillStyle = theme.wallFace; ctx.beginPath(); ctx.moveTo(ox, oy - TH / 2); ctx.lineTo(ox + TW / 2, oy); ctx.lineTo(ox + TW / 2, oy - WALL_H); ctx.lineTo(ox, oy - WALL_H - TH / 2); ctx.closePath(); ctx.fill(); /* front-right face (SE) */ ctx.fillStyle = D2.util.shade(theme.wallFace, 0.72); ctx.beginPath(); ctx.moveTo(ox + TW / 2, oy); ctx.lineTo(ox + TW, oy - TH / 2); ctx.lineTo(ox + TW, oy - TH / 2 - WALL_H); ctx.lineTo(ox + TW / 2, oy - WALL_H); ctx.closePath(); ctx.fill(); /* top */ diamondPath(ctx, ox + TW / 2, oy - WALL_H - TH / 2 + TH / 2, TW / 2, TH / 2); ctx.fillStyle = theme.wallTop; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.stroke(); /* brick lines on faces */ ctx.strokeStyle = 'rgba(0,0,0,.28)'; ctx.lineWidth = 1; for (let row = 1; row < 4; row++) { const yy = oy - (row / 4) * WALL_H; ctx.beginPath(); ctx.moveTo(ox, yy - TH / 2 * (1 - row / 4)); ctx.lineTo(ox + TW / 2, yy); ctx.stroke(); ctx.beginPath(); ctx.moveTo(ox + TW / 2, yy); ctx.lineTo(ox + TW, yy - TH / 2 * (1 - row / 4)); ctx.stroke(); } return c; } /* ---------------- props ---------------- */ function bakeProps() { const P = cache.props; /* barrel */ let c = mkCanvas(40, 46); let x = c.getContext('2d'); barrelShape(x, 20, 30, '#6a4a2a', '#4a3018'); P.barrel = c; /* urn */ c = mkCanvas(36, 42); x = c.getContext('2d'); x.fillStyle = '#8a7a5e'; x.beginPath(); x.ellipse(18, 26, 11, 13, 0, 0, Math.PI * 2); x.fill(); x.fillStyle = '#a89a78'; x.beginPath(); x.ellipse(18, 16, 7, 5, 0, 0, Math.PI * 2); x.fill(); x.fillStyle = '#3a3020'; x.fillRect(14, 8, 8, 5); highlight(x, 13, 20, 5); P.urn = c; /* chest closed / open */ c = mkCanvas(48, 42); x = c.getContext('2d'); x.fillStyle = '#5e3f22'; roundRect(x, 6, 18, 36, 20, 3); x.fill(); x.fillStyle = '#7a5530'; roundRect(x, 6, 8, 36, 14, 6); x.fill(); x.strokeStyle = '#c8a35a'; x.lineWidth = 2; x.strokeRect(6, 18, 36, 1); x.fillStyle = '#c8a35a'; x.fillRect(21, 16, 6, 8); metalEdges(x, 6, 8, 36, 30); P.chest_closed = c; c = mkCanvas(48, 42); x = c.getContext('2d'); x.fillStyle = '#5e3f22'; roundRect(x, 6, 18, 36, 20, 3); x.fill(); x.fillStyle = '#241505'; x.fillRect(8, 14, 32, 8); x.fillStyle = '#7a5530'; roundRect(x, 4, 4, 40, 10, 5); x.fill(); metalEdges(x, 6, 8, 36, 30); P.chest_open = c; /* shrine (rune glow added at runtime) */ c = mkCanvas(52, 60); x = c.getContext('2d'); x.fillStyle = '#5a564e'; x.beginPath(); x.moveTo(26, 6); x.lineTo(42, 20); x.lineTo(38, 54); x.lineTo(14, 54); x.lineTo(10, 20); x.closePath(); x.fill(); x.fillStyle = '#6c685e'; x.beginPath(); x.moveTo(26, 6); x.lineTo(34, 14); x.lineTo(22, 22); x.closePath(); x.fill(); P.shrine = c; /* torch bracket (flame drawn dynamically) */ c = mkCanvas(24, 44); x = c.getContext('2d'); x.fillStyle = '#3a2c1c'; x.fillRect(10, 18, 5, 22); x.fillStyle = '#55432c'; x.fillRect(7, 14, 11, 8); P.torch = c; /* stairs down (dark pit with steps) */ c = mkCanvas(TW, TH + 18); x = c.getContext('2d'); x.save(); x.translate(TW / 2, 9); diamondPath(x, 0, 0, TW / 2 - 2, TH / 2 - 1); x.fillStyle = '#0c0906'; x.fill(); x.clip(); for (let i = 0; i < 5; i++) { diamondPath(x, 0, -i * 3.5, (TW / 2 - 3) * (1 - i * 0.09), (TH / 2 - 1) * (1 - i * 0.09)); x.fillStyle = ['#181310', '#221b15', '#2c231a', '#372c1f', '#43362a'][i]; x.fill(); } x.restore(); P.stairs_down = c; /* stairs up */ c = mkCanvas(TW, TH + 18); x = c.getContext('2d'); x.save(); x.translate(TW / 2, 9); diamondPath(x, 0, 0, TW / 2 - 2, TH / 2 - 1); x.fillStyle = '#d8cba8'; x.fill(); x.clip(); for (let i = 0; i < 5; i++) { diamondPath(x, 0, i * 3.5, (TW / 2 - 3) * (1 - i * 0.09), (TH / 2 - 1) * (1 - i * 0.09)); x.fillStyle = ['#cfc2a0', '#bfb294', '#afa286', '#9f9278', '#8f826a'][i]; x.fill(); } x.restore(); P.stairs_up = c; /* bones decor */ c = mkCanvas(36, 20); x = c.getContext('2d'); x.strokeStyle = '#cfc7ae'; x.lineWidth = 3; x.lineCap = 'round'; x.beginPath(); x.moveTo(6, 14); x.lineTo(28, 8); x.stroke(); x.beginPath(); x.moveTo(10, 6); x.lineTo(24, 15); x.stroke(); x.fillStyle = '#cfc7ae'; x.beginPath(); x.arc(29, 7, 4, 0, Math.PI * 2); x.fill(); x.fillStyle = '#222'; x.fillRect(28, 5, 2, 2); x.fillRect(31, 6, 2, 2); P.bones = c; /* waypoint stone */ c = mkCanvas(48, 62); x = c.getContext('2d'); x.fillStyle = '#4e5a66'; x.beginPath(); x.moveTo(24, 4); x.lineTo(38, 16); x.lineTo(34, 56); x.lineTo(14, 56); x.lineTo(10, 16); x.closePath(); x.fill(); x.fillStyle = '#61707e'; x.beginPath(); x.moveTo(24, 4); x.lineTo(31, 12); x.lineTo(17, 20); x.closePath(); x.fill(); x.strokeStyle = '#8ac4ff'; x.lineWidth = 2; x.beginPath(); x.arc(24, 30, 8, 0, Math.PI * 2); x.stroke(); x.beginPath(); x.moveTo(24, 22); x.lineTo(24, 38); x.moveTo(17, 30); x.lineTo(31, 30); x.stroke(); P.waypoint = c; /* NPCs */ c = mkCanvas(52, 58); x = c.getContext('2d'); // blacksmith humanoid(x, 26, 34, { body: '#5a4a3a', accent: '#8a6a3a', cloth: '#3a2c1c' }, 1.15); x.fillStyle = '#333'; x.fillRect(38, 40, 14, 8); // anvil block x.fillStyle = '#555'; x.fillRect(36, 36, 18, 5); P.npc_smith = c; c = mkCanvas(52, 58); x = c.getContext('2d'); // healer humanoid(x, 26, 34, { body: '#d8d4c8', accent: '#e8c87b', cloth: '#b8b4a8' }, 1.1); x.strokeStyle = '#e86a5a'; x.lineWidth = 3; x.beginPath(); x.moveTo(26, 22); x.lineTo(26, 40); x.moveTo(18, 30); x.lineTo(34, 30); x.stroke(); P.npc_healer = c; c = mkCanvas(52, 58); x = c.getContext('2d'); // stash keeper chest x.fillStyle = '#4a3a26'; roundRect(x, 10, 22, 32, 26, 4); x.fill(); x.fillStyle = '#5e4a30'; roundRect(x, 10, 14, 32, 12, 5); x.fill(); x.fillStyle = '#c8a35a'; x.fillRect(23, 20, 6, 10); metalEdges(x, 10, 14, 32, 34); P.npc_stash = c; c = mkCanvas(52, 58); x = c.getContext('2d'); // Charsi the smith (renamed key) humanoid(x, 26, 34, { body: '#8a6a52', accent: '#c89a5a', cloth: '#4a3626' }, 1.15); x.fillStyle = '#333'; x.fillRect(38, 40, 14, 8); x.fillStyle = '#555'; x.fillRect(36, 36, 18, 5); P.npc_charsi = P.npc_smith; c = mkCanvas(52, 58); x = c.getContext('2d'); // Akara — robed priestess humanoid(x, 26, 34, { body: '#c8b090', accent: '#e86a5a', cloth: '#7a2c3a' }, 1.05); x.fillStyle = '#e8d8a8'; x.beginPath(); x.moveTo(26, 14); x.lineTo(34, 22); x.lineTo(18, 22); x.closePath(); x.fill(); x.strokeStyle = '#f0e0b0'; x.lineWidth = 2; x.beginPath(); x.moveTo(26, 24); x.lineTo(26, 44); x.stroke(); P.npc_akara = c; c = mkCanvas(52, 58); x = c.getContext('2d'); // Kashya — rogue captain humanoid(x, 26, 34, { body: '#7a6a5a', accent: '#a83a3a', cloth: '#3a3236' }, 1.1); x.strokeStyle = '#9a8a6a'; x.lineWidth = 3; x.beginPath(); x.moveTo(40, 10); x.quadraticCurveTo(46, 26, 42, 44); x.stroke(); // bow x.fillStyle = '#8a2c2c'; x.beginPath(); x.moveTo(20, 16); x.lineTo(32, 16); x.lineTo(26, 10); x.closePath(); x.fill(); P.npc_kashya = c; c = mkCanvas(52, 58); x = c.getContext('2d'); // Deckard Cain — elder scholar humanoid(x, 26, 34, { body: '#c0b09a', accent: '#d8cba8', cloth: '#584a38' }, 0.92); x.fillStyle = '#e8e0d0'; x.beginPath(); x.arc(26, 18, 9, Math.PI * 0.15, Math.PI * 0.85); x.fill(); // white beard x.fillStyle = '#6a5030'; x.fillRect(14, 30, 12, 9); x.fillStyle = '#e8dcc0'; x.fillRect(15, 31, 10, 7); // open book P.npc_cain = c; c = mkCanvas(52, 58); x = c.getContext('2d'); // Gheed — rotund merchant humanoid(x, 26, 36, { body: '#b08a62', accent: '#d8b04a', cloth: '#6a4a2c' }, 1.28); x.fillStyle = '#e8c84a'; x.beginPath(); x.arc(26, 30, 6, 0, Math.PI * 2); x.fill(); // coin pouch glint x.strokeStyle = '#caa232'; x.lineWidth = 2; x.beginPath(); x.moveTo(33, 34); x.lineTo(39, 40); x.stroke(); P.npc_gheed = c; } function barrelShape(x, cx, baseY, main, dark) { x.fillStyle = dark; x.beginPath(); x.ellipse(cx, baseY - 4, 13, 5, 0, 0, Math.PI * 2); x.fill(); x.fillStyle = main; x.beginPath(); x.moveTo(cx - 12, baseY - 4); x.bezierCurveTo(cx - 16, baseY - 16, cx - 16, baseY - 26, cx - 11, baseY - 34); x.lineTo(cx + 11, baseY - 34); x.bezierCurveTo(cx + 16, baseY - 26, cx + 16, baseY - 16, cx + 12, baseY - 4); x.closePath(); x.fill(); x.strokeStyle = '#3a2812'; x.lineWidth = 2; x.beginPath(); x.moveTo(cx - 13, baseY - 12); x.quadraticCurveTo(cx, baseY - 8, cx + 13, baseY - 12); x.stroke(); x.beginPath(); x.moveTo(cx - 13, baseY - 26); x.quadraticCurveTo(cx, baseY - 22, cx + 13, baseY - 26); x.stroke(); highlight(x, cx - 6, baseY - 22, 4); } function humanoid(x, cx, baseY, pal, s = 1) { const S = v => v * s; x.fillStyle = pal.cloth; roundRect(x, cx - S(8), baseY - S(26), S(16), S(20), S(4)); x.fill(); // torso x.fillStyle = pal.body; x.beginPath(); x.arc(cx, baseY - S(32), S(6.5), 0, Math.PI * 2); x.fill(); // head roundRect(x, cx - S(11), baseY - S(8), S(6), S(9), 2); x.fill(); // legs roundRect(x, cx + S(5), baseY - S(8), S(6), S(9), 2); x.fill(); x.fillStyle = pal.accent; roundRect(x, cx - S(10), baseY - S(22), S(20), S(4), 2); x.fill(); // belt } function metalEdges(x, rx, ry, rw, rh) { x.strokeStyle = 'rgba(200,163,90,.5)'; x.lineWidth = 1.5; x.strokeRect(rx, ry, rw, rh); } function roundRect(x, rx, ry, rw, rh, r) { x.beginPath(); x.moveTo(rx + r, ry); x.arcTo(rx + rw, ry, rx + rw, ry + rh, r); x.arcTo(rx + rw, ry + rh, rx, ry + rh, r); x.arcTo(rx, ry + rh, rx, ry, r); x.arcTo(rx, ry, rx + rw, ry, r); x.closePath(); } function highlight(x, hx, hy, hr) { x.fillStyle = 'rgba(255,255,255,.18)'; x.beginPath(); x.ellipse(hx, hy, hr, hr * 1.8, -0.4, 0, Math.PI * 2); x.fill(); } /* ---------------- item icons ---------------- */ const MAT_COLORS = { melee: ['#9aa0aa', '#7a808a'], ranged: ['#8a6a42', '#6e5432'], cast: ['#7a5a9a', '#5e4478'], shield: ['#7a808a', '#5a606a'], armor: ['#8a8578', '#6a665a'], jewelry: ['#c8a35a', '#a8853a'], }; /** returns a fresh canvas with the item's icon */ function itemIconCanvas(item, size = 48) { const c = mkCanvas(size, size); const x = c.getContext('2d'); x.save(); x.scale(size / 48, size / 48); const rarCol = D2.Items.RARITIES[item.rarity].color; const kind = item.kind || 'armor'; switch (item.slot) { case 'weapon': drawWeaponIcon(x, item, kind, rarCol); break; case 'offhand': kind === 'shield' ? drawShieldIcon(x, rarCol) : drawFocusIcon(x, kind, rarCol); break; case 'head': drawHelmIcon(x, rarCol); break; case 'chest': drawChestIcon(x, rarCol); break; case 'hands': drawGlovesIcon(x, rarCol); break; case 'feet': drawBootsIcon(x, rarCol); break; case 'amulet': drawAmuletIcon(x, rarCol); break; case 'ring': drawRingIcon(x, rarCol); break; } x.restore(); return c; } function drawWeaponIcon(x, item, kind, rarCol) { if (kind === 'melee') { const heavy = ['battleaxe', 'warhammer', 'reaver'].includes(item.baseId); const blunt = ['club', 'mace', 'warhammer'].includes(item.baseId); x.translate(24, 24); x.rotate(-Math.PI / 4); if (heavy && !blunt) { // axe x.fillStyle = '#6a4a2a'; x.fillRect(-2.5, -4, 5, 26); x.fillStyle = MAT_COLORS.melee[0]; x.beginPath(); x.moveTo(2, -4); x.quadraticCurveTo(20, -2, 14, 10); x.lineTo(2, 8); x.closePath(); x.fill(); x.beginPath(); x.moveTo(-2, -4); x.quadraticCurveTo(-20, -2, -14, 10); x.lineTo(-2, 8); x.closePath(); x.fill(); } else if (blunt) { // mace x.fillStyle = '#6a4a2a'; x.fillRect(-2.5, 0, 5, 22); x.fillStyle = MAT_COLORS.melee[0]; x.beginPath(); x.arc(0, -5, 9, 0, Math.PI * 2); x.fill(); x.fillStyle = '#555'; x.fillRect(-9, -7, 18, 4); } else { // sword / dagger const len = item.baseId === 'greatsword' ? 30 : item.baseId === 'dagger' ? 16 : 24; x.fillStyle = '#6a4a2a'; x.fillRect(-2, len - 4, 4, 10); x.fillStyle = MAT_COLORS.melee[0]; x.beginPath(); x.moveTo(-3, len - 6); x.lineTo(-3, -len + 6); x.lineTo(0, -len); x.lineTo(3, -len + 6); x.lineTo(3, len - 6); x.closePath(); x.fill(); x.fillStyle = '#c8a35a'; x.fillRect(-8, len - 6, 16, 4); } } else if (kind === 'ranged') { // bow x.strokeStyle = MAT_COLORS.ranged[0]; x.lineWidth = 4; x.beginPath(); x.arc(20, 24, 16, Math.PI * 0.6, -Math.PI * 0.6); x.stroke(); x.strokeStyle = '#d8cba8'; x.lineWidth = 1.5; x.beginPath(); x.moveTo(20 + 16 * Math.cos(Math.PI * 0.6), 24 + 16 * Math.sin(Math.PI * 0.6)); x.lineTo(20 + 16 * Math.cos(-Math.PI * 0.6), 24 + 16 * Math.sin(-Math.PI * 0.6)); x.stroke(); x.strokeStyle = rarCol; x.lineWidth = 3; x.beginPath(); x.moveTo(8, 24); x.lineTo(34, 24); x.stroke(); x.beginPath(); x.moveTo(34, 24); x.lineTo(28, 21); x.moveTo(34, 24); x.lineTo(28, 27); x.stroke(); } else { // staff x.translate(24, 24); x.rotate(Math.PI / 5); x.fillStyle = '#6a4a2a'; roundRect(x, -3, -8, 6, 32, 3); x.fill(); const g = x.createRadialGradient(0, -12, 1, 0, -12, 10); g.addColorStop(0, '#fff'); g.addColorStop(0.4, rarCol); g.addColorStop(1, 'rgba(0,0,0,0)'); x.fillStyle = g; x.beginPath(); x.arc(0, -12, 10, 0, Math.PI * 2); x.fill(); } } function drawShieldIcon(x, rarCol) { x.fillStyle = MAT_COLORS.shield[0]; x.beginPath(); x.moveTo(24, 6); x.quadraticCurveTo(40, 8, 39, 22); x.quadraticCurveTo(38, 38, 24, 44); x.quadraticCurveTo(10, 38, 9, 22); x.quadraticCurveTo(8, 8, 24, 6); x.closePath(); x.fill(); x.strokeStyle = '#444'; x.lineWidth = 2; x.stroke(); x.strokeStyle = rarCol; x.lineWidth = 2.5; x.beginPath(); x.moveTo(24, 14); x.lineTo(24, 34); x.moveTo(15, 24); x.lineTo(33, 24); x.stroke(); } function drawFocusIcon(x, kind, rarCol) { if (kind === 'focus') { const g = x.createRadialGradient(24, 24, 2, 24, 24, 16); g.addColorStop(0, '#dff'); g.addColorStop(0.5, '#6ab8ff'); g.addColorStop(1, '#245'); x.fillStyle = g; x.beginPath(); x.arc(24, 24, 14, 0, Math.PI * 2); x.fill(); x.strokeStyle = '#c8a35a'; x.lineWidth = 2.5; x.beginPath(); x.arc(24, 24, 16, 0, Math.PI * 2); x.stroke(); } else { // quiver x.fillStyle = '#6a4a2a'; roundRect(x, 16, 12, 16, 26, 4); x.fill(); x.strokeStyle = '#c8a35a'; x.lineWidth = 2; for (let i = 0; i < 3; i++) { x.beginPath(); x.moveTo(19 + i * 5, 14); x.lineTo(19 + i * 5, 4); x.stroke(); } } } function drawHelmIcon(x, rarCol) { x.fillStyle = MAT_COLORS.armor[0]; x.beginPath(); x.arc(24, 26, 15, Math.PI, 0); x.lineTo(39, 34); x.lineTo(9, 34); x.closePath(); x.fill(); x.fillStyle = '#222'; x.fillRect(15, 24, 18, 5); x.fillStyle = rarCol; x.fillRect(22, 10, 4, 8); } function drawChestIcon(x, rarCol) { x.fillStyle = MAT_COLORS.armor[0]; x.beginPath(); x.moveTo(14, 10); x.lineTo(34, 10); x.lineTo(40, 18); x.lineTo(36, 42); x.lineTo(12, 42); x.lineTo(8, 18); x.closePath(); x.fill(); x.fillStyle = 'rgba(0,0,0,.25)'; x.fillRect(21, 10, 6, 32); x.strokeStyle = rarCol; x.lineWidth = 2; x.beginPath(); x.moveTo(14, 20); x.lineTo(34, 20); x.stroke(); } function drawGlovesIcon(x, rarCol) { x.fillStyle = MAT_COLORS.armor[0]; roundRect(x, 8, 16, 13, 20, 4); x.fill(); roundRect(x, 27, 16, 13, 20, 4); x.fill(); roundRect(x, 8, 34, 13, 8, 3); x.fill(); roundRect(x, 27, 34, 13, 8, 3); x.fill(); x.strokeStyle = rarCol; x.lineWidth = 2; x.beginPath(); x.moveTo(14, 16); x.lineTo(14, 30); x.moveTo(33, 16); x.lineTo(33, 30); x.stroke(); } function drawBootsIcon(x, rarCol) { x.fillStyle = MAT_COLORS.armor[0]; roundRect(x, 12, 10, 10, 26, 3); x.fill(); roundRect(x, 12, 32, 22, 8, 3); x.fill(); roundRect(x, 26, 10, 10, 26, 3); x.fill(); roundRect(x, 26, 32, 14, 8, 3); x.fill(); x.fillStyle = rarCol; x.fillRect(12, 28, 10, 3); x.fillRect(26, 28, 10, 3); } function drawAmuletIcon(x, rarCol) { x.strokeStyle = '#c8a35a'; x.lineWidth = 2.5; x.beginPath(); x.arc(24, 22, 12, Math.PI * 0.15, Math.PI * 0.85, true); x.stroke(); const g = x.createRadialGradient(24, 34, 1, 24, 34, 8); g.addColorStop(0, '#fff'); g.addColorStop(0.5, rarCol); g.addColorStop(1, '#222'); x.fillStyle = g; x.beginPath(); x.moveTo(24, 26); x.lineTo(31, 33); x.lineTo(24, 43); x.lineTo(17, 33); x.closePath(); x.fill(); x.strokeStyle = '#c8a35a'; x.lineWidth = 1.5; x.stroke(); } function drawRingIcon(x, rarCol) { x.strokeStyle = MAT_COLORS.jewelry[0]; x.lineWidth = 5; x.beginPath(); x.arc(24, 27, 11, 0, Math.PI * 2); x.stroke(); x.strokeStyle = 'rgba(255,255,255,.4)'; x.lineWidth = 1.5; x.beginPath(); x.arc(24, 27, 13, Math.PI * 1.1, Math.PI * 1.6); x.stroke(); const g = x.createRadialGradient(24, 12, 1, 24, 12, 7); g.addColorStop(0, '#fff'); g.addColorStop(0.6, rarCol); g.addColorStop(1, '#222'); x.fillStyle = g; x.beginPath(); x.arc(24, 12, 6, 0, Math.PI * 2); x.fill(); } /* ---------------- skill icons ---------------- */ const SKILL_ICON_STYLE = { cleave: { col: '#e8d8b0', draw: (x) => arcSweep(x, '#e8d8b0') }, whirlwind: { col: '#c8e8ff', draw: (x) => spiral(x, '#c8e8ff') }, consecrate: { col: '#ffe86a', draw: (x) => pillar(x, '#ffe86a') }, charge: { col: '#ffb04a', draw: (x) => arrowFat(x, '#ffb04a') }, warcry: { col: '#ff6a4a', draw: (x) => shoutRings(x, '#ff6a4a') }, seismic: { col: '#d8a84a', draw: (x) => crackBurst(x, '#d8a84a') }, ironskin: { col: '#aab4be', draw: (x) => shieldGlyph(x, '#aab4be') }, renewal: { col: '#7ade8a', draw: (x) => crossPulse(x, '#7ade8a') }, laststand: { col: '#e85a4a', draw: (x) => heartGuard(x, '#e85a4a') }, multishot: { col: '#d8cba8', draw: (x) => fanArrows(x, '#d8cba8', 3) }, piercingbolt: { col: '#ffe8b0', draw: (x) => fanArrows(x, '#ffe8b0', 1) }, arrowstorm: { col: '#ffd24a', draw: (x) => rainDown(x, '#ffd24a') }, shadowstep: { col: '#8a6aff', draw: (x) => blinkGlyph(x, '#8a6aff') }, fanofknives: { col: '#c8d8e8', draw: (x) => radialKnives(x, '#c8d8e8') }, smokeveil: { col: '#9aa8b8', draw: (x) => puffCloud(x, '#9aa8b8') }, predator: { col: '#ff8a4a', draw: (x) => eyeGlyph(x, '#ff8a4a') }, bleedingedge: { col: '#e83a5a', draw: (x) => dropletBlade(x, '#e83a5a') }, swiftvantage: { col: '#8ad8ff', draw: (x) => windLines(x, '#8ad8ff') }, firebolt: { col: '#ff7a2a', draw: (x) => fireOrb(x, '#ff7a2a') }, meteor: { col: '#ff5a1a', draw: (x) => fallingRock(x, '#ff5a1a') }, flamewave: { col: '#ffa02a', draw: (x) => novaRing(x, '#ffa02a') }, iceshards: { col: '#8ad8ff', draw: (x) => radialKnives(x, '#8ad8ff') }, frostnova: { col: '#b8ecff', draw: (x) => snowflake(x, '#b8ecff') }, blizzard: { col: '#c8f0ff', draw: (x) => rainDown(x, '#c8f0ff') }, chainlightning: { col: '#ffee6a', draw: (x) => boltZigzag(x, '#ffee6a') }, teleport: { col: '#c86aff', draw: (x) => blinkGlyph(x, '#c86aff') }, arcaneecho: { col: '#b08aff', draw: (x) => spiral(x, '#b08aff') }, basic_melee: { col: '#e8d8b0', draw: (x) => swordGlyph(x, '#e8d8b0') }, basic_ranged: { col: '#d8cba8', draw: (x) => fanArrows(x, '#d8cba8', 1) }, basic_cast: { col: '#c86aff', draw: (x) => fireOrb(x, '#c86aff') }, potion_hp: { col: '#e84a3a', draw: (x) => potionGlyph(x, '#e84a3a') }, potion_mp: { col: '#4a7ae8', draw: (x) => potionGlyph(x, '#4a7ae8') }, }; function skillIconCanvas(iconId, size = 48) { const key = iconId + '_' + size; if (!cache.skills[key]) { const c = mkCanvas(size, size); const x = c.getContext('2d'); x.save(); x.scale(size / 48, size / 48); const st = SKILL_ICON_STYLE[iconId] || { col: '#ccc', draw: xx => orb(xx, '#ccc') }; /* backdrop */ const g = x.createRadialGradient(24, 24, 4, 24, 24, 24); g.addColorStop(0, 'rgba(60,50,40,.9)'); g.addColorStop(1, 'rgba(15,10,8,.95)'); x.fillStyle = g; x.fillRect(0, 0, 48, 48); x.strokeStyle = 'rgba(200,163,90,.35)'; x.strokeRect(1, 1, 46, 46); st.draw(x); x.restore(); cache.skills[key] = c; } return cache.skills[key]; } function cloneCanvas(src) { const c = mkCanvas(src.width, src.height); c.getContext('2d').drawImage(src, 0, 0); return c; } /* little glyph helpers */ function orb(x, col) { const g = x.createRadialGradient(24, 22, 2, 24, 22, 13); g.addColorStop(0, '#fff'); g.addColorStop(0.45, col); g.addColorStop(1, 'rgba(0,0,0,.1)'); x.fillStyle = g; x.beginPath(); x.arc(24, 22, 13, 0, 7); x.fill(); } function fireOrb(x, col) { orb(x, col); x.strokeStyle = col; x.lineWidth = 2; x.beginPath(); x.moveTo(24, 36); x.quadraticCurveTo(20, 30, 24, 26); x.stroke(); } function arcSweep(x, col) { x.strokeStyle = col; x.lineWidth = 5; x.lineCap = 'round'; x.beginPath(); x.arc(24, 30, 15, Math.PI * 1.15, Math.PI * 1.85); x.stroke(); x.lineWidth = 2; x.beginPath(); x.arc(24, 30, 9, Math.PI * 1.2, Math.PI * 1.8); x.stroke(); } function spiral(x, col) { x.strokeStyle = col; x.lineWidth = 3.5; x.lineCap = 'round'; x.beginPath(); for (let a = 0; a < Math.PI * 4; a += 0.2) { const r = 3 + a * 1.7; const px = 24 + Math.cos(a) * r, py = 24 + Math.sin(a) * r * 0.8; a === 0 ? x.moveTo(px, py) : x.lineTo(px, py); } x.stroke(); } function pillar(x, col) { const g = x.createLinearGradient(0, 8, 0, 40); g.addColorStop(0, 'rgba(255,255,255,.9)'); g.addColorStop(1, 'rgba(0,0,0,0)'); x.fillStyle = g; x.fillRect(18, 8, 12, 32); x.strokeStyle = col; x.lineWidth = 2; x.beginPath(); x.arc(24, 36, 12, Math.PI, 0); x.stroke(); } function arrowFat(x, col) { x.fillStyle = col; x.beginPath(); x.moveTo(40, 24); x.lineTo(26, 16); x.lineTo(30, 24); x.lineTo(26, 32); x.closePath(); x.fill(); x.fillRect(8, 21, 20, 6); } function shoutRings(x, col) { x.strokeStyle = col; x.lineWidth = 2.5; for (const r of [6, 12, 18]) { x.globalAlpha = 1 - r / 22; x.beginPath(); x.arc(24, 26, r, 0, 7); x.stroke(); } x.globalAlpha = 1; } function crackBurst(x, col) { x.strokeStyle = col; x.lineWidth = 3; x.lineCap = 'round'; for (let i = 0; i < 6; i++) { const a = i / 6 * Math.PI * 2; x.beginPath(); x.moveTo(24, 26); x.lineTo(24 + Math.cos(a) * 16, 26 + Math.sin(a) * 13); x.stroke(); } } function shieldGlyph(x, col) { x.fillStyle = col; x.beginPath(); x.moveTo(24, 8); x.quadraticCurveTo(38, 12, 37, 24); x.quadraticCurveTo(35, 36, 24, 42); x.quadraticCurveTo(13, 36, 11, 24); x.quadraticCurveTo(10, 12, 24, 8); x.closePath(); x.fill(); x.fillStyle = 'rgba(0,0,0,.3)'; x.beginPath(); x.moveTo(24, 8); x.lineTo(24, 42); x.lineTo(13, 36); x.lineTo(11, 24); x.quadraticCurveTo(10, 12, 24, 8); x.closePath(); x.fill(); } function crossPulse(x, col) { x.fillStyle = col; x.fillRect(20, 10, 8, 28); x.fillRect(10, 20, 28, 8); x.strokeStyle = col; x.globalAlpha = .5; x.lineWidth = 2; x.beginPath(); x.arc(24, 24, 19, 0, 7); x.stroke(); x.globalAlpha = 1; } function heartGuard(x, col) { x.fillStyle = col; x.beginPath(); x.moveTo(24, 38); x.bezierCurveTo(6, 24, 14, 8, 24, 18); x.bezierCurveTo(34, 8, 42, 24, 24, 38); x.fill(); x.strokeStyle = '#fff'; x.lineWidth = 2; x.globalAlpha = .6; x.beginPath(); x.moveTo(24, 18); x.lineTo(24, 34); x.stroke(); x.globalAlpha = 1; } function fanArrows(x, col, n) { x.strokeStyle = col; x.lineWidth = 3; x.lineCap = 'round'; const angles = n === 1 ? [0] : [-0.5, 0, 0.5]; for (const a of angles) { x.save(); x.translate(24, 34); x.rotate(a); x.beginPath(); x.moveTo(0, 0); x.lineTo(0, -22); x.stroke(); x.beginPath(); x.moveTo(-3, -18); x.lineTo(0, -24); x.lineTo(3, -18); x.stroke(); x.restore(); } } function rainDown(x, col) { x.strokeStyle = col; x.lineWidth = 2.5; x.lineCap = 'round'; for (let i = 0; i < 5; i++) { const px = 8 + i * 8, py = 8 + (i % 3) * 8; x.beginPath(); x.moveTo(px, py); x.lineTo(px - 2, py + 10); x.stroke(); } x.beginPath(); x.arc(24, 36, 9, Math.PI, 0); x.stroke(); } function blinkGlyph(x, col) { x.fillStyle = col; x.globalAlpha = .35; x.beginPath(); x.arc(14, 30, 8, 0, 7); x.fill(); x.globalAlpha = .8; x.beginPath(); x.arc(33, 18, 8, 0, 7); x.fill(); x.globalAlpha = 1; x.strokeStyle = col; x.lineWidth = 2; x.setLineDash([4, 3]); x.beginPath(); x.moveTo(17, 26); x.lineTo(29, 21); x.stroke(); x.setLineDash([]); } function radialKnives(x, col) { x.strokeStyle = col; x.lineWidth = 2.5; x.lineCap = 'round'; for (let i = 0; i < 8; i++) { const a = i / 8 * Math.PI * 2; x.beginPath(); x.moveTo(24 + Math.cos(a) * 7, 24 + Math.sin(a) * 7); x.lineTo(24 + Math.cos(a) * 17, 24 + Math.sin(a) * 17); x.stroke(); } } function puffCloud(x, col) { x.fillStyle = col; x.globalAlpha = .75; for (const [ox, oy, r] of [[18, 28, 9], [28, 24, 11], [22, 18, 8], [32, 30, 7]]) { x.beginPath(); x.arc(ox, oy, r, 0, 7); x.fill(); } x.globalAlpha = 1; } function eyeGlyph(x, col) { x.strokeStyle = col; x.lineWidth = 2.5; x.beginPath(); x.moveTo(8, 24); x.quadraticCurveTo(24, 10, 40, 24); x.quadraticCurveTo(24, 38, 8, 24); x.stroke(); x.fillStyle = col; x.beginPath(); x.arc(24, 24, 6, 0, 7); x.fill(); x.fillStyle = '#000'; x.beginPath(); x.arc(24, 24, 2.5, 0, 7); x.fill(); } function dropletBlade(x, col) { x.strokeStyle = '#c8d8e8'; x.lineWidth = 4; x.lineCap = 'round'; x.beginPath(); x.moveTo(14, 36); x.lineTo(34, 12); x.stroke(); x.fillStyle = col; x.beginPath(); x.moveTo(30, 30); x.quadraticCurveTo(38, 36, 32, 41); x.quadraticCurveTo(25, 37, 30, 30); x.fill(); } function windLines(x, col) { x.strokeStyle = col; x.lineWidth = 2.5; x.lineCap = 'round'; for (const [y0, len] of [[16, 22], [24, 30], [32, 20]]) { x.beginPath(); x.moveTo(40 - len, y0); x.quadraticCurveTo(40 - len * .5, y0 - 4, 40, y0); x.stroke(); } } function fallingRock(x, col) { x.fillStyle = '#7a5a3a'; x.beginPath(); x.moveTo(28, 8); x.lineTo(36, 14); x.lineTo(33, 24); x.lineTo(24, 22); x.closePath(); x.fill(); x.strokeStyle = col; x.lineWidth = 3; x.beginPath(); x.moveTo(20, 14); x.lineTo(14, 22); x.stroke(); x.beginPath(); x.moveTo(24, 10); x.lineTo(20, 16); x.stroke(); const g = x.createRadialGradient(24, 36, 2, 24, 36, 12); g.addColorStop(0, col); g.addColorStop(1, 'rgba(0,0,0,0)'); x.fillStyle = g; x.beginPath(); x.arc(24, 36, 12, 0, 7); x.fill(); } function novaRing(x, col) { x.strokeStyle = col; x.lineWidth = 5; x.globalAlpha = .9; x.beginPath(); x.arc(24, 24, 14, 0, 7); x.stroke(); x.globalAlpha = .4; x.lineWidth = 3; x.beginPath(); x.arc(24, 24, 20, 0, 7); x.stroke(); x.globalAlpha = 1; } function snowflake(x, col) { x.strokeStyle = col; x.lineWidth = 2.5; x.lineCap = 'round'; for (let i = 0; i < 6; i++) { const a = i / 6 * Math.PI * 2; x.beginPath(); x.moveTo(24, 24); x.lineTo(24 + Math.cos(a) * 16, 24 + Math.sin(a) * 16); x.stroke(); x.beginPath(); x.arc(24 + Math.cos(a) * 16, 24 + Math.sin(a) * 16, 2.5, 0, 7); x.fillStyle = col; x.fill(); } } function boltZigzag(x, col) { x.strokeStyle = col; x.lineWidth = 3.5; x.lineJoin = 'round'; x.beginPath(); x.moveTo(28, 6); x.lineTo(18, 22); x.lineTo(26, 24); x.lineTo(16, 42); x.stroke(); x.globalAlpha = .5; x.lineWidth = 2; x.beginPath(); x.moveTo(36, 12); x.lineTo(30, 24); x.lineTo(36, 26); x.lineTo(30, 38); x.stroke(); x.globalAlpha = 1; } function swordGlyph(x, col) { x.save(); x.translate(24, 24); x.rotate(-Math.PI / 4); x.fillStyle = col; x.beginPath(); x.moveTo(-3, 12); x.lineTo(-3, -14); x.lineTo(0, -19); x.lineTo(3, -14); x.lineTo(3, 12); x.closePath(); x.fill(); x.fillStyle = '#c8a35a'; x.fillRect(-8, 12, 16, 4); x.fillRect(-2, 16, 4, 6); x.restore(); } function potionGlyph(x, col) { x.fillStyle = 'rgba(220,220,220,.25)'; x.beginPath(); x.arc(24, 30, 12, 0, 7); x.fill(); x.fillStyle = col; x.beginPath(); x.arc(24, 30, 12, 0, 7); x.fill(); x.fillStyle = '#caa'; x.fillRect(20, 8, 8, 8); x.fillStyle = 'rgba(255,255,255,.35)'; x.beginPath(); x.ellipse(19, 25, 4, 6, -.4, 0, 7); x.fill(); } /* ---------------- init ---------------- */ function init() { for (const [id, th] of Object.entries(D2.world.THEMES)) { cache.themes[id] = { floors: th.floorCols.map((col, v) => bakeFloorTile(th, col, v)), wallBlock: bakeWallBlock(th), }; } bakeProps(); } D2.sprites = { init, TW, TH, WALL_H, itemIconCanvas, skillIconCanvas, get themes() { return cache.themes; }, get props() { return cache.props; }, }; })(window.D2);