// ============================================================ // RENDERER.JS — Изометрический рендерер с частицами // ============================================================ const Renderer = { canvas: null, ctx: null, TW: 64, // ширина тайла TH: 32, // высота тайла TD: 18, // глубина тайла (боковые грани) OY: 85, // смещение по Y от верха particles: [], floatingTexts: [], camera: { x: 0, y: 0 }, lightCanvas: null, lightCtx: null, shake: { x: 0, y: 0, power: 0 }, patterns: {}, _flash: null, _currentMapId: 'village', // ────────────────────────────────── // Типы тайлов // ────────────────────────────────── TILES: { 0: { name: 'Трава', top: '#3d7a3c', l: '#2d5a2c', r: '#1d4020', pat: 'grass' }, 1: { name: 'Вода', top: '#2a5a8c', l: '#1a4a7c', r: '#0d3560', anim: true }, 2: { name: 'Камень', top: '#5a5a5a', l: '#484848', r: '#383838', pat: 'stone' }, 3: { name: 'Песок', top: '#c2a55e', l: '#b09550', r: '#9e8540', pat: 'sand' }, 4: { name: 'Стена', top: '#4a3828', l: '#3a2818', r: '#2a1808', pat: 'brick', solid: true }, 5: { name: 'Дерево', top: '#7a5010', l: '#6a4000', r: '#5a3000', pat: 'wood' }, 6: { name: 'Лава', top: '#cc4400', l: '#bb3300', r: '#aa2200', anim: true }, 7: { name: 'Снег', top: '#dde0ee', l: '#cdd0de', r: '#bdc0ce', pat: 'snow' }, 8: { name: 'Земля', top: '#7a4a20', l: '#6a3a12', r: '#5a2a08', pat: 'dirt' }, 9: { name: 'Булыжник', top: '#606060', l: '#505050', r: '#404040', pat: 'cobble' }, 10: { name: 'Болото', top: '#3e5a30', l: '#2e4a20', r: '#1e3a10' }, 11: { name: 'Лёд', top: '#90b8d8', l: '#80a8c8', r: '#7098b8' }, 12: { name: 'Бездна', top: '#060008', l: '#040006', r: '#020004', pat: 'void' }, }, // ────────────────────────────────── // Инициализация // ────────────────────────────────── init(canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.ctx.imageSmoothingEnabled = false; this.buildPatterns(); this.initLightCanvas(); }, // ────────────────────────────────── // Координатные преобразования // ────────────────────────────────── toIso(gx, gy) { return { x: (gx - gy) * (this.TW / 2) + this.canvas.width / 2 + this.shake.x + this.camera.x, y: (gx + gy) * (this.TH / 2) + this.OY + this.shake.y + this.camera.y }; }, fromIso(sx, sy) { const ax = sx - this.canvas.width / 2 - this.shake.x - this.camera.x; const ay = sy - this.OY - this.shake.y - this.camera.y; const hw = this.TW / 2, hh = this.TH / 2; return { x: Math.floor((ax / hw + ay / hh) / 2), y: Math.floor((ay / hh - ax / hw) / 2) }; }, // ────────────────────────────────── // Процедурные текстуры // ────────────────────────────────── initLightCanvas() { this.lightCanvas = document.createElement('canvas'); this.lightCanvas.width = this.canvas.width; this.lightCanvas.height = this.canvas.height; this.lightCtx = this.lightCanvas.getContext('2d'); }, buildPatterns() { this.patterns.grass = this._makeGrass(); this.patterns.stone = this._makeStone(); this.patterns.sand = this._makeSand(); this.patterns.brick = this._makeBrick(); this.patterns.wood = this._makeWood(); this.patterns.snow = this._makeSnow(); this.patterns.dirt = this._makeDirt(); this.patterns.cobble = this._makeCobble(); this.patterns.void = this._makeVoid(); }, _pat(w, h, fn) { const c = document.createElement('canvas'); c.width = w; c.height = h; const x = c.getContext('2d'); fn(x, w, h); return this.ctx.createPattern(c, 'repeat'); }, _makeGrass() { return this._pat(32, 32, (p) => { p.fillStyle = '#3d7a3c'; p.fillRect(0,0,32,32); p.strokeStyle = '#4a9048'; p.lineWidth = 1; for (let i=0;i<18;i++) { const x=Math.random()*32, y=Math.random()*32, h=3+Math.random()*5; p.beginPath(); p.moveTo(x,y); p.lineTo(x+(Math.random()*4-2),y-h); p.stroke(); } p.fillStyle = '#2d5a2c'; for (let i=0;i<4;i++) { p.beginPath(); p.arc(Math.random()*32,Math.random()*32,1+Math.random()*2,0,Math.PI*2); p.fill(); } }); }, _makeStone() { return this._pat(32, 32, (p) => { p.fillStyle = '#5a5a5a'; p.fillRect(0,0,32,32); p.strokeStyle = '#484848'; p.lineWidth = 1; p.strokeRect(0,0,16,16); p.strokeRect(16,0,16,16); p.strokeRect(0,16,16,16); p.strokeRect(16,16,16,16); p.fillStyle = '#4a4a4a'; p.fillRect(4,4,3,2); p.fillRect(20,9,2,3); p.fillRect(7,22,4,2); }); }, _makeSand() { return this._pat(32, 32, (p) => { p.fillStyle = '#c2a55e'; p.fillRect(0,0,32,32); p.fillStyle = '#d4b870'; for (let i=0;i<30;i++) p.fillRect(Math.random()*32,Math.random()*32,1,1); p.fillStyle = '#b09550'; for (let i=0;i<20;i++) p.fillRect(Math.random()*32,Math.random()*32,1,1); }); }, _makeBrick() { return this._pat(32, 32, (p) => { p.fillStyle = '#4a3828'; p.fillRect(0,0,32,32); p.strokeStyle = '#2a1808'; p.lineWidth = 2; p.beginPath(); p.moveTo(0,8); p.lineTo(32,8); p.moveTo(0,24); p.lineTo(32,24); p.moveTo(16,0); p.lineTo(16,8); p.moveTo(8,8); p.lineTo(8,24); p.moveTo(24,8); p.lineTo(24,24); p.moveTo(16,24); p.lineTo(16,32); p.stroke(); }); }, _makeWood() { return this._pat(32, 32, (p) => { p.fillStyle = '#7a5010'; p.fillRect(0,0,32,32); p.strokeStyle = '#6a4000'; p.lineWidth = 1; for (let i=0;i<8;i++) { p.beginPath(); p.moveTo(i*4,0); p.lineTo(i*4,32); p.stroke(); } }); }, _makeSnow() { return this._pat(32, 32, (p) => { p.fillStyle = '#dde0ee'; p.fillRect(0,0,32,32); p.fillStyle = '#c8cce0'; for (let i=0;i<12;i++) { p.beginPath(); p.arc(Math.random()*32,Math.random()*32,1+Math.random()*1.5,0,Math.PI*2); p.fill(); } }); }, _makeDirt() { return this._pat(32, 32, (p) => { p.fillStyle = '#7a4a20'; p.fillRect(0,0,32,32); p.fillStyle = '#6a3a12'; for (let i=0;i<12;i++) { p.beginPath(); p.arc(Math.random()*32,Math.random()*32,1+Math.random()*2,0,Math.PI*2); p.fill(); } }); }, _makeCobble() { return this._pat(32, 32, (p) => { p.fillStyle = '#606060'; p.fillRect(0,0,32,32); const stones = [[8,8,7],[24,8,6],[8,24,6],[24,24,7],[16,16,5]]; p.fillStyle = '#505050'; stones.forEach(([x,y,r]) => { p.beginPath(); p.arc(x,y,r,0,Math.PI*2); p.fill(); }); p.strokeStyle = '#383838'; p.lineWidth = 1; stones.forEach(([x,y,r]) => { p.beginPath(); p.arc(x,y,r,0,Math.PI*2); p.stroke(); }); }); }, _makeVoid() { return this._pat(16, 16, (p) => { p.fillStyle = '#060008'; p.fillRect(0,0,16,16); p.fillStyle = '#1a0028'; [[3,3],[11,7],[6,12],[13,2],[1,10]].forEach(([px,py]) => { p.beginPath(); p.arc(px,py,1.2,0,Math.PI*2); p.fill(); }); p.fillStyle = '#0d0015'; [[8,4],[4,11],[12,9]].forEach(([px,py]) => { p.beginPath(); p.arc(px,py,0.8,0,Math.PI*2); p.fill(); }); }); }, // ────────────────────────────────── // Рисование тайла // ────────────────────────────────── drawTile(gx, gy, type, hover, time) { const p = this.toIso(gx, gy); const t = this.TILES[type] || this.TILES[0]; const hw = this.TW / 2, hh = this.TH / 2; let topColor = t.top; if (t.anim) { const wave = Math.sin(time / 500 + gx + gy) * 12; topColor = this._adj(t.top, wave); } // Верхняя грань this.ctx.beginPath(); this.ctx.moveTo(p.x, p.y - hh); this.ctx.lineTo(p.x + hw, p.y); this.ctx.lineTo(p.x, p.y + hh); this.ctx.lineTo(p.x - hw, p.y); this.ctx.closePath(); if (t.pat && this.patterns[t.pat]) { this.ctx.save(); this.ctx.clip(); this.ctx.save(); this.ctx.translate(p.x, p.y); this.ctx.scale(1, 0.5); this.ctx.rotate(Math.PI / 4); this.ctx.fillStyle = this.patterns[t.pat]; this.ctx.fillRect(-60, -60, 120, 120); this.ctx.restore(); this.ctx.restore(); } else { this.ctx.fillStyle = topColor; this.ctx.fill(); } this.ctx.strokeStyle = hover ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.35)'; this.ctx.lineWidth = hover ? 2 : 0.8; this.ctx.beginPath(); this.ctx.moveTo(p.x, p.y - hh); this.ctx.lineTo(p.x + hw, p.y); this.ctx.lineTo(p.x, p.y + hh); this.ctx.lineTo(p.x - hw, p.y); this.ctx.closePath(); this.ctx.stroke(); // Левая грань this.ctx.beginPath(); this.ctx.moveTo(p.x - hw, p.y); this.ctx.lineTo(p.x, p.y + hh); this.ctx.lineTo(p.x, p.y + hh + this.TD); this.ctx.lineTo(p.x - hw, p.y + this.TD); this.ctx.closePath(); this.ctx.fillStyle = t.l; this.ctx.fill(); this.ctx.strokeStyle = 'rgba(0,0,0,0.4)'; this.ctx.lineWidth = 0.8; this.ctx.stroke(); // Правая грань this.ctx.beginPath(); this.ctx.moveTo(p.x + hw, p.y); this.ctx.lineTo(p.x, p.y + hh); this.ctx.lineTo(p.x, p.y + hh + this.TD); this.ctx.lineTo(p.x + hw, p.y + this.TD); this.ctx.closePath(); this.ctx.fillStyle = t.r; this.ctx.fill(); this.ctx.stroke(); // Украшения this._tileDeco(gx, gy, type, p, time); if (hover) { this.ctx.fillStyle = 'rgba(255,255,255,0.7)'; this.ctx.font = '10px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText(t.name, p.x, p.y + 4); } }, _tileDeco(gx, gy, type, p, time) { const seed = (gx * 7 + gy * 13) % 10; if (type === 0 && seed < 2) this._drawTree(p.x, p.y, 0.55 + seed * 0.12); if (type === 0 && seed >= 2 && seed < 4) this._drawFlower(p.x, p.y, seed); if (type === 1) { // вода — рябь const ctx = this.ctx; const phase = time/300 + gx*0.9 + gy*1.1; // первое кольцо ряби const ox1 = Math.sin(phase)*4; ctx.fillStyle = `rgba(255,255,255,${0.07 + 0.05*Math.sin(phase)})`; ctx.beginPath(); ctx.ellipse(p.x + ox1, p.y, 8, 3, 0, 0, Math.PI*2); ctx.fill(); // второе кольцо (сдвинуто по фазе и позиции) const phase2 = time/240 + gx*1.3 + gy*0.7 + 2.1; const ox2 = Math.cos(phase2)*3; ctx.fillStyle = `rgba(180,220,255,${0.05 + 0.04*Math.abs(Math.sin(phase2))})`; ctx.beginPath(); ctx.ellipse(p.x + ox2, p.y - 2, 5, 2, 0, 0, Math.PI*2); ctx.fill(); // блик-искра if (seed < 3) { const sparkA = 0.3 + 0.3*Math.abs(Math.sin(time/180 + gx*2.5)); ctx.fillStyle = `rgba(255,255,255,${sparkA})`; ctx.beginPath(); ctx.arc(p.x + (seed-1)*5, p.y - 1, 1.2, 0, Math.PI*2); ctx.fill(); } } if (type === 6) { // лава — пузыри + свечение + дрожание const ctx = this.ctx; // тепловое свечение (halo) const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 14); glow.addColorStop(0, `rgba(255,80,0,${0.18 + 0.08*Math.abs(Math.sin(time/350+gx))})`); glow.addColorStop(1, 'rgba(255,80,0,0)'); ctx.fillStyle = glow; ctx.beginPath(); ctx.ellipse(p.x, p.y, 14, 6, 0, 0, Math.PI*2); ctx.fill(); // несколько пузырей по детерминированным смещениям for (let b = 0; b < 3; b++) { const bPhase = time/200 + gx*3.1 + gy*2.3 + b*2.09; const bx = p.x + (seed%3)*5 - 4 + b*4; const by = p.y - 4 + Math.sin(bPhase)*3; const br = 1.5 + Math.abs(Math.sin(bPhase*1.3))*1.5; const ba = 0.35 + 0.3*Math.abs(Math.sin(bPhase)); if (Math.abs(Math.sin(bPhase)) > 0.4) { // пузырь виден часть цикла ctx.fillStyle = `rgba(255,${120 + Math.floor(60*Math.abs(Math.sin(bPhase)))},0,${ba})`; ctx.beginPath(); ctx.arc(bx, by, br, 0, Math.PI*2); ctx.fill(); } } } }, _drawTree(x, y, s = 1) { const th = 14 * s, cr = 18 * s; this.ctx.fillStyle = '#4a2808'; this.ctx.fillRect(x - 3*s, y - th, 6*s, th + 4); this.ctx.fillStyle = '#1a4818'; this.ctx.beginPath(); this.ctx.moveTo(x, y - th - cr); this.ctx.lineTo(x - cr*0.8, y - th*0.4); this.ctx.lineTo(x + cr*0.8, y - th*0.4); this.ctx.closePath(); this.ctx.fill(); this.ctx.fillStyle = '#2a6028'; this.ctx.beginPath(); this.ctx.moveTo(x, y - th - cr*1.2); this.ctx.lineTo(x - cr*0.6, y - th - cr*0.3); this.ctx.lineTo(x + cr*0.6, y - th - cr*0.3); this.ctx.closePath(); this.ctx.fill(); }, _drawFlower(x, y, seed) { const cols = ['#ff5555','#ffff44','#ff55ff','#55ffff']; this.ctx.strokeStyle = '#2a4a2a'; this.ctx.lineWidth = 1; this.ctx.beginPath(); this.ctx.moveTo(x,y); this.ctx.lineTo(x,y-8); this.ctx.stroke(); this.ctx.fillStyle = cols[seed % cols.length]; for (let i=0;i<5;i++) { const a = i/5*Math.PI*2; this.ctx.beginPath(); this.ctx.arc(x+Math.cos(a)*3, y-8+Math.sin(a)*3, 2, 0, Math.PI*2); this.ctx.fill(); } this.ctx.fillStyle = '#ffff00'; this.ctx.beginPath(); this.ctx.arc(x,y-8,1.5,0,Math.PI*2); this.ctx.fill(); }, // ────────────────────────────────── // Рисование всей карты // ────────────────────────────────── drawMap(map, hoverTile, time) { const tiles = []; for (let y=0; y (a.x+a.y)-(b.x+b.y)); tiles.forEach(tile => { const hover = hoverTile && hoverTile.x===tile.x && hoverTile.y===tile.y; this.drawTile(tile.x, tile.y, tile.t, hover, time); }); }, // ────────────────────────────────── // Игрок (спрайтовая анимация) // ────────────────────────────────── drawPlayer(player, time) { // ИСПРАВЛЕНО: mp_move (прогресс шага 0→1), а НЕ mp (мана) const wx = player.isMoving ? player.x + (player.tx - player.x) * player.mp_move : player.x; const wy = player.isMoving ? player.y + (player.ty - player.y) * player.mp_move : player.y; const p = this.toIso(wx, wy); // Анимация ходьбы const wc = player.isMoving ? time / 130 : 0; const legSwing = Math.sin(wc) * 5; // левая нога вперёд → правая назад const armSwing = -Math.sin(wc) * 6; // руки противофазны ногам const bodyRise = player.isMoving ? Math.abs(Math.sin(wc)) * 2 : 0; // подъём тела при шаге const breathe = player.isMoving ? 0 : Math.sin(time / 2200) * 1.2; // дыхание в покое const CLASS_COLORS = { warrior:'#e74c3c', mage:'#3498db', archer:'#27ae60', paladin:'#f1c40f', necromancer:'#8e44ad', berserker:'#e67e22', druid:'#2ecc71' }; const bodyCol = CLASS_COLORS[player.class] || '#e74c3c'; const darkCol = this._adj(bodyCol, -35); const bx = p.x; const by = p.y + bodyRise; // Y тела с учётом шага const hb = breathe; // смещение головы (дыхание) // ── ТЕНЬ ── this.ctx.beginPath(); this.ctx.ellipse(bx, p.y + 8, 18, 9, 0, 0, Math.PI * 2); this.ctx.fillStyle = 'rgba(0,0,0,0.28)'; this.ctx.fill(); // ── ЛЕВАЯ НОГА ── this.ctx.fillStyle = darkCol; this.ctx.fillRect(bx - 7, by - 4 + legSwing, 5, 13); this.ctx.fillStyle = '#2a1808'; this.ctx.fillRect(bx - 8, by + 7 + legSwing, 7, 4); // ── ПРАВАЯ НОГА ── this.ctx.fillStyle = darkCol; this.ctx.fillRect(bx + 2, by - 4 - legSwing, 5, 13); this.ctx.fillStyle = '#2a1808'; this.ctx.fillRect(bx + 1, by + 7 - legSwing, 7, 4); // ── ТЕЛО ── this.ctx.fillStyle = bodyCol; this.ctx.fillRect(bx - 9, by - 24 + hb, 18, 20); this.ctx.fillStyle = this._adj(darkCol, -10); this.ctx.fillRect(bx - 9, by - 8 + hb, 18, 3); // пояс this.ctx.strokeStyle = darkCol; this.ctx.lineWidth = 1.5; this.ctx.strokeRect(bx - 9, by - 24 + hb, 18, 20); // ── ЛЕВАЯ РУКА ── this.ctx.fillStyle = this._adj(bodyCol, -15); this.ctx.fillRect(bx - 14, by - 23 + hb - armSwing, 5, 12); this.ctx.fillStyle = '#ffcc99'; this.ctx.fillRect(bx - 14, by - 13 + hb - armSwing, 5, 5); // ── ПРАВАЯ РУКА (оружие) ── this.ctx.fillStyle = this._adj(bodyCol, -15); this.ctx.fillRect(bx + 9, by - 23 + hb + armSwing, 5, 12); this.ctx.fillStyle = '#ffcc99'; this.ctx.fillRect(bx + 9, by - 13 + hb + armSwing, 5, 5); // ── ОРУЖИЕ В РУКЕ (по классу) ── this._drawPlayerWeapon(bx, by + hb, player.class, armSwing, time); // ── ГОЛОВА ── const hy = by - 38 + hb; this.ctx.beginPath(); this.ctx.arc(bx, hy, 10, 0, Math.PI * 2); this.ctx.fillStyle = '#ffcc99'; this.ctx.fill(); this.ctx.strokeStyle = '#cc9966'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); // ── ГОЛОВНОЙ УБОР / ПРИЧЁСКА (зависит от класса) ── this._drawPlayerHead(bx, hy, player.class, bodyCol, player.hairColor || '#3a2510', time); // ── ГЛАЗА (смотрят в сторону последнего движения) ── const facing = player.facing || 'down'; const eox = facing === 'right' ? 2 : facing === 'left' ? -2 : 0; this.ctx.fillStyle = '#1a1a2a'; this.ctx.beginPath(); this.ctx.arc(bx - 3.5 + eox, hy + 1, 1.8, 0, Math.PI * 2); this.ctx.arc(bx + 3.5 + eox, hy + 1, 1.8, 0, Math.PI * 2); this.ctx.fill(); this.ctx.fillStyle = 'rgba(255,255,255,0.7)'; this.ctx.beginPath(); this.ctx.arc(bx - 2.5 + eox, hy, 0.7, 0, Math.PI * 2); this.ctx.arc(bx + 4.5 + eox, hy, 0.7, 0, Math.PI * 2); this.ctx.fill(); // ── УРОВЕНЬ ── this.ctx.fillStyle = '#ffd700'; this.ctx.font = 'bold 10px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText('Lv.' + player.level, bx, by - 53 + hb); // ── АУРА (маг / некромант) ── if (player.class === 'mage' || player.class === 'necromancer') { const auraCol = player.class === 'mage' ? 'rgba(52,152,219,0.35)' : 'rgba(142,68,173,0.35)'; this.ctx.strokeStyle = auraCol; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.arc(bx, by - 30 + hb, 18 + Math.sin(time / 400) * 2, 0, Math.PI * 2); this.ctx.stroke(); } // ── HP БАР ── const barW = 28; const hpPct = player.hp / player.maxHp; this.ctx.fillStyle = '#222'; this.ctx.fillRect(bx - barW / 2, by - 60 + hb, barW, 4); this.ctx.fillStyle = hpPct > 0.5 ? '#27ae60' : hpPct > 0.25 ? '#e67e22' : '#e74c3c'; this.ctx.fillRect(bx - barW / 2, by - 60 + hb, barW * hpPct, 4); }, // Головные уборы / причёски по классу _drawPlayerHead(bx, hy, cls, bodyCol, hairColor, time) { const ctx = this.ctx; switch (cls) { case 'warrior': ctx.fillStyle = '#909090'; ctx.beginPath(); ctx.arc(bx, hy - 2, 11, Math.PI, 0); ctx.fill(); ctx.fillStyle = '#707070'; ctx.fillRect(bx - 11, hy - 2, 22, 4); ctx.strokeStyle = '#505050'; ctx.lineWidth = 1; ctx.strokeRect(bx - 11, hy - 2, 22, 4); ctx.fillStyle = '#b8b8b8'; ctx.fillRect(bx - 7, hy, 14, 2); // забрало break; case 'mage': ctx.fillStyle = bodyCol; ctx.beginPath(); ctx.moveTo(bx, hy - 27); ctx.lineTo(bx - 11, hy - 3); ctx.lineTo(bx + 11, hy - 3); ctx.closePath(); ctx.fill(); ctx.strokeStyle = this._adj(bodyCol, -25); ctx.lineWidth = 1.2; ctx.stroke(); ctx.fillStyle = this._adj(bodyCol, -15); ctx.fillRect(bx - 13, hy - 5, 26, 5); // ободок ctx.fillStyle = `rgba(255,220,50,${0.7 + 0.3 * Math.sin(time / 300)})`; ctx.font = 'bold 9px Arial'; ctx.textAlign = 'center'; ctx.fillText('✦', bx, hy - 12); break; case 'archer': ctx.fillStyle = bodyCol; ctx.beginPath(); ctx.arc(bx, hy - 1, 12, Math.PI * 1.08, Math.PI * 1.92); ctx.fill(); ctx.fillStyle = this._adj(bodyCol, -15); ctx.fillRect(bx - 12, hy - 1, 24, 4); break; case 'paladin': ctx.fillStyle = '#c8a810'; ctx.beginPath(); ctx.arc(bx, hy - 2, 11, Math.PI, 0); ctx.fill(); ctx.fillStyle = '#a88808'; ctx.fillRect(bx - 11, hy - 2, 22, 4); ctx.fillStyle = '#ffffff'; // крест ctx.fillRect(bx - 1.5, hy - 14, 3, 14); ctx.fillRect(bx - 6, hy - 9, 12, 3); break; case 'necromancer': ctx.fillStyle = '#1a0a2a'; ctx.beginPath(); ctx.moveTo(bx, hy - 28); ctx.lineTo(bx - 12, hy - 2); ctx.lineTo(bx + 12, hy - 2); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.arc(bx, hy, 13, Math.PI * 1.03, Math.PI * 1.97); ctx.fill(); ctx.fillStyle = `rgba(142,68,173,${0.22 + 0.18 * Math.sin(time / 400)})`; ctx.beginPath(); ctx.arc(bx, hy, 11, 0, Math.PI * 2); ctx.fill(); break; case 'berserker': ctx.fillStyle = '#5a3a20'; ctx.beginPath(); ctx.arc(bx, hy - 2, 11, Math.PI, 0); ctx.fill(); ctx.fillStyle = '#4a2a10'; ctx.fillRect(bx - 11, hy - 2, 22, 4); ctx.fillStyle = '#c8b090'; // рога ctx.beginPath(); ctx.moveTo(bx - 8, hy - 9); ctx.lineTo(bx - 16, hy - 25); ctx.lineTo(bx - 4, hy - 9); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(bx + 8, hy - 9); ctx.lineTo(bx + 16, hy - 25); ctx.lineTo(bx + 4, hy - 9); ctx.closePath(); ctx.fill(); break; case 'druid': ctx.fillStyle = hairColor; ctx.beginPath(); ctx.arc(bx, hy - 3, 9, Math.PI, 0); ctx.fill(); for (let i = 0; i < 7; i++) { const a = (i / 7) * Math.PI + Math.PI; ctx.fillStyle = ['#2ecc71','#27ae60','#1abc9c'][i % 3]; ctx.save(); ctx.translate(bx + Math.cos(a) * 10, hy - 3 + Math.sin(a) * 5); ctx.rotate(a + Math.PI / 2); ctx.beginPath(); ctx.ellipse(0, 0, 4, 2.5, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } break; default: ctx.fillStyle = hairColor; ctx.beginPath(); ctx.arc(bx, hy - 4, 9, Math.PI, 0); ctx.fill(); } }, // Оружие в правой руке (по классу) _drawPlayerWeapon(bx, by, cls, armSwing, time) { const ctx = this.ctx; ctx.save(); ctx.translate(bx + 14, by - 20 + armSwing); switch (cls) { case 'warrior': case 'paladin': ctx.rotate(-0.25); ctx.fillStyle = '#c8c8d8'; ctx.fillRect(-2, -22, 4, 24); // клинок ctx.fillStyle = '#c8a810'; ctx.fillRect(-6, 0, 12, 3); // гарда ctx.fillStyle = '#5a3010'; ctx.fillRect(-2, 3, 4, 8); // рукоять ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.fillRect(0, -20, 1.5, 17); // блик break; case 'mage': { ctx.rotate(-0.1); ctx.fillStyle = '#7a5010'; ctx.fillRect(-2, -34, 4, 40); // посох const hue = (time / 25) % 360; ctx.fillStyle = `hsl(${hue},85%,65%)`; ctx.beginPath(); ctx.arc(0, -35, 5, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 1; ctx.stroke(); ctx.fillStyle = `hsla(${hue},85%,75%,${0.15 + 0.1 * Math.sin(time / 200)})`; ctx.beginPath(); ctx.arc(0, -35, 12, 0, Math.PI * 2); ctx.fill(); break; } case 'archer': ctx.rotate(0.1); ctx.strokeStyle = '#7a5010'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(0, -10, 14, -0.9, 0.9); ctx.stroke(); ctx.strokeStyle = '#ddd'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(Math.cos(-0.9) * 14, Math.sin(-0.9) * 14 - 10); ctx.lineTo(Math.cos(0.9) * 14, Math.sin(0.9) * 14 - 10); ctx.stroke(); break; case 'berserker': ctx.rotate(0.3); ctx.fillStyle = '#5a3010'; ctx.fillRect(-2.5, -28, 5, 32); ctx.fillStyle = '#acacac'; ctx.beginPath(); ctx.moveTo(2, -24); ctx.lineTo(15, -17); ctx.lineTo(15, -7); ctx.lineTo(2, -4); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#888'; ctx.lineWidth = 1; ctx.stroke(); break; case 'necromancer': { ctx.rotate(-0.1); ctx.fillStyle = '#1a0a2a'; ctx.fillRect(-2, -34, 4, 40); ctx.fillStyle = '#d0d0c0'; ctx.beginPath(); ctx.arc(0, -35, 5.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#1a0a2a'; ctx.fillRect(-2.5, -32, 2, 3); ctx.fillRect(0.5, -32, 2, 3); const glow = 0.3 + 0.2 * Math.sin(time / 300); ctx.fillStyle = `rgba(142,68,173,${glow})`; ctx.beginPath(); ctx.arc(0, -35, 9, 0, Math.PI * 2); ctx.fill(); break; } case 'druid': ctx.rotate(-0.1); ctx.fillStyle = '#5a3a10'; ctx.fillRect(-2, -32, 4, 38); ctx.fillStyle = '#2ecc71'; ctx.beginPath(); ctx.arc(0, -33, 4.5, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#27ae60'; ctx.lineWidth = 1; ctx.stroke(); ctx.fillStyle = `rgba(46,204,113,${0.2 + 0.15 * Math.sin(time / 400)})`; ctx.beginPath(); ctx.arc(0, -33, 10, 0, Math.PI * 2); ctx.fill(); break; } ctx.restore(); }, // ────────────────────────────────── // Враг // ────────────────────────────────── drawEnemy(enemy, time) { const p = this.toIso(enemy.x, enemy.y); const bob = Math.sin(time/200) * 2; const atk = enemy.isAtk ? Math.sin(time/60) * 6 : 0; const CFG = { goblin: { body:'#3a6a2a', head:'#4a7a3a', eye:'#ff2200', h:40 }, orc: { body:'#4a5a2a', head:'#5a6a3a', eye:'#ff4400', h:44 }, skeleton: { body:'#c8c8b0', head:'#dcdcc8', eye:'#00ff44', h:42 }, slime: { body:'#00aa55', head:'#00cc66', eye:'#fff', h:28 }, bandit: { body:'#6a5540', head:'#7a6550', eye:'#ff6600', h:42 }, wolf: { body:'#5a4840', head:'#6a5850', eye:'#ffcc00', h:36 }, spider: { body:'#1a1a2a', head:'#2a2a3a', eye:'#ff0000', h:34 }, troll: { body:'#4a5a3a', head:'#5a6a4a', eye:'#ff3300', h:50 }, dragon: { body:'#8b0000', head:'#a00000', eye:'#ffff00', h:55 }, zombie: { body:'#3a5a30', head:'#4a6a40', eye:'#88ff44', h:42 }, bat: { body:'#2a1a3a', head:'#3a2a4a', eye:'#ff44ff', h:30 }, yeti: { body:'#ccccdd', head:'#ddddee', eye:'#00ccff', h:52 }, witch: { body:'#2a1a4a', head:'#3a2a5a', eye:'#ff00ff', h:44 }, golem: { body:'#606050', head:'#707060', eye:'#ff6600', h:56 }, ghost: { body:'rgba(160,200,255,0.55)', head:'rgba(180,220,255,0.65)', eye:'#aaddff', h:44 }, wyvern: { body:'#2a5a30', head:'#3a7a40', eye:'#ffee00', h:50 }, chaos_lord: { body:'#1a0020', head:'#2e0040', eye:'#cc00ff', h:62 }, }; const cfg = CFG[enemy.type] || CFG.goblin; const dy = p.y + atk; // Тень this.ctx.beginPath(); this.ctx.ellipse(p.x, dy + 8, 16, 8, 0, 0, Math.PI*2); this.ctx.fillStyle = 'rgba(0,0,0,0.25)'; this.ctx.fill(); if (enemy.type === 'slime') { // Слизень — каплевидная форма this.ctx.beginPath(); this.ctx.ellipse(p.x, dy - 10 + bob, 14, 10, 0, 0, Math.PI*2); this.ctx.fillStyle = cfg.body; this.ctx.fill(); this.ctx.strokeStyle = '#00885a'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); this.ctx.fillStyle = '#fff'; // глаза this.ctx.beginPath(); this.ctx.arc(p.x-4, dy-12+bob, 3, 0, Math.PI*2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.arc(p.x+4, dy-12+bob, 3, 0, Math.PI*2); this.ctx.fill(); this.ctx.fillStyle = '#000'; this.ctx.beginPath(); this.ctx.arc(p.x-4, dy-12+bob, 1.5, 0, Math.PI*2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.arc(p.x+4, dy-12+bob, 1.5, 0, Math.PI*2); this.ctx.fill(); } else if (enemy.type === 'bat') { // Летучая мышь this.ctx.fillStyle = cfg.body; this.ctx.beginPath(); const wingFlap = Math.sin(time/80) * 8; this.ctx.ellipse(p.x, dy - 15 + bob, 7, 5, 0, 0, Math.PI*2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(p.x-6, dy-15+bob); this.ctx.lineTo(p.x-18, dy-20+wingFlap); this.ctx.lineTo(p.x-8, dy-13+bob); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(p.x+6, dy-15+bob); this.ctx.lineTo(p.x+18, dy-20+wingFlap); this.ctx.lineTo(p.x+8, dy-13+bob); this.ctx.fill(); } else if (enemy.type === 'ghost') { // Призрак — полупрозрачная парящая фигура const glow = 0.3 + 0.2 * Math.sin(time / 400); this.ctx.shadowColor = '#88ccff'; this.ctx.shadowBlur = 14; this.ctx.globalAlpha = 0.55 + 0.15 * Math.sin(time / 500); // Мантия / тело this.ctx.fillStyle = cfg.body; this.ctx.beginPath(); this.ctx.arc(p.x, dy - 28 + bob, 10, Math.PI, 0); // верх this.ctx.lineTo(p.x + 14, dy - 8 + bob); // рваный низ for (let i = 3; i >= 0; i--) { const wx = p.x + 14 - i * 7; this.ctx.lineTo(wx, dy - 8 + bob + (i % 2 === 0 ? 8 : 0)); } this.ctx.closePath(); this.ctx.fill(); // Голова this.ctx.beginPath(); this.ctx.arc(p.x, dy - 38 + bob, 10, 0, Math.PI * 2); this.ctx.fillStyle = cfg.head; this.ctx.fill(); // Глаза — светящиеся this.ctx.fillStyle = '#aaddff'; this.ctx.shadowBlur = 10; this.ctx.beginPath(); this.ctx.ellipse(p.x - 3.5, dy - 39 + bob, 3, 2, 0, 0, Math.PI * 2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.ellipse(p.x + 3.5, dy - 39 + bob, 3, 2, 0, 0, Math.PI * 2); this.ctx.fill(); this.ctx.globalAlpha = 1; this.ctx.shadowBlur = 0; } else if (enemy.type === 'wyvern') { // Виверна — дракон с крыльями const wf = Math.sin(time / 110) * 10; // Крылья this.ctx.fillStyle = 'rgba(30,80,40,0.75)'; this.ctx.beginPath(); this.ctx.moveTo(p.x - 7, dy - 28 + bob); this.ctx.lineTo(p.x - 30, dy - 48 + wf); this.ctx.lineTo(p.x - 7, dy - 10 + bob); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(p.x + 7, dy - 28 + bob); this.ctx.lineTo(p.x + 30, dy - 48 + wf); this.ctx.lineTo(p.x + 7, dy - 10 + bob); this.ctx.fill(); // Тело this.ctx.fillStyle = cfg.body; this.ctx.fillRect(p.x - 7, dy - cfg.h * 0.4 + bob, 14, cfg.h * 0.45); this.ctx.strokeStyle = '#1a4a20'; this.ctx.lineWidth = 1.5; this.ctx.strokeRect(p.x - 7, dy - cfg.h * 0.4 + bob, 14, cfg.h * 0.45); // Хвост this.ctx.beginPath(); this.ctx.moveTo(p.x + 7, dy - 15 + bob); this.ctx.quadraticCurveTo(p.x + 22, dy - 5 + bob, p.x + 18, dy + 8 + bob); this.ctx.strokeStyle = cfg.body; this.ctx.lineWidth = 4; this.ctx.stroke(); // Голова this.ctx.beginPath(); this.ctx.arc(p.x, dy - cfg.h + bob, 10, 0, Math.PI * 2); this.ctx.fillStyle = cfg.head; this.ctx.fill(); this.ctx.strokeStyle = '#1a4a20'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); // Глаза this.ctx.fillStyle = cfg.eye; this.ctx.beginPath(); this.ctx.arc(p.x - 3, dy - cfg.h - 1 + bob, 2.5, 0, Math.PI * 2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.arc(p.x + 3, dy - cfg.h - 1 + bob, 2.5, 0, Math.PI * 2); this.ctx.fill(); } else { // Стандартная гуманоидная фигура this.ctx.fillStyle = cfg.body; this.ctx.fillRect(p.x-7, dy - cfg.h*0.4 + bob, 14, cfg.h*0.45); this.ctx.strokeStyle = '#000'; this.ctx.lineWidth = 1.5; this.ctx.strokeRect(p.x-7, dy - cfg.h*0.4 + bob, 14, cfg.h*0.45); // Ноги this.ctx.fillRect(p.x-6, dy - cfg.h*0.02 + bob, 5, 12); this.ctx.fillRect(p.x+1, dy - cfg.h*0.02 + bob, 5, 12); // Голова this.ctx.beginPath(); this.ctx.arc(p.x, dy - cfg.h + bob, 10, 0, Math.PI*2); this.ctx.fillStyle = cfg.head; this.ctx.fill(); this.ctx.strokeStyle = '#000'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); // Глаза this.ctx.fillStyle = cfg.eye; this.ctx.beginPath(); this.ctx.arc(p.x-3, dy-cfg.h-1+bob, 2.5, 0, Math.PI*2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.arc(p.x+3, dy-cfg.h-1+bob, 2.5, 0, Math.PI*2); this.ctx.fill(); // Клыки у орков/гоблинов if (enemy.type==='orc'||enemy.type==='goblin') { this.ctx.fillStyle = '#fff'; this.ctx.beginPath(); this.ctx.moveTo(p.x-2,dy-cfg.h+5+bob); this.ctx.lineTo(p.x-1,dy-cfg.h+9+bob); this.ctx.lineTo(p.x,dy-cfg.h+5+bob); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(p.x+2,dy-cfg.h+5+bob); this.ctx.lineTo(p.x+3,dy-cfg.h+9+bob); this.ctx.lineTo(p.x+4,dy-cfg.h+5+bob); this.ctx.fill(); } // Дракон — крылья if (enemy.type==='dragon') { this.ctx.fillStyle = 'rgba(100,0,0,0.6)'; this.ctx.beginPath(); this.ctx.moveTo(p.x-7, dy-cfg.h*0.3+bob); this.ctx.lineTo(p.x-28, dy-cfg.h*0.55+bob); this.ctx.lineTo(p.x-7, dy-cfg.h*0.05+bob); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(p.x+7, dy-cfg.h*0.3+bob); this.ctx.lineTo(p.x+28, dy-cfg.h*0.55+bob); this.ctx.lineTo(p.x+7, dy-cfg.h*0.05+bob); this.ctx.fill(); } } // Аура босса if (enemy.isBoss) { this.ctx.strokeStyle = `rgba(220,0,0,${0.3+Math.sin(time/200)*0.15})`; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.arc(p.x, dy - cfg.h*0.5, 26, 0, Math.PI*2); this.ctx.stroke(); } // Статус-эффект: кольцо вокруг врага if (enemy.status) { const statusCols = { poison: 'rgba(30,230,80,', burn: 'rgba(255,110,0,', slow: 'rgba(80,160,255,', }; const sc = statusCols[enemy.status] || 'rgba(220,220,220,'; const pulse = 0.5 + 0.35 * Math.sin(time / 180); this.ctx.strokeStyle = sc + pulse + ')'; this.ctx.lineWidth = 2.5; this.ctx.beginPath(); this.ctx.arc(p.x, dy - cfg.h * 0.52, 22, 0, Math.PI * 2); this.ctx.stroke(); // Мерцающие точки по окружности for (let i = 0; i < 4; i++) { const a = (time / 600 + i / 4) * Math.PI * 2; const dotAlpha = 0.4 + 0.5 * Math.abs(Math.sin(time / 200 + i)); this.ctx.fillStyle = sc + dotAlpha + ')'; this.ctx.beginPath(); this.ctx.arc(p.x + Math.cos(a) * 22, dy - cfg.h * 0.52 + Math.sin(a) * 22, 2.5, 0, Math.PI * 2); this.ctx.fill(); } } // HP бар const barW = 32, hp = enemy.hp / enemy.maxHp; this.ctx.fillStyle = '#1a1a1a'; this.ctx.fillRect(p.x-barW/2, dy - cfg.h - 18 + bob, barW, 5); this.ctx.fillStyle = hp > 0.5 ? '#27ae60' : hp > 0.25 ? '#e67e22' : '#e74c3c'; this.ctx.fillRect(p.x-barW/2, dy - cfg.h - 18 + bob, barW*hp, 5); this.ctx.strokeStyle = '#000'; this.ctx.lineWidth = 0.5; this.ctx.strokeRect(p.x-barW/2, dy - cfg.h - 18 + bob, barW, 5); // Имя this.ctx.fillStyle = enemy.isBoss ? '#ff4444' : '#dddddd'; this.ctx.font = (enemy.isBoss ? 'bold ' : '') + '9px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText(enemy.name, p.x, dy - cfg.h - 22 + bob); }, // ────────────────────────────────── // NPC // ────────────────────────────────── drawNPC(npc, time) { const p = this.toIso(npc.x, npc.y); const bob = Math.sin(time/300) * 2; const c = npc.color || '#8b6914'; this.ctx.beginPath(); this.ctx.ellipse(p.x, p.y+8, 16, 8, 0, 0, Math.PI*2); this.ctx.fillStyle = 'rgba(0,0,0,0.22)'; this.ctx.fill(); // Тело this.ctx.fillStyle = c; this.ctx.fillRect(p.x-7, p.y-28+bob, 14, 18); this.ctx.fillRect(p.x-5, p.y-10+bob, 4, 10); this.ctx.fillRect(p.x+1, p.y-10+bob, 4, 10); // Голова this.ctx.beginPath(); this.ctx.arc(p.x, p.y-40+bob, 10, 0, Math.PI*2); this.ctx.fillStyle = '#ffcc99'; this.ctx.fill(); this.ctx.strokeStyle = '#cc9966'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); // Индикатор диалога const bounce2 = Math.sin(time/180) * 4; this.ctx.fillStyle = '#ffd700'; this.ctx.font = 'bold 14px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText('💬', p.x, p.y - 55 + bounce2); // Имя this.ctx.fillStyle = '#88ffaa'; this.ctx.font = 'bold 10px Arial'; this.ctx.fillText(npc.name, p.x, p.y - 70); }, // ────────────────────────────────── // Предметы на земле // ────────────────────────────────── drawGroundItem(item, time) { const p = this.toIso(item.x, item.y); const bounce = Math.sin(time/300) * 4; // Свечение редких предметов const RGLOW = { uncommon: 'rgba(39,174,96,', rare: 'rgba(41,128,185,', epic: 'rgba(142,68,173,', legendary: 'rgba(230,126,34,', }; const gc = RGLOW[item.rarity]; if (gc) { const pulse = 0.22 + 0.18 * Math.sin(time / 350 + item.x); const gr = this.ctx.createRadialGradient(p.x, p.y - 12, 0, p.x, p.y - 12, 24); gr.addColorStop(0, gc + (pulse * 1.4) + ')'); gr.addColorStop(1, gc + '0)'); this.ctx.beginPath(); this.ctx.arc(p.x, p.y - 12, 24, 0, Math.PI * 2); this.ctx.fillStyle = gr; this.ctx.fill(); // Дополнительный ореол для легендарных if (item.rarity === 'legendary') { this.ctx.strokeStyle = gc + (0.5 + 0.4 * Math.sin(time / 200)) + ')'; this.ctx.lineWidth = 1.5; this.ctx.beginPath(); this.ctx.arc(p.x, p.y - 12 - bounce, 14, 0, Math.PI * 2); this.ctx.stroke(); } } if (item.type === 'gold') { this.ctx.save(); this.ctx.translate(p.x, p.y - 12 - bounce); this.ctx.beginPath(); this.ctx.arc(0,0,8,0,Math.PI*2); this.ctx.fillStyle = '#ffd700'; this.ctx.fill(); this.ctx.strokeStyle = '#aa8800'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); this.ctx.fillStyle = '#aa8800'; this.ctx.font = 'bold 9px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText('$', 0, 3); for (let i=0;i<3;i++) { const a = time/200+i*2; this.ctx.fillStyle = '#fff'; this.ctx.fillRect(Math.cos(a)*10, Math.sin(a)*10, 2, 2); } this.ctx.restore(); } else if (item.type === 'potion') { const col = item.healAmount ? '#e74c3c' : '#3498db'; this._drawPotionIcon(p.x, p.y - 12 - bounce, col); } else if (item.type === 'weapon') { this.ctx.save(); this.ctx.translate(p.x, p.y - 14 - bounce); this.ctx.rotate(Math.PI/4); this.ctx.fillStyle = '#c0c0c0'; this.ctx.fillRect(-2, -12, 4, 18); this.ctx.fillStyle = '#8b4513'; this.ctx.fillRect(-2, 6, 4, 6); this.ctx.fillStyle = '#ffd700'; this.ctx.fillRect(-6, 4, 12, 3); this.ctx.restore(); } else if (item.type === 'armor') { this.ctx.save(); this.ctx.translate(p.x, p.y - 14 - bounce); this.ctx.beginPath(); this.ctx.arc(0,0,9,0,Math.PI*2); this.ctx.fillStyle = '#4a4a6a'; this.ctx.fill(); this.ctx.strokeStyle = '#6a6a9a'; this.ctx.lineWidth = 1.5; this.ctx.stroke(); this.ctx.restore(); } else { // Квестовый / неизвестный this.ctx.fillStyle = '#ff44ff'; this.ctx.font = '16px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText('❓', p.x, p.y - 10 - bounce); } }, drawLoreNote(note, time) { const p = this.toIso(note.gx, note.gy); const ctx = this.ctx; const bounce = Math.sin(time / 280) * 3; const pulse = 0.5 + 0.4 * Math.abs(Math.sin(time / 500 + note.gx)); // мягкое свечение const glow = ctx.createRadialGradient(p.x, p.y - 14, 0, p.x, p.y - 14, 20); glow.addColorStop(0, `rgba(140,120,255,${pulse * 0.4})`); glow.addColorStop(1, 'rgba(140,120,255,0)'); ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(p.x, p.y - 14, 20, 0, Math.PI * 2); ctx.fill(); // пергамент ctx.save(); ctx.translate(p.x, p.y - 16 - bounce); ctx.fillStyle = '#d4c49a'; ctx.fillRect(-7, -9, 14, 16); ctx.strokeStyle = '#8a6a20'; ctx.lineWidth = 1.2; ctx.strokeRect(-7, -9, 14, 16); // строчки ctx.strokeStyle = '#8a6a3080'; ctx.lineWidth = 1; for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.moveTo(-5, -4 + i*4); ctx.lineTo(5, -4 + i*4); ctx.stroke(); } ctx.restore(); // иконка сверху ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.fillText('📜', p.x, p.y - 26 - bounce); }, _drawPotionIcon(x, y, color) { this.ctx.fillStyle = '#4a2a10'; this.ctx.fillRect(x-3, y-18, 6, 5); this.ctx.beginPath(); this.ctx.moveTo(x, y-13); this.ctx.lineTo(x-7, y+3); this.ctx.lineTo(x+7, y+3); this.ctx.closePath(); this.ctx.fillStyle = color; this.ctx.fill(); this.ctx.strokeStyle = this._adj(color,-30); this.ctx.lineWidth = 1.5; this.ctx.stroke(); this.ctx.fillStyle = 'rgba(255,255,255,0.3)'; this.ctx.beginPath(); this.ctx.ellipse(x-2, y-6, 2, 4, 0, 0, Math.PI*2); this.ctx.fill(); }, // ────────────────────────────────── // Декорации // ────────────────────────────────── drawDecoration(dec, time) { const p = this.toIso(dec.x, dec.y); switch(dec.type) { case 'portal': this._drawPortal(p, dec, time); break; case 'tree': this._drawTree(p.x, p.y, 0.8); break; case 'house': this._drawHouse(p); break; case 'tavern': this._drawTavern(p, dec); break; case 'table': this._drawTable(p); break; case 'torch': this._drawTorch(p, time); break; case 'crystal': this._drawCrystal(p, time); break; case 'pillar': this._drawPillar(p); break; case 'rock': this._drawRock(p); break; case 'well': this._drawWell(p); break; case 'fountain':this._drawFountain(p, time); break; } }, _drawPortal(p, dec, time) { const isVoid = dec.destination === 'abyss' || this._currentMapId === 'abyss'; const pulse = Math.sin(time/200)*4; const ctx = this.ctx; ctx.beginPath(); ctx.ellipse(p.x, p.y-20, 14+pulse/2, 22+pulse, 0, 0, Math.PI*2); const g = ctx.createRadialGradient(p.x,p.y-20,0,p.x,p.y-20,24); if (isVoid) { g.addColorStop(0,'rgba(180,0,255,0.95)'); g.addColorStop(0.4,'rgba(60,0,120,0.7)'); g.addColorStop(1,'rgba(10,0,30,0.2)'); ctx.fillStyle = g; ctx.fill(); ctx.strokeStyle = '#6600aa'; ctx.lineWidth = 2.5; ctx.stroke(); // Вращающийся ореол из частиц const rot = time/1200; for (let i=0;i<6;i++) { const a = rot + i/6*Math.PI*2; ctx.beginPath(); ctx.arc(p.x+Math.cos(a)*18, p.y-20+Math.sin(a)*26, 2.5, 0, Math.PI*2); ctx.fillStyle = `rgba(${100+Math.floor(Math.sin(a+time/400)*50)},0,255,0.7)`; ctx.fill(); } ctx.fillStyle = '#cc00ff'; } else { g.addColorStop(0,'rgba(255,255,255,0.9)'); g.addColorStop(0.4,'rgba(150,0,255,0.7)'); g.addColorStop(1,'rgba(50,0,150,0.2)'); ctx.fillStyle = g; ctx.fill(); ctx.strokeStyle = '#cc88ff'; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = '#ffd700'; } ctx.font = 'bold 9px Arial'; ctx.textAlign = 'center'; ctx.fillText(dec.name || '?', p.x, p.y - 46); }, _drawHouse(p) { const ctx = this.ctx; ctx.fillStyle = '#8b6914'; ctx.fillRect(p.x-18, p.y-28, 36, 28); ctx.strokeStyle = '#5a4000'; ctx.lineWidth = 1.5; ctx.strokeRect(p.x-18, p.y-28, 36, 28); ctx.fillStyle = '#c0392b'; ctx.beginPath(); ctx.moveTo(p.x-20, p.y-28); ctx.lineTo(p.x, p.y-50); ctx.lineTo(p.x+20, p.y-28); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#922b21'; ctx.stroke(); ctx.fillStyle = '#4a2a0a'; ctx.fillRect(p.x-5, p.y-18, 10, 18); ctx.fillStyle = '#88ccff'; ctx.fillRect(p.x-14, p.y-24, 10, 8); ctx.strokeStyle = '#aaddff'; ctx.lineWidth = 1; ctx.strokeRect(p.x-14, p.y-24, 10, 8); }, _drawTavern(p, dec) { const ctx = this.ctx; // Стены (тёплый коричневый, шире обычного дома) ctx.fillStyle = '#5a3a1a'; ctx.fillRect(p.x-22, p.y-32, 44, 32); ctx.strokeStyle = '#3a2010'; ctx.lineWidth = 1.5; ctx.strokeRect(p.x-22, p.y-32, 44, 32); // Крыша (тёмно-коричневая) ctx.fillStyle = '#8B4513'; ctx.beginPath(); ctx.moveTo(p.x-24, p.y-32); ctx.lineTo(p.x, p.y-58); ctx.lineTo(p.x+24, p.y-32); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#5a2d0c'; ctx.lineWidth = 1; ctx.stroke(); // Дверь (двойная) ctx.fillStyle = '#2a1a08'; ctx.fillRect(p.x-7, p.y-22, 14, 22); ctx.strokeStyle = '#4a3018'; ctx.lineWidth = 1; ctx.strokeRect(p.x-7, p.y-22, 14, 22); ctx.strokeStyle = '#3a2010'; ctx.beginPath(); ctx.moveTo(p.x, p.y-22); ctx.lineTo(p.x, p.y); ctx.stroke(); // Два окна ctx.fillStyle = '#ffddaa'; ctx.fillRect(p.x-20, p.y-26, 9, 7); ctx.fillStyle = '#ffddaa'; ctx.fillRect(p.x+11, p.y-26, 9, 7); ctx.strokeStyle = '#8b6914'; ctx.lineWidth = 1; ctx.strokeRect(p.x-20, p.y-26, 9, 7); ctx.strokeRect(p.x+11, p.y-26, 9, 7); // Вывеска ctx.fillStyle = '#c8a020'; ctx.fillRect(p.x-18, p.y-52, 36, 14); ctx.strokeStyle = '#7a6010'; ctx.lineWidth = 1; ctx.strokeRect(p.x-18, p.y-52, 36, 14); ctx.font = '10px serif'; ctx.textAlign = 'center'; ctx.fillStyle = '#2a1a00'; ctx.fillText('🍺', p.x, p.y - 41); // Название ctx.fillStyle = '#ffd700'; ctx.font = 'bold 9px Arial'; ctx.fillText(dec && dec.name ? dec.name : 'Таверна', p.x, p.y - 64); }, _drawTable(p) { const ctx = this.ctx; // Столешница ctx.fillStyle = '#8B5A2B'; ctx.fillRect(p.x-12, p.y-12, 24, 12); ctx.strokeStyle = '#5a3a10'; ctx.lineWidth = 1; ctx.strokeRect(p.x-12, p.y-12, 24, 12); // Верхний кант ctx.fillStyle = '#a06030'; ctx.fillRect(p.x-12, p.y-14, 24, 4); ctx.strokeStyle = '#6a4020'; ctx.strokeRect(p.x-12, p.y-14, 24, 4); // Кружка ctx.font = '10px serif'; ctx.textAlign = 'center'; ctx.fillText('🍺', p.x, p.y - 9); }, _drawTorch(p, time) { const ctx = this.ctx; ctx.fillStyle = '#5a3000'; ctx.fillRect(p.x-2, p.y-20, 4, 20); // составное мерцание из двух частот const flicker = Math.sin(time/80)*2 + Math.cos(time/55)*1; const radius = 11 + Math.abs(Math.sin(time/120))*2; const cx = p.x + Math.sin(time/95)*1.2; const cy = p.y - 24 + flicker; // внешнее свечение const outer = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius*2.2); outer.addColorStop(0,'rgba(255,150,0,0.18)'); outer.addColorStop(1,'rgba(255,50,0,0)'); ctx.fillStyle = outer; ctx.beginPath(); ctx.arc(cx, cy, radius*2.2, 0, Math.PI*2); ctx.fill(); // основное пламя const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); g.addColorStop(0,'rgba(255,220,60,0.95)'); g.addColorStop(0.55,'rgba(255,110,0,0.6)'); g.addColorStop(1,'rgba(255,40,0,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI*2); ctx.fill(); // искра (одна, летит вверх) const sparkPhase = (time/600) % 1; if (sparkPhase < 0.55) { const sy = cy - sparkPhase * 18; const sa = 0.9 - sparkPhase * 1.6; ctx.fillStyle = `rgba(255,220,80,${Math.max(0,sa)})`; ctx.beginPath(); ctx.arc(cx + Math.sin(sparkPhase*8)*2, sy, 1.2, 0, Math.PI*2); ctx.fill(); } }, _drawCrystal(p, time) { const ctx = this.ctx; const pulse = Math.sin(time/400)*3; const hue = (time/30)%360; ctx.fillStyle = `hsla(${hue},80%,60%,0.8)`; ctx.beginPath(); ctx.moveTo(p.x, p.y-26-pulse); ctx.lineTo(p.x-8, p.y-14); ctx.lineTo(p.x, p.y-4); ctx.lineTo(p.x+8, p.y-14); ctx.closePath(); ctx.fill(); ctx.strokeStyle = `hsla(${hue},100%,80%,0.6)`; ctx.lineWidth = 1.5; ctx.stroke(); }, _drawPillar(p) { const ctx = this.ctx; const isAbyss = this._currentMapId === 'abyss'; ctx.fillStyle = isAbyss ? '#1a0028' : '#909090'; ctx.fillRect(p.x-6, p.y-30, 12, 30); ctx.fillStyle = isAbyss ? '#110018' : '#808080'; ctx.fillRect(p.x-8, p.y-32, 16, 6); ctx.fillStyle = isAbyss ? '#220033' : '#a0a0a0'; ctx.fillRect(p.x-8, p.y-4, 16, 6); ctx.strokeStyle = isAbyss ? '#330044' : '#606060'; ctx.lineWidth = 1; ctx.strokeRect(p.x-6, p.y-30, 12, 30); if (isAbyss) { ctx.shadowColor = '#6600aa'; ctx.shadowBlur = 10; ctx.strokeStyle = '#440066'; ctx.lineWidth = 2; ctx.strokeRect(p.x-5, p.y-29, 10, 28); ctx.shadowBlur = 0; } }, _drawRock(p) { const ctx = this.ctx; ctx.fillStyle = '#555'; ctx.beginPath(); ctx.ellipse(p.x, p.y-8, 12, 7, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#666'; ctx.beginPath(); ctx.ellipse(p.x-2, p.y-10, 7, 4, 0, 0, Math.PI*2); ctx.fill(); }, _drawWell(p) { const ctx = this.ctx; ctx.fillStyle = '#7a7a7a'; ctx.fillRect(p.x-10, p.y-16, 20, 16); ctx.fillStyle = '#3a3a5a'; ctx.beginPath(); ctx.arc(p.x, p.y-16, 10, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#5a5a8a'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.fillStyle = '#5a3a10'; ctx.fillRect(p.x-2, p.y-28, 4, 14); ctx.fillRect(p.x-12, p.y-28, 4, 8); ctx.fillRect(p.x+8, p.y-28, 4, 8); ctx.strokeStyle = '#3a2000'; ctx.lineWidth = 1; ctx.strokeRect(p.x-12, p.y-28, 24, 4); }, _drawFountain(p, time) { const ctx = this.ctx; ctx.fillStyle = '#7a7a9a'; ctx.beginPath(); ctx.arc(p.x, p.y, 14, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#2a5a8c'; ctx.beginPath(); ctx.arc(p.x, p.y, 10, 0, Math.PI*2); ctx.fill(); const spray = Math.sin(time/200)*2; ctx.strokeStyle = 'rgba(100,180,255,0.7)'; ctx.lineWidth = 1.5; for (let i=0;i<5;i++) { const a = i/5*Math.PI*2; ctx.beginPath(); ctx.moveTo(p.x, p.y-8); ctx.lineTo(p.x+Math.cos(a)*10+spray, p.y-18+Math.sin(a)*4); ctx.stroke(); } }, // ────────────────────────────────── // Частицы // ────────────────────────────────── addParticle(worldX, worldY, type, count) { count = count || 6; const pos = this.toIso(worldX, worldY); const palettes = { hit: ['#ff4444','#ff8800','#ffcc00'], magic: ['#4488ff','#aa44ff','#ffffff'], heal: ['#44ff88','#88ff44','#ffffff'], fire: ['#ff6600','#ff9900','#ffcc00'], ice: ['#88ccff','#aaddff','#ffffff'], poison: ['#44cc44','#88dd44','#ccff44'], gold: ['#ffd700','#ffaa00','#ffffaa'], death: ['#666','#888','#aaa'], holy: ['#ffee44','#ffffff','#fff0aa'], void: ['#4a0080','#6600aa','#330055'], }; const cols = palettes[type] || palettes.hit; for (let i = 0; i < count; i++) { this.particles.push({ x: pos.x + (Math.random()-0.5)*20, y: pos.y - 15 + (Math.random()-0.5)*20, vx: (Math.random()-0.5)*3.5, vy: -1.5 - Math.random()*3, life: 1, decay: 0.025 + Math.random()*0.03, size: 2 + Math.random()*3, col: cols[Math.floor(Math.random()*cols.length)] }); } }, updateParticles(dt) { this.particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.vy += 0.12; p.life -= p.decay; }); this.particles = this.particles.filter(p => p.life > 0); }, drawParticles() { this.particles.forEach(p => { this.ctx.save(); this.ctx.globalAlpha = p.life; this.ctx.fillStyle = p.col; this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI*2); this.ctx.fill(); this.ctx.restore(); }); }, // ────────────────────────────────── // Встряска экрана // ────────────────────────────────── shakeScreen(power) { this.shake.power = Math.max(this.shake.power, power); }, updateShake() { if (this.shake.power > 0.1) { this.shake.x = (Math.random()-0.5) * this.shake.power; this.shake.y = (Math.random()-0.5) * this.shake.power; this.shake.power *= 0.82; } else { this.shake.power = 0; this.shake.x = 0; this.shake.y = 0; } }, // ────────────────────────────────── // Вспышка экрана // ────────────────────────────────── flashScreen(color, alpha, dur) { this._flash = { color: color||'#ff0000', alpha: alpha||0.30, life: 1, speed: 1/(dur||7) }; }, drawFlash() { if (!this._flash || this._flash.life <= 0) return; this.ctx.save(); this.ctx.globalAlpha = this._flash.life * this._flash.alpha; this.ctx.fillStyle = this._flash.color; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); this._flash.life -= this._flash.speed; }, // ────────────────────────────────── // Атмосфера Бездны // ────────────────────────────────── drawAbyssAtmosphere(time) { const ctx = this.ctx; const pulse = Math.sin(time / 1400) * 0.04; ctx.save(); ctx.globalAlpha = 0.25 + pulse; const g = ctx.createRadialGradient( this.canvas.width*0.5, this.canvas.height*0.4, 0, this.canvas.width*0.5, this.canvas.height*0.4, this.canvas.width*0.75 ); g.addColorStop(0, 'rgba(20,0,40,0)'); g.addColorStop(0.5,'rgba(10,0,28,0.5)'); g.addColorStop(1, 'rgba(0,0,8,0.95)'); ctx.fillStyle = g; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Полосы тумана ctx.globalAlpha = 0.07 + Math.sin(time/900)*0.03; for (let i=0;i<3;i++) { const fy = ((time/3200 + i*0.33) % 1) * this.canvas.height; const fg = ctx.createLinearGradient(0, fy-22, 0, fy+22); fg.addColorStop(0,'transparent'); fg.addColorStop(0.5,'rgba(40,0,70,0.55)'); fg.addColorStop(1,'transparent'); ctx.fillStyle = fg; ctx.fillRect(0, fy-22, this.canvas.width, 44); } ctx.restore(); }, // ────────────────────────────────── // Всплывающие числа урона/лечения // ────────────────────────────────── addFloatingText(worldX, worldY, text, color, size) { const pos = this.toIso(worldX, worldY); this.floatingTexts.push({ x: pos.x, y: pos.y - 30, text: String(text), color: color || '#ff4444', size: size || 16, life: 1, decay: 0.016, vy: -1.1, }); }, updateFloatingTexts(dt) { this.floatingTexts.forEach(t => { t.y += t.vy; t.vy += 0.025; // замедление подъёма t.life -= t.decay; }); this.floatingTexts = this.floatingTexts.filter(t => t.life > 0); }, drawFloatingTexts() { const ctx = this.ctx; this.floatingTexts.forEach(t => { ctx.save(); ctx.globalAlpha = Math.min(1, t.life * 2.2); ctx.font = `bold ${t.size}px Arial`; ctx.textAlign = 'center'; // Тень ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillText(t.text, t.x + 1, t.y + 1); // Текст ctx.fillStyle = t.color; ctx.fillText(t.text, t.x, t.y); ctx.restore(); }); }, // ────────────────────────────────── // Миникарта // ────────────────────────────────── drawMinimap(map, player, enemies, questDots) { const ctx = this.ctx; const mx = this.canvas.width - 110, my = 52, ms = 95; const mw = map[0].length, mh = map.length; const tw = ms / mw, th = ms / mh; // Фон ctx.fillStyle = 'rgba(4,4,12,0.82)'; ctx.fillRect(mx-2, my-2, ms+4, ms+4); ctx.strokeStyle = '#1e1e38'; ctx.lineWidth = 1; ctx.strokeRect(mx-2, my-2, ms+4, ms+4); const MCOLS = { 0:'#2d5a1a',1:'#1a4a7c',2:'#4a4a4a',3:'#8a7a4a', 4:'#2a1808',5:'#5a4020',6:'#8a3a00',7:'#b0b0c0', 8:'#5a3a1a',9:'#505050',10:'#2a4a2a',11:'#7090b0' }; for (let y=0; y { const col = d.type === 'give' ? '#ffd700' : d.type === 'advance' ? '#ffaa00' : d.type === 'complete'? '#27ae60' : d.type === 'target' ? '#88aaff' : '#fff'; const r = d.type === 'target' ? tw * 1.6 : tw * 1.2; ctx.fillStyle = col + '88'; ctx.strokeStyle = col; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.arc(mx + d.x * tw + tw/2, my + d.y * th + th/2, r, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); }); } // Враги enemies.forEach(e => { ctx.fillStyle = '#e74c3c'; ctx.fillRect(mx + e.x*tw, my + e.y*th, tw+1, th+1); }); // Игрок const px = Math.round(player.x), py = Math.round(player.y); ctx.fillStyle = '#ffffff'; ctx.fillRect(mx + px*tw - 1, my + py*th - 1, tw+2, th+2); }, // ────────────────────────────────── // Маркеры квестов над NPC // ────────────────────────────────── drawQuestMarkers(npcs, questData, time) { if (!questData || !npcs || !npcs.length) return; const ctx = this.ctx; const bounce = Math.sin(time / 300) * 3; npcs.forEach(npc => { const mtype = questData[npc.name]; if (!mtype) return; const p = this.toIso(npc.x, npc.y); const cx = p.x; const cy = p.y - 80 + bounce; const r = 9; ctx.save(); if (mtype === 'give') { // Жёлтый "!" — новый квест ctx.fillStyle = 'rgba(255,215,0,0.18)'; ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffd700'; ctx.font = 'bold 13px Arial'; ctx.textAlign = 'center'; ctx.fillText('!', cx, cy + 5); } else if (mtype === 'advance') { // Мигающий оранжевый "?" — нужно отчитаться const pulse = 0.55 + 0.45 * Math.sin(time / 180); ctx.globalAlpha = pulse; ctx.fillStyle = 'rgba(255,170,0,0.22)'; ctx.strokeStyle = '#ffaa00'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffaa00'; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'center'; ctx.fillText('?', cx, cy + 4); } else if (mtype === 'complete') { // Зелёный "✓" ctx.fillStyle = 'rgba(39,174,96,0.18)'; ctx.strokeStyle = '#27ae60'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#27ae60'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('✓', cx, cy + 4); } ctx.restore(); }); }, // ────────────────────────────────── // Очистка и наложение день/ночь // ────────────────────────────────── clear(brightness) { const ctx = this.ctx; ctx.fillStyle = '#07070f'; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); const grad = ctx.createRadialGradient(450, 300, 0, 450, 300, 500); grad.addColorStop(0, '#12122a'); grad.addColorStop(1, '#07070f'); ctx.fillStyle = grad; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); }, // Динамическое освещение: тьма с "дырками" от источников света drawLightMask(brightness, lights, time) { if (brightness >= 1) return; const lc = this.lightCtx; const darkness = Math.max(0, 1 - brightness) * 0.80; lc.clearRect(0, 0, this.lightCanvas.width, this.lightCanvas.height); lc.fillStyle = `rgba(0,0,40,${darkness})`; lc.fillRect(0, 0, this.lightCanvas.width, this.lightCanvas.height); if (lights && lights.length > 0) { lc.globalCompositeOperation = 'destination-out'; lights.forEach(light => { const pos = this.toIso(light.x, light.y); const flicker = light.flicker ? Math.sin(time / 80 + (light.x || 0) * 3.3) * 9 : 0; const r = (light.radius || 90) + flicker; const cy = pos.y - 18; const g = lc.createRadialGradient(pos.x, cy, 0, pos.x, cy, r); g.addColorStop(0, 'rgba(0,0,0,0.90)'); g.addColorStop(0.45, 'rgba(0,0,0,0.55)'); g.addColorStop(0.80, 'rgba(0,0,0,0.15)'); g.addColorStop(1, 'rgba(0,0,0,0)'); lc.fillStyle = g; lc.beginPath(); lc.arc(pos.x, cy, r, 0, Math.PI * 2); lc.fill(); }); lc.globalCompositeOperation = 'source-over'; } this.ctx.drawImage(this.lightCanvas, 0, 0); }, // Дождь drawRain(particles) { const ctx = this.ctx; ctx.strokeStyle = 'rgba(130,160,255,0.45)'; ctx.lineWidth = 1; particles.forEach(p => { ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - 2, p.y + 10); ctx.stroke(); }); }, // Снег drawSnow(particles) { const ctx = this.ctx; ctx.fillStyle = 'rgba(220,220,255,0.6)'; particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill(); }); }, // Туман drawFog(time) { const ctx = this.ctx; ctx.fillStyle = `rgba(180,190,200,${0.08+0.04*Math.sin(time/2000)})`; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); }, // Звёздное небо (ночью) drawStars(time, brightness) { if (brightness > 0.6) return; const alpha = (1 - brightness / 0.6) * 0.7; const ctx = this.ctx; ctx.fillStyle = `rgba(255,255,255,${alpha})`; // Постоянный seed для позиций for (let i = 0; i < 60; i++) { const sx = (((i * 1619 + 7) * 9301 + 49297) % 233280) / 233280 * 900; const sy = (((i * 3491 + 13) * 9301 + 49297) % 233280) / 233280 * 120; const twinkle = 0.3 + 0.7 * Math.abs(Math.sin(time/800 + i)); const r = 0.5 + (i%3)*0.4; ctx.globalAlpha = alpha * twinkle; ctx.beginPath(); ctx.arc(sx, sy, r, 0, Math.PI*2); ctx.fill(); } ctx.globalAlpha = 1; }, // ────────────────────────────────── // Утилиты // ────────────────────────────────── _adj(hex, amt) { const n = parseInt(hex.replace('#',''), 16); const R = Math.max(0, Math.min(255, (n>>16)+amt)); const G = Math.max(0, Math.min(255, ((n>>8)&0xff)+amt)); const B = Math.max(0, Math.min(255, (n&0xff)+amt)); return '#'+(0x1000000+R*0x10000+G*0x100+B).toString(16).slice(1); }, drawText(text, x, y, color, size='13px', align='left') { this.ctx.font = `${size} Arial`; this.ctx.textAlign = align; this.ctx.fillStyle = '#000'; this.ctx.fillText(text, x+1, y+1); this.ctx.fillStyle = color; this.ctx.fillText(text, x, y); }, // ══════════════════════════════════════════ // ПОРТРЕТЫ — отдельный canvas для боя // ══════════════════════════════════════════ drawPlayerPortrait(player, canvas) { if (!canvas) return; const c = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; c.clearRect(0, 0, W, H); // фон c.fillStyle = '#06060e'; c.fillRect(0, 0, W, H); const cls = player.cls || 'warrior'; const cx = W / 2; // ── Базовое тело (общее) ────────────────── const _body = (torsoColor, shoulderColor) => { // тело c.fillStyle = torsoColor; c.fillRect(cx - 14, 58, 28, 26); // плечи c.fillStyle = shoulderColor; c.fillRect(cx - 19, 55, 10, 10); c.fillRect(cx + 9, 55, 10, 10); // руки c.fillStyle = torsoColor; c.fillRect(cx - 21, 65, 8, 14); c.fillRect(cx + 13, 65, 8, 14); }; const _head = (faceColor, eyeColor, pupilColor) => { // голова c.fillStyle = faceColor; c.beginPath(); c.arc(cx, 40, 14, 0, Math.PI * 2); c.fill(); // глаза c.fillStyle = eyeColor; c.beginPath(); c.arc(cx - 5, 38, 3.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 38, 3.5, 0, Math.PI * 2); c.fill(); // зрачки c.fillStyle = pupilColor; c.beginPath(); c.arc(cx - 5, 38, 1.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 38, 1.5, 0, Math.PI * 2); c.fill(); // рот c.strokeStyle = '#8b5e3c'; c.lineWidth = 1.5; c.beginPath(); c.arc(cx, 44, 4, 0.15 * Math.PI, 0.85 * Math.PI); c.stroke(); }; switch (cls) { case 'warrior': { _body('#4a6080', '#607090'); // стальной шлем c.fillStyle = '#7080a0'; c.fillRect(cx - 14, 24, 28, 22); c.beginPath(); c.arc(cx, 26, 14, Math.PI, 0); c.fill(); // визор c.fillStyle = '#30404a'; c.fillRect(cx - 10, 34, 20, 8); // красный плащ c.fillStyle = '#8a1a1a'; c.beginPath(); c.moveTo(cx - 16, 60); c.lineTo(cx - 22, 90); c.lineTo(cx + 22, 90); c.lineTo(cx + 16, 60); c.fill(); break; } case 'mage': { _body('#2a2a6a', '#1a1a5a'); _head('#c8a882', '#88aaff', '#2233aa'); // синяя роба c.fillStyle = '#1a1a6a'; c.fillRect(cx - 14, 58, 28, 30); // остроконечная шляпа c.fillStyle = '#1a1a8a'; c.beginPath(); c.moveTo(cx, 2); c.lineTo(cx - 18, 38); c.lineTo(cx + 18, 38); c.fill(); c.fillStyle = '#2828aa'; c.fillRect(cx - 20, 36, 40, 6); // светящиеся глаза c.fillStyle = '#88aaff'; c.shadowColor = '#88aaff'; c.shadowBlur = 8; c.beginPath(); c.arc(cx - 5, 38, 3, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 38, 3, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; break; } case 'archer': { _body('#2a4a1a', '#3a5a28'); _head('#c8a882', '#8a6a2a', '#2a1a00'); // зелёный капюшон c.fillStyle = '#2a4a1a'; c.beginPath(); c.arc(cx, 36, 18, Math.PI, 0); c.fill(); c.fillRect(cx - 18, 36, 36, 12); c.fillStyle = '#3a5a28'; c.fillRect(cx - 10, 26, 20, 14); // лук за спиной c.strokeStyle = '#6a4a1a'; c.lineWidth = 3; c.beginPath(); c.arc(cx + 22, 55, 20, -0.7 * Math.PI, 0.4 * Math.PI); c.stroke(); c.strokeStyle = '#b8a060'; c.lineWidth = 1; c.beginPath(); c.moveTo(cx + 22, 41); c.lineTo(cx + 22, 82); c.stroke(); break; } case 'paladin': { _body('#b8902a', '#d4a832'); _head('#c8a882', '#3a3a8a', '#0a0a3a'); // золотой шлем c.fillStyle = '#c8a020'; c.beginPath(); c.arc(cx, 28, 15, Math.PI, 0); c.fill(); c.fillRect(cx - 15, 28, 30, 18); // крест на шлеме c.fillStyle = '#ffffff'; c.fillRect(cx - 2, 20, 4, 14); c.fillRect(cx - 8, 26, 16, 4); // белый плащ c.fillStyle = '#e0e0d0'; c.beginPath(); c.moveTo(cx - 16, 60); c.lineTo(cx - 22, 90); c.lineTo(cx + 22, 90); c.lineTo(cx + 16, 60); c.fill(); break; } case 'necromancer': { _body('#181828', '#0e0e1e'); _head('#9098a8', '#aa44cc', '#5500aa'); // тёмный капюшон c.fillStyle = '#0e0e22'; c.beginPath(); c.arc(cx, 36, 19, Math.PI, 0); c.fill(); c.fillRect(cx - 19, 36, 38, 14); c.beginPath(); c.moveTo(cx, 4); c.lineTo(cx - 16, 38); c.lineTo(cx + 16, 38); c.fill(); // пурпурные глаза c.fillStyle = '#cc44ff'; c.shadowColor = '#aa00ff'; c.shadowBlur = 10; c.beginPath(); c.arc(cx - 5, 38, 3.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 38, 3.5, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; break; } case 'berserker': { _body('#5a3020', '#6a3828'); _head('#c8a078', '#cc4422', '#660000'); // кожаный шлем с рогами c.fillStyle = '#4a2810'; c.beginPath(); c.arc(cx, 34, 15, Math.PI, 0); c.fill(); c.fillRect(cx - 15, 34, 30, 10); // рога c.fillStyle = '#8a7050'; c.beginPath(); c.moveTo(cx - 14, 26); c.lineTo(cx - 22, 10); c.lineTo(cx - 8, 28); c.fill(); c.beginPath(); c.moveTo(cx + 14, 26); c.lineTo(cx + 22, 10); c.lineTo(cx + 8, 28); c.fill(); // шрамы c.strokeStyle = '#8a4030'; c.lineWidth = 1.5; c.beginPath(); c.moveTo(cx - 8, 35); c.lineTo(cx - 2, 42); c.stroke(); c.beginPath(); c.moveTo(cx + 4, 34); c.lineTo(cx + 8, 43); c.stroke(); break; } case 'druid': { _body('#2a4a18', '#1e3810'); _head('#c8a882', '#3a7a20', '#0a3a00'); // венок из листьев c.fillStyle = '#2a6a10'; c.beginPath(); c.arc(cx, 34, 17, Math.PI, 0); c.fill(); c.fillRect(cx - 17, 34, 34, 6); // листочки [cx - 16, cx - 8, cx, cx + 8, cx + 16].forEach((lx, i) => { c.fillStyle = i % 2 === 0 ? '#2a8020' : '#3a6a18'; c.beginPath(); c.ellipse(lx, 26, 5, 8, (i - 2) * 0.3, 0, Math.PI * 2); c.fill(); }); // зелёное одеяние c.fillStyle = '#1a3a10'; c.fillRect(cx - 14, 58, 28, 30); break; } default: { _body('#4a4a6a', '#5a5a7a'); _head('#c8a882', '#888888', '#333333'); } } }, drawEnemyPortrait(enemy, canvas) { if (!canvas || !enemy) return; const c = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; c.clearRect(0, 0, W, H); c.fillStyle = '#06060e'; c.fillRect(0, 0, W, H); const type = enemy.type || 'goblin'; const cx = W / 2; switch (type) { case 'goblin': { // голова c.fillStyle = '#4a7a2a'; c.beginPath(); c.arc(cx, 42, 18, 0, Math.PI * 2); c.fill(); // уши c.beginPath(); c.arc(cx - 17, 38, 7, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 17, 38, 7, 0, Math.PI * 2); c.fill(); // глаза c.fillStyle = '#ff4400'; c.beginPath(); c.arc(cx - 6, 40, 4, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 40, 4, 0, Math.PI * 2); c.fill(); c.fillStyle = '#000'; c.beginPath(); c.arc(cx - 6, 40, 1.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 40, 1.5, 0, Math.PI * 2); c.fill(); // нос c.fillStyle = '#3a6a1a'; c.beginPath(); c.ellipse(cx, 47, 3, 2.5, 0, 0, Math.PI * 2); c.fill(); // зубы c.fillStyle = '#f0e890'; c.fillRect(cx - 6, 52, 4, 5); c.fillRect(cx + 2, 52, 4, 5); // тело c.fillStyle = '#3a6018'; c.fillRect(cx - 12, 60, 24, 22); break; } case 'slime': { c.fillStyle = '#30c060'; c.beginPath(); c.ellipse(cx, 62, 28, 22, 0, 0, Math.PI * 2); c.fill(); // пузырьки c.fillStyle = '#50e880'; c.beginPath(); c.arc(cx - 8, 55, 6, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 52, 4, 0, Math.PI * 2); c.fill(); // глаза c.fillStyle = '#fff'; c.beginPath(); c.arc(cx - 7, 62, 5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 7, 62, 5, 0, Math.PI * 2); c.fill(); c.fillStyle = '#000'; c.beginPath(); c.arc(cx - 7, 62, 2.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 7, 62, 2.5, 0, Math.PI * 2); c.fill(); break; } case 'bat': { // крылья c.fillStyle = '#3a1a4a'; c.beginPath(); c.moveTo(cx, 50); c.lineTo(cx - 40, 30); c.lineTo(cx - 30, 60); c.fill(); c.beginPath(); c.moveTo(cx, 50); c.lineTo(cx + 40, 30); c.lineTo(cx + 30, 60); c.fill(); // тело c.fillStyle = '#2a1a3a'; c.beginPath(); c.ellipse(cx, 55, 14, 18, 0, 0, Math.PI * 2); c.fill(); // голова c.beginPath(); c.arc(cx, 38, 12, 0, Math.PI * 2); c.fill(); // уши c.beginPath(); c.moveTo(cx - 9, 30); c.lineTo(cx - 14, 14); c.lineTo(cx - 2, 28); c.fill(); c.beginPath(); c.moveTo(cx + 9, 30); c.lineTo(cx + 14, 14); c.lineTo(cx + 2, 28); c.fill(); // глаза c.fillStyle = '#ff2222'; c.shadowColor = '#ff0000'; c.shadowBlur = 6; c.beginPath(); c.arc(cx - 5, 37, 3, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 37, 3, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; break; } case 'wolf': { // туловище c.fillStyle = '#606060'; c.beginPath(); c.ellipse(cx, 68, 22, 14, 0, 0, Math.PI * 2); c.fill(); // голова c.beginPath(); c.ellipse(cx - 6, 44, 16, 13, -0.3, 0, Math.PI * 2); c.fill(); // морда c.fillStyle = '#888'; c.beginPath(); c.ellipse(cx - 18, 48, 9, 6, -0.4, 0, Math.PI * 2); c.fill(); // уши c.fillStyle = '#505050'; c.beginPath(); c.moveTo(cx - 4, 34); c.lineTo(cx - 12, 20); c.lineTo(cx + 2, 32); c.fill(); c.beginPath(); c.moveTo(cx + 4, 34); c.lineTo(cx + 10, 20); c.lineTo(cx + 8, 33); c.fill(); // глаза c.fillStyle = '#ffaa00'; c.beginPath(); c.arc(cx - 9, 42, 3, 0, Math.PI * 2); c.fill(); c.fillStyle = '#000'; c.beginPath(); c.arc(cx - 9, 42, 1.5, 0, Math.PI * 2); c.fill(); // нос c.fillStyle = '#222'; c.beginPath(); c.ellipse(cx - 22, 47, 4, 2.5, 0, 0, Math.PI * 2); c.fill(); break; } case 'spider': { // тело c.fillStyle = '#2a1a0a'; c.beginPath(); c.ellipse(cx, 60, 16, 20, 0, 0, Math.PI * 2); c.fill(); // голова c.beginPath(); c.arc(cx, 38, 12, 0, Math.PI * 2); c.fill(); // ноги (8 штук) c.strokeStyle = '#1a0a00'; c.lineWidth = 2.5; [[-1, -35, -1, -55], [1, -30, 1, -50], [-1, -20, -1, -42], [1, -15, 1, -38]].forEach(([sx, sy, ex, ey], i) => { const side = i % 2 === 0 ? -1 : 1; c.beginPath(); c.moveTo(cx + side * 15, 58); c.lineTo(cx + side * (15 + Math.abs(sx) * 18), 58 + sy); c.stroke(); c.beginPath(); c.moveTo(cx + side * 15, 65); c.lineTo(cx + side * (15 + Math.abs(sx) * 18), 65 + sy + 10); c.stroke(); }); // глаза c.fillStyle = '#ff4400'; for (let i = 0; i < 4; i++) { c.beginPath(); c.arc(cx - 9 + i * 6, 36, 2, 0, Math.PI * 2); c.fill(); } break; } case 'bandit': { // тело c.fillStyle = '#3a3028'; c.fillRect(cx - 13, 58, 26, 24); // голова c.fillStyle = '#c8a078'; c.beginPath(); c.arc(cx, 42, 14, 0, Math.PI * 2); c.fill(); // маска/повязка c.fillStyle = '#1a1010'; c.fillRect(cx - 12, 38, 24, 8); // шляпа c.fillStyle = '#2a2018'; c.fillRect(cx - 16, 26, 32, 4); c.fillRect(cx - 11, 16, 22, 14); // глаза c.fillStyle = '#cc8800'; c.beginPath(); c.arc(cx - 5, 42, 2.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 42, 2.5, 0, Math.PI * 2); c.fill(); // кинжал c.strokeStyle = '#aaa'; c.lineWidth = 2; c.beginPath(); c.moveTo(cx + 18, 58); c.lineTo(cx + 18, 82); c.stroke(); c.fillStyle = '#6a5030'; c.fillRect(cx + 15, 58, 6, 5); break; } case 'skeleton': { // кости тела c.fillStyle = '#d0c8a0'; c.fillRect(cx - 8, 58, 16, 28); c.fillStyle = '#e8e0b8'; c.fillRect(cx - 3, 62, 6, 5); c.fillRect(cx - 3, 71, 6, 5); c.fillRect(cx - 3, 80, 6, 5); // руки-кости c.fillRect(cx - 20, 62, 8, 18); c.fillRect(cx + 12, 62, 8, 18); // череп c.beginPath(); c.arc(cx, 40, 16, 0, Math.PI * 2); c.fill(); // глазницы c.fillStyle = '#1a1a0a'; c.beginPath(); c.arc(cx - 6, 38, 5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 38, 5, 0, Math.PI * 2); c.fill(); // нос c.beginPath(); c.arc(cx, 46, 2, 0, Math.PI * 2); c.fill(); // зубы c.fillStyle = '#d0c8a0'; for (let i = 0; i < 5; i++) c.fillRect(cx - 9 + i * 5, 50, 4, 4); break; } case 'orc': { // тело c.fillStyle = '#3a5a1a'; c.fillRect(cx - 18, 56, 36, 30); // плечи c.beginPath(); c.arc(cx - 18, 58, 10, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 18, 58, 10, 0, Math.PI * 2); c.fill(); // голова c.fillStyle = '#4a6a28'; c.beginPath(); c.arc(cx, 38, 18, 0, Math.PI * 2); c.fill(); // клыки c.fillStyle = '#e8e0a0'; c.fillRect(cx - 8, 50, 5, 8); c.fillRect(cx + 3, 50, 5, 8); // глаза c.fillStyle = '#ff6600'; c.beginPath(); c.arc(cx - 6, 36, 5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 36, 5, 0, Math.PI * 2); c.fill(); c.fillStyle = '#000'; c.beginPath(); c.arc(cx - 6, 36, 2, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 36, 2, 0, Math.PI * 2); c.fill(); break; } case 'zombie': { // тело (гниющее) c.fillStyle = '#3a5a28'; c.fillRect(cx - 13, 58, 26, 28); // пятна гнили c.fillStyle = '#2a4018'; c.beginPath(); c.arc(cx - 4, 65, 5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 74, 4, 0, Math.PI * 2); c.fill(); // голова c.fillStyle = '#7a9a5a'; c.beginPath(); c.arc(cx, 42, 15, 0, Math.PI * 2); c.fill(); // раны c.fillStyle = '#4a0a0a'; c.beginPath(); c.arc(cx + 6, 36, 4, 0, Math.PI * 2); c.fill(); // глаза мёртвые c.fillStyle = '#b8c8a0'; c.beginPath(); c.arc(cx - 5, 40, 4, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 40, 4, 0, Math.PI * 2); c.fill(); c.fillStyle = '#888a70'; c.beginPath(); c.arc(cx - 5, 40, 2, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 40, 2, 0, Math.PI * 2); c.fill(); // рот открыт c.fillStyle = '#1a0a08'; c.beginPath(); c.arc(cx, 48, 5, 0.1 * Math.PI, 0.9 * Math.PI); c.fill(); break; } case 'troll': { // огромное тело c.fillStyle = '#4a6a30'; c.beginPath(); c.ellipse(cx, 68, 28, 22, 0, 0, Math.PI * 2); c.fill(); // плечи широкие c.beginPath(); c.arc(cx - 26, 60, 14, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 26, 60, 14, 0, Math.PI * 2); c.fill(); // голова c.beginPath(); c.arc(cx, 38, 20, 0, Math.PI * 2); c.fill(); // нос крупный c.fillStyle = '#3a5a20'; c.beginPath(); c.ellipse(cx, 40, 7, 8, 0, 0, Math.PI * 2); c.fill(); // глаза маленькие c.fillStyle = '#ff8800'; c.beginPath(); c.arc(cx - 8, 34, 4, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 8, 34, 4, 0, Math.PI * 2); c.fill(); break; } case 'yeti': { // белая шерсть c.fillStyle = '#d8e0e8'; c.beginPath(); c.ellipse(cx, 66, 26, 22, 0, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx - 24, 58, 13, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 24, 58, 13, 0, Math.PI * 2); c.fill(); // голова c.beginPath(); c.arc(cx, 38, 20, 0, Math.PI * 2); c.fill(); // тёмная морда c.fillStyle = '#b0b8c0'; c.beginPath(); c.ellipse(cx, 44, 12, 10, 0, 0, Math.PI * 2); c.fill(); // нос чёрный c.fillStyle = '#202830'; c.beginPath(); c.ellipse(cx, 42, 5, 3.5, 0, 0, Math.PI * 2); c.fill(); // глаза c.fillStyle = '#88aacc'; c.beginPath(); c.arc(cx - 9, 33, 4.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 9, 33, 4.5, 0, Math.PI * 2); c.fill(); c.fillStyle = '#000'; c.beginPath(); c.arc(cx - 9, 33, 2, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 9, 33, 2, 0, Math.PI * 2); c.fill(); break; } case 'witch': { // тело в чёрном c.fillStyle = '#181820'; c.fillRect(cx - 13, 58, 26, 28); // голова c.fillStyle = '#b0a080'; c.beginPath(); c.arc(cx, 42, 14, 0, Math.PI * 2); c.fill(); // шляпа c.fillStyle = '#101018'; c.fillRect(cx - 18, 26, 36, 5); c.fillRect(cx - 10, 10, 20, 18); // зелёные глаза c.fillStyle = '#22cc44'; c.shadowColor = '#00ff44'; c.shadowBlur = 8; c.beginPath(); c.arc(cx - 5, 40, 3.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 5, 40, 3.5, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; // нос крючковатый c.fillStyle = '#988060'; c.beginPath(); c.moveTo(cx, 46); c.lineTo(cx - 5, 54); c.lineTo(cx + 2, 53); c.fill(); break; } case 'golem': { // каменное тело c.fillStyle = '#606870'; c.fillRect(cx - 20, 54, 40, 32); // детали камня c.fillStyle = '#505860'; c.fillRect(cx - 18, 60, 14, 10); c.fillRect(cx + 4, 64, 14, 8); // плечи кубические c.fillRect(cx - 28, 50, 14, 14); c.fillRect(cx + 14, 50, 14, 14); // голова c.fillStyle = '#686e78'; c.fillRect(cx - 16, 24, 32, 32); // кристалл в груди c.fillStyle = '#22aaff'; c.shadowColor = '#0088ff'; c.shadowBlur = 10; c.beginPath(); c.moveTo(cx, 58); c.lineTo(cx - 7, 68); c.lineTo(cx, 76); c.lineTo(cx + 7, 68); c.fill(); c.shadowBlur = 0; // глаза-кристаллы c.fillStyle = '#44ccff'; c.shadowColor = '#0088ff'; c.shadowBlur = 8; c.fillRect(cx - 10, 32, 8, 6); c.fillRect(cx + 2, 32, 8, 6); c.shadowBlur = 0; break; } case 'dragon': { // туловище c.fillStyle = '#8a1a1a'; c.beginPath(); c.ellipse(cx + 4, 66, 26, 18, 0.2, 0, Math.PI * 2); c.fill(); // шея c.fillStyle = '#9a2020'; c.fillRect(cx - 6, 44, 16, 24); // голова c.beginPath(); c.ellipse(cx - 2, 36, 18, 14, -0.3, 0, Math.PI * 2); c.fill(); // морда c.beginPath(); c.ellipse(cx - 16, 40, 10, 7, -0.4, 0, Math.PI * 2); c.fill(); // рога c.fillStyle = '#6a1010'; c.beginPath(); c.moveTo(cx + 6, 26); c.lineTo(cx + 14, 8); c.lineTo(cx + 10, 28); c.fill(); c.beginPath(); c.moveTo(cx - 2, 24); c.lineTo(cx + 4, 8); c.lineTo(cx + 2, 26); c.fill(); // глаза огненные c.fillStyle = '#ffaa00'; c.shadowColor = '#ff6600'; c.shadowBlur = 10; c.beginPath(); c.arc(cx - 4, 34, 5, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; // крыло c.fillStyle = '#6a1010'; c.beginPath(); c.moveTo(cx + 18, 52); c.lineTo(cx + 42, 26); c.lineTo(cx + 40, 60); c.lineTo(cx + 22, 68); c.fill(); break; } case 'lich': { // мантия c.fillStyle = '#0e0818'; c.beginPath(); c.moveTo(cx - 20, 58); c.lineTo(cx - 28, 96); c.lineTo(cx + 28, 96); c.lineTo(cx + 20, 58); c.fill(); c.fillRect(cx - 13, 56, 26, 8); // руки-кости c.fillStyle = '#c8c0a8'; c.fillRect(cx - 22, 62, 6, 20); c.fillRect(cx + 16, 62, 6, 20); // череп c.beginPath(); c.arc(cx, 38, 17, 0, Math.PI * 2); c.fill(); // глазницы с огнём c.fillStyle = '#0a0818'; c.beginPath(); c.arc(cx - 6, 36, 6, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 36, 6, 0, Math.PI * 2); c.fill(); // пурпурный огонь в глазах c.fillStyle = '#cc22ff'; c.shadowColor = '#aa00ff'; c.shadowBlur = 12; c.beginPath(); c.arc(cx - 6, 36, 3.5, 0, Math.PI * 2); c.fill(); c.beginPath(); c.arc(cx + 6, 36, 3.5, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; // зубы черепа c.fillStyle = '#c8c0a8'; for (let i = 0; i < 5; i++) c.fillRect(cx - 9 + i * 5, 48, 4, 5); // корона тёмная c.fillStyle = '#3a0060'; c.fillRect(cx - 14, 18, 28, 6); [cx - 12, cx - 4, cx + 4, cx + 12].forEach(kx => { c.fillRect(kx - 2, 10, 5, 10); }); // самоцветы c.fillStyle = '#cc22ff'; [cx - 10, cx + 2, cx + 14].forEach(kx => { c.beginPath(); c.arc(kx, 14, 2.5, 0, Math.PI * 2); c.fill(); }); break; } case 'ghost': { // Полупрозрачная фигура призрака c.globalAlpha = 0.75; // Свечение const grd = c.createRadialGradient(cx, 55, 0, cx, 55, 40); grd.addColorStop(0, 'rgba(140,200,255,0.5)'); grd.addColorStop(1, 'rgba(80,140,255,0)'); c.fillStyle = grd; c.beginPath(); c.arc(cx, 55, 40, 0, Math.PI * 2); c.fill(); // Мантия-тело c.fillStyle = 'rgba(160,210,255,0.8)'; c.beginPath(); c.moveTo(cx - 14, 46); c.lineTo(cx - 18, 85); c.lineTo(cx - 8, 78); c.lineTo(cx, 85); c.lineTo(cx + 8, 78); c.lineTo(cx + 18, 85); c.lineTo(cx + 14, 46); c.closePath(); c.fill(); // Голова c.fillStyle = 'rgba(200,230,255,0.9)'; c.beginPath(); c.arc(cx, 36, 16, 0, Math.PI * 2); c.fill(); // Светящиеся глаза c.fillStyle = '#88ccff'; c.shadowColor = '#66aaff'; c.shadowBlur = 12; c.beginPath(); c.ellipse(cx - 5, 35, 4.5, 3, 0, 0, Math.PI * 2); c.fill(); c.beginPath(); c.ellipse(cx + 5, 35, 4.5, 3, 0, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; c.globalAlpha = 1; break; } case 'wyvern': { // Виверна — небольшой дракон с крыльями // Крыло слева c.fillStyle = '#1a5028'; c.beginPath(); c.moveTo(cx - 8, 52); c.lineTo(cx - 42, 30); c.lineTo(cx - 30, 60); c.lineTo(cx - 10, 68); c.fill(); // Туловище c.fillStyle = '#2a6032'; c.beginPath(); c.ellipse(cx + 2, 68, 22, 16, 0.15, 0, Math.PI * 2); c.fill(); // Шея c.fillRect(cx - 5, 48, 14, 22); // Голова c.beginPath(); c.ellipse(cx, 36, 14, 11, -0.2, 0, Math.PI * 2); c.fill(); // Морда c.beginPath(); c.ellipse(cx - 14, 40, 8, 5, -0.3, 0, Math.PI * 2); c.fill(); // Хвост c.strokeStyle = '#2a6032'; c.lineWidth = 6; c.beginPath(); c.moveTo(cx + 20, 68); c.quadraticCurveTo(cx + 38, 72, cx + 40, 88); c.stroke(); // Рог/гребень c.fillStyle = '#1a4020'; c.beginPath(); c.moveTo(cx + 4, 28); c.lineTo(cx + 10, 14); c.lineTo(cx + 8, 30); c.fill(); c.beginPath(); c.moveTo(cx - 2, 26); c.lineTo(cx + 2, 12); c.lineTo(cx + 2, 28); c.fill(); // Глаза c.fillStyle = '#eecc00'; c.shadowColor = '#ffaa00'; c.shadowBlur = 8; c.beginPath(); c.arc(cx - 4, 35, 4, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; break; } case 'chaos_lord': { // Тёмный аморфный силуэт — тело c.fillStyle = '#0d0018'; c.beginPath(); c.ellipse(cx, 68, 26, 22, 0, 0, Math.PI * 2); c.fill(); // Верхняя часть — голова-туман c.fillStyle = '#1a0028'; c.beginPath(); c.ellipse(cx, 40, 22, 25, 0, 0, Math.PI * 2); c.fill(); // Корона из тьмы c.fillStyle = '#3a0055'; for (let i = 0; i < 5; i++) { const px2 = cx - 20 + i * 10, spiky = i % 2 === 0 ? 14 : 8; c.beginPath(); c.moveTo(px2 - 5, 26); c.lineTo(px2, 26 - spiky); c.lineTo(px2 + 5, 26); c.fill(); } c.fillRect(cx - 22, 23, 44, 6); // Множество глаз const eyes = [[cx - 9, 38], [cx + 9, 38], [cx - 3, 45], [cx + 3, 45], [cx, 32]]; eyes.forEach(([ex, ey]) => { c.fillStyle = '#cc00ff'; c.shadowColor = '#ff00ff'; c.shadowBlur = 6; c.beginPath(); c.arc(ex, ey, 2.5, 0, Math.PI * 2); c.fill(); }); c.shadowBlur = 0; // Щупальца внизу c.strokeStyle = '#1a0030'; c.lineWidth = 4; for (let i = -2; i <= 2; i++) { c.beginPath(); c.moveTo(cx + i * 8, 82); c.quadraticCurveTo(cx + i * 12, 92, cx + i * 6 + Math.sin(i) * 4, 100); c.stroke(); } break; } } }, };