Files
RPG_FromClaude/renderer.js
Maxim Dolgolyov ac1f348311 Initial commit: RPG game project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:01:02 +03:00

2174 lines
102 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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<map.length; y++)
for (let x=0; x<map[y].length; x++)
tiles.push({x, y, t: map[y][x]});
tiles.sort((a,b) => (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<mh; y++)
for (let x=0; x<mw; x++) {
ctx.fillStyle = MCOLS[map[y][x]] || '#333';
ctx.fillRect(mx + x*tw, my + y*th, Math.ceil(tw), Math.ceil(th));
}
// Маркеры квестов на миникарте
if (questDots && questDots.length) {
questDots.forEach(d => {
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;
}
}
},
};