1551 lines
66 KiB
JavaScript
1551 lines
66 KiB
JavaScript
// ========================================
|
||
// 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));
|
||
}
|
||
};
|