Noilil commited on
Commit
3a7c895
·
verified ·
1 Parent(s): 84bfc55

Upload 3 files

Browse files
Waveborne_Prodigy_Copy/index.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Waveborne: Prodigy</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="ui">
11
+ <div class="stat"><span>Класс:</span> <span id="classValue">-</span></div>
12
+ <div class="stat"><span>HP:</span> <span id="hpValue">0</span></div>
13
+ <div class="stat"><span>Мана:</span> <span id="manaValue">-</span></div>
14
+ <div class="stat"><span>Уровень:</span> <span id="levelValue">1</span></div>
15
+ <div class="stat"><span>Мастерство:</span> <span id="profValue">0</span></div>
16
+ <div class="stat"><span>Волна:</span> <span id="waveValue">1</span></div>
17
+ <div class="stat"><span>Время волны:</span> <span id="waveTime">0:00</span></div>
18
+ <div class="stat warrior-only" id="comboContainer" style="display: none;"><span>Комбо:</span> <span id="comboValue">0</span></div>
19
+ <div class="stat danger" id="pressureTimer" style="display: none;"><span>Напряжение:</span> <span id="pressureTime">0:00</span></div>
20
+ </div>
21
+
22
+ <div id="canvasWrapper">
23
+ <canvas id="gameCanvas" width="960" height="600"></canvas>
24
+ <div id="classSelect" class="overlay">
25
+ <div class="panel">
26
+ <h2>Выбери класс</h2>
27
+ <p class="hint">От выбора меняются базовые статы и тип оружия</p>
28
+ <div class="class-grid">
29
+ <button class="class-card" data-class="warrior">
30
+ <div class="icon sword"></div>
31
+ <div class="title">Warrior</div>
32
+ <div class="desc">Меч, комбо-удары, быстрые атаки в ближнем бою</div>
33
+ </button>
34
+ <button class="class-card" data-class="archer">
35
+ <div class="icon bow"></div>
36
+ <div class="title">Archer</div>
37
+ <div class="desc">Лук, дальний бой, мобильность</div>
38
+ </button>
39
+ <button class="class-card" data-class="mage">
40
+ <div class="icon staff"></div>
41
+ <div class="title">Mage</div>
42
+ <div class="desc">Стихийные сферы, умное наведение по целям</div>
43
+ </button>
44
+ <button class="class-card" data-class="acolyte">
45
+ <div class="icon shield"></div>
46
+ <div class="title">Acolyte</div>
47
+ <div class="desc">Ауры, поддержка, устойчивость</div>
48
+ </button>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div id="gameOver" class="overlay hidden">
53
+ <div class="panel">
54
+ <h2>Падение героя</h2>
55
+ <p id="gameOverStats"></p>
56
+ <button id="restartBtn">Заново</button>
57
+ </div>
58
+ </div>
59
+ <div id="levelUp" class="overlay hidden">
60
+ <div class="panel">
61
+ <h2>Повышение уровня</h2>
62
+ <p class="hint">Выберите улучшение</p>
63
+ <div id="levelChoices" class="choice-grid"></div>
64
+ </div>
65
+ </div>
66
+ <div id="divineReward" class="overlay hidden">
67
+ <div class="panel">
68
+ <h2>Божественная награда</h2>
69
+ <p class="hint">Выберите божественный навык</p>
70
+ <div id="divineChoices" class="choice-grid"></div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <div id="skillBar">
76
+ <div class="skills-section">
77
+ <div class="section-title">Активные</div>
78
+ <div id="activeSkills" class="icon-row"></div>
79
+ </div>
80
+ <div class="skills-section">
81
+ <div class="section-title">Пассивные</div>
82
+ <div id="passiveSkills" class="icon-row"></div>
83
+ </div>
84
+ <div class="skills-section">
85
+ <div class="section-title">Комбо / Мета</div>
86
+ <div id="metaSkills" class="meta-row"></div>
87
+ </div>
88
+ </div>
89
+
90
+ <script src="main.js"></script>
91
+ </body>
92
+ </html>
Waveborne_Prodigy_Copy/main.js ADDED
@@ -0,0 +1,2623 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const canvas = document.getElementById("gameCanvas");
2
+ const ctx = canvas.getContext("2d");
3
+
4
+ const keys = {};
5
+ let lastTime = 0;
6
+
7
+ const GAME_STATE = {
8
+ MENU: "menu",
9
+ PLAYING: "playing",
10
+ LEVELUP: "levelup",
11
+ DIVINE_REWARD: "divine_reward",
12
+ OVER: "over",
13
+ };
14
+
15
+ // ============ КОНСТАНТЫ ВРЕМЕНИ ============
16
+ const WAVE_DURATION = 60; // 1 минута для обычных волн
17
+ const BOSS_DURATION = 180; // 3 минуты для босса
18
+
19
+ // ============ СИСТЕМА СОХРАНЕНИЯ ============
20
+ const SAVE_KEY = 'waveborne_save';
21
+ let gameLoaded = false;
22
+
23
+ function saveGame() {
24
+ if (!player || !proficiency) return;
25
+
26
+ const save = {
27
+ classKey: classKey,
28
+ player: {
29
+ x: player.x,
30
+ y: player.y,
31
+ hp: player.hp,
32
+ maxHp: player.maxHp,
33
+ mana: player.mana,
34
+ maxMana: player.maxMana,
35
+ level: player.level,
36
+ xp: player.xp,
37
+ xpToNext: player.xpToNext
38
+ },
39
+ proficiency: {
40
+ name: proficiency.name,
41
+ currentLevel: proficiency.currentLevel,
42
+ exp: proficiency.exp,
43
+ expToNextLevel: proficiency.expToNextLevel,
44
+ unlockedPerkIds: proficiency.unlockedPerkIds
45
+ },
46
+ wave: wave,
47
+ waveTimer: waveTimer,
48
+ perks: perkLevels,
49
+ currentPassives: currentPassives,
50
+ currentActives: currentActives,
51
+ divinePerksUnlocked: divinePerksUnlocked,
52
+ modifiers: modifiers,
53
+ timestamp: Date.now(),
54
+ enemiesKilled: enemiesKilled,
55
+ totalDamageDealt: totalDamageDealt
56
+ };
57
+
58
+ try {
59
+ localStorage.setItem(SAVE_KEY, JSON.stringify(save));
60
+ console.log('Игра сохранена');
61
+ } catch (e) {
62
+ console.error('Ошибка сохранения:', e);
63
+ }
64
+ }
65
+
66
+ function loadGame() {
67
+ try {
68
+ const saved = localStorage.getItem(SAVE_KEY);
69
+ if (!saved) return false;
70
+
71
+ const save = JSON.parse(saved);
72
+ if (!save || !save.classKey) return false;
73
+
74
+ // Проверяем, не устарело ли сохранение (больше 1 часа)
75
+ const now = Date.now();
76
+ if (now - save.timestamp > 3600000) {
77
+ console.log('Сохранение устарело (больше 1 часа)');
78
+ return false;
79
+ }
80
+
81
+ return save;
82
+ } catch (e) {
83
+ console.error('Ошибка загрузки:', e);
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function deleteSave() {
89
+ localStorage.removeItem(SAVE_KEY);
90
+ }
91
+
92
+ // ============ УНИКАЛЬНАЯ СИСТЕМА КОМБО ДЛЯ ВАРРИОРА ============
93
+ class WarriorComboSystem {
94
+ constructor() {
95
+ this.combo = 0;
96
+ this.multiplier = 1.0;
97
+ this.timer = 0;
98
+ this.maxCombo = 0;
99
+ this.comboHits = 0; // Счетчик ударов для комбо
100
+ this.lastHitTime = 0; // Время последнего удара
101
+ this.COMBO_WINDOW = 0.5; // Окно для комбо - 0.5 секунды
102
+ this.COMBO_RESET_TIME = 1.0; // Сброс комбо через 1 секунду без ударов
103
+ }
104
+
105
+ addHit() {
106
+ const now = Date.now() / 1000;
107
+
108
+ // Проверяем, был ли удар в окне комбо
109
+ if (now - this.lastHitTime <= this.COMBO_WINDOW) {
110
+ this.comboHits++;
111
+
112
+ // Каждые 3 удара в окне комбо дают +1 к комбо
113
+ if (this.comboHits >= 3) {
114
+ this.combo++;
115
+ this.comboHits = 0;
116
+
117
+ // Обновляем множитель
118
+ this.multiplier = 1.0 + Math.min(1.5, (this.combo * 0.05)); // до 2.5x множителя
119
+
120
+ if (this.combo > this.maxCombo) {
121
+ this.maxCombo = this.combo;
122
+ }
123
+
124
+ // Сбрасываем таймер
125
+ this.timer = this.COMBO_RESET_TIME;
126
+
127
+ console.log(`Комбо: ${this.combo} (x${this.multiplier.toFixed(2)})`);
128
+ }
129
+ } else {
130
+ // Если удар был вне окна комбо, начинаем заново
131
+ this.comboHits = 1;
132
+ }
133
+
134
+ this.lastHitTime = now;
135
+ this.timer = this.COMBO_RESET_TIME;
136
+
137
+ return this.multiplier;
138
+ }
139
+
140
+ reset() {
141
+ if (this.combo > 5) {
142
+ // Бонус опыта за сброшенное комбо
143
+ const bonusXP = Math.floor(this.combo * 0.8);
144
+ if (player) player.giveXp(bonusXP);
145
+ }
146
+ this.combo = 0;
147
+ this.comboHits = 0;
148
+ this.multiplier = 1.0;
149
+ this.timer = 0;
150
+ }
151
+
152
+ update(delta) {
153
+ if (this.timer > 0) {
154
+ this.timer -= delta;
155
+ if (this.timer <= 0) {
156
+ this.reset();
157
+ }
158
+ }
159
+ }
160
+
161
+ getMultiplier() {
162
+ return this.multiplier;
163
+ }
164
+
165
+ getComboHits() {
166
+ return this.comboHits;
167
+ }
168
+
169
+ getComboProgress() {
170
+ return this.comboHits / 3; // Прогресс до следующего комбо (0-1)
171
+ }
172
+ }
173
+
174
+ const comboSystem = new WarriorComboSystem();
175
+
176
+ // ============ СИСТЕМА УМНОГО НАВЕДЕНИЯ ДЛЯ МАГА ============
177
+ class SmartTargeting {
178
+ constructor() {
179
+ this.activeProjectiles = new Set();
180
+ this.targetedEnemies = new Map(); // enemy -> projectile count
181
+ }
182
+
183
+ registerProjectile(projectileId) {
184
+ this.activeProjectiles.add(projectileId);
185
+ }
186
+
187
+ unregisterProjectile(projectileId) {
188
+ this.activeProjectiles.delete(projectileId);
189
+ }
190
+
191
+ getBestTarget(enemies, playerX, playerY, range, excludeTarget = null) {
192
+ // Фильтруем живых врагов в радиусе
193
+ const availableEnemies = enemies.filter(enemy =>
194
+ !enemy.isDead() &&
195
+ Math.hypot(enemy.x - playerX, enemy.y - playerY) <= range &&
196
+ enemy !== excludeTarget
197
+ );
198
+
199
+ if (availableEnemies.length === 0) return null;
200
+
201
+ // Находим врага с наименьшим количеством снарядов, летящих в него
202
+ let bestTarget = null;
203
+ let minProjectiles = Infinity;
204
+
205
+ for (const enemy of availableEnemies) {
206
+ const projectileCount = this.targetedEnemies.get(enemy) || 0;
207
+
208
+ // Предпочитаем цели, в которые летит меньше снарядов
209
+ if (projectileCount < minProjectiles) {
210
+ minProjectiles = projectileCount;
211
+ bestTarget = enemy;
212
+ } else if (projectileCount === minProjectiles) {
213
+ // При равенстве выбираем ближайшего
214
+ const currentDist = bestTarget ? Math.hypot(bestTarget.x - playerX, bestTarget.y - playerY) : Infinity;
215
+ const newDist = Math.hypot(enemy.x - playerX, enemy.y - playerY);
216
+
217
+ if (newDist < currentDist) {
218
+ bestTarget = enemy;
219
+ }
220
+ }
221
+ }
222
+
223
+ return bestTarget;
224
+ }
225
+
226
+ assignTarget(enemy) {
227
+ const count = this.targetedEnemies.get(enemy) || 0;
228
+ this.targetedEnemies.set(enemy, count + 1);
229
+
230
+ // Возвращаем ID для отслеживания
231
+ const projectileId = Date.now() + Math.random();
232
+ return projectileId;
233
+ }
234
+
235
+ releaseTarget(enemy, projectileId) {
236
+ const count = this.targetedEnemies.get(enemy);
237
+ if (count !== undefined) {
238
+ if (count <= 1) {
239
+ this.targetedEnemies.delete(enemy);
240
+ } else {
241
+ this.targetedEnemies.set(enemy, count - 1);
242
+ }
243
+ }
244
+ this.activeProjectiles.delete(projectileId);
245
+ }
246
+
247
+ clear() {
248
+ this.activeProjectiles.clear();
249
+ this.targetedEnemies.clear();
250
+ }
251
+ }
252
+
253
+ const smartTargeting = new SmartTargeting();
254
+
255
+ // ============ ВСПЛЫВАЮЩИЙ ТЕКСТ ============
256
+ let damageNumbers = [];
257
+ let healNumbers = [];
258
+ let comboNumbers = [];
259
+
260
+ class FloatingText {
261
+ constructor(x, y, text, color, isCritical = false, fontSize = 16) {
262
+ this.x = x;
263
+ this.y = y;
264
+ this.text = text;
265
+ this.color = color;
266
+ this.life = 1.0;
267
+ this.velocityY = -40;
268
+ this.gravity = 10;
269
+ this.fontSize = isCritical ? `bold ${fontSize}px` : `bold ${fontSize}px`;
270
+ this.isCritical = isCritical;
271
+ }
272
+
273
+ update(delta) {
274
+ this.y += this.velocityY * delta;
275
+ this.velocityY += this.gravity * delta;
276
+ this.life -= delta;
277
+ }
278
+
279
+ draw(ctx) {
280
+ const alpha = Math.min(1, this.life * 2);
281
+ ctx.font = this.fontSize + ' Arial';
282
+ ctx.fillStyle = this.color.replace(')', `, ${alpha})`).replace('rgb', 'rgba');
283
+ ctx.textAlign = 'center';
284
+
285
+ if (this.isCritical) {
286
+ ctx.shadowColor = this.color;
287
+ ctx.shadowBlur = 10;
288
+ ctx.fillText(this.text, this.x, this.y);
289
+ ctx.shadowBlur = 0;
290
+ } else {
291
+ ctx.fillText(this.text, this.x, this.y);
292
+ }
293
+ }
294
+
295
+ isAlive() {
296
+ return this.life > 0;
297
+ }
298
+ }
299
+
300
+ // ============ ДОСТИЖЕНИЯ ============
301
+ const ACHIEVEMENTS = {
302
+ FIRST_BLOOD: {
303
+ id: 'first_blood',
304
+ name: 'Первая кровь',
305
+ desc: 'Убить первого врага',
306
+ icon: '🩸',
307
+ unlocked: false,
308
+ check: () => enemiesKilled >= 1
309
+ },
310
+ COMBO_MASTER: {
311
+ id: 'combo_master',
312
+ name: 'Мастер комбо',
313
+ desc: 'Набрать комбо из 10 ударов',
314
+ icon: '⚡',
315
+ unlocked: false,
316
+ check: () => classKey === 'warrior' && comboSystem.maxCombo >= 10
317
+ },
318
+ WAVE_10: {
319
+ id: 'wave_10',
320
+ name: 'Выживший',
321
+ desc: 'Достичь 10 волны',
322
+ icon: '🏆',
323
+ unlocked: false,
324
+ check: () => wave >= 10
325
+ },
326
+ BOSS_SLAYER: {
327
+ id: 'boss_slayer',
328
+ name: 'Убийца боссов',
329
+ desc: 'Победить первого босса',
330
+ icon: '👑',
331
+ unlocked: false,
332
+ check: () => bossKilled
333
+ },
334
+ PERK_COLLECTOR: {
335
+ id: 'perk_collector',
336
+ name: 'Коллекционер',
337
+ desc: 'Собрать 10 различных навыков',
338
+ icon: '📚',
339
+ unlocked: false,
340
+ check: () => Object.keys(perkLevels).length >= 10
341
+ }
342
+ };
343
+
344
+ let unlockedAchievements = [];
345
+ let enemiesKilled = 0;
346
+ let bossKilled = false;
347
+ let totalDamageDealt = 0;
348
+
349
+ function checkAchievements() {
350
+ Object.values(ACHIEVEMENTS).forEach(achievement => {
351
+ if (!achievement.unlocked && achievement.check()) {
352
+ achievement.unlocked = true;
353
+ unlockedAchievements.push(achievement);
354
+ showAchievementPopup(achievement);
355
+ }
356
+ });
357
+ }
358
+
359
+ function showAchievementPopup(achievement) {
360
+ const popup = document.createElement('div');
361
+ popup.style.cssText = `
362
+ position: fixed;
363
+ top: 20px;
364
+ right: 20px;
365
+ background: rgba(0, 0, 0, 0.8);
366
+ color: white;
367
+ padding: 15px;
368
+ border-radius: 10px;
369
+ border-left: 5px solid gold;
370
+ z-index: 1000;
371
+ animation: slideIn 0.5s ease-out;
372
+ max-width: 300px;
373
+ `;
374
+
375
+ popup.innerHTML = `
376
+ <div style="font-size: 24px; margin-bottom: 5px;">${achievement.icon} ${achievement.name}</div>
377
+ <div style="color: #ccc; font-size: 14px;">${achievement.desc}</div>
378
+ `;
379
+
380
+ document.body.appendChild(popup);
381
+
382
+ setTimeout(() => {
383
+ popup.style.animation = 'slideOut 0.5s ease-in';
384
+ setTimeout(() => {
385
+ if (popup.parentNode) {
386
+ popup.parentNode.removeChild(popup);
387
+ }
388
+ }, 500);
389
+ }, 3000);
390
+ }
391
+
392
+ // Добавляем стили для анимации
393
+ const style = document.createElement('style');
394
+ style.textContent = `
395
+ @keyframes slideIn {
396
+ from {
397
+ transform: translateX(100%);
398
+ opacity: 0;
399
+ }
400
+ to {
401
+ transform: translateX(0);
402
+ opacity: 1;
403
+ }
404
+ }
405
+
406
+ @keyframes slideOut {
407
+ from {
408
+ transform: translateX(0);
409
+ opacity: 1;
410
+ }
411
+ to {
412
+ transform: translateX(100%);
413
+ opacity: 0;
414
+ }
415
+ }
416
+ `;
417
+ document.head.appendChild(style);
418
+
419
+ // ============ БАЗОВЫЕ КОНСТАНТЫ ============
420
+
421
+ // Базовые навыки для каждого класса (открываются на уровне мастерства 1)
422
+ const BASE_PERKS = {
423
+ warrior: [
424
+ { id: "base_toughness", rarity: "common", max: 3, name: "Выносливость", desc: "+10 HP", effect: (m) => { m.hpBonus += 10; } },
425
+ { id: "base_strength", rarity: "common", max: 3, name: "Сила", desc: "+5% урон", effect: (m) => { m.damageMult *= 1.05; } },
426
+ { id: "base_speed", rarity: "common", max: 3, name: "Проворство", desc: "+4% скорость", effect: (m) => { m.speedMult *= 1.04; } },
427
+ { id: "base_defense", rarity: "common", max: 3, name: "Защита", desc: "-5% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.95; } },
428
+ { id: "base_haste", rarity: "common", max: 3, name: "Спешка", desc: "-4% кд", effect: (m) => { m.cooldownMult *= 0.96; } },
429
+ ],
430
+ archer: [
431
+ { id: "base_agility", rarity: "common", max: 3, name: "Ловкость", desc: "+6% скорость", effect: (m) => { m.speedMult *= 1.06; } },
432
+ { id: "base_aim", rarity: "common", max: 3, name: "Прицел", desc: "+4% урон", effect: (m) => { m.damageMult *= 1.04; } },
433
+ { id: "base_quickness", rarity: "common", max: 3, name: "Быстрота", desc: "-5% кд", effect: (m) => { m.cooldownMult *= 0.95; } },
434
+ { id: "base_range", rarity: "common", max: 3, name: "Дальность", desc: "+15 радиус", effect: (m) => { m.rangeBonus += 15; } },
435
+ { id: "base_evasion", rarity: "common", max: 3, name: "Уклонение", desc: "-4% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.96; } },
436
+ ],
437
+ mage: [
438
+ { id: "base_wisdom", rarity: "common", max: 3, name: "Мудрость", desc: "+15 макс маны", effect: (m) => { m.manaBonus += 15; } },
439
+ { id: "base_power", rarity: "common", max: 3, name: "Мощь", desc: "+5% урон", effect: (m) => { m.damageMult *= 1.05; } },
440
+ { id: "base_flow", rarity: "common", max: 3, name: "Поток", desc: "+1 реген маны/с", effect: (m) => { m.manaRegen += 1; } },
441
+ { id: "base_efficiency", rarity: "common", max: 3, name: "Эффективность", desc: "-5% манакост", effect: (m) => { m.manaCostMult *= 0.95; } },
442
+ { id: "base_focus", rarity: "common", max: 3, name: "Фокус", desc: "-4% кд", effect: (m) => { m.cooldownMult *= 0.96; } },
443
+ ],
444
+ acolyte: [
445
+ { id: "base_vitality", rarity: "common", max: 3, name: "Жизненность", desc: "+15 HP", effect: (m) => { m.hpBonus += 15; } },
446
+ { id: "base_healing", rarity: "common", max: 3, name: "Исцеление", desc: "+1 исцеление/пульс", effect: (m) => { m.auraHeal += 1; } },
447
+ { id: "base_protection", rarity: "common", max: 3, name: "Защита", desc: "-5% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.95; } },
448
+ { id: "base_radiance", rarity: "common", max: 3, name: "Сияние", desc: "+4% урон", effect: (m) => { m.damageMult *= 1.04; } },
449
+ { id: "base_presence", rarity: "common", max: 3, name: "Присутствие", desc: "+8 радиус", effect: (m) => { m.rangeBonus += 8; } },
450
+ ],
451
+ };
452
+
453
+ // Per-class perk kits (10 passives, 4 actives) with levels
454
+ const CLASS_KITS = {
455
+ warrior: {
456
+ passives: [
457
+ { id: "iron_skin", rarity: "common", max: 3, name: "Железная кожа", desc: "+20 HP, +10% защита", effect: (m) => { m.hpBonus += 20; m.damageTakenMult *= 0.9; } },
458
+ { id: "battle_fervor", rarity: "uncommon", max: 3, name: "Боевой пыл", desc: "+12% урон", effect: (m) => { m.damageMult *= 1.12; } },
459
+ { id: "blade_mastery", rarity: "rare", max: 3, name: "Мастер клинка", desc: "-10% кд удара", effect: (m) => { m.cooldownMult *= 0.9; } },
460
+ { id: "riposte", rarity: "rare", max: 3, name: "Рипост", desc: "5% шанс доп. удара", effect: (m) => { m.doubleHit += 0.05; } },
461
+ { id: "heavy_guard", rarity: "uncommon", max: 3, name: "Тяжёлый гард", desc: "+15% защита, -5% скорость", effect: (m) => { m.damageTakenMult *= 0.85; m.speedMult *= 0.95; } },
462
+ { id: "bloodlust", rarity: "uncommon", max: 3, name: "Кровожадность", desc: "Лечение 2 HP за убийство", effect: (m) => { m.killHeal += 2; } },
463
+ { id: "sweeping_edge", rarity: "rare", max: 3, name: "Режущий вихрь", desc: "+12 радиус удара", effect: (m) => { m.rangeBonus += 12; } },
464
+ { id: "adamant", rarity: "epic", max: 1, name: "Непреклонный", desc: "Иммунитет к отбрасыванию (флаг)", effect: () => {} },
465
+ { id: "momentum", rarity: "uncommon", max: 3, name: "Импульс", desc: "+8% скорость", effect: (m) => { m.speedMult *= 1.08; } },
466
+ { id: "war_banner", rarity: "rare", max: 3, name: "Знамя войны", desc: "Доп. 6% урона и 6% кд", effect: (m) => { m.damageMult *= 1.06; m.cooldownMult *= 0.94; } },
467
+ ],
468
+ actives: [
469
+ { id: "blade_dash", rarity: "uncommon", max: 3, name: "Рывок клинка", desc: "Скорость +15%", effect: (m) => { m.speedMult *= 1.15; } },
470
+ { id: "shockwave", rarity: "rare", max: 3, name: "Ударная волна", desc: "Урон +15% в ближнем бою", effect: (m) => { m.damageMult *= 1.15; } },
471
+ { id: "guard_stance", rarity: "uncommon", max: 3, name: "Стойка защиты", desc: "-12% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.88; } },
472
+ { id: "whirlwind", rarity: "epic", max: 3, name: "Вихрь", desc: "Шанс доп. удара +8%", effect: (m) => { m.doubleHit += 0.08; } },
473
+ ],
474
+ },
475
+ archer: {
476
+ passives: [
477
+ { id: "quickdraw", rarity: "uncommon", max: 3, name: "Быстрый выстрел", desc: "-12% кд", effect: (m) => { m.cooldownMult *= 0.88; } },
478
+ { id: "light_steps", rarity: "uncommon", max: 3, name: "Лёгкие шаги", desc: "+12% скорость", effect: (m) => { m.speedMult *= 1.12; } },
479
+ { id: "piercing_arrows", rarity: "rare", max: 3, name: "Пробитие", desc: "Стрелы проходят врагов (флаг)", effect: (m) => { m.pierce += 1; } },
480
+ { id: "hunter_focus", rarity: "rare", max: 3, name: "Фокус охотника", desc: "+14% урон", effect: (m) => { m.damageMult *= 1.14; } },
481
+ { id: "ricochet", rarity: "epic", max: 3, name: "Рикошет", desc: "5% шанс рикошета", effect: (m) => { m.ricochet += 0.05; } },
482
+ { id: "wind_runner", rarity: "uncommon", max: 3, name: "Бег по ветру", desc: "Скорость +8% и +5% урон", effect: (m) => { m.speedMult *= 1.08; m.damageMult *= 1.05; } },
483
+ { id: "bleeding_edge", rarity: "rare", max: 3, name: "Кровоточащий", desc: "Накладывает 4 доп. урона", effect: (m) => { m.flatDamage += 4; } },
484
+ { id: "camo", rarity: "uncommon", max: 3, name: "Маскировка", desc: "-8% получаемого урона", effect: (m) => { m.damageTakenMult *= 0.92; } },
485
+ { id: "falcon_eye", rarity: "rare", max: 3, name: "Глаз ястреба", desc: "+30 радиус", effect: (m) => { m.rangeBonus += 30; } },
486
+ { id: "arrow_surge", rarity: "rare", max: 3, name: "Сурж", desc: "Кд -6%, урон +6%", effect: (m) => { m.cooldownMult *= 0.94; m.damageMult *= 1.06; } },
487
+ ],
488
+ actives: [
489
+ { id: "power_shot", rarity: "rare", max: 3, name: "Силовой выстрел", desc: "+18% урон", effect: (m) => { m.damageMult *= 1.18; } },
490
+ { id: "volley", rarity: "epic", max: 3, name: "Залп", desc: "Доп. снаряд (пирсинг)", effect: (m) => { m.extraProjectiles += 1; } },
491
+ { id: "evasive_roll", rarity: "uncommon", max: 3, name: "Кувырок", desc: "+10% скорость", effect: (m) => { m.speedMult *= 1.1; } },
492
+ { id: "marked_target", rarity: "rare", max: 3, name: "Метка", desc: "+12% урон по ближ. цели", effect: (m) => { m.damageMult *= 1.12; } },
493
+ ],
494
+ },
495
+ mage: {
496
+ passives: [
497
+ { id: "ember_focus", rarity: "rare", max: 3, name: "Жар", desc: "+16% урон огнём", effect: (m) => { m.damageMult *= 1.16; m.burn += 4; } },
498
+ { id: "mana_font", rarity: "uncommon", max: 3, name: "Источник маны", desc: "+30 макс маны, +1.5/с реген", effect: (m) => { m.manaBonus += 30; m.manaRegen += 1.5; } },
499
+ { id: "arcane_precision", rarity: "uncommon", max: 3, name: "Точность", desc: "-10% кд", effect: (m) => { m.cooldownMult *= 0.9; } },
500
+ { id: "frost_weave", rarity: "rare", max: 3, name: "Мороз", desc: "Замедляет врагов при попадании", effect: (m) => { m.slowOnHit = true; } },
501
+ { id: "overload", rarity: "epic", max: 3, name: "Перегруз", desc: "+20% урон, +10% манакост", effect: (m) => { m.damageMult *= 1.2; m.manaCostMult *= 1.1; } },
502
+ { id: "runic_shield", rarity: "uncommon", max: 3, name: "Рун. щит", desc: "-10% вход. урон", effect: (m) => { m.damageTakenMult *= 0.9; } },
503
+ { id: "elemental_sync", rarity: "rare", max: 3, name: "Синхрония", desc: "-12% манакост", effect: (m) => { m.manaCostMult *= 0.88; } },
504
+ { id: "astral_echo", rarity: "epic", max: 3, name: "Астральный эхо", desc: "5% шанс двойного шара", effect: (m) => { m.doubleHit += 0.05; } },
505
+ { id: "channeling", rarity: "uncommon", max: 3, name: "Канал", desc: "+2.5 реген маны", effect: (m) => { m.manaRegen += 2.5; } },
506
+ { id: "mystic_amp", rarity: "epic", max: 3, name: "Усиление", desc: "+18% урон", effect: (m) => { m.damageMult *= 1.18; } },
507
+ ],
508
+ actives: [
509
+ { id: "arcane_comet", rarity: "rare", max: 3, name: "Комета", desc: "Больший AoE", effect: (m) => { m.splashBonus += 18; } },
510
+ { id: "flame_burst", rarity: "uncommon", max: 3, name: "Вспышка", desc: "Манакост -10%", effect: (m) => { m.manaCostMult *= 0.9; } },
511
+ { id: "frost_nova", rarity: "rare", max: 3, name: "Фрост нова", desc: "Замедление сильнее", effect: (m) => { m.slowStrength += 0.2; } },
512
+ { id: "mana_barrier", rarity: "rare", max: 3, name: "Барьер", desc: "+12% защита", effect: (m) => { m.damageTakenMult *= 0.88; } },
513
+ ],
514
+ },
515
+ acolyte: {
516
+ passives: [
517
+ { id: "steadfast", rarity: "common", max: 3, name: "Стойкость", desc: "+25 HP", effect: (m) => { m.hpBonus += 25; } },
518
+ { id: "radiant_armor", rarity: "uncommon", max: 3, name: "Сияющая броня", desc: "-12% вход. урон", effect: (m) => { m.damageTakenMult *= 0.88; } },
519
+ { id: "soothing_light", rarity: "uncommon", max: 3, name: "Утешение", desc: "+2 исцеление/пульс", effect: (m) => { m.auraHeal += 2; } },
520
+ { id: "devotion", rarity: "rare", max: 3, name: "Преданность", desc: "+10% урон ауры", effect: (m) => { m.damageMult *= 1.1; } },
521
+ { id: "sanctuary", rarity: "rare", max: 3, name: "Святыня", desc: "+12 радиус ауры", effect: (m) => { m.rangeBonus += 12; } },
522
+ { id: "spirit_chain", rarity: "uncommon", max: 3, name: "Цепь духа", desc: "Лечение за убийство 3 HP", effect: (m) => { m.killHeal += 3; } },
523
+ { id: "warding", rarity: "uncommon", max: 3, name: "Оберег", desc: "-8% кд", effect: (m) => { m.cooldownMult *= 0.92; } },
524
+ { id: "resolve", rarity: "uncommon", max: 3, name: "Решимость", desc: "+8% скорость", effect: (m) => { m.speedMult *= 1.08; } },
525
+ { id: "holy_edge", rarity: "rare", max: 3, name: "Святая грань", desc: "+10% урон", effect: (m) => { m.damageMult *= 1.1; } },
526
+ { id: "blessing", rarity: "epic", max: 3, name: "Благословение", desc: "+3 исцеление при ударе", effect: (m) => { m.onHitHeal += 3; } },
527
+ ],
528
+ actives: [
529
+ { id: "healing_wave", rarity: "rare", max: 3, name: "Волна исцел.", desc: "+3 исцеление/пульс", effect: (m) => { m.auraHeal += 3; } },
530
+ { id: "smite", rarity: "rare", max: 3, name: "Кара", desc: "+15% урон", effect: (m) => { m.damageMult *= 1.15; } },
531
+ { id: "consecrate", rarity: "rare", max: 3, name: "Освящение", desc: "+14 радиус", effect: (m) => { m.rangeBonus += 14; } },
532
+ { id: "guardian_aura", rarity: "epic", max: 3, name: "Аура стража", desc: "-10% вход. урон", effect: (m) => { m.damageTakenMult *= 0.9; } },
533
+ ],
534
+ },
535
+ };
536
+
537
+ function getPerkDef(id) {
538
+ if (!classKey) return null;
539
+ const kit = CLASS_KITS[classKey];
540
+ const basePerks = BASE_PERKS[classKey] || [];
541
+ if (!kit) return null;
542
+ return [...basePerks, ...kit.passives, ...kit.actives].find((p) => p.id === id) || null;
543
+ }
544
+
545
+ function rarityLabel(r) {
546
+ const map = {
547
+ common: "Обычный",
548
+ uncommon: "Необычный",
549
+ rare: "Редкий",
550
+ unique: "Уникальный",
551
+ epic: "Мифический",
552
+ legendary: "Легендарный",
553
+ divine: "Божественный",
554
+ };
555
+ return map[r] || "Обычный";
556
+ }
557
+
558
+ const CLASS_DEFS = {
559
+ warrior: {
560
+ name: "Warrior",
561
+ color: "#6cf1ff",
562
+ hp: 140,
563
+ mana: 0,
564
+ manaRegen: 0,
565
+ speed: 210,
566
+ weapon: "sword",
567
+ proficiency: "Владение Мечом",
568
+ },
569
+ archer: {
570
+ name: "Archer",
571
+ color: "#f4d35e",
572
+ hp: 110,
573
+ mana: 20,
574
+ manaRegen: 1,
575
+ speed: 240,
576
+ weapon: "bow",
577
+ proficiency: "Владение Луком",
578
+ },
579
+ mage: {
580
+ name: "Mage",
581
+ color: "#c9b4ff",
582
+ hp: 100,
583
+ mana: 120,
584
+ manaRegen: 6,
585
+ speed: 225,
586
+ weapon: "orb",
587
+ proficiency: "Стихийная Магия",
588
+ },
589
+ acolyte: {
590
+ name: "Acolyte",
591
+ color: "#9be7ff",
592
+ hp: 150,
593
+ mana: 60,
594
+ manaRegen: 3,
595
+ speed: 205,
596
+ weapon: "aura",
597
+ proficiency: "Сакральная Магия",
598
+ },
599
+ };
600
+
601
+ function baseModifiers() {
602
+ return {
603
+ hpBonus: 0,
604
+ damageMult: 1,
605
+ cooldownMult: 1,
606
+ rangeBonus: 0,
607
+ speedMult: 1,
608
+ manaBonus: 0,
609
+ manaRegen: 0,
610
+ manaCostMult: 1,
611
+ damageTakenMult: 1,
612
+ doubleHit: 0,
613
+ killHeal: 0,
614
+ flatDamage: 0,
615
+ pierce: 0,
616
+ ricochet: 0,
617
+ extraProjectiles: 0,
618
+ splashBonus: 0,
619
+ burn: 0,
620
+ slowOnHit: false,
621
+ slowStrength: 0.25,
622
+ auraHeal: 0,
623
+ onHitHeal: 0,
624
+ // Божественные навыки
625
+ divineStrike: false,
626
+ divineStrikeCooldown: 0,
627
+ divineStrikeReady: false,
628
+ celestialShield: false,
629
+ celestialShieldCooldown: 0,
630
+ celestialShieldActive: false,
631
+ timeDilation: false,
632
+ timeDilationCooldown: 0,
633
+ timeDilationActive: false,
634
+ phoenixRebirth: false,
635
+ phoenixRebirthCooldown: 0,
636
+ voidStep: false,
637
+ voidStepCooldown: 0,
638
+ divineFury: false,
639
+ divineFuryCooldown: 0,
640
+ divineFuryActive: false,
641
+ eternalFlame: false,
642
+ eternalFlameCooldown: 0,
643
+ starfall: false,
644
+ starfallCooldown: 0,
645
+ divineGrace: false,
646
+ divineGraceCooldown: 0,
647
+ cosmicAwareness: false,
648
+ cosmicAwarenessCooldown: 0,
649
+ cosmicAwarenessActive: false,
650
+ cosmicAwarenessTimer: 0,
651
+ timeDilationTimer: 0,
652
+ };
653
+ }
654
+
655
+ class Player {
656
+ constructor(x, y, color, hp, speed, mana, manaRegen) {
657
+ this.x = x;
658
+ this.y = y;
659
+ this.size = 32;
660
+ this.color = color;
661
+ this.maxHp = hp;
662
+ this.hp = hp;
663
+ this.baseSpeed = speed;
664
+ this.mana = mana;
665
+ this.maxMana = mana;
666
+ this.manaRegen = manaRegen;
667
+ this.level = 1;
668
+ this.xp = 0;
669
+ this.xpToNext = 40;
670
+ this.hurtTimer = 0;
671
+ }
672
+
673
+ update(delta, mods) {
674
+ let dx = 0;
675
+ let dy = 0;
676
+ if (keys["ArrowUp"] || keys["KeyW"]) dy -= 1;
677
+ if (keys["ArrowDown"] || keys["KeyS"]) dy += 1;
678
+ if (keys["ArrowLeft"] || keys["KeyA"]) dx -= 1;
679
+ if (keys["ArrowRight"] || keys["KeyD"]) dx += 1;
680
+
681
+ if (dx !== 0 || dy !== 0) {
682
+ const len = Math.hypot(dx, dy);
683
+ dx /= len;
684
+ dy /= len;
685
+ this.x += dx * this.baseSpeed * mods.speedMult * delta;
686
+ this.y += dy * this.baseSpeed * mods.speedMult * delta;
687
+ }
688
+
689
+ this.x = Math.max(this.size / 2, Math.min(canvas.width - this.size / 2, this.x));
690
+ this.y = Math.max(this.size / 2, Math.min(canvas.height - this.size / 2, this.y));
691
+
692
+ if (this.hurtTimer > 0) this.hurtTimer -= delta;
693
+
694
+ // mana regen
695
+ this.mana = Math.min(this.maxMana + mods.manaBonus, this.mana + (this.manaRegen + mods.manaRegen) * delta);
696
+
697
+ // Божественные навыки - обновление кд
698
+ if (mods.divineStrike) {
699
+ mods.divineStrikeCooldown = Math.max(0, mods.divineStrikeCooldown - delta);
700
+ mods.divineStrikeReady = mods.divineStrikeCooldown <= 0;
701
+ }
702
+ if (mods.celestialShield) {
703
+ mods.celestialShieldCooldown = Math.max(0, mods.celestialShieldCooldown - delta);
704
+ }
705
+ if (mods.timeDilation) {
706
+ mods.timeDilationCooldown = Math.max(0, mods.timeDilationCooldown - delta);
707
+ if (mods.timeDilationActive) {
708
+ mods.timeDilationActive = false;
709
+ mods.cooldownMult /= 0.5; // Возврат к нормальному кд
710
+ }
711
+ }
712
+ if (mods.phoenixRebirth) {
713
+ mods.phoenixRebirthCooldown = Math.max(0, mods.phoenixRebirthCooldown - delta);
714
+ }
715
+ if (mods.voidStep) {
716
+ mods.voidStepCooldown = Math.max(0, mods.voidStepCooldown - delta);
717
+ }
718
+ if (mods.divineFury) {
719
+ mods.divineFuryCooldown = Math.max(0, mods.divineFuryCooldown - delta);
720
+ if (mods.divineFuryActive && this.hp >= this.maxHp * 0.3) {
721
+ mods.divineFuryActive = false;
722
+ mods.damageMult /= 1.4;
723
+ mods.speedMult /= 1.25;
724
+ }
725
+ }
726
+ if (mods.eternalFlame) {
727
+ mods.eternalFlameCooldown = Math.max(0, mods.eternalFlameCooldown - delta);
728
+ }
729
+ if (mods.starfall) {
730
+ mods.starfallCooldown = Math.max(0, mods.starfallCooldown - delta);
731
+ }
732
+ if (mods.divineGrace) {
733
+ mods.divineGraceCooldown = Math.max(0, mods.divineGraceCooldown - delta);
734
+ }
735
+ if (mods.cosmicAwareness) {
736
+ mods.cosmicAwarenessCooldown = Math.max(0, mods.cosmicAwarenessCooldown - delta);
737
+ if (mods.cosmicAwarenessActive) {
738
+ mods.cosmicAwarenessActive = false;
739
+ mods.damageMult /= 1.2;
740
+ }
741
+ }
742
+ }
743
+
744
+ giveXp(amount) {
745
+ this.xp += amount;
746
+ if (this.xp >= this.xpToNext) {
747
+ this.xp -= this.xpToNext;
748
+ this.level += 1;
749
+ this.xpToNext = Math.round(this.xpToNext * 1.25 + 15);
750
+ openLevelUp();
751
+ }
752
+ }
753
+
754
+ takeDamage(amount, mods) {
755
+ // Небесный щит
756
+ if (mods.celestialShield && mods.celestialShieldCooldown <= 0 && !mods.celestialShieldActive) {
757
+ mods.celestialShieldActive = true;
758
+ mods.celestialShieldCooldown = mods.celestialShieldCooldown || 12;
759
+ return; // Блокирует урон
760
+ }
761
+ if (mods.celestialShieldActive) {
762
+ mods.celestialShieldActive = false;
763
+ return;
764
+ }
765
+
766
+ // Возрождение феникса
767
+ if (this.hp - amount * mods.damageTakenMult <= 0 && mods.phoenixRebirth && mods.phoenixRebirthCooldown <= 0) {
768
+ this.hp = Math.max(1, this.maxHp * 0.5);
769
+ mods.phoenixRebirthCooldown = mods.phoenixRebirthCooldown || 60;
770
+ return;
771
+ }
772
+
773
+ this.hp -= amount * mods.damageTakenMult;
774
+ this.hurtTimer = 0.25;
775
+
776
+ // Божественная ярость
777
+ if (this.hp < this.maxHp * 0.3 && mods.divineFury && mods.divineFuryCooldown <= 0 && !mods.divineFuryActive) {
778
+ mods.divineFuryActive = true;
779
+ mods.divineFuryCooldown = mods.divineFuryCooldown || 20;
780
+ mods.damageMult *= 1.4;
781
+ mods.speedMult *= 1.25;
782
+ }
783
+ }
784
+
785
+ draw(context) {
786
+ context.fillStyle = this.hurtTimer > 0 ? "#ff7b7b" : this.color;
787
+ context.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
788
+ }
789
+ }
790
+
791
+ class Proficiency {
792
+ constructor(name) {
793
+ this.name = name;
794
+ this.currentLevel = 0;
795
+ this.exp = 0;
796
+ this.expToNextLevel = 25;
797
+ this.unlockedPerkIds = []; // ID навыков, открытых через мастерство
798
+ }
799
+
800
+ getModifiers() {
801
+ // Базовые модификаторы от уровня мастерства (минимальные)
802
+ const dmg = 1 + this.currentLevel * 0.03;
803
+ const cd = Math.max(0.85, 1 - this.currentLevel * 0.01);
804
+ const range = this.currentLevel * 1;
805
+ return { damageMult: dmg, cooldownMult: cd, rangeBonus: range };
806
+ }
807
+
808
+ addExp(amount) {
809
+ this.exp += amount;
810
+ while (this.exp >= this.expToNextLevel) {
811
+ this.exp -= this.expToNextLevel;
812
+ this.currentLevel += 1;
813
+ console.log(`[${this.name}] уровень повышен до ${this.currentLevel}!`);
814
+ this.expToNextLevel = Math.round(this.expToNextLevel * 1.35 + 8);
815
+ this.unlockRandomPerk();
816
+ refreshSkillIcons();
817
+ }
818
+ }
819
+
820
+ unlockRandomPerk() {
821
+ if (!classKey) return;
822
+
823
+ // На 1 уровне мастерства открываем все базовые навыки
824
+ if (this.currentLevel === 1) {
825
+ const basePerks = BASE_PERKS[classKey] || [];
826
+ basePerks.forEach(perk => {
827
+ if (!this.unlockedPerkIds.includes(perk.id)) {
828
+ this.unlockedPerkIds.push(perk.id);
829
+ console.log(`[${this.name}] открыт базовый навык: ${perk.name}`);
830
+ }
831
+ });
832
+ } else {
833
+ // На остальных уровнях - случайный навык из основного пула класса
834
+ const kit = CLASS_KITS[classKey];
835
+ const allPerks = [...kit.passives, ...kit.actives];
836
+ const available = allPerks.filter(p => !this.unlockedPerkIds.includes(p.id));
837
+ if (available.length > 0) {
838
+ const randomPerk = available[Math.floor(Math.random() * available.length)];
839
+ this.unlockedPerkIds.push(randomPerk.id);
840
+ console.log(`[${this.name}] уровень ${this.currentLevel}: открыт навык ${randomPerk.name} (${randomPerk.rarity || 'common'})`);
841
+ } else {
842
+ console.log(`[${this.name}] все навыки уже открыты!`);
843
+ }
844
+ }
845
+ }
846
+ }
847
+
848
+ class Weapon {
849
+ constructor(player, proficiency, mods) {
850
+ this.player = player;
851
+ this.proficiency = proficiency;
852
+ this.mods = mods;
853
+ }
854
+ update() {}
855
+ draw() {}
856
+ syncMods(mods) {
857
+ this.mods = mods;
858
+ }
859
+ }
860
+
861
+ class SwordWeapon extends Weapon {
862
+ constructor(player, proficiency, mods) {
863
+ super(player, proficiency, mods);
864
+ this.baseRange = 52;
865
+ this.baseDamage = 16;
866
+ this.baseCooldown = 0.6;
867
+ this.timer = 0;
868
+ this.swingMarker = 0;
869
+ this.comboVisualTimer = 0;
870
+ }
871
+
872
+ update(enemies, delta) {
873
+ this.timer += delta;
874
+ const prof = this.proficiency.getModifiers();
875
+ const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
876
+ const range = this.baseRange + prof.rangeBonus + this.mods.rangeBonus;
877
+
878
+ // Визуальный таймер комбо
879
+ if (this.comboVisualTimer > 0) {
880
+ this.comboVisualTimer -= delta;
881
+ }
882
+
883
+ if (this.timer < cooldown) return;
884
+
885
+ const target = findClosestEnemy(this.player, enemies, range);
886
+ if (!target) return;
887
+
888
+ let damage = (this.baseDamage + this.mods.flatDamage) * prof.damageMult * this.mods.damageMult;
889
+
890
+ // Применяем множитель комбо (только для воина)
891
+ if (classKey === 'warrior') {
892
+ damage *= comboSystem.getMultiplier();
893
+ const multiplier = comboSystem.addHit();
894
+
895
+ // Визуальный эффект при увеличении комбо
896
+ if (comboSystem.getComboHits() === 3) {
897
+ this.comboVisualTimer = 0.3;
898
+ comboNumbers.push(new FloatingText(
899
+ this.player.x,
900
+ this.player.y - 40,
901
+ `COMBO x${comboSystem.combo}!`,
902
+ '#ffd166',
903
+ true,
904
+ 20
905
+ ));
906
+ }
907
+ }
908
+
909
+ // Божественный удар
910
+ if (this.mods.divineStrike && this.mods.divineStrikeReady) {
911
+ damage *= 3;
912
+ this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
913
+ this.mods.divineStrikeReady = false;
914
+ }
915
+
916
+ target.takeDamage(damage);
917
+
918
+ // Добавляем всплывающий текст урона
919
+ const isCrit = classKey === 'warrior' && comboSystem.getMultiplier() > 1.2;
920
+ damageNumbers.push(new FloatingText(
921
+ target.x,
922
+ target.y,
923
+ Math.round(damage),
924
+ '#ff7b7b',
925
+ isCrit
926
+ ));
927
+
928
+ if (Math.random() < this.mods.doubleHit) {
929
+ const extraDamage = damage * 0.6;
930
+ target.takeDamage(extraDamage);
931
+ damageNumbers.push(new FloatingText(
932
+ target.x,
933
+ target.y - 15,
934
+ Math.round(extraDamage),
935
+ '#ffaa44',
936
+ false
937
+ ));
938
+ }
939
+
940
+ this.timer = 0;
941
+ this.swingMarker = 0.2;
942
+ this.proficiency.addExp(6);
943
+
944
+ if (this.mods.onHitHeal) {
945
+ this.player.hp = Math.min(this.player.maxHp + this.mods.hpBonus, this.player.hp + this.mods.onHitHeal);
946
+ healNumbers.push(new FloatingText(
947
+ this.player.x,
948
+ this.player.y - 20,
949
+ '+' + this.mods.onHitHeal,
950
+ '#54e894',
951
+ false
952
+ ));
953
+ }
954
+ }
955
+
956
+ draw(context) {
957
+ if (this.swingMarker > 0) {
958
+ const prof = this.proficiency.getModifiers();
959
+ const range = this.baseRange + prof.rangeBonus + this.mods.rangeBonus;
960
+ context.strokeStyle = "rgba(255,255,255,0.35)";
961
+ context.beginPath();
962
+ context.arc(this.player.x, this.player.y, range, 0, Math.PI * 2);
963
+ context.stroke();
964
+ this.swingMarker -= 1 / 60;
965
+ }
966
+
967
+ // Визуализация комбо для воина
968
+ if (classKey === 'warrior' && this.comboVisualTimer > 0) {
969
+ const alpha = this.comboVisualTimer / 0.3;
970
+ const radius = 60 + comboSystem.combo * 5;
971
+
972
+ context.strokeStyle = `rgba(255, 209, 102, ${alpha})`;
973
+ context.lineWidth = 3;
974
+ context.beginPath();
975
+ context.arc(this.player.x, this.player.y, radius, 0, Math.PI * 2);
976
+ context.stroke();
977
+
978
+ // Прогресс комбо
979
+ const progress = comboSystem.getComboProgress();
980
+ if (progress > 0) {
981
+ context.fillStyle = `rgba(255, 209, 102, ${alpha * 0.3})`;
982
+ context.beginPath();
983
+ context.moveTo(this.player.x, this.player.y);
984
+ context.arc(this.player.x, this.player.y, 40, -Math.PI/2, -Math.PI/2 + progress * Math.PI * 2);
985
+ context.closePath();
986
+ context.fill();
987
+ }
988
+ }
989
+ }
990
+ }
991
+
992
+ class BowWeapon extends Weapon {
993
+ constructor(player, proficiency, mods) {
994
+ super(player, proficiency, mods);
995
+ this.baseRange = 420;
996
+ this.baseDamage = 10;
997
+ this.baseCooldown = 0.55;
998
+ this.projectiles = [];
999
+ this.timer = 0;
1000
+ }
1001
+
1002
+ update(enemies, delta) {
1003
+ this.timer += delta;
1004
+ const prof = this.proficiency.getModifiers();
1005
+ const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
1006
+ if (this.timer >= cooldown) {
1007
+ const target = findClosestEnemy(this.player, enemies, this.baseRange + this.mods.rangeBonus);
1008
+ if (target) {
1009
+ const angle = Math.atan2(target.y - this.player.y, target.x - this.player.x);
1010
+ const speed = 360;
1011
+ const count = 1 + this.mods.extraProjectiles;
1012
+ for (let i = 0; i < count; i++) {
1013
+ const spread = (i - (count - 1) / 2) * 0.12;
1014
+ let damage = (this.baseDamage + this.mods.flatDamage) * prof.damageMult * this.mods.damageMult;
1015
+
1016
+ if (this.mods.divineStrike && this.mods.divineStrikeReady) {
1017
+ damage *= 3;
1018
+ this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
1019
+ this.mods.divineStrikeReady = false;
1020
+ }
1021
+ this.projectiles.push({
1022
+ x: this.player.x,
1023
+ y: this.player.y,
1024
+ vx: Math.cos(angle + spread) * speed,
1025
+ vy: Math.sin(angle + spread) * speed,
1026
+ life: 1.2,
1027
+ radius: 5,
1028
+ damage: damage,
1029
+ pierce: this.mods.pierce,
1030
+ });
1031
+ }
1032
+ this.timer = 0;
1033
+ this.proficiency.addExp(4);
1034
+ }
1035
+ }
1036
+
1037
+ for (let i = this.projectiles.length - 1; i >= 0; i--) {
1038
+ const p = this.projectiles[i];
1039
+ p.x += p.vx * delta;
1040
+ p.y += p.vy * delta;
1041
+ p.life -= delta;
1042
+
1043
+ for (const enemy of enemies) {
1044
+ if (Math.hypot(enemy.x - p.x, enemy.y - p.y) < enemy.radius + p.radius) {
1045
+ enemy.takeDamage(p.damage);
1046
+ damageNumbers.push(new FloatingText(
1047
+ enemy.x,
1048
+ enemy.y - 10,
1049
+ Math.round(p.damage),
1050
+ '#f4d35e',
1051
+ false
1052
+ ));
1053
+
1054
+ if (this.mods.ricochet > 0 && Math.random() < this.mods.ricochet) {
1055
+ const target = findClosestEnemy(enemy, enemies, 200);
1056
+ if (target) {
1057
+ p.vx = (target.x - enemy.x) * 3;
1058
+ p.vy = (target.y - enemy.y) * 3;
1059
+ p.life = 0.4;
1060
+ continue;
1061
+ }
1062
+ }
1063
+ if (p.pierce > 0) {
1064
+ p.pierce -= 1;
1065
+ } else {
1066
+ this.projectiles.splice(i, 1);
1067
+ }
1068
+ break;
1069
+ }
1070
+ }
1071
+ if (p.life <= 0) this.projectiles.splice(i, 1);
1072
+ }
1073
+ }
1074
+
1075
+ draw(context) {
1076
+ context.fillStyle = "#f4d35e";
1077
+ this.projectiles.forEach((p) => {
1078
+ context.beginPath();
1079
+ context.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
1080
+ context.fill();
1081
+ });
1082
+ }
1083
+ }
1084
+
1085
+ class OrbWeapon extends Weapon {
1086
+ constructor(player, proficiency, mods) {
1087
+ super(player, proficiency, mods);
1088
+ this.baseCooldown = 0.8;
1089
+ this.baseDamage = 18;
1090
+ this.timer = 0;
1091
+ this.projectiles = [];
1092
+ this.manaCost = 10;
1093
+ this.activeProjectiles = new Map(); // projectileId -> target
1094
+ }
1095
+
1096
+ update(enemies, delta) {
1097
+ this.timer += delta;
1098
+ const prof = this.proficiency.getModifiers();
1099
+ const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
1100
+ const cost = this.manaCost * this.mods.manaCostMult;
1101
+
1102
+ if (this.timer >= cooldown) {
1103
+ // Используем умное наведение для выбора цели
1104
+ let target = smartTargeting.getBestTarget(enemies, this.player.x, this.player.y, 520 + this.mods.rangeBonus);
1105
+
1106
+ if (target && this.player.mana >= cost) {
1107
+ this.player.mana -= cost;
1108
+ const angle = Math.atan2(target.y - this.player.y, target.x - this.player.x);
1109
+ const speed = 260;
1110
+ let damage = this.baseDamage * prof.damageMult * this.mods.damageMult;
1111
+
1112
+ if (this.mods.divineStrike && this.mods.divineStrikeReady) {
1113
+ damage *= 3;
1114
+ this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
1115
+ this.mods.divineStrikeReady = false;
1116
+ }
1117
+
1118
+ const projectileId = smartTargeting.assignTarget(target);
1119
+ this.projectiles.push({
1120
+ id: projectileId,
1121
+ x: this.player.x,
1122
+ y: this.player.y,
1123
+ vx: Math.cos(angle) * speed,
1124
+ vy: Math.sin(angle) * speed,
1125
+ life: 1.6,
1126
+ radius: 10,
1127
+ damage: damage,
1128
+ splash: 32 + prof.rangeBonus + this.mods.rangeBonus + this.mods.splashBonus,
1129
+ target: target,
1130
+ originalTarget: target
1131
+ });
1132
+
1133
+ this.timer = 0;
1134
+ this.proficiency.addExp(5);
1135
+ }
1136
+ }
1137
+
1138
+ for (let i = this.projectiles.length - 1; i >= 0; i--) {
1139
+ const p = this.projectiles[i];
1140
+ p.x += p.vx * delta;
1141
+ p.y += p.vy * delta;
1142
+ p.life -= delta;
1143
+
1144
+ // Проверяем, жив ли исходный целевой враг
1145
+ if (p.target && p.target.isDead()) {
1146
+ // Ищем новую цель, исключая текущую
1147
+ p.target = smartTargeting.getBestTarget(enemies, p.x, p.y, 200, p.originalTarget);
1148
+
1149
+ if (p.target) {
1150
+ // Пересчитываем направление к новой цели
1151
+ const angle = Math.atan2(p.target.y - p.y, p.target.x - p.x);
1152
+ const speed = Math.hypot(p.vx, p.vy);
1153
+ p.vx = Math.cos(angle) * speed;
1154
+ p.vy = Math.sin(angle) * speed;
1155
+ }
1156
+ }
1157
+
1158
+ let hit = false;
1159
+ for (const enemy of enemies) {
1160
+ if (Math.hypot(enemy.x - p.x, enemy.y - p.y) < enemy.radius + p.radius) {
1161
+ hit = true;
1162
+ for (const aoe of enemies) {
1163
+ if (Math.hypot(aoe.x - p.x, aoe.y - p.y) <= p.splash) {
1164
+ let dmg = p.damage;
1165
+ if (this.mods.burn) dmg += this.mods.burn;
1166
+ aoe.takeDamage(dmg);
1167
+ damageNumbers.push(new FloatingText(
1168
+ aoe.x,
1169
+ aoe.y,
1170
+ Math.round(dmg),
1171
+ '#c9b4ff',
1172
+ this.mods.burn > 0
1173
+ ));
1174
+ if (this.mods.slowOnHit) aoe.slow = this.mods.slowStrength || 0.25;
1175
+ }
1176
+ }
1177
+
1178
+ // Освобождаем цель
1179
+ if (p.originalTarget) {
1180
+ smartTargeting.releaseTarget(p.originalTarget, p.id);
1181
+ }
1182
+
1183
+ if (this.mods.doubleHit && Math.random() < this.mods.doubleHit) {
1184
+ p.life = 0.3; // second detonation soon
1185
+ } else {
1186
+ break;
1187
+ }
1188
+ }
1189
+ }
1190
+ if (hit || p.life <= 0) {
1191
+ if (p.originalTarget) {
1192
+ smartTargeting.releaseTarget(p.originalTarget, p.id);
1193
+ }
1194
+ this.projectiles.splice(i, 1);
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ draw(context) {
1200
+ context.fillStyle = "#c9b4ff";
1201
+ this.projectiles.forEach((p) => {
1202
+ context.beginPath();
1203
+ context.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
1204
+ context.fill();
1205
+
1206
+ context.strokeStyle = "rgba(201,180,255,0.25)";
1207
+ context.beginPath();
1208
+ context.arc(p.x, p.y, p.splash, 0, Math.PI * 2);
1209
+ context.stroke();
1210
+
1211
+ // Линия к цели (если есть)
1212
+ if (p.target && !p.target.isDead()) {
1213
+ context.strokeStyle = "rgba(255,255,255,0.15)";
1214
+ context.setLineDash([5, 5]);
1215
+ context.beginPath();
1216
+ context.moveTo(p.x, p.y);
1217
+ context.lineTo(p.target.x, p.target.y);
1218
+ context.stroke();
1219
+ context.setLineDash([]);
1220
+ }
1221
+ });
1222
+ }
1223
+ }
1224
+
1225
+ class AuraWeapon extends Weapon {
1226
+ constructor(player, proficiency, mods) {
1227
+ super(player, proficiency, mods);
1228
+ this.baseCooldown = 1.1;
1229
+ this.baseDamage = 10;
1230
+ this.radius = 90;
1231
+ this.timer = 0;
1232
+ }
1233
+
1234
+ update(enemies, delta) {
1235
+ this.timer += delta;
1236
+ const prof = this.proficiency.getModifiers();
1237
+ const cooldown = this.baseCooldown * prof.cooldownMult * this.mods.cooldownMult;
1238
+ if (this.timer >= cooldown) {
1239
+ let damage = this.baseDamage * prof.damageMult * this.mods.damageMult;
1240
+
1241
+ if (this.mods.divineStrike && this.mods.divineStrikeReady) {
1242
+ damage *= 3;
1243
+ this.mods.divineStrikeCooldown = this.mods.divineStrikeCooldown || 8;
1244
+ this.mods.divineStrikeReady = false;
1245
+ }
1246
+ for (const enemy of enemies) {
1247
+ if (Math.hypot(enemy.x - this.player.x, enemy.y - this.player.y) < this.radius + this.mods.rangeBonus + enemy.radius) {
1248
+ enemy.takeDamage(damage);
1249
+ damageNumbers.push(new FloatingText(
1250
+ enemy.x,
1251
+ enemy.y,
1252
+ Math.round(damage),
1253
+ '#9be7ff',
1254
+ false
1255
+ ));
1256
+ }
1257
+ }
1258
+ // heal feedback
1259
+ const heal = 4 + this.mods.auraHeal;
1260
+ this.player.hp = Math.min(this.player.maxHp + this.mods.hpBonus, this.player.hp + heal);
1261
+ if (heal > 0) {
1262
+ healNumbers.push(new FloatingText(
1263
+ this.player.x,
1264
+ this.player.y - 20,
1265
+ '+' + heal,
1266
+ '#54e894',
1267
+ false
1268
+ ));
1269
+ }
1270
+ this.timer = 0;
1271
+ this.proficiency.addExp(4);
1272
+ }
1273
+ }
1274
+
1275
+ draw(context) {
1276
+ context.strokeStyle = "rgba(155, 231, 255, 0.35)";
1277
+ context.beginPath();
1278
+ context.arc(this.player.x, this.player.y, this.radius + this.mods.rangeBonus, 0, Math.PI * 2);
1279
+ context.stroke();
1280
+ }
1281
+ }
1282
+
1283
+ class Enemy {
1284
+ constructor(x, y, waveScale = 1) {
1285
+ this.x = x;
1286
+ this.y = y;
1287
+ this.radius = 16;
1288
+ this.speed = 82;
1289
+ this.baseHp = 28;
1290
+ this.hp = Math.round(this.baseHp * (1 + waveScale * 0.15));
1291
+ this.color = "#f45b69";
1292
+ this.attackCooldown = 0.8;
1293
+ this.timer = Math.random() * 0.5;
1294
+ this.baseTouchDamage = 8;
1295
+ this.touchDamage = Math.round(this.baseTouchDamage * (1 + waveScale * 0.12));
1296
+ this.slow = 0;
1297
+ this.waveScale = waveScale;
1298
+ }
1299
+
1300
+ update(player, delta, mods) {
1301
+ const dx = player.x - this.x;
1302
+ const dy = player.y - this.y;
1303
+ const len = Math.hypot(dx, dy) || 1;
1304
+ const slowMult = this.slow ? 1 - this.slow : 1;
1305
+ this.x += (dx / len) * this.speed * slowMult * delta;
1306
+ this.y += (dy / len) * this.speed * slowMult * delta;
1307
+ if (this.slow) this.slow = Math.max(0, this.slow - delta * 0.6);
1308
+
1309
+ this.timer += delta;
1310
+ const dist = Math.hypot(player.x - this.x, player.y - this.y);
1311
+ if (dist < this.radius + player.size / 2 && this.timer >= this.attackCooldown) {
1312
+ player.takeDamage(this.touchDamage, mods);
1313
+ this.timer = 0;
1314
+ }
1315
+ }
1316
+
1317
+ takeDamage(amount) {
1318
+ this.hp -= amount;
1319
+ totalDamageDealt += amount;
1320
+ }
1321
+
1322
+ isDead() {
1323
+ return this.hp <= 0;
1324
+ }
1325
+
1326
+ draw(context) {
1327
+ context.fillStyle = this.color;
1328
+ context.beginPath();
1329
+ context.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
1330
+ context.fill();
1331
+ }
1332
+ }
1333
+
1334
+ class Boss {
1335
+ constructor(type, wave) {
1336
+ this.x = canvas.width / 2;
1337
+ this.y = canvas.height / 2;
1338
+ this.radius = 40;
1339
+ this.type = type;
1340
+ this.wave = wave;
1341
+ this.maxHp = 800 + wave * 200;
1342
+ this.hp = this.maxHp;
1343
+ this.attackTimer = 0;
1344
+ this.phase = 0;
1345
+ this.projectiles = [];
1346
+ this.color = "#ff4444";
1347
+ this.name = "";
1348
+ this.bossTimeLimit = BOSS_DURATION; // 3 минуты
1349
+ this.bossTimer = 0;
1350
+
1351
+ if (type === 1) {
1352
+ this.name = "Архимаг Хаоса";
1353
+ this.color = "#ff6b9d";
1354
+ } else if (type === 2) {
1355
+ this.name = "Теневой Повелитель";
1356
+ this.color = "#8b4fff";
1357
+ } else {
1358
+ this.name = "Древний Титан";
1359
+ this.color = "#ffaa44";
1360
+ }
1361
+ }
1362
+
1363
+ update(player, delta) {
1364
+ this.bossTimer += delta;
1365
+ if (this.bossTimer >= this.bossTimeLimit) {
1366
+ // Время вышло - игрок проиграл
1367
+ player.hp = 0;
1368
+ return;
1369
+ }
1370
+
1371
+ this.attackTimer += delta;
1372
+ const phase = Math.floor(this.hp / this.maxHp * 3);
1373
+ this.phase = phase;
1374
+
1375
+ if (this.type === 1) {
1376
+ this.updateArchmage(player, delta);
1377
+ } else if (this.type === 2) {
1378
+ this.updateShadowLord(player, delta);
1379
+ } else {
1380
+ this.updateTitan(player, delta);
1381
+ }
1382
+
1383
+ // Обновление снарядов босса
1384
+ for (let i = this.projectiles.length - 1; i >= 0; i--) {
1385
+ const proj = this.projectiles[i];
1386
+ proj.x += proj.vx * delta;
1387
+ proj.y += proj.vy * delta;
1388
+ proj.life -= delta;
1389
+
1390
+ const dist = Math.hypot(player.x - proj.x, player.y - proj.y);
1391
+ if (dist < player.size / 2 + proj.radius) {
1392
+ player.takeDamage(proj.damage, modifiers);
1393
+ this.projectiles.splice(i, 1);
1394
+ } else if (proj.life <= 0) {
1395
+ this.projectiles.splice(i, 1);
1396
+ }
1397
+ }
1398
+ }
1399
+
1400
+ updateArchmage(player, delta) {
1401
+ // Фаза 1: Кольцо огня каждые 2.5 сек
1402
+ if (this.phase >= 2 && this.attackTimer >= 2.5) {
1403
+ this.attackTimer = 0;
1404
+ for (let i = 0; i < 8; i++) {
1405
+ const angle = (Math.PI * 2 / 8) * i;
1406
+ this.projectiles.push({
1407
+ x: this.x,
1408
+ y: this.y,
1409
+ vx: Math.cos(angle) * 120,
1410
+ vy: Math.sin(angle) * 120,
1411
+ radius: 8,
1412
+ damage: 12,
1413
+ life: 3,
1414
+ color: "#ff6b9d"
1415
+ });
1416
+ }
1417
+ }
1418
+ // Фаза 2: Направленные снаряды каждые 1.8 сек
1419
+ if (this.phase >= 1 && this.attackTimer >= 1.8) {
1420
+ this.attackTimer = 0;
1421
+ const angle = Math.atan2(player.y - this.y, player.x - this.x);
1422
+ for (let i = -1; i <= 1; i++) {
1423
+ this.projectiles.push({
1424
+ x: this.x,
1425
+ y: this.y,
1426
+ vx: Math.cos(angle + i * 0.3) * 180,
1427
+ vy: Math.sin(angle + i * 0.3) * 180,
1428
+ radius: 10,
1429
+ damage: 15,
1430
+ life: 4,
1431
+ color: "#ff6b9d"
1432
+ });
1433
+ }
1434
+ }
1435
+ // Фаза 3: Спираль каждые 3 сек
1436
+ if (this.phase === 0 && this.attackTimer >= 3) {
1437
+ this.attackTimer = 0;
1438
+ const baseAngle = Math.atan2(player.y - this.y, player.x - this.x);
1439
+ for (let i = 0; i < 12; i++) {
1440
+ const angle = baseAngle + (Math.PI * 2 / 12) * i;
1441
+ this.projectiles.push({
1442
+ x: this.x,
1443
+ y: this.y,
1444
+ vx: Math.cos(angle) * 100,
1445
+ vy: Math.sin(angle) * 100,
1446
+ radius: 9,
1447
+ damage: 18,
1448
+ life: 5,
1449
+ color: "#ff6b9d"
1450
+ });
1451
+ }
1452
+ }
1453
+ }
1454
+
1455
+ updateShadowLord(player, delta) {
1456
+ // Фаза 1: Теневые клинки каждые 2 сек
1457
+ if (this.phase >= 2 && this.attackTimer >= 2) {
1458
+ this.attackTimer = 0;
1459
+ for (let i = 0; i < 6; i++) {
1460
+ const angle = Math.atan2(player.y - this.y, player.x - this.x) + (i - 2.5) * 0.4;
1461
+ this.projectiles.push({
1462
+ x: this.x,
1463
+ y: this.y,
1464
+ vx: Math.cos(angle) * 200,
1465
+ vy: Math.sin(angle) * 200,
1466
+ radius: 7,
1467
+ damage: 14,
1468
+ life: 3,
1469
+ color: "#8b4fff"
1470
+ });
1471
+ }
1472
+ }
1473
+ // Фаза 2: Взрывающиеся сферы каждые 2.5 сек
1474
+ if (this.phase >= 1 && this.attackTimer >= 2.5) {
1475
+ this.attackTimer = 0;
1476
+ const targets = [
1477
+ { x: player.x, y: player.y },
1478
+ { x: player.x + 100, y: player.y },
1479
+ { x: player.x - 100, y: player.y }
1480
+ ];
1481
+ targets.forEach(target => {
1482
+ const angle = Math.atan2(target.y - this.y, target.x - this.x);
1483
+ this.projectiles.push({
1484
+ x: this.x,
1485
+ y: this.y,
1486
+ vx: Math.cos(angle) * 150,
1487
+ vy: Math.sin(angle) * 150,
1488
+ radius: 12,
1489
+ damage: 20,
1490
+ life: 3,
1491
+ color: "#8b4fff",
1492
+ explode: true,
1493
+ explodeRadius: 80
1494
+ });
1495
+ });
1496
+ }
1497
+ // Фаза 3: Крест теней каждые 3.5 сек
1498
+ if (this.phase === 0 && this.attackTimer >= 3.5) {
1499
+ this.attackTimer = 0;
1500
+ const dirs = [
1501
+ { x: 1, y: 0 }, { x: -1, y: 0 },
1502
+ { x: 0, y: 1 }, { x: 0, y: -1 }
1503
+ ];
1504
+ dirs.forEach(dir => {
1505
+ for (let i = 0; i < 5; i++) {
1506
+ this.projectiles.push({
1507
+ x: this.x + dir.x * i * 30,
1508
+ y: this.y + dir.y * i * 30,
1509
+ vx: dir.x * 140,
1510
+ vy: dir.y * 140,
1511
+ radius: 8,
1512
+ damage: 16,
1513
+ life: 4,
1514
+ color: "#8b4fff"
1515
+ });
1516
+ }
1517
+ });
1518
+ }
1519
+ }
1520
+
1521
+ updateTitan(player, delta) {
1522
+ // Фаза 1: Ударные волны каждые 2.2 сек
1523
+ if (this.phase >= 2 && this.attackTimer >= 2.2) {
1524
+ this.attackTimer = 0;
1525
+ const angle = Math.atan2(player.y - this.y, player.x - this.x);
1526
+ for (let i = -2; i <= 2; i++) {
1527
+ this.projectiles.push({
1528
+ x: this.x,
1529
+ y: this.y,
1530
+ vx: Math.cos(angle + i * 0.25) * 160,
1531
+ vy: Math.sin(angle + i * 0.25) * 160,
1532
+ radius: 15,
1533
+ damage: 22,
1534
+ life: 4,
1535
+ color: "#ffaa44"
1536
+ });
1537
+ }
1538
+ }
1539
+ // Фаза 2: Круговые молнии каждые 2.8 сек
1540
+ if (this.phase >= 1 && this.attackTimer >= 2.8) {
1541
+ this.attackTimer = 0;
1542
+ for (let i = 0; i < 10; i++) {
1543
+ const angle = (Math.PI * 2 / 10) * i;
1544
+ this.projectiles.push({
1545
+ x: this.x + Math.cos(angle) * 60,
1546
+ y: this.y + Math.sin(angle) * 60,
1547
+ vx: Math.cos(angle) * 130,
1548
+ vy: Math.sin(angle) * 130,
1549
+ radius: 11,
1550
+ damage: 19,
1551
+ life: 3.5,
1552
+ color: "#ffaa44"
1553
+ });
1554
+ }
1555
+ }
1556
+ // Фаза 3: Метеоритный дождь каждые 4 сек
1557
+ if (this.phase === 0 && this.attackTimer >= 4) {
1558
+ this.attackTimer = 0;
1559
+ for (let i = 0; i < 8; i++) {
1560
+ const x = Math.random() * canvas.width;
1561
+ this.projectiles.push({
1562
+ x: x,
1563
+ y: -30,
1564
+ vx: (Math.random() - 0.5) * 50,
1565
+ vy: 200,
1566
+ radius: 14,
1567
+ damage: 25,
1568
+ life: 4,
1569
+ color: "#ffaa44"
1570
+ });
1571
+ }
1572
+ }
1573
+ }
1574
+
1575
+ takeDamage(amount) {
1576
+ this.hp -= amount;
1577
+ totalDamageDealt += amount;
1578
+ }
1579
+
1580
+ isDead() {
1581
+ return this.hp <= 0;
1582
+ }
1583
+
1584
+ draw(context) {
1585
+ // Тело босса
1586
+ context.fillStyle = this.color;
1587
+ context.beginPath();
1588
+ context.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
1589
+ context.fill();
1590
+
1591
+ // Обводка
1592
+ context.strokeStyle = "#ffffff";
1593
+ context.lineWidth = 3;
1594
+ context.stroke();
1595
+
1596
+ // Имя
1597
+ context.fillStyle = "#ffffff";
1598
+ context.font = "bold 16px Arial";
1599
+ context.textAlign = "center";
1600
+ context.fillText(this.name, this.x, this.y - this.radius - 10);
1601
+
1602
+ // HP бар
1603
+ const barWidth = 120;
1604
+ const barHeight = 8;
1605
+ context.fillStyle = "#333333";
1606
+ context.fillRect(this.x - barWidth / 2, this.y + this.radius + 15, barWidth, barHeight);
1607
+ context.fillStyle = "#ff4444";
1608
+ context.fillRect(this.x - barWidth / 2, this.y + this.radius + 15, barWidth * (this.hp / this.maxHp), barHeight);
1609
+
1610
+ // Снаряды
1611
+ this.projectiles.forEach(proj => {
1612
+ context.fillStyle = proj.color;
1613
+ context.beginPath();
1614
+ context.arc(proj.x, proj.y, proj.radius, 0, Math.PI * 2);
1615
+ context.fill();
1616
+ });
1617
+ }
1618
+ }
1619
+
1620
+ let player = null;
1621
+ let proficiency = null;
1622
+ let weapon = null;
1623
+ let enemies = [];
1624
+ let spawnTimer = 0;
1625
+ let wave = 1;
1626
+ let waveTimer = 0;
1627
+ let currentBoss = null;
1628
+ let gameState = GAME_STATE.MENU;
1629
+ let classKey = null;
1630
+ let perkLevels = {};
1631
+ let currentPassives = [];
1632
+ let currentActives = [];
1633
+ let modifiers = baseModifiers();
1634
+ let divinePerksUnlocked = [];
1635
+ let pressureTimer = 0;
1636
+ let pressureDuration = 30; // 30 секунд до поражения при перегрузке
1637
+ let pressureActive = false;
1638
+
1639
+ function findClosestEnemy(origin, list, maxRange = Infinity) {
1640
+ let closest = null;
1641
+ let closestDist = Number.MAX_VALUE;
1642
+ for (const enemy of list) {
1643
+ const dist = Math.hypot(enemy.x - origin.x, enemy.y - origin.y);
1644
+ if (dist < closestDist && dist <= maxRange) {
1645
+ closest = enemy;
1646
+ closestDist = dist;
1647
+ }
1648
+ }
1649
+ return closest;
1650
+ }
1651
+
1652
+ function spawnEnemy() {
1653
+ const edge = Math.floor(Math.random() * 4);
1654
+ let x, y;
1655
+ switch (edge) {
1656
+ case 0:
1657
+ x = Math.random() * canvas.width;
1658
+ y = -20;
1659
+ break;
1660
+ case 1:
1661
+ x = canvas.width + 20;
1662
+ y = Math.random() * canvas.height;
1663
+ break;
1664
+ case 2:
1665
+ x = Math.random() * canvas.width;
1666
+ y = canvas.height + 20;
1667
+ break;
1668
+ default:
1669
+ x = -20;
1670
+ y = Math.random() * canvas.height;
1671
+ }
1672
+ const waveScale = Math.max(0, wave - 1);
1673
+ enemies.push(new Enemy(x, y, waveScale));
1674
+ }
1675
+
1676
+ function update(delta) {
1677
+ if (gameState !== GAME_STATE.PLAYING) return;
1678
+
1679
+ // Обновляем систему комбо (только для воина)
1680
+ if (classKey === 'warrior') {
1681
+ comboSystem.update(delta);
1682
+ }
1683
+
1684
+ // Очищаем систему умного наведения для мага
1685
+ if (classKey === 'mage') {
1686
+ // Периодическая очистка мертвых целей
1687
+ if (Math.floor(waveTimer) % 5 === 0) {
1688
+ // Нет автоматической очистки - она происходит при попадании/смерти снаряда
1689
+ }
1690
+ }
1691
+
1692
+ // Обновляем всплывающий текст
1693
+ for (let i = damageNumbers.length - 1; i >= 0; i--) {
1694
+ damageNumbers[i].update(delta);
1695
+ if (!damageNumbers[i].isAlive()) {
1696
+ damageNumbers.splice(i, 1);
1697
+ }
1698
+ }
1699
+
1700
+ for (let i = healNumbers.length - 1; i >= 0; i--) {
1701
+ healNumbers[i].update(delta);
1702
+ if (!healNumbers[i].isAlive()) {
1703
+ healNumbers.splice(i, 1);
1704
+ }
1705
+ }
1706
+
1707
+ for (let i = comboNumbers.length - 1; i >= 0; i--) {
1708
+ comboNumbers[i].update(delta);
1709
+ if (!comboNumbers[i].isAlive()) {
1710
+ comboNumbers.splice(i, 1);
1711
+ }
1712
+ }
1713
+
1714
+ // Проверка на босса (каждые 10 волн: 10, 20, 30...)
1715
+ if (wave % 10 === 0 && !currentBoss && enemies.length === 0) {
1716
+ const bossType = Math.floor((wave / 10 - 1) % 3) + 1;
1717
+ currentBoss = new Boss(bossType, wave);
1718
+ waveTimer = 0; // Сброс таймера для босса
1719
+ smartTargeting.clear(); // Очищаем систему наведения при появлении босса
1720
+ }
1721
+
1722
+ // Обработка божественных навыков
1723
+ if (modifiers.eternalFlame && modifiers.eternalFlameCooldown <= 0) {
1724
+ modifiers.eternalFlameCooldown = modifiers.eternalFlameCooldown || 6;
1725
+ const targets = currentBoss ? [currentBoss] : enemies;
1726
+ targets.forEach(target => {
1727
+ const dist = Math.hypot(target.x - player.x, target.y - player.y);
1728
+ if (dist <= 150) {
1729
+ target.takeDamage(15);
1730
+ damageNumbers.push(new FloatingText(
1731
+ target.x,
1732
+ target.y,
1733
+ '15',
1734
+ '#ff4444',
1735
+ true
1736
+ ));
1737
+ }
1738
+ });
1739
+ }
1740
+
1741
+ if (modifiers.starfall && modifiers.starfallCooldown <= 0) {
1742
+ modifiers.starfallCooldown = modifiers.starfallCooldown || 18;
1743
+ const targets = currentBoss ? [currentBoss] : enemies.filter(e => !e.isDead());
1744
+ for (let i = 0; i < Math.min(5, targets.length); i++) {
1745
+ const target = targets[Math.floor(Math.random() * targets.length)];
1746
+ if (target) {
1747
+ target.takeDamage(30);
1748
+ damageNumbers.push(new FloatingText(
1749
+ target.x,
1750
+ target.y,
1751
+ '30',
1752
+ '#ffaa44',
1753
+ true
1754
+ ));
1755
+ }
1756
+ }
1757
+ }
1758
+
1759
+ if (modifiers.divineGrace && modifiers.divineGraceCooldown <= 0) {
1760
+ modifiers.divineGraceCooldown = modifiers.divineGraceCooldown || 25;
1761
+ player.hp = Math.min(player.maxHp + modifiers.hpBonus, player.hp + 40);
1762
+ player.mana = Math.min(player.maxMana + modifiers.manaBonus, player.mana + 30);
1763
+ healNumbers.push(new FloatingText(
1764
+ player.x,
1765
+ player.y - 30,
1766
+ '+40 HP',
1767
+ '#54e894',
1768
+ true
1769
+ ));
1770
+ }
1771
+
1772
+ if (modifiers.cosmicAwareness && modifiers.cosmicAwarenessCooldown <= 0) {
1773
+ modifiers.cosmicAwarenessCooldown = modifiers.cosmicAwarenessCooldown || 14;
1774
+ modifiers.cosmicAwarenessActive = true;
1775
+ modifiers.cosmicAwarenessTimer = 4;
1776
+ modifiers.damageMult *= 1.2;
1777
+ }
1778
+ if (modifiers.cosmicAwarenessActive) {
1779
+ modifiers.cosmicAwarenessTimer -= delta;
1780
+ if (modifiers.cosmicAwarenessTimer <= 0) {
1781
+ modifiers.cosmicAwarenessActive = false;
1782
+ modifiers.damageMult /= 1.2;
1783
+ }
1784
+ }
1785
+
1786
+ if (modifiers.timeDilation && modifiers.timeDilationCooldown <= 0) {
1787
+ modifiers.timeDilationCooldown = modifiers.timeDilationCooldown || 15;
1788
+ modifiers.timeDilationActive = true;
1789
+ modifiers.timeDilationTimer = 3;
1790
+ modifiers.cooldownMult *= 0.5;
1791
+ }
1792
+ if (modifiers.timeDilationActive) {
1793
+ modifiers.timeDilationTimer -= delta;
1794
+ if (modifiers.timeDilationTimer <= 0) {
1795
+ modifiers.timeDilationActive = false;
1796
+ modifiers.cooldownMult /= 0.5;
1797
+ }
1798
+ }
1799
+
1800
+ if (currentBoss) {
1801
+ // Бой с боссом
1802
+ currentBoss.update(player, delta);
1803
+ weapon.update([currentBoss], delta);
1804
+
1805
+ // Проверка смерти босса
1806
+ if (currentBoss.isDead()) {
1807
+ player.giveXp(500 + wave * 50);
1808
+ bossKilled = true;
1809
+ openDivineReward();
1810
+ currentBoss = null;
1811
+ wave++;
1812
+ waveTimer = 0;
1813
+ } else if (player.hp <= 0) {
1814
+ gameState = GAME_STATE.OVER;
1815
+ showGameOver();
1816
+ return;
1817
+ }
1818
+ } else {
1819
+ // Обычная волна (1 минута)
1820
+ waveTimer += delta;
1821
+
1822
+ // Система напряжения: если врагов >= 10, запускается обратный отсчёт
1823
+ const enemyCount = enemies.filter(e => !e.isDead()).length;
1824
+ if (enemyCount >= 10) {
1825
+ if (!pressureActive) {
1826
+ pressureActive = true;
1827
+ pressureTimer = pressureDuration;
1828
+ }
1829
+ pressureTimer -= delta;
1830
+ if (pressureTimer <= 0) {
1831
+ // Поражение от перегрузки
1832
+ player.hp = 0;
1833
+ }
1834
+ } else {
1835
+ // Если врагов меньше 10, сбрасываем таймер напряжения
1836
+ if (pressureActive) {
1837
+ pressureActive = false;
1838
+ pressureTimer = 0;
1839
+ }
1840
+ }
1841
+
1842
+ // Завершение волны через 1 минуту
1843
+ if (waveTimer >= WAVE_DURATION) {
1844
+ wave++;
1845
+ waveTimer = 0;
1846
+ enemies = []; // Очистка врагов
1847
+ spawnTimer = 0;
1848
+ pressureActive = false;
1849
+ pressureTimer = 0;
1850
+ smartTargeting.clear(); // Очищаем систему наведения между волнами
1851
+ }
1852
+
1853
+ spawnTimer += delta;
1854
+ const spawnRate = Math.max(0.25, 1.0 - wave * 0.03);
1855
+ if (spawnTimer > spawnRate) {
1856
+ spawnEnemy();
1857
+ spawnTimer = 0;
1858
+ }
1859
+
1860
+ player.update(delta, modifiers);
1861
+ enemies.forEach((enemy) => enemy.update(player, delta, modifiers));
1862
+ weapon.update(enemies, delta);
1863
+
1864
+ for (let i = enemies.length - 1; i >= 0; i--) {
1865
+ if (enemies[i].isDead()) {
1866
+ enemiesKilled++;
1867
+
1868
+ if (modifiers.killHeal) {
1869
+ player.hp = Math.min(player.maxHp + modifiers.hpBonus, player.hp + modifiers.killHeal);
1870
+ healNumbers.push(new FloatingText(
1871
+ player.x,
1872
+ player.y - 30,
1873
+ '+' + modifiers.killHeal,
1874
+ '#54e894',
1875
+ false
1876
+ ));
1877
+ }
1878
+
1879
+ const xpGain = Math.round(12 * (1 + Math.max(0, wave - 1) * 0.1));
1880
+ player.giveXp(xpGain);
1881
+ enemies.splice(i, 1);
1882
+ }
1883
+ }
1884
+ }
1885
+
1886
+ if (player.hp <= 0) {
1887
+ gameState = GAME_STATE.OVER;
1888
+ showGameOver();
1889
+ }
1890
+
1891
+ // Автосохранение каждые 30 секунд
1892
+ if (Math.floor(waveTimer) % 30 === 0 && Math.floor(waveTimer) > 0) {
1893
+ saveGame();
1894
+ }
1895
+
1896
+ // Проверка достижений
1897
+ checkAchievements();
1898
+
1899
+ updateUI();
1900
+ }
1901
+
1902
+ function draw() {
1903
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1904
+
1905
+ // backdrop grid
1906
+ ctx.strokeStyle = "rgba(255,255,255,0.04)";
1907
+ for (let x = 0; x < canvas.width; x += 40) {
1908
+ ctx.beginPath();
1909
+ ctx.moveTo(x, 0);
1910
+ ctx.lineTo(x, canvas.height);
1911
+ ctx.stroke();
1912
+ }
1913
+ for (let y = 0; y < canvas.height; y += 40) {
1914
+ ctx.beginPath();
1915
+ ctx.moveTo(0, y);
1916
+ ctx.lineTo(canvas.width, y);
1917
+ ctx.stroke();
1918
+ }
1919
+
1920
+ // Отображаем всплывающий текст
1921
+ damageNumbers.forEach(text => text.draw(ctx));
1922
+ healNumbers.forEach(text => text.draw(ctx));
1923
+ comboNumbers.forEach(text => text.draw(ctx));
1924
+
1925
+ if (weapon) weapon.draw(ctx);
1926
+ if (player) player.draw(ctx);
1927
+ if (currentBoss) {
1928
+ currentBoss.draw(ctx);
1929
+ } else {
1930
+ enemies.forEach((enemy) => enemy.draw(ctx));
1931
+ }
1932
+
1933
+ // Отображаем комбо для воина
1934
+ if (classKey === 'warrior' && comboSystem.combo > 0) {
1935
+ drawWarriorCombo(ctx);
1936
+ }
1937
+ }
1938
+
1939
+ function drawWarriorCombo(ctx) {
1940
+ const combo = comboSystem.combo;
1941
+ const multiplier = comboSystem.getMultiplier().toFixed(2);
1942
+ const alpha = Math.min(1, comboSystem.timer / comboSystem.COMBO_RESET_TIME);
1943
+
1944
+ // Фон
1945
+ ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.5})`;
1946
+ ctx.fillRect(canvas.width - 120, 10, 110, 50);
1947
+
1948
+ // Текст комбо
1949
+ ctx.font = 'bold 24px Arial';
1950
+ ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
1951
+ ctx.textAlign = 'right';
1952
+ ctx.fillText(`КОМБО: ${combo}`, canvas.width - 15, 35);
1953
+
1954
+ // Множитель
1955
+ ctx.font = '16px Arial';
1956
+ ctx.fillStyle = `rgba(255, 215, 102, ${alpha})`;
1957
+ ctx.fillText(`${multiplier}x`, canvas.width - 15, 55);
1958
+
1959
+ // Прогресс до следующего комбо
1960
+ const progress = comboSystem.getComboProgress();
1961
+ const progressWidth = 100 * progress;
1962
+ ctx.fillStyle = `rgba(255, 105, 180, ${alpha})`;
1963
+ ctx.fillRect(canvas.width - 110, 60, progressWidth, 3);
1964
+
1965
+ // Таймер
1966
+ const timerWidth = 100 * (comboSystem.timer / comboSystem.COMBO_RESET_TIME);
1967
+ ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.3})`;
1968
+ ctx.fillRect(canvas.width - 110, 60, timerWidth, 3);
1969
+ }
1970
+
1971
+ function updateUI() {
1972
+ document.getElementById("hpValue").textContent = player
1973
+ ? `${Math.max(0, Math.round(player.hp))}/${Math.round(player.maxHp + modifiers.hpBonus)}`
1974
+ : "-";
1975
+ document.getElementById("manaValue").textContent = player
1976
+ ? `${Math.round(player.mana)}/${Math.round(player.maxMana + modifiers.manaBonus)}`
1977
+ : "-";
1978
+ document.getElementById("profValue").textContent = proficiency
1979
+ ? `${proficiency.currentLevel} (${proficiency.exp}/${proficiency.expToNextLevel})`
1980
+ : "-";
1981
+ document.getElementById("waveValue").textContent = wave;
1982
+ document.getElementById("classValue").textContent = classKey ? CLASS_DEFS[classKey].name : "-";
1983
+ document.getElementById("levelValue").textContent = player
1984
+ ? `${player.level} (${Math.round(player.xp)}/${player.xpToNext})`
1985
+ : "-";
1986
+
1987
+ // Комбо только для воина
1988
+ const comboContainer = document.getElementById("comboContainer");
1989
+ const comboElement = document.getElementById("comboValue");
1990
+
1991
+ if (classKey === 'warrior') {
1992
+ comboContainer.style.display = "flex";
1993
+ if (comboSystem.combo > 0) {
1994
+ comboElement.textContent = `${comboSystem.combo} (${comboSystem.getMultiplier().toFixed(2)}x)`;
1995
+ comboElement.style.color = '#ffd166';
1996
+ } else {
1997
+ comboElement.textContent = '0';
1998
+ comboElement.style.color = '#a7b7d5';
1999
+ }
2000
+ } else {
2001
+ comboContainer.style.display = "none";
2002
+ }
2003
+
2004
+ // Таймер волны
2005
+ if (currentBoss) {
2006
+ const remaining = Math.max(0, BOSS_DURATION - waveTimer);
2007
+ const mins = Math.floor(remaining / 60);
2008
+ const secs = Math.floor(remaining % 60);
2009
+ document.getElementById("waveTime").textContent = `Босс: ${mins}:${secs.toString().padStart(2, '0')}`;
2010
+ } else {
2011
+ const remaining = Math.max(0, WAVE_DURATION - waveTimer);
2012
+ const mins = Math.floor(remaining / 60);
2013
+ const secs = Math.floor(remaining % 60);
2014
+ document.getElementById("waveTime").textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
2015
+ }
2016
+
2017
+ // Таймер напряжения
2018
+ const pressureEl = document.getElementById("pressureTimer");
2019
+ const pressureTimeEl = document.getElementById("pressureTime");
2020
+ if (pressureActive && !currentBoss) {
2021
+ pressureEl.style.display = "flex";
2022
+ const remaining = Math.max(0, pressureTimer);
2023
+ const secs = Math.floor(remaining);
2024
+ const ms = Math.floor((remaining - secs) * 10);
2025
+ pressureTimeEl.textContent = `${secs}.${ms}`;
2026
+ pressureTimeEl.style.color = remaining < 10 ? "#ff4444" : "#ffaa44";
2027
+ } else {
2028
+ pressureEl.style.display = "none";
2029
+ }
2030
+ }
2031
+
2032
+ function loop(timestamp) {
2033
+ if (!lastTime) lastTime = timestamp;
2034
+ const delta = Math.min(0.05, (timestamp - lastTime) / 1000);
2035
+ lastTime = timestamp;
2036
+
2037
+ update(delta);
2038
+ draw();
2039
+ requestAnimationFrame(loop);
2040
+ }
2041
+
2042
+ function setupClassSelect() {
2043
+ const buttons = document.querySelectorAll(".class-card");
2044
+ buttons.forEach((btn) => {
2045
+ btn.addEventListener("click", () => {
2046
+ startGame(btn.dataset.class);
2047
+ });
2048
+ });
2049
+
2050
+ // Проверяем есть ли сохранение
2051
+ const saved = loadGame();
2052
+ if (saved) {
2053
+ const continueBtn = document.createElement('button');
2054
+ continueBtn.textContent = 'Продолжить';
2055
+ continueBtn.style.cssText = `
2056
+ margin-top: 10px;
2057
+ padding: 10px 20px;
2058
+ background: linear-gradient(135deg, #7cfbce, #4be0ff);
2059
+ color: #041022;
2060
+ border: none;
2061
+ border-radius: 8px;
2062
+ font-weight: bold;
2063
+ cursor: pointer;
2064
+ width: 100%;
2065
+ `;
2066
+ continueBtn.addEventListener('click', () => {
2067
+ continueGame(saved);
2068
+ });
2069
+
2070
+ const panel = document.querySelector('#classSelect .panel');
2071
+ panel.appendChild(continueBtn);
2072
+ }
2073
+
2074
+ document.getElementById("restartBtn").addEventListener("click", () => {
2075
+ deleteSave(); // Удаляем сохранение при перезапуске
2076
+ resetGame();
2077
+ });
2078
+
2079
+ // Горячие клавиши
2080
+ window.addEventListener('keydown', (e) => {
2081
+ keys[e.code] = true;
2082
+
2083
+ // ESC - пауза/меню
2084
+ if (e.code === 'Escape') {
2085
+ if (gameState === GAME_STATE.PLAYING) {
2086
+ saveGame();
2087
+ showQuickMessage('Игра сохранена!');
2088
+ }
2089
+ }
2090
+
2091
+ // F5 - быстрое сохранение
2092
+ if (e.code === 'F5') {
2093
+ e.preventDefault();
2094
+ saveGame();
2095
+ showQuickMessage('Игра сохранена!');
2096
+ }
2097
+
2098
+ // F9 - быстрая загрузка
2099
+ if (e.code === 'F9') {
2100
+ e.preventDefault();
2101
+ const saved = loadGame();
2102
+ if (saved) {
2103
+ continueGame(saved);
2104
+ showQuickMessage('Игра загружена!');
2105
+ }
2106
+ }
2107
+ });
2108
+
2109
+ window.addEventListener("keyup", (e) => {
2110
+ keys[e.code] = false;
2111
+ });
2112
+ }
2113
+
2114
+ function showQuickMessage(text) {
2115
+ const msg = document.createElement('div');
2116
+ msg.textContent = text;
2117
+ msg.style.cssText = `
2118
+ position: fixed;
2119
+ top: 50%;
2120
+ left: 50%;
2121
+ transform: translate(-50%, -50%);
2122
+ background: rgba(0, 0, 0, 0.8);
2123
+ color: white;
2124
+ padding: 20px 40px;
2125
+ border-radius: 10px;
2126
+ z-index: 1001;
2127
+ animation: fadeInOut 2s;
2128
+ `;
2129
+
2130
+ document.body.appendChild(msg);
2131
+
2132
+ setTimeout(() => {
2133
+ if (msg.parentNode) {
2134
+ msg.parentNode.removeChild(msg);
2135
+ }
2136
+ }, 2000);
2137
+ }
2138
+
2139
+ // Добавляем анимацию для быстрого сообщения
2140
+ const quickMsgStyle = document.createElement('style');
2141
+ quickMsgStyle.textContent = `
2142
+ @keyframes fadeInOut {
2143
+ 0%, 100% { opacity: 0; }
2144
+ 20%, 80% { opacity: 1; }
2145
+ }
2146
+ `;
2147
+ document.head.appendChild(quickMsgStyle);
2148
+
2149
+ function continueGame(save) {
2150
+ classKey = save.classKey;
2151
+ const def = CLASS_DEFS[classKey];
2152
+
2153
+ // Восстанавливаем игрока
2154
+ player = new Player(
2155
+ save.player.x || canvas.width / 2,
2156
+ save.player.y || canvas.height / 2,
2157
+ def.color,
2158
+ save.player.maxHp || def.hp,
2159
+ def.speed,
2160
+ save.player.maxMana || def.mana,
2161
+ def.manaRegen
2162
+ );
2163
+
2164
+ // Восстанавливаем состояние
2165
+ player.hp = save.player.hp;
2166
+ player.mana = save.player.mana;
2167
+ player.level = save.player.level;
2168
+ player.xp = save.player.xp;
2169
+ player.xpToNext = save.player.xpToNext;
2170
+
2171
+ // Восстанавливаем мастерство
2172
+ proficiency = new Proficiency(def.proficiency);
2173
+ proficiency.currentLevel = save.proficiency.currentLevel;
2174
+ proficiency.exp = save.proficiency.exp;
2175
+ proficiency.expToNextLevel = save.proficiency.expToNextLevel;
2176
+ proficiency.unlockedPerkIds = save.proficiency.unlockedPerkIds;
2177
+
2178
+ // Восстанавливаем остальное
2179
+ wave = save.wave || 1;
2180
+ waveTimer = save.waveTimer || 0;
2181
+ perkLevels = save.perks || {};
2182
+ currentPassives = save.currentPassives || [];
2183
+ currentActives = save.currentActives || [];
2184
+ divinePerksUnlocked = save.divinePerksUnlocked || [];
2185
+ modifiers = save.modifiers || baseModifiers();
2186
+ enemiesKilled = save.enemiesKilled || 0;
2187
+ totalDamageDealt = save.totalDamageDealt || 0;
2188
+ bossKilled = save.bossKilled || false;
2189
+
2190
+ // Создаем оружие
2191
+ switch (def.weapon) {
2192
+ case "bow":
2193
+ weapon = new BowWeapon(player, proficiency, modifiers);
2194
+ break;
2195
+ case "orb":
2196
+ weapon = new OrbWeapon(player, proficiency, modifiers);
2197
+ break;
2198
+ case "aura":
2199
+ weapon = new AuraWeapon(player, proficiency, modifiers);
2200
+ break;
2201
+ default:
2202
+ weapon = new SwordWeapon(player, proficiency, modifiers);
2203
+ }
2204
+
2205
+ // Начинаем игру
2206
+ enemies = [];
2207
+ spawnTimer = 0;
2208
+ currentBoss = null;
2209
+ gameState = GAME_STATE.PLAYING;
2210
+ gameLoaded = true;
2211
+
2212
+ // Очищаем системы
2213
+ damageNumbers = [];
2214
+ healNumbers = [];
2215
+ comboNumbers = [];
2216
+ smartTargeting.clear();
2217
+ if (classKey === 'warrior') {
2218
+ comboSystem.reset();
2219
+ }
2220
+
2221
+ document.getElementById("classSelect").classList.add("hidden");
2222
+ document.getElementById("gameOver").classList.add("hidden");
2223
+
2224
+ refreshSkillIcons();
2225
+ updateUI();
2226
+
2227
+ console.log('Игра загружена');
2228
+ }
2229
+
2230
+ function startGame(key) {
2231
+ classKey = key;
2232
+ const def = CLASS_DEFS[key];
2233
+ modifiers = baseModifiers();
2234
+ perkLevels = {};
2235
+ player = new Player(canvas.width / 2, canvas.height / 2, def.color, def.hp, def.speed, def.mana, def.manaRegen);
2236
+ proficiency = new Proficiency(def.proficiency);
2237
+ // Открываем базовый навык сразу при старте (уровень мастерства 0 -> 1)
2238
+ proficiency.currentLevel = 1;
2239
+ proficiency.unlockRandomPerk();
2240
+ currentPassives = [];
2241
+ currentActives = [];
2242
+
2243
+ switch (def.weapon) {
2244
+ case "bow":
2245
+ weapon = new BowWeapon(player, proficiency, modifiers);
2246
+ break;
2247
+ case "orb":
2248
+ weapon = new OrbWeapon(player, proficiency, modifiers);
2249
+ break;
2250
+ case "aura":
2251
+ weapon = new AuraWeapon(player, proficiency, modifiers);
2252
+ break;
2253
+ default:
2254
+ weapon = new SwordWeapon(player, proficiency, modifiers);
2255
+ }
2256
+
2257
+ enemies = [];
2258
+ spawnTimer = 0;
2259
+ wave = 1;
2260
+ waveTimer = 0;
2261
+ currentBoss = null;
2262
+ divinePerksUnlocked = [];
2263
+ pressureActive = false;
2264
+ pressureTimer = 0;
2265
+ enemiesKilled = 0;
2266
+ totalDamageDealt = 0;
2267
+ bossKilled = false;
2268
+ damageNumbers = [];
2269
+ healNumbers = [];
2270
+ comboNumbers = [];
2271
+ smartTargeting.clear();
2272
+ comboSystem.reset();
2273
+
2274
+ gameState = GAME_STATE.PLAYING;
2275
+ document.getElementById("classSelect").classList.add("hidden");
2276
+ document.getElementById("gameOver").classList.add("hidden");
2277
+ document.getElementById("levelUp").classList.add("hidden");
2278
+ document.getElementById("divineReward").classList.add("hidden");
2279
+
2280
+ renderSkillIcons(currentActives, currentPassives);
2281
+ renderMetaIcons();
2282
+ updateUI();
2283
+ }
2284
+
2285
+ function resetGame() {
2286
+ gameState = GAME_STATE.MENU;
2287
+ document.getElementById("classSelect").classList.remove("hidden");
2288
+ document.getElementById("gameOver").classList.add("hidden");
2289
+ enemies = [];
2290
+ player = null;
2291
+ weapon = null;
2292
+ proficiency = null;
2293
+ classKey = null;
2294
+ currentActives = [];
2295
+ currentPassives = [];
2296
+ perkLevels = {};
2297
+ currentBoss = null;
2298
+ waveTimer = 0;
2299
+ divinePerksUnlocked = [];
2300
+ pressureActive = false;
2301
+ pressureTimer = 0;
2302
+ enemiesKilled = 0;
2303
+ totalDamageDealt = 0;
2304
+ bossKilled = false;
2305
+ comboSystem.reset();
2306
+ damageNumbers = [];
2307
+ healNumbers = [];
2308
+ comboNumbers = [];
2309
+ smartTargeting.clear();
2310
+ }
2311
+
2312
+ function showGameOver() {
2313
+ let stats = `Волна: ${wave} · Мастерство: ${proficiency.currentLevel} · Уровень: ${player.level} · Убийств: ${enemiesKilled}`;
2314
+
2315
+ if (classKey === 'warrior') {
2316
+ stats += ` · Макс комбо: ${comboSystem.maxCombo}`;
2317
+ }
2318
+
2319
+ document.getElementById("gameOverStats").textContent = stats;
2320
+ document.getElementById("gameOver").classList.remove("hidden");
2321
+ }
2322
+
2323
+ function openLevelUp() {
2324
+ gameState = GAME_STATE.LEVELUP;
2325
+ const choices = document.getElementById("levelChoices");
2326
+ choices.innerHTML = "";
2327
+
2328
+ const pool = getAvailablePerks();
2329
+ shuffle(pool);
2330
+ const pick = pool.slice(0, 3);
2331
+ pick.forEach((perk) => {
2332
+ const card = document.createElement("div");
2333
+ const rarity = perk.rarity || "common";
2334
+ card.className = `choice-card rarity-${rarity}`;
2335
+ card.innerHTML = `<div class="title">${perk.name}</div><div class="type">${rarityLabel(rarity)} · ${perk.type === "passive" ? "Пассив" : "Актив"}</div><div class="desc">${perk.desc}</div>`;
2336
+ card.addEventListener("click", () => {
2337
+ applyPerk(perk);
2338
+ document.getElementById("levelUp").classList.add("hidden");
2339
+ gameState = GAME_STATE.PLAYING;
2340
+ });
2341
+ choices.appendChild(card);
2342
+ });
2343
+
2344
+ document.getElementById("levelUp").classList.remove("hidden");
2345
+ }
2346
+
2347
+ function openDivineReward() {
2348
+ gameState = GAME_STATE.DIVINE_REWARD;
2349
+ const choices = document.getElementById("divineChoices");
2350
+ choices.innerHTML = "";
2351
+
2352
+ const available = DIVINE_PERKS.filter(p => !divinePerksUnlocked.includes(p.id));
2353
+ shuffle(available);
2354
+ const pick = available.slice(0, 3);
2355
+
2356
+ if (pick.length === 0) {
2357
+ // Все божественные навыки получены - даём обычные награды
2358
+ openLevelUp();
2359
+ return;
2360
+ }
2361
+
2362
+ pick.forEach((perk) => {
2363
+ const card = document.createElement("div");
2364
+ card.className = `choice-card rarity-divine`;
2365
+ card.innerHTML = `<div class="title">${perk.name}</div><div class="type">Божественный · Пассив</div><div class="desc">${perk.desc}</div>`;
2366
+ card.addEventListener("click", () => {
2367
+ applyDivinePerk(perk);
2368
+ document.getElementById("divineReward").classList.add("hidden");
2369
+ gameState = GAME_STATE.PLAYING;
2370
+ });
2371
+ choices.appendChild(card);
2372
+ });
2373
+
2374
+ document.getElementById("divineReward").classList.remove("hidden");
2375
+ }
2376
+
2377
+ function applyDivinePerk(perk) {
2378
+ perk.effect(modifiers);
2379
+ divinePerksUnlocked.push(perk.id);
2380
+ weapon.syncMods(modifiers);
2381
+ refreshSkillIcons();
2382
+ }
2383
+
2384
+ function getAvailablePerks() {
2385
+ if (!classKey || !proficiency) return [];
2386
+ const kit = CLASS_KITS[classKey];
2387
+ const basePerks = BASE_PERKS[classKey] || [];
2388
+
2389
+ // Объединяем базовые и основные навыки
2390
+ const allPerks = [
2391
+ ...basePerks.map((p) => ({ ...p, type: "passive" })),
2392
+ ...kit.passives.map((p) => ({ ...p, type: "passive" })),
2393
+ ...kit.actives.map((p) => ({ ...p, type: "active" })),
2394
+ ];
2395
+
2396
+ // Фильтруем только открытые через мастерство
2397
+ const unlocked = allPerks.filter(p => proficiency.unlockedPerkIds.includes(p.id));
2398
+
2399
+ return unlocked;
2400
+ }
2401
+
2402
+ function applyPerk(perk) {
2403
+ const info = perkLevels[perk.id] || { level: 0, ascended: 0, type: perk.type };
2404
+ info.level += 1;
2405
+ const max = perk.max || 3;
2406
+ const isAscend = info.level > max;
2407
+ // apply effect; for ascension, add a light extra scaling
2408
+ perk.effect(modifiers);
2409
+ if (isAscend) {
2410
+ modifiers.damageMult *= 1.04;
2411
+ modifiers.cooldownMult *= 0.98;
2412
+ modifiers.hpBonus += 6;
2413
+ modifiers.rangeBonus += 3;
2414
+ }
2415
+ info.ascended = Math.max(0, info.level - max);
2416
+ perkLevels[perk.id] = info;
2417
+
2418
+ if (perk.type === "passive" && !currentPassives.includes(perk.id)) currentPassives.push(perk.id);
2419
+ if (perk.type === "active" && !currentActives.includes(perk.id)) currentActives.push(perk.id);
2420
+ weapon.syncMods(modifiers);
2421
+ refreshSkillIcons();
2422
+ }
2423
+
2424
+ function refreshSkillIcons() {
2425
+ renderSkillIcons(currentActives, currentPassives);
2426
+ renderMetaIcons();
2427
+ }
2428
+
2429
+ function renderSkillIcons(activeIds, passiveIds) {
2430
+ const activeContainer = document.getElementById("activeSkills");
2431
+ const passiveContainer = document.getElementById("passiveSkills");
2432
+ activeContainer.innerHTML = "";
2433
+ passiveContainer.innerHTML = "";
2434
+
2435
+ activeIds.forEach((id) => activeContainer.appendChild(makeSkillIcon(id)));
2436
+ passiveIds.forEach((id) => passiveContainer.appendChild(makeSkillIcon(id)));
2437
+ }
2438
+
2439
+ const META_GROUPS = {
2440
+ warrior: [
2441
+ { id: "w_meta_1", title: "Вихрь + Рипост", needs: ["whirlwind", "riposte"] },
2442
+ { id: "w_meta_2", title: "Пыл + Знамя", needs: ["battle_fervor", "war_banner"] },
2443
+ ],
2444
+ archer: [
2445
+ { id: "a_meta_1", title: "Залп + Пробитие", needs: ["volley", "piercing_arrows"] },
2446
+ { id: "a_meta_2", title: "Рико + Марка", needs: ["ricochet", "marked_target"] },
2447
+ ],
2448
+ mage: [
2449
+ { id: "m_meta_1", title: "Комета + Жар", needs: ["arcane_comet", "ember_focus"] },
2450
+ { id: "m_meta_2", title: "Синхрония + Барьер", needs: ["elemental_sync", "mana_barrier"] },
2451
+ ],
2452
+ acolyte: [
2453
+ { id: "c_meta_1", title: "Освящение + Свет", needs: ["consecrate", "soothing_light"] },
2454
+ { id: "c_meta_2", title: "Страж + Броня", needs: ["guardian_aura", "radiant_armor"] },
2455
+ ],
2456
+ };
2457
+
2458
+ function renderMetaIcons() {
2459
+ const container = document.getElementById("metaSkills");
2460
+ if (!container) return;
2461
+ container.innerHTML = "";
2462
+ if (!classKey) return;
2463
+ const groups = META_GROUPS[classKey] || [];
2464
+ groups.forEach((g) => {
2465
+ const div = document.createElement("div");
2466
+ div.className = "skill-icon meta";
2467
+ const dot = document.createElement("div");
2468
+ dot.className = "meta-dot";
2469
+ const ready = g.needs.every((id) => perkLevels[id]?.level >= 1);
2470
+ if (ready) dot.classList.add("ready");
2471
+ const text = document.createElement("div");
2472
+ text.className = "meta-text";
2473
+ text.textContent = g.title;
2474
+ div.appendChild(dot);
2475
+ div.appendChild(text);
2476
+ container.appendChild(div);
2477
+ });
2478
+ }
2479
+
2480
+ function makeSkillIcon(id) {
2481
+ const wrapper = document.createElement("div");
2482
+ const rarity = (getPerkDef(id)?.rarity) || "common";
2483
+ wrapper.className = `skill-icon rarity-${rarity}`;
2484
+ const canvasIcon = document.createElement("canvas");
2485
+ canvasIcon.width = 32;
2486
+ canvasIcon.height = 32;
2487
+ drawIcon(canvasIcon.getContext("2d"), id);
2488
+ wrapper.appendChild(canvasIcon);
2489
+
2490
+ const label = document.createElement("div");
2491
+ label.className = "skill-label";
2492
+ const kit = CLASS_KITS[classKey];
2493
+ const all = kit ? [...kit.passives, ...kit.actives] : [];
2494
+ const max = all.find((p) => p.id === id)?.max || 3;
2495
+ const info = perkLevels[id];
2496
+ const lv = info ? info.level : 0;
2497
+ const asc = info ? info.ascended : 0;
2498
+ label.textContent = `${shortLabel(id)} ${lv}/${max}${asc > 0 ? "+" + asc : ""}`;
2499
+ wrapper.appendChild(label);
2500
+ return wrapper;
2501
+ }
2502
+
2503
+ function shortLabel(id) {
2504
+ const map = {
2505
+ // warrior
2506
+ iron_skin: "Skin",
2507
+ battle_fervor: "Ferv",
2508
+ blade_mastery: "Blade",
2509
+ riposte: "Rip",
2510
+ heavy_guard: "Guard",
2511
+ bloodlust: "Blood",
2512
+ sweeping_edge: "Sweep",
2513
+ adamant: "Adam",
2514
+ momentum: "Move",
2515
+ war_banner: "Banner",
2516
+ blade_dash: "Dash",
2517
+ shockwave: "Wave",
2518
+ guard_stance: "Stance",
2519
+ whirlwind: "Whirl",
2520
+ // archer
2521
+ quickdraw: "QDraw",
2522
+ light_steps: "Steps",
2523
+ piercing_arrows: "Pierce",
2524
+ hunter_focus: "Focus",
2525
+ ricochet: "Rico",
2526
+ wind_runner: "Wind",
2527
+ bleeding_edge: "Bleed",
2528
+ camo: "Camo",
2529
+ falcon_eye: "Eye",
2530
+ arrow_surge: "Surge",
2531
+ power_shot: "Power",
2532
+ volley: "Volley",
2533
+ evasive_roll: "Roll",
2534
+ marked_target: "Mark",
2535
+ // mage
2536
+ ember_focus: "Ember",
2537
+ mana_font: "Font",
2538
+ arcane_precision: "Prec",
2539
+ frost_weave: "Frost",
2540
+ overload: "Over",
2541
+ runic_shield: "Shield",
2542
+ elemental_sync: "Sync",
2543
+ astral_echo: "Echo",
2544
+ channeling: "Chan",
2545
+ mystic_amp: "Amp",
2546
+ arcane_comet: "Comet",
2547
+ flame_burst: "Burst",
2548
+ frost_nova: "Nova",
2549
+ mana_barrier: "Barrier",
2550
+ // acolyte
2551
+ steadfast: "Stand",
2552
+ radiant_armor: "Armor",
2553
+ soothing_light: "Light",
2554
+ devotion: "Dev",
2555
+ sanctuary: "Sanc",
2556
+ spirit_chain: "Chain",
2557
+ warding: "Ward",
2558
+ resolve: "Res",
2559
+ holy_edge: "Edge",
2560
+ blessing: "Bless",
2561
+ healing_wave: "Heal",
2562
+ smite: "Smite",
2563
+ consecrate: "Cons",
2564
+ guardian_aura: "GuardA",
2565
+ };
2566
+ return map[id] || "Perk";
2567
+ }
2568
+
2569
+ function drawIcon(c, id) {
2570
+ c.imageSmoothingEnabled = false;
2571
+ c.clearRect(0, 0, 32, 32);
2572
+ const px = 4;
2573
+ const drawPixel = (x, y, color) => {
2574
+ c.fillStyle = color;
2575
+ c.fillRect(x * px, y * px, px, px);
2576
+ };
2577
+
2578
+ // simple shapes per type
2579
+ const swordColors = ["#9bd5ff", "#d7ecff"];
2580
+ const bowColors = ["#c17b2a", "#f4d35e"];
2581
+ const orbColors = ["#c9b4ff", "#ff7b7b", "#ffa64d"];
2582
+ const shieldColors = ["#9be7ff", "#5bbcf2"];
2583
+
2584
+ if (id.includes("blade") || id.includes("whirl") || id.includes("slash") || id.includes("shock")) {
2585
+ swordColors.forEach((color, idx) => {
2586
+ drawPixel(4 + idx, 1 + idx, color);
2587
+ drawPixel(5 + idx, 2 + idx, color);
2588
+ drawPixel(6 + idx, 3 + idx, color);
2589
+ });
2590
+ } else if (id.includes("arrow") || id.includes("shot") || id.includes("bow") || id.includes("mark")) {
2591
+ drawPixel(1, 2, bowColors[0]);
2592
+ drawPixel(2, 2, bowColors[1]);
2593
+ drawPixel(3, 2, bowColors[0]);
2594
+ drawPixel(4, 2, bowColors[1]);
2595
+ drawPixel(5, 2, bowColors[0]);
2596
+ drawPixel(4, 1, bowColors[1]);
2597
+ drawPixel(4, 3, bowColors[1]);
2598
+ } else if (id.includes("orb") || id.includes("mana") || id.includes("flame") || id.includes("frost") || id.includes("comet")) {
2599
+ drawPixel(2, 2, orbColors[0]);
2600
+ drawPixel(2, 3, orbColors[1]);
2601
+ drawPixel(3, 2, orbColors[2]);
2602
+ drawPixel(3, 3, orbColors[0]);
2603
+ } else if (id.includes("guard") || id.includes("armor") || id.includes("shield") || id.includes("aura")) {
2604
+ shieldColors.forEach((color, idx) => {
2605
+ drawPixel(2 + idx, 1, color);
2606
+ drawPixel(2 + idx, 2, color);
2607
+ drawPixel(2 + idx, 3, color);
2608
+ drawPixel(2 + idx, 4, color);
2609
+ });
2610
+ } else {
2611
+ drawPixel(2, 2, "#ffffff");
2612
+ }
2613
+ }
2614
+
2615
+ function shuffle(arr) {
2616
+ for (let i = arr.length - 1; i > 0; i--) {
2617
+ const j = Math.floor(Math.random() * (i + 1));
2618
+ [arr[i], arr[j]] = [arr[j], arr[i]];
2619
+ }
2620
+ }
2621
+
2622
+ setupClassSelect();
2623
+ requestAnimationFrame(loop);
Waveborne_Prodigy_Copy/style.css ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ body {
8
+ background: #0f1624;
9
+ color: #e8ecf3;
10
+ font-family: "Segoe UI", Arial, sans-serif;
11
+ height: 100vh;
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ justify-content: flex-start;
16
+ gap: 12px;
17
+ padding: 16px;
18
+ }
19
+
20
+ #ui {
21
+ display: flex;
22
+ gap: 16px;
23
+ background: rgba(255, 255, 255, 0.06);
24
+ padding: 8px 12px;
25
+ border-radius: 8px;
26
+ border: 1px solid rgba(255, 255, 255, 0.08);
27
+ backdrop-filter: blur(2px);
28
+ }
29
+
30
+ .stat span:first-child {
31
+ color: #9bb3ff;
32
+ margin-right: 4px;
33
+ }
34
+
35
+ .stat.danger {
36
+ color: #ff4444;
37
+ animation: pulse 1s ease-in-out infinite;
38
+ }
39
+
40
+ .stat.danger span:first-child {
41
+ color: #ffaa44;
42
+ }
43
+
44
+ @keyframes pulse {
45
+ 0%, 100% { opacity: 1; }
46
+ 50% { opacity: 0.7; }
47
+ }
48
+
49
+ #gameCanvas {
50
+ border: 1px solid rgba(255, 255, 255, 0.08);
51
+ background: radial-gradient(circle at 40% 30%, #15213a, #0b1220 55%, #070c16 100%);
52
+ width: 960px;
53
+ height: 600px;
54
+ }
55
+
56
+ #canvasWrapper {
57
+ position: relative;
58
+ }
59
+
60
+ .overlay {
61
+ position: absolute;
62
+ inset: 0;
63
+ background: rgba(7, 12, 22, 0.82);
64
+ display: grid;
65
+ place-items: center;
66
+ }
67
+
68
+ .overlay.hidden {
69
+ display: none;
70
+ }
71
+
72
+ .panel {
73
+ background: #10182c;
74
+ border: 1px solid rgba(255, 255, 255, 0.08);
75
+ border-radius: 12px;
76
+ padding: 18px;
77
+ width: 420px;
78
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
79
+ }
80
+
81
+ .panel h2 {
82
+ margin-bottom: 8px;
83
+ letter-spacing: 0.5px;
84
+ }
85
+
86
+ .panel .hint {
87
+ color: #a7b7d5;
88
+ margin-bottom: 12px;
89
+ font-size: 14px;
90
+ }
91
+
92
+ .choice-grid {
93
+ display: grid;
94
+ grid-template-columns: repeat(3, 1fr);
95
+ gap: 10px;
96
+ }
97
+
98
+ .choice-card {
99
+ background: linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
100
+ border: 1px solid rgba(255,255,255,0.12);
101
+ border-radius: 10px;
102
+ padding: 10px;
103
+ cursor: pointer;
104
+ transition: transform 0.12s ease, border-color 0.12s ease;
105
+ min-height: 110px;
106
+ }
107
+
108
+ .choice-card:hover, .choice-card:focus {
109
+ transform: translateY(-2px);
110
+ border-color: #7cfbce;
111
+ }
112
+
113
+ .choice-card .title {
114
+ font-weight: 700;
115
+ margin-bottom: 6px;
116
+ }
117
+
118
+ .choice-card .type {
119
+ font-size: 12px;
120
+ color: #9bb3ff;
121
+ margin-bottom: 6px;
122
+ }
123
+
124
+ .choice-card .desc {
125
+ font-size: 13px;
126
+ color: #c7d3e6;
127
+ }
128
+
129
+ .choice-card.rarity-common { border-color: rgba(255,255,255,0.12); }
130
+ .choice-card.rarity-uncommon { border-color: #54e894aa; }
131
+ .choice-card.rarity-rare { border-color: #5aa9ffaa; }
132
+ .choice-card.rarity-unique { border-color: #ff7bcaaa; }
133
+ .choice-card.rarity-epic { border-color: #c082ffaa; }
134
+ .choice-card.rarity-legendary { border-color: #ffa94daa; }
135
+ .choice-card.rarity-divine { border-color: #ff5f5faa; box-shadow: 0 0 12px rgba(255,95,95,0.3); }
136
+
137
+ .class-grid {
138
+ display: grid;
139
+ grid-template-columns: repeat(2, 1fr);
140
+ gap: 10px;
141
+ }
142
+
143
+ .class-card {
144
+ background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
145
+ border: 1px solid rgba(255, 255, 255, 0.08);
146
+ border-radius: 10px;
147
+ color: inherit;
148
+ padding: 10px;
149
+ text-align: left;
150
+ cursor: pointer;
151
+ transition: transform 0.1s ease, border-color 0.1s ease;
152
+ }
153
+
154
+ .class-card:hover, .class-card:focus {
155
+ transform: translateY(-2px);
156
+ border-color: #6cf1ff;
157
+ }
158
+
159
+ .class-card .title {
160
+ font-weight: 700;
161
+ margin-top: 4px;
162
+ }
163
+
164
+ .class-card .desc {
165
+ color: #a7b7d5;
166
+ font-size: 13px;
167
+ }
168
+
169
+ .icon {
170
+ width: 36px;
171
+ height: 36px;
172
+ margin-bottom: 4px;
173
+ }
174
+
175
+ .icon.sword {
176
+ background: linear-gradient(135deg, #9bd5ff 30%, #d7ecff 70%);
177
+ clip-path: polygon(40% 0%, 60% 0%, 65% 55%, 50% 100%, 35% 55%);
178
+ }
179
+
180
+ .icon.bow {
181
+ background: radial-gradient(circle at 30% 50%, #f4d35e 35%, rgba(0,0,0,0) 36%), radial-gradient(circle at 70% 50%, #f4d35e 35%, rgba(0,0,0,0) 36%), linear-gradient(90deg, rgba(0,0,0,0) 45%, #f4d35e 46%, #f4d35e 54%, rgba(0,0,0,0) 55%), linear-gradient(135deg, #c17b2a 0%, #8b4f20 100%);
182
+ border-radius: 4px;
183
+ }
184
+
185
+ .icon.staff {
186
+ background: linear-gradient(135deg, #8f7bff, #c8b8ff);
187
+ position: relative;
188
+ }
189
+
190
+ .icon.staff::after {
191
+ content: "";
192
+ position: absolute;
193
+ width: 14px;
194
+ height: 14px;
195
+ background: radial-gradient(circle, #ff7b7b 20%, #ffa64d 70%);
196
+ border-radius: 50%;
197
+ top: -4px;
198
+ right: -4px;
199
+ box-shadow: 0 0 6px #ffb899;
200
+ }
201
+
202
+ .icon.shield {
203
+ background: linear-gradient(180deg, #7ad1ff 0%, #4b87c5 100%);
204
+ clip-path: polygon(10% 0%, 90% 0%, 70% 100%, 30% 100%);
205
+ }
206
+
207
+ #skillBar {
208
+ width: 960px;
209
+ display: grid;
210
+ grid-template-columns: repeat(3, 1fr);
211
+ gap: 10px;
212
+ background: rgba(255, 255, 255, 0.04);
213
+ border: 1px solid rgba(255, 255, 255, 0.08);
214
+ border-radius: 10px;
215
+ padding: 10px;
216
+ }
217
+
218
+ .skills-section {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 6px;
222
+ }
223
+
224
+ .section-title {
225
+ color: #9bb3ff;
226
+ font-weight: 700;
227
+ font-size: 14px;
228
+ }
229
+
230
+ .icon-row {
231
+ display: flex;
232
+ gap: 8px;
233
+ }
234
+
235
+ .meta-row {
236
+ display: flex;
237
+ gap: 8px;
238
+ flex-wrap: wrap;
239
+ }
240
+
241
+ .skill-icon {
242
+ width: 40px;
243
+ height: 40px;
244
+ border: 1px solid rgba(255, 255, 255, 0.12);
245
+ background: #0c1220;
246
+ border-radius: 6px;
247
+ display: grid;
248
+ place-items: center;
249
+ position: relative;
250
+ }
251
+
252
+ .skill-icon canvas {
253
+ image-rendering: pixelated;
254
+ }
255
+
256
+ .skill-icon.meta {
257
+ width: auto;
258
+ padding: 6px 8px;
259
+ background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
260
+ display: inline-flex;
261
+ align-items: center;
262
+ gap: 6px;
263
+ }
264
+
265
+ .meta-dot {
266
+ width: 10px;
267
+ height: 10px;
268
+ border-radius: 50%;
269
+ background: #5f6c85;
270
+ }
271
+
272
+ .meta-dot.ready {
273
+ background: #ffd166;
274
+ box-shadow: 0 0 6px #ffd166;
275
+ }
276
+
277
+ .meta-text {
278
+ font-size: 12px;
279
+ color: #e8ecf3;
280
+ }
281
+
282
+ .rarity-common {
283
+ background: #101828;
284
+ border-color: rgba(255,255,255,0.12);
285
+ }
286
+ .rarity-uncommon {
287
+ background: radial-gradient(circle, #143020, #0f1f16);
288
+ border-color: #54e894aa;
289
+ }
290
+ .rarity-rare {
291
+ background: radial-gradient(circle, #0f2c4a, #0c1b2e);
292
+ border-color: #5aa9ffaa;
293
+ }
294
+ .rarity-unique {
295
+ background: radial-gradient(circle, #3a1535, #1f0c1e);
296
+ border-color: #ff7bcaaa;
297
+ }
298
+ .rarity-epic {
299
+ background: radial-gradient(circle, #2d114a, #190a2d);
300
+ border-color: #c082ffaa;
301
+ }
302
+ .rarity-legendary {
303
+ background: radial-gradient(circle, #3b2008, #1e1207);
304
+ border-color: #ffa94daa;
305
+ }
306
+ .rarity-divine {
307
+ background: radial-gradient(circle, #3a0b0b, #1c0606);
308
+ border-color: #ff5f5faa;
309
+ box-shadow: 0 0 12px rgba(255,95,95,0.35);
310
+ }
311
+
312
+ .skill-label {
313
+ position: absolute;
314
+ bottom: -14px;
315
+ font-size: 11px;
316
+ color: #a7b7d5;
317
+ width: 100%;
318
+ text-align: center;
319
+ }
320
+
321
+ #gameOver button,
322
+ .panel button {
323
+ margin-top: 12px;
324
+ padding: 10px 12px;
325
+ border: none;
326
+ border-radius: 8px;
327
+ background: linear-gradient(135deg, #4be0ff, #7cfbce);
328
+ color: #041022;
329
+ font-weight: 700;
330
+ cursor: pointer;
331
+ }
332
+