Files
Some_Projects/RPG_FromFreeModel/game.js

1551 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========================================
// 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));
}
};