From c397a4cdee7cac9cb351deca8596640cf8b473fa Mon Sep 17 00:00:00 2001 From: "maxim.dolgolyov" Date: Mon, 23 Feb 2026 21:20:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?RPG=5FFromFreeModel=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RPG_FromFreeModel/README.md | 138 +++ RPG_FromFreeModel/game.js | 1550 +++++++++++++++++++++++++++++++++ RPG_FromFreeModel/index.html | 295 +++++++ RPG_FromFreeModel/renderer.js | 1090 +++++++++++++++++++++++ RPG_FromFreeModel/rpg.js | 1252 ++++++++++++++++++++++++++ 5 files changed, 4325 insertions(+) create mode 100644 RPG_FromFreeModel/README.md create mode 100644 RPG_FromFreeModel/game.js create mode 100644 RPG_FromFreeModel/index.html create mode 100644 RPG_FromFreeModel/renderer.js create mode 100644 RPG_FromFreeModel/rpg.js 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; +}