diff --git a/RPG_FromFreeModel/README.md b/RPG_FromFreeModel/README.md
new file mode 100644
index 0000000..b06d285
--- /dev/null
+++ b/RPG_FromFreeModel/README.md
@@ -0,0 +1,138 @@
+# Isometric RPG 2.5D на HTML5 Canvas
+
+## 📋 Описание проекта
+
+Полноценная изометрическая RPG-игра в 2.5D стиле, созданная на чистом JavaScript с использованием HTML5 Canvas. Игра включает систему классов, инвентарь, экипировку, боевую систему, квесты и несколько локаций.
+
+---
+
+## 🎮 Особенности игры
+
+### Изометрическая графика
+- Псевдо-3D (2.5D) изометрическая проекция
+- Текстуры для тайлов и объектов
+- Система освещения (день/ночь)
+- Погодные эффекты (дождь, снег)
+
+### Система классов
+6 уникальных классов с разным стартовым снаряжением:
+
+| Класс | Оружие | Стартовые статы |
+|-------|--------|-----------------|
+| Воин (Warrior) | Меч | STR:15, DEF:10 |
+| Маг (Mage) | Посох | INT:15, DEF:5 |
+| Лучник (Archer) | Лук | DEX:15, DEF:5 |
+| Плут (Thief) | Кинжал | DEX:12, SPD:12 |
+| Паладин (Paladin) | Булава | STR:12, DEF:12 |
+| Некромант (Necromancer) | Посох | INT:12, DEF:8 |
+
+### Инвентарь и экипировка
+- 20 слотов в инвентаре
+- 2 слота экипировки (оружие, щит)
+- Цветовая кодировка редкости предметов (обычный → легендарный)
+- Статы от экипировки добавляются к персонажу
+- Клик для экипировки/снятия
+
+### Боевая система
+- Пошаговые бои
+- Физический и магический урон
+- Защита и сопротивление
+- Заклинания для каждого класса
+- Отображение HP с анимацией
+
+### Квесты и локации
+- 4 локации: Деревня, Лес, Подземелье, Пещера
+- Порталы для быстрого перемещения (клавиша M)
+- Система квестов с наградами
+
+---
+
+## 🎛 Управление
+
+| Клавиша | Действие |
+|---------|----------|
+| **WASD / Стрелки** | Движение персонажа |
+| **I** | Открыть инвентарь |
+| **M** | Меню перемещения |
+| **E** | Взаимодействие |
+| **Пробел** | Атака / Пропустить |
+| **ESC** | Закрыть меню |
+
+---
+
+## 🏗 Архитектура проекта
+
+```
+Рпг/
+├── index.html # HTML с Canvas и UI
+├── renderer.js # Изометрический рендерер
+├── rpg.js # RPG-механики (классы, предметы, статы)
+└── game.js # Основная логика игры
+```
+
+### Основные модули:
+
+**renderer.js**
+- Изометрическая проекция (2:1 соотношение)
+- Текстуры тайлов
+- Рендеринг персонажей и объектов
+- День/ночь, погода
+
+**rpg.js**
+- `Character` - класс персонажа
+- `Item` - система предметов
+- `RPG` - основной класс с getTotalStats()
+
+**game.js**
+- Игровой цикл
+- Обработка ввода
+- UI (меню, диалоги, инвентарь)
+- Локации и переходы
+
+---
+
+## 🎨 UI/UX
+
+### Стартовое меню
+- Сетка 3x2 с выбором класса
+- Иконки классов
+- Информация о стартовом оружии
+- Кнопка продолжения игры
+
+### Инвентарь
+- Сетка предметов
+- Слоты экипировки (крупные, справа)
+- Подсказки при наведении
+- Клик для экипировки
+
+### Боевая система
+- Кнопки действий
+- Полосы HP
+- Отображение урона
+
+---
+
+## 🔧 Технические детали
+
+- **Canvas API** для рендеринга
+- **requestAnimationFrame** для игрового цикла
+- **localStorage** для сохранений
+- **JSON** для данных предметов и квестов
+
+---
+
+## 🚀 Запуск
+
+Просто откройте `index.html` в браузере. Никаких зависимостей не требуется.
+
+---
+
+## 📝 To-Do / Возможности для расширения
+
+- [ ] Сохранение в localStorage
+- [ ] Больше врагов и боссов
+- [ ] Звуковые эффекты
+- [ ] Анимации
+- [ ] Система навыков
+- [ ] Торговцы
+- [ ] Крафтинг
diff --git a/RPG_FromFreeModel/game.js b/RPG_FromFreeModel/game.js
new file mode 100644
index 0000000..8a21d12
--- /dev/null
+++ b/RPG_FromFreeModel/game.js
@@ -0,0 +1,1550 @@
+// ========================================
+// GAME.JS - Изометрическая RPG
+// ========================================
+
+const Game = {
+ state: 'playing',
+ timeOfDay: 12,
+ dayNightCycle: true,
+ currentMap: 'village',
+ maps: {},
+ map: [],
+ mapWidth: 14,
+ mapHeight: 14,
+ mapName: 'Деревня',
+ player: null,
+ items: [],
+ enemies: [],
+ npcs: [],
+ decorations: [],
+ weather: 'none',
+ weatherParticles: [],
+ ui: {
+ showInventory: false,
+ showQuests: false,
+ showCombat: false,
+ showShop: false,
+ showSave: false,
+ showDialog: false,
+ showTravel: false,
+ message: '',
+ messageTime: 0,
+ dialogNPC: null,
+ dialogText: '',
+ dialogOptions: []
+ },
+
+ // Dialog system
+ dialogDatabase: {
+ 'Торговец': {
+ text: 'Привет, путник! Хочешь купить что-нибудь?',
+ options: [
+ { text: '📦 Магазин', action: () => { this.ui.showShop = true; this.ui.showDialog = false; } },
+ { text: '❌ Выход', action: () => { this.ui.showDialog = false; } }
+ ]
+ },
+ 'Стражник': {
+ text: 'Осторожно в лесу! Там водятся волки и гоблины. А в подземелье - скелеты и тролль!',
+ options: [
+ { text: '📋 Подробнее', action: () => this.showDialogText('Стражник', 'В лесу живут гоблины - слабые, но их много. Волки появляются ночью. В подземелье опасно, но там есть сокровища!') },
+ { text: '⚔️ Квест', action: () => {
+ if (!this.player.quests.find(q => q.id === 'first_blood' || q.completed)) {
+ this.addQuestToPlayer('first_blood');
+ this.showDialogText('Стражник', 'Отлично! Убей 3 гоблинов в лесу. Они обычно около входа в лес. Удачи!');
+ } else {
+ this.showDialogText('Стражник', 'Спасибо за помощь! Если будут ещё враги - вернёшься.');
+ }
+ }},
+ { text: '❌ Выход', action: () => { this.ui.showDialog = false; } }
+ ]
+ },
+ 'Целитель': {
+ text: 'Я могу исцелить тебя за 20 золота. Хочешь?',
+ options: [
+ { text: '💚 Исцелить', action: () => {
+ if (this.player.gold >= 20) {
+ this.player.gold -= 20;
+ this.player.hp = this.player.maxHp;
+ this.showMessage('Исцелен! -20 золота');
+ this.updateUI();
+ } else {
+ this.showMessage('Нужно 20 золота!');
+ }
+ }},
+ { text: '❌ Выход', action: () => { this.ui.showDialog = false; } }
+ ]
+ },
+ 'Старик': {
+ text: 'Говорят, в пещерах живет древний дракон. Тот, кто его убьет, станет героем!',
+ options: [
+ { text: '🗺️ Где пещера?', action: () => this.showDialogText('Старик', 'Портал в пещеру находится в деревне, на востоке. Но будь осторожен - дорога туда опасна!') },
+ { text: '🐉 О драконе', action: () => this.showDialogText('Старик', 'Дракон - древнее существо. Говорят, его чешуя непроницаема для обычного оружия. Нужно найти особенный клинок...') },
+ { text: '⚔️ Квест', action: () => {
+ if (!this.player.quests.find(q => q.id === 'dragon_slayer' || q.completed)) {
+ this.addQuestToPlayer('dragon_slayer');
+ this.showDialogText('Старик', 'Убей дракона в пещере! Это будет нелегко, но ты сильный воин.') + this.showDialogText('Старик', 'Найди портал в пещеру (восток деревни) и пройди через лес.');
+ } else {
+ this.showDialogText('Старик', 'Ты уже взял этот квест. Найди дракона в пещере!');
+ }
+ }},
+ { text: '❌ Выход', action: () => { this.ui.showDialog = false; } }
+ ]
+ },
+ 'Эльф': {
+ text: 'Тихо! Ты спугнешь мою добычу!',
+ options: [
+ { text: '😢 Извини', action: () => { this.ui.showDialog = false; } }
+ ]
+ },
+ 'Призрак': {
+ text: 'Ты не должен был сюда приходить... Это место проклято...',
+ options: [
+ { text: '❓ Что случилось?', action: () => this.showDialogText('Призрак', 'Много лет назад здесь погибла целая армия. Теперь их духи не могут упокоиться...') },
+ { text: '👋 Уйду', action: () => { this.ui.showDialog = false; } }
+ ]
+ }
+ },
+ mouse: { x: 0, y: 0, tileX: -1, tileY: -1 },
+ lastTime: 0,
+ time: 0,
+ dayCount: 1,
+ dialogButtons: [],
+ travelButtons: [],
+ shopButtons: [],
+ combatButtons: [],
+ equipmentButtons: [],
+ inventoryButtons: [],
+ locations: {
+ village: { name: 'Деревня', safe: true, music: 'village' },
+ forest: { name: 'Лес', safe: false, music: 'forest' },
+ dungeon: { name: 'Подземелье', safe: false, music: 'dungeon' },
+ cave: { name: 'Пещера', safe: false, music: 'cave' }
+ },
+ shopItems: [],
+ questDatabase: {
+ quests: [
+ { id: 'first_blood', name: 'Первая кровь', desc: 'Убей 3 гоблинов', type: 'kill', target: 'goblin', count: 3, reward: 50 },
+ { id: 'clear_forest', name: 'Очистка леса', desc: 'Убей 5 врагов', type: 'kill_any', count: 5, reward: 100 },
+ { id: 'slime_trouble', name: 'Проблема со слизнями', desc: 'Убей 3 слизней', type: 'kill', target: 'slime', count: 3, reward: 75 },
+ { id: 'dungeon_entry', name: 'Вход в подземелье', desc: 'Достигни подземелья', type: 'location', target: 'dungeon', reward: 25 },
+ { id: 'skeleton_horde', name: 'Полчище скелетов', desc: 'Убей 4 скелета', type: 'kill', target: 'skeleton', count: 4, reward: 150 },
+ { id: 'boss_troll', name: 'Бой с троллем', desc: 'Убей тролля', type: 'kill', target: 'troll', count: 1, reward: 200 },
+ { id: 'cave_entry', name: 'Вход в пещеру', desc: 'Найди пещеру', type: 'location', target: 'cave', reward: 50 },
+ { id: 'dragon_slayer', name: 'Убийца драконов', desc: 'Убей дракона', type: 'kill', target: 'dragon', count: 1, reward: 500 }
+ ]
+ },
+
+ init(classType = 'warrior') {
+ // Always initialize renderer first
+ Renderer.init('gameCanvas');
+
+ // Handle continue option
+ if (classType === 'continue') {
+ const saved = this.loadGame(true);
+ if (!saved || !saved.player) {
+ classType = 'warrior';
+ }
+ }
+
+ // Load saved game if exists (for auto-load)
+ const saved = this.loadGame();
+ if (saved && saved.player && classType !== 'continue') {
+ // New game but save exists - check if we should ask
+ // For now, just start new game
+ this.player = saved.player;
+ this.dayCount = saved.dayCount || 1;
+ this.timeOfDay = saved.timeOfDay || 12;
+ this.generateAllMaps();
+ this.setMap(saved.currentMap || 'village');
+ this.spawnEnemies();
+ this.spawnNPCs();
+ this.spawnDecorations();
+ this.spawnItems();
+ this.showMessage('Игра загружена! День ' + this.dayCount);
+ } else if (classType === 'continue' && saved && saved.player) {
+ this.player = saved.player;
+ this.dayCount = saved.dayCount || 1;
+ this.timeOfDay = saved.timeOfDay || 12;
+ this.generateAllMaps();
+ this.setMap(saved.currentMap || 'village');
+ this.spawnEnemies();
+ this.spawnNPCs();
+ this.spawnDecorations();
+ this.spawnItems();
+ this.showMessage('Игра загружена! День ' + this.dayCount);
+ } else {
+ Renderer.init('gameCanvas');
+ this.generateAllMaps();
+ this.setMap('village');
+ this.player = RPG.createCharacter('Герой', 'male', classType);
+ this.player.quests = [];
+
+ if (classType === 'mage') this.player.learnedSpells = ['fireball', 'heal'];
+ else if (classType === 'paladin') this.player.learnedSpells = ['heal', 'holy_fire'];
+ else if (classType === 'necromancer') this.player.learnedSpells = ['life_drain', 'curse'];
+ else if (classType === 'archer') this.player.learnedSpells = ['lightning', 'frostbolt'];
+ else if (classType === 'thief') this.player.learnedSpells = ['curse', 'shield'];
+
+ this.addStarterItems();
+ this.spawnEnemies();
+ this.spawnNPCs();
+ this.spawnDecorations();
+ this.spawnItems();
+ this.addQuestToPlayer('first_blood');
+ this.showMessage('Добро пожаловать в изометрическую RPG!');
+ }
+
+ this.setupInput();
+ this.setupShop();
+ this.updateUI();
+ this.start();
+ },
+
+ generateAllMaps() {
+ this.maps.village = this.generateMap('village');
+ this.maps.forest = this.generateMap('forest');
+ this.maps.dungeon = this.generateMap('dungeon');
+ this.maps.cave = this.generateMap('cave');
+ },
+
+ generateMap(type) {
+ const size = 14, map = [];
+ for (let y = 0; y < size; y++) {
+ map[y] = [];
+ for (let x = 0; x < size; x++) {
+ if (x === 0 || x === size - 1 || y === 0 || y === size - 1) {
+ map[y][x] = 4;
+ continue;
+ }
+ let tile = 0;
+ const rand = Math.random();
+ if (type === 'village') tile = rand < 0.75 ? 0 : rand < 0.85 ? 9 : 8;
+ else if (type === 'forest') tile = rand < 0.5 ? 0 : rand < 0.7 ? 2 : rand < 0.85 ? 9 : 1;
+ else if (type === 'dungeon') tile = rand < 0.3 ? 4 : rand < 0.7 ? 8 : 9;
+ else tile = rand < 0.4 ? 4 : rand < 0.7 ? 2 : 9;
+ map[y][x] = tile;
+ }
+ }
+ // Central clearing
+ for (let y = 5; y < 9; y++) {
+ for (let x = 5; x < 9; x++) map[y][x] = type === 'dungeon' ? 8 : 0;
+ }
+ // Village houses area
+ if (type === 'village') {
+ for (let y = 9; y < 12; y++) for (let x = 10; x < 13; x++) map[y][x] = 1;
+ }
+ // Forest path
+ if (type === 'forest') {
+ for (let x = 6; x < 8; x++) map[12][x] = 0;
+ }
+ return map;
+ },
+
+ setMap(mapName) {
+ if (!this.maps[mapName]) return;
+ this.currentMap = mapName;
+ this.map = this.maps[mapName];
+ this.mapName = this.locations[mapName].name;
+ this.mapWidth = this.map[0].length;
+ this.mapHeight = this.map.length;
+ if (this.player) {
+ this.player.x = 6;
+ this.player.y = 6;
+ this.player.targetX = 6;
+ this.player.targetY = 6;
+ }
+ // Update weather based on location
+ this.updateWeather();
+ },
+
+ updateWeather() {
+ const weathers = {
+ village: ['none', 'none', 'rain', 'rain', 'sunny'],
+ forest: ['rain', 'rain', 'none', 'none', 'fog'],
+ dungeon: ['none', 'none', 'none'],
+ cave: ['none', 'fog']
+ };
+ const options = weathers[this.currentMap] || ['none'];
+ this.weather = options[Math.floor(Math.random() * options.length)];
+ this.weatherParticles = [];
+ },
+
+ addStarterItems() {
+ const items = [];
+ if (this.player.class === 'warrior') {
+ items.push(RPG.createItem('sword_1', 'weapon', 'Меч', { damage: 5, value: 50 }));
+ items.push(RPG.createItem('shield_1', 'armor', 'Щит', { defense: 3, value: 40 }));
+ } else if (this.player.class === 'mage') {
+ items.push(RPG.createItem('staff_1', 'weapon', 'Посох', { damage: 3, value: 60 }));
+ } else if (this.player.class === 'archer') {
+ items.push(RPG.createItem('bow_1', 'weapon', 'Лук', { damage: 7, value: 70 }));
+ } else if (this.player.class === 'paladin') {
+ items.push(RPG.createItem('hammer_1', 'weapon', 'Молот', { damage: 6, value: 55 }));
+ items.push(RPG.createItem('shield_1', 'armor', 'Щит', { defense: 4, value: 45 }));
+ } else if (this.player.class === 'necromancer') {
+ items.push(RPG.createItem('staff_1', 'weapon', 'Посох тьмы', { damage: 4, value: 70 }));
+ } else {
+ items.push(RPG.createItem('dagger_1', 'weapon', 'Кинжал', { damage: 6, value: 40 }));
+ }
+ items.push(RPG.createItem('health_1', 'potion', 'Зелье HP', { healAmount: 30, value: 20, stackable: true, quantity: 3 }));
+ items.push(RPG.createItem('mana_1', 'potion', 'Зелье MP', { restoreMp: 20, value: 25, stackable: true, quantity: 2 }));
+ items.forEach(item => RPG.addItemToInventory(this.player, item));
+ const weapon = this.player.inventory.find(i => i.type === 'weapon');
+ if (weapon) RPG.equipItem(this.player, weapon);
+ },
+
+ spawnEnemies() {
+ this.enemies = [];
+ const baseSpawns = {
+ forest: [
+ {x:3,y:3,type:'goblin',l:1},{x:10,y:2,type:'slime',l:1},{x:5,y:8,type:'goblin',l:2},
+ {x:2,y:10,type:'slime',l:1},{x:11,y:8,type:'goblin',l:2},{x:7,y:4,type:'wolf',l:2}
+ ],
+ dungeon: [
+ {x:2,y:2,type:'skeleton',l:3},{x:11,y:3,type:'skeleton',l:3},{x:6,y:10,type:'skeleton',l:4},
+ {x:10,y:11,type:'troll',l:5},{x:3,y:8,type:'skeleton',l:3},{x:8,y:5,type:'zombie',l:4}
+ ],
+ cave: [
+ {x:3,y:3,type:'slime',l:2},{x:8,y:5,type:'goblin',l:3},{x:10,y:8,type:'orc',l:4},
+ {x:12,y:2,type:'dragon',l:10},{x:5,y:10,type:'bat',l:2},{x:11,y:10,type:'orc',l:5}
+ ]
+ };
+
+ const spawns = baseSpawns[this.currentMap] || [];
+
+ // Add random spawns based on time of day
+ if (this.currentMap === 'forest' && this.timeOfDay < 6) {
+ spawns.push({x:8,y:3,type:'wolf',l:3});
+ }
+
+ spawns.forEach(s => {
+ this.enemies.push(RPG.createEnemy(s.type, s.l, s.x, s.y));
+ });
+ },
+
+ spawnNPCs() {
+ this.npcs = [];
+ if (this.currentMap === 'village') {
+ this.npcs.push(RPG.createNPC('Торговец', 2, 5, { color: '#4a6a8a', dialog: 'Привет! Купи чего-нибудь.', type: 'shop' }));
+ this.npcs.push(RPG.createNPC('Стражник', 5, 1, { color: '#8b0000', dialog: 'Осторожно в лесу! Там водятся волки.', type: 'quest' }));
+ this.npcs.push(RPG.createNPC('Целитель', 10, 3, { color: '#44ff44', dialog: 'Могу исцелить тебя за 20 золота.', type: 'healer' }));
+ this.npcs.push(RPG.createNPC('Старик', 8, 8, { color: '#aaaaaa', dialog: 'Говорят, в пещере живет древний дракон...', type: 'lore' }));
+ } else if (this.currentMap === 'forest') {
+ this.npcs.push(RPG.createNPC('Эльф', 10, 10, { color: '#2ecc71', dialog: 'Тихо! Ты спугнешь мою добычу!', type: 'quest' }));
+ } else if (this.currentMap === 'dungeon') {
+ this.npcs.push(RPG.createNPC('Призрак', 7, 7, { color: '#aaffff', dialog: 'Ты не должен был сюда приходить...', type: 'lore' }));
+ }
+ },
+
+ spawnDecorations() {
+ this.decorations = [];
+ if (this.currentMap === 'village') {
+ this.decorations.push({x:2,y:2,type:'house'});
+ this.decorations.push({x:11,y:2,type:'house'});
+ this.decorations.push({x:2,y:9,type:'house'});
+ this.decorations.push({x:10,y:6,type:'tree'});
+ this.decorations.push({x:4,y:6,type:'tree'});
+ this.decorations.push({x:12,y:6,type:'tree'});
+ this.decorations.push({x:3,y:4,type:'fountain'});
+ this.decorations.push({x:8,y:2,type:'well'});
+ this.decorations.push({x:12,y:6,type:'portal',destination:'forest',name:'Лес'});
+ this.decorations.push({x:6,y:12,type:'portal',destination:'dungeon',name:'Подзем'});
+ this.decorations.push({x:1,y:6,type:'portal',destination:'cave',name:'Пещера'});
+ } else if (this.currentMap === 'forest') {
+ for (let i = 0; i < 8; i++) {
+ this.decorations.push({x:1+i*1.5,y:1+i*0.8,type:'tree'});
+ }
+ this.decorations.push({x:6,y:12,type:'portal',destination:'village',name:'Деревня'});
+ this.decorations.push({x:4,y:4,type:'rock'});
+ this.decorations.push({x:10,y:10,type:'rock'});
+ } else if (this.currentMap === 'dungeon') {
+ this.decorations.push({x:1,y:1,type:'pillar'});
+ this.decorations.push({x:12,y:1,type:'pillar'});
+ this.decorations.push({x:1,y:12,type:'pillar'});
+ this.decorations.push({x:12,y:12,type:'pillar'});
+ this.decorations.push({x:6,y:12,type:'portal',destination:'village',name:'Деревня'});
+ this.decorations.push({x:4,y:4,type:'torch'});
+ this.decorations.push({x:9,y:4,type:'torch'});
+ } else if (this.currentMap === 'cave') {
+ this.decorations.push({x:2,y:2,type:'crystal'});
+ this.decorations.push({x:11,y:3,type:'crystal'});
+ this.decorations.push({x:8,y:8,type:'crystal'});
+ this.decorations.push({x:12,y:12,type:'portal',destination:'village',name:'Деревня'});
+ }
+ },
+
+ spawnItems() {
+ this.items = [];
+ // Random loot spawns
+ if (Math.random() < 0.3) {
+ const loot = [
+ RPG.createItem('gold_coin', 'gold', 'Золото', { value: Math.floor(Math.random() * 20) + 10, stackable: true, quantity: 1 }),
+ RPG.createItem('health_1', 'potion', 'Зелье HP', { healAmount: 30, value: 20, stackable: true, quantity: 1 })
+ ];
+ this.items.push({...loot[Math.floor(Math.random() * loot.length)], x: 3 + Math.floor(Math.random() * 8), y: 3 + Math.floor(Math.random() * 8), collected: false});
+ }
+ },
+
+ setupShop() {
+ this.shopItems = [
+ RPG.createItem('health_1', 'potion', 'Зелье HP', { healAmount: 30, value: 20 }),
+ RPG.createItem('mana_1', 'potion', 'Зелье MP', { restoreMp: 20, value: 25 }),
+ RPG.createItem('health_2', 'potion', 'Большое зелье HP', { healAmount: 60, value: 50 }),
+ RPG.createItem('sword_2', 'weapon', 'Стальной меч', { damage: 10, value: 150 }),
+ RPG.createItem('shield_2', 'armor', 'Стальной щит', { defense: 6, value: 120 }),
+ RPG.createItem('bow_2', 'weapon', 'Длинный лук', { damage: 12, value: 180 }),
+ RPG.createItem('staff_2', 'weapon', 'Магический посох', { damage: 8, value: 200, magBonus: 5 })
+ ];
+ },
+
+ setupInput() {
+ document.addEventListener('keydown', e => this.handleKeyDown(e.key));
+ this.gameCanvas = document.getElementById('gameCanvas');
+ this.gameCanvas.addEventListener('mousemove', e => {
+ const r = this.gameCanvas.getBoundingClientRect();
+ this.mouse.x = e.clientX - r.left;
+ this.mouse.y = e.clientY - r.top;
+ const iso = Renderer.fromIso(this.mouse.x, this.mouse.y);
+ this.mouse.tileX = iso.x;
+ this.mouse.tileY = iso.y;
+ });
+ this.gameCanvas.addEventListener('click', e => this.handleAllClicks(e));
+ },
+
+ handleAllClicks(e) {
+ const r = this.gameCanvas.getBoundingClientRect();
+ const mx = e.clientX - r.left;
+ const my = e.clientY - r.top;
+
+ // Dialog buttons
+ if (this.ui.showDialog && this.dialogButtons) {
+ this.dialogButtons.forEach(btn => {
+ if (mx >= btn.x && mx <= btn.x + btn.w && my >= btn.y && my <= btn.y + btn.h) {
+ btn.action();
+ }
+ });
+ return;
+ }
+
+ // Shop buttons
+ if (this.ui.showShop && this.shopButtons) {
+ this.shopButtons.forEach(btn => {
+ if (mx >= btn.x && mx <= btn.x + btn.w && my >= btn.y && my <= btn.y + btn.h) {
+ this.buyItem(btn.item);
+ }
+ });
+ return;
+ }
+
+ // Combat buttons
+ if (this.state === 'combat' && this.combatButtons) {
+ this.combatButtons.forEach(btn => {
+ if (mx >= btn.x && mx <= btn.x + btn.w && my >= btn.y && my <= btn.y + btn.h) {
+ btn.action();
+ }
+ });
+ return;
+ }
+
+ // Travel buttons
+ if (this.ui.showTravel && this.travelButtons) {
+ this.travelButtons.forEach(btn => {
+ if (mx >= btn.x && mx <= btn.x + btn.w && my >= btn.y && my <= btn.y + btn.h) {
+ this.travelTo(btn.location);
+ this.ui.showTravel = false;
+ }
+ });
+ return;
+ }
+
+ // Inventory - equipment slots (unequip)
+ if (this.ui.showInventory && this.equipmentButtons) {
+ this.equipmentButtons.forEach(btn => {
+ if (mx >= btn.x && mx <= btn.x + btn.w && my >= btn.y && my <= btn.y + btn.h) {
+ const item = this.player.equipment[btn.slot];
+ if (item) {
+ RPG.unequipItem(this.player, btn.slot);
+ this.showMessage('Снято: ' + item.name);
+ this.updateUI();
+ }
+ }
+ });
+
+ // Inventory items (equip)
+ if (this.inventoryButtons) {
+ this.inventoryButtons.forEach(btn => {
+ if (mx >= btn.x && mx <= btn.x + btn.w && my >= btn.y && my <= btn.y + btn.h) {
+ const result = RPG.equipItem(this.player, btn.item);
+ if (result) {
+ this.showMessage('Экипировано: ' + btn.item.name);
+ this.updateUI();
+ }
+ }
+ });
+ }
+ return;
+ }
+
+ // Movement - only when no UI is open and in playing state
+ if (!this.ui.showInventory && !this.ui.showQuests && !this.ui.showShop && !this.ui.showDialog && !this.ui.showTravel && this.state === 'playing' && this.mouse.tileX >= 0) {
+ const dx = this.mouse.tileX - Math.round(this.player.x);
+ const dy = this.mouse.tileY - Math.round(this.player.y);
+ if (Math.abs(dx) + Math.abs(dy) === 1 && !this.player.isMoving) {
+ this.movePlayer(dx, dy);
+ }
+ }
+ },
+
+
+
+ handleKeyDown(key) {
+ if (this.state === 'playing' && !this.player.isMoving) {
+ let dx = 0, dy = 0;
+ if (key === 'w' || key === 'ArrowUp') dy = -1;
+ else if (key === 's' || key === 'ArrowDown') dy = 1;
+ else if (key === 'a' || key === 'ArrowLeft') dx = -1;
+ else if (key === 'd' || key === 'ArrowRight') dx = 1;
+ if (dx || dy) this.movePlayer(dx, dy);
+ }
+ if (key === 'i' || key === 'I') { this.ui.showInventory = !this.ui.showInventory; this.ui.showShop = false; this.ui.showTravel = false; }
+ if (key === 'q' || key === 'Q') { this.ui.showQuests = !this.ui.showQuests; this.ui.showTravel = false; }
+ if (key === 'h' || key === 'H' || key === 'k' || key === 'K') this.showHelp();
+ if (key === 'p' || key === 'P') this.saveGame();
+ if (key === 'l' || key === 'L') this.loadGame(true);
+ if (key === 'm' || key === 'M') { this.ui.showTravel = !this.ui.showTravel; this.ui.showInventory = false; this.ui.showQuests = false; }
+ if (key === 'Escape') { this.ui.showInventory = false; this.ui.showQuests = false; this.ui.showShop = false; this.ui.showDialog = false; this.ui.showTravel = false; this.state = 'playing'; }
+ if (this.state === 'combat') {
+ if (key === '1') this.combatAttack();
+ if (key === '2') this.combatHeal();
+ if (key === '3') this.combatUseSpell();
+ if (key === '4') this.fleeCombat();
+ }
+ },
+
+ movePlayer(dx, dy) {
+ const newX = Math.round(this.player.x) + dx;
+ const newY = Math.round(this.player.y) + dy;
+ if (!RPG.isTilePassable(this.map, newX, newY)) return;
+
+ // Check for NPC collision
+ const npc = this.npcs.find(n => n.x === newX && n.y === newY);
+ if (npc) {
+ this.interactWithNPC(npc);
+ return;
+ }
+
+ this.player.targetX = newX;
+ this.player.targetY = newY;
+ this.player.isMoving = true;
+ this.player.moveProgress = 0;
+ },
+
+
+
+ interactWithNPC(npc) {
+ // Check if NPC has dialog in database
+ const dialog = this.dialogDatabase[npc.name];
+ if (dialog) {
+ this.ui.showDialog = true;
+ this.ui.dialogNPC = npc;
+ this.ui.dialogText = dialog.text;
+ this.ui.dialogOptions = dialog.options;
+ } else if (npc.type === 'shop') {
+ this.ui.showShop = true;
+ this.ui.showInventory = false;
+ this.showMessage('Открыт магазин');
+ } else if (npc.type === 'healer') {
+ if (this.player.gold >= 20 && this.player.hp < this.player.maxHp) {
+ this.player.gold -= 20;
+ this.player.hp = this.player.maxHp;
+ this.showMessage('Исцелен! -20 золота');
+ this.updateUI();
+ } else if (this.player.gold < 20) {
+ this.showMessage('Нужно 20 золота!');
+ } else {
+ this.showMessage('Вы уже здоровы!');
+ }
+ } else {
+ this.showMessage(npc.dialog);
+ }
+ },
+
+ showDialogText(name, text) {
+ this.ui.dialogText = text;
+ this.ui.dialogOptions = [
+ { text: 'Назад', action: () => {
+ const d = this.dialogDatabase[name];
+ if (d) {
+ this.ui.dialogText = d.text;
+ this.ui.dialogOptions = d.options;
+ }
+ }},
+ { text: 'Уйти', action: () => { this.ui.showDialog = false; } }
+ ];
+ },
+
+ update(dt) {
+ if (this.state !== 'playing') return;
+
+ // Day/night cycle
+ if (this.dayNightCycle) {
+ this.timeOfDay += dt / 50000;
+ if (this.timeOfDay >= 24) {
+ this.timeOfDay = 0;
+ this.dayCount++;
+ this.showMessage('День ' + this.dayCount);
+ // Respawn enemies each day
+ this.spawnEnemies();
+ }
+ }
+
+ // Update weather particles
+ this.updateWeatherParticles(dt);
+
+ if (this.player.isMoving) {
+ this.player.moveProgress += dt / 200;
+ if (this.player.moveProgress >= 1) {
+ this.player.moveProgress = 0;
+ this.player.isMoving = false;
+ this.player.x = this.player.targetX;
+ this.player.y = this.player.targetY;
+ this.checkCollisions();
+ }
+ }
+ if (this.ui.messageTime && Date.now() > this.ui.messageTime) {
+ this.ui.message = '';
+ }
+ },
+
+ updateWeatherParticles(dt) {
+ if (this.weather === 'rain') {
+ for (let i = 0; i < 3; i++) {
+ this.weatherParticles.push({
+ x: Math.random() * canvas.width,
+ y: -10,
+ speed: 5 + Math.random() * 5
+ });
+ }
+ }
+ this.weatherParticles.forEach(p => p.y += p.speed);
+ this.weatherParticles = this.weatherParticles.filter(p => p.y < canvas.height);
+ },
+
+ checkCollisions() {
+ // Decorations (portals)
+ for (let dec of this.decorations) {
+ if (dec.type === 'portal' && dec.x === Math.round(this.player.x) && dec.y === Math.round(this.player.y)) {
+ this.travelTo(dec.destination);
+ return;
+ }
+ }
+ // Items
+ const idx = RPG.checkItemCollision(this.player, this.items);
+ if (idx >= 0) {
+ const item = this.items[idx];
+ if (item.type === 'gold') {
+ this.player.gold += item.value;
+ this.showMessage('+' + item.value + ' золота!');
+ } else {
+ RPG.addItemToInventory(this.player, item);
+ this.showMessage('Получено: ' + item.name);
+ }
+ this.items[idx].collected = true;
+ this.updateUI();
+ }
+ // Enemies
+ const enemy = RPG.checkEnemyCollision(this.player, this.enemies);
+ if (enemy) this.startCombat(enemy);
+ },
+
+ travelTo(mapName) {
+ this.setMap(mapName);
+ this.spawnEnemies();
+ this.spawnNPCs();
+ this.spawnDecorations();
+ this.spawnItems();
+ this.showMessage('Переход: ' + this.locations[mapName].name);
+ this.updateQuestProgress('location', mapName);
+ this.autoSave();
+ },
+
+ // Quest System
+ addQuestToPlayer(questId) {
+ if (this.player.quests.find(q => q.id === questId)) return;
+ const q = this.questDatabase.quests.find(q => q.id === questId);
+ if (!q) return;
+ this.player.quests.push({...q, progress: 0, completed: false});
+ this.showMessage('Новый квест: ' + q.name);
+ },
+
+ updateQuestProgress(type, target) {
+ this.player.quests.forEach(q => {
+ if (q.completed) return;
+ if (q.type === type) {
+ if (type === 'kill' && (q.target === target || q.type === 'kill_any')) {
+ q.progress++;
+ this.showMessage(q.name + ': ' + q.progress + '/' + q.count);
+ if (q.progress >= q.count) {
+ q.completed = true;
+ this.player.exp += q.reward;
+ this.showMessage('Квест выполнен! +' + q.reward + ' XP');
+ // Next quest
+ const next = {first_blood:'clear_forest',clear_forest:'slime_trouble',slime_trouble:'dungeon_entry',
+ dungeon_entry:'skeleton_horde',skeleton_horde:'boss_troll',boss_troll:'cave_entry',cave_entry:'dragon_slayer'};
+ if (next[q.id]) setTimeout(() => this.addQuestToPlayer(next[q.id]), 2000);
+ }
+ }
+ if (type === 'location' && q.target === target) {
+ q.completed = true;
+ this.player.exp += q.reward;
+ this.showMessage('Квест выполнен! +' + q.reward + ' XP');
+ }
+ }
+ });
+ RPG.checkLevelUp(this.player);
+ this.updateUI();
+ },
+
+ // Combat
+ startCombat(enemy) {
+ this.state = 'combat';
+ this.player.inCombat = true;
+ this.player.currentEnemy = enemy;
+ this.ui.showCombat = true;
+ this.player.x = this.player.targetX;
+ this.player.y = this.player.targetY;
+ this.showMessage('Бой с ' + enemy.name + '!');
+ },
+
+ combatAttack() {
+ if (this.state !== 'combat' || !this.player.currentEnemy) return;
+ const enemy = this.player.currentEnemy;
+ const result = RPG.attack(this.player, enemy);
+ const damage = isNaN(result.damage) ? 0 : result.damage;
+ this.showMessage('Нанесено: ' + damage + (result.isCrit ? ' КРИТ!' : ''));
+ if (enemy.hp <= 0) { this.endCombat(true); return; }
+ setTimeout(() => this.enemyAttack(), 600);
+ },
+
+ combatHeal() {
+ const potion = this.player.inventory.find(i => i.type === 'potion' && i.healAmount);
+ if (!potion) { this.showMessage('Нет зелий!'); return; }
+ RPG.useItem(this.player, potion);
+ this.showMessage('HP восстановлено!');
+ setTimeout(() => this.enemyAttack(), 600);
+ },
+
+ combatUseSpell() {
+ if (this.state !== 'combat') return;
+ if (!this.player.learnedSpells.length) { this.showMessage('Нет заклинаний!'); return; }
+ const enemy = this.player.currentEnemy;
+ const spell = RPG.SpellDatabase[this.player.learnedSpells[0]];
+ if (this.player.mp < spell.mpCost) { this.showMessage('Нет MP!'); return; }
+ this.player.mp -= spell.mpCost;
+ if (spell.damage) enemy.hp -= spell.damage;
+ if (spell.heal) this.player.hp = Math.min(this.player.hp + spell.heal, this.player.maxHp);
+ this.showMessage(spell.name + '!');
+ if (enemy.hp <= 0) { this.endCombat(true); return; }
+ setTimeout(() => this.enemyAttack(), 600);
+ },
+
+ enemyAttack() {
+ if (this.state !== 'combat' || !this.player.currentEnemy) return;
+ const enemy = this.player.currentEnemy;
+ const result = RPG.enemyAttack(enemy, this.player);
+ this.showMessage(enemy.name + ' бьет на ' + result.damage);
+ if (this.player.hp <= 0) this.endCombat(false);
+ },
+
+ endCombat(won) {
+ this.state = 'playing';
+ this.ui.showCombat = false;
+ if (won) {
+ const enemy = this.player.currentEnemy;
+ this.player.exp += enemy.exp;
+ const loot = RPG.generateLoot(enemy);
+ loot.forEach(item => RPG.addItemToInventory(this.player, item));
+ this.showMessage('Победа! +' + enemy.exp + ' XP');
+ this.enemies = this.enemies.filter(e => e !== enemy);
+ this.updateQuestProgress('kill', enemy.type);
+ this.autoSave();
+ } else {
+ this.player.gold = Math.max(0, this.player.gold - 10);
+ this.player.hp = Math.floor(this.player.maxHp / 2);
+ this.showMessage('Поражение! -10 золота');
+ }
+ this.player.inCombat = false;
+ this.player.currentEnemy = null;
+ RPG.checkLevelUp(this.player);
+ this.updateUI();
+ },
+
+ fleeCombat() {
+ if (this.state !== 'combat') return;
+ if (Math.random() < 0.5) { this.showMessage('Не удалось сбежать!'); setTimeout(() => this.enemyAttack(), 500); }
+ else { this.showMessage('Сбежал!'); this.state = 'playing'; this.ui.showCombat = false; this.player.inCombat = false; this.player.currentEnemy = null; }
+ },
+
+ // Save/Load System
+ saveGame() {
+ const saveData = {
+ player: this.player,
+ currentMap: this.currentMap,
+ dayCount: this.dayCount,
+ timeOfDay: this.timeOfDay,
+ savedAt: Date.now()
+ };
+ localStorage.setItem('rpg_save', JSON.stringify(saveData));
+ this.showMessage('Игра сохранена!');
+ },
+
+ autoSave() {
+ const saveData = {
+ player: this.player,
+ currentMap: this.currentMap,
+ dayCount: this.dayCount,
+ timeOfDay: this.timeOfDay,
+ savedAt: Date.now()
+ };
+ localStorage.setItem('rpg_save', JSON.stringify(saveData));
+ },
+
+ loadGame(manual = false) {
+ const saved = localStorage.getItem('rpg_save');
+ if (saved) {
+ try {
+ const data = JSON.parse(saved);
+ if (manual) {
+ this.player = data.player;
+ this.dayCount = data.dayCount || 1;
+ this.timeOfDay = data.timeOfDay || 12;
+ this.generateAllMaps();
+ this.setMap(data.currentMap || 'village');
+ this.spawnEnemies();
+ this.spawnNPCs();
+ this.spawnDecorations();
+ this.spawnItems();
+ this.showMessage('Игра загружена! День ' + this.dayCount);
+ this.updateUI();
+ }
+ return data;
+ } catch(e) {
+ console.error('Save load error:', e);
+ return null;
+ }
+ }
+ return null;
+ },
+
+ showHelp() { this.showMessage('H: Помощь | WASD: Ходить | I: Инвентарь | Q: Квесты | P: Сохранить | L: Загрузить | 1-4: Бой'); },
+ showMessage(text, dur = 2500) { this.ui.message = text; this.ui.messageTime = Date.now() + dur; },
+ updateUI() {
+ const s = RPG.getTotalStats(this.player);
+ document.getElementById('hp').textContent = Math.floor(this.player.hp) + '/' + this.player.maxHp;
+ document.getElementById('mp').textContent = Math.floor(this.player.mp) + '/' + this.player.maxMp;
+ document.getElementById('level').textContent = this.player.level;
+ document.getElementById('gold').textContent = this.player.gold;
+ document.getElementById('strength').textContent = s.damage;
+ },
+
+ render() {
+ // Apply day/night filter
+ const brightness = this.getDayNightBrightness();
+
+ Renderer.clear();
+
+ const hover = this.mouse.tileX >= 0 ? {x:this.mouse.tileX,y:this.mouse.tileY} : null;
+ Renderer.drawMap(this.map, null, hover, this.time);
+
+ // Objects
+ const objs = [];
+ this.decorations.forEach(d => { if (d.type === 'portal') objs.push({x:d.x,y:d.y,draw:()=>this.renderPortal(d)})});
+ objs.push({x:this.player.isMoving?this.player.x+(this.player.targetX-this.player.x)*this.player.moveProgress:this.player.x,
+ y:this.player.isMoving?this.player.y+(this.player.targetY-this.player.y)*this.player.moveProgress:this.player.y,
+ draw:()=>Renderer.drawPlayer(this.player,this.time)});
+ this.enemies.forEach(e => objs.push({x:e.x,y:e.y,draw:()=>Renderer.drawEnemy(e,this.time)}));
+ this.npcs.forEach(n => objs.push({x:n.x,y:n.y,draw:()=>Renderer.drawNPC(n,this.time)}));
+ this.items.filter(i=>!i.collected).forEach(i => objs.push({x:i.x,y:i.y,draw:()=>Renderer.drawItem(i,this.time)}));
+ objs.sort((a,b) => (a.x+a.y)-(b.x+b.y)).forEach(o => o.draw());
+
+ // Weather effects
+ this.renderWeather();
+
+ // Day/night overlay
+ this.renderDayNightOverlay(brightness);
+
+ this.renderUI();
+ },
+
+ getDayNightBrightness() {
+ // Returns brightness multiplier (1 = full day, 0.3 = darkest night)
+ if (this.timeOfDay >= 6 && this.timeOfDay < 20) {
+ // Daytime
+ return 1;
+ } else if (this.timeOfDay >= 20 || this.timeOfDay < 5) {
+ // Night
+ return 0.4;
+ } else {
+ // Dawn/Dusk
+ return 0.7;
+ }
+ },
+
+ renderDayNightOverlay(brightness) {
+ if (brightness >= 1) return;
+ const ctx = Renderer.ctx;
+ ctx.fillStyle = `rgba(0, 0, 50, ${1 - brightness})`;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderWeather() {
+ if (this.weather === 'none') return;
+ const ctx = Renderer.ctx;
+
+ if (this.weather === 'rain') {
+ ctx.strokeStyle = 'rgba(150, 150, 255, 0.5)';
+ ctx.lineWidth = 1;
+ this.weatherParticles.forEach(p => {
+ ctx.beginPath();
+ ctx.moveTo(p.x, p.y);
+ ctx.lineTo(p.x - 2, p.y + 10);
+ ctx.stroke();
+ });
+ } else if (this.weather === 'fog') {
+ ctx.fillStyle = 'rgba(200, 200, 200, 0.2)';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+ },
+
+ renderPortal(p, time) {
+ const pos = Renderer.toIso(p.x, p.y);
+ const pulse = Math.sin(time/200)*5;
+ const ctx = Renderer.ctx;
+ ctx.beginPath();
+ ctx.ellipse(pos.x, pos.y-20, 15+pulse/2, 25+pulse, 0, 0, Math.PI*2);
+ const g = ctx.createRadialGradient(pos.x,pos.y-20,0,pos.x,pos.y-20,25);
+ g.addColorStop(0,'#fff');g.addColorStop(0.5,'#a0f');g.addColorStop(1,'#40a');
+ ctx.fillStyle = g; ctx.fill(); ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke();
+ ctx.fillStyle='#fd0';ctx.font='bold 10px Arial';ctx.textAlign='center';ctx.fillText(p.name,pos.x,pos.y-50);
+ },
+
+ renderUI() {
+ const ctx = Renderer.ctx;
+ if (this.ui.message) {
+ ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(canvas.width/2-150,50,300,40);
+ ctx.fillStyle='#fff';ctx.font='16px Arial';ctx.textAlign='center';ctx.fillText(this.ui.message,canvas.width/2,75);
+ }
+
+ // Location name and time
+ ctx.fillStyle='#fd0';ctx.font='bold 16px Arial';ctx.textAlign='left';ctx.fillText(this.mapName,70,50);
+ ctx.fillStyle='#aaa';ctx.font='12px Arial';ctx.fillText('День ' + this.dayCount, 70, 70);
+ ctx.fillText(Math.floor(this.timeOfDay) + ':00', 150, 70);
+
+ // Weather indicator
+ if (this.weather !== 'none') {
+ ctx.fillStyle = '#aaa';
+ const weatherNames = { rain: '🌧️', fog: '🌫️', snow: '❄️' };
+ ctx.fillText(weatherNames[this.weather] || this.weather, 200, 70);
+ }
+
+ if (this.ui.showInventory) this.renderInventory();
+ if (this.ui.showQuests) this.renderQuests();
+ if (this.ui.showCombat) this.renderCombat();
+ if (this.ui.showShop) this.renderShop();
+ if (this.ui.showDialog) this.renderDialog();
+ if (this.ui.showTravel) this.renderTravel();
+ },
+
+ renderTravel() {
+ const ctx = Renderer.ctx;
+ const w = 350, h = 320;
+ const x = (canvas.width - w) / 2;
+ const y = (canvas.height - h) / 2;
+
+ // Background
+ ctx.fillStyle = 'rgba(20, 20, 40, 0.95)';
+ ctx.fillRect(x, y, w, h);
+ ctx.strokeStyle = '#ffd700';
+ ctx.lineWidth = 3;
+ ctx.strokeRect(x, y, w, h);
+
+ // Title
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 22px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('ПЕРЕХОД', canvas.width / 2, y + 30);
+
+ // Current location
+ ctx.fillStyle = '#aaa';
+ ctx.font = '14px Arial';
+ ctx.fillText('Сейчас: ' + this.mapName, canvas.width / 2, y + 55);
+
+ // Store buttons for click detection
+ this.travelButtons = [];
+
+ // Locations - simple list
+ const locations = ['village', 'forest', 'dungeon', 'cave'];
+ const names = { village: 'Деревня', forest: 'Лес', dungeon: 'Подземелье', cave: 'Пещера' };
+ const icons = { village: '🏠', forest: '🌲', dungeon: '⚰️', cave: '🦇' };
+
+ let ly = y + 85;
+ const btnH = 45;
+ const btnW = w - 40;
+ const btnX = x + 20;
+
+ locations.forEach((locId) => {
+ const isCurrent = locId === this.currentMap;
+
+ // Store button position
+ if (!isCurrent) {
+ this.travelButtons.push({
+ x: btnX, y: ly, w: btnW, h: btnH,
+ location: locId
+ });
+ }
+
+ // Draw button
+ ctx.fillStyle = isCurrent ? 'rgba(80, 80, 80, 0.5)' : 'rgba(40, 60, 40, 0.8)';
+ ctx.fillRect(btnX, ly, btnW, btnH);
+ ctx.strokeStyle = isCurrent ? '#666' : '#4f4';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(btnX, ly, btnW, btnH);
+
+ ctx.fillStyle = isCurrent ? '#888' : '#4f4';
+ ctx.font = 'bold 16px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText(icons[locId] + ' ' + names[locId] + (isCurrent ? ' (здесь)' : ''), btnX + btnW / 2, ly + 28);
+
+ ly += btnH + 10;
+ });
+
+ // Instructions
+ ctx.fillStyle = '#888';
+ ctx.font = '12px Arial';
+ ctx.fillText('Кликни на локацию | M или ESC - закрыть', canvas.width / 2, y + h - 15);
+ },
+
+ renderDialog() {
+ const ctx = Renderer.ctx;
+ const npc = this.ui.dialogNPC;
+
+ // Fixed position dialog at bottom center - larger for buttons
+ const dw = 550, dh = 220;
+ const dx = (canvas.width - dw) / 2;
+ const dy = canvas.height - dh - 30;
+
+ // Background
+ ctx.fillStyle = 'rgba(20, 20, 40, 0.95)';
+ ctx.fillRect(dx, dy, dw, dh);
+ ctx.strokeStyle = '#ffd700';
+ ctx.lineWidth = 3;
+ ctx.strokeRect(dx, dy, dw, dh);
+
+ // NPC name
+ if (npc) {
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 20px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText(npc.name, canvas.width / 2, dy + 30);
+ }
+
+ // Dialog text
+ ctx.fillStyle = '#fff';
+ ctx.font = '15px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText(this.ui.dialogText.substring(0, 55) + (this.ui.dialogText.length > 55 ? '...' : ''), canvas.width / 2, dy + 65);
+
+ // Store button positions for click detection
+ this.dialogButtons = [];
+
+ // Options - simple horizontal layout, centered in dialog
+ const numBtns = this.ui.dialogOptions.length;
+ const btnW = Math.min(150, (dw - 40) / numBtns);
+ const btnH = 38;
+ const btnGap = 15;
+ const totalBtnsW = btnW * numBtns + btnGap * (numBtns - 1);
+ const startX = dx + (dw - totalBtnsW) / 2;
+ const btnY = dy + dh - 60;
+
+ this.ui.dialogOptions.forEach((opt, i) => {
+ const btnX = startX + i * (btnW + btnGap);
+
+ // Store for click detection
+ this.dialogButtons.push({ x: btnX, y: btnY, w: btnW, h: btnH, action: opt.action });
+
+ // Draw button
+ ctx.fillStyle = '#2a2a4a';
+ ctx.fillRect(btnX, btnY, btnW, btnH);
+ ctx.strokeStyle = '#ffd700';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(btnX, btnY, btnW, btnH);
+
+ ctx.fillStyle = '#fff';
+ ctx.font = '12px Arial';
+ ctx.textAlign = 'center';
+ // Shorten text if needed
+ const shortText = opt.text.length > 15 ? opt.text.substring(0, 14) + '..' : opt.text;
+ ctx.fillText(shortText, btnX + btnW / 2, btnY + 24);
+ });
+ },
+
+ renderInventory() {
+ const ctx = Renderer.ctx;
+ const w = 750, h = 550;
+ const x = (canvas.width - w) / 2;
+ const y = (canvas.height - h) / 2;
+
+ // Main background
+ ctx.fillStyle = 'rgba(15, 15, 30, 0.95)';
+ ctx.fillRect(x, y, w, h);
+
+ // Border with gradient effect
+ ctx.strokeStyle = '#ffd700';
+ ctx.lineWidth = 3;
+ ctx.strokeRect(x, y, w, h);
+
+ // Inner border
+ ctx.strokeStyle = 'rgba(255, 215, 0, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(x + 5, y + 5, w - 10, h - 10);
+
+ // Title
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 28px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('🎒 ИНВЕНТАРЬ 🎒', canvas.width / 2, y + 40);
+
+ // Stats bar
+ const s = RPG.getTotalStats(this.player);
+ const statsY = y + 70;
+
+ // HP bar
+ ctx.fillStyle = '#333';
+ ctx.fillRect(x + 30, statsY, 150, 18);
+ ctx.fillStyle = '#e74c3c';
+ ctx.fillRect(x + 30, statsY, 150 * (this.player.hp / this.player.maxHp), 18);
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 12px Arial';
+ ctx.fillText('❤️ ' + Math.floor(this.player.hp) + '/' + this.player.maxHp, x + 105, statsY + 14);
+
+ // MP bar
+ ctx.fillStyle = '#333';
+ ctx.fillRect(x + 200, statsY, 150, 18);
+ ctx.fillStyle = '#3498db';
+ ctx.fillRect(x + 200, statsY, 150 * (this.player.mp / this.player.maxMp), 18);
+ ctx.fillStyle = '#fff';
+ ctx.fillText('💧 ' + Math.floor(this.player.mp) + '/' + this.player.maxMp, x + 275, statsY + 14);
+
+ // Level and gold
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 16px Arial';
+ ctx.fillText('⭐ LVL ' + this.player.level, x + 400, statsY + 14);
+ ctx.fillText('💰 ' + this.player.gold, x + 520, statsY + 14);
+
+ // Equipment section
+ ctx.fillStyle = '#888';
+ ctx.font = 'bold 14px Arial';
+ ctx.textAlign = 'left';
+ ctx.fillText('ЭКИПИРОВКА (клик для снятия)', x + 30, y + 115);
+
+ // Store equipment button positions
+ this.equipmentButtons = [];
+
+ // Equipment slots - more detailed
+ const eqY = y + 130;
+ const eqW = 170, eqH = 55;
+
+ // Weapon slot
+ ctx.fillStyle = 'rgba(40, 40, 60, 0.8)';
+ ctx.fillRect(x + 30, eqY, eqW, eqH);
+ ctx.strokeStyle = this.player.equipment.mainHand ? '#ffd700' : '#666';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(x + 30, eqY, eqW, eqH);
+ ctx.fillStyle = '#888';
+ ctx.font = '11px Arial';
+ ctx.fillText('⚔️ ОРУЖИЕ', x + 40, eqY + 18);
+ ctx.fillStyle = '#fff';
+ ctx.font = '13px Arial';
+ ctx.fillText(this.player.equipment.mainHand ? this.player.equipment.mainHand.name.substring(0, 15) : '---', x + 40, eqY + 40);
+ if (this.player.equipment.mainHand) {
+ this.equipmentButtons.push({ x: x + 30, y: eqY, w: eqW, h: eqH, slot: 'mainHand' });
+ }
+
+ // Shield slot
+ ctx.fillStyle = 'rgba(40, 40, 60, 0.8)';
+ ctx.fillRect(x + 220, eqY, eqW, eqH);
+ ctx.strokeStyle = this.player.equipment.offHand ? '#ffd700' : '#666';
+ ctx.strokeRect(x + 220, eqY, eqW, eqH);
+ ctx.fillStyle = '#888';
+ ctx.font = '11px Arial';
+ ctx.fillText('🛡️ ЩИТ', x + 230, eqY + 18);
+ ctx.fillStyle = '#fff';
+ ctx.font = '13px Arial';
+ ctx.fillText(this.player.equipment.offHand ? this.player.equipment.offHand.name.substring(0, 15) : '---', x + 230, eqY + 40);
+ if (this.player.equipment.offHand) {
+ this.equipmentButtons.push({ x: x + 220, y: eqY, w: eqW, h: eqH, slot: 'offHand' });
+ }
+
+ // Stats display
+ ctx.fillStyle = '#aaa';
+ ctx.font = 'bold 13px Arial';
+ ctx.fillText('⚔️ Сила: ' + s.damage + ' | 🛡️ Защита: ' + s.defense + ' | ✨ Магия: ' + s.magic + ' | ⚡ Скорость: ' + s.speed, x + 30, y + 205);
+
+ // Items section
+ ctx.fillStyle = '#888';
+ ctx.font = 'bold 14px Arial';
+ ctx.fillText('ПРЕДМЕТЫ (клик для экипировки)', x + 30, y + 235);
+
+ // Store item button positions
+ this.inventoryButtons = [];
+
+ // Items grid - larger
+ const itemW = 160, itemH = 45;
+ const itemsPerRow = 4;
+ const startX = x + 30;
+ const startY = y + 260;
+
+ this.player.inventory.forEach((item, i) => {
+ const col = i % itemsPerRow;
+ const row = Math.floor(i / itemsPerRow);
+ const ix = startX + col * (itemW + 12);
+ const iy = startY + row * (itemH + 10);
+
+ // Store button position
+ this.inventoryButtons.push({ x: ix, y: iy, w: itemW, h: itemH, item: item });
+
+ ctx.fillStyle = 'rgba(30, 30, 50, 0.8)';
+ ctx.fillRect(ix, iy, itemW, itemH);
+ ctx.strokeStyle = RPG.RarityColors[item.rarity] || '#555';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(ix, iy, itemW, itemH);
+
+ // Item icon/type
+ ctx.fillStyle = '#888';
+ ctx.font = '10px Arial';
+ const icon = item.type === 'weapon' ? '⚔️' : item.type === 'armor' ? '🛡️' : item.type === 'potion' ? '🧪' : '📦';
+ ctx.fillText(icon, ix + 8, iy + 14);
+
+ // Item name
+ ctx.fillStyle = RPG.RarityColors[item.rarity] || '#fff';
+ ctx.font = 'bold 12px Arial';
+ ctx.fillText(item.name.substring(0, 18), ix + 28, iy + 18);
+
+ // Item stats
+ ctx.fillStyle = '#888';
+ ctx.font = '10px Arial';
+ let stats = '';
+ if (item.damage) stats = '⚔️+' + item.damage;
+ else if (item.defense) stats = '🛡️+' + item.defense;
+ else if (item.healAmount) stats = '❤️+' + item.healAmount;
+ ctx.fillText(stats, ix + 8, iy + 36);
+ });
+
+ // Instructions
+ ctx.fillStyle = '#666';
+ ctx.font = '12px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('Клик на предмет - экипировать | Клик на слот - снять | I или ESC - закрыть', canvas.width / 2, y + h - 20);
+ },
+
+ renderQuests() {
+ const ctx = Renderer.ctx;
+ // Main container
+ const w = 600, h = 500, x = (canvas.width - w) / 2, y = 50;
+ ctx.fillStyle = 'rgba(20, 20, 40, 0.95)';
+ ctx.fillRect(x, y, w, h);
+ ctx.strokeStyle = '#ffd700';
+ ctx.lineWidth = 3;
+ ctx.strokeRect(x, y, w, h);
+
+ // Title
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 28px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('📜 КВЕСТЫ 📜', canvas.width / 2, y + 40);
+
+ // Active quests section
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 16px Arial';
+ ctx.textAlign = 'left';
+ ctx.fillText('АКТИВНЫЕ:', x + 20, y + 80);
+
+ let py = y + 110;
+ const active = this.player.quests ? this.player.quests.filter(q => !q.completed) : [];
+
+ if (active.length === 0) {
+ ctx.fillStyle = '#888';
+ ctx.font = '14px Arial';
+ ctx.fillText('Нет активных квестов', x + 20, py);
+ py += 30;
+ } else {
+ active.forEach(q => {
+ // Quest name
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 15px Arial';
+ ctx.fillText('⚔️ ' + q.name, x + 20, py);
+ py += 22;
+
+ // Quest description
+ ctx.fillStyle = '#aaa';
+ ctx.font = '12px Arial';
+ ctx.fillText(q.desc, x + 30, py);
+ py += 18;
+
+ // Progress bar
+ const progress = q.count > 0 ? q.progress / q.count : 0;
+ ctx.fillStyle = '#333';
+ ctx.fillRect(x + 30, py, w - 80, 12);
+ ctx.fillStyle = progress >= 1 ? '#ffd700' : '#4f4';
+ ctx.fillRect(x + 30, py, (w - 80) * Math.min(progress, 1), 12);
+
+ // Progress text
+ ctx.fillStyle = '#fff';
+ ctx.font = '11px Arial';
+ ctx.fillText(q.progress + '/' + q.count, x + w - 60, py + 10);
+ py += 30;
+ });
+ }
+
+ // Completed quests
+ py += 20;
+ ctx.fillStyle = '#888';
+ ctx.font = 'bold 14px Arial';
+ ctx.fillText('ВЫПОЛНЕННЫЕ:', x + 20, py);
+ py += 25;
+
+ const completed = this.player.quests ? this.player.quests.filter(q => q.completed) : [];
+ if (completed.length === 0) {
+ ctx.fillStyle = '#666';
+ ctx.font = '12px Arial';
+ ctx.fillText('Нет выполненных квестов', x + 30, py);
+ } else {
+ completed.slice(-8).forEach(q => {
+ ctx.fillStyle = '#4a4';
+ ctx.font = '12px Arial';
+ ctx.fillText('✓ ' + q.name, x + 30, py);
+ py += 20;
+ });
+ }
+
+ // Instructions
+ ctx.fillStyle = '#666';
+ ctx.font = '12px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('Q или ESC - закрыть', canvas.width / 2, y + h - 20);
+ },
+
+ renderCombat() {
+ const ctx = Renderer.ctx;
+ const e = this.player.currentEnemy;
+ if (!e) { this.state='playing';this.ui.showCombat=false;return; }
+
+ // Dark overlay
+ ctx.fillStyle='rgba(0,0,0,0.8)';ctx.fillRect(0,0,canvas.width,canvas.height);
+
+ const w = 700, h = 280;
+ const x = (canvas.width - w) / 2;
+ const y = canvas.height - h - 20;
+
+ // Combat box
+ ctx.fillStyle = 'rgba(30, 20, 40, 0.95)';
+ ctx.fillRect(x, y, w, h);
+ ctx.strokeStyle = '#e74c3c';
+ ctx.lineWidth = 4;
+ ctx.strokeRect(x, y, w, h);
+
+ // Title
+ ctx.fillStyle = '#e74c3c';
+ ctx.font = 'bold 28px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('⚔️ БОЙ ⚔️', canvas.width / 2, y + 40);
+
+ // Enemy info
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 22px Arial';
+ ctx.fillText('👹 ' + e.name + ' (Lv.' + e.level + ')', canvas.width / 2, y + 75);
+
+ // Enemy HP bar
+ const hpBarY = y + 95;
+ const hpBarW = w - 200;
+ const hpX = x + 100;
+ const enemyMaxHp = e.maxHp || 1;
+ const enemyHp = isNaN(e.hp) ? 0 : e.hp;
+ ctx.fillStyle = '#333';
+ ctx.fillRect(hpX, hpBarY, hpBarW, 22);
+ const hpPercent = Math.max(0, enemyHp) / enemyMaxHp;
+ ctx.fillStyle = hpPercent > 0.5 ? '#e74c3c' : '#c0392b';
+ ctx.fillRect(hpX, hpBarY, hpBarW * hpPercent, 22);
+ ctx.strokeStyle = '#fff';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(hpX, hpBarY, hpBarW, 22);
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 13px Arial';
+ ctx.fillText(Math.floor(enemyHp) + '/' + enemyMaxHp, hpX + hpBarW / 2, hpBarY + 16);
+
+ // Player HP bar
+ const phpBarY = y + 135;
+ ctx.fillStyle = '#333';
+ ctx.fillRect(hpX, phpBarY, hpBarW, 18);
+ const phpPercent = this.player.hp / this.player.maxHp;
+ ctx.fillStyle = phpPercent > 0.5 ? '#27ae60' : '#e67e22';
+ ctx.fillRect(hpX, phpBarY, hpBarW * phpPercent, 18);
+ ctx.strokeStyle = '#fff';
+ ctx.strokeRect(hpX, phpBarY, hpBarW, 18);
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 12px Arial';
+ ctx.fillText('HP: ' + Math.floor(this.player.hp) + '/' + this.player.maxHp, hpX + hpBarW / 2, phpBarY + 14);
+
+ // Action buttons
+ const btnW = 140, btnH = 45;
+ const btnY = y + 175;
+ const btnGap = 20;
+ const totalBtns = 4;
+ const startBtnX = x + (w - (btnW * totalBtns + btnGap * (totalBtns - 1))) / 2;
+
+ // Button 1 - Attack
+ ctx.fillStyle = '#c0392b';
+ ctx.fillRect(startBtnX, btnY, btnW, btnH);
+ ctx.strokeStyle = '#e74c3c';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(startBtnX, btnY, btnW, btnH);
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 14px Arial';
+ ctx.fillText('⚔️ АТАКА (1)', startBtnX + btnW / 2, btnY + 28);
+
+ // Button 2 - Potion
+ ctx.fillStyle = '#27ae60';
+ ctx.fillRect(startBtnX + btnW + btnGap, btnY, btnW, btnH);
+ ctx.strokeStyle = '#2ecc71';
+ ctx.strokeRect(startBtnX + btnW + btnGap, btnY, btnW, btnH);
+ ctx.fillStyle = '#fff';
+ ctx.fillText('💚 ЗЕЛЬЕ (2)', startBtnX + btnW + btnGap + btnW / 2, btnY + 28);
+
+ // Button 3 - Magic
+ ctx.fillStyle = '#2980b9';
+ ctx.fillRect(startBtnX + (btnW + btnGap) * 2, btnY, btnW, btnH);
+ ctx.strokeStyle = '#3498db';
+ ctx.strokeRect(startBtnX + (btnW + btnGap) * 2, btnY, btnW, btnH);
+ ctx.fillStyle = '#fff';
+ ctx.fillText('✨ МАГИЯ (3)', startBtnX + (btnW + btnGap) * 2 + btnW / 2, btnY + 28);
+
+ // Button 4 - Flee
+ ctx.fillStyle = '#7f8c8d';
+ ctx.fillRect(startBtnX + (btnW + btnGap) * 3, btnY, btnW, btnH);
+ ctx.strokeStyle = '#95a5a6';
+ ctx.strokeRect(startBtnX + (btnW + btnGap) * 3, btnY, btnW, btnH);
+ ctx.fillStyle = '#fff';
+ ctx.fillText('🏃 БЕЖАТЬ (4)', startBtnX + (btnW + btnGap) * 3 + btnW / 2, btnY + 28);
+
+ // Store combat button positions
+ this.combatButtons = [
+ { x: startBtnX, y: btnY, w: btnW, h: btnH, action: () => this.combatAttack() },
+ { x: startBtnX + btnW + btnGap, y: btnY, w: btnW, h: btnH, action: () => this.combatHeal() },
+ { x: startBtnX + (btnW + btnGap) * 2, y: btnY, w: btnW, h: btnH, action: () => this.combatUseSpell() },
+ { x: startBtnX + (btnW + btnGap) * 3, y: btnY, w: btnW, h: btnH, action: () => this.fleeCombat() }
+ ];
+ },
+
+ renderShop() {
+ const ctx = Renderer.ctx;
+ const w = 700, h = 480;
+ const x = (canvas.width - w) / 2;
+ const y = (canvas.height - h) / 2;
+
+ // Background
+ ctx.fillStyle = 'rgba(15, 25, 40, 0.95)';
+ ctx.fillRect(x, y, w, h);
+ ctx.strokeStyle = '#2ecc71';
+ ctx.lineWidth = 3;
+ ctx.strokeRect(x, y, w, h);
+
+ // Title
+ ctx.fillStyle = '#2ecc71';
+ ctx.font = 'bold 28px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('🏪 МАГАЗИН 🏪', canvas.width / 2, y + 40);
+
+ // Gold display
+ ctx.fillStyle = '#ffd700';
+ ctx.font = 'bold 18px Arial';
+ ctx.fillText('💰 Ваше золото: ' + this.player.gold, canvas.width / 2, y + 75);
+
+ // Store shop buttons
+ this.shopButtons = [];
+
+ // Items grid
+ const itemW = 180, itemH = 55;
+ const itemsPerRow = 3;
+ const startX = x + 40;
+ const startY = y + 110;
+ const gapX = 15, gapY = 12;
+
+ this.shopItems.forEach((item, i) => {
+ const col = i % itemsPerRow;
+ const row = Math.floor(i / itemsPerRow);
+ const ix = startX + col * (itemW + gapX);
+ const iy = startY + row * (itemH + gapY);
+
+ // Store button position
+ this.shopButtons.push({ x: ix, y: iy, w: itemW, h: itemH, item: item });
+
+ // Draw button
+ ctx.fillStyle = 'rgba(30, 50, 40, 0.9)';
+ ctx.fillRect(ix, iy, itemW, itemH);
+ ctx.strokeStyle = '#27ae60';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(ix, iy, itemW, itemH);
+
+ // Item name
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 13px Arial';
+ ctx.textAlign = 'left';
+ ctx.fillText(item.name, ix + 10, iy + 22);
+
+ // Price
+ ctx.fillStyle = '#ffd700';
+ ctx.font = '12px Arial';
+ ctx.fillText('💰 ' + item.value, ix + 10, iy + 42);
+
+ // Stats hint
+ if (item.damage) {
+ ctx.fillStyle = '#e74c3c';
+ ctx.fillText('⚔️ ' + item.damage, ix + 90, iy + 42);
+ } else if (item.defense) {
+ ctx.fillStyle = '#3498db';
+ ctx.fillText('🛡️ ' + item.defense, ix + 90, iy + 42);
+ }
+ });
+
+ // Instructions
+ ctx.fillStyle = '#666';
+ ctx.font = '12px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('Кликни на предмет для покупки | I или ESC - закрыть', canvas.width / 2, y + h - 20);
+ },
+
+ buyItem(item) {
+ if (this.player.gold >= item.value) {
+ this.player.gold -= item.value;
+ RPG.addItemToInventory(this.player, {...item, id: item.id + '_' + Date.now()});
+ this.showMessage('Куплено: ' + item.name);
+ this.updateUI();
+ } else {
+ this.showMessage('Недостаточно золота!');
+ }
+ },
+
+ gameLoop(ts) {
+ const dt = ts - this.lastTime;
+ this.lastTime = ts;
+ this.time = ts;
+ this.update(dt);
+ this.render();
+ requestAnimationFrame(t => this.gameLoop(t));
+ },
+
+ start() {
+ this.updateUI();
+ requestAnimationFrame(t => this.gameLoop(t));
+ }
+};
diff --git a/RPG_FromFreeModel/index.html b/RPG_FromFreeModel/index.html
new file mode 100644
index 0000000..d2e2868
--- /dev/null
+++ b/RPG_FromFreeModel/index.html
@@ -0,0 +1,295 @@
+
+
+
+
+
+ Изометрическая RPG
+
+
+
+ ⚔️ Изометрическая RPG
+
+
+
+
❤️ HP: 100
+
💧 MP: 50
+
⭐ Уровень: 1
+
💰 Золото: 0
+
🗡️ Сила: 10
+
+
+
+
+
⚔️ Изометрическая RPG ⚔️
+
Выберите своего героя
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WASD или ↑↓←→ - движение |
+ I - инвентарь | Q - квесты | H - помощь |
+ Мышь - клик для перемещения
+
+
+
+
+
+
+
+
diff --git a/RPG_FromFreeModel/renderer.js b/RPG_FromFreeModel/renderer.js
new file mode 100644
index 0000000..63a3818
--- /dev/null
+++ b/RPG_FromFreeModel/renderer.js
@@ -0,0 +1,1090 @@
+// ========================================
+// 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);
+ }
+};
diff --git a/RPG_FromFreeModel/rpg.js b/RPG_FromFreeModel/rpg.js
new file mode 100644
index 0000000..03d474a
--- /dev/null
+++ b/RPG_FromFreeModel/rpg.js
@@ -0,0 +1,1252 @@
+// ========================================
+// RPG.JS - Полная RPG система
+// ========================================
+
+const RPG = {
+ // Типы предметов
+ ItemType: {
+ WEAPON: 'weapon',
+ ARMOR: 'armor',
+ POTION: 'potion',
+ SCROLL: 'scroll',
+ QUEST: 'quest',
+ MATERIAL: 'material',
+ GOLD: 'gold',
+ FOOD: 'food',
+ GEM: 'gem',
+ KEY: 'key'
+ },
+
+ // Слоты экипировки
+ EquipmentSlot: {
+ HEAD: 'head',
+ CHEST: 'chest',
+ LEGS: 'legs',
+ FEET: 'feet',
+ HANDS: 'hands',
+ MAIN_HAND: 'main_hand',
+ OFF_HAND: 'off_hand',
+ ACCESSORY1: 'accessory1',
+ ACCESSORY2: 'accessory2'
+ },
+
+ // Типы оружия
+ WeaponType: {
+ SWORD: 'sword',
+ AXE: 'axe',
+ MACE: 'mace',
+ DAGGER: 'dagger',
+ BOW: 'bow',
+ STAFF: 'staff',
+ WAND: 'wand'
+ },
+
+ // Типы брони
+ ArmorType: {
+ HELMET: 'helmet',
+ CHESTPLATE: 'chestplate',
+ LEGGINGS: 'leggings',
+ BOOTS: 'boots',
+ SHIELD: 'shield',
+ CLOAK: 'cloak'
+ },
+
+ // Редкость предметов
+ Rarity: {
+ COMMON: 'common',
+ UNCOMMON: 'uncommon',
+ RARE: 'rare',
+ EPIC: 'epic',
+ LEGENDARY: 'legendary',
+ MYTHIC: 'mythic'
+ },
+
+ // Цвета редкости
+ RarityColors: {
+ common: '#b0b0b0',
+ uncommon: '#44ff44',
+ rare: '#4444ff',
+ epic: '#aa44ff',
+ legendary: '#ffaa00',
+ mythic: '#ff44ff'
+ },
+
+ // ========================================
+ // СОЗДАНИЕ ПЕРСОНАЖА
+ // ========================================
+
+ createCharacter(name, gender = 'male', classType = 'warrior') {
+ const classStats = {
+ warrior: {
+ hp: 120, mp: 30, str: 12, def: 10, mag: 5, spd: 8,
+ description: 'Сильный воин с высокой защитой'
+ },
+ mage: {
+ hp: 70, mp: 100, str: 4, def: 4, mag: 15, spd: 6,
+ description: 'Мастер магии с мощными заклинаниями'
+ },
+ archer: {
+ hp: 90, mp: 50, str: 10, def: 6, mag: 6, spd: 12,
+ description: 'Быстрый стрелок с высокой ловкостью'
+ },
+ thief: {
+ hp: 85, mp: 45, str: 8, def: 5, mag: 8, spd: 14,
+ description: 'Ловкий плут, мастер скрытности'
+ },
+ paladin: {
+ hp: 110, mp: 60, str: 10, def: 12, mag: 8, spd: 6,
+ description: 'Святой воин с лечащей магией'
+ },
+ necromancer: {
+ hp: 80, mp: 90, str: 6, def: 6, mag: 14, spd: 7,
+ description: 'Мастер темной магии и призыва'
+ }
+ };
+
+ const stats = classStats[classType] || classStats.warrior;
+
+ return {
+ id: 'player_' + Date.now(),
+ name: name,
+ gender: gender,
+ class: classType,
+ description: classStats[classType].description,
+ level: 1,
+ exp: 0,
+ expToNextLevel: 100,
+ hp: stats.hp,
+ maxHp: stats.hp,
+ mp: stats.mp,
+ maxMp: stats.mp,
+ baseStr: stats.str,
+ baseDef: stats.def,
+ baseMag: stats.mag,
+ baseSpd: stats.spd,
+ str: stats.str,
+ def: stats.def,
+ mag: stats.mag,
+ spd: stats.spd,
+ gold: 50,
+ hairColor: '#4a3520',
+ // Позиция
+ x: 5,
+ y: 5,
+ targetX: 5,
+ targetY: 5,
+ isMoving: false,
+ moveProgress: 0,
+ // Инвентарь и экипировка
+ inventory: [],
+ equipment: {
+ head: null,
+ chest: null,
+ legs: null,
+ feet: null,
+ main_hand: null,
+ off_hand: null,
+ accessory1: null,
+ accessory2: null
+ },
+ // Навыки и заклинания
+ skills: [],
+ spells: [],
+ learnedSpells: [],
+ // Квесты
+ quests: [],
+ completedQuests: [],
+ // Достижения
+ achievements: [],
+ // Боевые флаги
+ inCombat: false,
+ currentEnemy: null,
+ combatStats: {
+ attacks: 0,
+ damageDealt: 0,
+ damageTaken: 0,
+ enemiesKilled: 0
+ }
+ };
+ },
+
+ // ========================================
+ // СОЗДАНИЕ ПРЕДМЕТОВ
+ // ========================================
+
+ createItem(id, type, name, options = {}) {
+ return {
+ id: id || 'item_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
+ type: type,
+ name: name,
+ description: options.description || '',
+ value: options.value || 0,
+ rarity: options.rarity || 'common',
+ // Для оружия
+ damage: options.damage || 0,
+ damageType: options.damageType || 'physical', // physical, magic, fire, ice, lightning
+ weaponType: options.weaponType || null,
+ // Для брони
+ defense: options.defense || 0,
+ armorType: options.armorType || null,
+ // Для зелий
+ healAmount: options.healAmount || 0,
+ restoreMp: options.restoreMp || 0,
+ // Для еды
+ hungerRestore: options.hungerRestore || 0,
+ // Для свитков/книг
+ spell: options.spell || null,
+ spellLevel: options.spellLevel || 1,
+ // Требования
+ requiredLevel: options.requiredLevel || 1,
+ requiredClass: options.requiredClass || null,
+ // Особые свойства
+ special: options.special || null, // { name: 'something', value: x }
+ // Визуал
+ icon: options.icon || '?',
+ color: options.color || null,
+ // Остальное
+ stackable: options.stackable || false,
+ quantity: options.quantity || 1,
+ subtype: options.subtype || type,
+ // Статы
+ bonusStr: options.bonusStr || 0,
+ bonusDef: options.bonusDef || 0,
+ bonusMag: options.bonusMag || 0,
+ bonusSpd: options.bonusSpd || 0,
+ bonusHp: options.bonusHp || 0,
+ bonusMp: options.bonusMp || 0
+ };
+ },
+
+ // ========================================
+ // СОЗДАНИЕ ВРАГОВ
+ // ========================================
+
+ createEnemy(type, level, x, y) {
+ const enemyTemplates = {
+ // Обычные враги
+ goblin: {
+ name: 'Гоблин',
+ baseHp: 30, baseDmg: 8, baseDef: 2,
+ exp: 20, gold: 10,
+ lootTable: ['goblin_ear', 'herb'],
+ behavior: 'aggressive'
+ },
+ orc: {
+ name: 'Орк',
+ baseHp: 50, baseDmg: 12, baseDef: 4,
+ exp: 40, gold: 25,
+ lootTable: ['orc_tusk', 'meat'],
+ behavior: 'aggressive'
+ },
+ skeleton: {
+ name: 'Скелет',
+ baseHp: 40, baseDmg: 10, baseDef: 6,
+ exp: 30, gold: 15,
+ lootTable: ['bone', 'bone_dagger'],
+ behavior: 'aggressive'
+ },
+ slime: {
+ name: 'Слизень',
+ baseHp: 20, baseDmg: 5, baseDef: 0,
+ exp: 10, gold: 5,
+ lootTable: ['slime_gel'],
+ behavior: 'passive'
+ },
+ bandit: {
+ name: 'Разбойник',
+ baseHp: 35, baseDmg: 9, baseDef: 3,
+ exp: 25, gold: 20,
+ lootTable: ['money_pouch', 'dagger'],
+ behavior: 'aggressive'
+ },
+ // Маги
+ mage: {
+ name: 'Маг',
+ baseHp: 25, baseDmg: 20, baseDef: 2, baseMp: 50,
+ exp: 50, gold: 30,
+ lootTable: ['mana_potion', 'scroll'],
+ behavior: 'ranged'
+ },
+ // Элитные
+ troll: {
+ name: 'Тролль',
+ baseHp: 100, baseDmg: 18, baseDef: 8,
+ exp: 80, gold: 60,
+ lootTable: ['troll_heart', 'club'],
+ behavior: 'aggressive'
+ },
+ // Боссы
+ dragon: {
+ name: 'Дракон',
+ baseHp: 200, baseDmg: 30, baseDef: 15,
+ exp: 200, gold: 200,
+ lootTable: ['dragon_scale', 'dragon_heart', 'dragon_egg'],
+ behavior: 'boss',
+ isBoss: true
+ },
+ demon: {
+ name: 'Демон',
+ baseHp: 150, baseDmg: 25, baseDef: 10,
+ exp: 150, gold: 150,
+ lootTable: ['demon_heart', 'infernal_orb'],
+ behavior: 'boss',
+ isBoss: true
+ },
+ lich: {
+ name: 'Лич',
+ baseHp: 100, baseDmg: 35, baseDef: 8, baseMp: 100,
+ exp: 180, gold: 120,
+ lootTable: ['necronomicon', 'skull_staff'],
+ behavior: 'boss',
+ isBoss: true
+ }
+ };
+
+ const template = enemyTemplates[type] || enemyTemplates.goblin;
+ const levelMultiplier = 1 + (level - 1) * 0.2;
+
+ return {
+ id: 'enemy_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
+ type: type,
+ name: template.name,
+ level: level,
+ hp: Math.floor(template.baseHp * levelMultiplier),
+ maxHp: Math.floor(template.baseHp * levelMultiplier),
+ mp: (template.baseMp || 0) * levelMultiplier,
+ maxMp: (template.baseMp || 0) * levelMultiplier,
+ damage: Math.floor(template.baseDmg * levelMultiplier),
+ defense: Math.floor((template.baseDef || 0) * levelMultiplier),
+ exp: Math.floor(template.exp * levelMultiplier),
+ gold: Math.floor(template.gold * levelMultiplier),
+ lootTable: template.lootTable,
+ behavior: template.behavior,
+ isBoss: template.isBoss || false,
+ x: x,
+ y: y,
+ height: type === 'slime' ? 25 : 45,
+ isAttacking: false,
+ attackCooldown: 0
+ };
+ },
+
+ // ========================================
+ // СОЗДАНИЕ NPC
+ // ========================================
+
+ createNPC(name, x, y, options = {}) {
+ return {
+ id: 'npc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
+ name: name,
+ x: x,
+ y: y,
+ color: options.color || '#8b6914',
+ // Диалоги
+ dialog: options.dialog || 'Приветствую, путник!',
+ dialogOptions: options.dialogOptions || [],
+ // Торговля
+ shop: options.shop || null,
+ shopName: options.shopName || 'Магазин',
+ // Квесты
+ quests: options.quests || [],
+ // Услуги
+ services: options.services || [], // heal, teleport, identify
+ // Внешность
+ sprite: options.sprite || 'villager',
+ // Отношения
+ faction: options.faction || 'neutral',
+ attitude: options.attitude || 0
+ };
+ },
+
+ // ========================================
+ // КВЕСТОВАЯ СИСТЕМА
+ // ========================================
+
+ QuestStatus: {
+ NOT_STARTED: 'not_started',
+ ACTIVE: 'active',
+ COMPLETED: 'completed',
+ FAILED: 'failed'
+ },
+
+ createQuest(id, name, description, options = {}) {
+ return {
+ id: id,
+ name: name,
+ description: description,
+ // Тип квеста
+ type: options.type || 'kill', // kill, collect, talk, explore, deliver
+ // Цели
+ objectives: options.objectives || [],
+ // Награды
+ rewards: options.rewards || { exp: 0, gold: 0, items: [] },
+ // Требования
+ requiredLevel: options.requiredLevel || 1,
+ requiredQuest: options.requiredQuest || null,
+ // Класс
+ requiredClass: options.requiredClass || null,
+ // Цепочка
+ nextQuest: options.nextQuest || null,
+ // Отслеживание
+ progress: {},
+ status: 'not_started'
+ };
+ },
+
+ // Добавить квест персонажу
+ addQuest(character, quest) {
+ if (character.completedQuests.includes(quest.id)) return false;
+ if (character.quests.find(q => q.id === quest.id)) return false;
+
+ const newQuest = JSON.parse(JSON.stringify(quest));
+ newQuest.status = 'active';
+
+ // Инициализация прогресса
+ newQuest.objectives.forEach(obj => {
+ newQuest.progress[obj.id] = { current: 0, target: obj.target, completed: false };
+ });
+
+ character.quests.push(newQuest);
+ return true;
+ },
+
+ // Обновить прогресс квеста
+ updateQuestProgress(character, objectiveType, targetId, amount = 1) {
+ character.quests.forEach(quest => {
+ if (quest.status !== 'active') return;
+
+ quest.objectives.forEach(obj => {
+ if (obj.type === objectiveType && obj.target === targetId) {
+ quest.progress[obj.id].current += amount;
+ if (quest.progress[obj.id].current >= obj.target) {
+ quest.progress[obj.id].completed = true;
+ }
+ }
+ });
+ });
+ },
+
+ // Проверить завершение квеста
+ checkQuestCompletion(character, questId) {
+ const quest = character.quests.find(q => q.id === questId);
+ if (!quest || quest.status !== 'active') return false;
+
+ const allCompleted = quest.objectives.every(obj => quest.progress[obj.id].completed);
+ if (allCompleted) {
+ quest.status = 'completed';
+ return true;
+ }
+ return false;
+ },
+
+ // Получить награду квеста
+ completeQuest(character, questId) {
+ const questIndex = character.quests.findIndex(q => q.id === questId);
+ if (questIndex === -1) return null;
+
+ const quest = character.quests[questIndex];
+ if (quest.status !== 'completed') return null;
+
+ // Удаляем из активных и добавляем в завершённые
+ character.quests.splice(questIndex, 1);
+ character.completedQuests.push(questId);
+
+ // Выдаём награды
+ character.exp += quest.rewards.exp;
+ character.gold += quest.rewards.gold;
+
+ const items = [];
+ if (quest.rewards.items) {
+ quest.rewards.items.forEach(itemTemplate => {
+ const item = this.createItem(itemTemplate.id, itemTemplate.type, itemTemplate.name, itemTemplate);
+ this.addItemToInventory(character, item);
+ items.push(item);
+ });
+ }
+
+ return { quest, items };
+ },
+
+ // ========================================
+ // МАГИЧЕСКАЯ СИСТЕМА
+ // ========================================
+
+ SpellType: {
+ FIRE: 'fire',
+ ICE: 'ice',
+ LIGHTNING: 'lightning',
+ HEALING: 'healing',
+ BUFF: 'buff',
+ DEBUFF: 'debuff',
+ SUMMON: 'summon',
+ DARK: 'dark',
+ HOLY: 'holy'
+ },
+
+ SpellSchool: {
+ ELEMENTAL: 'elemental',
+ MYSTIC: 'mystic',
+ DARK: 'dark',
+ HOLY: 'holy'
+ },
+
+ // База заклинаний
+ SpellDatabase: {
+ // Огненная магия
+ fireball: {
+ name: 'Огненный шар',
+ type: 'fire',
+ school: 'elemental',
+ mpCost: 15,
+ damage: 25,
+ damageType: 'fire',
+ range: 5,
+ cooldown: 3000,
+ description: 'Наносит урон огнём'
+ },
+ fireblast: {
+ name: 'Огненный взрыв',
+ type: 'fire',
+ school: 'elemental',
+ mpCost: 25,
+ damage: 40,
+ damageType: 'fire',
+ range: 3,
+ cooldown: 5000,
+ description: 'Мощный взрыв огня вокруг'
+ },
+ // Ледяная магия
+ frostbolt: {
+ name: 'Ледяная стрела',
+ type: 'ice',
+ school: 'elemental',
+ mpCost: 12,
+ damage: 20,
+ damageType: 'ice',
+ range: 6,
+ cooldown: 2000,
+ description: 'Наносит урон льдом и замедляет'
+ },
+ blizzard: {
+ name: 'Метель',
+ type: 'ice',
+ school: 'elemental',
+ mpCost: 35,
+ damage: 50,
+ damageType: 'ice',
+ range: 4,
+ cooldown: 8000,
+ description: 'Атакует всех врагов вокруг'
+ },
+ // Молния
+ lightning: {
+ name: 'Молния',
+ type: 'lightning',
+ school: 'elemental',
+ mpCost: 20,
+ damage: 35,
+ damageType: 'lightning',
+ range: 6,
+ cooldown: 3000,
+ description: 'Быстрая атака молнией'
+ },
+ // Лечение
+ heal: {
+ name: 'Исцеление',
+ type: 'healing',
+ school: 'mystic',
+ mpCost: 10,
+ heal: 30,
+ range: 4,
+ cooldown: 4000,
+ description: 'Восстанавливает здоровье'
+ },
+ greater_heal: {
+ name: 'Greater Heal',
+ type: 'healing',
+ school: 'mystic',
+ mpCost: 25,
+ heal: 70,
+ range: 4,
+ cooldown: 6000,
+ description: 'Мощное исцеление'
+ },
+ // Баффы
+ power: {
+ name: 'Усиление',
+ type: 'buff',
+ school: 'mystic',
+ mpCost: 15,
+ buff: { stat: 'damage', value: 1.5, duration: 10000 },
+ cooldown: 15000,
+ description: 'Увеличивает урон на 50%'
+ },
+ shield: {
+ name: 'Щит',
+ type: 'buff',
+ school: 'mystic',
+ mpCost: 20,
+ buff: { stat: 'defense', value: 2, duration: 15000 },
+ cooldown: 20000,
+ description: 'Удваивает защиту'
+ },
+ // Тёмная магия
+ life_drain: {
+ name: 'Похищение жизни',
+ type: 'dark',
+ school: 'dark',
+ mpCost: 18,
+ damage: 15,
+ heal: 10,
+ range: 4,
+ cooldown: 5000,
+ description: 'Наносит урон и восстанавливает HP'
+ },
+ curse: {
+ name: 'Проклятие',
+ type: 'debuff',
+ school: 'dark',
+ mpCost: 15,
+ debuff: { stat: 'damage', value: 0.7, duration: 8000 },
+ range: 5,
+ cooldown: 10000,
+ description: 'Снижает урон врага на 30%'
+ },
+ // Святая магия
+ holy_fire: {
+ name: 'Священный огонь',
+ type: 'holy',
+ school: 'holy',
+ mpCost: 22,
+ damage: 30,
+ damageType: 'holy',
+ range: 5,
+ cooldown: 4000,
+ description: 'Атакует нечисть'
+ },
+ resurrect: {
+ name: 'Воскрешение',
+ type: 'holy',
+ school: 'holy',
+ mpCost: 50,
+ resurrect: true,
+ cooldown: 60000,
+ description: 'Воскрешает союзника в бою'
+ }
+ },
+
+ // Изучить заклинание
+ learnSpell(character, spellId) {
+ const spell = this.SpellDatabase[spellId];
+ if (!spell) return false;
+ if (character.learnedSpells.includes(spellId)) return false;
+
+ // Проверка требований (уровень, класс)
+ if (spell.requiredLevel && character.level < spell.requiredLevel) return false;
+ if (spell.requiredClass && character.class !== spell.requiredClass) return false;
+
+ character.learnedSpells.push(spellId);
+ return true;
+ },
+
+ // Использовать заклинание
+ castSpell(character, spellId, target = null) {
+ const spell = this.SpellDatabase[spellId];
+ if (!spell) return { success: false, message: 'Заклинание не найдено!' };
+ if (!character.learnedSpells.includes(spellId)) return { success: false, message: 'Вы не изучили это заклинание!' };
+
+ // Проверка MP
+ if (character.mp < spell.mpCost) return { success: false, message: 'Недостаточно маны!' };
+
+ // Проверка кулдауна
+ const lastCast = character.combatStats.lastSpellCast || {};
+ const now = Date.now();
+ if (lastCast[spellId] && now - lastCast[spellId] < spell.cooldown) {
+ return { success: false, message: 'Заклинание на перезарядке!' };
+ }
+
+ // Списываем ману
+ character.mp -= spell.mpCost;
+
+ // Записываем кулдаун
+ character.combatStats.lastSpellCast = character.combatStats.lastSpellCast || {};
+ character.combatStats.lastSpellCast[spellId] = now;
+
+ return {
+ success: true,
+ spell: spell,
+ target: target,
+ message: `Прочитано: ${spell.name}!`
+ };
+ },
+
+ // ========================================
+ // ИНВЕНТАРЬ И ЭКИПИРОВКА
+ // ========================================
+
+ addItemToInventory(character, item) {
+ // Проверка стекируемости
+ if (item.stackable) {
+ const existing = character.inventory.find(i =>
+ i.id === item.id && i.stackable
+ );
+ if (existing) {
+ existing.quantity += item.quantity;
+ return true;
+ }
+ }
+
+ // Проверка лимита инвентаря
+ if (character.inventory.length >= 25) {
+ return false;
+ }
+
+ character.inventory.push(item);
+ return true;
+ },
+
+ removeItemFromInventory(character, itemId, quantity = 1) {
+ const index = character.inventory.findIndex(i => i.id === itemId);
+ if (index === -1) return false;
+
+ const item = character.inventory[index];
+
+ if (item.stackable) {
+ if (item.quantity >= quantity) {
+ item.quantity -= quantity;
+ if (item.quantity <= 0) {
+ character.inventory.splice(index, 1);
+ }
+ return true;
+ }
+ } else {
+ character.inventory.splice(index, 1);
+ return true;
+ }
+
+ return false;
+ },
+
+ equipItem(character, item) {
+ if (item.type !== this.ItemType.WEAPON && item.type !== this.ItemType.ARMOR) {
+ return { success: false, message: 'Этот предмет нельзя экипировать!' };
+ }
+
+ // Проверка требований
+ if (item.requiredLevel && character.level < item.requiredLevel) {
+ return { success: false, message: `Требуется уровень ${item.requiredLevel}!` };
+ }
+ if (item.requiredClass && character.class !== item.requiredClass) {
+ return { success: false, message: `Только для класса ${item.requiredClass}!` };
+ }
+
+ let slot;
+ if (item.type === this.ItemType.WEAPON) {
+ slot = this.EquipmentSlot.MAIN_HAND;
+ } else if (item.type === this.ItemType.ARMOR) {
+ switch(item.armorType) {
+ case this.ArmorType.HELMET: slot = this.EquipmentSlot.HEAD; break;
+ case this.ArmorType.CHESTPLATE: slot = this.EquipmentSlot.CHEST; break;
+ case this.ArmorType.LEGGINGS: slot = this.EquipmentSlot.LEGS; break;
+ case this.ArmorType.BOOTS: slot = this.EquipmentSlot.FEET; break;
+ case this.ArmorType.SHIELD: slot = this.EquipmentSlot.OFF_HAND; break;
+ case this.ArmorType.CLOAK: slot = this.EquipmentSlot.CHEST; break;
+ default: slot = this.EquipmentSlot.CHEST;
+ }
+ }
+
+ // Снятие текущего предмета
+ if (character.equipment[slot]) {
+ this.unequipItem(character, slot);
+ }
+
+ // Экипировка
+ character.equipment[slot] = item;
+
+ // Удаляем из инвентаря
+ const invIndex = character.inventory.findIndex(i => i.id === item.id);
+ if (invIndex >= 0) {
+ character.inventory.splice(invIndex, 1);
+ }
+
+ return { success: true, message: `Экипировано: ${item.name}` };
+ },
+
+ unequipItem(character, slot) {
+ const item = character.equipment[slot];
+ if (!item) return false;
+
+ this.addItemToInventory(character, item);
+ character.equipment[slot] = null;
+ return true;
+ },
+
+ // Получение бонусов от экипировки
+ getEquipmentBonuses(character) {
+ const bonuses = {
+ damage: 0,
+ defense: 0,
+ hp: 0,
+ mp: 0,
+ str: 0,
+ def: 0,
+ mag: 0,
+ spd: 0
+ };
+
+ Object.values(character.equipment).forEach(item => {
+ if (!item) return;
+ bonuses.damage += item.damage || 0;
+ bonuses.defense += item.defense || 0;
+ bonuses.hp += item.bonusHp || 0;
+ bonuses.mp += item.bonusMp || 0;
+ bonuses.str += item.bonusStr || 0;
+ bonuses.def += item.bonusDef || 0;
+ bonuses.mag += item.bonusMag || 0;
+ bonuses.spd += item.bonusSpd || 0;
+ });
+
+ return bonuses;
+ },
+
+ // Полный расчет статов
+ getTotalStats(character) {
+ const bonuses = this.getEquipmentBonuses(character);
+
+ return {
+ hp: (character.maxHp || 100) + bonuses.hp,
+ mp: (character.maxMp || 50) + bonuses.mp,
+ damage: (character.str || 10) + bonuses.damage,
+ defense: (character.def || 5) + bonuses.defense,
+ magic: (character.mag || 5) + bonuses.mag,
+ speed: (character.spd || 5) + bonuses.spd
+ };
+ },
+
+ // Использование предмета
+ useItem(character, item) {
+ if (item.type === this.ItemType.POTION) {
+ const oldHp = character.hp;
+ const oldMp = character.mp;
+
+ character.hp = Math.min(character.hp + (item.healAmount || 0), character.maxHp);
+ character.mp = Math.min(character.mp + (item.restoreMp || 0), character.maxMp);
+
+ const healed = character.hp - oldHp;
+ const restored = character.mp - oldMp;
+
+ if (item.stackable) {
+ this.removeItemFromInventory(character, item.id, 1);
+ } else {
+ this.removeItemFromInventory(character, item.id);
+ }
+
+ return {
+ success: true,
+ message: healed > 0 ? `HP: +${healed}` : `MP: +${restored}`,
+ used: true
+ };
+ }
+
+ if (item.type === this.ItemType.FOOD) {
+ character.hp = Math.min(character.hp + (item.healAmount || 20), character.maxHp);
+
+ if (item.stackable) {
+ this.removeItemFromInventory(character, item.id, 1);
+ } else {
+ this.removeItemFromInventory(character, item.id);
+ }
+
+ return { success: true, message: `Съедено: ${item.name}` };
+ }
+
+ if (item.type === this.ItemType.SCROLL && item.spell) {
+ return {
+ success: true,
+ message: `Вы изучили заклинание: ${item.spell}!`,
+ scrollUsed: true,
+ spellLearned: item.spell
+ };
+ }
+
+ return { success: false, message: 'Этот предмет нельзя использовать!' };
+ },
+
+ // ========================================
+ // БОЕВАЯ СИСТЕМА
+ // ========================================
+
+ attack(attacker, defender) {
+ const attackerStats = attacker.isPlayer ? this.getTotalStats(attacker) : {
+ damage: attacker.damage,
+ defense: attacker.defense || 0,
+ speed: attacker.level * 2
+ };
+
+ const defenderStats = defender.isPlayer ? this.getTotalStats(defender) : {
+ damage: defender.damage || 0,
+ defense: defender.defense || 0,
+ speed: defender.level * 2
+ };
+
+ // Базовый урон
+ let damage = Math.max(1, attackerStats.damage - defenderStats.defense);
+
+ // Критический удар (10% базовый + бонус от скорости)
+ const critChance = 0.1 + (attackerStats.speed || 0) * 0.01;
+ const isCrit = Math.random() < critChance;
+ if (isCrit) {
+ damage *= 1.5 + (Math.random() * 0.5);
+ }
+
+ // Элементальная уязвимость
+ if (attacker.damageType && defender.weakness === attacker.damageType) {
+ damage *= 1.5;
+ }
+
+ // Случайное отклонение (±15%)
+ const variance = 0.85 + Math.random() * 0.3;
+ damage = Math.floor(damage * variance);
+
+ defender.hp = Math.max(0, defender.hp - damage);
+
+ // Статистика
+ if (attacker.isPlayer) {
+ attacker.combatStats.damageDealt += damage;
+ attacker.combatStats.attacks++;
+ }
+ if (defender.isPlayer) {
+ defender.combatStats.damageTaken += damage;
+ }
+
+ return {
+ damage: damage,
+ isCrit: isCrit,
+ killed: defender.hp <= 0
+ };
+ },
+
+ // Атака врага (AI)
+ enemyAttack(enemy, player) {
+ const stats = RPG.getTotalStats(player);
+ let damage = Math.max(1, enemy.damage - stats.defense);
+
+ const isCrit = Math.random() < 0.1;
+ if (isCrit) damage *= 1.5;
+
+ damage = Math.floor(damage * (0.9 + Math.random() * 0.2));
+
+ player.hp = Math.max(0, player.hp - damage);
+ player.combatStats.damageTaken += damage;
+
+ return { damage, isCrit, killed: player.hp <= 0 };
+ },
+
+ // ========================================
+ // СИСТЕМА УРОВНЕЙ
+ // ========================================
+
+ checkLevelUp(character) {
+ let leveledUp = false;
+
+ while (character.exp >= character.expToNextLevel) {
+ character.exp -= character.expToNextLevel;
+ character.level++;
+ character.expToNextLevel = Math.floor(100 * Math.pow(1.5, character.level - 1));
+
+ // Бонус к статам по классу
+ const classBonuses = {
+ warrior: { hp: 15, mp: 3, str: 3, def: 2, mag: 1, spd: 1 },
+ mage: { hp: 5, mp: 15, str: 1, def: 1, mag: 3, spd: 1 },
+ archer: { hp: 8, mp: 5, str: 2, def: 1, mag: 1, spd: 2 },
+ thief: { hp: 7, mp: 4, str: 2, def: 1, mag: 2, spd: 3 },
+ paladin: { hp: 12, mp: 8, str: 2, def: 3, mag: 2, spd: 1 },
+ necromancer: { hp: 6, mp: 12, str: 1, def: 1, mag: 4, spd: 1 }
+ };
+
+ const bonus = classBonuses[character.class] || classBonuses.warrior;
+ character.maxHp += bonus.hp;
+ character.maxMp += bonus.mp;
+ character.baseStr += bonus.str;
+ character.baseDef += bonus.def;
+ character.baseMag += bonus.mag;
+ character.baseSpd += bonus.spd;
+
+ // Пересчитываем текущие статы
+ character.str = character.baseStr;
+ character.def = character.baseDef;
+ character.mag = character.baseMag;
+ character.spd = character.baseSpd;
+
+ // Восстановление при левелапе
+ character.hp = character.maxHp;
+ character.mp = character.maxMp;
+
+ leveledUp = true;
+ }
+
+ return leveledUp;
+ },
+
+ // ========================================
+ // ГЕНЕРАЦИЯ ЛУТА
+ // ========================================
+
+ generateLoot(enemy) {
+ const loot = [];
+
+ // Всегда даёт золото
+ loot.push(this.createItem(
+ 'gold',
+ this.ItemType.GOLD,
+ 'Золото',
+ { value: enemy.gold, stackable: true, quantity: enemy.gold, rarity: 'common' }
+ ));
+
+ // Шанс выпадения из таблицы
+ if (enemy.lootTable) {
+ enemy.lootTable.forEach(lootId => {
+ if (Math.random() < 0.2) { // 20% шанс на каждый предмет
+ loot.push(this.createLootItem(lootId, enemy.level));
+ }
+ });
+ }
+
+ // Шанс случайного предмета (30%)
+ if (Math.random() < 0.3) {
+ const randomItems = [
+ { type: 'potion', healAmount: 30 },
+ { type: 'potion', healAmount: 50 },
+ { type: 'potion', restoreMp: 20 },
+ { type: 'food', healAmount: 15 },
+ { type: 'gold', value: Math.floor(enemy.gold * 0.5) }
+ ];
+ const template = randomItems[Math.floor(Math.random() * randomItems.length)];
+ loot.push(this.createItem(
+ 'loot_' + Date.now(),
+ template.type,
+ template.type === 'gold' ? 'Золото' : 'Зелье',
+ template
+ ));
+ }
+
+ return loot;
+ },
+
+ createLootItem(lootId, level) {
+ const lootDatabase = {
+ // Материалы
+ goblin_ear: { name: 'Ухо гоблина', value: 5, type: 'material' },
+ orc_tusk: { name: 'Клык орка', value: 10, type: 'material' },
+ bone: { name: 'Кость', value: 3, type: 'material' },
+ herb: { name: 'Трава', value: 8, type: 'material' },
+ slime_gel: { name: 'Слизь', value: 5, type: 'material' },
+ troll_heart: { name: 'Сердце тролля', value: 50, type: 'material', rarity: 'rare' },
+ dragon_scale: { name: 'Чешуя дракона', value: 100, type: 'material', rarity: 'epic' },
+ demon_heart: { name: 'Сердце демона', value: 80, type: 'material', rarity: 'epic' },
+ // Оружие
+ bone_dagger: { name: 'Костяной кинжал', value: 25, type: 'weapon', damage: 8, rarity: 'uncommon' },
+ club: { name: 'Дубина', value: 15, type: 'weapon', damage: 6 },
+ skull_staff: { name: 'Посох черепа', value: 80, type: 'weapon', damage: 15, rarity: 'rare' },
+ // Щиты
+ shield_wood: { name: 'Деревянный щит', value: 20, type: 'armor', defense: 3 },
+ // Зелья
+ health_potion: { name: 'Зелье HP', value: 25, type: 'potion', healAmount: 40, rarity: 'uncommon' },
+ mana_potion: { name: 'Зелье MP', value: 30, type: 'potion', restoreMp: 30, rarity: 'uncommon' },
+ // Еда
+ meat: { name: 'Мясо', value: 10, type: 'food', healAmount: 20 },
+ // Книги
+ scroll: { name: 'Свиток', value: 50, type: 'scroll', spell: 'fireball' },
+ necronomicon: { name: 'Некрономикон', value: 200, type: 'scroll', spell: 'life_drain', rarity: 'rare' },
+ // Разное
+ money_pouch: { name: 'Кошелёк', value: 15, type: 'gold' },
+ dragon_heart: { name: 'Сердце дракона', value: 300, type: 'material', rarity: 'legendary' },
+ dragon_egg: { name: 'Яйцо дракона', value: 500, type: 'quest', rarity: 'legendary' },
+ infernal_orb: { name: 'Инфернальная сфера', value: 150, type: 'material', rarity: 'epic' }
+ };
+
+ const template = lootDatabase[lootId] || { name: 'Предмет', value: 10, type: 'material' };
+ return this.createItem(
+ lootId + '_' + Date.now(),
+ template.type,
+ template.name,
+ {
+ value: template.value,
+ damage: template.damage,
+ defense: template.defense,
+ healAmount: template.healAmount,
+ restoreMp: template.restoreMp,
+ spell: template.spell,
+ rarity: template.rarity || 'common'
+ }
+ );
+ },
+
+ // ========================================
+ // ПРОВЕРКА ПРОХОДИМОСТИ
+ // ========================================
+
+ isTilePassable(map, x, y) {
+ if (x < 0 || y < 0 || x >= map[0].length || y >= map.length) {
+ return false;
+ }
+
+ const tile = map[y][x];
+ return tile !== 1 && tile !== 4 && tile !== 6; // вода, стена, лава
+ },
+
+ getAdjacentTiles(x, y, map) {
+ const directions = [
+ { dx: 0, dy: -1 },
+ { dx: 1, dy: 0 },
+ { dx: 0, dy: 1 },
+ { dx: -1, dy: 0 }
+ ];
+
+ return directions
+ .map(d => ({ x: x + d.dx, y: y + d.dy }))
+ .filter(pos => this.isTilePassable(map, pos.x, pos.y));
+ },
+
+ // ========================================
+ // КОЛЛИЗИИ
+ // ========================================
+
+ checkEnemyCollision(character, enemies) {
+ const charX = Math.round(character.x);
+ const charY = Math.round(character.y);
+
+ for (let enemy of enemies) {
+ const enemyX = Math.round(enemy.x);
+ const enemyY = Math.round(enemy.y);
+
+ if (charX === enemyX && charY === enemyY) {
+ return enemy;
+ }
+ }
+
+ return null;
+ },
+
+ checkItemCollision(character, items) {
+ const charX = Math.round(character.x);
+ const charY = Math.round(character.y);
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (!item.collected && item.x === charX && item.y === charY) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ checkNPCCollision(character, npcs) {
+ const charX = Math.round(character.x);
+ const charY = Math.round(character.y);
+
+ for (let npc of npcs) {
+ if (npc.x === charX && npc.y === charY) {
+ return npc;
+ }
+ }
+
+ return null;
+ },
+
+ // ========================================
+ // СОХРАНЕНИЕ И ЗАГРУЗКА
+ // ========================================
+
+ saveGame(character, filename = 'rpg_save') {
+ const saveData = {
+ version: '1.0',
+ timestamp: Date.now(),
+ character: character
+ };
+
+ try {
+ localStorage.setItem(filename, JSON.stringify(saveData));
+ return { success: true, message: 'Игра сохранена!' };
+ } catch (e) {
+ return { success: false, message: 'Ошибка сохранения!' };
+ }
+ },
+
+ loadGame(filename = 'rpg_save') {
+ try {
+ const saveData = localStorage.getItem(filename);
+ if (!saveData) {
+ return { success: false, message: 'Нет сохранённой игры!' };
+ }
+
+ const data = JSON.parse(saveData);
+ return { success: true, character: data.character, message: 'Игра загружена!' };
+ } catch (e) {
+ return { success: false, message: 'Ошибка загрузки!' };
+ }
+ },
+
+ deleteSave(filename = 'rpg_save') {
+ try {
+ localStorage.removeItem(filename);
+ return { success: true, message: 'Сохранение удалено!' };
+ } catch (e) {
+ return { success: false, message: 'Ошибка удаления!' };
+ }
+ },
+
+ hasSave(filename = 'rpg_save') {
+ return localStorage.getItem(filename) !== null;
+ },
+
+ // ========================================
+ // БАЗЫ ДАННЫХ
+ // ========================================
+
+ ItemDatabase: {
+ weapons: [
+ { id: 'sword_1', name: 'Железный меч', damage: 5, value: 50 },
+ { id: 'sword_2', name: 'Стальной меч', damage: 10, value: 150 },
+ { id: 'sword_3', name: 'Меч рыцаря', damage: 18, value: 400 },
+ { id: 'sword_legend', name: 'Экскалибур', damage: 35, value: 2000, rarity: 'legendary' },
+ { id: 'axe_1', name: 'Боевой топор', damage: 8, value: 80 },
+ { id: 'axe_2', name: 'Секира варвара', damage: 15, value: 300 },
+ { id: 'staff_1', name: 'Посох ученика', damage: 3, magic: 8, value: 60 },
+ { id: 'staff_2', name: 'Посох мага', damage: 5, magic: 15, value: 200 },
+ { id: 'dagger_1', name: 'Кинжал', damage: 4, value: 30 },
+ { id: 'bow_1', name: 'Лук', damage: 7, value: 70 }
+ ],
+
+ armor: [
+ { id: 'helmet_1', name: 'Кожаный шлем', defense: 2, value: 40 },
+ { id: 'helmet_2', name: 'Железный шлем', defense: 5, value: 120 },
+ { id: 'chest_1', name: 'Кожаная куртка', defense: 3, value: 50 },
+ { id: 'chest_2', name: 'Кольчуга', defense: 8, value: 200 },
+ { id: 'chest_3', name: 'Латы рыцаря', defense: 15, value: 500 },
+ { id: 'chest_epic', name: 'Доспех дракона', defense: 25, value: 1500, rarity: 'epic' },
+ { id: 'legs_1', name: 'Кожаные поножи', defense: 2, value: 35 },
+ { id: 'boots_1', name: 'Кожаные сапоги', defense: 1, value: 25 },
+ { id: 'boots_2', name: 'Железные сапоги', defense: 3, value: 80 },
+ { id: 'shield_1', name: 'Деревянный щит', defense: 3, value: 40 },
+ { id: 'shield_2', name: 'Железный щит', defense: 6, value: 150 },
+ { id: 'shield_epic', name: 'Щит небожителя', defense: 12, value: 800, rarity: 'epic' }
+ ],
+
+ potions: [
+ { id: 'health_potion_1', name: 'Малое зелье HP', healAmount: 30, value: 20 },
+ { id: 'health_potion_2', name: 'Среднее зелье HP', healAmount: 60, value: 50 },
+ { id: 'health_potion_3', name: 'Большое зелье HP', healAmount: 100, value: 100 },
+ { id: 'mana_potion_1', name: 'Малое зелье MP', restoreMp: 20, value: 25 },
+ { id: 'mana_potion_2', name: 'Среднее зелье MP', restoreMp: 40, value: 60 },
+ { id: 'mana_potion_3', name: 'Большое зелье MP', restoreMp: 70, value: 120 },
+ { id: 'elixir_1', name: 'Эликсир жизни', healAmount: 200, value: 300, rarity: 'rare' }
+ ]
+ }
+};
+
+// Экспорт
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = RPG;
+}