1091 lines
38 KiB
JavaScript
1091 lines
38 KiB
JavaScript
// ========================================
|
||
// RENDERER.JS - Изометрический рендерер с текстурами
|
||
// ========================================
|
||
|
||
const Renderer = {
|
||
canvas: null,
|
||
ctx: null,
|
||
TILE_WIDTH: 64,
|
||
TILE_HEIGHT: 32,
|
||
TILE_DEPTH: 20,
|
||
|
||
// Кэшированные паттерны для текстур
|
||
patterns: {},
|
||
|
||
// Палитра цветов для тайлов с текстурами
|
||
TILE_COLORS: {
|
||
grass: {
|
||
top: '#4a7c3f',
|
||
left: '#3d6834',
|
||
right: '#2d5228',
|
||
name: 'Трава',
|
||
pattern: 'grass'
|
||
},
|
||
water: {
|
||
top: '#3d6b8c',
|
||
left: '#2d5a78',
|
||
right: '#1d4868',
|
||
name: 'Вода',
|
||
pattern: 'water',
|
||
animated: true
|
||
},
|
||
stone: {
|
||
top: '#6a6a6a',
|
||
left: '#5a5a5a',
|
||
right: '#4a4a4a',
|
||
name: 'Камень',
|
||
pattern: 'stone'
|
||
},
|
||
sand: {
|
||
top: '#c4a86c',
|
||
left: '#b4985c',
|
||
right: '#a4884c',
|
||
name: 'Песок',
|
||
pattern: 'sand'
|
||
},
|
||
wall: {
|
||
top: '#5a4a3a',
|
||
left: '#4a3a2a',
|
||
right: '#3a2a1a',
|
||
name: 'Стена',
|
||
pattern: 'brick'
|
||
},
|
||
wood: {
|
||
top: '#8b6914',
|
||
left: '#7b5904',
|
||
right: '#6b4904',
|
||
name: 'Дерево',
|
||
pattern: 'wood'
|
||
},
|
||
lava: {
|
||
top: '#cc4400',
|
||
left: '#bb3300',
|
||
right: '#aa2200',
|
||
name: 'Лава',
|
||
pattern: 'lava',
|
||
animated: true
|
||
},
|
||
snow: {
|
||
top: '#e8e8f0',
|
||
left: '#d8d8e0',
|
||
right: '#c8c8d0',
|
||
name: 'Снег',
|
||
pattern: 'snow'
|
||
},
|
||
dirt: {
|
||
top: '#8b5a2b',
|
||
left: '#7b4a1b',
|
||
right: '#6b3a0b',
|
||
name: 'Земля',
|
||
pattern: 'dirt'
|
||
},
|
||
cobblestone: {
|
||
top: '#707070',
|
||
left: '#606060',
|
||
right: '#505050',
|
||
name: 'Булыжник',
|
||
pattern: 'cobblestone'
|
||
}
|
||
},
|
||
|
||
// Инициализация
|
||
init(canvasId) {
|
||
this.canvas = document.getElementById(canvasId);
|
||
this.ctx = this.canvas.getContext('2d');
|
||
this.ctx.imageSmoothingEnabled = false;
|
||
|
||
// Создание текстурных паттернов
|
||
this.createPatterns();
|
||
},
|
||
|
||
// Создание паттернов для текстур
|
||
createPatterns() {
|
||
// Трава
|
||
this.patterns.grass = this.createGrassPattern();
|
||
// Камень
|
||
this.patterns.stone = this.createStonePattern();
|
||
// Песок
|
||
this.patterns.sand = this.createSandPattern();
|
||
// Кирпич
|
||
this.patterns.brick = this.createBrickPattern();
|
||
// Дерево
|
||
this.patterns.wood = this.createWoodPattern();
|
||
// Булыжник
|
||
this.patterns.cobblestone = this.createCobblePattern();
|
||
// Земля
|
||
this.patterns.dirt = this.createDirtPattern();
|
||
},
|
||
|
||
// Создание текстуры травы
|
||
createGrassPattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
// Базовый цвет
|
||
pctx.fillStyle = '#4a7c3f';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
// Травинки
|
||
pctx.strokeStyle = '#5a9c4f';
|
||
pctx.lineWidth = 1;
|
||
for (let i = 0; i < 20; i++) {
|
||
const x = Math.random() * 32;
|
||
const y = Math.random() * 32;
|
||
const h = 4 + Math.random() * 6;
|
||
pctx.beginPath();
|
||
pctx.moveTo(x, y);
|
||
pctx.lineTo(x + Math.random() * 4 - 2, y - h);
|
||
pctx.stroke();
|
||
}
|
||
|
||
// Тёмные пятна
|
||
pctx.fillStyle = '#3d6834';
|
||
for (let i = 0; i < 5; i++) {
|
||
pctx.beginPath();
|
||
pctx.arc(Math.random() * 32, Math.random() * 32, 2 + Math.random() * 3, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
}
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Создание текстуры камня
|
||
createStonePattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
pctx.fillStyle = '#6a6a6a';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
// Плитки
|
||
pctx.strokeStyle = '#5a5a5a';
|
||
pctx.lineWidth = 1;
|
||
pctx.strokeRect(0, 0, 16, 16);
|
||
pctx.strokeRect(16, 0, 16, 16);
|
||
pctx.strokeRect(0, 16, 16, 16);
|
||
pctx.strokeRect(16, 16, 16, 16);
|
||
|
||
// Пятна
|
||
pctx.fillStyle = '#5a5a5a';
|
||
pctx.fillRect(5, 5, 4, 3);
|
||
pctx.fillRect(20, 10, 3, 4);
|
||
pctx.fillRect(8, 22, 5, 3);
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Создание текстуры песка
|
||
createSandPattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
pctx.fillStyle = '#c4a86c';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
// Песчинки
|
||
pctx.fillStyle = '#d4b87c';
|
||
for (let i = 0; i < 30; i++) {
|
||
pctx.fillRect(Math.random() * 32, Math.random() * 32, 1, 1);
|
||
}
|
||
|
||
pctx.fillStyle = '#b4985c';
|
||
for (let i = 0; i < 20; i++) {
|
||
pctx.fillRect(Math.random() * 32, Math.random() * 32, 1, 1);
|
||
}
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Создание текстуры кирпича
|
||
createBrickPattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
pctx.fillStyle = '#5a4a3a';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
pctx.strokeStyle = '#3a2a1a';
|
||
pctx.lineWidth = 2;
|
||
|
||
// Горизонтальные линии
|
||
pctx.beginPath();
|
||
pctx.moveTo(0, 8);
|
||
pctx.lineTo(32, 8);
|
||
pctx.moveTo(0, 24);
|
||
pctx.lineTo(32, 24);
|
||
pctx.stroke();
|
||
|
||
// Вертикальные линии (со смещением)
|
||
pctx.beginPath();
|
||
pctx.moveTo(16, 0);
|
||
pctx.lineTo(16, 8);
|
||
pctx.moveTo(8, 8);
|
||
pctx.lineTo(8, 24);
|
||
pctx.moveTo(24, 8);
|
||
pctx.lineTo(24, 24);
|
||
pctx.moveTo(16, 24);
|
||
pctx.lineTo(16, 32);
|
||
pctx.stroke();
|
||
|
||
// Оттенки кирпичей
|
||
pctx.fillStyle = '#6a5a4a';
|
||
pctx.fillRect(2, 2, 12, 4);
|
||
pctx.fillStyle = '#4a3a2a';
|
||
pctx.fillRect(18, 2, 12, 4);
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Создание текстуры дерева
|
||
createWoodPattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
pctx.fillStyle = '#8b6914';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
// Волокна
|
||
pctx.strokeStyle = '#7b5914';
|
||
pctx.lineWidth = 1;
|
||
for (let i = 0; i < 8; i++) {
|
||
pctx.beginPath();
|
||
pctx.moveTo(i * 4 + 2, 0);
|
||
pctx.lineTo(i * 4 + 2, 32);
|
||
pctx.stroke();
|
||
}
|
||
|
||
// Годовые кольца
|
||
pctx.strokeStyle = '#6b4914';
|
||
pctx.beginPath();
|
||
pctx.arc(16, 16, 8, 0, Math.PI * 2);
|
||
pctx.stroke();
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Создание текстуры булыжника
|
||
createCobblePattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
pctx.fillStyle = '#707070';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
// Камни
|
||
pctx.fillStyle = '#606060';
|
||
pctx.beginPath();
|
||
pctx.arc(8, 8, 7, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
|
||
pctx.beginPath();
|
||
pctx.arc(24, 8, 6, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
|
||
pctx.beginPath();
|
||
pctx.arc(8, 24, 6, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
|
||
pctx.beginPath();
|
||
pctx.arc(24, 24, 7, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
|
||
pctx.beginPath();
|
||
pctx.arc(16, 16, 5, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Создание текстуры земли
|
||
createDirtPattern() {
|
||
const patternCanvas = document.createElement('canvas');
|
||
patternCanvas.width = 32;
|
||
patternCanvas.height = 32;
|
||
const pctx = patternCanvas.getContext('2d');
|
||
|
||
pctx.fillStyle = '#8b5a2b';
|
||
pctx.fillRect(0, 0, 32, 32);
|
||
|
||
// Комочки
|
||
pctx.fillStyle = '#7b4a1b';
|
||
for (let i = 0; i < 15; i++) {
|
||
pctx.beginPath();
|
||
pctx.arc(Math.random() * 32, Math.random() * 32, 2 + Math.random() * 2, 0, Math.PI * 2);
|
||
pctx.fill();
|
||
}
|
||
|
||
pctx.fillStyle = '#9b6a3b';
|
||
for (let i = 0; i < 10; i++) {
|
||
pctx.fillRect(Math.random() * 32, Math.random() * 32, 1, 1);
|
||
}
|
||
|
||
return this.ctx.createPattern(patternCanvas, 'repeat');
|
||
},
|
||
|
||
// Конвертация изометрических координат в экранные
|
||
toIso(x, y) {
|
||
return {
|
||
x: (x - y) * (this.TILE_WIDTH / 2) + this.canvas.width / 2,
|
||
y: (x + y) * (this.TILE_HEIGHT / 2) + 80
|
||
};
|
||
},
|
||
|
||
// Конвертация экранных координат в изометрические
|
||
fromIso(screenX, screenY) {
|
||
const adjX = screenX - this.canvas.width / 2;
|
||
const adjY = screenY - 80;
|
||
|
||
const x = (adjX / (this.TILE_WIDTH / 2) + adjY / (this.TILE_HEIGHT / 2)) / 2;
|
||
const y = (adjY / (this.TILE_HEIGHT / 2) - adjX / (this.TILE_WIDTH / 2)) / 2;
|
||
|
||
return { x: Math.floor(x), y: Math.floor(y) };
|
||
},
|
||
|
||
// Отрисовка изометрического тайла с текстурой
|
||
drawTile(x, y, tileType, highlight = false, hover = false, time = 0) {
|
||
const pos = this.toIso(x, y);
|
||
const colors = this.TILE_COLORS[tileType] || this.TILE_COLORS.grass;
|
||
|
||
// Модификация цвета для анимации (вода, лава)
|
||
let topColor = colors.top;
|
||
if (colors.animated) {
|
||
const wave = Math.sin(time / 500 + x + y) * 10;
|
||
topColor = this.adjustBrightness(colors.top, wave);
|
||
}
|
||
|
||
// Верхняя грань с текстурой
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(pos.x, pos.y - this.TILE_HEIGHT / 2);
|
||
this.ctx.lineTo(pos.x + this.TILE_WIDTH / 2, pos.y);
|
||
this.ctx.lineTo(pos.x, pos.y + this.TILE_HEIGHT / 2);
|
||
this.ctx.lineTo(pos.x - this.TILE_WIDTH / 2, pos.y);
|
||
this.ctx.closePath();
|
||
|
||
// Используем паттерн или цвет
|
||
if (this.patterns[colors.pattern]) {
|
||
this.ctx.save();
|
||
this.ctx.translate(pos.x - this.TILE_WIDTH/2, pos.y - this.TILE_HEIGHT/2);
|
||
this.ctx.scale(1, 0.5);
|
||
this.ctx.rotate(Math.PI / 4);
|
||
this.ctx.translate(-pos.x, -pos.y);
|
||
this.ctx.fillStyle = this.patterns[colors.pattern];
|
||
this.ctx.fill();
|
||
this.ctx.restore();
|
||
} else {
|
||
this.ctx.fillStyle = topColor;
|
||
this.ctx.fill();
|
||
}
|
||
|
||
this.ctx.strokeStyle = hover ? '#ffffff' : 'rgba(0,0,0,0.4)';
|
||
this.ctx.lineWidth = hover ? 2 : 1;
|
||
this.ctx.stroke();
|
||
|
||
// Левая грань
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(pos.x - this.TILE_WIDTH / 2, pos.y);
|
||
this.ctx.lineTo(pos.x, pos.y + this.TILE_HEIGHT / 2);
|
||
this.ctx.lineTo(pos.x, pos.y + this.TILE_HEIGHT / 2 + this.TILE_DEPTH);
|
||
this.ctx.lineTo(pos.x - this.TILE_WIDTH / 2, pos.y + this.TILE_DEPTH);
|
||
this.ctx.closePath();
|
||
this.ctx.fillStyle = colors.left;
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = 'rgba(0,0,0,0.4)';
|
||
this.ctx.lineWidth = 1;
|
||
this.ctx.stroke();
|
||
|
||
// Правая грань
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(pos.x + this.TILE_WIDTH / 2, pos.y);
|
||
this.ctx.lineTo(pos.x, pos.y + this.TILE_HEIGHT / 2);
|
||
this.ctx.lineTo(pos.x, pos.y + this.TILE_HEIGHT / 2 + this.TILE_DEPTH);
|
||
this.ctx.lineTo(pos.x + this.TILE_WIDTH / 2, pos.y + this.TILE_DEPTH);
|
||
this.ctx.closePath();
|
||
this.ctx.fillStyle = colors.right;
|
||
this.ctx.fill();
|
||
this.ctx.stroke();
|
||
|
||
// Декорации на тайлах
|
||
this.drawTileDecorations(x, y, tileType, pos, time);
|
||
|
||
// Название тайла при наведении
|
||
if (hover) {
|
||
this.ctx.fillStyle = '#ffffff';
|
||
this.ctx.font = '12px Arial';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText(colors.name, pos.x, pos.y + 5);
|
||
}
|
||
},
|
||
|
||
// Отрисовка декораций на тайлах
|
||
drawTileDecorations(x, y, tileType, pos, time) {
|
||
// Деревья на траве
|
||
if (tileType === 0) { // grass
|
||
// Случайные декорации на некоторых клетках
|
||
const seed = (x * 7 + y * 13) % 10;
|
||
if (seed < 2) {
|
||
this.drawTree(pos.x, pos.y, 0.6 + (seed * 0.1));
|
||
} else if (seed < 4) {
|
||
this.drawFlower(pos.x, pos.y, seed);
|
||
}
|
||
}
|
||
|
||
// Камни
|
||
if (tileType === 2) { // stone
|
||
const seed = (x * 5 + y * 11) % 7;
|
||
if (seed < 2) {
|
||
this.drawRock(pos.x, pos.y, seed);
|
||
}
|
||
}
|
||
|
||
// Вода - рябь
|
||
if (tileType === 1) { // water
|
||
const wave = Math.sin(time / 300 + x * 0.5 + y * 0.5) * 2;
|
||
this.ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x + wave, pos.y, 8, 3, 0, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
}
|
||
},
|
||
|
||
// Отрисовка дерева
|
||
drawTree(x, y, scale = 1) {
|
||
const trunkHeight = 15 * scale;
|
||
const crownRadius = 20 * scale;
|
||
|
||
// Ствол
|
||
this.ctx.fillStyle = '#5a3a1a';
|
||
this.ctx.fillRect(x - 3 * scale, y - trunkHeight, 6 * scale, trunkHeight + 5);
|
||
|
||
// Крона
|
||
this.ctx.fillStyle = '#2a5a2a';
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(x, y - trunkHeight - crownRadius);
|
||
this.ctx.lineTo(x - crownRadius * 0.8, y - trunkHeight * 0.5);
|
||
this.ctx.lineTo(x + crownRadius * 0.8, y - trunkHeight * 0.5);
|
||
this.ctx.closePath();
|
||
this.ctx.fill();
|
||
|
||
this.ctx.fillStyle = '#3a6a3a';
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(x - crownRadius * 0.6, y - trunkHeight - crownRadius * 0.5);
|
||
this.ctx.lineTo(x + crownRadius * 0.6, y - trunkHeight - crownRadius * 0.5);
|
||
this.ctx.lineTo(x, y - trunkHeight - crownRadius);
|
||
this.ctx.closePath();
|
||
this.ctx.fill();
|
||
},
|
||
|
||
// Отрисовка цветка
|
||
drawFlower(x, y, seed) {
|
||
const colors = ['#ff6b6b', '#ffff6b', '#ff6bff', '#ffffff'];
|
||
const color = colors[seed % colors.length];
|
||
|
||
// Стебель
|
||
this.ctx.strokeStyle = '#3a5a3a';
|
||
this.ctx.lineWidth = 1;
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(x, y);
|
||
this.ctx.lineTo(x, y - 10);
|
||
this.ctx.stroke();
|
||
|
||
// Лепестки
|
||
this.ctx.fillStyle = color;
|
||
for (let i = 0; i < 5; i++) {
|
||
const angle = (i / 5) * Math.PI * 2;
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(x + Math.cos(angle) * 3, y - 10 + Math.sin(angle) * 3, 2, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
}
|
||
|
||
// Центр
|
||
this.ctx.fillStyle = '#ffff00';
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(x, y - 10, 1.5, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
},
|
||
|
||
// Отрисовка камня
|
||
drawRock(x, y, seed) {
|
||
const size = 8 + seed * 2;
|
||
this.ctx.fillStyle = '#5a5a5a';
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(x, y - size/2, size, size/2, 0, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
|
||
this.ctx.fillStyle = '#6a6a6a';
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(x - 2, y - size/2 - 2, size * 0.6, size * 0.3, 0, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
},
|
||
|
||
// Отрисовка игрока
|
||
drawPlayer(player, time) {
|
||
let drawX = player.x;
|
||
let drawY = player.y;
|
||
|
||
if (player.isMoving) {
|
||
drawX = player.x + (player.targetX - player.x) * player.moveProgress;
|
||
drawY = player.y + (player.targetY - player.y) * player.moveProgress;
|
||
}
|
||
|
||
const pos = this.toIso(drawX, drawY);
|
||
const height = 50;
|
||
const bob = Math.sin(time / 200) * 2;
|
||
const walkBob = player.isMoving ? Math.sin(time / 100) * 3 : 0;
|
||
|
||
// Тень
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y + 10, 20, 10, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||
this.ctx.fill();
|
||
|
||
// Цвет в зависимости от класса
|
||
const classColors = {
|
||
warrior: { body: '#e74c3c', outline: '#c0392b' },
|
||
mage: { body: '#3498db', outline: '#2980b9' },
|
||
archer: { body: '#2ecc71', outline: '#27ae60' },
|
||
thief: { body: '#9b59b6', outline: '#8e44ad' }
|
||
};
|
||
|
||
const classColor = classColors[player.class] || classColors.warrior;
|
||
|
||
// Тело персонажа
|
||
const bodyColor = player.gender === 'male' ? classColor.body : '#e91e63';
|
||
|
||
// Основное тело
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y - 5 + walkBob, 15, 8, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = bodyColor;
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = classColor.outline;
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Верх тела
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y - height + walkBob, 15, 8, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = this.lightenColor(bodyColor, 20);
|
||
this.ctx.fill();
|
||
this.ctx.stroke();
|
||
|
||
// Детали одежды
|
||
this.ctx.fillStyle = '#ffd700'; // Золотая пряжка
|
||
this.ctx.fillRect(pos.x - 3, pos.y - 15 + walkBob, 6, 4);
|
||
|
||
// Голова
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, pos.y - height - 15 + bob, 12, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = '#ffcc99';
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#ddaa77';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Волосы
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, pos.y - height - 18 + bob, 10, Math.PI, 0);
|
||
this.ctx.fillStyle = player.hairColor || '#4a3520';
|
||
this.ctx.fill();
|
||
|
||
// Глаза
|
||
this.ctx.fillStyle = '#333';
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x - 4, pos.y - height - 15 + bob, 2, 0, Math.PI * 2);
|
||
this.ctx.arc(pos.x + 4, pos.y - height - 15 + bob, 2, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
|
||
// Брови
|
||
this.ctx.strokeStyle = '#333';
|
||
this.ctx.lineWidth = 1;
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(pos.x - 6, pos.y - height - 18 + bob);
|
||
this.ctx.lineTo(pos.x - 2, pos.y - height - 19 + bob);
|
||
this.ctx.moveTo(pos.x + 2, pos.y - height - 19 + bob);
|
||
this.ctx.lineTo(pos.x + 6, pos.y - height - 18 + bob);
|
||
this.ctx.stroke();
|
||
|
||
// Уровень над головой
|
||
this.ctx.fillStyle = '#ffd700';
|
||
this.ctx.font = 'bold 11px Arial';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText('Lv.' + player.level, pos.x, pos.y - height - 35);
|
||
|
||
// Эффект свечения если есть мана
|
||
if (player.class === 'mage') {
|
||
this.ctx.strokeStyle = 'rgba(100, 100, 255, 0.5)';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, pos.y - height - 10 + bob, 18, 0, Math.PI * 2);
|
||
this.ctx.stroke();
|
||
}
|
||
},
|
||
|
||
// Отрисовка предмета
|
||
drawItem(item, time) {
|
||
const pos = this.toIso(item.x, item.y);
|
||
const bounce = Math.sin(time / 300) * 5;
|
||
const rotate = time / 1000;
|
||
|
||
if (item.type === 'gold' || item.type === 'coin') {
|
||
// Монетка
|
||
this.ctx.save();
|
||
this.ctx.translate(pos.x, pos.y - 15 - bounce);
|
||
|
||
// Блик
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = '#ffd700';
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#cc9900';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
this.ctx.fillStyle = '#cc9900';
|
||
this.ctx.font = 'bold 12px Arial';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText('$', 0, 4);
|
||
|
||
// Искры
|
||
this.ctx.fillStyle = '#ffffff';
|
||
for (let i = 0; i < 3; i++) {
|
||
const sparkAngle = time / 200 + i * 2;
|
||
const sx = Math.cos(sparkAngle) * 12;
|
||
const sy = Math.sin(sparkAngle) * 12;
|
||
this.ctx.fillRect(sx, sy, 2, 2);
|
||
}
|
||
|
||
this.ctx.restore();
|
||
|
||
} else if (item.type === 'potion' || item.type === 'health_potion') {
|
||
// Зелье здоровья
|
||
this._drawPotion(pos.x, pos.y - 15 - bounce, '#ff4444', '#cc2222');
|
||
|
||
} else if (item.type === 'mana_potion') {
|
||
// Зелье маны
|
||
this._drawPotion(pos.x, pos.y - 15 - bounce, '#4444ff', '#2222cc');
|
||
|
||
} else if (item.type === 'weapon') {
|
||
// Оружие
|
||
this._drawWeapon(pos.x, pos.y - 15 - bounce, item.subtype);
|
||
|
||
} else if (item.type === 'armor') {
|
||
// Броня
|
||
this._drawArmor(pos.x, pos.y - 15 - bounce, item.subtype);
|
||
|
||
} else if (item.type === 'quest') {
|
||
// Квестовый предмет
|
||
this.ctx.save();
|
||
this.ctx.translate(pos.x, pos.y - 20 - bounce);
|
||
this.ctx.rotate(Math.sin(time / 500) * 0.2);
|
||
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(0, -15);
|
||
this.ctx.lineTo(-10, 0);
|
||
this.ctx.lineTo(10, 0);
|
||
this.ctx.closePath();
|
||
this.ctx.fillStyle = '#ff00ff';
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#aa00aa';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
this.ctx.fillStyle = '#ffffff';
|
||
this.ctx.font = 'bold 14px Arial';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText('!', 0, 5);
|
||
|
||
this.ctx.restore();
|
||
}
|
||
},
|
||
|
||
_drawPotion(x, y, color, darkColor) {
|
||
// Бутылка
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(x, y - 15);
|
||
this.ctx.lineTo(x - 8, y + 5);
|
||
this.ctx.lineTo(x + 8, y + 5);
|
||
this.ctx.closePath();
|
||
this.ctx.fillStyle = color;
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = darkColor;
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Жидкость внутри
|
||
this.ctx.fillStyle = this.lightenColor(color, 30);
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(x - 4, y - 8);
|
||
this.ctx.lineTo(x - 6, y + 3);
|
||
this.ctx.lineTo(x + 6, y + 3);
|
||
this.ctx.lineTo(x + 4, y - 8);
|
||
this.ctx.closePath();
|
||
this.ctx.fill();
|
||
|
||
// Пробка
|
||
this.ctx.fillStyle = '#8b4513';
|
||
this.ctx.fillRect(x - 4, y - 20, 8, 8);
|
||
|
||
// Блик
|
||
this.ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(x - 3, y - 5, 2, 4, 0, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
},
|
||
|
||
_drawWeapon(x, y, subtype) {
|
||
this.ctx.save();
|
||
this.ctx.translate(x, y);
|
||
|
||
if (subtype === 'sword') {
|
||
// Клинок
|
||
const gradient = this.ctx.createLinearGradient(0, -15, 0, 5);
|
||
gradient.addColorStop(0, '#e0e0e0');
|
||
gradient.addColorStop(0.5, '#c0c0c0');
|
||
gradient.addColorStop(1, '#909090');
|
||
|
||
this.ctx.fillStyle = gradient;
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(0, -20);
|
||
this.ctx.lineTo(-4, 0);
|
||
this.ctx.lineTo(0, 2);
|
||
this.ctx.lineTo(4, 0);
|
||
this.ctx.closePath();
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#707070';
|
||
this.ctx.lineWidth = 1;
|
||
this.ctx.stroke();
|
||
|
||
// Рукоять
|
||
this.ctx.fillStyle = '#8b4513';
|
||
this.ctx.fillRect(-2, 2, 4, 8);
|
||
|
||
// Гарда
|
||
this.ctx.fillStyle = '#ffd700';
|
||
this.ctx.fillRect(-6, 0, 12, 3);
|
||
}
|
||
|
||
this.ctx.restore();
|
||
},
|
||
|
||
_drawArmor(x, y, subtype) {
|
||
this.ctx.save();
|
||
this.ctx.translate(x, y);
|
||
|
||
// Броня
|
||
this.ctx.fillStyle = '#4a4a6a';
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(0, 0, 12, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#3a3a5a';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Узор
|
||
this.ctx.strokeStyle = '#6a6a8a';
|
||
this.ctx.lineWidth = 1;
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(0, 0, 8, 0, Math.PI * 2);
|
||
this.ctx.stroke();
|
||
|
||
// Блик
|
||
this.ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(-3, -3, 4, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
|
||
this.ctx.restore();
|
||
},
|
||
|
||
// Отрисовка врага
|
||
drawEnemy(enemy, time) {
|
||
const pos = this.toIso(enemy.x, enemy.y);
|
||
const height = enemy.height || 45;
|
||
const bob = Math.sin(time / 200) * 3;
|
||
const attackBob = enemy.isAttacking ? Math.sin(time / 50) * 5 : 0;
|
||
|
||
// Тень
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y + 10, 18, 9, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||
this.ctx.fill();
|
||
|
||
// Цвет в зависимости от типа врага
|
||
let bodyColor, headColor, eyeColor, nameColor;
|
||
|
||
switch(enemy.type) {
|
||
case 'goblin':
|
||
bodyColor = '#4a6a3a';
|
||
headColor = '#5a7a4a';
|
||
eyeColor = '#ff0000';
|
||
nameColor = '#ff6b6b';
|
||
break;
|
||
case 'orc':
|
||
bodyColor = '#4a5a3a';
|
||
headColor = '#5a6a4a';
|
||
eyeColor = '#ff4400';
|
||
nameColor = '#ff8844';
|
||
break;
|
||
case 'skeleton':
|
||
bodyColor = '#d0d0c0';
|
||
headColor = '#e0e0d0';
|
||
eyeColor = '#00ff00';
|
||
nameColor = '#cccccc';
|
||
break;
|
||
case 'dragon':
|
||
bodyColor = '#8b0000';
|
||
headColor = '#a00000';
|
||
eyeColor = '#ffff00';
|
||
nameColor = '#ff0000';
|
||
break;
|
||
case 'slime':
|
||
bodyColor = '#00aa00';
|
||
headColor = '#00cc00';
|
||
eyeColor = '#ffffff';
|
||
nameColor = '#44ff44';
|
||
break;
|
||
case 'bandit':
|
||
bodyColor = '#6a5a4a';
|
||
headColor = '#7a6a5a';
|
||
eyeColor = '#ff6600';
|
||
nameColor = '#cc9944';
|
||
break;
|
||
case 'mage':
|
||
bodyColor = '#4a3a6a';
|
||
headColor = '#5a4a7a';
|
||
eyeColor = '#ff00ff';
|
||
nameColor = '#aa44ff';
|
||
break;
|
||
default:
|
||
bodyColor = '#6b4a3a';
|
||
headColor = '#8a6a5a';
|
||
eyeColor = '#ff0000';
|
||
nameColor = '#ffffff';
|
||
}
|
||
|
||
// Анимация атаки
|
||
const drawY = pos.y + attackBob;
|
||
|
||
// Тело
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, drawY - 5 + bob, 14, 7, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = bodyColor;
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#000000';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Детали тела
|
||
if (enemy.type === 'bandit' || enemy.type === 'orc') {
|
||
// Ремень
|
||
this.ctx.fillStyle = '#3a2a1a';
|
||
this.ctx.fillRect(pos.x - 10, drawY - 7 + bob, 20, 3);
|
||
|
||
// Пряжка
|
||
this.ctx.fillStyle = '#ffd700';
|
||
this.ctx.fillRect(pos.x - 2, drawY - 8 + bob, 4, 5);
|
||
}
|
||
|
||
// Голова
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, drawY - height + bob, 11, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = headColor;
|
||
this.ctx.fill();
|
||
this.ctx.stroke();
|
||
|
||
// Глаза
|
||
this.ctx.fillStyle = eyeColor;
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x - 3, drawY - height - 2 + bob, 2, 0, Math.PI * 2);
|
||
this.ctx.arc(pos.x + 3, drawY - height - 2 + bob, 2, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
|
||
// Рот (для некоторых врагов)
|
||
if (enemy.type === 'orc' || enemy.type === 'goblin') {
|
||
this.ctx.fillStyle = '#2a1a0a';
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, drawY - height + 5 + bob, 4, 0, Math.PI);
|
||
this.ctx.fill();
|
||
|
||
// Клыки
|
||
this.ctx.fillStyle = '#ffffff';
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(pos.x - 3, drawY - height + 5 + bob);
|
||
this.ctx.lineTo(pos.x - 1, drawY - height + 8 + bob);
|
||
this.ctx.lineTo(pos.x + 1, drawY - height + 5 + bob);
|
||
this.ctx.fill();
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(pos.x + 1, drawY - height + 5 + bob);
|
||
this.ctx.lineTo(pos.x + 3, drawY - height + 8 + bob);
|
||
this.ctx.lineTo(pos.x + 5, drawY - height + 5 + bob);
|
||
this.ctx.fill();
|
||
}
|
||
|
||
// HP бар
|
||
const barWidth = 30;
|
||
const barHeight = 5;
|
||
const hpPercent = enemy.hp / enemy.maxHp;
|
||
|
||
this.ctx.fillStyle = '#333333';
|
||
this.ctx.fillRect(pos.x - barWidth/2, drawY - height - 15 + bob, barWidth, barHeight);
|
||
|
||
const hpColor = hpPercent > 0.5 ? '#44ff44' : hpPercent > 0.25 ? '#ffff44' : '#ff4444';
|
||
this.ctx.fillStyle = hpColor;
|
||
this.ctx.fillRect(pos.x - barWidth/2, drawY - height - 15 + bob, barWidth * hpPercent, barHeight);
|
||
|
||
this.ctx.strokeStyle = '#000000';
|
||
this.ctx.lineWidth = 1;
|
||
this.ctx.strokeRect(pos.x - barWidth/2, drawY - height - 15 + bob, barWidth, barHeight);
|
||
|
||
// MP бар если есть
|
||
if (enemy.mp !== undefined) {
|
||
const mpPercent = enemy.mp / enemy.maxMp;
|
||
this.ctx.fillStyle = '#333333';
|
||
this.ctx.fillRect(pos.x - barWidth/2, drawY - height - 9 + bob, barWidth, 3);
|
||
this.ctx.fillStyle = '#4444ff';
|
||
this.ctx.fillRect(pos.x - barWidth/2, drawY - height - 9 + bob, barWidth * mpPercent, 3);
|
||
}
|
||
|
||
// Имя врага с цветом
|
||
this.ctx.fillStyle = nameColor;
|
||
this.ctx.font = 'bold 10px Arial';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText(enemy.name, pos.x, drawY - height - 22 + bob);
|
||
|
||
// Уровень
|
||
this.ctx.fillStyle = '#888888';
|
||
this.ctx.font = '9px Arial';
|
||
this.ctx.fillText('Lv.' + enemy.level, pos.x, drawY - height - 32 + bob);
|
||
|
||
// Угрожающая аура для боссов
|
||
if (enemy.isBoss) {
|
||
this.ctx.strokeStyle = `rgba(255, 0, 0, ${0.3 + Math.sin(time / 200) * 0.2})`;
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, drawY - height/2, 25, 0, Math.PI * 2);
|
||
this.ctx.stroke();
|
||
}
|
||
},
|
||
|
||
// Отрисовка NPC
|
||
drawNPC(npc, time) {
|
||
const pos = this.toIso(npc.x, npc.y);
|
||
const height = 45;
|
||
const bob = Math.sin(time / 300) * 2;
|
||
|
||
// Тень
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y + 10, 18, 9, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||
this.ctx.fill();
|
||
|
||
// Одежда NPC
|
||
const clothColor = npc.color || '#8b6914';
|
||
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y - 5 + bob, 14, 7, 0, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = clothColor;
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#000000';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Детали одежды
|
||
this.ctx.fillStyle = this.lightenColor(clothColor, 30);
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(pos.x, pos.y - height + bob, 12, 6, 0, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
this.ctx.stroke();
|
||
|
||
// Голова
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(pos.x, pos.y - height - 12 + bob, 10, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = '#ffcc99';
|
||
this.ctx.fill();
|
||
this.ctx.strokeStyle = '#ddaa77';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
|
||
// Индиктор диалога
|
||
const bounce = Math.sin(time / 200) * 5;
|
||
this.ctx.fillStyle = '#ffff00';
|
||
this.ctx.font = 'bold 16px Arial';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText('...', pos.x, pos.y - height - 30 + bounce);
|
||
|
||
// Имя NPC
|
||
this.ctx.fillStyle = '#88ff88';
|
||
this.ctx.font = 'bold 11px Arial';
|
||
this.ctx.fillText(npc.name, pos.x, pos.y - height - 45);
|
||
},
|
||
|
||
// Отрисовка всей карты
|
||
drawMap(gameMap, highlightTile, hoverTile, time = 0) {
|
||
const tiles = [];
|
||
for (let y = 0; y < gameMap.length; y++) {
|
||
for (let x = 0; x < gameMap[y].length; x++) {
|
||
tiles.push({ x, y, type: gameMap[y][x] });
|
||
}
|
||
}
|
||
|
||
tiles.sort((a, b) => (a.x + a.y) - (b.x + b.y));
|
||
tiles.forEach(tile => {
|
||
const isHighlight = highlightTile && highlightTile.x === tile.x && highlightTile.y === tile.y;
|
||
const isHover = hoverTile && hoverTile.x === tile.x && hoverTile.y === tile.y;
|
||
this.drawTile(tile.x, tile.y, tile.type, isHighlight, isHover, time);
|
||
});
|
||
|
||
return tiles;
|
||
},
|
||
|
||
// Очистка canvas
|
||
clear() {
|
||
this.ctx.fillStyle = '#0a0a15';
|
||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||
|
||
// Градиентный фон
|
||
const gradient = this.ctx.createRadialGradient(
|
||
this.canvas.width / 2, this.canvas.height / 2, 0,
|
||
this.canvas.width / 2, this.canvas.height / 2, this.canvas.width
|
||
);
|
||
gradient.addColorStop(0, '#1a1a2e');
|
||
gradient.addColorStop(1, '#0a0a15');
|
||
this.ctx.fillStyle = gradient;
|
||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||
},
|
||
|
||
// Осветление цвета
|
||
adjustBrightness(color, amount) {
|
||
const num = parseInt(color.replace('#', ''), 16);
|
||
const R = Math.max(0, Math.min(255, (num >> 16) + amount));
|
||
const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
|
||
const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
|
||
|
||
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
|
||
},
|
||
|
||
lightenColor(color, percent) {
|
||
return this.adjustBrightness(color, percent);
|
||
},
|
||
|
||
// Отрисовка текста с тенью
|
||
drawText(text, x, y, color, size = '14px', align = 'left') {
|
||
this.ctx.font = `${size} Arial`;
|
||
this.ctx.textAlign = align;
|
||
this.ctx.fillStyle = '#000000';
|
||
this.ctx.fillText(text, x + 1, y + 1);
|
||
this.ctx.fillStyle = color;
|
||
this.ctx.fillText(text, x, y);
|
||
}
|
||
};
|