2174 lines
102 KiB
JavaScript
2174 lines
102 KiB
JavaScript
// ============================================================
|
||
// 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;
|
||
}
|
||
}
|
||
},
|
||
};
|