// ======================================== // 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)); } };