Noilil's picture
Upload 3 files
3a7c895 verified
raw
history blame
88.1 kB
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const keys = {};
let lastTime = 0;
const GAME_STATE = {
MENU: "menu",
PLAYING: "playing",
LEVELUP: "levelup",
DIVINE_REWARD: "divine_reward",
OVER: "over",
};
// ============ КОНСТАНТЫ ВРЕМЕНИ ============
const WAVE_DURATION = 60; // 1 минута для обычных волн
const BOSS_DURATION = 180; // 3 минуты для босса
// ============ СИСТЕМА СОХРАНЕНИЯ ============
const SAVE_KEY = 'waveborne_save';
let gameLoaded = false;
function saveGame() {
if (!player || !proficiency) return;
const save = {
classKey: classKey,
player: {
x: player.x,
y: player.y,
hp: player.hp,
maxHp: player.maxHp,
mana: player.mana,
maxMana: player.maxMana,
level: player.level,
xp: player.xp,
xpToNext: player.xpToNext
},
proficiency: {
name: proficiency.name,
currentLevel: proficiency.currentLevel,
exp: proficiency.exp,
expToNextLevel: proficiency.expToNextLevel,
unlockedPerkIds: proficiency.unlockedPerkIds
},
wave: wave,
waveTimer: waveTimer,
perks: perkLevels,
currentPassives: currentPassives,
currentActives: currentActives,
divinePerksUnlocked: divinePerksUnlocked,
modifiers: modifiers,
timestamp: Date.now(),
enemiesKilled: enemiesKilled,
totalDamageDealt: totalDamageDealt
};
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(save));
console.log('Игра сохранена');
} catch (e) {
console.error('Ошибка сохранения:', e);
}
}
function loadGame() {
try {
const saved = localStorage.getItem(SAVE_KEY);
if (!saved) return false;
const save = JSON.parse(saved);
if (!save || !save.classKey) return false;
// Проверяем, не устарело ли сохранение (больше 1 часа)
const now = Date.now();
if (now - save.timestamp > 3600000) {
console.log('Сохранение устарело (больше 1 часа)');
return false;
}
return save;
} catch (e) {
console.error('Ошибка загрузки:', e);
return false;
}
}
function deleteSave() {
localStorage.removeItem(SAVE_KEY);
}
// ============ УНИКАЛЬНАЯ СИСТЕМА КОМБО ДЛЯ ВАРРИОРА ============
class WarriorComboSystem {
constructor() {
this.combo = 0;
this.multiplier = 1.0;
this.timer = 0;
this.maxCombo = 0;
this.comboHits = 0; // Счетчик ударов для комбо
this.lastHitTime = 0; // Время последнего удара
this.COMBO_WINDOW = 0.5; // Окно для комбо - 0.5 секунды
this.COMBO_RESET_TIME = 1.0; // Сброс комбо через 1 секунду без ударов
}
addHit() {
const now = Date.now() / 1000;
// Проверяем, был ли удар в окне комбо
if (now - this.lastHitTime <= this.COMBO_WINDOW) {
this.comboHits++;
// Каждые 3 удара в окне комбо дают +1 к комбо
if (this.comboHits >= 3) {
this.combo++;
this.comboHits = 0;
// Обновляем множитель
this.multiplier = 1.0 + Math.min(1.5, (this.combo * 0.05)); // до 2.5x множителя
if (this.combo > this.maxCombo) {
this.maxCombo = this.combo;
}
// Сбрасываем таймер
this.timer = this.COMBO_RESET_TIME;
console.log(`Комбо: ${this.combo} (x${this.multiplier.toFixed(2)})`);
}
} else {
// Если удар был вне окна комбо, начинаем заново
this.comboHits = 1;
}
this.lastHitTime = now;
this.timer = this.COMBO_RESET_TIME;
return this.multiplier;
}
reset() {
if (this.combo > 5) {
// Бонус опыта за сброшенное комбо
const bonusXP = Math.floor(this.combo * 0.8);
if (player) player.giveXp(bonusXP);
}
this.combo = 0;
this.comboHits = 0;
this.multiplier = 1.0;
this.timer = 0;
}
update(delta) {
if (this.timer > 0) {
this.timer -= delta;
if (this.timer <= 0) {
this.reset();
}
}
}
getMultiplier() {
return this.multiplier;
}
getComboHits() {
return this.comboHits;
}
getComboProgress() {
return this.comboHits / 3; // Прогресс до следующего комбо (0-1)
}
}
const comboSystem = new WarriorComboSystem();
// ============ СИСТЕМА УМНОГО НАВЕДЕНИЯ ДЛЯ МАГА ============
class SmartTargeting {
constructor() {
this.activeProjectiles = new Set();
this.targetedEnemies = new Map(); // enemy -> projectile count
}
registerProjectile(projectileId) {
this.activeProjectiles.add(projectileId);
}
unregisterProjectile(projectileId) {
this.activeProjectiles.delete(projectileId);
}
getBestTarget(enemies, playerX, playerY, range, excludeTarget = null) {
// Фильтруем живых врагов в радиусе
const availableEnemies = enemies.filter(enemy =>
!enemy.isDead() &&
Math.hypot(enemy.x - playerX, enemy.y - playerY) <= range &&
enemy !== excludeTarget
);
if (availableEnemies.length === 0) return null;
// Находим врага с наименьшим количеством снарядов, летящих в него
let bestTarget = null;
let minProjectiles = Infinity;
for (const enemy of availableEnemies) {
const projectileCount = this.targetedEnemies.get(enemy) || 0;
// Предпочитаем цели, в которые летит меньше снарядов
if (projectileCount < minProjectiles) {
minProjectiles = projectileCount;
bestTarget = enemy;
} else if (projectileCount === minProjectiles) {
// При равенстве выбираем ближайшего
const currentDist = bestTarget ? Math.hypot(bestTarget.x - playerX, bestTarget.y - playerY) : Infinity;
const newDist = Math.hypot(enemy.x - playerX, enemy.y - playerY);
if (newDist < currentDist) {
bestTarget = enemy;
}
}
}
return bestTarget;
}
assignTarget(enemy) {
const count = this.targetedEnemies.get(enemy) || 0;
this.targetedEnemies.set(enemy, count + 1);
// Возвращаем ID для отслеживания
const projectileId = Date.now() + Math.random();
return projectileId;
}
releaseTarget(enemy, projectileId) {
const count = this.targetedEnemies.get(enemy);
if (count !== undefined) {
if (count <= 1) {
this.targetedEnemies.delete(enemy);
} else {
this.targetedEnemies.set(enemy, count - 1);
}
}
this.activeProjectiles.delete(projectileId);
}
clear() {
this.activeProjectiles.clear();
this.targetedEnemies.clear();
}
}
const smartTargeting = new SmartTargeting();
// ============ ВСПЛЫВАЮЩИЙ ТЕКСТ ============
let damageNumbers = [];
let healNumbers = [];
let comboNumbers = [];
class FloatingText {
constructor(x, y, text, color, isCritical = false, fontSize = 16) {
this.x = x;
this.y = y;
this.text = text;
this.color = color;
this.life = 1.0;
this.velocityY = -40;
this.gravity = 10;
this.fontSize = isCritical ? `bold ${fontSize}px` : `bold ${fontSize}px`;
this.isCritical = isCritical;
}
update(delta) {
this.y += this.velocityY * delta;
this.velocityY += this.gravity * delta;
this.life -= delta;
}
draw(ctx) {
const alpha = Math.min(1, this.life * 2);
ctx.font = this.fontSize + ' Arial';
ctx.fillStyle = this.color.replace(')', `, ${alpha})`).replace('rgb', 'rgba');
ctx.textAlign = 'center';
if (this.isCritical) {
ctx.shadowColor = this.color;
ctx.shadowBlur = 10;
ctx.fillText(this.text, this.x, this.y);
ctx.shadowBlur = 0;
} else {
ctx.fillText(this.text, this.x, this.y);
}
}
isAlive() {
return this.life > 0;
}
}
// ============ ДОСТИЖЕНИЯ ============
const ACHIEVEMENTS = {
FIRST_BLOOD: {
id: 'first_blood',
name: 'Первая кровь',
desc: 'Убить первого врага',
icon: '🩸',
unlocked: false,
check: () => enemiesKilled >= 1
},
COMBO_MASTER: {
id: 'combo_master',
name: 'Мастер комбо',
desc: 'Набрать комбо из 10 ударов',
icon: '⚡',
unlocked: false,
check: () => classKey === 'warrior' && comboSystem.maxCombo >= 10
},
WAVE_10: {
id: 'wave_10',
name: 'Выживший',
desc: 'Достичь 10 волны',
icon: '🏆',
unlocked: false,
check: () => wave >= 10
},
BOSS_SLAYER: {
id: 'boss_slayer',
name: 'Убийца боссов',
desc: 'Победить первого босса',
icon: '👑',
unlocked: false,
check: () => bossKilled
},
PERK_COLLECTOR: {
id: 'perk_collector',
name: 'Коллекционер',
desc: 'Собрать 10 различных навыков',
icon: '📚',
unlocked: false,
check: () => Object.keys(perkLevels).length >= 10
}
};
let unlockedAchievements = [];
let enemiesKilled = 0;
let bossKilled = false;
let totalDamageDealt = 0;
function checkAchievements() {
Object.values(ACHIEVEMENTS).forEach(achievement => {
if (!achievement.unlocked && achievement.check()) {
achievement.unlocked = true;
unlockedAchievements.push(achievement);
showAchievementPopup(achievement);
}
});
}
function showAchievementPopup(achievement) {
const popup = document.createElement('div');
popup.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 10px;
border-left: 5px solid gold;
z-index: 1000;
animation: slideIn 0.5s ease-out;
max-width: 300px;
`;
popup.innerHTML = `
<div style="font-size: 24px; margin-bottom: 5px;">${achievement.icon} ${achievement.name}</div>
<div style="color: #ccc; font-size: 14px;">${achievement.desc}</div>
`;
document.body.appendChild(popup);
setTimeout(() => {
popup.style.animation = 'slideOut 0.5s ease-in';
setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 500);
}, 3000);
}
// Добавляем стили для анимации
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
// ============ БАЗОВЫЕ КОНСТАНТЫ ============
// Базовые навыки для каждого класса (открываются на уровне мастерства 1)
const BASE_PERKS = {
warrior: [
{ id: "base_toughness", rarity: "common", max: 3, name: "Выносливость", desc: "+10 HP", effect: (m) => { m.hpBonus += 10; } },
{ id: "base_strength", rarity: "common", max: 3, name: "Сила", desc: "+5% урон", effect: (m) => { m.damageMult *= 1.05; } },
{ id: "base_speed", rarity: "common", max: 3, name: "Проворство", desc: "+4% скорость", effect: (m) => { m.speedMult *= 1.04; } },
{ id: "base_defense", rarity: "common", max: 3, name: "Защита", desc: "-5% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.95; } },
{ id: "base_haste", rarity: "common", max: 3, name: "Спешка", desc: "-4% кд", effect: (m) => { m.cooldownMult *= 0.96; } },
],
archer: [
{ id: "base_agility", rarity: "common", max: 3, name: "Ловкость", desc: "+6% скорость", effect: (m) => { m.speedMult *= 1.06; } },
{ id: "base_aim", rarity: "common", max: 3, name: "Прицел", desc: "+4% урон", effect: (m) => { m.damageMult *= 1.04; } },
{ id: "base_quickness", rarity: "common", max: 3, name: "Быстрота", desc: "-5% кд", effect: (m) => { m.cooldownMult *= 0.95; } },
{ id: "base_range", rarity: "common", max: 3, name: "Дальность", desc: "+15 радиус", effect: (m) => { m.rangeBonus += 15; } },
{ id: "base_evasion", rarity: "common", max: 3, name: "Уклонение", desc: "-4% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.96; } },
],
mage: [
{ id: "base_wisdom", rarity: "common", max: 3, name: "Мудрость", desc: "+15 макс маны", effect: (m) => { m.manaBonus += 15; } },
{ id: "base_power", rarity: "common", max: 3, name: "Мощь", desc: "+5% урон", effect: (m) => { m.damageMult *= 1.05; } },
{ id: "base_flow", rarity: "common", max: 3, name: "Поток", desc: "+1 реген маны/с", effect: (m) => { m.manaRegen += 1; } },
{ id: "base_efficiency", rarity: "common", max: 3, name: "Эффективность", desc: "-5% манакост", effect: (m) => { m.manaCostMult *= 0.95; } },
{ id: "base_focus", rarity: "common", max: 3, name: "Фокус", desc: "-4% кд", effect: (m) => { m.cooldownMult *= 0.96; } },
],
acolyte: [
{ id: "base_vitality", rarity: "common", max: 3, name: "Жизненность", desc: "+15 HP", effect: (m) => { m.hpBonus += 15; } },
{ id: "base_healing", rarity: "common", max: 3, name: "Исцеление", desc: "+1 исцеление/пульс", effect: (m) => { m.auraHeal += 1; } },
{ id: "base_protection", rarity: "common", max: 3, name: "Защита", desc: "-5% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.95; } },
{ id: "base_radiance", rarity: "common", max: 3, name: "Сияние", desc: "+4% урон", effect: (m) => { m.damageMult *= 1.04; } },
{ id: "base_presence", rarity: "common", max: 3, name: "Присутствие", desc: "+8 радиус", effect: (m) => { m.rangeBonus += 8; } },
],
};
// Per-class perk kits (10 passives, 4 actives) with levels
const CLASS_KITS = {
warrior: {
passives: [
{ id: "iron_skin", rarity: "common", max: 3, name: "Железная кожа", desc: "+20 HP, +10% защита", effect: (m) => { m.hpBonus += 20; m.damageTakenMult *= 0.9; } },
{ id: "battle_fervor", rarity: "uncommon", max: 3, name: "Боевой пыл", desc: "+12% урон", effect: (m) => { m.damageMult *= 1.12; } },
{ id: "blade_mastery", rarity: "rare", max: 3, name: "Мастер клинка", desc: "-10% кд удара", effect: (m) => { m.cooldownMult *= 0.9; } },
{ id: "riposte", rarity: "rare", max: 3, name: "Рипост", desc: "5% шанс доп. удара", effect: (m) => { m.doubleHit += 0.05; } },
{ id: "heavy_guard", rarity: "uncommon", max: 3, name: "Тяжёлый гард", desc: "+15% защита, -5% скорость", effect: (m) => { m.damageTakenMult *= 0.85; m.speedMult *= 0.95; } },
{ id: "bloodlust", rarity: "uncommon", max: 3, name: "Кровожадность", desc: "Лечение 2 HP за убийство", effect: (m) => { m.killHeal += 2; } },
{ id: "sweeping_edge", rarity: "rare", max: 3, name: "Режущий вихрь", desc: "+12 радиус удара", effect: (m) => { m.rangeBonus += 12; } },
{ id: "adamant", rarity: "epic", max: 1, name: "Непреклонный", desc: "Иммунитет к отбрасыванию (флаг)", effect: () => {} },
{ id: "momentum", rarity: "uncommon", max: 3, name: "Импульс", desc: "+8% скорость", effect: (m) => { m.speedMult *= 1.08; } },
{ id: "war_banner", rarity: "rare", max: 3, name: "Знамя войны", desc: "Доп. 6% урона и 6% кд", effect: (m) => { m.damageMult *= 1.06; m.cooldownMult *= 0.94; } },
],
actives: [
{ id: "blade_dash", rarity: "uncommon", max: 3, name: "Рывок клинка", desc: "Скорость +15%", effect: (m) => { m.speedMult *= 1.15; } },
{ id: "shockwave", rarity: "rare", max: 3, name: "Ударная волна", desc: "Урон +15% в ближнем бою", effect: (m) => { m.damageMult *= 1.15; } },
{ id: "guard_stance", rarity: "uncommon", max: 3, name: "Стойка защиты", desc: "-12% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.88; } },
{ id: "whirlwind", rarity: "epic", max: 3, name: "Вихрь", desc: "Шанс доп. удара +8%", effect: (m) => { m.doubleHit += 0.08; } },
],
},
archer: {
passives: [
{ id: "quickdraw", rarity: "uncommon", max: 3, name: "Быстрый выстрел", desc: "-12% кд", effect: (m) => { m.cooldownMult *= 0.88; } },
{ id: "light_steps", rarity: "uncommon", max: 3, name: "Лёгкие шаги", desc: "+12% скорость", effect: (m) => { m.speedMult *= 1.12; } },
{ id: "piercing_arrows", rarity: "rare", max: 3, name: "Пробитие", desc: "Стрелы проходят врагов (флаг)", effect: (m) => { m.pierce += 1; } },
{ id: "hunter_focus", rarity: "rare", max: 3, name: "Фокус охотника", desc: "+14% урон", effect: (m) => { m.damageMult *= 1.14; } },
{ id: "ricochet", rarity: "epic", max: 3, name: "Рикошет", desc: "5% шанс рикошета", effect: (m) => { m.ricochet += 0.05; } },
{ id: "wind_runner", rarity: "uncommon", max: 3, name: "Бег по ветру", desc: "Скорость +8% и +5% урон", effect: (m) => { m.speedMult *= 1.08; m.damageMult *= 1.05; } },
{ id: "bleeding_edge", rarity: "rare", max: 3, name: "Кровоточащий", desc: "Накладывает 4 доп. урона", effect: (m) => { m.flatDamage += 4; } },
{ id: "camo", rarity: "uncommon", max: 3, name: "Маскировка", desc: "-8% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.92; } },
{ id: "falcon_eye", rarity: "rare", max: 3, name: "Глаз ястреба", desc: "+30 радиус", effect: (m) => { m.rangeBonus += 30; } },
{ id: "arrow_surge", rarity: "rare", max: 3, name: "Сурж", desc: "Кд -6%, урон +6%", effect: (m) => { m.cooldownMult *= 0.94; m.damageMult *= 1.06; } },
],
actives: [
{ id: "power_shot", rarity: "rare", max: 3, name: "Силовой выстрел", desc: "+18% урон", effect: (m) => { m.damageMult *= 1.18; } },
{ id: "volley", rarity: "epic", max: 3, name: "Залп", desc: "Доп. снаряд (пирсинг)", effect: (m) => { m.extraProjectiles += 1; } },
{ id: "evasive_roll", rarity: "uncommon", max: 3, name: "Кувырок", desc: "+10% скорость", effect: (m) => { m.speedMult *= 1.1; } },
{ id: "marked_target", rarity: "rare", max: 3, name: "Метка", desc: "+12% урон по ближ. цели", effect: (m) => { m.damageMult *= 1.12; } },
],
},
mage: {
passives: [
{ id: "ember_focus", rarity: "rare", max: 3, name: "Жар", desc: "+16% урон огнём", effect: (m) => { m.damageMult *= 1.16; m.burn += 4; } },
{ id: "mana_font", rarity: "uncommon", max: 3, name: "Источник маны", desc: "+30 макс маны, +1.5/с реген", effect: (m) => { m.manaBonus += 30; m.manaRegen += 1.5; } },
{ id: "arcane_precision", rarity: "uncommon", max: 3, name: "Точность", desc: "-10% кд", effect: (m) => { m.cooldownMult *= 0.9; } },
{ id: "frost_weave", rarity: "rare", max: 3, name: "Мороз", desc: "Замедляет врагов при попадании", effect: (m) => { m.slowOnHit = true; } },
{ id: "overload", rarity: "epic", max: 3, name: "Перегруз", desc: "+20% урон, +10% манакост", effect: (m) => { m.damageMult *= 1.2; m.manaCostMult *= 1.1; } },
{ id: "runic_shield", rarity: "uncommon", max: 3, name: "Рун. щит", desc: "-10% вход. урон", effect: (m) => { m.damageTakenMult *= 0.9; } },
{ id: "elemental_sync", rarity: "rare", max: 3, name: "Синхрония", desc: "-12% манакост", effect: (m) => { m.manaCostMult *= 0.88; } },
{ id: "astral_echo", rarity: "epic", max: 3, name: "Астральный эхо", desc: "5% шанс двойного шара", effect: (m) => { m.doubleHit += 0.05; } },
{ id: "channeling", rarity: "uncommon", max: 3, name: "Канал", desc: "+2.5 реген маны", effect: (m) => { m.manaRegen += 2.5; } },
{ id: "mystic_amp", rarity: "epic", max: 3, name: "Усиление", desc: "+18% урон", effect: (m) => { m.damageMult *= 1.18; } },
],
actives: [
{ id: "arcane_comet", rarity: "rare", max: 3, name: "Комета", desc: "Больший AoE", effect: (m) => { m.splashBonus += 18; } },
{ id: "flame_burst", rarity: "uncommon", max: 3, name: "Вспышка", desc: "Манакост -10%", effect: (m) => { m.manaCostMult *= 0.9; } },
{ id: "frost_nova", rarity: "rare", max: 3, name: "Фрост нова", desc: "Замедление сильнее", effect: (m) => { m.slowStrength += 0.2; } },
{ id: "mana_barrier", rarity: "rare", max: 3, name: "Барьер", desc: "+12% защита", effect: (m) => { m.damageTakenMult *= 0.88; } },
],
},
acolyte: {
passives: [
{ id: "steadfast", rarity: "common", max: 3, name: "Стойкость", desc: "+25 HP", effect: (m) => { m.hpBonus += 25; } },
{ id: "radiant_armor", rarity: "uncommon", max: 3, name: "Сияющая броня", desc: "-12% вход. урон", effect: (m) => { m.damageTakenMult *= 0.88; } },
{ id: "soothing_light", rarity: "uncommon", max: 3, name: "Утешение", desc: "+2 исцеление/пульс", effect: (m) => { m.auraHeal += 2; } },
{ id: "devotion", rarity: "rare", max: 3, name: "Преданность", desc: "+10% урон ауры", effect: (m) => { m.damageMult *= 1.1; } },
{ id: "sanctuary", rarity: "rare", max: 3, name: "Святыня", desc: "+12 радиус ауры", effect: (m) => { m.rangeBonus += 12; } },
{ id: "spirit_chain", rarity: "uncommon", max: 3, name: "Цепь духа", desc: "Лечение за убийство 3 HP", effect: (m) => { m.killHeal += 3; } },
{ id: "warding", rarity: "uncommon", max: 3, name: "Оберег", desc: "-8% кд", effect: (m) => { m.cooldownMult *= 0.92; } },
{ id: "resolve", rarity: "uncommon", max: 3, name: "Решимость", desc: "+8% скорость", effect: (m) => { m.speedMult *= 1.08; } },
{ id: "holy_edge", rarity: "rare", max: 3, name: "Святая грань", desc: "+10% урон", effect: (m) => { m.damageMult *= 1.1; } },
{ id: "blessing", rarity: "epic", max: 3, name: "Благословение", desc: "+3 исцеление при ударе", effect: (m) => { m.onHitHeal += 3; } },
],
actives: [
{ id: "healing_wave", rarity: "rare", max: 3, name: "Волна исцел.", desc: "+3 исцеление/пульс", effect: (m) => { m.auraHeal += 3; } },
{ id: "smite", rarity: "rare", max: 3, name: "Кара", desc: "+15% урон", effect: (m) => { m.damageMult *= 1.15; } },
{ id: "consecrate", rarity: "rare", max: 3, name: "Освящение", desc: "+14 радиус", effect: (m) => { m.rangeBonus += 14; } },
{ id: "guardian_aura", rarity: "epic", max: 3, name: "Аура стража", desc: "-10% вход. урон", effect: (m) => { m.damageTakenMult *= 0.9; } },
],
},
};
function getPerkDef(id) {
if (!classKey) return null;
const kit = CLASS_KITS[classKey];
const basePerks = BASE_PERKS[classKey] || [];
if (!kit) return null;
return [...basePerks, ...kit.passives, ...kit.actives].find((p) => p.id === id) || null;
}
function rarityLabel(r) {
const map = {
common: "Обычный",
uncommon: "Необычный",
rare: "Редкий",
unique: "Уникальный",
epic: "Мифический",
legendary: "Легендарный",
divine: "Божественный",
};
return map[r] || "Обычный";
}
const CLASS_DEFS = {
warrior: {
name: "Warrior",
color: "#6cf1ff",
hp: 140,
mana: 0,
manaRegen: 0,
speed: 210,
weapon: "sword",
proficiency: "Владение Мечом",
},
archer: {
name: "Archer",
color: "#f4d35e",
hp: 110,
mana: 20,
manaRegen: 1,
speed: 240,
weapon: "bow",
proficiency: "Владение Луком",
},
mage: {
name: "Mage",
color: "#c9b4ff",
hp: 100,
mana: 120,
manaRegen: 6,
speed: 225,
weapon: "orb",
proficiency: "Стихийная Магия",
},
acolyte: {
name: "Acolyte",
color: "#9be7ff",
hp: 150,
mana: 60,
manaRegen: 3,
speed: 205,
weapon: "aura",
proficiency: "Сакральная Магия",
},
};
function baseModifiers() {
return {
hpBonus: 0,
damageMult: 1,
cooldownMult: 1,
rangeBonus: 0,
speedMult: 1,
manaBonus: 0,
manaRegen: 0,
manaCostMult: 1,
damageTakenMult: 1,
doubleHit: 0,
killHeal: 0,
flatDamage: 0,
pierce: 0,
ricochet: 0,
extraProjectiles: 0,
splashBonus: 0,
burn: 0,
slowOnHit: false,
slowStrength: 0.25,
auraHeal: 0,
onHitHeal: 0,
// Божественные навыки
divineStrike: false,
divineStrikeCooldown: 0,
divineStrikeReady: false,
celestialShield: false,
celestialShieldCooldown: 0,
celestialShieldActive: false,
timeDilation: false,
timeDilationCooldown: 0,
timeDilationActive: false,
phoenixRebirth: false,
phoenixRebirthCooldown: 0,
voidStep: false,
voidStepCooldown: 0,
divineFury: false,
divineFuryCooldown: 0,
divineFuryActive: false,
eternalFlame: false,
eternalFlameCooldown: 0,
starfall: false,
starfallCooldown: 0,
divineGrace: false,
divineGraceCooldown: 0,
cosmicAwareness: false,
cosmicAwarenessCooldown: 0,
cosmicAwarenessActive: false,
cosmicAwarenessTimer: 0,
timeDilationTimer: 0,
};
}
class Player {
constructor(x, y, color, hp, speed, mana, manaRegen) {
this.x = x;
this.y = y;
this.size = 32;
this.color = color;
this.maxHp = hp;
this.hp = hp;
this.baseSpeed = speed;
this.mana = mana;
this.maxMana = mana;
this.manaRegen = manaRegen;
this.level = 1;
this.xp = 0;
this.xpToNext = 40;
this.hurtTimer = 0;
}
update(delta, mods) {
let dx = 0;
let dy = 0;
if (keys["ArrowUp"] || keys["KeyW"]) dy -= 1;
if (keys["ArrowDown"] || keys["KeyS"]) dy += 1;
if (keys["ArrowLeft"] || keys["KeyA"]) dx -= 1;
if (keys["ArrowRight"] || keys["KeyD"]) dx += 1;
if (dx !== 0 || dy !== 0) {
const len = Math.hypot(dx, dy);
dx /= len;
dy /= len;
this.x += dx * this.baseSpeed * mods.speedMult * delta;
this.y += dy * this.baseSpeed * mods.speedMult * delta;
}
this.x = Math.max(this.size / 2, Math.min(canvas.width - this.size / 2, this.x));
this.y = Math.max(this.size / 2, Math.min(canvas.height - this.size / 2, this.y));
if (this.hurtTimer > 0) this.hurtTimer -= delta;
// mana regen
this.mana = Math.min(this.maxMana + mods.manaBonus, this.mana + (this.manaRegen + mods.manaRegen) * delta);
// Божественные навыки - обновление кд
if (mods.divineStrike) {
mods.divineStrikeCooldown = Math.max(0, mods.divineStrikeCooldown - delta);
mods.divineStrikeReady = mods.divineStrikeCooldown <= 0;
}
if (mods.celestialShield) {
mods.celestialShieldCooldown = Math.max(0, mods.celestialShieldCooldown - delta);
}
if (mods.timeDilation) {
mods.timeDilationCooldown = Math.max(0, mods.timeDilationCooldown - delta);
if (mods.timeDilationActive) {
mods.timeDilationActive = false;
mods.cooldownMult /= 0.5; // Возврат к нормальному кд
}
}
if (mods.phoenixRebirth) {
mods.phoenixRebirthCooldown = Math.max(0, mods.phoenixRebirthCooldown - delta);
}
if (mods.voidStep) {
mods.voidStepCooldown = Math.max(0, mods.voidStepCooldown - delta);
}
if (mods.divineFury) {
mods.divineFuryCooldown = Math.max(0, mods.divineFuryCooldown - delta);
if (mods.divineFuryActive && this.hp >= this.maxHp * 0.3) {
mods.divineFuryActive = false;
mods.damageMult /= 1.4;
mods.speedMult /= 1.25;
}
}
if (mods.eternalFlame) {
mods.eternalFlameCooldown = Math.max(0, mods.eternalFlameCooldown - delta);
}
if (mods.starfall) {
mods.starfallCooldown = Math.max(0, mods.starfallCooldown - delta);
}
if (mods.divineGrace) {
mods.divineGraceCooldown = Math.max(0, mods.divineGraceCooldown - delta);
}
if (mods.cosmicAwareness) {
mods.cosmicAwarenessCooldown = Math.max(0, mods.cosmicAwarenessCooldown - delta);
if (mods.cosmicAwarenessActive) {
mods.cosmicAwarenessActive = false;
mods.damageMult /= 1.2;
}
}
}
giveXp(amount) {
this.xp += amount;
if (this.xp >= this.xpToNext) {
this.xp -= this.xpToNext;
this.level += 1;
this.xpToNext = Math.round(this.xpToNext * 1.25 + 15);
openLevelUp();
}
}
takeDamage(amount, mods) {
// Небесный щит
if (mods.celestialShield && mods.celestialShieldCooldown <= 0 && !mods.celestialShieldActive) {
mods.celestialShieldActive = true;
mods.celestialShieldCooldown = mods.celestialShieldCooldown || 12;
return; // Блокирует урон
}
if (mods.celestialShieldActive) {
mods.celestialShieldActive = false;
return;
}
// Возрождение феникса
if (this.hp - amount * mods.damageTakenMult <= 0 && mods.phoenixRebirth && mods.phoenixRebirthCooldown <= 0) {
this.hp = Math.max(1, this.maxHp * 0.5);
mods.phoenixRebirthCooldown = mods.phoenixRebirthCooldown || 60;
return;
}
this.hp -= amount * mods.damageTakenMult;
this.hurtTimer = 0.25;
// Божественная ярость
if (this.hp < this.maxHp * 0.3 && mods.divineFury && mods.divineFuryCooldown <= 0 && !mods.divineFuryActive) {
mods.divineFuryActive = true;
mods.divineFuryCooldown = mods.divineFuryCooldown || 20;
mods.damageMult *= 1.4;
mods.speedMult *= 1.25;
}
}
draw(context) {
context.fillStyle = this.hurtTimer > 0 ? "#ff7b7b" : this.color;
context.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
}
}
class Proficiency {
constructor(name) {
this.name = name;
this.currentLevel = 0;
this.exp = 0;
this.expToNextLevel = 25;
this.unlockedPerkIds = []; // ID навыков, открытых через мастерство
}
getModifiers() {
// Базовые модификаторы от уровня мастерства (минимальные)
const dmg = 1 + this.currentLevel * 0.03;
const cd = Math.max(0.85, 1 - this.currentLevel * 0.01);
const range = this.currentLevel * 1;
return { damageMult: dmg, cooldownMult: cd, rangeBonus: range };
}
addExp(amount) {
this.exp += amount;
while (this.exp >= this.expToNextLevel) {
this.exp -= this.expToNextLevel;
this.currentLevel += 1;
console.log(`[${this.name}] уровень повышен до ${this.currentLevel}!`);
this.expToNextLevel = Math.round(this.expToNextLevel * 1.35 + 8);
this.unlockRandomPerk();
refreshSkillIcons();
}
}
unlockRandomPerk() {
if (!classKey) return;
// На 1 уровне мастерства открываем все базовые навыки
if (this.currentLevel === 1) {
const basePerks = BASE_PERKS[classKey] || [];
basePerks.forEach(perk => {
if (!this.unlockedPerkIds.includes(perk.id)) {
this.unlockedPerkIds.push(perk.id);
console.log(`[${this.name}] открыт базовый навык: ${perk.name}`);
}
});
} else {
// На остальных уровнях - случайный навык из основного пула класса
const kit = CLASS_KITS[classKey];
const allPerks = [...kit.passives, ...kit.actives];
const available = allPerks.filter(p => !this.unlockedPerkIds.includes(p.id));
if (available.length > 0) {
const randomPerk = available[Math.floor(Math.random() * available.length)];
this.unlockedPerkIds.push(randomPerk.id);
console.log(`[${this.name}] уровень ${this.currentLevel}: открыт навык ${randomPerk.name} (${randomPerk.rarity || 'common'})`);
} else {
console.log(`[${this.name}] все навыки уже открыты!`);
}
}
}
}
class Weapon {
constructor(player, proficiency, mods) {
this.player = player;
this.proficiency = proficiency;
this.mods = mods;
}
update() {}
draw() {}
syncMods(mods) {
this.mods = mods;
}
}
class SwordWeapon extends Weapon {
constructor(player, proficiency, mods) {
super(player, proficiency, mods);
this.baseRange = 52;
this.baseDamage = 16;
this.baseCooldown = 0.6;
this.timer = 0;
this.swingMarker = 0;
this.comboVisualTimer = 0;
}
update(enemies, delta) {
this.timer += delta;
const prof = this.proficiency.getModifiers();
const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
const range = this.baseRange + prof.rangeBonus + this.mods.rangeBonus;
// Визуальный таймер комбо
if (this.comboVisualTimer > 0) {
this.comboVisualTimer -= delta;
}
if (this.timer < cooldown) return;
const target = findClosestEnemy(this.player, enemies, range);
if (!target) return;
let damage = (this.baseDamage + this.mods.flatDamage) * prof.damageMult * this.mods.damageMult;
// Применяем множитель комбо (только для воина)
if (classKey === 'warrior') {
damage *= comboSystem.getMultiplier();
const multiplier = comboSystem.addHit();
// Визуальный эффект при увеличении комбо
if (comboSystem.getComboHits() === 3) {
this.comboVisualTimer = 0.3;
comboNumbers.push(new FloatingText(
this.player.x,
this.player.y - 40,
`COMBO x${comboSystem.combo}!`,
'#ffd166',
true,
20
));
}
}
// Божественный удар
if (this.mods.divineStrike && this.mods.divineStrikeReady) {
damage *= 3;
this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
this.mods.divineStrikeReady = false;
}
target.takeDamage(damage);
// Добавляем всплывающий текст урона
const isCrit = classKey === 'warrior' && comboSystem.getMultiplier() > 1.2;
damageNumbers.push(new FloatingText(
target.x,
target.y,
Math.round(damage),
'#ff7b7b',
isCrit
));
if (Math.random() < this.mods.doubleHit) {
const extraDamage = damage * 0.6;
target.takeDamage(extraDamage);
damageNumbers.push(new FloatingText(
target.x,
target.y - 15,
Math.round(extraDamage),
'#ffaa44',
false
));
}
this.timer = 0;
this.swingMarker = 0.2;
this.proficiency.addExp(6);
if (this.mods.onHitHeal) {
this.player.hp = Math.min(this.player.maxHp + this.mods.hpBonus, this.player.hp + this.mods.onHitHeal);
healNumbers.push(new FloatingText(
this.player.x,
this.player.y - 20,
'+' + this.mods.onHitHeal,
'#54e894',
false
));
}
}
draw(context) {
if (this.swingMarker > 0) {
const prof = this.proficiency.getModifiers();
const range = this.baseRange + prof.rangeBonus + this.mods.rangeBonus;
context.strokeStyle = "rgba(255,255,255,0.35)";
context.beginPath();
context.arc(this.player.x, this.player.y, range, 0, Math.PI * 2);
context.stroke();
this.swingMarker -= 1 / 60;
}
// Визуализация комбо для воина
if (classKey === 'warrior' && this.comboVisualTimer > 0) {
const alpha = this.comboVisualTimer / 0.3;
const radius = 60 + comboSystem.combo * 5;
context.strokeStyle = `rgba(255, 209, 102, ${alpha})`;
context.lineWidth = 3;
context.beginPath();
context.arc(this.player.x, this.player.y, radius, 0, Math.PI * 2);
context.stroke();
// Прогресс комбо
const progress = comboSystem.getComboProgress();
if (progress > 0) {
context.fillStyle = `rgba(255, 209, 102, ${alpha * 0.3})`;
context.beginPath();
context.moveTo(this.player.x, this.player.y);
context.arc(this.player.x, this.player.y, 40, -Math.PI/2, -Math.PI/2 + progress * Math.PI * 2);
context.closePath();
context.fill();
}
}
}
}
class BowWeapon extends Weapon {
constructor(player, proficiency, mods) {
super(player, proficiency, mods);
this.baseRange = 420;
this.baseDamage = 10;
this.baseCooldown = 0.55;
this.projectiles = [];
this.timer = 0;
}
update(enemies, delta) {
this.timer += delta;
const prof = this.proficiency.getModifiers();
const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
if (this.timer >= cooldown) {
const target = findClosestEnemy(this.player, enemies, this.baseRange + this.mods.rangeBonus);
if (target) {
const angle = Math.atan2(target.y - this.player.y, target.x - this.player.x);
const speed = 360;
const count = 1 + this.mods.extraProjectiles;
for (let i = 0; i < count; i++) {
const spread = (i - (count - 1) / 2) * 0.12;
let damage = (this.baseDamage + this.mods.flatDamage) * prof.damageMult * this.mods.damageMult;
if (this.mods.divineStrike && this.mods.divineStrikeReady) {
damage *= 3;
this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
this.mods.divineStrikeReady = false;
}
this.projectiles.push({
x: this.player.x,
y: this.player.y,
vx: Math.cos(angle + spread) * speed,
vy: Math.sin(angle + spread) * speed,
life: 1.2,
radius: 5,
damage: damage,
pierce: this.mods.pierce,
});
}
this.timer = 0;
this.proficiency.addExp(4);
}
}
for (let i = this.projectiles.length - 1; i >= 0; i--) {
const p = this.projectiles[i];
p.x += p.vx * delta;
p.y += p.vy * delta;
p.life -= delta;
for (const enemy of enemies) {
if (Math.hypot(enemy.x - p.x, enemy.y - p.y) < enemy.radius + p.radius) {
enemy.takeDamage(p.damage);
damageNumbers.push(new FloatingText(
enemy.x,
enemy.y - 10,
Math.round(p.damage),
'#f4d35e',
false
));
if (this.mods.ricochet > 0 && Math.random() < this.mods.ricochet) {
const target = findClosestEnemy(enemy, enemies, 200);
if (target) {
p.vx = (target.x - enemy.x) * 3;
p.vy = (target.y - enemy.y) * 3;
p.life = 0.4;
continue;
}
}
if (p.pierce > 0) {
p.pierce -= 1;
} else {
this.projectiles.splice(i, 1);
}
break;
}
}
if (p.life <= 0) this.projectiles.splice(i, 1);
}
}
draw(context) {
context.fillStyle = "#f4d35e";
this.projectiles.forEach((p) => {
context.beginPath();
context.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
context.fill();
});
}
}
class OrbWeapon extends Weapon {
constructor(player, proficiency, mods) {
super(player, proficiency, mods);
this.baseCooldown = 0.8;
this.baseDamage = 18;
this.timer = 0;
this.projectiles = [];
this.manaCost = 10;
this.activeProjectiles = new Map(); // projectileId -> target
}
update(enemies, delta) {
this.timer += delta;
const prof = this.proficiency.getModifiers();
const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
const cost = this.manaCost * this.mods.manaCostMult;
if (this.timer >= cooldown) {
// Используем умное наведение для выбора цели
let target = smartTargeting.getBestTarget(enemies, this.player.x, this.player.y, 520 + this.mods.rangeBonus);
if (target && this.player.mana >= cost) {
this.player.mana -= cost;
const angle = Math.atan2(target.y - this.player.y, target.x - this.player.x);
const speed = 260;
let damage = this.baseDamage * prof.damageMult * this.mods.damageMult;
if (this.mods.divineStrike && this.mods.divineStrikeReady) {
damage *= 3;
this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
this.mods.divineStrikeReady = false;
}
const projectileId = smartTargeting.assignTarget(target);
this.projectiles.push({
id: projectileId,
x: this.player.x,
y: this.player.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.6,
radius: 10,
damage: damage,
splash: 32 + prof.rangeBonus + this.mods.rangeBonus + this.mods.splashBonus,
target: target,
originalTarget: target
});
this.timer = 0;
this.proficiency.addExp(5);
}
}
for (let i = this.projectiles.length - 1; i >= 0; i--) {
const p = this.projectiles[i];
p.x += p.vx * delta;
p.y += p.vy * delta;
p.life -= delta;
// Проверяем, жив ли исходный целевой враг
if (p.target && p.target.isDead()) {
// Ищем новую цель, исключая текущую
p.target = smartTargeting.getBestTarget(enemies, p.x, p.y, 200, p.originalTarget);
if (p.target) {
// Пересчитываем направление к новой цели
const angle = Math.atan2(p.target.y - p.y, p.target.x - p.x);
const speed = Math.hypot(p.vx, p.vy);
p.vx = Math.cos(angle) * speed;
p.vy = Math.sin(angle) * speed;
}
}
let hit = false;
for (const enemy of enemies) {
if (Math.hypot(enemy.x - p.x, enemy.y - p.y) < enemy.radius + p.radius) {
hit = true;
for (const aoe of enemies) {
if (Math.hypot(aoe.x - p.x, aoe.y - p.y) <= p.splash) {
let dmg = p.damage;
if (this.mods.burn) dmg += this.mods.burn;
aoe.takeDamage(dmg);
damageNumbers.push(new FloatingText(
aoe.x,
aoe.y,
Math.round(dmg),
'#c9b4ff',
this.mods.burn > 0
));
if (this.mods.slowOnHit) aoe.slow = this.mods.slowStrength || 0.25;
}
}
// Освобождаем цель
if (p.originalTarget) {
smartTargeting.releaseTarget(p.originalTarget, p.id);
}
if (this.mods.doubleHit && Math.random() < this.mods.doubleHit) {
p.life = 0.3; // second detonation soon
} else {
break;
}
}
}
if (hit || p.life <= 0) {
if (p.originalTarget) {
smartTargeting.releaseTarget(p.originalTarget, p.id);
}
this.projectiles.splice(i, 1);
}
}
}
draw(context) {
context.fillStyle = "#c9b4ff";
this.projectiles.forEach((p) => {
context.beginPath();
context.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
context.fill();
context.strokeStyle = "rgba(201,180,255,0.25)";
context.beginPath();
context.arc(p.x, p.y, p.splash, 0, Math.PI * 2);
context.stroke();
// Линия к цели (если есть)
if (p.target && !p.target.isDead()) {
context.strokeStyle = "rgba(255,255,255,0.15)";
context.setLineDash([5, 5]);
context.beginPath();
context.moveTo(p.x, p.y);
context.lineTo(p.target.x, p.target.y);
context.stroke();
context.setLineDash([]);
}
});
}
}
class AuraWeapon extends Weapon {
constructor(player, proficiency, mods) {
super(player, proficiency, mods);
this.baseCooldown = 1.1;
this.baseDamage = 10;
this.radius = 90;
this.timer = 0;
}
update(enemies, delta) {
this.timer += delta;
const prof = this.proficiency.getModifiers();
const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
if (this.timer >= cooldown) {
let damage = this.baseDamage * prof.damageMult * this.mods.damageMult;
if (this.mods.divineStrike && this.mods.divineStrikeReady) {
damage *= 3;
this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
this.mods.divineStrikeReady = false;
}
for (const enemy of enemies) {
if (Math.hypot(enemy.x - this.player.x, enemy.y - this.player.y) < this.radius + this.mods.rangeBonus + enemy.radius) {
enemy.takeDamage(damage);
damageNumbers.push(new FloatingText(
enemy.x,
enemy.y,
Math.round(damage),
'#9be7ff',
false
));
}
}
// heal feedback
const heal = 4 + this.mods.auraHeal;
this.player.hp = Math.min(this.player.maxHp + this.mods.hpBonus, this.player.hp + heal);
if (heal > 0) {
healNumbers.push(new FloatingText(
this.player.x,
this.player.y - 20,
'+' + heal,
'#54e894',
false
));
}
this.timer = 0;
this.proficiency.addExp(4);
}
}
draw(context) {
context.strokeStyle = "rgba(155, 231, 255, 0.35)";
context.beginPath();
context.arc(this.player.x, this.player.y, this.radius + this.mods.rangeBonus, 0, Math.PI * 2);
context.stroke();
}
}
class Enemy {
constructor(x, y, waveScale = 1) {
this.x = x;
this.y = y;
this.radius = 16;
this.speed = 82;
this.baseHp = 28;
this.hp = Math.round(this.baseHp * (1 + waveScale * 0.15));
this.color = "#f45b69";
this.attackCooldown = 0.8;
this.timer = Math.random() * 0.5;
this.baseTouchDamage = 8;
this.touchDamage = Math.round(this.baseTouchDamage * (1 + waveScale * 0.12));
this.slow = 0;
this.waveScale = waveScale;
}
update(player, delta, mods) {
const dx = player.x - this.x;
const dy = player.y - this.y;
const len = Math.hypot(dx, dy) || 1;
const slowMult = this.slow ? 1 - this.slow : 1;
this.x += (dx / len) * this.speed * slowMult * delta;
this.y += (dy / len) * this.speed * slowMult * delta;
if (this.slow) this.slow = Math.max(0, this.slow - delta * 0.6);
this.timer += delta;
const dist = Math.hypot(player.x - this.x, player.y - this.y);
if (dist < this.radius + player.size / 2 && this.timer >= this.attackCooldown) {
player.takeDamage(this.touchDamage, mods);
this.timer = 0;
}
}
takeDamage(amount) {
this.hp -= amount;
totalDamageDealt += amount;
}
isDead() {
return this.hp <= 0;
}
draw(context) {
context.fillStyle = this.color;
context.beginPath();
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
context.fill();
}
}
class Boss {
constructor(type, wave) {
this.x = canvas.width / 2;
this.y = canvas.height / 2;
this.radius = 40;
this.type = type;
this.wave = wave;
this.maxHp = 800 + wave * 200;
this.hp = this.maxHp;
this.attackTimer = 0;
this.phase = 0;
this.projectiles = [];
this.color = "#ff4444";
this.name = "";
this.bossTimeLimit = BOSS_DURATION; // 3 минуты
this.bossTimer = 0;
if (type === 1) {
this.name = "Архимаг Хаоса";
this.color = "#ff6b9d";
} else if (type === 2) {
this.name = "Теневой Повелитель";
this.color = "#8b4fff";
} else {
this.name = "Древний Титан";
this.color = "#ffaa44";
}
}
update(player, delta) {
this.bossTimer += delta;
if (this.bossTimer >= this.bossTimeLimit) {
// Время вышло - игрок проиграл
player.hp = 0;
return;
}
this.attackTimer += delta;
const phase = Math.floor(this.hp / this.maxHp * 3);
this.phase = phase;
if (this.type === 1) {
this.updateArchmage(player, delta);
} else if (this.type === 2) {
this.updateShadowLord(player, delta);
} else {
this.updateTitan(player, delta);
}
// Обновление снарядов босса
for (let i = this.projectiles.length - 1; i >= 0; i--) {
const proj = this.projectiles[i];
proj.x += proj.vx * delta;
proj.y += proj.vy * delta;
proj.life -= delta;
const dist = Math.hypot(player.x - proj.x, player.y - proj.y);
if (dist < player.size / 2 + proj.radius) {
player.takeDamage(proj.damage, modifiers);
this.projectiles.splice(i, 1);
} else if (proj.life <= 0) {
this.projectiles.splice(i, 1);
}
}
}
updateArchmage(player, delta) {
// Фаза 1: Кольцо огня каждые 2.5 сек
if (this.phase >= 2 && this.attackTimer >= 2.5) {
this.attackTimer = 0;
for (let i = 0; i < 8; i++) {
const angle = (Math.PI * 2 / 8) * i;
this.projectiles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle) * 120,
vy: Math.sin(angle) * 120,
radius: 8,
damage: 12,
life: 3,
color: "#ff6b9d"
});
}
}
// Фаза 2: Направленные снаряды каждые 1.8 сек
if (this.phase >= 1 && this.attackTimer >= 1.8) {
this.attackTimer = 0;
const angle = Math.atan2(player.y - this.y, player.x - this.x);
for (let i = -1; i <= 1; i++) {
this.projectiles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle + i * 0.3) * 180,
vy: Math.sin(angle + i * 0.3) * 180,
radius: 10,
damage: 15,
life: 4,
color: "#ff6b9d"
});
}
}
// Фаза 3: Спираль каждые 3 сек
if (this.phase === 0 && this.attackTimer >= 3) {
this.attackTimer = 0;
const baseAngle = Math.atan2(player.y - this.y, player.x - this.x);
for (let i = 0; i < 12; i++) {
const angle = baseAngle + (Math.PI * 2 / 12) * i;
this.projectiles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle) * 100,
vy: Math.sin(angle) * 100,
radius: 9,
damage: 18,
life: 5,
color: "#ff6b9d"
});
}
}
}
updateShadowLord(player, delta) {
// Фаза 1: Теневые клинки каждые 2 сек
if (this.phase >= 2 && this.attackTimer >= 2) {
this.attackTimer = 0;
for (let i = 0; i < 6; i++) {
const angle = Math.atan2(player.y - this.y, player.x - this.x) + (i - 2.5) * 0.4;
this.projectiles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle) * 200,
vy: Math.sin(angle) * 200,
radius: 7,
damage: 14,
life: 3,
color: "#8b4fff"
});
}
}
// Фаза 2: Взрывающиеся сферы каждые 2.5 сек
if (this.phase >= 1 && this.attackTimer >= 2.5) {
this.attackTimer = 0;
const targets = [
{ x: player.x, y: player.y },
{ x: player.x + 100, y: player.y },
{ x: player.x - 100, y: player.y }
];
targets.forEach(target => {
const angle = Math.atan2(target.y - this.y, target.x - this.x);
this.projectiles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle) * 150,
vy: Math.sin(angle) * 150,
radius: 12,
damage: 20,
life: 3,
color: "#8b4fff",
explode: true,
explodeRadius: 80
});
});
}
// Фаза 3: Крест теней каждые 3.5 сек
if (this.phase === 0 && this.attackTimer >= 3.5) {
this.attackTimer = 0;
const dirs = [
{ x: 1, y: 0 }, { x: -1, y: 0 },
{ x: 0, y: 1 }, { x: 0, y: -1 }
];
dirs.forEach(dir => {
for (let i = 0; i < 5; i++) {
this.projectiles.push({
x: this.x + dir.x * i * 30,
y: this.y + dir.y * i * 30,
vx: dir.x * 140,
vy: dir.y * 140,
radius: 8,
damage: 16,
life: 4,
color: "#8b4fff"
});
}
});
}
}
updateTitan(player, delta) {
// Фаза 1: Ударные волны каждые 2.2 сек
if (this.phase >= 2 && this.attackTimer >= 2.2) {
this.attackTimer = 0;
const angle = Math.atan2(player.y - this.y, player.x - this.x);
for (let i = -2; i <= 2; i++) {
this.projectiles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle + i * 0.25) * 160,
vy: Math.sin(angle + i * 0.25) * 160,
radius: 15,
damage: 22,
life: 4,
color: "#ffaa44"
});
}
}
// Фаза 2: Круговые молнии каждые 2.8 сек
if (this.phase >= 1 && this.attackTimer >= 2.8) {
this.attackTimer = 0;
for (let i = 0; i < 10; i++) {
const angle = (Math.PI * 2 / 10) * i;
this.projectiles.push({
x: this.x + Math.cos(angle) * 60,
y: this.y + Math.sin(angle) * 60,
vx: Math.cos(angle) * 130,
vy: Math.sin(angle) * 130,
radius: 11,
damage: 19,
life: 3.5,
color: "#ffaa44"
});
}
}
// Фаза 3: Метеоритный дождь каждые 4 сек
if (this.phase === 0 && this.attackTimer >= 4) {
this.attackTimer = 0;
for (let i = 0; i < 8; i++) {
const x = Math.random() * canvas.width;
this.projectiles.push({
x: x,
y: -30,
vx: (Math.random() - 0.5) * 50,
vy: 200,
radius: 14,
damage: 25,
life: 4,
color: "#ffaa44"
});
}
}
}
takeDamage(amount) {
this.hp -= amount;
totalDamageDealt += amount;
}
isDead() {
return this.hp <= 0;
}
draw(context) {
// Тело босса
context.fillStyle = this.color;
context.beginPath();
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
context.fill();
// Обводка
context.strokeStyle = "#ffffff";
context.lineWidth = 3;
context.stroke();
// Имя
context.fillStyle = "#ffffff";
context.font = "bold 16px Arial";
context.textAlign = "center";
context.fillText(this.name, this.x, this.y - this.radius - 10);
// HP бар
const barWidth = 120;
const barHeight = 8;
context.fillStyle = "#333333";
context.fillRect(this.x - barWidth / 2, this.y + this.radius + 15, barWidth, barHeight);
context.fillStyle = "#ff4444";
context.fillRect(this.x - barWidth / 2, this.y + this.radius + 15, barWidth * (this.hp / this.maxHp), barHeight);
// Снаряды
this.projectiles.forEach(proj => {
context.fillStyle = proj.color;
context.beginPath();
context.arc(proj.x, proj.y, proj.radius, 0, Math.PI * 2);
context.fill();
});
}
}
let player = null;
let proficiency = null;
let weapon = null;
let enemies = [];
let spawnTimer = 0;
let wave = 1;
let waveTimer = 0;
let currentBoss = null;
let gameState = GAME_STATE.MENU;
let classKey = null;
let perkLevels = {};
let currentPassives = [];
let currentActives = [];
let modifiers = baseModifiers();
let divinePerksUnlocked = [];
let pressureTimer = 0;
let pressureDuration = 30; // 30 секунд до поражения при перегрузке
let pressureActive = false;
function findClosestEnemy(origin, list, maxRange = Infinity) {
let closest = null;
let closestDist = Number.MAX_VALUE;
for (const enemy of list) {
const dist = Math.hypot(enemy.x - origin.x, enemy.y - origin.y);
if (dist < closestDist && dist <= maxRange) {
closest = enemy;
closestDist = dist;
}
}
return closest;
}
function spawnEnemy() {
const edge = Math.floor(Math.random() * 4);
let x, y;
switch (edge) {
case 0:
x = Math.random() * canvas.width;
y = -20;
break;
case 1:
x = canvas.width + 20;
y = Math.random() * canvas.height;
break;
case 2:
x = Math.random() * canvas.width;
y = canvas.height + 20;
break;
default:
x = -20;
y = Math.random() * canvas.height;
}
const waveScale = Math.max(0, wave - 1);
enemies.push(new Enemy(x, y, waveScale));
}
function update(delta) {
if (gameState !== GAME_STATE.PLAYING) return;
// Обновляем систему комбо (только для воина)
if (classKey === 'warrior') {
comboSystem.update(delta);
}
// Очищаем систему умного наведения для мага
if (classKey === 'mage') {
// Периодическая очистка мертвых целей
if (Math.floor(waveTimer) % 5 === 0) {
// Нет автоматической очистки - она происходит при попадании/смерти снаряда
}
}
// Обновляем всплывающий текст
for (let i = damageNumbers.length - 1; i >= 0; i--) {
damageNumbers[i].update(delta);
if (!damageNumbers[i].isAlive()) {
damageNumbers.splice(i, 1);
}
}
for (let i = healNumbers.length - 1; i >= 0; i--) {
healNumbers[i].update(delta);
if (!healNumbers[i].isAlive()) {
healNumbers.splice(i, 1);
}
}
for (let i = comboNumbers.length - 1; i >= 0; i--) {
comboNumbers[i].update(delta);
if (!comboNumbers[i].isAlive()) {
comboNumbers.splice(i, 1);
}
}
// Проверка на босса (каждые 10 волн: 10, 20, 30...)
if (wave % 10 === 0 && !currentBoss && enemies.length === 0) {
const bossType = Math.floor((wave / 10 - 1) % 3) + 1;
currentBoss = new Boss(bossType, wave);
waveTimer = 0; // Сброс таймера для босса
smartTargeting.clear(); // Очищаем систему наведения при появлении босса
}
// Обработка божественных навыков
if (modifiers.eternalFlame && modifiers.eternalFlameCooldown <= 0) {
modifiers.eternalFlameCooldown = modifiers.eternalFlameCooldown || 6;
const targets = currentBoss ? [currentBoss] : enemies;
targets.forEach(target => {
const dist = Math.hypot(target.x - player.x, target.y - player.y);
if (dist <= 150) {
target.takeDamage(15);
damageNumbers.push(new FloatingText(
target.x,
target.y,
'15',
'#ff4444',
true
));
}
});
}
if (modifiers.starfall && modifiers.starfallCooldown <= 0) {
modifiers.starfallCooldown = modifiers.starfallCooldown || 18;
const targets = currentBoss ? [currentBoss] : enemies.filter(e => !e.isDead());
for (let i = 0; i < Math.min(5, targets.length); i++) {
const target = targets[Math.floor(Math.random() * targets.length)];
if (target) {
target.takeDamage(30);
damageNumbers.push(new FloatingText(
target.x,
target.y,
'30',
'#ffaa44',
true
));
}
}
}
if (modifiers.divineGrace && modifiers.divineGraceCooldown <= 0) {
modifiers.divineGraceCooldown = modifiers.divineGraceCooldown || 25;
player.hp = Math.min(player.maxHp + modifiers.hpBonus, player.hp + 40);
player.mana = Math.min(player.maxMana + modifiers.manaBonus, player.mana + 30);
healNumbers.push(new FloatingText(
player.x,
player.y - 30,
'+40 HP',
'#54e894',
true
));
}
if (modifiers.cosmicAwareness && modifiers.cosmicAwarenessCooldown <= 0) {
modifiers.cosmicAwarenessCooldown = modifiers.cosmicAwarenessCooldown || 14;
modifiers.cosmicAwarenessActive = true;
modifiers.cosmicAwarenessTimer = 4;
modifiers.damageMult *= 1.2;
}
if (modifiers.cosmicAwarenessActive) {
modifiers.cosmicAwarenessTimer -= delta;
if (modifiers.cosmicAwarenessTimer <= 0) {
modifiers.cosmicAwarenessActive = false;
modifiers.damageMult /= 1.2;
}
}
if (modifiers.timeDilation && modifiers.timeDilationCooldown <= 0) {
modifiers.timeDilationCooldown = modifiers.timeDilationCooldown || 15;
modifiers.timeDilationActive = true;
modifiers.timeDilationTimer = 3;
modifiers.cooldownMult *= 0.5;
}
if (modifiers.timeDilationActive) {
modifiers.timeDilationTimer -= delta;
if (modifiers.timeDilationTimer <= 0) {
modifiers.timeDilationActive = false;
modifiers.cooldownMult /= 0.5;
}
}
if (currentBoss) {
// Бой с боссом
currentBoss.update(player, delta);
weapon.update([currentBoss], delta);
// Проверка смерти босса
if (currentBoss.isDead()) {
player.giveXp(500 + wave * 50);
bossKilled = true;
openDivineReward();
currentBoss = null;
wave++;
waveTimer = 0;
} else if (player.hp <= 0) {
gameState = GAME_STATE.OVER;
showGameOver();
return;
}
} else {
// Обычная волна (1 минута)
waveTimer += delta;
// Система напряжения: если врагов >= 10, запускается обратный отсчёт
const enemyCount = enemies.filter(e => !e.isDead()).length;
if (enemyCount >= 10) {
if (!pressureActive) {
pressureActive = true;
pressureTimer = pressureDuration;
}
pressureTimer -= delta;
if (pressureTimer <= 0) {
// Поражение от перегрузки
player.hp = 0;
}
} else {
// Если врагов меньше 10, сбрасываем таймер напряжения
if (pressureActive) {
pressureActive = false;
pressureTimer = 0;
}
}
// Завершение волны через 1 минуту
if (waveTimer >= WAVE_DURATION) {
wave++;
waveTimer = 0;
enemies = []; // Очистка врагов
spawnTimer = 0;
pressureActive = false;
pressureTimer = 0;
smartTargeting.clear(); // Очищаем систему наведения между волнами
}
spawnTimer += delta;
const spawnRate = Math.max(0.25, 1.0 - wave * 0.03);
if (spawnTimer > spawnRate) {
spawnEnemy();
spawnTimer = 0;
}
player.update(delta, modifiers);
enemies.forEach((enemy) => enemy.update(player, delta, modifiers));
weapon.update(enemies, delta);
for (let i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].isDead()) {
enemiesKilled++;
if (modifiers.killHeal) {
player.hp = Math.min(player.maxHp + modifiers.hpBonus, player.hp + modifiers.killHeal);
healNumbers.push(new FloatingText(
player.x,
player.y - 30,
'+' + modifiers.killHeal,
'#54e894',
false
));
}
const xpGain = Math.round(12 * (1 + Math.max(0, wave - 1) * 0.1));
player.giveXp(xpGain);
enemies.splice(i, 1);
}
}
}
if (player.hp <= 0) {
gameState = GAME_STATE.OVER;
showGameOver();
}
// Автосохранение каждые 30 секунд
if (Math.floor(waveTimer) % 30 === 0 && Math.floor(waveTimer) > 0) {
saveGame();
}
// Проверка достижений
checkAchievements();
updateUI();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// backdrop grid
ctx.strokeStyle = "rgba(255,255,255,0.04)";
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// Отображаем всплывающий текст
damageNumbers.forEach(text => text.draw(ctx));
healNumbers.forEach(text => text.draw(ctx));
comboNumbers.forEach(text => text.draw(ctx));
if (weapon) weapon.draw(ctx);
if (player) player.draw(ctx);
if (currentBoss) {
currentBoss.draw(ctx);
} else {
enemies.forEach((enemy) => enemy.draw(ctx));
}
// Отображаем комбо для воина
if (classKey === 'warrior' && comboSystem.combo > 0) {
drawWarriorCombo(ctx);
}
}
function drawWarriorCombo(ctx) {
const combo = comboSystem.combo;
const multiplier = comboSystem.getMultiplier().toFixed(2);
const alpha = Math.min(1, comboSystem.timer / comboSystem.COMBO_RESET_TIME);
// Фон
ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.5})`;
ctx.fillRect(canvas.width - 120, 10, 110, 50);
// Текст комбо
ctx.font = 'bold 24px Arial';
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.textAlign = 'right';
ctx.fillText(`КОМБО: ${combo}`, canvas.width - 15, 35);
// Множитель
ctx.font = '16px Arial';
ctx.fillStyle = `rgba(255, 215, 102, ${alpha})`;
ctx.fillText(`${multiplier}x`, canvas.width - 15, 55);
// Прогресс до следующего комбо
const progress = comboSystem.getComboProgress();
const progressWidth = 100 * progress;
ctx.fillStyle = `rgba(255, 105, 180, ${alpha})`;
ctx.fillRect(canvas.width - 110, 60, progressWidth, 3);
// Таймер
const timerWidth = 100 * (comboSystem.timer / comboSystem.COMBO_RESET_TIME);
ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.3})`;
ctx.fillRect(canvas.width - 110, 60, timerWidth, 3);
}
function updateUI() {
document.getElementById("hpValue").textContent = player
? `${Math.max(0, Math.round(player.hp))}/${Math.round(player.maxHp + modifiers.hpBonus)}`
: "-";
document.getElementById("manaValue").textContent = player
? `${Math.round(player.mana)}/${Math.round(player.maxMana + modifiers.manaBonus)}`
: "-";
document.getElementById("profValue").textContent = proficiency
? `${proficiency.currentLevel} (${proficiency.exp}/${proficiency.expToNextLevel})`
: "-";
document.getElementById("waveValue").textContent = wave;
document.getElementById("classValue").textContent = classKey ? CLASS_DEFS[classKey].name : "-";
document.getElementById("levelValue").textContent = player
? `${player.level} (${Math.round(player.xp)}/${player.xpToNext})`
: "-";
// Комбо только для воина
const comboContainer = document.getElementById("comboContainer");
const comboElement = document.getElementById("comboValue");
if (classKey === 'warrior') {
comboContainer.style.display = "flex";
if (comboSystem.combo > 0) {
comboElement.textContent = `${comboSystem.combo} (${comboSystem.getMultiplier().toFixed(2)}x)`;
comboElement.style.color = '#ffd166';
} else {
comboElement.textContent = '0';
comboElement.style.color = '#a7b7d5';
}
} else {
comboContainer.style.display = "none";
}
// Таймер волны
if (currentBoss) {
const remaining = Math.max(0, BOSS_DURATION - waveTimer);
const mins = Math.floor(remaining / 60);
const secs = Math.floor(remaining % 60);
document.getElementById("waveTime").textContent = `Босс: ${mins}:${secs.toString().padStart(2, '0')}`;
} else {
const remaining = Math.max(0, WAVE_DURATION - waveTimer);
const mins = Math.floor(remaining / 60);
const secs = Math.floor(remaining % 60);
document.getElementById("waveTime").textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Таймер напряжения
const pressureEl = document.getElementById("pressureTimer");
const pressureTimeEl = document.getElementById("pressureTime");
if (pressureActive && !currentBoss) {
pressureEl.style.display = "flex";
const remaining = Math.max(0, pressureTimer);
const secs = Math.floor(remaining);
const ms = Math.floor((remaining - secs) * 10);
pressureTimeEl.textContent = `${secs}.${ms}`;
pressureTimeEl.style.color = remaining < 10 ? "#ff4444" : "#ffaa44";
} else {
pressureEl.style.display = "none";
}
}
function loop(timestamp) {
if (!lastTime) lastTime = timestamp;
const delta = Math.min(0.05, (timestamp - lastTime) / 1000);
lastTime = timestamp;
update(delta);
draw();
requestAnimationFrame(loop);
}
function setupClassSelect() {
const buttons = document.querySelectorAll(".class-card");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
startGame(btn.dataset.class);
});
});
// Проверяем есть ли сохранение
const saved = loadGame();
if (saved) {
const continueBtn = document.createElement('button');
continueBtn.textContent = 'Продолжить';
continueBtn.style.cssText = `
margin-top: 10px;
padding: 10px 20px;
background: linear-gradient(135deg, #7cfbce, #4be0ff);
color: #041022;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
width: 100%;
`;
continueBtn.addEventListener('click', () => {
continueGame(saved);
});
const panel = document.querySelector('#classSelect .panel');
panel.appendChild(continueBtn);
}
document.getElementById("restartBtn").addEventListener("click", () => {
deleteSave(); // Удаляем сохранение при перезапуске
resetGame();
});
// Горячие клавиши
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
// ESC - пауза/меню
if (e.code === 'Escape') {
if (gameState === GAME_STATE.PLAYING) {
saveGame();
showQuickMessage('Игра сохранена!');
}
}
// F5 - быстрое сохранение
if (e.code === 'F5') {
e.preventDefault();
saveGame();
showQuickMessage('Игра сохранена!');
}
// F9 - быстрая загрузка
if (e.code === 'F9') {
e.preventDefault();
const saved = loadGame();
if (saved) {
continueGame(saved);
showQuickMessage('Игра загружена!');
}
}
});
window.addEventListener("keyup", (e) => {
keys[e.code] = false;
});
}
function showQuickMessage(text) {
const msg = document.createElement('div');
msg.textContent = text;
msg.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 40px;
border-radius: 10px;
z-index: 1001;
animation: fadeInOut 2s;
`;
document.body.appendChild(msg);
setTimeout(() => {
if (msg.parentNode) {
msg.parentNode.removeChild(msg);
}
}, 2000);
}
// Добавляем анимацию для быстрого сообщения
const quickMsgStyle = document.createElement('style');
quickMsgStyle.textContent = `
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
20%, 80% { opacity: 1; }
}
`;
document.head.appendChild(quickMsgStyle);
function continueGame(save) {
classKey = save.classKey;
const def = CLASS_DEFS[classKey];
// Восстанавливаем игрока
player = new Player(
save.player.x || canvas.width / 2,
save.player.y || canvas.height / 2,
def.color,
save.player.maxHp || def.hp,
def.speed,
save.player.maxMana || def.mana,
def.manaRegen
);
// Восстанавливаем состояние
player.hp = save.player.hp;
player.mana = save.player.mana;
player.level = save.player.level;
player.xp = save.player.xp;
player.xpToNext = save.player.xpToNext;
// Восстанавливаем мастерство
proficiency = new Proficiency(def.proficiency);
proficiency.currentLevel = save.proficiency.currentLevel;
proficiency.exp = save.proficiency.exp;
proficiency.expToNextLevel = save.proficiency.expToNextLevel;
proficiency.unlockedPerkIds = save.proficiency.unlockedPerkIds;
// Восстанавливаем остальное
wave = save.wave || 1;
waveTimer = save.waveTimer || 0;
perkLevels = save.perks || {};
currentPassives = save.currentPassives || [];
currentActives = save.currentActives || [];
divinePerksUnlocked = save.divinePerksUnlocked || [];
modifiers = save.modifiers || baseModifiers();
enemiesKilled = save.enemiesKilled || 0;
totalDamageDealt = save.totalDamageDealt || 0;
bossKilled = save.bossKilled || false;
// Создаем оружие
switch (def.weapon) {
case "bow":
weapon = new BowWeapon(player, proficiency, modifiers);
break;
case "orb":
weapon = new OrbWeapon(player, proficiency, modifiers);
break;
case "aura":
weapon = new AuraWeapon(player, proficiency, modifiers);
break;
default:
weapon = new SwordWeapon(player, proficiency, modifiers);
}
// Начинаем игру
enemies = [];
spawnTimer = 0;
currentBoss = null;
gameState = GAME_STATE.PLAYING;
gameLoaded = true;
// Очищаем системы
damageNumbers = [];
healNumbers = [];
comboNumbers = [];
smartTargeting.clear();
if (classKey === 'warrior') {
comboSystem.reset();
}
document.getElementById("classSelect").classList.add("hidden");
document.getElementById("gameOver").classList.add("hidden");
refreshSkillIcons();
updateUI();
console.log('Игра загружена');
}
function startGame(key) {
classKey = key;
const def = CLASS_DEFS[key];
modifiers = baseModifiers();
perkLevels = {};
player = new Player(canvas.width / 2, canvas.height / 2, def.color, def.hp, def.speed, def.mana, def.manaRegen);
proficiency = new Proficiency(def.proficiency);
// Открываем базовый навык сразу при старте (уровень мастерства 0 -> 1)
proficiency.currentLevel = 1;
proficiency.unlockRandomPerk();
currentPassives = [];
currentActives = [];
switch (def.weapon) {
case "bow":
weapon = new BowWeapon(player, proficiency, modifiers);
break;
case "orb":
weapon = new OrbWeapon(player, proficiency, modifiers);
break;
case "aura":
weapon = new AuraWeapon(player, proficiency, modifiers);
break;
default:
weapon = new SwordWeapon(player, proficiency, modifiers);
}
enemies = [];
spawnTimer = 0;
wave = 1;
waveTimer = 0;
currentBoss = null;
divinePerksUnlocked = [];
pressureActive = false;
pressureTimer = 0;
enemiesKilled = 0;
totalDamageDealt = 0;
bossKilled = false;
damageNumbers = [];
healNumbers = [];
comboNumbers = [];
smartTargeting.clear();
comboSystem.reset();
gameState = GAME_STATE.PLAYING;
document.getElementById("classSelect").classList.add("hidden");
document.getElementById("gameOver").classList.add("hidden");
document.getElementById("levelUp").classList.add("hidden");
document.getElementById("divineReward").classList.add("hidden");
renderSkillIcons(currentActives, currentPassives);
renderMetaIcons();
updateUI();
}
function resetGame() {
gameState = GAME_STATE.MENU;
document.getElementById("classSelect").classList.remove("hidden");
document.getElementById("gameOver").classList.add("hidden");
enemies = [];
player = null;
weapon = null;
proficiency = null;
classKey = null;
currentActives = [];
currentPassives = [];
perkLevels = {};
currentBoss = null;
waveTimer = 0;
divinePerksUnlocked = [];
pressureActive = false;
pressureTimer = 0;
enemiesKilled = 0;
totalDamageDealt = 0;
bossKilled = false;
comboSystem.reset();
damageNumbers = [];
healNumbers = [];
comboNumbers = [];
smartTargeting.clear();
}
function showGameOver() {
let stats = `Волна: ${wave} · Мастерство: ${proficiency.currentLevel} · Уровень: ${player.level} · Убийств: ${enemiesKilled}`;
if (classKey === 'warrior') {
stats += ` · Макс комбо: ${comboSystem.maxCombo}`;
}
document.getElementById("gameOverStats").textContent = stats;
document.getElementById("gameOver").classList.remove("hidden");
}
function openLevelUp() {
gameState = GAME_STATE.LEVELUP;
const choices = document.getElementById("levelChoices");
choices.innerHTML = "";
const pool = getAvailablePerks();
shuffle(pool);
const pick = pool.slice(0, 3);
pick.forEach((perk) => {
const card = document.createElement("div");
const rarity = perk.rarity || "common";
card.className = `choice-card rarity-${rarity}`;
card.innerHTML = `<div class="title">${perk.name}</div><div class="type">${rarityLabel(rarity)} · ${perk.type === "passive" ? "Пассив" : "Актив"}</div><div class="desc">${perk.desc}</div>`;
card.addEventListener("click", () => {
applyPerk(perk);
document.getElementById("levelUp").classList.add("hidden");
gameState = GAME_STATE.PLAYING;
});
choices.appendChild(card);
});
document.getElementById("levelUp").classList.remove("hidden");
}
function openDivineReward() {
gameState = GAME_STATE.DIVINE_REWARD;
const choices = document.getElementById("divineChoices");
choices.innerHTML = "";
const available = DIVINE_PERKS.filter(p => !divinePerksUnlocked.includes(p.id));
shuffle(available);
const pick = available.slice(0, 3);
if (pick.length === 0) {
// Все божественные навыки получены - даём обычные награды
openLevelUp();
return;
}
pick.forEach((perk) => {
const card = document.createElement("div");
card.className = `choice-card rarity-divine`;
card.innerHTML = `<div class="title">${perk.name}</div><div class="type">Божественный · Пассив</div><div class="desc">${perk.desc}</div>`;
card.addEventListener("click", () => {
applyDivinePerk(perk);
document.getElementById("divineReward").classList.add("hidden");
gameState = GAME_STATE.PLAYING;
});
choices.appendChild(card);
});
document.getElementById("divineReward").classList.remove("hidden");
}
function applyDivinePerk(perk) {
perk.effect(modifiers);
divinePerksUnlocked.push(perk.id);
weapon.syncMods(modifiers);
refreshSkillIcons();
}
function getAvailablePerks() {
if (!classKey || !proficiency) return [];
const kit = CLASS_KITS[classKey];
const basePerks = BASE_PERKS[classKey] || [];
// Объединяем базовые и основные навыки
const allPerks = [
...basePerks.map((p) => ({ ...p, type: "passive" })),
...kit.passives.map((p) => ({ ...p, type: "passive" })),
...kit.actives.map((p) => ({ ...p, type: "active" })),
];
// Фильтруем только открытые через мастерство
const unlocked = allPerks.filter(p => proficiency.unlockedPerkIds.includes(p.id));
return unlocked;
}
function applyPerk(perk) {
const info = perkLevels[perk.id] || { level: 0, ascended: 0, type: perk.type };
info.level += 1;
const max = perk.max || 3;
const isAscend = info.level > max;
// apply effect; for ascension, add a light extra scaling
perk.effect(modifiers);
if (isAscend) {
modifiers.damageMult *= 1.04;
modifiers.cooldownMult *= 0.98;
modifiers.hpBonus += 6;
modifiers.rangeBonus += 3;
}
info.ascended = Math.max(0, info.level - max);
perkLevels[perk.id] = info;
if (perk.type === "passive" && !currentPassives.includes(perk.id)) currentPassives.push(perk.id);
if (perk.type === "active" && !currentActives.includes(perk.id)) currentActives.push(perk.id);
weapon.syncMods(modifiers);
refreshSkillIcons();
}
function refreshSkillIcons() {
renderSkillIcons(currentActives, currentPassives);
renderMetaIcons();
}
function renderSkillIcons(activeIds, passiveIds) {
const activeContainer = document.getElementById("activeSkills");
const passiveContainer = document.getElementById("passiveSkills");
activeContainer.innerHTML = "";
passiveContainer.innerHTML = "";
activeIds.forEach((id) => activeContainer.appendChild(makeSkillIcon(id)));
passiveIds.forEach((id) => passiveContainer.appendChild(makeSkillIcon(id)));
}
const META_GROUPS = {
warrior: [
{ id: "w_meta_1", title: "Вихрь + Рипост", needs: ["whirlwind", "riposte"] },
{ id: "w_meta_2", title: "Пыл + Знамя", needs: ["battle_fervor", "war_banner"] },
],
archer: [
{ id: "a_meta_1", title: "Залп + Пробитие", needs: ["volley", "piercing_arrows"] },
{ id: "a_meta_2", title: "Рико + Марка", needs: ["ricochet", "marked_target"] },
],
mage: [
{ id: "m_meta_1", title: "Комета + Жар", needs: ["arcane_comet", "ember_focus"] },
{ id: "m_meta_2", title: "Синхрония + Барьер", needs: ["elemental_sync", "mana_barrier"] },
],
acolyte: [
{ id: "c_meta_1", title: "Освящение + Свет", needs: ["consecrate", "soothing_light"] },
{ id: "c_meta_2", title: "Страж + Броня", needs: ["guardian_aura", "radiant_armor"] },
],
};
function renderMetaIcons() {
const container = document.getElementById("metaSkills");
if (!container) return;
container.innerHTML = "";
if (!classKey) return;
const groups = META_GROUPS[classKey] || [];
groups.forEach((g) => {
const div = document.createElement("div");
div.className = "skill-icon meta";
const dot = document.createElement("div");
dot.className = "meta-dot";
const ready = g.needs.every((id) => perkLevels[id]?.level >= 1);
if (ready) dot.classList.add("ready");
const text = document.createElement("div");
text.className = "meta-text";
text.textContent = g.title;
div.appendChild(dot);
div.appendChild(text);
container.appendChild(div);
});
}
function makeSkillIcon(id) {
const wrapper = document.createElement("div");
const rarity = (getPerkDef(id)?.rarity) || "common";
wrapper.className = `skill-icon rarity-${rarity}`;
const canvasIcon = document.createElement("canvas");
canvasIcon.width = 32;
canvasIcon.height = 32;
drawIcon(canvasIcon.getContext("2d"), id);
wrapper.appendChild(canvasIcon);
const label = document.createElement("div");
label.className = "skill-label";
const kit = CLASS_KITS[classKey];
const all = kit ? [...kit.passives, ...kit.actives] : [];
const max = all.find((p) => p.id === id)?.max || 3;
const info = perkLevels[id];
const lv = info ? info.level : 0;
const asc = info ? info.ascended : 0;
label.textContent = `${shortLabel(id)} ${lv}/${max}${asc > 0 ? "+" + asc : ""}`;
wrapper.appendChild(label);
return wrapper;
}
function shortLabel(id) {
const map = {
// warrior
iron_skin: "Skin",
battle_fervor: "Ferv",
blade_mastery: "Blade",
riposte: "Rip",
heavy_guard: "Guard",
bloodlust: "Blood",
sweeping_edge: "Sweep",
adamant: "Adam",
momentum: "Move",
war_banner: "Banner",
blade_dash: "Dash",
shockwave: "Wave",
guard_stance: "Stance",
whirlwind: "Whirl",
// archer
quickdraw: "QDraw",
light_steps: "Steps",
piercing_arrows: "Pierce",
hunter_focus: "Focus",
ricochet: "Rico",
wind_runner: "Wind",
bleeding_edge: "Bleed",
camo: "Camo",
falcon_eye: "Eye",
arrow_surge: "Surge",
power_shot: "Power",
volley: "Volley",
evasive_roll: "Roll",
marked_target: "Mark",
// mage
ember_focus: "Ember",
mana_font: "Font",
arcane_precision: "Prec",
frost_weave: "Frost",
overload: "Over",
runic_shield: "Shield",
elemental_sync: "Sync",
astral_echo: "Echo",
channeling: "Chan",
mystic_amp: "Amp",
arcane_comet: "Comet",
flame_burst: "Burst",
frost_nova: "Nova",
mana_barrier: "Barrier",
// acolyte
steadfast: "Stand",
radiant_armor: "Armor",
soothing_light: "Light",
devotion: "Dev",
sanctuary: "Sanc",
spirit_chain: "Chain",
warding: "Ward",
resolve: "Res",
holy_edge: "Edge",
blessing: "Bless",
healing_wave: "Heal",
smite: "Smite",
consecrate: "Cons",
guardian_aura: "GuardA",
};
return map[id] || "Perk";
}
function drawIcon(c, id) {
c.imageSmoothingEnabled = false;
c.clearRect(0, 0, 32, 32);
const px = 4;
const drawPixel = (x, y, color) => {
c.fillStyle = color;
c.fillRect(x * px, y * px, px, px);
};
// simple shapes per type
const swordColors = ["#9bd5ff", "#d7ecff"];
const bowColors = ["#c17b2a", "#f4d35e"];
const orbColors = ["#c9b4ff", "#ff7b7b", "#ffa64d"];
const shieldColors = ["#9be7ff", "#5bbcf2"];
if (id.includes("blade") || id.includes("whirl") || id.includes("slash") || id.includes("shock")) {
swordColors.forEach((color, idx) => {
drawPixel(4 + idx, 1 + idx, color);
drawPixel(5 + idx, 2 + idx, color);
drawPixel(6 + idx, 3 + idx, color);
});
} else if (id.includes("arrow") || id.includes("shot") || id.includes("bow") || id.includes("mark")) {
drawPixel(1, 2, bowColors[0]);
drawPixel(2, 2, bowColors[1]);
drawPixel(3, 2, bowColors[0]);
drawPixel(4, 2, bowColors[1]);
drawPixel(5, 2, bowColors[0]);
drawPixel(4, 1, bowColors[1]);
drawPixel(4, 3, bowColors[1]);
} else if (id.includes("orb") || id.includes("mana") || id.includes("flame") || id.includes("frost") || id.includes("comet")) {
drawPixel(2, 2, orbColors[0]);
drawPixel(2, 3, orbColors[1]);
drawPixel(3, 2, orbColors[2]);
drawPixel(3, 3, orbColors[0]);
} else if (id.includes("guard") || id.includes("armor") || id.includes("shield") || id.includes("aura")) {
shieldColors.forEach((color, idx) => {
drawPixel(2 + idx, 1, color);
drawPixel(2 + idx, 2, color);
drawPixel(2 + idx, 3, color);
drawPixel(2 + idx, 4, color);
});
} else {
drawPixel(2, 2, "#ffffff");
}
}
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
setupClassSelect();
requestAnimationFrame(loop);