Diablo2D — Shadows of Tristram: complete browser ARPG
- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
This commit is contained in:
+1102
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,769 @@
|
||||
/* ============================================================
|
||||
* 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);
|
||||
Reference in New Issue
Block a user