Initial commit: RPG game project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
saves/
|
||||||
|
.claude/
|
||||||
770
README.md
Normal file
770
README.md
Normal file
@@ -0,0 +1,770 @@
|
|||||||
|
# ⚔️ Хроники Эйдона — Изометрическая RPG
|
||||||
|
|
||||||
|
Браузерная изометрическая RPG, написанная на чистом JavaScript + HTML5 Canvas. Без фреймворков, без зависимостей — только нативный веб. Полная игра с боевой системой, прогрессией персонажа, крафтом, зачарованиями, сюжетными квестами, лором и финальным боссом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
1. [Технологии](#технологии)
|
||||||
|
2. [Архитектура](#архитектура)
|
||||||
|
3. [Управление](#управление)
|
||||||
|
4. [Классы персонажей](#классы-персонажей)
|
||||||
|
5. [Боевая система](#боевая-система)
|
||||||
|
6. [Заклинания](#заклинания)
|
||||||
|
7. [Дерево перков](#дерево-перков)
|
||||||
|
8. [Локации и мир](#локации-и-мир)
|
||||||
|
9. [Враги и боссы](#враги-и-боссы)
|
||||||
|
10. [NPC и диалоги](#npc-и-диалоги)
|
||||||
|
11. [Квесты](#квесты)
|
||||||
|
12. [Инвентарь и экипировка](#инвентарь-и-экипировка)
|
||||||
|
13. [Сеты экипировки](#сеты-экипировки)
|
||||||
|
14. [Зачарования](#зачарования)
|
||||||
|
15. [Крафт и алхимия](#крафт-и-алхимия)
|
||||||
|
16. [Магазин](#магазин)
|
||||||
|
17. [Журнал лора](#журнал-лора)
|
||||||
|
18. [Достижения](#достижения)
|
||||||
|
19. [Бестиарий](#бестиарий)
|
||||||
|
20. [Аудиосистема](#аудиосистема)
|
||||||
|
21. [Система сохранений](#система-сохранений)
|
||||||
|
22. [Визуальные эффекты](#визуальные-эффекты)
|
||||||
|
23. [Статистика контента](#статистика-контента)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технологии
|
||||||
|
|
||||||
|
| Технология | Применение |
|
||||||
|
|---|---|
|
||||||
|
| HTML5 Canvas | Изометрический рендеринг мира и персонажей |
|
||||||
|
| Web Audio API | Процедурная музыка и SFX без аудиофайлов |
|
||||||
|
| Vanilla JS (ES6+) | Вся игровая логика |
|
||||||
|
| CSS3 | UI панели, анимации, HUD |
|
||||||
|
| localStorage | Сохранение прогресса (3 слота) |
|
||||||
|
| File System Access API | Экспорт сохранений в JSON-файлы |
|
||||||
|
|
||||||
|
**Файловая структура:**
|
||||||
|
|
||||||
|
```
|
||||||
|
├── index.html — разметка UI (HUD, панели, боевой экран, меню)
|
||||||
|
├── style.css — стили всего интерфейса
|
||||||
|
├── game.js — основная игровая логика, ИИ боссов, квесты
|
||||||
|
├── renderer.js — изометрический рендерер, портреты, частицы
|
||||||
|
├── rpg.js — механики RPG: классы, заклинания, перки, формулы
|
||||||
|
├── audio.js — Web Audio API: SFX + процедурная музыка
|
||||||
|
├── saves.js — система сохранений (localStorage + JSON-файлы)
|
||||||
|
├── data-loader.js — загрузчик JSON-данных игры
|
||||||
|
├── menu.js — стартовый экран и выбор класса
|
||||||
|
└── data/
|
||||||
|
├── world.json — локации, спавны, NPC, декорации, погода
|
||||||
|
├── enemies.json — все типы врагов, боссов, уникальный лут
|
||||||
|
├── quests.json — все квесты с условиями и наградами
|
||||||
|
├── lore.json — записки лора с координатами на карте
|
||||||
|
├── items/
|
||||||
|
│ ├── loot.json — все собираемые предметы и материалы
|
||||||
|
│ ├── shop.json — ассортимент магазина
|
||||||
|
│ └── recipes.json — рецепты крафта
|
||||||
|
└── story/
|
||||||
|
└── story_quests.json — сюжетные квесты с диалогами
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Игра использует паттерн «объект-синглтон» для каждого модуля:
|
||||||
|
|
||||||
|
- **`Game`** — главный объект: игровой цикл, состояние, ввод, переходы между локациями, система квестов, достижений, бой
|
||||||
|
- **`Renderer`** — изометрический рендерер с камерой, системой глубины (depth sorting), частицами, плавающим текстом, тряской экрана
|
||||||
|
- **`RPG`** — все формулы и механики: урон, защита, крит, заклинания, перки, лут, крафт, зачарование, прокачка
|
||||||
|
- **`Audio`** — процедурная музыка через `OscillatorNode` + `GainNode`, 9 тем и 7 типов SFX
|
||||||
|
- **`Saves`** — сериализация/десериализация состояния игрока, поддержка `Set` через Array при JSON
|
||||||
|
- **`DataLoader`** — асинхронная загрузка всех JSON-файлов перед стартом
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Управление
|
||||||
|
|
||||||
|
| Клавиша | Действие |
|
||||||
|
|---|---|
|
||||||
|
| `WASD` / `↑↓←→` | Движение по карте |
|
||||||
|
| `I` | Инвентарь |
|
||||||
|
| `Q` | Журнал квестов |
|
||||||
|
| `T` | Дерево перков |
|
||||||
|
| `C` | Крафтинг |
|
||||||
|
| `B` | Бестиарий |
|
||||||
|
| `E` | Зачарование |
|
||||||
|
| `L` | Журнал лора |
|
||||||
|
| `H` | Достижения |
|
||||||
|
| `M` | Карта мира / быстрое путешествие |
|
||||||
|
| `P` | Сохранить |
|
||||||
|
| `1` | Атака (в бою) |
|
||||||
|
| `2` | Использовать предмет (в бою) |
|
||||||
|
| `3` | Бежать (в бою) |
|
||||||
|
| `2–9` | Заклинания (в бою, если изучены) |
|
||||||
|
| `ESC` | Закрыть все панели |
|
||||||
|
|
||||||
|
Также поддерживается **клик мышью** по соседней клетке для перемещения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Классы персонажей
|
||||||
|
|
||||||
|
В игре 7 классов с уникальными стартовыми параметрами, бонусами за уровень, заклинаниями и деревьями перков.
|
||||||
|
|
||||||
|
| Класс | HP | MP | STR | DEF | MAG | SPD | Описание |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| ⚔️ Воин | 130 | 30 | 14 | 10 | 4 | 7 | Физический боец. Высокий HP и урон, низкая магия |
|
||||||
|
| 🔮 Маг | 65 | 110 | 4 | 4 | 16 | 6 | Маг с мощными заклинаниями, хрупкий в ближнем бою |
|
||||||
|
| 🏹 Лучник | 90 | 55 | 10 | 6 | 6 | 13 | Быстрый дальний боец. Максимальная скорость |
|
||||||
|
| 🛡️ Паладин | 115 | 65 | 10 | 13 | 8 | 6 | Защитник с исцелением и святым уроном |
|
||||||
|
| 💀 Некромант | 75 | 95 | 5 | 5 | 15 | 7 | Тёмный маг, похищающий жизнь и накладывающий проклятия |
|
||||||
|
| 🪓 Берсерк | 150 | 20 | 18 | 6 | 2 | 10 | Максимальная сила и HP, почти нет магии |
|
||||||
|
| 🌿 Друид | 85 | 85 | 7 | 7 | 11 | 9 | Универсальный класс с природной магией |
|
||||||
|
|
||||||
|
**Бонусы за каждый уровень** (HP / MP / STR / DEF / MAG / SPD):
|
||||||
|
- Воин: +15 / +3 / +3 / +2 / +1 / +1
|
||||||
|
- Маг: +5 / +15 / +1 / +1 / +3 / +1
|
||||||
|
- Лучник: +8 / +5 / +2 / +1 / +1 / +3
|
||||||
|
- Паладин: +12 / +8 / +2 / +3 / +2 / +1
|
||||||
|
- Некромант: +6 / +12 / +1 / +1 / +4 / +1
|
||||||
|
- Берсерк: +20 / +2 / +4 / +1 / +1 / +1
|
||||||
|
- Друид: +9 / +9 / +2 / +2 / +2 / +1
|
||||||
|
|
||||||
|
**Стартовое снаряжение** выдаётся автоматически по классу: оружие, реагенты, базовая броня.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Боевая система
|
||||||
|
|
||||||
|
Пошаговые бои. Порядок: ход игрока → ход врага.
|
||||||
|
|
||||||
|
### Ход игрока
|
||||||
|
- **Атака** — базовый урон = `STR + weapon.damage`, снижается защитой врага. Шанс крит-удара (×1.5–3.0 урон) зависит от перков.
|
||||||
|
- **Заклинание** — урон/лечение по формулам с учётом `MAG`. Тратит `MP`.
|
||||||
|
- **Предмет** — использовать зелье из инвентаря (лечебное или боевое).
|
||||||
|
- **Бежать** — 50% шанс успешного побега.
|
||||||
|
|
||||||
|
### Ход врага
|
||||||
|
Враг атакует по формуле: `enemy.dmg - player.DEF` (минимум 1). Поведение зависит от AI-типа.
|
||||||
|
|
||||||
|
### Типы урона и слабости
|
||||||
|
| Тип | Иконка | Усиление при слабости |
|
||||||
|
|---|---|---|
|
||||||
|
| Физический | ⚔️ | ×1.5 |
|
||||||
|
| Огонь | 🔥 | ×2.0 |
|
||||||
|
| Лёд | ❄️ | ×2.0 |
|
||||||
|
| Святость | ✨ | ×2.0 |
|
||||||
|
| Магия | 🔮 | ×1.8 |
|
||||||
|
| Яд | ☠️ | — (DoT) |
|
||||||
|
|
||||||
|
### Статус-эффекты (DoT)
|
||||||
|
- **Яд** (poison) — наносит урон каждый ход, несколько ходов
|
||||||
|
- **Горение** (burn) — то же, но с огненным уроном
|
||||||
|
- **Заморозка** (freeze) — пропуск хода
|
||||||
|
- **Оглушение** (stunned) — пропуск одного хода
|
||||||
|
|
||||||
|
### Специальные механики
|
||||||
|
- **Неуязвимость** (`_invincible`) — атаки полностью блокируются (используется некоторыми боссами)
|
||||||
|
- **Уклонение** (shadow assassin, перк) — 35% шанс полностью уклониться
|
||||||
|
- **Фазирование** (призрак) — следующая атака игрока проходит насквозь
|
||||||
|
- **Слепота** (летучая мышь) — следующий удар игрока промахивается
|
||||||
|
- **Вампиризм** (перки, Корвус, Мрак) — восстанавливает HP от нанесённого урона
|
||||||
|
- **Смертный рывок** (паладин перк) — один раз за бой выжить с 1 HP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Заклинания
|
||||||
|
|
||||||
|
Всего **18 заклинаний**. Некоторые доступны сразу по классу, остальные выбираются при повышении уровня или находятся в свитках.
|
||||||
|
|
||||||
|
| Заклинание | MP | Тип | Эффект |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Огненный шар | 15 | 🔥 Огонь | 28 урона |
|
||||||
|
| Ледяная стрела | 12 | ❄️ Лёд | 20 урона + заморозка |
|
||||||
|
| Молния | 20 | ⚡ Электро | 35 урона |
|
||||||
|
| Исцеление | 10 | 💚 Лечение | +35 HP |
|
||||||
|
| Священный огонь | 22 | ✨ Святость | 30 урона |
|
||||||
|
| Похищение жизни | 18 | 💜 Тьма | 15 урона + 12 HP себе |
|
||||||
|
| Мощный удар | 12 | ⚔️ Физика | ×2.2 урон |
|
||||||
|
| Облако яда | 20 | ☠️ Яд | DoT яд на 4 хода |
|
||||||
|
| Огненный шторм | 30 | 🔥 Огонь | 55 урона (AoE) |
|
||||||
|
| Метель | 35 | ❄️ Лёд | 50 урона + заморозка |
|
||||||
|
| Цепная молния | 28 | ⚡ Электро | 45 урона |
|
||||||
|
| Берсеркерство | 8 | 💪 Бафф | STR ×1.8 на 8 сек |
|
||||||
|
| Каменная кожа | 20 | 🛡️ Бафф | DEF ×2 на 12 сек |
|
||||||
|
| Теневой шаг | 15 | 👁️ Бафф | SPD ×2 на 5 сек |
|
||||||
|
| Проклятие | 15 | 💀 Дебафф | ATK врага ×0.7 |
|
||||||
|
| Мощное исцеление | 25 | 💚 Лечение | +75 HP |
|
||||||
|
| Дождь стрел | 22 | 🏹 Физика | 40 урона |
|
||||||
|
| Землетрясение | 40 | 🌍 Физика | 60 урона |
|
||||||
|
| Вихрь | 18 | ⚔️ Физика | 25 урона |
|
||||||
|
| Воскрешение | 50 | 💚 Лечение | Полное восстановление HP |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Дерево перков
|
||||||
|
|
||||||
|
Каждый класс имеет **4 ветви** по **3 перка** (Tier 1–3). Перки открываются последовательно внутри ветви. За каждый уровень — +1 очко таланта.
|
||||||
|
|
||||||
|
### Примеры перков по классам
|
||||||
|
|
||||||
|
**Воин** — Ветви: Мощь, Броня, Выживание, Скорость
|
||||||
|
- Сокрушающий удар T1: +15% крит-урон
|
||||||
|
- Несокрушимый T2: +20% физической защиты
|
||||||
|
- Кровавая ярость T3: вампиризм 15% от урона
|
||||||
|
|
||||||
|
**Маг** — Ветви: Арканум, Огонь, Лёд, Внутренняя сила
|
||||||
|
- Мастер заклинаний T1: -20% стоимость MP
|
||||||
|
- Огненное сердце T2: +30% урон огнём
|
||||||
|
- Ледяная кровь T3: иммунитет к заморозке
|
||||||
|
|
||||||
|
**Лучник** — Ветви: Меткость, Выживание, Яд, Скорость
|
||||||
|
- Острый глаз T1: +25% крит-шанс
|
||||||
|
- Яд на стрелах T2: удары ядовиты
|
||||||
|
- Ураганный залп T3: шанс двойного удара
|
||||||
|
|
||||||
|
**Паладин** — Ветви: Святость, Щит, Благодать, Крестовый поход
|
||||||
|
- Аура защиты T1: +10 к DEF
|
||||||
|
- Смертный рывок T2: выжить с 1 HP (1×/бой)
|
||||||
|
- Святой гнев T3: +50% урон нежити
|
||||||
|
|
||||||
|
**Некромант** — Ветви: Тьма, Нежить, Проклятие, Душа
|
||||||
|
- Тёмный пакт T1: +20% урон заклинаниями
|
||||||
|
- Личи T2: восстановление 5 MP при убийстве
|
||||||
|
- Власть над тьмой T3: шанс оглушить врага
|
||||||
|
|
||||||
|
**Берсерк** — Ветви: Ярость, Кровь, Жестокость, Буйство
|
||||||
|
- Боевой транс T1: +20% STR при HP < 50%
|
||||||
|
- Кровожадность T2: +8% вампиризма
|
||||||
|
- Неостановимый T3: иммунитет к оглушению
|
||||||
|
|
||||||
|
**Друид** — Ветви: Природа, Зверь, Земля, Возрождение
|
||||||
|
- Слияние с природой T1: регенерация 5 HP/ход
|
||||||
|
- Зов зверя T2: +15% к скорости и уклонению
|
||||||
|
- Перерождение T3: воскреснуть с 30% HP (1×/бой)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Локации и мир
|
||||||
|
|
||||||
|
Изометрическая сетка 15×15 тайлов. Камера следует за игроком со сглаживанием.
|
||||||
|
|
||||||
|
| Локация | Тип | Погода | Особенности |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 🏘️ Деревня | Безопасная | Ясно, дождь | Стартовая. Торговец, Целитель, Стражник, Старик |
|
||||||
|
| 🌲 Лес | Опасная | Дождь, туман | Деревья, камни. Гоблины, волки, Зубастый |
|
||||||
|
| 🏚️ Подземелье | Опасная | Туман | Колонны, факелы. Скелеты, зомби, Корвус |
|
||||||
|
| 🪨 Пещера | Опасная | Туман | Кристаллы. Летучие мыши, орки, Дракон, Колосс |
|
||||||
|
| ⛰️ Горы | Опасная | Снег | Скалы. Йети, Голем, Ледяной Великан Скарр |
|
||||||
|
| 🌿 Болото | Опасная | Туман, дождь | Топи. Пауки, Ведьма, Лич, Гидра |
|
||||||
|
| 🏛️ Руины | Опасная | Туман | Столбы, факелы. Призраки, Виверны, Призрак Ирис, портал в Бездну |
|
||||||
|
| 🌑 Бездна | Опасная | Туман (всегда) | Финальная локация. Тени, Нежить, **Мрак Безликий** |
|
||||||
|
|
||||||
|
### Система погоды
|
||||||
|
- **Дождь** (rain) — анимированные капли, снижает яркость
|
||||||
|
- **Снег** (snow) — кружащиеся снежинки
|
||||||
|
- **Туман** (fog) — анимированный туман поверх карты
|
||||||
|
- **Ясно** / **Солнечно** — нормальная видимость
|
||||||
|
|
||||||
|
### День и ночь
|
||||||
|
Непрерывный цикл дня и ночи (настраиваемая скорость). Ночью карта темнее, источники света (факелы, кристаллы) отбрасывают динамические ореолы.
|
||||||
|
|
||||||
|
### Система порталов
|
||||||
|
Порталы расположены на краях карт. Обозначены иконкой ⬛. Наступить на портал — мгновенный переход в локацию. Бездна доступна только из Руин.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Враги и боссы
|
||||||
|
|
||||||
|
### Обычные враги (15 типов)
|
||||||
|
|
||||||
|
| Враг | HP | DMG | DEF | EXP | Слабость | Сопр. | ИИ |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 👺 Гоблин | 32 | 8 | 2 | 20 | Огонь | — | Трус (паникует при низком HP) |
|
||||||
|
| 🧌 Орк | 55 | 13 | 4 | 40 | Магия | Физика | Берсерк (+50% урон при HP<30%) |
|
||||||
|
| 💀 Скелет | 42 | 11 | 6 | 30 | Святость | Яд | Нейтральный |
|
||||||
|
| 🟢 Слизень | 22 | 5 | 0 | 10 | Огонь | Физика | Кислота (DoT при ударе) |
|
||||||
|
| 🗡️ Разбойник | 38 | 10 | 3 | 25 | Святость | — | Кража (крадёт золото) |
|
||||||
|
| 🐺 Волк | 30 | 9 | 2 | 18 | Огонь | — | Боевой клич (буст урона) |
|
||||||
|
| 🕷️ Паук | 28 | 12 | 1 | 22 | Огонь | Яд | Яд (отравляет 40%) |
|
||||||
|
| 🧟 Зомби | 48 | 8 | 8 | 28 | Огонь | Яд | Разложение (снижает DEF) |
|
||||||
|
| 🦇 Летучая мышь | 18 | 7 | 0 | 12 | Святость | — | Рой (слепота) |
|
||||||
|
| 👹 Тролль | 110 | 20 | 8 | 90 | Огонь | Физика | Регенерация (+5% HP/ход) |
|
||||||
|
| 🏔️ Йети | 95 | 18 | 10 | 80 | Огонь | Лёд | Берсерк (+50% урон при HP<30%) |
|
||||||
|
| 🧙 Ведьма | 60 | 22 | 3 | 70 | Святость | Магия | Исцеление (55% при HP<45%) |
|
||||||
|
| 🗿 Голем | 130 | 16 | 20 | 100 | Магия | Физика | Оглушение (28% шанс) |
|
||||||
|
| 👻 Призрак | 55 | 20 | 0 | 80 | Святость | Физика | Фазирование (эфирный план) |
|
||||||
|
| 🐉 Виверна | 95 | 26 | 7 | 140 | Лёд | Огонь | Пике (+55% урон при HP<60%) |
|
||||||
|
|
||||||
|
### Боссы (2)
|
||||||
|
|
||||||
|
| Босс | HP | DMG | DEF | EXP | Слабость | ИИ |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 🐲 Дракон | 220 | 32 | 16 | 220 | Лёд | Ярость (двойной удар при HP<40%) |
|
||||||
|
| ☠️ Лич | 140 | 38 | 10 | 200 | Святость | Призыв скелета + саморегенерация |
|
||||||
|
|
||||||
|
### Мини-боссы (6) — с боссбаром и уникальным лутом
|
||||||
|
|
||||||
|
| Мини-босс | Локация | HP | EXP | Механика | Уникальный лут |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 👑 Зубастый | Лес | 145 | 280 | Боевой клич +30% урон, призыв гоблина | Корона Зубастого |
|
||||||
|
| 💜 Корвус Некромант | Подземелье | 180 | 420 | Призыв зомби, филактерий (2 хода неуязв.), вампиризм 25% | Посох Корвуса |
|
||||||
|
| 🐍 Болотная Гидра | Болото | 210 | 360 | Регенерация 7%/ход, 2–3 головы (доп. атаки) | Клык Гидры |
|
||||||
|
| 🧊 Ледяной Великан Скарр | Горы | 200 | 390 | Метель (оглушение + бонус урон), заморозка | Топор Великана |
|
||||||
|
| 🗿 Каменный Колосс | Пещера | 260 | 440 | Непробиваемая броня каждые 3 хода, сокрушение ×2.5 | Щит Колосса |
|
||||||
|
| 🗡️ Призрак Ирис | Руины | 160 | 410 | Уклонение 35%, удар из тени ×3, яд клинка | Клинок Ирис |
|
||||||
|
|
||||||
|
### Финальный мега-босс
|
||||||
|
|
||||||
|
**🌑 Мрак Безликий** (Бездна)
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| HP | 700 |
|
||||||
|
| DMG | 55 |
|
||||||
|
| DEF | 22 |
|
||||||
|
| EXP | 3 000 |
|
||||||
|
| Gold | 800 |
|
||||||
|
| Слабость | Святость |
|
||||||
|
| Сопротивление | Магия |
|
||||||
|
|
||||||
|
**Три фазы боя:**
|
||||||
|
- **Фаза 1 (HP < 70%)** — призывает Призрака + Виверну
|
||||||
|
- **Фаза 2 (HP < 50%)** — принимает истинную форму (+35% DMG), становится неуязвим на 1 ход
|
||||||
|
- **Фаза 3 (HP < 30%)** — вампиризм 40% от урона + логируется «Мрак поглощает всё»
|
||||||
|
|
||||||
|
**Дополнительно:** 25% шанс случайного дебаффа каждый ход (яд / горение / оглушение)
|
||||||
|
|
||||||
|
**Уникальный лут:** 🌑 Корона Бездны — DEF+25, HP+80, MAG+15, STR+10 (Легендарная)
|
||||||
|
|
||||||
|
### Поведение ИИ (17 типов)
|
||||||
|
|
||||||
|
| ИИ | Поведение |
|
||||||
|
|---|---|
|
||||||
|
| `coward` | Паника при HP<20%, пропускает ход |
|
||||||
|
| `berserk` | +50% урон при HP<30% |
|
||||||
|
| `regen` | +5% HP регенерация каждый ход |
|
||||||
|
| `hex` | Самоисцеление при HP<45% (55% шанс) |
|
||||||
|
| `summon` | Призыв скелета при HP<50% |
|
||||||
|
| `warcry` | +30% урон + призыв гоблина при HP<60% |
|
||||||
|
| `necroboss` | Призыв зомби + филактерий (2 хода неуязвимости) + вампиризм |
|
||||||
|
| `hydra` | Регенерация + доп. атаки из голов (2 и 3 при HP<50%/30%) |
|
||||||
|
| `frost` | Метель (оглушение) + заморозка |
|
||||||
|
| `colossus` | Каменная броня каждые 3 хода + сокрушение |
|
||||||
|
| `shadow` | Уклонение 35% + удар из тени ×3 + яд |
|
||||||
|
| `fury` | Двойной удар при HP<40% |
|
||||||
|
| `acid` | Кислота (горение) 35% |
|
||||||
|
| `stun` | Оглушение 28% |
|
||||||
|
| `venom` | Яд 40% |
|
||||||
|
| `howl` | Усиление следующей атаки при HP<50% |
|
||||||
|
| `steal` | Кража золота 20% |
|
||||||
|
| `phase` | Фазирование (следующая атака промажет) |
|
||||||
|
| `dive` | Пике +55% урон при HP<60% |
|
||||||
|
| `decay` | Снижение DEF игрока |
|
||||||
|
| `swarm` | Слепота (следующая атака промажет) |
|
||||||
|
| `chaos` | 3-фазная система + случайные дебаффы |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NPC и диалоги
|
||||||
|
|
||||||
|
Разветвлённая диалоговая система с историей выборов, наградами и платными опциями.
|
||||||
|
|
||||||
|
| NPC | Локация | Функция |
|
||||||
|
|---|---|---|
|
||||||
|
| 🔵 Торговец | Деревня | Магазин с 18 предметами |
|
||||||
|
| 🔴 Стражник | Деревня | Квесты + советы о локациях (платный совет за 15 💰) |
|
||||||
|
| 🟢 Целитель | Деревня | Полное исцеление HP+MP за 20 💰 |
|
||||||
|
| ⚪ Старик | Деревня | Лор мира + благословение за 50 💰 |
|
||||||
|
| 🟩 Эльф | Лес | Квесты + информация об угрозах леса |
|
||||||
|
| 🟣 Шаман | Болото | Квесты + исцеление от яда |
|
||||||
|
| 🩵 Призрак | Подземелье | Квесты + тайна победы над Личем |
|
||||||
|
| 💙 Страж | Руины | Квесты + история замка |
|
||||||
|
|
||||||
|
**Диалоговые ветки** поддерживают:
|
||||||
|
- Платные опции (вычитают золото)
|
||||||
|
- Награды (опыт, предметы, баффы)
|
||||||
|
- Многоуровневые ветки (`next:` переходы)
|
||||||
|
- Отображение квест-маркеров над головами NPC (❗ новый квест / ❓ сдать / ✓ выполнен)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Квесты
|
||||||
|
|
||||||
|
### 26 квестов с цепочкой прогресса
|
||||||
|
|
||||||
|
Квесты открываются последовательно по мере выполнения предыдущих (система цепочки). Одновременно активно до 3 ближайших незавершённых квестов.
|
||||||
|
|
||||||
|
#### Квесты на убийство
|
||||||
|
| Квест | Цель | EXP | Золото |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Первая кровь | 3 гоблина | 50 | 20 |
|
||||||
|
| Охота на волков | 3 волка | 60 | 30 |
|
||||||
|
| Зачистка леса | 5 врагов | 100 | 40 |
|
||||||
|
| Проблема слизней | 3 слизня | 45 | 25 |
|
||||||
|
| Убрать разбойников | 4 разбойника | 80 | 60 |
|
||||||
|
| Армия скелетов | 4 скелета | 120 | 60 |
|
||||||
|
| Пауки болота | 5 пауков | 90 | 55 |
|
||||||
|
| Орочья угроза | 5 орков | 150 | 80 |
|
||||||
|
| Зачистка болота | 3 паука + 2 зомби | 130 | 70 |
|
||||||
|
| Бой с троллем | 1 тролль | 200 | 100 |
|
||||||
|
| Убийца дракона | 1 дракон | 500 | 300 |
|
||||||
|
| Конец некромантии | 1 Лич | 450 | 250 |
|
||||||
|
| Снежный зверь | 1 йети | 160 | 100 |
|
||||||
|
| Охота на виверн | 2 виверны | 280 | 160 |
|
||||||
|
| Упокоить призраков | 3 призрака | 200 | 100 |
|
||||||
|
| Король Гоблинов | Зубастый | 450 | 250 |
|
||||||
|
| Конец Корвуса | Корвус | 550 | 300 |
|
||||||
|
| Гидра болот | Гидра | 480 | 280 |
|
||||||
|
| Ледяной Великан | Скарр | 510 | 290 |
|
||||||
|
| Каменный Колосс | Колосс | 560 | 310 |
|
||||||
|
| Призрак Ирис | Ирис | 530 | 295 |
|
||||||
|
| **Конец Тьмы** | **Мрак Безликий** | **3000** | **500** |
|
||||||
|
|
||||||
|
#### Квесты на посещение
|
||||||
|
| Квест | Цель | EXP | Золото |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Вход в подземелье | Найти подземелье | 30 | 15 |
|
||||||
|
| Вход в пещеру | Найти пещеру | 50 | 20 |
|
||||||
|
| Исследователь | Посетить Руины | 80 | 40 |
|
||||||
|
|
||||||
|
### Сюжетные квесты
|
||||||
|
Многоэтапные квесты от именных NPC. Каждый этап имеет диалог до и после выполнения, отдельные награды за каждый этап.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Инвентарь и экипировка
|
||||||
|
|
||||||
|
### Слоты экипировки (7)
|
||||||
|
`weapon` (оружие) · `shield` (щит) · `head` (шлем) · `chest` (броня) · `legs` (поножи) · `feet` (сапоги) · `acc` (украшение)
|
||||||
|
|
||||||
|
### Предметы в инвентаре
|
||||||
|
Каждый предмет имеет:
|
||||||
|
- `rarity` — Common / Uncommon / Rare / Epic / Legendary (цветная рамка)
|
||||||
|
- `slot` — слот экипировки (если снаряжение)
|
||||||
|
- `stackable` + `qty` — стекируемые предметы (зелья, материалы)
|
||||||
|
- `enchant` — зачарование (иконка поверх предмета)
|
||||||
|
- `combatEffect` — яд/огонь при ударе (для некоторых оружий)
|
||||||
|
|
||||||
|
### Drag & Drop
|
||||||
|
Предметы в инвентаре можно **перетаскивать**:
|
||||||
|
- Между ячейками инвентаря — меняет порядок
|
||||||
|
- На слот экипировки — автоматически надевает (если подходящий тип)
|
||||||
|
|
||||||
|
### Материалы лута (22 типа)
|
||||||
|
Трава, ухо гоблина, клык орка, мясо, слизь, кость, волчья шкура, паучий яд, гнилая плоть, крыло летучей мыши, сердце тролля, шкура йети, зелье ведьмы, ядро голема, чешуя дракона, сердце дракона, эссенция призрака, чешуя виверны, яд виверны, чешуя гидры, сердце мороза, сердцевина колосса.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сеты экипировки
|
||||||
|
|
||||||
|
При ношении нескольких предметов одного сета активируются бонусы.
|
||||||
|
|
||||||
|
### ⚔️ Стальной доспех (Steel)
|
||||||
|
Предметы: стальной меч, кольчуга, стальной щит, шлем воина
|
||||||
|
- 2 предмета: +4 DEF
|
||||||
|
- 4 предмета: +6 STR, +4 DEF, +20 HP
|
||||||
|
|
||||||
|
### ✨ Посох чародея (Arcane)
|
||||||
|
Предметы: посох силы, кольцо мага, посох бури
|
||||||
|
- 1+ предмет: +5 MAG, +20 MP
|
||||||
|
|
||||||
|
### 💀 Тёмные чары (Shadow)
|
||||||
|
Предметы: посох черепа, костяной кинжал, ночной клинок
|
||||||
|
- 2 предмета: +6 MAG, +4 STR
|
||||||
|
|
||||||
|
### ⛪ Доспех паладина (Holy)
|
||||||
|
Предметы: нагрудник паладина (из магазина или крафта)
|
||||||
|
- 1 предмет: +5 DEF, +15 HP
|
||||||
|
- 2 предмета: +8 DEF, +30 HP, +4 MAG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Зачарования
|
||||||
|
|
||||||
|
Система улучшения снаряжения с расходными материалами. Открывается клавишей `E`.
|
||||||
|
|
||||||
|
| Зачарование | Эффект | Материал | Золото | Тип предмета |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 🔥 Огненный | +5 огненного урона | Чешуя дракона | 150 | Оружие |
|
||||||
|
| ❄️ Ледяной | +4 защиты | Шкура йети | 130 | Броня |
|
||||||
|
| ✨ Святой | +4 урона | Сердце тролля | 180 | Оружие |
|
||||||
|
| 💚 Жизненный | +25 HP | 3× Трава | 80 | Любое |
|
||||||
|
| 🔮 Магический | +4 MAG, +15 MP | 2× Слизь | 120 | Оружие |
|
||||||
|
| ⚡ Быстрый | +4 STR | 2× Крыло мыши | 100 | Любое |
|
||||||
|
| 🛡️ Стражника | +6 DEF | 3× Кость | 110 | Броня |
|
||||||
|
| ☠️ Ядовитый | +4 ядовитого урона | 2× Паучий яд | 70 | Оружие |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Крафт и алхимия
|
||||||
|
|
||||||
|
32 рецепта в 5 категориях. Открывается клавишей `C`.
|
||||||
|
|
||||||
|
### Зелья
|
||||||
|
| Рецепт | Ингредиенты | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| Малое зелье HP | Трава + Слизь | +40 HP |
|
||||||
|
| Среднее зелье HP | 2× Трава + Слизь | +60 HP |
|
||||||
|
| Большое зелье HP | 3× Трава + Сердце тролля | +100 HP |
|
||||||
|
| Зелье маны | 2× Крыло мыши + Паучий яд | +50 MP |
|
||||||
|
| Антидот | Трава + Волчья шкура | Снятие яда |
|
||||||
|
|
||||||
|
### Алхимия (боевые зелья)
|
||||||
|
| Рецепт | Ингредиенты | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| Зелье силы | 2× Трава + Клык орка | STR ×1.5 на 45 сек |
|
||||||
|
| Зелье камня | 2× Шкура + 2× Кость | DEF ×2 на 30 сек |
|
||||||
|
| Зелье регенерации | 3× Трава + Сердце тролля | +40 HP + рег. |
|
||||||
|
| Яд-склянка | 2× Паучий яд + Крыло | Яд на врага (3 хода) |
|
||||||
|
| Огненная колба | Чешуя дракона + Трава | 35 огненного урона |
|
||||||
|
| Оберег призраков | 2× Эссенция + 2× Трава | +30 HP, защита от нежити |
|
||||||
|
| Зелье тени | Паучий яд + Крыло мыши | Эликсир уклонения |
|
||||||
|
| Эликсир могущества | Сердце дракона + Зелье ведьмы | Легендарный усилитель всех стат |
|
||||||
|
|
||||||
|
### Снаряжение из крафта
|
||||||
|
| Рецепт | Ингредиенты | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| Чешуйчатая броня | 2× Чешуя + 2× Шкура | DEF+14, редкое |
|
||||||
|
| Посох Смерти | Посох черепа + Ядро + 3× Ухо | DMG+22, MAG+12, эпик |
|
||||||
|
| Амулет жизни | Сердце дракона + Сердце тролля | +60 HP, +30 MP, акс. |
|
||||||
|
| Нагрудник паладина | Ядро + Сердце тролля + 3× Кость | DEF+16, HP+40 |
|
||||||
|
| Посох бури | Чешуя + Шкура йети + Ядро | DMG+12, MAG+18, эпик |
|
||||||
|
| Ледяной клинок | Сердце мороза + Чешуя | DMG+25, замораживание |
|
||||||
|
| Броня Колосса | Сердцевина + Ядро | DEF+22, HP+60, легенд. |
|
||||||
|
| Кольчуга Гидры | 2× Чешуя Гидры + Чешуя виверны | DEF+18, Яд при ударе |
|
||||||
|
|
||||||
|
### Руны
|
||||||
|
| Рецепт | Ингредиенты | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| Руна Силы | Ядро голема + 2× Клык | +5 STR навсегда |
|
||||||
|
| Руна Магии | Чешуя + Некрономикон | +5 MAG навсегда |
|
||||||
|
| Руна Защиты | Шкура йети + 3× Кость | +4 DEF навсегда |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Магазин
|
||||||
|
|
||||||
|
18 предметов у Торговца в Деревне. Цены указаны в золоте.
|
||||||
|
|
||||||
|
| Предмет | Тип | Характеристики | Цена |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ⚔️ Стальной меч | Оружие | DMG+12 | 120 |
|
||||||
|
| 🪓 Боевой топор | Оружие | DMG+14 | 160 |
|
||||||
|
| 🪄 Посох силы | Оружие | DMG+5, MAG+8 | 140 |
|
||||||
|
| 🛡️ Кольчуга | Броня | DEF+8 | 110 |
|
||||||
|
| 🛡️ Стальной щит | Щит | DEF+6 | 90 |
|
||||||
|
| ⛑️ Шлем воина | Шлем | DEF+4, HP+15 | 95 |
|
||||||
|
| 👟 Сапоги ловкости | Сапоги | DEF+2, STR+3 | 75 |
|
||||||
|
| 💍 Кольцо мага | Акс. | MAG+5, MP+20 | 200 |
|
||||||
|
| 💍 Кольцо защиты | Акс. | DEF+4, armor+3 | 180 |
|
||||||
|
| 🧪 Среднее зелье HP | Зелье | +60 HP | 35 |
|
||||||
|
| 🧪 Большое зелье HP | Зелье | +100 HP | 70 |
|
||||||
|
| 🧪 Среднее зелье MP | Зелье | +40 MP | 45 |
|
||||||
|
| 📜 Свиток огня | Свиток | Заклинание: Огненный шар | 80 |
|
||||||
|
| 📜 Свиток исцеления | Свиток | Заклинание: Исцеление | 60 |
|
||||||
|
| 🗡️ Ночной клинок | Оружие | DMG+18, STR+4 (Легенд.) | 600 |
|
||||||
|
| 🌩️ Посох бури | Оружие | DMG+8, MAG+14, MP+30 | 550 |
|
||||||
|
| 🛡️ Нагрудник паладина | Броня | DEF+14, HP+30 | 500 |
|
||||||
|
| 🧪 Зелье регенерации | Зелье | +40 HP + рег. | 90 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Журнал лора
|
||||||
|
|
||||||
|
16 записок разбросаны по всем локациям. При наступлении на клетку с запиской она автоматически собирается и попадает в журнал (`L`). В журнале записки сгруппированы по локациям.
|
||||||
|
|
||||||
|
| Запись | Локация | Содержание |
|
||||||
|
|---|---|---|
|
||||||
|
| Старая записка | Деревня | Предупреждение о драконе |
|
||||||
|
| Объявление на столбе | Деревня | Розыск Зубастого |
|
||||||
|
| Эльфийский дневник | Лес | Изменения леса с появлением Шамана |
|
||||||
|
| Измятый пергамент | Лес | Предупреждение о Ведьме |
|
||||||
|
| Поваленное дерево | Лес | Указатель на логово Зубастого |
|
||||||
|
| Надпись на стене | Подземелье | Рисунок Лича, написано кровью |
|
||||||
|
| Дневник солдата | Подземелье | Записки заточённого бойца |
|
||||||
|
| Сожжённая страница | Подземелье | О захвате Корвусом подземелья |
|
||||||
|
| Записка исследователя | Пещера | Кристаллы поглощают магию |
|
||||||
|
| Рунный камень | Пещера | Загадка об огне и льде |
|
||||||
|
| Предостережение | Пещера | О Каменном Колоссе |
|
||||||
|
| Высеченный текст | Горы | История Первого Голема |
|
||||||
|
| Табличка на перевале | Горы | Предупреждение о Скарре |
|
||||||
|
| Страница гримуара | Болото | Рецепт зелья тени |
|
||||||
|
| Записка беглеца | Болото | Слабость Ведьмы |
|
||||||
|
| Болотный знак | Болото | Легенда о Гидре |
|
||||||
|
| Выцветший пергамент | Руины | Легенда об Ирис |
|
||||||
|
| Чёрный портал | Руины | Предчувствие чего-то тёмного |
|
||||||
|
| Записка Первого Героя | Бездна | Послание Эйдора I о Мраке |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Достижения
|
||||||
|
|
||||||
|
20 достижений. Открываются автоматически при выполнении условий. Тост-уведомление с анимацией. Панель достижений открывается клавишей `H`.
|
||||||
|
|
||||||
|
| Иконка | Название | Условие |
|
||||||
|
|---|---|---|
|
||||||
|
| 🩸 | Первая кровь | Убить первого врага |
|
||||||
|
| ⚔️ | Убийца | Убить 50 врагов |
|
||||||
|
| 👹 | Охотник за боссами | Убить любого мини-босса |
|
||||||
|
| 🏆 | Чемпион | Убить всех 6 мини-боссов |
|
||||||
|
| 💀 | Легенда | Убить Мрака Безликого |
|
||||||
|
| ⭐ | Опытный | Достигнуть 5 уровня |
|
||||||
|
| 🌟 | Ветеран | Достигнуть 10 уровня |
|
||||||
|
| 💰 | Богач | Накопить 500 золота |
|
||||||
|
| 👑 | Золотой король | Накопить 1000 золота |
|
||||||
|
| 🗺️ | Исследователь | Посетить все 8 локаций |
|
||||||
|
| 🌑 | Путь в бездну | Достигнуть локации Бездна |
|
||||||
|
| ⚗️ | Алхимик | Скрафтить 5 предметов |
|
||||||
|
| 📖 | Зоолог | Открыть 10 записей бестиария |
|
||||||
|
| 🛡️ | Непробиваемый | Выиграть бой без потери HP |
|
||||||
|
| 💥 | Снайпер | Нанести 10 критических ударов |
|
||||||
|
| ✨ | Чародей | Использовать заклинания 10 раз |
|
||||||
|
| 📜 | Летописец | Прочесть все записки на карте |
|
||||||
|
| 📋 | Герой | Выполнить 10 квестов |
|
||||||
|
| 🔮 | Зачарователь | Зачаровать предмет |
|
||||||
|
| 🎒 | Барахольщик | Собрать 20 предметов в инвентаре |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Бестиарий
|
||||||
|
|
||||||
|
Открывается клавишей `B`. Содержит карточки всех типов врагов. Неизвестные враги скрыты (`???`). После первого убийства открывается:
|
||||||
|
|
||||||
|
- Нарисованный портрет (canvas)
|
||||||
|
- Лорное описание
|
||||||
|
- Счётчик убийств
|
||||||
|
- Слабость и сопротивление
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Аудиосистема
|
||||||
|
|
||||||
|
Полностью процедурная генерация звука через **Web Audio API** — никаких аудиофайлов.
|
||||||
|
|
||||||
|
### Звуковые эффекты (SFX)
|
||||||
|
| Событие | Описание |
|
||||||
|
|---|---|
|
||||||
|
| Атака / крит | Sawtooth burst с экспоненциальным затуханием. Крит — выше частота |
|
||||||
|
| Заклинание | Огонь: нарастающий свип. Лёд: нисходящий. Исцеление: аккорд. Магия: арпеджио |
|
||||||
|
| Шаг | Тихий низкочастотный клик |
|
||||||
|
| Повышение уровня | Восходящее арпеджио C-E-G-C' |
|
||||||
|
| Победа | Фанфара 3 ноты |
|
||||||
|
| Смерть | Нисходящий минорный аккорд |
|
||||||
|
| Открытие сундука / записка | Короткий джингл |
|
||||||
|
|
||||||
|
### Фоновые темы (9 локаций)
|
||||||
|
| Тема | BPM | Характер |
|
||||||
|
|---|---|---|
|
||||||
|
| Деревня | 80 | C-мажорная пентатоника, спокойно |
|
||||||
|
| Лес | 95 | Арпеджированный минор, быстрее |
|
||||||
|
| Подземелье | 50 | Низкий мрачный дрон |
|
||||||
|
| Болото | 55 | Эерная хроматика, неравномерный ритм |
|
||||||
|
| Горы | 60 | Медленный эпический минор |
|
||||||
|
| Пещера | 55 | Тёмная пещерная атмосфера |
|
||||||
|
| Бой | 140 | Быстрый стаккато минор |
|
||||||
|
| Руины | — | Тихий атмосферный |
|
||||||
|
| **Бездна** | **40** | **Зловещий хроматический дрейф, контрабас** |
|
||||||
|
|
||||||
|
Кнопка 🔊 в HUD переключает звук. Темы плавно меняются при переходе между локациями.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Система сохранений
|
||||||
|
|
||||||
|
### 3 слота сохранений
|
||||||
|
Каждый слот показывает: класс персонажа, уровень, количество часов игры, дату сохранения.
|
||||||
|
|
||||||
|
### Автосохранение
|
||||||
|
Происходит автоматически: после каждого боя, после путешествия, при выполнении квеста.
|
||||||
|
|
||||||
|
### Ручное сохранение
|
||||||
|
Клавиша `P` — мгновенное сохранение с анимацией индикатора.
|
||||||
|
|
||||||
|
### Экспорт
|
||||||
|
Кнопка «📁 Папка сохранений» + «💾 Экспорт» — сохраняет все слоты в выбранную папку как JSON-файлы через File System Access API.
|
||||||
|
|
||||||
|
### Формат данных
|
||||||
|
Сохраняется: персонаж (стат, инвентарь, квесты, перки, бестиарий, найденный лор, достижения), текущая локация, время суток, день.
|
||||||
|
|
||||||
|
**Важно:** `Set`-объекты (achievements, _visited) автоматически конвертируются в `Array` при JSON-сериализации и восстанавливаются при загрузке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Визуальные эффекты
|
||||||
|
|
||||||
|
### Изометрический рендерер
|
||||||
|
- Depth sorting — все объекты отрисовываются в правильном изометрическом порядке по значению `x + y`
|
||||||
|
- Плавное движение персонажа между тайлами (линейная интерполяция)
|
||||||
|
- Боб-анимация врагов (покачивание `sin(time)`)
|
||||||
|
- Эффект атаки врага (смещение вперёд при ударе)
|
||||||
|
|
||||||
|
### Частицы
|
||||||
|
| Тип | Описание |
|
||||||
|
|---|---|
|
||||||
|
| `hit` | Красные искры при ударе |
|
||||||
|
| `heal` | Зелёные частицы при исцелении |
|
||||||
|
| `magic` | Фиолетовые звёздочки заклинаний |
|
||||||
|
| `death` | Рассеивающийся силуэт |
|
||||||
|
| `gold` | Золотые монеты |
|
||||||
|
| `holy` | Белые лучи святости |
|
||||||
|
| `fire` | Оранжевые искры |
|
||||||
|
| `ice` | Голубые кристаллы |
|
||||||
|
|
||||||
|
### Плавающий текст
|
||||||
|
Урон, лечение, критический удар, статус-эффекты — всплывают над персонажами с анимацией подъёма и затухания.
|
||||||
|
|
||||||
|
### Тряска экрана
|
||||||
|
Шейкер активируется при критических ударах и мощных ударах боссов (интенсивность 3–8 пикселей).
|
||||||
|
|
||||||
|
### Динамическое освещение
|
||||||
|
Факелы и кристаллы создают анимированные световые ореолы с пульсацией. Ночью интенсивность возрастает.
|
||||||
|
|
||||||
|
### Анимация портретов в бою
|
||||||
|
- **Дыхание**: CSS-анимация `translateY(-2px)` — игрок 3.2s, враг 2.4s
|
||||||
|
- **Моргание**: JS-интервал каждые 4–6 сек, перекрытие глаз на 120ms
|
||||||
|
|
||||||
|
### Босс-бар
|
||||||
|
При встрече с мини-боссом или финальным боссом снизу экрана появляется расширенная полоска HP с именем и анимацией входа.
|
||||||
|
|
||||||
|
### Меню
|
||||||
|
Анимированный canvas с изометрической сценой на фоне стартового экрана. Заголовок с пульсирующим золотым свечением.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Статистика контента
|
||||||
|
|
||||||
|
| Категория | Количество |
|
||||||
|
|---|---|
|
||||||
|
| Локации | 8 |
|
||||||
|
| Классы персонажей | 7 |
|
||||||
|
| Обычных врагов | 15 |
|
||||||
|
| Боссов и мини-боссов | 8 |
|
||||||
|
| Всего типов врагов | 23 |
|
||||||
|
| Типов ИИ поведения | 21 |
|
||||||
|
| Заклинаний | 19 |
|
||||||
|
| Перков в деревьях | 84 (7 классов × 4 ветви × 3 перка) |
|
||||||
|
| Квестов | 26 |
|
||||||
|
| Сюжетных квестов | +N этапных квестов |
|
||||||
|
| NPC с диалогами | 8 |
|
||||||
|
| Записок лора | 18 |
|
||||||
|
| Достижений | 20 |
|
||||||
|
| Зачарований | 8 |
|
||||||
|
| Рецептов крафта | 32 |
|
||||||
|
| Предметов в магазине | 18 |
|
||||||
|
| Уникальных материалов лута | 22 |
|
||||||
|
| Легендарных предметов | 8 |
|
||||||
|
| Сетов экипировки | 4 |
|
||||||
|
| Музыкальных тем | 9 |
|
||||||
|
| Слотов сохранения | 3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
Просто откройте `index.html` в браузере. Никаких сборщиков, серверов или зависимостей не требуется. Данные игры загружаются из `data/*.json` через `fetch`, поэтому при открытии с локального диска может потребоваться простой локальный сервер (например, Live Server в VS Code или `npx serve .`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Разработано с помощью Claude Sonnet 4.6*
|
||||||
479
audio.js
Normal file
479
audio.js
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// Audio — процедурные SFX + фоновая музыка (Web Audio API)
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
const Audio = {
|
||||||
|
ctx: null,
|
||||||
|
_master: null,
|
||||||
|
_musicGain: null, // отдельный узел для музыки — глушится при смене темы
|
||||||
|
_menuBgm: null, // HTML-аудио для mainmenu.mp3
|
||||||
|
_musicSeqId: 0,
|
||||||
|
currentTheme: null,
|
||||||
|
muted: false,
|
||||||
|
_volume: 0.6,
|
||||||
|
_lastStep: 0,
|
||||||
|
|
||||||
|
// ── Инициализация ─────────────────────────────────────────
|
||||||
|
init() {
|
||||||
|
// Создать HTML-элемент для MP3 меню (не требует AudioContext)
|
||||||
|
if (!this._menuBgm) {
|
||||||
|
const el = document.createElement('audio');
|
||||||
|
el.src = 'mainmenu.mp3';
|
||||||
|
el.loop = true;
|
||||||
|
el.volume = this._volume;
|
||||||
|
el.muted = this.muted;
|
||||||
|
this._menuBgm = el;
|
||||||
|
}
|
||||||
|
if (this.ctx) return;
|
||||||
|
try {
|
||||||
|
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
this._master = this.ctx.createGain();
|
||||||
|
this._master.gain.value = this._volume;
|
||||||
|
this._master.connect(this.ctx.destination);
|
||||||
|
// Отдельный гейн для музыкальных нот (мгновенно глушится при смене темы)
|
||||||
|
this._musicGain = this.ctx.createGain();
|
||||||
|
this._musicGain.gain.value = 1;
|
||||||
|
this._musicGain.connect(this._master);
|
||||||
|
} catch(e) { console.warn('Web Audio недоступен:', e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMute() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
this.muted = !this.muted;
|
||||||
|
this._master.gain.value = this.muted ? 0 : this._volume;
|
||||||
|
if (this._menuBgm) this._menuBgm.muted = this.muted;
|
||||||
|
const btn = document.getElementById('btn-mute');
|
||||||
|
if (btn) btn.textContent = this.muted ? '🔇' : '🔊';
|
||||||
|
},
|
||||||
|
|
||||||
|
setVolume(v) {
|
||||||
|
this._volume = Math.max(0, Math.min(1, v));
|
||||||
|
if (this._master && !this.muted) this._master.gain.value = this._volume;
|
||||||
|
if (this._menuBgm) this._menuBgm.volume = this._volume;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Низкоуровневый синтез ──────────────────────────────────
|
||||||
|
_note(freq, startTime, dur, type, gainVal, dest, filterFreq) {
|
||||||
|
if (!this.ctx || this.muted) return;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
osc.type = type || 'sine';
|
||||||
|
osc.frequency.setValueAtTime(freq, startTime);
|
||||||
|
const atk = 0.01;
|
||||||
|
const rel = Math.min(dur * 0.4, 0.15);
|
||||||
|
g.gain.setValueAtTime(0, startTime);
|
||||||
|
g.gain.linearRampToValueAtTime(gainVal, startTime + atk);
|
||||||
|
g.gain.setValueAtTime(gainVal, startTime + dur - rel);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, startTime + dur);
|
||||||
|
if (filterFreq) {
|
||||||
|
const f = this.ctx.createBiquadFilter();
|
||||||
|
f.type = 'lowpass';
|
||||||
|
f.frequency.value = filterFreq;
|
||||||
|
osc.connect(f); f.connect(g);
|
||||||
|
} else {
|
||||||
|
osc.connect(g);
|
||||||
|
}
|
||||||
|
g.connect(dest || this._master);
|
||||||
|
osc.start(startTime);
|
||||||
|
osc.stop(startTime + dur + 0.01);
|
||||||
|
},
|
||||||
|
|
||||||
|
_noise(startTime, dur, gainVal, filterFreq) {
|
||||||
|
if (!this.ctx || this.muted) return;
|
||||||
|
const bufLen = Math.ceil(this.ctx.sampleRate * dur);
|
||||||
|
const buf = this.ctx.createBuffer(1, bufLen, this.ctx.sampleRate);
|
||||||
|
const data = buf.getChannelData(0);
|
||||||
|
for (let i = 0; i < bufLen; i++) data[i] = Math.random() * 2 - 1;
|
||||||
|
const src = this.ctx.createBufferSource();
|
||||||
|
src.buffer = buf;
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
g.gain.setValueAtTime(gainVal, startTime);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, startTime + dur);
|
||||||
|
const f = this.ctx.createBiquadFilter();
|
||||||
|
f.type = 'bandpass';
|
||||||
|
f.frequency.value = filterFreq || 800;
|
||||||
|
f.Q.value = 0.5;
|
||||||
|
src.connect(f); f.connect(g); g.connect(this._master);
|
||||||
|
src.start(startTime); src.stop(startTime + dur + 0.01);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── SFX ───────────────────────────────────────────────────
|
||||||
|
playHit(crit) {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const mul = crit ? 1.6 : 1;
|
||||||
|
// удар — нисходящий шум + низкий удар
|
||||||
|
this._noise(t, 0.08, crit ? 0.3 : 0.18, 1200 * mul);
|
||||||
|
this._note(180 * mul, t, 0.1, 'sawtooth', 0.18, null, 400);
|
||||||
|
this._note(90, t + 0.04, 0.12, 'sine', 0.25, null, 300);
|
||||||
|
if (crit) {
|
||||||
|
// дополнительный хруст для крита
|
||||||
|
this._note(440, t, 0.05, 'square', 0.12);
|
||||||
|
this._note(330, t + 0.05, 0.08, 'square', 0.1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
playSpell(type) {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
switch (type) {
|
||||||
|
case 'fire': {
|
||||||
|
// восходящий пламенный свист
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(200, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(900, t + 0.35);
|
||||||
|
g.gain.setValueAtTime(0.15, t);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);
|
||||||
|
osc.connect(g); g.connect(this._master);
|
||||||
|
osc.start(t); osc.stop(t + 0.36);
|
||||||
|
this._noise(t, 0.25, 0.1, 1800);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ice': {
|
||||||
|
// нисходящий кристальный звон
|
||||||
|
[1046, 784, 523, 392].forEach((f, i) => {
|
||||||
|
this._note(f, t + i * 0.06, 0.18, 'sine', 0.12);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'heal': {
|
||||||
|
// мажорный аккорд
|
||||||
|
[261, 329, 392, 523].forEach((f, i) => {
|
||||||
|
this._note(f, t + i * 0.04, 0.4, 'sine', 0.1);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'magic':
|
||||||
|
default: {
|
||||||
|
// арпеджио вверх-вниз
|
||||||
|
const notes = [261, 329, 392, 523, 392, 329];
|
||||||
|
notes.forEach((f, i) => {
|
||||||
|
this._note(f, t + i * 0.07, 0.1, 'triangle', 0.12);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
playStep() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this._lastStep < 250) return; // дроссель
|
||||||
|
this._lastStep = now;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
this._noise(t, 0.04, 0.04, 300);
|
||||||
|
this._note(80, t, 0.04, 'sine', 0.06, null, 200);
|
||||||
|
},
|
||||||
|
|
||||||
|
playLevelUp() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
// восходящее арпеджио C-E-G-C'
|
||||||
|
[261, 329, 392, 523].forEach((f, i) => {
|
||||||
|
this._note(f, t + i * 0.12, 0.2, 'triangle', 0.15);
|
||||||
|
this._note(f * 2, t + i * 0.12 + 0.06, 0.1, 'sine', 0.07);
|
||||||
|
});
|
||||||
|
// завершение — аккорд
|
||||||
|
[523, 659, 784].forEach(f => {
|
||||||
|
this._note(f, t + 0.6, 0.5, 'sine', 0.1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
playVictory() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
// небольшая фанфара
|
||||||
|
const mel = [392, 392, 392, 523, 392, 523, 659];
|
||||||
|
const durs = [0.15, 0.15, 0.15, 0.4, 0.15, 0.15, 0.6];
|
||||||
|
let pos = 0;
|
||||||
|
mel.forEach((f, i) => {
|
||||||
|
this._note(f, t + pos, durs[i] * 0.9, 'triangle', 0.18);
|
||||||
|
this._note(f / 2, t + pos, durs[i] * 0.9, 'sine', 0.08);
|
||||||
|
pos += durs[i];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
playDeath() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
// нисходящий минорный аккорд
|
||||||
|
[220, 261, 311].forEach((f, i) => {
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
osc.type = 'sine';
|
||||||
|
osc.frequency.setValueAtTime(f, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(f * 0.5, t + 1.2);
|
||||||
|
g.gain.setValueAtTime(0.15, t);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 1.2);
|
||||||
|
osc.connect(g); g.connect(this._master);
|
||||||
|
osc.start(t); osc.stop(t + 1.25);
|
||||||
|
});
|
||||||
|
this._noise(t, 0.3, 0.08, 200);
|
||||||
|
},
|
||||||
|
|
||||||
|
playOpenChest() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
// позвякивание
|
||||||
|
[784, 1046, 1318, 1046, 1318, 1568].forEach((f, i) => {
|
||||||
|
this._note(f, t + i * 0.07, 0.15, 'triangle', 0.1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Музыкальные темы ──────────────────────────────────────
|
||||||
|
THEMES: {
|
||||||
|
// ── Главное меню: тёмная эпическая баллада, A-минор ──
|
||||||
|
menu: {
|
||||||
|
bpm: 58,
|
||||||
|
notes: [
|
||||||
|
// Фраза 1 — вступление (A3 → E4)
|
||||||
|
[220, 2, 0.070], // A3
|
||||||
|
[0, 0.5, 0 ],
|
||||||
|
[196, 0.5, 0.055], // G3
|
||||||
|
[220, 1, 0.065], // A3
|
||||||
|
[0, 0.5, 0 ],
|
||||||
|
[261, 1.5, 0.070], // C4
|
||||||
|
[329, 2, 0.080], // E4
|
||||||
|
|
||||||
|
// Фраза 2 — подъём к кульминации (G4 → A4)
|
||||||
|
[0, 0.5, 0 ],
|
||||||
|
[392, 1, 0.075], // G4
|
||||||
|
[440, 1.5, 0.090], // A4 — кульминация
|
||||||
|
|
||||||
|
// Фраза 3 — спуск (G4 → C4)
|
||||||
|
[0, 0.5, 0 ],
|
||||||
|
[392, 0.5, 0.065], // G4
|
||||||
|
[349, 0.5, 0.060], // F4
|
||||||
|
[329, 1, 0.070], // E4
|
||||||
|
[293, 0.5, 0.060], // D4
|
||||||
|
[261, 2, 0.070], // C4
|
||||||
|
|
||||||
|
// Фраза 4 — разрешение (B3 → A3)
|
||||||
|
[0, 0.5, 0 ],
|
||||||
|
[246, 0.5, 0.055], // B3
|
||||||
|
[220, 3, 0.080], // A3 — финал
|
||||||
|
[0, 2, 0 ],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[55, 4, 0.085], // A1 — тоника
|
||||||
|
[65, 4, 0.075], // C2 — параллельный мажор
|
||||||
|
[82, 4, 0.080], // E2 — доминанта
|
||||||
|
[73, 4, 0.075], // D2 — субдоминанта
|
||||||
|
[55, 6, 0.080], // A1 — разрешение
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
village: {
|
||||||
|
bpm: 80,
|
||||||
|
notes: [
|
||||||
|
// C мажорная пентатоника: C D E G A C'
|
||||||
|
[261,1,0.055],[294,0.5,0.045],[329,1,0.055],[0,0.5,0],
|
||||||
|
[392,0.5,0.045],[440,1,0.065],[523,1,0.055],[0,0.5,0],
|
||||||
|
[440,0.5,0.04],[392,0.5,0.04],[329,1,0.05],[0,0.5,0],
|
||||||
|
[294,0.5,0.04],[261,1.5,0.055],[0,2,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[65,2,0.06],[65,2,0.05],[73,2,0.06],[65,2,0.05],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
forest: {
|
||||||
|
bpm: 100,
|
||||||
|
notes: [
|
||||||
|
// a минор арпеджио
|
||||||
|
[220,0.5,0.06],[261,0.5,0.05],[329,0.5,0.06],[261,0.5,0.05],
|
||||||
|
[247,0.5,0.06],[294,0.5,0.05],[370,0.5,0.065],[294,0.5,0.05],
|
||||||
|
[220,0.5,0.06],[261,0.5,0.055],[329,1,0.06],[0,1,0],
|
||||||
|
[196,0.5,0.055],[220,0.5,0.05],[261,0.5,0.06],[220,0.5,0.05],
|
||||||
|
[196,0.5,0.055],[174,0.5,0.05],[220,1.5,0.065],[0,1,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[55,1,0.07],[55,1,0.06],[62,1,0.07],[55,1,0.06],
|
||||||
|
[49,1,0.07],[49,1,0.06],[55,2,0.065],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dungeon: {
|
||||||
|
bpm: 50,
|
||||||
|
notes: [
|
||||||
|
// мрачный хроматический дрон
|
||||||
|
[130,3,0.07],[0,1,0],[116,2,0.06],[0,2,0],
|
||||||
|
[138,3,0.065],[0,1,0],[123,2,0.06],[0,3,0],
|
||||||
|
[146,2,0.07],[130,2,0.065],[0,4,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[32,4,0.09],[32,4,0.08],[36,4,0.09],[32,8,0.07],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
swamp: {
|
||||||
|
bpm: 60,
|
||||||
|
notes: [
|
||||||
|
// жуткая хроматика, нерегулярный ритм
|
||||||
|
[185,0.5,0.05],[196,1,0.06],[0,0.5,0],[174,0.5,0.05],
|
||||||
|
[0,1.5,0],[185,0.5,0.065],[207,1,0.055],[185,0.5,0.05],
|
||||||
|
[0,2,0],[174,0.5,0.06],[164,2,0.055],[0,2,0],
|
||||||
|
[155,0.5,0.05],[0,0.5,0],[164,1.5,0.065],[0,3,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[46,3,0.08],[0,1,0],[41,3,0.07],[0,2,0],[43,4,0.075],[0,3,0],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
mountain: {
|
||||||
|
bpm: 55,
|
||||||
|
notes: [
|
||||||
|
// медленный эпичный минор
|
||||||
|
[220,2,0.07],[196,1,0.06],[174,1,0.065],[0,1,0],
|
||||||
|
[185,2,0.07],[220,2,0.065],[0,2,0],
|
||||||
|
[261,1.5,0.075],[246,0.5,0.065],[220,2,0.07],[0,1,0],
|
||||||
|
[196,1,0.065],[174,1,0.06],[185,3,0.07],[0,2,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[55,4,0.09],[46,4,0.085],[49,4,0.09],[55,4,0.085],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
combat: {
|
||||||
|
bpm: 145,
|
||||||
|
notes: [
|
||||||
|
// быстрый staccato минор
|
||||||
|
[220,0.5,0.08],[0,0.5,0],[261,0.5,0.075],[0,0.5,0],
|
||||||
|
[196,0.5,0.08],[220,1,0.085],[0,0.5,0],
|
||||||
|
[165,0.5,0.075],[185,0.5,0.08],[0,0.5,0],[220,0.5,0.075],
|
||||||
|
[0,0.5,0],[246,0.5,0.08],[220,1,0.085],[0,1,0],
|
||||||
|
[174,0.5,0.075],[196,0.5,0.08],[0,0.5,0],[220,0.5,0.075],
|
||||||
|
[0,0.5,0],[196,0.5,0.08],[174,1.5,0.085],[0,1,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[55,0.5,0.1],[0,0.5,0],[55,0.5,0.09],[0,0.5,0],
|
||||||
|
[55,0.5,0.1],[0,0.5,0],[55,0.5,0.09],[0,0.5,0],
|
||||||
|
[49,0.5,0.1],[0,0.5,0],[49,0.5,0.09],[0,0.5,0],
|
||||||
|
[49,0.5,0.1],[0,0.5,0],[55,1,0.095],[0,1,0],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
ruins: {
|
||||||
|
bpm: 45,
|
||||||
|
notes: [
|
||||||
|
// мрачная эолийская гамма, редкие ноты — атмосфера разрушенного замка
|
||||||
|
[110,3,0.055],[0,2,0],[98,2,0.05],[0,3,0],
|
||||||
|
[116,3,0.06],[110,2,0.05],[0,3,0],
|
||||||
|
[92,2,0.055],[0,2,0],[104,4,0.06],[0,4,0],
|
||||||
|
[110,2,0.05],[0,2,0],[98,3,0.055],[0,3,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[27,6,0.075],[0,2,0],[24,6,0.07],[0,4,0],
|
||||||
|
[29,6,0.075],[0,2,0],[27,4,0.065],[0,6,0],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
cave: {
|
||||||
|
bpm: 55,
|
||||||
|
notes: [
|
||||||
|
// тёмная пещерная атмосфера
|
||||||
|
[138,3,0.06],[0,2,0],[123,2,0.055],[0,3,0],
|
||||||
|
[146,3,0.065],[138,2,0.055],[0,4,0],
|
||||||
|
[116,2,0.06],[0,2,0],[130,3,0.06],[0,3,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[34,5,0.08],[0,3,0],[30,5,0.075],[0,4,0],
|
||||||
|
[36,5,0.08],[0,3,0],[34,4,0.07],[0,5,0],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
abyss: {
|
||||||
|
bpm: 40,
|
||||||
|
notes: [
|
||||||
|
// зловещий хроматический дрейф
|
||||||
|
[41,6,0.09],[0,2,0],[37,5,0.08],[0,3,0],
|
||||||
|
[44,4,0.07],[0,4,0],[39,6,0.09],[0,2,0],
|
||||||
|
[34,5,0.075],[0,3,0],[41,4,0.08],[0,4,0],
|
||||||
|
],
|
||||||
|
bass: [
|
||||||
|
[20,8,0.1],[0,4,0],[18,8,0.09],[0,4,0],
|
||||||
|
[22,8,0.1],[0,4,0],[20,6,0.09],[0,6,0],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Воспроизведение музыки ────────────────────────────────
|
||||||
|
playTheme(name) {
|
||||||
|
if (this.currentTheme === name) return;
|
||||||
|
|
||||||
|
// Тема 'menu' управляется через #menu-bgm в HTML напрямую
|
||||||
|
if (name === 'menu') {
|
||||||
|
// Остановить процедурную музыку при переходе в меню
|
||||||
|
if (this._musicGain) {
|
||||||
|
this._musicGain.disconnect();
|
||||||
|
if (this.ctx) {
|
||||||
|
this._musicGain = this.ctx.createGain();
|
||||||
|
this._musicGain.gain.value = 1;
|
||||||
|
this._musicGain.connect(this._master);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._musicSeqId++;
|
||||||
|
this.currentTheme = 'menu';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Процедурные темы — требуют AudioContext ─────────────
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
// Остановить MP3 меню
|
||||||
|
if (this._menuBgm && !this._menuBgm.paused) {
|
||||||
|
this._menuBgm.pause();
|
||||||
|
this._menuBgm.currentTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отключить старый musicGain — мгновенно глушит все запланированные ноты
|
||||||
|
if (this._musicGain) {
|
||||||
|
this._musicGain.disconnect();
|
||||||
|
}
|
||||||
|
this._musicGain = this.ctx.createGain();
|
||||||
|
this._musicGain.gain.value = 1;
|
||||||
|
this._musicGain.connect(this._master);
|
||||||
|
|
||||||
|
this.currentTheme = name;
|
||||||
|
this._musicSeqId++;
|
||||||
|
const id = this._musicSeqId;
|
||||||
|
const theme = this.THEMES[name];
|
||||||
|
if (!theme) return;
|
||||||
|
this._scheduleMelody(theme.notes, theme.bpm, id, false);
|
||||||
|
if (theme.bass) this._scheduleMelody(theme.bass, theme.bpm, id, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopMusic() {
|
||||||
|
if (this._menuBgm && !this._menuBgm.paused) {
|
||||||
|
this._menuBgm.pause();
|
||||||
|
this._menuBgm.currentTime = 0;
|
||||||
|
}
|
||||||
|
if (this._musicGain) {
|
||||||
|
this._musicGain.disconnect();
|
||||||
|
if (this.ctx) {
|
||||||
|
this._musicGain = this.ctx.createGain();
|
||||||
|
this._musicGain.gain.value = 1;
|
||||||
|
this._musicGain.connect(this._master);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._musicSeqId++;
|
||||||
|
this.currentTheme = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_scheduleMelody(notes, bpm, loopId, isBass) {
|
||||||
|
if (this._musicSeqId !== loopId) return;
|
||||||
|
if (!this.ctx || this.muted) {
|
||||||
|
// музыка заглушена — перепланировать через секунду
|
||||||
|
const beat = 60 / bpm;
|
||||||
|
const totalMs = notes.reduce((s, n) => s + n[1], 0) * beat * 1000;
|
||||||
|
setTimeout(() => this._scheduleMelody(notes, bpm, loopId, isBass), totalMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let t = this.ctx.currentTime + 0.06; // совпадает с задержкой _musicGain восстановления
|
||||||
|
const beat = 60 / bpm;
|
||||||
|
notes.forEach(([freq, beats, gainVal]) => {
|
||||||
|
if (freq > 0 && gainVal > 0) {
|
||||||
|
const dur = beats * beat * 0.88;
|
||||||
|
// ноты идут через _musicGain, а не напрямую в _master
|
||||||
|
this._note(freq, t, dur, isBass ? 'triangle' : 'sine', gainVal,
|
||||||
|
this._musicGain || null, isBass ? 300 : null);
|
||||||
|
}
|
||||||
|
t += beats * beat;
|
||||||
|
});
|
||||||
|
const totalMs = notes.reduce((s, n) => s + n[1], 0) * beat * 1000;
|
||||||
|
setTimeout(() => this._scheduleMelody(notes, bpm, loopId, isBass), totalMs + 30);
|
||||||
|
},
|
||||||
|
};
|
||||||
86
data-loader.js
Normal file
86
data-loader.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// ============================================================
|
||||||
|
// DATA-LOADER.JS — Загрузка игровых данных из JSON-файлов
|
||||||
|
// ============================================================
|
||||||
|
// Требует локальный сервер (Live Server в VS Code, или
|
||||||
|
// python -m http.server / npx serve).
|
||||||
|
// При запуске через file:// будет показана подсказка.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const DataLoader = {
|
||||||
|
_loaded: false,
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (this._loaded) return;
|
||||||
|
|
||||||
|
const FILES = [
|
||||||
|
'enemies', 'quests', 'recipes', 'shop', 'loot',
|
||||||
|
'lore', 'sets', 'enchants', 'classes', 'world',
|
||||||
|
];
|
||||||
|
|
||||||
|
let results;
|
||||||
|
try {
|
||||||
|
results = await Promise.all(
|
||||||
|
FILES.map(f =>
|
||||||
|
fetch('data/' + f + '.json')
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + f + '.json');
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('DataLoader.load() failed:', e);
|
||||||
|
this._showError(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [enemies, quests, recipes, shop, loot,
|
||||||
|
lore, sets, enchants, classes, world] = results;
|
||||||
|
|
||||||
|
// ── Заполнение RPG ──────────────────────────────────
|
||||||
|
RPG.ENEMY_DB = enemies;
|
||||||
|
RPG.QUEST_DB = quests;
|
||||||
|
RPG.CRAFT_RECIPES = recipes;
|
||||||
|
RPG.SHOP_ITEMS = shop;
|
||||||
|
RPG.LOOT_DB = loot;
|
||||||
|
RPG.LORE_NOTES = lore;
|
||||||
|
RPG.EQUIPMENT_SETS = sets;
|
||||||
|
RPG.ENCHANTS = enchants;
|
||||||
|
RPG.CLASSES = classes.classes;
|
||||||
|
RPG.SPELLS = classes.spells;
|
||||||
|
RPG.SKILLS = classes.skills;
|
||||||
|
RPG.PERK_TREE = classes.perkTree;
|
||||||
|
|
||||||
|
// ── Заполнение Game ─────────────────────────────────
|
||||||
|
Game.LOCATIONS = world.locations;
|
||||||
|
Game.NPC_DIALOGS = world.dialogs;
|
||||||
|
Game._WORLD = world; // spawns, npcs, decos, weather
|
||||||
|
|
||||||
|
this._loaded = true;
|
||||||
|
console.log('[DataLoader] Все данные загружены ✓');
|
||||||
|
},
|
||||||
|
|
||||||
|
_showError(e) {
|
||||||
|
// Удалим старые баннеры если есть
|
||||||
|
const old = document.getElementById('dl-error');
|
||||||
|
if (old) old.remove();
|
||||||
|
|
||||||
|
const isFileProtocol = location.protocol === 'file:';
|
||||||
|
const msg = isFileProtocol
|
||||||
|
? '⚠️ Игра требует локальный сервер для загрузки JSON-данных.<br>' +
|
||||||
|
'Откройте через <b>Live Server</b> (VS Code) или запустите:<br>' +
|
||||||
|
'<code>python -m http.server</code> и перейдите на <b>http://localhost:8000</b>'
|
||||||
|
: '⚠️ Ошибка загрузки данных: ' + e.message + '<br>Откройте консоль (F12) для подробностей.';
|
||||||
|
|
||||||
|
const banner = document.createElement('div');
|
||||||
|
banner.id = 'dl-error';
|
||||||
|
banner.style.cssText = [
|
||||||
|
'position:fixed', 'top:0', 'left:0', 'right:0',
|
||||||
|
'background:#8b0000', 'color:#fff', 'padding:14px 20px',
|
||||||
|
'font-size:13px', 'text-align:center', 'z-index:99999',
|
||||||
|
'line-height:1.7', 'font-family:monospace',
|
||||||
|
].join(';');
|
||||||
|
banner.innerHTML = msg;
|
||||||
|
document.body.prepend(banner);
|
||||||
|
},
|
||||||
|
};
|
||||||
241
data/classes.json
Normal file
241
data/classes.json
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
{
|
||||||
|
"classes": {
|
||||||
|
"warrior": {
|
||||||
|
"name":"Воин", "icon":"⚔️", "hp":130, "mp":30, "str":14, "def":10, "mag":4, "spd":7,
|
||||||
|
"desc":"Мощный боец с высоким HP",
|
||||||
|
"startSpells":[], "startSkills":[],
|
||||||
|
"lvlBonuses":{ "hp":15, "mp":3, "str":3, "def":2, "mag":1, "spd":1 }
|
||||||
|
},
|
||||||
|
"mage": {
|
||||||
|
"name":"Маг", "icon":"🔮", "hp":65, "mp":110, "str":4, "def":4, "mag":16, "spd":6,
|
||||||
|
"desc":"Мастер заклинаний",
|
||||||
|
"startSpells":["fireball","frostbolt"],
|
||||||
|
"lvlBonuses":{ "hp":5, "mp":15, "str":1, "def":1, "mag":3, "spd":1 }
|
||||||
|
},
|
||||||
|
"archer": {
|
||||||
|
"name":"Лучник", "icon":"🏹", "hp":90, "mp":55, "str":10, "def":6, "mag":6, "spd":13,
|
||||||
|
"desc":"Быстрый и меткий",
|
||||||
|
"startSpells":["lightning"],
|
||||||
|
"lvlBonuses":{ "hp":8, "mp":5, "str":2, "def":1, "mag":1, "spd":3 }
|
||||||
|
},
|
||||||
|
"paladin": {
|
||||||
|
"name":"Паладин", "icon":"🛡️", "hp":115, "mp":65, "str":10, "def":13, "mag":8, "spd":6,
|
||||||
|
"desc":"Святой воин-защитник",
|
||||||
|
"startSpells":["heal","holy_fire"],
|
||||||
|
"lvlBonuses":{ "hp":12, "mp":8, "str":2, "def":3, "mag":2, "spd":1 }
|
||||||
|
},
|
||||||
|
"necromancer": {
|
||||||
|
"name":"Некромант", "icon":"💀", "hp":75, "mp":95, "str":5, "def":5, "mag":15, "spd":7,
|
||||||
|
"desc":"Властелин тёмной магии",
|
||||||
|
"startSpells":["life_drain","curse"],
|
||||||
|
"lvlBonuses":{ "hp":6, "mp":12, "str":1, "def":1, "mag":4, "spd":1 }
|
||||||
|
},
|
||||||
|
"berserker": {
|
||||||
|
"name":"Берсерк", "icon":"🪓", "hp":150, "mp":20, "str":18, "def":6, "mag":2, "spd":10,
|
||||||
|
"desc":"Ярость и сила превыше всего",
|
||||||
|
"startSpells":[],
|
||||||
|
"lvlBonuses":{ "hp":20, "mp":2, "str":4, "def":1, "mag":1, "spd":1 }
|
||||||
|
},
|
||||||
|
"druid": {
|
||||||
|
"name":"Друид", "icon":"🌿", "hp":85, "mp":85, "str":7, "def":7, "mag":11, "spd":9,
|
||||||
|
"desc":"Хранитель природы",
|
||||||
|
"startSpells":["heal","poison_cloud"],
|
||||||
|
"lvlBonuses":{ "hp":9, "mp":9, "str":2, "def":2, "mag":2, "spd":1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spells": {
|
||||||
|
"fireball": { "name":"Огненный шар", "icon":"🔥", "mp":15, "dmg":28, "type":"fire", "cd":3000 },
|
||||||
|
"frostbolt": { "name":"Ледяная стрела", "icon":"❄️", "mp":12, "dmg":20, "type":"ice", "cd":2000, "slow":true },
|
||||||
|
"lightning": { "name":"Молния", "icon":"⚡", "mp":20, "dmg":35, "type":"lightning", "cd":3000 },
|
||||||
|
"fireball2": { "name":"Огненный шторм", "icon":"🌋", "mp":30, "dmg":55, "type":"fire", "cd":6000 },
|
||||||
|
"blizzard": { "name":"Метель", "icon":"🌨️", "mp":35, "dmg":50, "type":"ice", "cd":8000 },
|
||||||
|
"chain_lightning":{ "name":"Цепная молния", "icon":"⚡", "mp":28, "dmg":45, "type":"lightning", "cd":5000 },
|
||||||
|
"heal": { "name":"Исцеление", "icon":"💚", "mp":10, "heal":35, "cd":4000 },
|
||||||
|
"greater_heal": { "name":"Мощное исцеление", "icon":"💖", "mp":25, "heal":75, "cd":6000 },
|
||||||
|
"holy_fire": { "name":"Священный огонь", "icon":"✨", "mp":22, "dmg":30, "type":"holy", "cd":4000 },
|
||||||
|
"resurrect": { "name":"Воскрешение", "icon":"🕊️", "mp":50, "heal":999, "cd":60000 },
|
||||||
|
"life_drain": { "name":"Похищение жизни", "icon":"🖤", "mp":18, "dmg":15, "heal":12, "type":"dark", "cd":5000 },
|
||||||
|
"curse": { "name":"Проклятие", "icon":"☠️", "mp":15, "debuff":"atk", "val":0.7, "cd":10000 },
|
||||||
|
"power_strike": { "name":"Мощный удар", "icon":"💥", "mp":12, "dmgMult":2.2, "cd":5000 },
|
||||||
|
"berserk": { "name":"Берсеркерство", "icon":"😡", "mp":8, "buff":"str", "val":1.8, "dur":8000, "cd":15000 },
|
||||||
|
"whirlwind": { "name":"Вихрь", "icon":"🌀", "mp":18, "dmg":25, "type":"physical", "cd":5000 },
|
||||||
|
"poison_cloud": { "name":"Облако яда", "icon":"🧪", "mp":20, "dot":"poison", "dotDmg":8, "dotTurns":4, "cd":6000 },
|
||||||
|
"stone_skin": { "name":"Каменная кожа", "icon":"🪨", "mp":20, "buff":"def", "val":2, "dur":12000, "cd":20000 },
|
||||||
|
"arrow_rain": { "name":"Дождь стрел", "icon":"🏹", "mp":22, "dmg":40, "type":"physical", "cd":5000 },
|
||||||
|
"shadow_step": { "name":"Теневой шаг", "icon":"🌑", "mp":15, "buff":"spd", "val":2, "dur":5000, "cd":12000 },
|
||||||
|
"earthquake": { "name":"Землетрясение", "icon":"🌍", "mp":40, "dmg":60, "type":"physical", "cd":10000 }
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"tough_skin": { "name":"Толстая кожа", "icon":"🛡️", "desc":"+15 HP навсегда", "effect":"hp", "val":15 },
|
||||||
|
"sharp_mind": { "name":"Острый ум", "icon":"🧠", "desc":"+10 MP навсегда", "effect":"mp", "val":10 },
|
||||||
|
"quick_feet": { "name":"Быстрые ноги", "icon":"👟", "desc":"+2 к скорости", "effect":"spd", "val":2 },
|
||||||
|
"iron_will": { "name":"Железная воля", "icon":"⚡", "desc":"+3 к силе атаки", "effect":"str", "val":3 },
|
||||||
|
"arcane_mastery": { "name":"Магия мастера", "icon":"✨", "desc":"+3 к магии", "effect":"mag", "val":3 },
|
||||||
|
"fortify": { "name":"Укрепление", "icon":"🪬", "desc":"+3 к защите", "effect":"def", "val":3 },
|
||||||
|
"learn_power_strike": { "name":"Мощный удар", "icon":"💥", "desc":"Изучить: Мощный удар", "effect":"spell", "val":"power_strike" },
|
||||||
|
"learn_fireball2": { "name":"Огненный шторм", "icon":"🌋", "desc":"Изучить: Огненный шторм", "effect":"spell", "val":"fireball2" },
|
||||||
|
"learn_berserk": { "name":"Берсеркерство", "icon":"😡", "desc":"Изучить: Берсеркерство", "effect":"spell", "val":"berserk" },
|
||||||
|
"learn_greater_heal": { "name":"Мощное исцеление", "icon":"💖", "desc":"Изучить: Мощное исцеление", "effect":"spell", "val":"greater_heal" },
|
||||||
|
"learn_blizzard": { "name":"Метель", "icon":"🌨️", "desc":"Изучить: Метель", "effect":"spell", "val":"blizzard" },
|
||||||
|
"learn_stone_skin": { "name":"Каменная кожа", "icon":"🪨", "desc":"Изучить: Каменная кожа", "effect":"spell", "val":"stone_skin" },
|
||||||
|
"learn_earthquake": { "name":"Землетрясение", "icon":"🌍", "desc":"Изучить: Землетрясение", "effect":"spell", "val":"earthquake" },
|
||||||
|
"learn_arrow_rain": { "name":"Дождь стрел", "icon":"🏹", "desc":"Изучить: Дождь стрел", "effect":"spell", "val":"arrow_rain" },
|
||||||
|
"learn_chain_lightning":{ "name":"Цепная молния", "icon":"⚡", "desc":"Изучить: Цепная молния", "effect":"spell", "val":"chain_lightning" }
|
||||||
|
},
|
||||||
|
"perkTree": {
|
||||||
|
"warrior": { "branches": [
|
||||||
|
{ "id":"might", "name":"Мощь", "icon":"⚔️", "perks":[
|
||||||
|
{ "id":"war_str1", "tier":1, "name":"Грубая сила", "icon":"💪", "desc":"+4 СИЛ", "effect":"stat", "stat":"str", "val":4 },
|
||||||
|
{ "id":"war_crit", "tier":2, "name":"Мощный удар", "icon":"💥", "desc":"+10% к крит. урону", "effect":"critDmg", "val":0.10 },
|
||||||
|
{ "id":"war_dbl", "tier":3, "name":"Двойной удар", "icon":"⚔️", "desc":"15% шанс ударить дважды", "effect":"doubleAtk", "val":0.15 }
|
||||||
|
]},
|
||||||
|
{ "id":"armor", "name":"Броня", "icon":"🛡️", "perks":[
|
||||||
|
{ "id":"war_def1", "tier":1, "name":"Закалка", "icon":"🛡️", "desc":"+4 ЗАЩ", "effect":"stat", "stat":"def", "val":4 },
|
||||||
|
{ "id":"war_hp", "tier":2, "name":"Несгибаемый", "icon":"🪨", "desc":"+25 макс. HP", "effect":"stat", "stat":"maxHp", "val":25 },
|
||||||
|
{ "id":"war_thorn", "tier":3, "name":"Шипы", "icon":"🌵", "desc":"10% отражение урона", "effect":"thorns", "val":0.10 }
|
||||||
|
]},
|
||||||
|
{ "id":"survival", "name":"Выживание", "icon":"❤️", "perks":[
|
||||||
|
{ "id":"war_life", "tier":1, "name":"Жизненная сила", "icon":"🩸", "desc":"+6% вампиризм", "effect":"lifesteal", "val":0.06 },
|
||||||
|
{ "id":"war_regen", "tier":2, "name":"Регенерация", "icon":"💚", "desc":"Восст. 4 HP за ход", "effect":"regenHp", "val":4 },
|
||||||
|
{ "id":"war_save", "tier":3, "name":"Несмертельный", "icon":"🛡️", "desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 }
|
||||||
|
]},
|
||||||
|
{ "id":"speed", "name":"Скорость", "icon":"💨", "perks":[
|
||||||
|
{ "id":"war_spd1", "tier":1, "name":"Быстрые ноги", "icon":"👟", "desc":"+3 СКР", "effect":"stat", "stat":"spd", "val":3 },
|
||||||
|
{ "id":"war_dodge", "tier":2, "name":"Уклонение", "icon":"💨", "desc":"+8% шанс уклониться", "effect":"dodge", "val":0.08 },
|
||||||
|
{ "id":"war_rage", "tier":3, "name":"Ярость", "icon":"😡", "desc":"+20% урон при <30% HP", "effect":"enrage", "val":0.20 }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
"mage": { "branches": [
|
||||||
|
{ "id":"arcane", "name":"Арканум", "icon":"🔮", "perks":[
|
||||||
|
{ "id":"mag_mag1", "tier":1, "name":"Знание рун", "icon":"📜", "desc":"+5 МАГ", "effect":"stat", "stat":"mag", "val":5 },
|
||||||
|
{ "id":"mag_mp", "tier":2, "name":"Мана-источник", "icon":"💧", "desc":"+25 макс. МА", "effect":"stat", "stat":"maxMp", "val":25 },
|
||||||
|
{ "id":"mag_spell", "tier":3, "name":"Могущество", "icon":"✨", "desc":"+18% к урону заклинаний", "effect":"spelldmg", "val":0.18 }
|
||||||
|
]},
|
||||||
|
{ "id":"fire", "name":"Огонь", "icon":"🔥", "perks":[
|
||||||
|
{ "id":"mag_mag2", "tier":1, "name":"Жар пламени", "icon":"🔥", "desc":"+3 МАГ", "effect":"stat", "stat":"mag", "val":3 },
|
||||||
|
{ "id":"mag_crit", "tier":2, "name":"Пылающий крит", "icon":"💥", "desc":"+12% к крит. урону", "effect":"critDmg", "val":0.12 },
|
||||||
|
{ "id":"mag_spl2", "tier":3, "name":"Испепеление", "icon":"☄️", "desc":"+20% урона заклинаний", "effect":"spelldmg", "val":0.20 }
|
||||||
|
]},
|
||||||
|
{ "id":"frost", "name":"Лёд", "icon":"❄️", "perks":[
|
||||||
|
{ "id":"mag_spd", "tier":1, "name":"Ледяная скорость", "icon":"⚡","desc":"+3 СКР", "effect":"stat", "stat":"spd", "val":3 },
|
||||||
|
{ "id":"mag_dodge", "tier":2, "name":"Морозное скольжение", "icon":"❄️","desc":"+10% уклонение", "effect":"dodge", "val":0.10 },
|
||||||
|
{ "id":"mag_mpx", "tier":3, "name":"Ледяная броня", "icon":"🧊","desc":"+30 макс. МА", "effect":"stat", "stat":"maxMp", "val":30 }
|
||||||
|
]},
|
||||||
|
{ "id":"inner", "name":"Внутр. сила","icon":"💜", "perks":[
|
||||||
|
{ "id":"mag_def", "tier":1, "name":"Магический щит", "icon":"🛡️", "desc":"+3 ЗАЩ", "effect":"stat", "stat":"def", "val":3 },
|
||||||
|
{ "id":"mag_life", "tier":2, "name":"Маговампир", "icon":"🩸", "desc":"+5% вампиризм", "effect":"lifesteal", "val":0.05 },
|
||||||
|
{ "id":"mag_save", "tier":3, "name":"Последний резерв","icon":"💜","desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
"archer": { "branches": [
|
||||||
|
{ "id":"precision", "name":"Меткость", "icon":"🎯", "perks":[
|
||||||
|
{ "id":"arc_spd1", "tier":1, "name":"Орлиный глаз", "icon":"👁️", "desc":"+4 СКР", "effect":"stat", "stat":"spd", "val":4 },
|
||||||
|
{ "id":"arc_crit", "tier":2, "name":"Смертельный выстрел","icon":"🎯","desc":"+12% крит. урон", "effect":"critDmg", "val":0.12 },
|
||||||
|
{ "id":"arc_dbl", "tier":3, "name":"Двойной выстрел", "icon":"🏹", "desc":"20% двойная атака", "effect":"doubleAtk", "val":0.20 }
|
||||||
|
]},
|
||||||
|
{ "id":"survival", "name":"Выживание", "icon":"🌿", "perks":[
|
||||||
|
{ "id":"arc_dodge", "tier":1, "name":"Стремительный", "icon":"💨", "desc":"+10% уклонение", "effect":"dodge", "val":0.10 },
|
||||||
|
{ "id":"arc_life", "tier":2, "name":"Кровавая стрела","icon":"🩸", "desc":"+6% вампиризм", "effect":"lifesteal", "val":0.06 },
|
||||||
|
{ "id":"arc_save", "tier":3, "name":"Уйти живым", "icon":"🛡️", "desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 }
|
||||||
|
]},
|
||||||
|
{ "id":"poison", "name":"Яд", "icon":"☠️", "perks":[
|
||||||
|
{ "id":"arc_def", "tier":1, "name":"Кожаная броня", "icon":"🛡️", "desc":"+3 ЗАЩ", "effect":"stat", "stat":"def", "val":3 },
|
||||||
|
{ "id":"arc_thorn", "tier":2, "name":"Ядовитые шипы", "icon":"🌵", "desc":"+8% отражение урона", "effect":"thorns", "val":0.08 },
|
||||||
|
{ "id":"arc_rage", "tier":3, "name":"Охотничья ярость","icon":"😡","desc":"+18% урон при <30% HP", "effect":"enrage", "val":0.18 }
|
||||||
|
]},
|
||||||
|
{ "id":"speed", "name":"Скорость", "icon":"⚡", "perks":[
|
||||||
|
{ "id":"arc_spd2", "tier":1, "name":"Ветер", "icon":"🌬️", "desc":"+5 СКР", "effect":"stat", "stat":"spd", "val":5 },
|
||||||
|
{ "id":"arc_str", "tier":2, "name":"Твёрдая рука", "icon":"💪", "desc":"+3 СИЛ", "effect":"stat", "stat":"str", "val":3 },
|
||||||
|
{ "id":"arc_dg2", "tier":3, "name":"Тень", "icon":"🌑", "desc":"+8% уклонение", "effect":"dodge", "val":0.08 }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
"paladin": { "branches": [
|
||||||
|
{ "id":"holy", "name":"Святость", "icon":"✨", "perks":[
|
||||||
|
{ "id":"pal_mag", "tier":1, "name":"Свет веры", "icon":"☀️", "desc":"+5 МАГ", "effect":"stat", "stat":"mag", "val":5 },
|
||||||
|
{ "id":"pal_spell", "tier":2, "name":"Святой свет", "icon":"✨", "desc":"+12% урона заклинаний", "effect":"spelldmg", "val":0.12 },
|
||||||
|
{ "id":"pal_regen", "tier":3, "name":"Благодать", "icon":"💚", "desc":"Восст. 5 HP за ход", "effect":"regenHp", "val":5 }
|
||||||
|
]},
|
||||||
|
{ "id":"shield", "name":"Щит", "icon":"🛡️", "perks":[
|
||||||
|
{ "id":"pal_def1", "tier":1, "name":"Мастер щита", "icon":"🛡️", "desc":"+5 ЗАЩ", "effect":"stat", "stat":"def", "val":5 },
|
||||||
|
{ "id":"pal_hp", "tier":2, "name":"Крепость духа", "icon":"🏰", "desc":"+25 макс. HP", "effect":"stat", "stat":"maxHp", "val":25 },
|
||||||
|
{ "id":"pal_thorn", "tier":3, "name":"Сталь и огонь", "icon":"🔥", "desc":"+12% отражение урона", "effect":"thorns", "val":0.12 }
|
||||||
|
]},
|
||||||
|
{ "id":"divine", "name":"Благодать", "icon":"💛", "perks":[
|
||||||
|
{ "id":"pal_hpmp", "tier":1, "name":"Тело и душа", "icon":"💛", "desc":"+15 HP, +15 МА", "effect":"stat", "stat":"maxHp", "val":15 },
|
||||||
|
{ "id":"pal_save", "tier":2, "name":"Воля Небес", "icon":"🕊️", "desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 },
|
||||||
|
{ "id":"pal_life", "tier":3, "name":"Священный вампир","icon":"🩸","desc":"+5% вампиризм", "effect":"lifesteal", "val":0.05 }
|
||||||
|
]},
|
||||||
|
{ "id":"crusade", "name":"Крестовый поход", "icon":"⚔️", "perks":[
|
||||||
|
{ "id":"pal_str", "tier":1, "name":"Кара Небес", "icon":"⚔️", "desc":"+4 СИЛ", "effect":"stat", "stat":"str", "val":4 },
|
||||||
|
{ "id":"pal_crit", "tier":2, "name":"Удар справедливости", "icon":"💥", "desc":"+10% крит. урон", "effect":"critDmg", "val":0.10 },
|
||||||
|
{ "id":"pal_dbl", "tier":3, "name":"Двойной удар", "icon":"⚔️", "desc":"+12% двойная атака","effect":"doubleAtk", "val":0.12 }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
"necromancer": { "branches": [
|
||||||
|
{ "id":"dark", "name":"Тьма", "icon":"🌑", "perks":[
|
||||||
|
{ "id":"nec_mag1", "tier":1, "name":"Тёмное знание", "icon":"📕", "desc":"+5 МАГ", "effect":"stat", "stat":"mag", "val":5 },
|
||||||
|
{ "id":"nec_spell", "tier":2, "name":"Сила смерти", "icon":"💀", "desc":"+18% урона заклинаний", "effect":"spelldmg", "val":0.18 },
|
||||||
|
{ "id":"nec_crit", "tier":3, "name":"Смертельное слово","icon":"💥","desc":"+12% крит. урон", "effect":"critDmg", "val":0.12 }
|
||||||
|
]},
|
||||||
|
{ "id":"undead", "name":"Нежить", "icon":"🦴", "perks":[
|
||||||
|
{ "id":"nec_hp", "tier":1, "name":"Труп-тело", "icon":"🦴", "desc":"+20 макс. HP", "effect":"stat", "stat":"maxHp", "val":20 },
|
||||||
|
{ "id":"nec_life", "tier":2, "name":"Поглощение жизни","icon":"🩸","desc":"+8% вампиризм", "effect":"lifesteal", "val":0.08 },
|
||||||
|
{ "id":"nec_save", "tier":3, "name":"Личная смерть", "icon":"💀", "desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 }
|
||||||
|
]},
|
||||||
|
{ "id":"curse", "name":"Проклятие", "icon":"🔮", "perks":[
|
||||||
|
{ "id":"nec_def", "tier":1, "name":"Тёмный покров", "icon":"🌑", "desc":"+3 ЗАЩ", "effect":"stat", "stat":"def", "val":3 },
|
||||||
|
{ "id":"nec_thorn", "tier":2, "name":"Проклятое тело", "icon":"☠️", "desc":"+8% отражение урона", "effect":"thorns", "val":0.08 },
|
||||||
|
{ "id":"nec_rage", "tier":3, "name":"Ярость тьмы", "icon":"😡", "desc":"+25% урон при <30% HP", "effect":"enrage", "val":0.25 }
|
||||||
|
]},
|
||||||
|
{ "id":"soul", "name":"Душа", "icon":"👻", "perks":[
|
||||||
|
{ "id":"nec_mp", "tier":1, "name":"Душехранитель", "icon":"👻", "desc":"+15 макс. МА", "effect":"stat", "stat":"maxMp", "val":15 },
|
||||||
|
{ "id":"nec_regen", "tier":2, "name":"Восстановление", "icon":"💚", "desc":"Восст. 4 HP за ход", "effect":"regenHp", "val":4 },
|
||||||
|
{ "id":"nec_dodge", "tier":3, "name":"Призрачность", "icon":"💨", "desc":"+10% уклонение", "effect":"dodge", "val":0.10 }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
"berserker": { "branches": [
|
||||||
|
{ "id":"rage", "name":"Ярость", "icon":"😡", "perks":[
|
||||||
|
{ "id":"ber_str1", "tier":1, "name":"Бешеная сила", "icon":"💪", "desc":"+5 СИЛ", "effect":"stat", "stat":"str", "val":5 },
|
||||||
|
{ "id":"ber_rage", "tier":2, "name":"Берсеркерская ярость","icon":"🔥","desc":"+25% урон при <30% HP","effect":"enrage", "val":0.25 },
|
||||||
|
{ "id":"ber_dbl", "tier":3, "name":"Смертельный вихрь", "icon":"⚔️","desc":"20% двойная атака", "effect":"doubleAtk", "val":0.20 }
|
||||||
|
]},
|
||||||
|
{ "id":"blood", "name":"Кровь", "icon":"🩸", "perks":[
|
||||||
|
{ "id":"ber_life", "tier":1, "name":"Кровожадность", "icon":"🩸", "desc":"+8% вампиризм", "effect":"lifesteal", "val":0.08 },
|
||||||
|
{ "id":"ber_regen", "tier":2, "name":"Регенерация крови", "icon":"💚","desc":"Восст. 5 HP за ход", "effect":"regenHp", "val":5 },
|
||||||
|
{ "id":"ber_save", "tier":3, "name":"Воля к жизни", "icon":"🛡️","desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 }
|
||||||
|
]},
|
||||||
|
{ "id":"brutality", "name":"Жестокость", "icon":"🪓", "perks":[
|
||||||
|
{ "id":"ber_crit", "tier":1, "name":"Brutal удар", "icon":"💥", "desc":"+12% крит. урон", "effect":"critDmg", "val":0.12 },
|
||||||
|
{ "id":"ber_thorn", "tier":2, "name":"Кровавые шипы", "icon":"🌵", "desc":"+10% отражение урона", "effect":"thorns", "val":0.10 },
|
||||||
|
{ "id":"ber_str2", "tier":3, "name":"Безудержная сила","icon":"💪","desc":"+5 СИЛ", "effect":"stat", "stat":"str", "val":5 }
|
||||||
|
]},
|
||||||
|
{ "id":"berserking","name":"Буйство", "icon":"⚡", "perks":[
|
||||||
|
{ "id":"ber_spd", "tier":1, "name":"Ветер смерти", "icon":"💨", "desc":"+5 СКР", "effect":"stat", "stat":"spd", "val":5 },
|
||||||
|
{ "id":"ber_dodge", "tier":2, "name":"Дикое уклонение","icon":"💨", "desc":"+10% уклонение", "effect":"dodge", "val":0.10 },
|
||||||
|
{ "id":"ber_rage2", "tier":3, "name":"Боевое безумие", "icon":"😡", "desc":"+15% урон при <30% HP", "effect":"enrage", "val":0.15 }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
"druid": { "branches": [
|
||||||
|
{ "id":"nature", "name":"Природа", "icon":"🌿", "perks":[
|
||||||
|
{ "id":"dru_mag", "tier":1, "name":"Голос леса", "icon":"🌿", "desc":"+4 МАГ", "effect":"stat", "stat":"mag", "val":4 },
|
||||||
|
{ "id":"dru_spell", "tier":2, "name":"Природная магия", "icon":"✨", "desc":"+12% урона заклинаний", "effect":"spelldmg", "val":0.12 },
|
||||||
|
{ "id":"dru_regen", "tier":3, "name":"Дыхание жизни", "icon":"💚", "desc":"Восст. 5 HP за ход", "effect":"regenHp", "val":5 }
|
||||||
|
]},
|
||||||
|
{ "id":"beast", "name":"Зверь", "icon":"🐾", "perks":[
|
||||||
|
{ "id":"dru_spd", "tier":1, "name":"Звериная прыть", "icon":"🐾", "desc":"+5 СКР", "effect":"stat", "stat":"spd", "val":5 },
|
||||||
|
{ "id":"dru_dodge", "tier":2, "name":"Уклонение зверя","icon":"💨", "desc":"+10% уклонение", "effect":"dodge", "val":0.10 },
|
||||||
|
{ "id":"dru_str", "tier":3, "name":"Когти зверя", "icon":"🐻", "desc":"+4 СИЛ", "effect":"stat", "stat":"str", "val":4 }
|
||||||
|
]},
|
||||||
|
{ "id":"earth", "name":"Земля", "icon":"🪨", "perks":[
|
||||||
|
{ "id":"dru_def", "tier":1, "name":"Кора дерева", "icon":"🪨", "desc":"+5 ЗАЩ", "effect":"stat", "stat":"def", "val":5 },
|
||||||
|
{ "id":"dru_hp", "tier":2, "name":"Корни земли", "icon":"🌱", "desc":"+20 макс. HP", "effect":"stat", "stat":"maxHp", "val":20 },
|
||||||
|
{ "id":"dru_thorn", "tier":3, "name":"Шипастая кора","icon":"🌵","desc":"+8% отражение урона", "effect":"thorns", "val":0.08 }
|
||||||
|
]},
|
||||||
|
{ "id":"regrowth", "name":"Возрождение","icon":"🔄", "perks":[
|
||||||
|
{ "id":"dru_life", "tier":1, "name":"Жизненная сила","icon":"🩸", "desc":"+6% вампиризм", "effect":"lifesteal", "val":0.06 },
|
||||||
|
{ "id":"dru_rg2", "tier":2, "name":"Лесное зелье", "icon":"💚", "desc":"Восст. 4 HP за ход", "effect":"regenHp", "val":4 },
|
||||||
|
{ "id":"dru_save", "tier":3, "name":"Перерождение", "icon":"🔄", "desc":"1×/бой выжить с 1 HP", "effect":"deathSave", "val":1 }
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
data/enchants.json
Normal file
10
data/enchants.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"flame": { "name":"Огненный", "icon":"🔥", "cost":150, "mat":"dragon_scale", "matQty":1, "bonus":{"damage":5}, "desc":"+5 урона огнём", "target":"weapon" },
|
||||||
|
"frost": { "name":"Ледяной", "icon":"❄️", "cost":130, "mat":"yeti_fur", "matQty":1, "bonus":{"defense":4}, "desc":"+4 защиты", "target":"armor" },
|
||||||
|
"holy": { "name":"Святой", "icon":"✨", "cost":180, "mat":"troll_heart", "matQty":1, "bonus":{"damage":4}, "desc":"+4 урона", "target":"weapon" },
|
||||||
|
"life": { "name":"Жизненный", "icon":"💚", "cost":80, "mat":"herb", "matQty":3, "bonus":{"hp":25}, "desc":"+25 HP", "target":"any" },
|
||||||
|
"arcane": { "name":"Магический", "icon":"💜", "cost":120, "mat":"slime_gel", "matQty":2, "bonus":{"mag":4, "mp":15}, "desc":"+4 магии, +15 MP", "target":"weapon" },
|
||||||
|
"swift": { "name":"Быстрый", "icon":"⚡", "cost":100, "mat":"bat_wing", "matQty":2, "bonus":{"str":4}, "desc":"+4 силы", "target":"any" },
|
||||||
|
"ward": { "name":"Стражника", "icon":"🛡️", "cost":110, "mat":"bone", "matQty":3, "bonus":{"defense":6}, "desc":"+6 защиты", "target":"armor" },
|
||||||
|
"venom": { "name":"Ядовитый", "icon":"☠️", "cost":70, "mat":"spider_venom", "matQty":2, "bonus":{"damage":4}, "desc":"+4 урона ядом", "target":"weapon" }
|
||||||
|
}
|
||||||
109
data/enemies.json
Normal file
109
data/enemies.json
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"goblin": { "name":"Гоблин", "hp":32, "dmg":8, "def":2, "exp":20, "gold":10, "loot":["herb","goblin_ear"],
|
||||||
|
"lore":"Хитрые зелёные существа, живущие стаями. Воруют всё, до чего дотянутся. Трусливы поодиночке, опасны в толпе.",
|
||||||
|
"weakness":"fire", "resist":null, "ai":"coward" },
|
||||||
|
"orc": { "name":"Орк", "hp":55, "dmg":13, "def":4, "exp":40, "gold":25, "loot":["orc_tusk","meat"],
|
||||||
|
"lore":"Могучие воители из горных кланов. Живут по закону силы — вождём становится сильнейший боец.",
|
||||||
|
"weakness":"magic", "resist":"physical", "ai":"berserk" },
|
||||||
|
"skeleton": { "name":"Скелет", "hp":42, "dmg":11, "def":6, "exp":30, "gold":15, "loot":["bone","bone_dagger"],
|
||||||
|
"lore":"Мертвецы, поднятые некромантами. Лишены боли и страха. Разваливаются от святого оружия.",
|
||||||
|
"weakness":"holy", "resist":"poison", "ai":null },
|
||||||
|
"slime": { "name":"Слизень", "hp":22, "dmg":5, "def":0, "exp":10, "gold":5, "loot":["slime_gel"],
|
||||||
|
"lore":"Аморфное существо из кислотной слизи. Не имеет органов, поглощает пищу целиком. Разделяется при ударе.",
|
||||||
|
"weakness":"fire", "resist":"physical", "ai":"acid" },
|
||||||
|
"bandit": { "name":"Разбойник", "hp":38, "dmg":10, "def":3, "exp":25, "gold":22, "loot":["money_pouch"],
|
||||||
|
"lore":"Бывшие солдаты и беглые крестьяне, избравшие путь разбоя. Опасны засадами и ядовитыми клинками.",
|
||||||
|
"weakness":"holy", "resist":null, "ai":"steal" },
|
||||||
|
"wolf": { "name":"Волк", "hp":30, "dmg":9, "def":2, "exp":18, "gold":8, "loot":["wolf_pelt"],
|
||||||
|
"lore":"Лесной хищник с острым чутьём. Охотится стаей, загоняя жертву. Вожак стаи втрое крупнее обычного волка.",
|
||||||
|
"weakness":"fire", "resist":null, "ai":"howl" },
|
||||||
|
"spider": { "name":"Паук", "hp":28, "dmg":12, "def":1, "exp":22, "gold":12, "loot":["spider_venom"],
|
||||||
|
"lore":"Ядовитый паук размером с собаку. Плетёт невидимые сети и вводит парализующий яд.",
|
||||||
|
"weakness":"fire", "resist":"poison", "ai":"venom" },
|
||||||
|
"zombie": { "name":"Зомби", "hp":48, "dmg":8, "def":8, "exp":28, "gold":12, "loot":["rot_flesh"],
|
||||||
|
"lore":"Восставшие мертвецы с гниющей плотью. Медлительны, но чрезвычайно живучи. Укус передаёт заразу.",
|
||||||
|
"weakness":"fire", "resist":"poison", "ai":"decay" },
|
||||||
|
"bat": { "name":"Летучая мышь","hp":18, "dmg":7, "def":0, "exp":12, "gold":6, "loot":["bat_wing"],
|
||||||
|
"lore":"Пещерные твари с эхолокацией. В темноте видят лучше любого существа. Нападают роем.",
|
||||||
|
"weakness":"holy", "resist":null, "ai":"swarm" },
|
||||||
|
"troll": { "name":"Тролль", "hp":110, "dmg":20, "def":8, "exp":90, "gold":65, "loot":["troll_heart","club"], "isBoss":false,
|
||||||
|
"lore":"Регенерирующий великан из болот. Плоть зарастает на глазах. Только огонь останавливает регенерацию.",
|
||||||
|
"weakness":"fire", "resist":"physical", "ai":"regen" },
|
||||||
|
"yeti": { "name":"Йети", "hp":95, "dmg":18, "def":10, "exp":80, "gold":55, "loot":["yeti_fur"],
|
||||||
|
"lore":"Белый великан горных вершин. Выдерживает любой мороз, но уязвим к огню. Ревниво охраняет свою территорию.",
|
||||||
|
"weakness":"fire", "resist":"ice", "ai":"berserk" },
|
||||||
|
"witch": { "name":"Ведьма", "hp":60, "dmg":22, "def":3, "exp":70, "gold":50, "loot":["witch_brew"], "hasMp":true,
|
||||||
|
"lore":"Колдунья, заключившая договор с тёмными силами. Варит яды и проклятия. Способна проклясть весь отряд.",
|
||||||
|
"weakness":"holy", "resist":"magic", "ai":"hex" },
|
||||||
|
"golem": { "name":"Голем", "hp":130, "dmg":16, "def":20, "exp":100, "gold":80, "loot":["golem_core"], "isBoss":false,
|
||||||
|
"lore":"Магический конструкт из камня и металла. Создан алхимиками для охраны. Разрушить можно только магией.",
|
||||||
|
"weakness":"magic", "resist":"physical", "ai":"stun" },
|
||||||
|
"dragon": { "name":"Дракон", "hp":220, "dmg":32, "def":16, "exp":220, "gold":220, "loot":["dragon_scale","dragon_heart"], "isBoss":true,
|
||||||
|
"lore":"Древнее создание с интеллектом мага и силой армии. Живёт тысячелетиями. Огонь его дыхания плавит доспехи.",
|
||||||
|
"weakness":"ice", "resist":"fire", "ai":"fury" },
|
||||||
|
"lich": { "name":"Лич", "hp":140, "dmg":38, "def":10, "exp":200, "gold":150, "loot":["necronomicon","skull_staff"], "isBoss":true, "hasMp":true,
|
||||||
|
"lore":"Некромант, победивший смерть ценой души. Хранит жизненную силу в филактерии. Повелевает армиями мертвецов.",
|
||||||
|
"weakness":"holy", "resist":"magic", "ai":"summon" },
|
||||||
|
"ghost": { "name":"Призрак", "hp":55, "dmg":20, "def":0, "exp":80, "gold":35, "loot":["ghost_essence"],
|
||||||
|
"lore":"Душа воина, погибшего в Руинах. Способна уходить в эфирный план, становясь неуязвимой на мгновение.",
|
||||||
|
"weakness":"holy", "resist":"physical", "ai":"phase" },
|
||||||
|
"wyvern": { "name":"Виверна", "hp":95, "dmg":26, "def":7, "exp":140, "gold":95, "loot":["wyvern_scale","wyvern_poison"],
|
||||||
|
"lore":"Двукрылая родственница дракона. Атакует стремительным пикированием с воздуха. Яд виверны разъедает доспехи.",
|
||||||
|
"weakness":"ice", "resist":"fire", "ai":"dive" },
|
||||||
|
|
||||||
|
"goblin_king": { "name":"Зубастый, Король Гоблинов", "hp":145, "dmg":20, "def":10, "exp":280, "gold":180,
|
||||||
|
"loot":["goblin_ear","herb"],
|
||||||
|
"lore":"Легендарный вожак гоблинов, носящий украденную корону убитого рыцаря. За его голову обещана награда ещё десять лет назад. Собрал крупнейшую в регионе стаю — более сотни особей.",
|
||||||
|
"weakness":"fire", "resist":null, "ai":"warcry",
|
||||||
|
"isBoss":true, "isMini":true,
|
||||||
|
"uniqueLoot":{ "id":"goblin_crown", "type":"armor", "name":"Корона Зубастого",
|
||||||
|
"opts":{ "defense":8, "bonusStr":5, "bonusDef":3, "value":280, "slot":"head", "icon":"👑", "rarity":"legendary", "desc":"Корона украденная у рыцаря. Пахнет гоблином." }}},
|
||||||
|
|
||||||
|
"corvus": { "name":"Корвус Некромант", "hp":180, "dmg":30, "def":9, "exp":420, "gold":260,
|
||||||
|
"loot":["necronomicon","bone"],
|
||||||
|
"lore":"Бывший придворный маг, изгнанный за опыты над мёртвыми. Корвус нашёл укрытие в подземельях под деревней и за годы изоляции лишился рассудка. Теперь он стремится поднять армию нежити и захватить регион.",
|
||||||
|
"weakness":"holy", "resist":"magic", "ai":"necroboss",
|
||||||
|
"isBoss":true, "isMini":true, "hasMp":true,
|
||||||
|
"uniqueLoot":{ "id":"corvus_staff", "type":"weapon", "name":"Посох Корвуса",
|
||||||
|
"opts":{ "damage":16, "bonusMag":22, "value":460, "slot":"weapon", "icon":"💜", "rarity":"legendary", "desc":"Навершие посоха светится мертвенным светом. Усиливает заклинания смерти." }}},
|
||||||
|
|
||||||
|
"hydra": { "name":"Болотная Гидра", "hp":210, "dmg":25, "def":6, "exp":360, "gold":200,
|
||||||
|
"loot":["hydra_scale","slime_gel"],
|
||||||
|
"lore":"Трёхголовое чудовище, поселившееся в глубинах болота поколения назад. Каждую отрубленную голову заменяют две новые. Только огонь не даёт ей регенерировать.",
|
||||||
|
"weakness":"fire", "resist":"physical", "ai":"hydra",
|
||||||
|
"isBoss":true, "isMini":true,
|
||||||
|
"uniqueLoot":{ "id":"hydra_fang", "type":"weapon", "name":"Клык Гидры",
|
||||||
|
"opts":{ "damage":23, "bonusMag":4, "value":340, "slot":"weapon", "icon":"🦷", "rarity":"legendary", "combatEffect":"poison", "combatDmg":9, "desc":"Полый клык, наполненный ядом Гидры. Каждый удар отравляет врага." }}},
|
||||||
|
|
||||||
|
"frost_giant": { "name":"Ледяной Великан Скарр", "hp":200, "dmg":30, "def":13, "exp":390, "gold":230,
|
||||||
|
"loot":["frost_heart","yeti_fur"],
|
||||||
|
"lore":"Древний великан из ледяного народа, изгнанный своим кланом за жестокость. Скарр обосновался на вершине горы и убивает любого, кто осмелится подняться на его территорию.",
|
||||||
|
"weakness":"fire", "resist":"ice", "ai":"frost",
|
||||||
|
"isBoss":true, "isMini":true,
|
||||||
|
"uniqueLoot":{ "id":"giant_axe", "type":"weapon", "name":"Топор Великана",
|
||||||
|
"opts":{ "damage":29, "bonusStr":8, "value":400, "slot":"weapon", "icon":"🪓", "rarity":"legendary", "desc":"Огромный топор из ледяного железа. Прежде чем поднять его, убедись что хватит сил." }}},
|
||||||
|
|
||||||
|
"stone_colossus": { "name":"Каменный Колосс", "hp":260, "dmg":24, "def":22, "exp":440, "gold":270,
|
||||||
|
"loot":["titan_core","golem_core"],
|
||||||
|
"lore":"Прото-голем, созданный магами Первой Эпохи для охраны пещерного святилища. Тысячелетиями он стоял неподвижно — пока не почуял чужака. Разрушить физически невозможно, только магией.",
|
||||||
|
"weakness":"magic", "resist":"physical", "ai":"colossus",
|
||||||
|
"isBoss":true, "isMini":true,
|
||||||
|
"uniqueLoot":{ "id":"colossus_shield", "type":"armor", "name":"Щит Колосса",
|
||||||
|
"opts":{ "defense":18, "bonusHp":40, "value":380, "slot":"shield", "icon":"🛡️", "rarity":"legendary", "desc":"Отколотая рука Колосса. Невероятно прочна." }}},
|
||||||
|
|
||||||
|
"shadow_assassin": { "name":"Призрак Ирис", "hp":160, "dmg":36, "def":7, "exp":410, "gold":240,
|
||||||
|
"loot":["ghost_essence","wyvern_poison"],
|
||||||
|
"lore":"Легендарный убийца, некогда служивший тайной службе королевства. После гибели в руинах её душа не ушла — она осталась охранять тайны, которые унесла с собой. Атакует из темноты.",
|
||||||
|
"weakness":"holy", "resist":"physical", "ai":"shadow",
|
||||||
|
"isBoss":true, "isMini":true,
|
||||||
|
"uniqueLoot":{ "id":"shadow_blade", "type":"weapon", "name":"Клинок Ирис",
|
||||||
|
"opts":{ "damage":26, "bonusDef":4, "value":420, "slot":"weapon", "icon":"🗡️", "rarity":"legendary", "combatEffect":"poison", "combatDmg":7, "desc":"Клинок, выкованный из теневого металла. Слабо светится в темноте." }}},
|
||||||
|
|
||||||
|
"chaos_lord": { "name":"Мрак Безликий", "hp":700, "dmg":55, "def":22, "exp":3000, "gold":800,
|
||||||
|
"loot":["dragon_heart","necronomicon","titan_core"],
|
||||||
|
"lore":"Первобытное существо из Бездны. Источник всего зла в Эйдоне. Не имеет лица, не имеет имени — только жажда хаоса.",
|
||||||
|
"weakness":"holy", "resist":"magic", "ai":"chaos",
|
||||||
|
"isBoss":true, "isMini":true, "hasMp":true,
|
||||||
|
"uniqueLoot":{ "id":"void_crown", "type":"armor", "name":"Корона Бездны",
|
||||||
|
"opts":{ "defense":25, "bonusHp":80, "bonusMag":15, "bonusStr":10, "value":2000, "slot":"head", "icon":"🌑", "rarity":"legendary", "desc":"Корона первобытного хаоса. Все стихии склоняются перед её владельцем." }}}
|
||||||
|
}
|
||||||
29
data/loot.json
Normal file
29
data/loot.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"herb": { "n":"Трава", "t":"material", "v":6 },
|
||||||
|
"goblin_ear": { "n":"Ухо гоблина", "t":"material", "v":5 },
|
||||||
|
"orc_tusk": { "n":"Клык орка", "t":"material", "v":12 },
|
||||||
|
"meat": { "n":"Мясо", "t":"food", "v":10, "heal":20 },
|
||||||
|
"slime_gel": { "n":"Слизь", "t":"material", "v":5 },
|
||||||
|
"bone": { "n":"Кость", "t":"material", "v":4 },
|
||||||
|
"bone_dagger": { "n":"Костяной кинжал", "t":"weapon", "v":28, "dmg":8, "slot":"weapon", "icon":"🗡️", "rarity":"uncommon", "setId":"shadow" },
|
||||||
|
"money_pouch": { "n":"Кошелёк", "t":"gold", "v":18 },
|
||||||
|
"wolf_pelt": { "n":"Волчья шкура", "t":"material", "v":15 },
|
||||||
|
"spider_venom": { "n":"Паучий яд", "t":"material", "v":20 },
|
||||||
|
"rot_flesh": { "n":"Гнилая плоть", "t":"material", "v":3 },
|
||||||
|
"bat_wing": { "n":"Крыло летучей мыши", "t":"material", "v":8 },
|
||||||
|
"troll_heart": { "n":"Сердце тролля", "t":"material", "v":55, "rarity":"rare" },
|
||||||
|
"club": { "n":"Дубина", "t":"weapon", "v":18, "dmg":7, "slot":"weapon", "icon":"🏏" },
|
||||||
|
"yeti_fur": { "n":"Шкура йети", "t":"material", "v":40, "rarity":"rare" },
|
||||||
|
"witch_brew": { "n":"Зелье ведьмы", "t":"potion", "v":60, "heal":80, "rarity":"rare" },
|
||||||
|
"golem_core": { "n":"Ядро голема", "t":"material", "v":90, "rarity":"epic" },
|
||||||
|
"dragon_scale": { "n":"Чешуя дракона", "t":"material", "v":110,"rarity":"epic" },
|
||||||
|
"dragon_heart": { "n":"Сердце дракона", "t":"material", "v":300,"rarity":"legendary" },
|
||||||
|
"necronomicon": { "n":"Некрономикон", "t":"scroll", "v":200,"spell":"life_drain", "rarity":"rare" },
|
||||||
|
"skull_staff": { "n":"Посох черепа", "t":"weapon", "v":90, "dmg":18, "bonusMag":5, "slot":"weapon", "icon":"💀", "rarity":"rare", "setId":"shadow" },
|
||||||
|
"ghost_essence": { "n":"Эссенция призрака", "t":"material", "v":40, "rarity":"rare" },
|
||||||
|
"wyvern_scale": { "n":"Чешуя виверны", "t":"material", "v":60, "rarity":"rare" },
|
||||||
|
"wyvern_poison": { "n":"Яд виверны", "t":"material", "v":50, "rarity":"rare" },
|
||||||
|
"hydra_scale": { "n":"Чешуя Гидры", "t":"material", "v":75, "rarity":"epic" },
|
||||||
|
"frost_heart": { "n":"Сердце Мороза", "t":"material", "v":90, "rarity":"epic" },
|
||||||
|
"titan_core": { "n":"Сердцевина Колосса", "t":"material", "v":100, "rarity":"epic" }
|
||||||
|
}
|
||||||
59
data/lore.json
Normal file
59
data/lore.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
[
|
||||||
|
{ "id":"ln_v1", "title":"Старая записка", "mapId":"village", "gx":11, "gy":11, "icon":"📜",
|
||||||
|
"text":"Говорят, в пещерах к востоку обитает дракон. Никто из ушедших туда не вернулся. Будь осторожен, путник.",
|
||||||
|
"reveals":{ "enemy":"dragon", "hint":"Дракон живёт в пещерах. Готовься к огненному бою." } },
|
||||||
|
{ "id":"ln_v2", "title":"Объявление на столбе", "mapId":"village", "gx":5, "gy":5, "icon":"📋",
|
||||||
|
"text":"РАЗЫСКИВАЕТСЯ: вожак гоблинов по прозвищу «Зубастый». Видели в лесу к северу. Награда: 100 золота. Обращаться к Стражнику.",
|
||||||
|
"reveals":{ "enemy":"goblin_king", "hint":"Зубастый прячется в лесу. Огонь — его слабость." } },
|
||||||
|
{ "id":"ln_f1", "title":"Эльфийский дневник", "mapId":"forest", "gx":4, "gy":4, "icon":"📖",
|
||||||
|
"text":"4-й день. Лес изменился. Существа стали агрессивнее с тех пор, как в болоте появился Шаман. Старые тропы больше не безопасны.",
|
||||||
|
"reveals":{ "enemy":"wolf", "hint":"Волки контролируют лесные тропы. Огонь отпугивает стаю." } },
|
||||||
|
{ "id":"ln_f2", "title":"Измятый пергамент", "mapId":"forest", "gx":10, "gy":9, "icon":"📜",
|
||||||
|
"text":"...Не ходите в болото ночью. Ведьма собирает души потерявшихся. Говорят, она слаба против святого пламени...",
|
||||||
|
"reveals":{ "enemy":"witch", "hint":"Ведьма уязвима к святой магии, но устойчива к обычным чарам." } },
|
||||||
|
{ "id":"ln_d1", "title":"Надпись на стене", "mapId":"dungeon", "gx":3, "gy":3, "icon":"🪨",
|
||||||
|
"text":"«Тот, кто слышит шорох в стенах — не одинок. Мы всегда рядом.» — написано кровью. Под надписью — рисунок Лича.",
|
||||||
|
"reveals":{ "enemy":"lich", "hint":"Лич повелевает нежитью. Святое оружие прожигает его магию." } },
|
||||||
|
{ "id":"ln_d2", "title":"Дневник солдата", "mapId":"dungeon", "gx":10, "gy":10, "icon":"📔",
|
||||||
|
"text":"12-й день в подземелье. Еды нет. Скелеты не дают пройти. Лич слаб против святого огня. Надеюсь, кто-то это найдёт.",
|
||||||
|
"reveals":{ "enemy":"skeleton", "hint":"Скелеты неуязвимы к яду, но святой свет разрушает их." } },
|
||||||
|
{ "id":"ln_c1", "title":"Записка исследователя","mapId":"cave", "gx":5, "gy":5, "icon":"📝",
|
||||||
|
"text":"Кристаллы в этой пещере поглощают магическую энергию. Я чувствую, как слабею. Дракон охраняет нечто в глубине — не ходите туда.",
|
||||||
|
"reveals":{ "enemy":"dragon", "hint":"Дракон устойчив к огню, но уязвим ко льду." } },
|
||||||
|
{ "id":"ln_c2", "title":"Рунный камень", "mapId":"cave", "gx":10, "gy":8, "icon":"🔮",
|
||||||
|
"text":"«Огонь рождён из холода, лёд — из пламени. Дракон, что дышит огнём, падёт от стрел мороза.»",
|
||||||
|
"reveals":{ "enemy":"dragon", "hint":"Ледяные заклинания наносят дракону полуторный урон!" } },
|
||||||
|
{ "id":"ln_m1", "title":"Высеченный текст", "mapId":"mountain", "gx":7, "gy":7, "icon":"⛏️",
|
||||||
|
"text":"«Здесь покоится Первый Голем, созданный Академией. Магия разрушает его — физический урон отскакивает от него, как от скалы.»",
|
||||||
|
"reveals":{ "enemy":"golem", "hint":"Голем устойчив к физическим атакам. Используй магию!" } },
|
||||||
|
{ "id":"ln_s1", "title":"Страница гримуара", "mapId":"swamp", "gx":5, "gy":9, "icon":"📖",
|
||||||
|
"text":"Рецепт зелья тени: 3 части паучьего яда, крыло летучей мыши, лепесток ночного цветка. Смешать в полночь. Осторожно — вдыхать нельзя.",
|
||||||
|
"reveals":{ "enemy":"spider", "hint":"Пауки устойчивы к яду, но огонь сжигает их паутину." } },
|
||||||
|
{ "id":"ln_s2", "title":"Записка беглеца", "mapId":"swamp", "gx":9, "gy":4, "icon":"📜",
|
||||||
|
"text":"Ведьма слабее у восточного огня. Принесите святую воду и она не сможет применить исцеление. Я ухожу. Прощайте.",
|
||||||
|
"reveals":{ "enemy":"witch", "hint":"Святая магия блокирует исцеление Ведьмы." } },
|
||||||
|
{ "id":"ln_f3", "title":"Поваленное дерево", "mapId":"forest", "gx":8, "gy":7, "icon":"📜",
|
||||||
|
"text":"Нашёл старый указатель: «Зубастый обитает в центре леса. Его стая — сотня гоблинов. Один не ходи — вернись с оружием и смелостью.» Дата: десять лет назад.",
|
||||||
|
"reveals":{ "enemy":"goblin", "hint":"Гоблины боятся огня. Используй огненные заклинания." } },
|
||||||
|
{ "id":"ln_d3", "title":"Сожжённая страница", "mapId":"dungeon", "gx":6, "gy":6, "icon":"📔",
|
||||||
|
"text":"«Корвус захватил нижний ярус. Бывший придворный маг — теперь безумный некромант. Его свита — армия нежити. Мы не смогли пробиться. Пишу это, чтобы предупредить следующих.»",
|
||||||
|
"reveals":{ "enemy":"corvus", "hint":"Корвус — святая магия его единственная слабость." } },
|
||||||
|
{ "id":"ln_s3", "title":"Болотный знак", "mapId":"swamp", "gx":3, "gy":8, "icon":"📖",
|
||||||
|
"text":"Шаман предупреждал: в глубинах болота спит трёхголовая Гидра. Она проснулась год назад. Регенерирует раненые головы — только огонь останавливает её. Не ходите туда.",
|
||||||
|
"reveals":{ "enemy":"hydra", "hint":"Гидра регенерирует, но огонь останавливает рост голов!" } },
|
||||||
|
{ "id":"ln_m2", "title":"Табличка на перевале", "mapId":"mountain", "gx":9, "gy":9, "icon":"⛏️",
|
||||||
|
"text":"СТОЙ! Выше — территория Скарра. Изгнанный ледяной великан убивает любого, кто поднимется на вершину. Огонь — единственное, чего он боится. Не говори, что тебя не предупреждали.",
|
||||||
|
"reveals":{ "enemy":"frost_giant", "hint":"Ледяной Великан боится огня. Лёд бесполезен против него." } },
|
||||||
|
{ "id":"ln_c3", "title":"Предостережение", "mapId":"cave", "gx":3, "gy":9, "icon":"🔮",
|
||||||
|
"text":"Каменный Колосс — тысячелетний страж. Он почуял чужака. Магия разрушает его — физическое оружие не берёт. Каждые несколько ходов он закрывается непробиваемой бронёй. Жди и атакуй в уязвимый момент.",
|
||||||
|
"reveals":{ "enemy":"stone_colossus", "hint":"Колосс устойчив к физике. Магия — единственный путь." } },
|
||||||
|
{ "id":"ln_r1", "title":"Выцветший пергамент", "mapId":"ruins", "gx":5, "gy":7, "icon":"📜",
|
||||||
|
"text":"«Ирис — легенда среди убийц. Она мертва, но не ушла. Её призрак охраняет тайны руин, которые унесла с собой. Нападает из темноты. Святое оружие — единственная защита от неё.»",
|
||||||
|
"reveals":{ "enemy":"shadow_assassin", "hint":"Призрак Ирис уязвима к святой магии." } },
|
||||||
|
{ "id":"ln_r2", "title":"Чёрный портал", "mapId":"ruins", "gx":7, "gy":2, "icon":"🌑",
|
||||||
|
"text":"Что-то пульсирует за этим порталом. Воздух холоднее, чем должен быть. Первый Герой запечатал это место — печать ослабла. Это начало конца, или его конец?",
|
||||||
|
"reveals":{ "enemy":"chaos_lord", "hint":"За порталом — древнее зло. Готовь святое оружие." } },
|
||||||
|
{ "id":"ln_a1", "title":"Записка Первого Героя","mapId":"abyss", "gx":4, "gy":4, "icon":"📜",
|
||||||
|
"text":"«Я запечатал его здесь тысячу лет назад. Если ты читаешь это — печать разрушена. Мрак Безликий — это не демон и не зверь. Это сама тьма, принявшая форму. Только святое оружие может причинить ему настоящий вред. Удачи, герой.» — Эйдор I",
|
||||||
|
"reveals":{ "enemy":"chaos_lord", "hint":"Мрак Безликий уязвим ТОЛЬКО к святой магии!" } }
|
||||||
|
]
|
||||||
27
data/quests.json
Normal file
27
data/quests.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{ "id":"q_first", "name":"Первая кровь", "desc":"Убей 3 гоблина", "type":"kill", "target":"goblin", "need":3, "reward":{ "exp":50, "gold":20 }},
|
||||||
|
{ "id":"q_wolves", "name":"Охота на волков", "desc":"Убей 3 волка", "type":"kill", "target":"wolf", "need":3, "reward":{ "exp":60, "gold":30 }},
|
||||||
|
{ "id":"q_forest", "name":"Зачистка леса", "desc":"Убей 5 любых монстров", "type":"kill", "target":"any", "need":5, "reward":{ "exp":100, "gold":40 }},
|
||||||
|
{ "id":"q_slime", "name":"Проблема слизней", "desc":"Убей 3 слизня", "type":"kill", "target":"slime", "need":3, "reward":{ "exp":45, "gold":25 }},
|
||||||
|
{ "id":"q_bandit", "name":"Убрать разбойников", "desc":"Убей 4 разбойника", "type":"kill", "target":"bandit", "need":4, "reward":{ "exp":80, "gold":60 }},
|
||||||
|
{ "id":"q_dungeon","name":"Вход в подземелье", "desc":"Доберись до подземелья", "type":"visit","target":"dungeon", "need":1, "reward":{ "exp":30, "gold":15 }},
|
||||||
|
{ "id":"q_cave", "name":"Вход в пещеру", "desc":"Найди пещеру", "type":"visit","target":"cave", "need":1, "reward":{ "exp":50, "gold":20 }},
|
||||||
|
{ "id":"q_skel", "name":"Армия скелетов", "desc":"Убей 4 скелета", "type":"kill", "target":"skeleton", "need":4, "reward":{ "exp":120, "gold":60 }},
|
||||||
|
{ "id":"q_troll", "name":"Бой с троллем", "desc":"Убей тролля", "type":"kill", "target":"troll", "need":1, "reward":{ "exp":200, "gold":100}},
|
||||||
|
{ "id":"q_spider", "name":"Пауки болота", "desc":"Убей 5 пауков", "type":"kill", "target":"spider", "need":5, "reward":{ "exp":90, "gold":55 }},
|
||||||
|
{ "id":"q_dragon", "name":"Убийца дракона", "desc":"Убей дракона", "type":"kill", "target":"dragon", "need":1, "reward":{ "exp":500, "gold":300}},
|
||||||
|
{ "id":"q_lich", "name":"Конец некромантии", "desc":"Уничтожь Лича", "type":"kill", "target":"lich", "need":1, "reward":{ "exp":450, "gold":250}},
|
||||||
|
{ "id":"q_ruins", "name":"Исследователь", "desc":"Посети Руины", "type":"visit","target":"ruins", "need":1, "reward":{ "exp":80, "gold":40 }},
|
||||||
|
{ "id":"q_ghost", "name":"Упокоить призраков", "desc":"Убей 3 призрака в Руинах", "type":"kill", "target":"ghost", "need":3, "reward":{ "exp":200, "gold":100}},
|
||||||
|
{ "id":"q_wyvern", "name":"Охота на виверн", "desc":"Убей 2 виверны", "type":"kill", "target":"wyvern", "need":2, "reward":{ "exp":280, "gold":160}},
|
||||||
|
{ "id":"q_orc5", "name":"Орочья угроза", "desc":"Убей 5 орков", "type":"kill", "target":"orc", "need":5, "reward":{ "exp":150, "gold":80 }},
|
||||||
|
{ "id":"q_yeti", "name":"Снежный зверь", "desc":"Убей йети", "type":"kill", "target":"yeti", "need":1, "reward":{ "exp":160, "gold":100}},
|
||||||
|
{ "id":"q_swamp_c", "name":"Зачистка болота", "desc":"Убей 3 паука и 2 зомби", "type":"kill","target":"spider", "need":3, "reward":{"exp":130, "gold":70 }},
|
||||||
|
{ "id":"q_goblin_king", "name":"Король Гоблинов", "desc":"Убей Зубастого в лесу", "type":"kill","target":"goblin_king", "need":1, "reward":{"exp":450, "gold":250 }},
|
||||||
|
{ "id":"q_corvus", "name":"Конец Корвуса", "desc":"Уничтожь Корвуса в подземелье", "type":"kill","target":"corvus", "need":1, "reward":{"exp":550, "gold":300 }},
|
||||||
|
{ "id":"q_hydra", "name":"Гидра болот", "desc":"Убей Болотную Гидру", "type":"kill","target":"hydra", "need":1, "reward":{"exp":480, "gold":280 }},
|
||||||
|
{ "id":"q_frost_giant", "name":"Ледяной Великан", "desc":"Сразись с Ледяным Великаном Скарр", "type":"kill","target":"frost_giant", "need":1, "reward":{"exp":510, "gold":290 }},
|
||||||
|
{ "id":"q_colossus", "name":"Каменный Колосс", "desc":"Уничтожь Каменного Колосса в пещере", "type":"kill","target":"stone_colossus", "need":1, "reward":{"exp":560, "gold":310 }},
|
||||||
|
{ "id":"q_shadow", "name":"Призрак Ирис", "desc":"Упокой Призрак Ирис в Руинах", "type":"kill","target":"shadow_assassin", "need":1, "reward":{"exp":530, "gold":295 }},
|
||||||
|
{ "id":"q_chaos_lord", "name":"Конец Тьмы", "desc":"Убей Мрака Безликого в Бездне", "type":"kill","target":"chaos_lord", "need":1, "reward":{"exp":3000,"gold":500 }}
|
||||||
|
]
|
||||||
92
data/recipes.json
Normal file
92
data/recipes.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
[
|
||||||
|
{ "id":"r_heal_sm", "name":"Малое зелье HP", "icon":"🧪", "category":"potions",
|
||||||
|
"ingredients":[{"id":"herb","qty":1},{"id":"slime_gel","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Малое зелье HP", "opts":{ "healAmount":40, "value":30, "stackable":true, "qty":1, "icon":"🧪" }}},
|
||||||
|
{ "id":"r_heal_md", "name":"Среднее зелье HP", "icon":"🧪", "category":"potions",
|
||||||
|
"ingredients":[{"id":"herb","qty":2},{"id":"slime_gel","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Среднее зелье HP", "opts":{ "healAmount":70, "value":55, "stackable":true, "qty":1, "icon":"🧪" }}},
|
||||||
|
{ "id":"r_heal_lg", "name":"Большое зелье HP", "icon":"🍶", "category":"potions",
|
||||||
|
"ingredients":[{"id":"herb","qty":3},{"id":"troll_heart","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Большое зелье HP", "opts":{ "healAmount":120, "value":95, "stackable":true, "qty":1, "icon":"🍶" }}},
|
||||||
|
{ "id":"r_mp_md", "name":"Зелье маны", "icon":"💧", "category":"potions",
|
||||||
|
"ingredients":[{"id":"bat_wing","qty":2},{"id":"spider_venom","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Зелье маны", "opts":{ "restoreMp":50, "value":60, "stackable":true, "qty":1, "icon":"💧" }}},
|
||||||
|
{ "id":"r_antidote", "name":"Антидот", "icon":"🩺", "category":"potions",
|
||||||
|
"ingredients":[{"id":"herb","qty":1},{"id":"wolf_pelt","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Антидот", "opts":{ "healAmount":15, "value":35, "stackable":true, "qty":1, "icon":"🩺", "desc":"Снимает яд и горение" }}},
|
||||||
|
{ "id":"r_stew", "name":"Мясное рагу", "icon":"🍲", "category":"potions",
|
||||||
|
"ingredients":[{"id":"meat","qty":2},{"id":"herb","qty":1}],
|
||||||
|
"result":{ "type":"food", "name":"Сытное рагу", "opts":{ "healAmount":50, "restoreMp":25, "value":40, "stackable":true, "qty":1, "icon":"🍲" }}},
|
||||||
|
{ "id":"r_smoke", "name":"Дымовая бомба", "icon":"💨", "category":"potions",
|
||||||
|
"ingredients":[{"id":"bat_wing","qty":1},{"id":"rot_flesh","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Дымовая бомба", "opts":{ "value":45, "stackable":true, "qty":1, "icon":"💨", "desc":"Гарантированный побег из боя", "healAmount":0 }}},
|
||||||
|
{ "id":"r_rune_str", "name":"Руна Силы", "icon":"🔴", "category":"runes",
|
||||||
|
"ingredients":[{"id":"golem_core","qty":1},{"id":"orc_tusk","qty":2}],
|
||||||
|
"result":{ "type":"material", "name":"Руна Силы", "opts":{ "bonusStr":5, "value":200, "icon":"🔴", "desc":"+5 СИЛ к персонажу", "rarity":"rare" }}},
|
||||||
|
{ "id":"r_rune_mag", "name":"Руна Магии", "icon":"🔵", "category":"runes",
|
||||||
|
"ingredients":[{"id":"dragon_scale","qty":1},{"id":"necronomicon","qty":1}],
|
||||||
|
"result":{ "type":"material", "name":"Руна Магии", "opts":{ "bonusMag":5, "value":260, "icon":"🔵", "desc":"+5 МАГ к персонажу", "rarity":"rare" }}},
|
||||||
|
{ "id":"r_rune_def", "name":"Руна Защиты", "icon":"🟢", "category":"runes",
|
||||||
|
"ingredients":[{"id":"yeti_fur","qty":1},{"id":"bone","qty":3}],
|
||||||
|
"result":{ "type":"material", "name":"Руна Защиты", "opts":{ "bonusDef":4, "value":185, "icon":"🟢", "desc":"+4 ЗАЩ к персонажу", "rarity":"uncommon" }}},
|
||||||
|
{ "id":"r_sharpen", "name":"Камень заточки", "icon":"⚙️", "category":"enhance",
|
||||||
|
"ingredients":[{"id":"bone","qty":2},{"id":"slime_gel","qty":1}],
|
||||||
|
"result":{ "type":"material", "name":"Камень заточки", "opts":{ "damage":3, "value":65, "icon":"⚙️", "desc":"+3 урона (применить к оружию)" }}},
|
||||||
|
{ "id":"r_poison_c", "name":"Яд на оружие", "icon":"☠️", "category":"enhance",
|
||||||
|
"ingredients":[{"id":"spider_venom","qty":2},{"id":"herb","qty":1}],
|
||||||
|
"result":{ "type":"scroll", "name":"Яд на оружие", "opts":{ "spell":"poison_cloud", "value":85, "icon":"☠️", "stackable":true, "qty":1 }}},
|
||||||
|
{ "id":"r_scale_a", "name":"Чешуйчатая броня", "icon":"🐉", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"dragon_scale","qty":2},{"id":"wolf_pelt","qty":2}],
|
||||||
|
"result":{ "type":"armor", "name":"Чешуйчатая броня", "opts":{ "defense":20, "bonusHp":30, "value":500, "slot":"chest", "icon":"🐉", "rarity":"epic" }}},
|
||||||
|
{ "id":"r_death_s", "name":"Посох Смерти", "icon":"💀", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"skull_staff","qty":1},{"id":"golem_core","qty":1},{"id":"goblin_ear","qty":3}],
|
||||||
|
"result":{ "type":"weapon", "name":"Посох Смерти", "opts":{ "damage":22, "bonusMag":8, "value":420, "slot":"weapon", "icon":"💀", "rarity":"epic" }}},
|
||||||
|
{ "id":"r_life_gem", "name":"Амулет жизни", "icon":"💎", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"dragon_heart","qty":1},{"id":"troll_heart","qty":1}],
|
||||||
|
"result":{ "type":"armor", "name":"Амулет жизни", "opts":{ "bonusHp":60, "bonusMp":30, "value":850, "slot":"acc", "icon":"💎", "rarity":"legendary" }}},
|
||||||
|
{ "id":"r_alch_str", "name":"Зелье силы", "icon":"💪", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"herb","qty":2},{"id":"orc_tusk","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Зелье силы 💪", "opts":{ "buffStat":"str", "buffVal":1.5, "buffDur":45000, "value":80, "stackable":true, "qty":1, "icon":"💪", "desc":"Урон ×1.5 на 45 сек" }}},
|
||||||
|
{ "id":"r_alch_def", "name":"Зелье камня", "icon":"🪨", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"wolf_pelt","qty":2},{"id":"bone","qty":2}],
|
||||||
|
"result":{ "type":"potion", "name":"Зелье камня 🪨", "opts":{ "buffStat":"def", "buffVal":2.0, "buffDur":30000, "value":70, "stackable":true, "qty":1, "icon":"🪨", "desc":"Защита ×2 на 30 сек" }}},
|
||||||
|
{ "id":"r_alch_mp", "name":"Эликсир маны", "icon":"🔮", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"slime_gel","qty":2},{"id":"bat_wing","qty":2}],
|
||||||
|
"result":{ "type":"potion", "name":"Эликсир маны 🔮", "opts":{ "restoreMp":80, "value":65, "stackable":true, "qty":1, "icon":"🔮" }}},
|
||||||
|
{ "id":"r_alch_regen","name":"Зелье регенерации", "icon":"💚", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"herb","qty":3},{"id":"troll_heart","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Зелье регенерации 💚", "opts":{ "healAmount":40, "buffStat":"regen", "buffVal":8, "buffDur":50000, "value":110, "stackable":true, "qty":1, "icon":"💚", "desc":"+40 HP сейчас, регенерация 8 HP/ход 5 ходов" }}},
|
||||||
|
{ "id":"r_alch_poison","name":"Яд-склянка", "icon":"☠️", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"spider_venom","qty":2},{"id":"bat_wing","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Яд-склянка ☠️", "opts":{ "combatEffect":"poison", "value":55, "stackable":true, "qty":1, "icon":"☠️", "desc":"Боевое: наносит яд врагу (3 хода)" }}},
|
||||||
|
{ "id":"r_alch_fire","name":"Огненная колба", "icon":"🔥", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"dragon_scale","qty":1},{"id":"herb","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Огненная колба 🔥", "opts":{ "combatEffect":"fire", "combatDmg":35, "value":90, "stackable":true, "qty":1, "icon":"🔥", "desc":"Боевое: 35 урона огнём", "rarity":"rare" }}},
|
||||||
|
{ "id":"r_alch_antidote","name":"Сильный антидот", "icon":"🩺", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"herb","qty":2},{"id":"slime_gel","qty":1},{"id":"bat_wing","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Сильный антидот 🩺", "opts":{ "healAmount":20, "cureStatus":true, "value":50, "stackable":true, "qty":1, "icon":"🩺", "desc":"Снимает яд, горение, все статусы" }}},
|
||||||
|
{ "id":"r_alch_elixir","name":"Эликсир могущества", "icon":"⚗️", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"dragon_heart","qty":1},{"id":"witch_brew","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Эликсир могущества ⚗️", "opts":{ "healAmount":80, "restoreMp":60, "buffStat":"str", "buffVal":1.4, "buffDur":60000, "value":350, "stackable":true, "qty":1, "icon":"⚗️", "desc":"HP+80 MP+60 Сила×1.4 на 60 сек", "rarity":"epic" }}},
|
||||||
|
{ "id":"r_ghost_ward","name":"Оберег от призраков", "icon":"🕯️", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"ghost_essence","qty":2},{"id":"herb","qty":2}],
|
||||||
|
"result":{ "type":"potion", "name":"Оберег от призраков 🕯️", "opts":{ "buffStat":"def", "buffVal":1.3, "buffDur":60000, "value":120, "stackable":true, "qty":1, "icon":"🕯️", "desc":"Защита ×1.3 и +30 HP против нежити", "healAmount":30 }}},
|
||||||
|
{ "id":"r_wyvern_venom","name":"Яд виверны", "icon":"☠️", "category":"alchemy",
|
||||||
|
"ingredients":[{"id":"wyvern_poison","qty":1},{"id":"spider_venom","qty":1}],
|
||||||
|
"result":{ "type":"potion", "name":"Яд виверны ☠️", "opts":{ "combatEffect":"poison", "value":95, "stackable":true, "qty":1, "icon":"☠️", "desc":"Боевое: сильный яд на врага (4 хода)", "rarity":"rare" }}},
|
||||||
|
{ "id":"r_holy_plate","name":"Нагрудник паладина", "icon":"⛪", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"golem_core","qty":1},{"id":"troll_heart","qty":1},{"id":"bone","qty":3}],
|
||||||
|
"result":{ "type":"armor", "name":"Нагрудник паладина ⛪", "opts":{ "defense":16, "bonusHp":40, "value":480, "slot":"chest", "icon":"⛪", "rarity":"epic", "setId":"holy" }}},
|
||||||
|
{ "id":"r_storm_staff","name":"Посох бури", "icon":"⚡", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"dragon_scale","qty":1},{"id":"yeti_fur","qty":1},{"id":"golem_core","qty":1}],
|
||||||
|
"result":{ "type":"weapon", "name":"Посох бури ⚡", "opts":{ "damage":10, "bonusMag":16, "bonusMp":35, "value":580, "slot":"weapon", "icon":"⚡", "rarity":"epic", "setId":"arcane" }}},
|
||||||
|
{ "id":"r_frost_blade","name":"Ледяной клинок", "icon":"❄️", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"frost_heart","qty":1},{"id":"dragon_scale","qty":1}],
|
||||||
|
"result":{ "type":"weapon", "name":"Ледяной клинок ❄️", "opts":{ "damage":27, "bonusStr":6, "value":620, "slot":"weapon", "icon":"❄️", "rarity":"legendary", "desc":"Выкован из ледяного сердца великана. Морозит врагов." }}},
|
||||||
|
{ "id":"r_titan_armor","name":"Броня Колосса", "icon":"🪨", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"titan_core","qty":1},{"id":"golem_core","qty":1}],
|
||||||
|
"result":{ "type":"armor", "name":"Броня Колосса 🪨", "opts":{ "defense":24, "bonusHp":55, "value":650, "slot":"chest", "icon":"🪨", "rarity":"legendary", "desc":"Выкована из осколков Каменного Колосса. Невероятно прочна." }}},
|
||||||
|
{ "id":"r_hydra_mail", "name":"Кольчуга Гидры", "icon":"🐍", "category":"equipment",
|
||||||
|
"ingredients":[{"id":"hydra_scale","qty":2},{"id":"wyvern_scale","qty":1}],
|
||||||
|
"result":{ "type":"armor", "name":"Кольчуга Гидры 🐍", "opts":{ "defense":17, "bonusHp":30, "bonusMp":20, "value":490, "slot":"chest", "icon":"🐍", "rarity":"epic", "desc":"Сделана из чешуи Болотной Гидры. Обладает природной регенерацией." }}}
|
||||||
|
]
|
||||||
32
data/sets.json
Normal file
32
data/sets.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"steel": {
|
||||||
|
"name": "Стальной доспех", "icon": "⚔️",
|
||||||
|
"pieces": ["s_sw2","s_ar2","s_sh2","s_hm2"],
|
||||||
|
"bonuses": {
|
||||||
|
"2": { "def":4, "desc":"2 предмета: +4 защиты" },
|
||||||
|
"4": { "def":4, "str":6, "hp":20, "desc":"4 предмета: +6 силы, +4 защиты, +20 HP" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arcane": {
|
||||||
|
"name": "Посох чародея", "icon": "✨",
|
||||||
|
"pieces": ["s_st3"],
|
||||||
|
"bonuses": {
|
||||||
|
"1": { "mag":5, "mp":20, "desc":"Набор: +5 магии, +20 MP" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shadow": {
|
||||||
|
"name": "Тёмные чары", "icon": "💀",
|
||||||
|
"pieces": ["skull_staff","bone_dagger"],
|
||||||
|
"bonuses": {
|
||||||
|
"2": { "mag":6, "str":4, "desc":"2 предмета: +6 магии, +4 силы" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"holy": {
|
||||||
|
"name": "Доспех паладина", "icon": "⛪",
|
||||||
|
"pieces": ["s_ar3","r_holy_plate"],
|
||||||
|
"bonuses": {
|
||||||
|
"1": { "def":5, "hp":15, "desc":"1 предмет: +5 защиты, +15 HP" },
|
||||||
|
"2": { "def":8, "hp":30, "mag":4, "desc":"2 предмета: +8 защиты, +30 HP, +4 магии" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
data/shop.json
Normal file
19
data/shop.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{ "id":"s_hp2", "type":"potion", "name":"Среднее зелье HP", "opts":{ "healAmount":60, "value":50, "stackable":true, "qty":1, "icon":"🧪" }},
|
||||||
|
{ "id":"s_hp3", "type":"potion", "name":"Большое зелье HP", "opts":{ "healAmount":100, "value":90, "stackable":true, "qty":1, "icon":"🧪" }},
|
||||||
|
{ "id":"s_mp2", "type":"potion", "name":"Среднее зелье MP", "opts":{ "restoreMp":40, "value":55, "stackable":true, "qty":1, "icon":"💧" }},
|
||||||
|
{ "id":"s_sw2", "type":"weapon", "name":"Стальной меч", "opts":{ "damage":12, "value":160, "slot":"weapon", "icon":"⚔️", "rarity":"uncommon", "setId":"steel" }},
|
||||||
|
{ "id":"s_ax2", "type":"weapon", "name":"Боевой топор", "opts":{ "damage":14, "value":200, "slot":"weapon", "icon":"🪓", "rarity":"uncommon" }},
|
||||||
|
{ "id":"s_st3", "type":"weapon", "name":"Посох силы", "opts":{ "damage":5, "bonusMag":8, "value":220, "slot":"weapon", "icon":"✨", "rarity":"uncommon", "setId":"arcane" }},
|
||||||
|
{ "id":"s_ar2", "type":"armor", "name":"Кольчуга", "opts":{ "defense":8, "value":200, "slot":"chest", "icon":"🔗", "rarity":"uncommon", "setId":"steel" }},
|
||||||
|
{ "id":"s_sh2", "type":"armor", "name":"Стальной щит", "opts":{ "defense":6, "value":150, "slot":"shield", "icon":"🛡️", "rarity":"uncommon", "setId":"steel" }},
|
||||||
|
{ "id":"s_hm2", "type":"armor", "name":"Шлем воина", "opts":{ "defense":4, "bonusHp":15, "value":130, "slot":"head", "icon":"⛑️", "rarity":"uncommon", "setId":"steel" }},
|
||||||
|
{ "id":"s_sc1", "type":"scroll", "name":"Свиток огня", "opts":{ "spell":"fireball", "value":80, "icon":"📜" }},
|
||||||
|
{ "id":"s_sc2", "type":"scroll", "name":"Свиток исцеления", "opts":{ "spell":"heal", "value":60, "icon":"📜" }},
|
||||||
|
{ "id":"s_ring1", "type":"armor", "name":"Кольцо мага", "opts":{ "bonusMag":5, "bonusMp":20, "value":280, "slot":"acc", "icon":"💍", "rarity":"rare", "setId":"arcane" }},
|
||||||
|
{ "id":"s_boot1", "type":"armor", "name":"Сапоги ловкости", "opts":{ "bonusStr":3, "defense":2, "value":200, "slot":"feet", "icon":"👟", "rarity":"uncommon" }},
|
||||||
|
{ "id":"s_sw3", "type":"weapon", "name":"Ночной клинок", "opts":{ "damage":18, "bonusStr":4, "value":450, "slot":"weapon", "icon":"🗡️", "rarity":"legendary", "setId":"shadow" }},
|
||||||
|
{ "id":"s_st4", "type":"weapon", "name":"Посох бури", "opts":{ "damage":8, "bonusMag":14, "bonusMp":30, "value":500, "slot":"weapon", "icon":"⚡", "rarity":"epic", "setId":"arcane" }},
|
||||||
|
{ "id":"s_ar3", "type":"armor", "name":"Нагрудник паладина","opts":{ "defense":14, "bonusHp":30, "value":400, "slot":"chest", "icon":"⛪", "rarity":"epic", "setId":"holy" }},
|
||||||
|
{ "id":"s_ring2", "type":"armor", "name":"Кольцо защиты", "opts":{ "bonusDef":4, "defense":3, "value":220, "slot":"acc", "icon":"🔮", "rarity":"rare" }}
|
||||||
|
]
|
||||||
351
data/world.json
Normal file
351
data/world.json
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"locations": {
|
||||||
|
"village": { "name":"Деревня", "safe":true },
|
||||||
|
"tavern": { "name":"Таверна", "safe":true },
|
||||||
|
"forest": { "name":"Лес", "safe":false },
|
||||||
|
"dungeon": { "name":"Подземелье", "safe":false },
|
||||||
|
"cave": { "name":"Пещера", "safe":false },
|
||||||
|
"mountain": { "name":"Горы", "safe":false },
|
||||||
|
"swamp": { "name":"Болото", "safe":false },
|
||||||
|
"ruins": { "name":"Руины", "safe":false },
|
||||||
|
"abyss": { "name":"Бездна", "safe":false }
|
||||||
|
},
|
||||||
|
"spawns": {
|
||||||
|
"forest": [
|
||||||
|
{"t":"goblin", "lOff":0, "x":3, "y":3 },
|
||||||
|
{"t":"wolf", "lOff":0, "x":10, "y":2 },
|
||||||
|
{"t":"goblin", "lOff":1, "x":5, "y":9 },
|
||||||
|
{"t":"slime", "lOff":0, "x":2, "y":10},
|
||||||
|
{"t":"bandit", "lOff":1, "x":11, "y":8 },
|
||||||
|
{"t":"wolf", "lOff":1, "x":7, "y":4 },
|
||||||
|
{"t":"goblin_king", "lOff":5, "x":8, "y":7 }
|
||||||
|
],
|
||||||
|
"dungeon": [
|
||||||
|
{"t":"skeleton", "lOff":2, "x":2, "y":2 },
|
||||||
|
{"t":"skeleton", "lOff":2, "x":11, "y":3 },
|
||||||
|
{"t":"zombie", "lOff":2, "x":6, "y":10},
|
||||||
|
{"t":"troll", "lOff":4, "x":10, "y":11},
|
||||||
|
{"t":"skeleton", "lOff":2, "x":3, "y":8 },
|
||||||
|
{"t":"zombie", "lOff":3, "x":8, "y":5 },
|
||||||
|
{"t":"corvus", "lOff":6, "x":5, "y":6 }
|
||||||
|
],
|
||||||
|
"cave": [
|
||||||
|
{"t":"bat", "lOff":2, "x":3, "y":3 },
|
||||||
|
{"t":"slime", "lOff":1, "x":8, "y":5 },
|
||||||
|
{"t":"orc", "lOff":3, "x":10, "y":8 },
|
||||||
|
{"t":"dragon", "lOff":7, "x":12, "y":2 },
|
||||||
|
{"t":"bat", "lOff":2, "x":5, "y":11},
|
||||||
|
{"t":"orc", "lOff":4, "x":11, "y":11},
|
||||||
|
{"t":"stone_colossus", "lOff":8, "x":6, "y":7 }
|
||||||
|
],
|
||||||
|
"mountain":[
|
||||||
|
{"t":"yeti", "lOff":3, "x":3, "y":3 },
|
||||||
|
{"t":"golem", "lOff":5, "x":11, "y":4 },
|
||||||
|
{"t":"yeti", "lOff":4, "x":6, "y":10},
|
||||||
|
{"t":"golem", "lOff":6, "x":10, "y":10},
|
||||||
|
{"t":"wolf", "lOff":2, "x":4, "y":7 },
|
||||||
|
{"t":"frost_giant", "lOff":7, "x":8, "y":7 }
|
||||||
|
],
|
||||||
|
"swamp": [
|
||||||
|
{"t":"spider", "lOff":2, "x":3, "y":4 },
|
||||||
|
{"t":"spider", "lOff":2, "x":11, "y":3 },
|
||||||
|
{"t":"witch", "lOff":4, "x":7, "y":11},
|
||||||
|
{"t":"slime", "lOff":1, "x":2, "y":9 },
|
||||||
|
{"t":"zombie", "lOff":3, "x":10, "y":9 },
|
||||||
|
{"t":"lich", "lOff":8, "x":12, "y":2 },
|
||||||
|
{"t":"hydra", "lOff":7, "x":5, "y":6 }
|
||||||
|
],
|
||||||
|
"ruins": [
|
||||||
|
{"t":"ghost", "lOff":3, "x":3, "y":3 },
|
||||||
|
{"t":"ghost", "lOff":3, "x":10, "y":4 },
|
||||||
|
{"t":"ghost", "lOff":4, "x":5, "y":10},
|
||||||
|
{"t":"wyvern", "lOff":5, "x":11, "y":10},
|
||||||
|
{"t":"wyvern", "lOff":6, "x":12, "y":3 },
|
||||||
|
{"t":"skeleton", "lOff":2, "x":2, "y":9 },
|
||||||
|
{"t":"zombie", "lOff":3, "x":9, "y":11},
|
||||||
|
{"t":"shadow_assassin", "lOff":8, "x":7, "y":7 }
|
||||||
|
],
|
||||||
|
"abyss": [
|
||||||
|
{"t":"shadow_assassin", "lOff":7, "x":3, "y":3 },
|
||||||
|
{"t":"lich", "lOff":8, "x":11, "y":3 },
|
||||||
|
{"t":"ghost", "lOff":6, "x":2, "y":11},
|
||||||
|
{"t":"chaos_lord", "lOff":12,"x":7, "y":5 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"npcs": {
|
||||||
|
"village": [
|
||||||
|
{"name":"Торговец", "x":2, "y":5, "color":"#3498db", "type":"shop"},
|
||||||
|
{"name":"Стражник", "x":5, "y":1, "color":"#8b0000", "type":"quest"},
|
||||||
|
{"name":"Целитель", "x":10, "y":3, "color":"#27ae60", "type":"healer"},
|
||||||
|
{"name":"Старик", "x":8, "y":8, "color":"#aaa", "type":"quest"}
|
||||||
|
],
|
||||||
|
"tavern": [
|
||||||
|
{"name":"Трактирщик", "x":7, "y":5, "color":"#c8a060", "type":"branch"}
|
||||||
|
],
|
||||||
|
"forest": [{"name":"Эльф", "x":10, "y":10, "color":"#2ecc71", "type":"quest"}],
|
||||||
|
"dungeon": [{"name":"Призрак", "x":7, "y":7, "color":"#aaffff", "type":"quest"}],
|
||||||
|
"swamp": [{"name":"Шаман", "x":3, "y":3, "color":"#8e44ad", "type":"quest"}],
|
||||||
|
"mountain": [{"name":"Старик", "x":5, "y":5, "color":"#aaa", "type":"quest"}],
|
||||||
|
"ruins": [{"name":"Страж", "x":6, "y":6, "color":"#88aacc", "type":"quest"}]
|
||||||
|
},
|
||||||
|
"decos": {
|
||||||
|
"village": [
|
||||||
|
{"x":2, "y":2, "type":"house"},
|
||||||
|
{"x":11, "y":2, "type":"house"},
|
||||||
|
{"x":2, "y":9, "type":"house"},
|
||||||
|
{"x":4, "y":6, "type":"tree"},
|
||||||
|
{"x":10, "y":6, "type":"tree"},
|
||||||
|
{"x":12, "y":8, "type":"tree"},
|
||||||
|
{"x":3, "y":4, "type":"fountain"},
|
||||||
|
{"x":8, "y":2, "type":"well"},
|
||||||
|
{"x":7, "y":9, "type":"tavern", "name":"Таверна"},
|
||||||
|
{"x":7, "y":9, "type":"portal", "destination":"tavern", "name":"🍺 Таверна"},
|
||||||
|
{"x":13, "y":7, "type":"portal", "destination":"forest", "name":"Лес"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"dungeon", "name":"Подземелье"},
|
||||||
|
{"x":1, "y":7, "type":"portal", "destination":"cave", "name":"Пещера"},
|
||||||
|
{"x":13, "y":1, "type":"portal", "destination":"mountain", "name":"Горы"},
|
||||||
|
{"x":1, "y":13, "type":"portal", "destination":"swamp", "name":"Болото"},
|
||||||
|
{"x":13, "y":13, "type":"portal", "destination":"ruins", "name":"Руины"}
|
||||||
|
],
|
||||||
|
"tavern": [
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"🚪 Выход"},
|
||||||
|
{"x":3, "y":3, "type":"torch"},
|
||||||
|
{"x":11, "y":3, "type":"torch"},
|
||||||
|
{"x":3, "y":10, "type":"torch"},
|
||||||
|
{"x":11, "y":10, "type":"torch"},
|
||||||
|
{"x":4, "y":6, "type":"table"},
|
||||||
|
{"x":4, "y":8, "type":"table"},
|
||||||
|
{"x":10, "y":6, "type":"table"},
|
||||||
|
{"x":10, "y":8, "type":"table"}
|
||||||
|
],
|
||||||
|
"forest": [
|
||||||
|
{"x":2, "y":2, "type":"tree"},
|
||||||
|
{"x":4, "y":3, "type":"tree"},
|
||||||
|
{"x":6, "y":4, "type":"tree"},
|
||||||
|
{"x":8, "y":5, "type":"tree"},
|
||||||
|
{"x":10, "y":6, "type":"tree"},
|
||||||
|
{"x":12, "y":7, "type":"tree"},
|
||||||
|
{"x":4, "y":4, "type":"rock"},
|
||||||
|
{"x":10, "y":10, "type":"rock"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"Деревня"}
|
||||||
|
],
|
||||||
|
"dungeon": [
|
||||||
|
{"x":1, "y":1, "type":"pillar"},
|
||||||
|
{"x":13, "y":1, "type":"pillar"},
|
||||||
|
{"x":1, "y":13, "type":"pillar"},
|
||||||
|
{"x":13, "y":13, "type":"pillar"},
|
||||||
|
{"x":4, "y":4, "type":"torch"},
|
||||||
|
{"x":9, "y":4, "type":"torch"},
|
||||||
|
{"x":4, "y":9, "type":"torch"},
|
||||||
|
{"x":9, "y":9, "type":"torch"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"Деревня"}
|
||||||
|
],
|
||||||
|
"cave": [
|
||||||
|
{"x":2, "y":2, "type":"crystal"},
|
||||||
|
{"x":11, "y":3, "type":"crystal"},
|
||||||
|
{"x":8, "y":8, "type":"crystal"},
|
||||||
|
{"x":3, "y":11, "type":"crystal"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"Деревня"}
|
||||||
|
],
|
||||||
|
"mountain": [
|
||||||
|
{"x":3, "y":3, "type":"rock"},
|
||||||
|
{"x":10, "y":3, "type":"rock"},
|
||||||
|
{"x":5, "y":8, "type":"rock"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"Деревня"}
|
||||||
|
],
|
||||||
|
"swamp": [
|
||||||
|
{"x":2, "y":2, "type":"rock"},
|
||||||
|
{"x":11, "y":4, "type":"rock"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"Деревня"}
|
||||||
|
],
|
||||||
|
"ruins": [
|
||||||
|
{"x":2, "y":2, "type":"pillar"},
|
||||||
|
{"x":11, "y":2, "type":"pillar"},
|
||||||
|
{"x":2, "y":11, "type":"pillar"},
|
||||||
|
{"x":11, "y":11, "type":"pillar"},
|
||||||
|
{"x":5, "y":5, "type":"rock"},
|
||||||
|
{"x":8, "y":3, "type":"rock"},
|
||||||
|
{"x":3, "y":8, "type":"rock"},
|
||||||
|
{"x":10, "y":9, "type":"rock"},
|
||||||
|
{"x":4, "y":4, "type":"torch"},
|
||||||
|
{"x":9, "y":4, "type":"torch"},
|
||||||
|
{"x":4, "y":9, "type":"torch"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"village", "name":"Деревня"},
|
||||||
|
{"x":7, "y":1, "type":"portal", "destination":"abyss", "name":"Бездна"}
|
||||||
|
],
|
||||||
|
"abyss": [
|
||||||
|
{"x":2, "y":2, "type":"pillar"},
|
||||||
|
{"x":12, "y":2, "type":"pillar"},
|
||||||
|
{"x":2, "y":12, "type":"pillar"},
|
||||||
|
{"x":12, "y":12, "type":"pillar"},
|
||||||
|
{"x":4, "y":4, "type":"torch"},
|
||||||
|
{"x":10, "y":4, "type":"torch"},
|
||||||
|
{"x":4, "y":10, "type":"torch"},
|
||||||
|
{"x":10, "y":10, "type":"torch"},
|
||||||
|
{"x":7, "y":13, "type":"portal", "destination":"ruins", "name":"Руины"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"village": ["none","none","rain","sunny"],
|
||||||
|
"tavern": ["none"],
|
||||||
|
"forest": ["rain","rain","fog","none"],
|
||||||
|
"dungeon": ["none","fog"],
|
||||||
|
"cave": ["none","fog"],
|
||||||
|
"mountain": ["snow","snow","none"],
|
||||||
|
"swamp": ["fog","fog","rain"],
|
||||||
|
"ruins": ["fog","fog","none"],
|
||||||
|
"abyss": ["fog","fog","fog"]
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"Трактирщик": {
|
||||||
|
"start": {
|
||||||
|
"text": "Добро пожаловать в «Золотой Кубок»! Что желаете?",
|
||||||
|
"opts": [
|
||||||
|
{ "label": "Отдохнуть (20 🪙)", "next": "rest", "cost": 20 },
|
||||||
|
{ "label": "Купить напиток (10 🪙)", "next": "drink", "cost": 10 },
|
||||||
|
{ "label": "Послушать слухи", "next": "rumors" },
|
||||||
|
{ "label": "До свидания", "next": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rest": {
|
||||||
|
"text": "Хорошо, комната готова. Ложитесь — утром будете как новенький!",
|
||||||
|
"opts": [{ "label": "Спасибо!", "next": null, "reward": { "hp": 9999, "mp": 9999 } }]
|
||||||
|
},
|
||||||
|
"drink": {
|
||||||
|
"text": "Вот кружка доброго эля. Придаст сил на бой!",
|
||||||
|
"opts": [{ "label": "За удачу!", "next": null, "reward": { "buff": "str" } }]
|
||||||
|
},
|
||||||
|
"rumors": {
|
||||||
|
"text": "Говорят, в Руинах видели странный свет по ночам... А ещё что Бездна всё расширяется. Путники туда заходят — и не возвращаются.",
|
||||||
|
"opts": [{ "label": "Занятно...", "next": null }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Стражник": {
|
||||||
|
"start": { "text":"Путник! Тут неспокойно. Гоблины снова шалят в лесу. Чем могу помочь?",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"📍 Расскажи о локациях", "next":"lore" },
|
||||||
|
{ "label":"💰 Заплатить за совет (15 💰)", "next":"tip", "cost":15 },
|
||||||
|
{ "label":"❌ Ничего, спасибо", "next":null }
|
||||||
|
]},
|
||||||
|
"lore": { "text":"Лес к северу опасен — там волки и гоблины. В подземелье орудует нежить. В пещере — дракон. На болоте засела Ведьма. Горы охраняет Голем.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Понятно, спасибо", "next":null }
|
||||||
|
]},
|
||||||
|
"tip": { "text":"Тайный совет: Паук ядовит — возьми антидот. Голем неуязвим к физике, только магия берёт. Ну и не суйся на болото ночью.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Ценная информация!", "next":null, "reward":{ "exp":30 } }
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"Старик": {
|
||||||
|
"start": { "text":"Хм... Давненько не видел таких юных искателей приключений. Что тебя интересует?",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"📖 История Эйдона", "next":"history" },
|
||||||
|
{ "label":"🧙 Тайны магии", "next":"magic" },
|
||||||
|
{ "label":"🎁 Благословение (50 💰)", "next":"bless", "cost":50 },
|
||||||
|
{ "label":"❌ Ничего", "next":null }
|
||||||
|
]},
|
||||||
|
"history": { "text":"Эйдон — земля, рождённая из хаоса. Когда-то здесь жили древние маги. Они создали Голема и Лича в качестве стражей. Но амбиции взяли верх, и мир погрузился в войну...",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Что случилось дальше?", "next":"history2" },
|
||||||
|
{ "label":"Понятно", "next":null }
|
||||||
|
]},
|
||||||
|
"history2": { "text":"Войну остановил Первый Герой — он запечатал Лича в болоте, а Голем заснул в горах. Но печать слабеет... Лич снова собирает силы.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Мне нужно остановить его", "next":null, "reward":{ "exp":50 } }
|
||||||
|
]},
|
||||||
|
"magic": { "text":"Магия идёт из земли. Кристаллы пещер — её источники. Но у каждой стихии есть противоположность: огонь слаб против льда, яд — против антидота.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Полезно знать", "next":null, "reward":{ "exp":25 } }
|
||||||
|
]},
|
||||||
|
"bless": { "text":"Пусть удача сопутствует тебе. Прими моё благословение — пусть следующий бой будет в твою пользу.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Благодарю, мудрец", "next":null, "reward":{ "exp":40, "buff":"def" } }
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"Эльф": {
|
||||||
|
"start": { "text":"О, смертный... Ты пришёл в лес в неспокойное время. Что тебе нужно?",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"🌿 Собрать травы", "next":"herbs" },
|
||||||
|
{ "label":"⚔️ Где самые опасные враги?", "next":"danger" },
|
||||||
|
{ "label":"🎁 Дар природы", "next":"gift" },
|
||||||
|
{ "label":"❌ Просто проходил мимо", "next":null }
|
||||||
|
]},
|
||||||
|
"herbs": { "text":"В этом лесу много целебных трав. Возьми — пригодятся для зелий.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Спасибо!", "next":null, "reward":{ "item":"herb", "qty":3 } }
|
||||||
|
]},
|
||||||
|
"danger": { "text":"Глубже в лес — волки и бандиты. Осторожнее с пауками — их яд силён. А тролль у болота регенерирует — нужен огонь.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Буду осторожен", "next":null }
|
||||||
|
]},
|
||||||
|
"gift": { "text":"Природа даёт дары тем, кто уважает её. Прими этот амулет леса.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Прекрасно!", "next":null, "reward":{ "exp":60, "item":"slime_gel", "qty":2 } }
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"Шаман": {
|
||||||
|
"start": { "text":"Я чувствую силу в тебе... и тьму вокруг. Болото охраняет тайны. Что ты ищешь?",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"🧪 Исцели меня", "next":"heal" },
|
||||||
|
{ "label":"📜 Секрет болота", "next":"secret" },
|
||||||
|
{ "label":"🔮 Предсказание", "next":"predict" },
|
||||||
|
{ "label":"❌ Ухожу", "next":null }
|
||||||
|
]},
|
||||||
|
"heal": { "text":"Болото очищает. Постой на этой земле... готово, яды ушли.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Благодарю шаман", "next":null, "reward":{ "cure":true, "hp":30 } }
|
||||||
|
]},
|
||||||
|
"secret": { "text":"Ведьма черпает силу из болотных трясин. Вытащи её на твёрдую землю — и она ослабнет. Святой огонь сожжёт её договор с тьмой.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Это важно", "next":null, "reward":{ "exp":45 } }
|
||||||
|
]},
|
||||||
|
"predict": { "text":"Я вижу... тебя окружают враги. Один из них обманет тебя. Но ты победишь. Цена предсказания — немного твоей силы.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Принять предсказание", "next":null, "reward":{ "exp":35 } }
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"Призрак": {
|
||||||
|
"start": { "text":"...ты слышишь меня? Хорошо. Я был солдатом. Погиб здесь давно. Чего ты хочешь, живой?",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Кто ты?", "next":"who" },
|
||||||
|
{ "label":"Как победить Лича?", "next":"lich" },
|
||||||
|
{ "label":"Покойся с миром", "next":null }
|
||||||
|
]},
|
||||||
|
"who": { "text":"Рядовой Эрик из армии Короля. Мы пришли зачистить подземелье от нежити. Никто не вернулся. Лич поглотил наши души... кроме моей. Я слишком упрямый.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Мне жаль", "next":"lich" },
|
||||||
|
{ "label":"Покойся с миром", "next":null }
|
||||||
|
]},
|
||||||
|
"lich": { "text":"Лич хранит свою душу в филактерии — магическом сосуде. Пока он цел — Лич не умрёт. Разбей его святым заклинанием... или просто бей достаточно сильно. Удачи тебе, живой.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Я уничтожу Лича", "next":null, "reward":{ "exp":80 } }
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"Страж": {
|
||||||
|
"start": { "text":"Стоп! Ты живой? Странно видеть здесь живых... Руины опасны. Духи воинов и виверны охраняют эти камни. Что тебе нужно?",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Расскажи об этом месте", "next":"history" },
|
||||||
|
{ "label":"Где найти виверн?", "next":"wyvern" },
|
||||||
|
{ "label":"Дай задание", "next":"quest" },
|
||||||
|
{ "label":"Ничего, спасибо", "next":null }
|
||||||
|
]},
|
||||||
|
"history": { "text":"Когда-то здесь стоял великий замок короля Эйдора. Армия тьмы уничтожила его за одну ночь. Я был среди защитников. Теперь мы здесь навсегда — ждём героя, который упокоит проклятие.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Как снять проклятие?", "next":"curse" },
|
||||||
|
{ "label":"Я помогу", "next":null, "reward":{ "exp":60 } }
|
||||||
|
]},
|
||||||
|
"curse": { "text":"Убей Виверну-Матриарха в северо-восточном углу. Она — источник проклятия. Её смерть ослабит духов. Это единственный путь.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Я её найду", "next":null, "reward":{ "exp":90, "item":"ghost_essence" } }
|
||||||
|
]},
|
||||||
|
"wyvern": { "text":"Виверны гнездятся в разрушенных башнях — смотри на северо-восток. Яд виверны — ценный алхимический компонент. Удача тебе нужна больше, чем смелость.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Спасибо за совет", "next":null, "reward":{ "exp":40 } }
|
||||||
|
]},
|
||||||
|
"quest": { "text":"Упокой трёх призраков в этих руинах. Они страдают. Их имена я знаю, но произнести не могу. Просто уничтожь их — это лучшее, что ты можешь сделать.",
|
||||||
|
"opts": [
|
||||||
|
{ "label":"Хорошо, займусь этим", "next":null, "reward":{ "exp":50 } }
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
317
index.html
Normal file
317
index.html
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>⚔️ Хроники Эйдона — RPG</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚔️</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game-container">
|
||||||
|
<canvas id="gameCanvas" width="900" height="600"></canvas>
|
||||||
|
|
||||||
|
<!-- HUD -->
|
||||||
|
<div id="hud">
|
||||||
|
<div class="hpill">❤️ <b id="h-hp">100/100</b></div>
|
||||||
|
<div class="hbar"><div class="hbar-hp" id="b-hp" style="width:100%"></div></div>
|
||||||
|
<div class="hpill">💧 <b id="h-mp">50/50</b></div>
|
||||||
|
<div class="hbar"><div class="hbar-mp" id="b-mp" style="width:100%"></div></div>
|
||||||
|
<div class="hpill">⭐ Ур.<b id="h-lv">1</b></div>
|
||||||
|
<div class="hbar" style="width:75px"><div class="hbar-exp" id="b-exp" style="width:0%"></div></div>
|
||||||
|
<div id="hud-right">
|
||||||
|
<div class="hpill">💰 <b id="h-gold">50</b></div>
|
||||||
|
<div class="hpill">⚔️ <b id="h-atk">10</b> 🛡️ <b id="h-def">5</b></div>
|
||||||
|
<span id="btn-mute" onclick="Audio.toggleMute()">🔊</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сообщения и достижения -->
|
||||||
|
<div id="msg-overlay"></div>
|
||||||
|
|
||||||
|
<!-- ════ BOSS BAR ════ -->
|
||||||
|
<div id="boss-bar" style="display:none">
|
||||||
|
<div id="boss-bar-name">БОСС</div>
|
||||||
|
<div id="boss-bar-track"><div id="boss-bar-fill"></div></div>
|
||||||
|
<div id="boss-bar-text">HP</div>
|
||||||
|
</div>
|
||||||
|
<div id="ach-toast">🏆 <span id="ach-text"></span></div>
|
||||||
|
|
||||||
|
<!-- ════ ИНВЕНТАРЬ ════ -->
|
||||||
|
<div id="inventory-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">🎒 Инвентарь</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('inventory')">×</button>
|
||||||
|
</div>
|
||||||
|
<!-- Вкладки -->
|
||||||
|
<div class="inv-tabs">
|
||||||
|
<button class="inv-tab active" onclick="Game.switchInvTab('equip')">⚔️ Снаряжение</button>
|
||||||
|
<button class="inv-tab" onclick="Game.switchInvTab('items')">🎒 Предметы</button>
|
||||||
|
<button class="inv-tab" onclick="Game.switchInvTab('stats')">📊 Статы</button>
|
||||||
|
</div>
|
||||||
|
<div class="inv-body">
|
||||||
|
<!-- Tab: Снаряжение — бумажная кукла -->
|
||||||
|
<div id="inv-tab-equip" class="inv-tab-pane active">
|
||||||
|
<div class="paperdoll">
|
||||||
|
<div class="pd-col">
|
||||||
|
<div class="pd-slot" id="pd-head" data-slot="head">🪖<br><small>Шлем</small></div>
|
||||||
|
<div class="pd-slot" id="pd-weapon" data-slot="weapon">⚔️<br><small>Оружие</small></div>
|
||||||
|
<div class="pd-slot" id="pd-chest" data-slot="chest">🥼<br><small>Броня</small></div>
|
||||||
|
</div>
|
||||||
|
<canvas id="portrait-inv" width="80" height="120" class="pd-portrait"></canvas>
|
||||||
|
<div class="pd-col">
|
||||||
|
<div class="pd-slot" id="pd-shield" data-slot="shield">🛡️<br><small>Щит</small></div>
|
||||||
|
<div class="pd-slot" id="pd-legs" data-slot="legs">👖<br><small>Поножи</small></div>
|
||||||
|
<div class="pd-slot" id="pd-feet" data-slot="feet">👟<br><small>Сапоги</small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pd-acc-row">
|
||||||
|
<div class="pd-slot pd-slot-wide" id="pd-acc" data-slot="acc">💍 <small>Украшение</small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Tab: Предметы -->
|
||||||
|
<div id="inv-tab-items" class="inv-tab-pane">
|
||||||
|
<div class="inv-grid" id="inv-grid"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Tab: Статы -->
|
||||||
|
<div id="inv-tab-stats" class="inv-tab-pane">
|
||||||
|
<div id="inv-stats-detail"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ МАГАЗИН ════ -->
|
||||||
|
<div id="shop-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">🏪 Магазин</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('shop')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="shop-gold">💰 Ваше золото: <strong id="shop-gold-val">0</strong></div>
|
||||||
|
<div class="shop-grid" id="shop-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ КВЕСТЫ ════ -->
|
||||||
|
<div id="quest-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">📜 Квесты</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('quest')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="quest-list" id="quest-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ ДИАЛОГ ════ -->
|
||||||
|
<div id="dialog-panel" class="panel">
|
||||||
|
<div class="dlg-name" id="dlg-npc-name">NPC</div>
|
||||||
|
<div class="dlg-text" id="dlg-text">...</div>
|
||||||
|
<div class="dlg-options" id="dlg-options"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ БОЙ ════ -->
|
||||||
|
<div id="combat-panel" class="panel">
|
||||||
|
<div class="cbt-head">
|
||||||
|
<span class="cbt-title">⚔️ БОЙ</span>
|
||||||
|
<span id="cbt-status">Ваш ход</span>
|
||||||
|
</div>
|
||||||
|
<div class="cbt-body">
|
||||||
|
<div class="cbt-fighter" id="cbt-enemy">
|
||||||
|
<div class="portrait-wrap">
|
||||||
|
<canvas id="portrait-enemy" class="portrait-canvas" width="90" height="100"></canvas>
|
||||||
|
<div class="portrait-blink" id="blink-enemy"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cf-name" id="cf-ename">Враг</div>
|
||||||
|
<div class="cf-hpbar"><div class="cf-hpfill cf-enemy" id="cf-ehp" style="width:100%"></div></div>
|
||||||
|
<div class="cf-hptext" id="cf-ehpt">HP: 0/0</div>
|
||||||
|
<div class="cf-status" id="cf-estatus"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cbt-fighter" id="cbt-player">
|
||||||
|
<div class="portrait-wrap">
|
||||||
|
<canvas id="portrait-player" class="portrait-canvas" width="90" height="100"></canvas>
|
||||||
|
<div class="portrait-blink" id="blink-player"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cf-name" id="cf-pname">Герой</div>
|
||||||
|
<div class="cf-hpbar"><div class="cf-hpfill cf-player" id="cf-php" style="width:100%"></div></div>
|
||||||
|
<div class="cf-hptext" id="cf-phpt">HP: 0/0</div>
|
||||||
|
<div class="cf-status" id="cf-pstatus"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cbt-actions" id="cbt-actions"></div>
|
||||||
|
<div class="cbt-log" id="cbt-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ СКИЛЛ ════ -->
|
||||||
|
<div id="skill-panel" class="panel">
|
||||||
|
<div class="ph"><span class="ph-title">✨ Повышение уровня!</span></div>
|
||||||
|
<div class="sk-intro">Выберите новый навык:</div>
|
||||||
|
<div class="sk-grid" id="sk-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ ДЕРЕВО ПЕРКОВ ════ -->
|
||||||
|
<div id="perk-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">🌳 Дерево умений</span>
|
||||||
|
<span id="perk-points-display" style="font-size:11px;color:#ffd700;padding:0 10px"></span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('perk')">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="perk-class-name" style="padding:3px 14px;font-size:10px;color:#555;border-bottom:1px solid #1e1e38"></div>
|
||||||
|
<div id="perk-branches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ ЖУРНАЛ ЛОРА ════ -->
|
||||||
|
<div id="lore-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">📖 Журнал лора</span>
|
||||||
|
<span id="lore-count" style="font-size:10px;color:#778;padding:0 10px"></span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('lore')">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="lore-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ ЗАЧАРОВАНИЕ ════ -->
|
||||||
|
<div id="enchant-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">✨ Зачарование</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('enchant')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="enchant-body">
|
||||||
|
<div class="enchant-left">
|
||||||
|
<div class="enchant-col-title">Предмет</div>
|
||||||
|
<div id="enchant-item-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="enchant-right">
|
||||||
|
<div class="enchant-col-title">Зачарование</div>
|
||||||
|
<div id="enchant-detail"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ БЕСТИАРИЙ ════ -->
|
||||||
|
<div id="bestiary-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">📖 Бестиарий</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('bestiary')">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="bestiary-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ ДОСТИЖЕНИЯ ════ -->
|
||||||
|
<div id="achiev-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">🏆 Достижения</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('achiev')">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="achiev-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ КРАФТИНГ ════ -->
|
||||||
|
<div id="craft-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">⚒️ Крафтинг</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('craft')">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;height:460px">
|
||||||
|
<div style="width:210px;border-right:1px solid #1e1e38;display:flex;flex-direction:column">
|
||||||
|
<div class="craft-tabs" id="craft-tabs"></div>
|
||||||
|
<div id="craft-recipe-list" style="overflow-y:auto;flex:1;padding:6px"></div>
|
||||||
|
</div>
|
||||||
|
<div id="craft-detail" style="flex:1;padding:12px;overflow-y:auto">
|
||||||
|
<div style="color:#333;font-size:12px;margin-top:50px;text-align:center">Выберите рецепт</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ СТАРТОВЫЙ ЭКРАН ════ -->
|
||||||
|
<audio id="menu-bgm" src="mainmenu.mp3" loop preload="auto"></audio>
|
||||||
|
|
||||||
|
<!-- ════ СПЛЭШ ЭКРАН ════ -->
|
||||||
|
<div id="splash-screen">
|
||||||
|
<canvas id="splash-canvas" width="900" height="600"></canvas>
|
||||||
|
<div class="splash-content">
|
||||||
|
<div class="splash-eyeline">— Хроники Эйдона —</div>
|
||||||
|
<div class="splash-title">ХРОНИКИ<br>ЭЙДОНА</div>
|
||||||
|
<div class="splash-subtitle">Изометрическая ролевая игра</div>
|
||||||
|
<button class="splash-btn" onclick="splashEnter()">⚔ Начать Путешествие ⚔</button>
|
||||||
|
<div class="splash-hint">нажмите для продолжения</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="start-screen" style="display:none">
|
||||||
|
<canvas id="menu-canvas" width="900" height="600"></canvas>
|
||||||
|
|
||||||
|
<div id="menu-main" class="menu-view">
|
||||||
|
<div class="s-title">⚔️ Хроники Эйдона ⚔️</div>
|
||||||
|
<div class="s-subtitle">Изометрическая RPG</div>
|
||||||
|
<div class="s-slots" id="s-slots"></div>
|
||||||
|
<button class="s-new-btn" onclick="menuShowClassSelect(null)">+ Новая игра</button>
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;justify-content:center">
|
||||||
|
<button id="btn-save-folder" class="s-folder-btn" onclick="menuSelectSaveFolder()" title="Выбрать папку для JSON-сохранений">📁 Папка сохранений</button>
|
||||||
|
<button class="s-folder-btn" onclick="menuExportSaves()" title="Экспортировать сохранения в выбранную папку">💾 Экспорт</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="menu-class" class="menu-view" style="display:none">
|
||||||
|
<div class="s-title" style="font-size:26px;margin-bottom:2px">⚔️ Хроники Эйдона ⚔️</div>
|
||||||
|
<div id="cls-slot-hint" class="cls-slot-hint">Выберите класс</div>
|
||||||
|
<div class="cls-grid" id="cls-grid"></div>
|
||||||
|
<button class="s-back-btn" onclick="menuBack()">← Назад</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ КАРТА МИРА ════ -->
|
||||||
|
<div id="worldmap-panel" class="panel">
|
||||||
|
<div class="ph">
|
||||||
|
<span class="ph-title">🗺️ Карта мира</span>
|
||||||
|
<button class="ph-close" onclick="Game.togglePanel('worldmap')">×</button>
|
||||||
|
</div>
|
||||||
|
<canvas id="worldmap-canvas" width="500" height="300" style="display:block;cursor:pointer;"></canvas>
|
||||||
|
<div style="text-align:center;font-size:10px;color:#445;padding:4px 0 6px;">Нажми на соседнюю локацию чтобы переместиться</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════ ПАУЗА ════ -->
|
||||||
|
<div id="pause-panel" class="panel pause-overlay">
|
||||||
|
<div class="pause-box">
|
||||||
|
<div class="pause-title">⏸ ПАУЗА</div>
|
||||||
|
<button class="pause-btn" onclick="Game.resumeGame()">▶ Продолжить</button>
|
||||||
|
<button class="pause-btn" onclick="Game.saveGame()">💾 Сохранить</button>
|
||||||
|
<div class="pause-vol">
|
||||||
|
<label>🔊 Громкость</label>
|
||||||
|
<input type="range" id="vol-slider" min="0" max="100" value="60"
|
||||||
|
oninput="Audio.setVolume(this.value/100)">
|
||||||
|
</div>
|
||||||
|
<button class="pause-btn pause-exit" onclick="Game.exitToMenu()">🚪 Выйти в меню</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Хинт взаимодействия -->
|
||||||
|
<div id="interact-hint"></div>
|
||||||
|
|
||||||
|
<!-- Индикатор сохранения -->
|
||||||
|
<div id="save-ind">💾 Сохранено</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Управление -->
|
||||||
|
<div style="margin-top:8px;font-size:10px;color:#333;text-align:center">
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">WASD/↑↓←→</span> движение
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">I</span> инвентарь
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">Q</span> квесты
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">T</span> таланты
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">C</span> крафтинг
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">L</span> журнал
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">E</span> зачарование
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">B</span> бестиарий
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">H</span> достижения
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">M</span> карта/путь
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">P</span> сохранить
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">1-5</span> бой
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">F</span> взаимодействие
|
||||||
|
<span style="background:#0d0d1a;border:1px solid #2a2a4a;border-radius:3px;padding:1px 5px;color:#666">ESC</span> закрыть
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
<script src="rpg.js"></script>
|
||||||
|
<script src="saves.js"></script>
|
||||||
|
<script src="audio.js"></script>
|
||||||
|
<script src="game.js"></script>
|
||||||
|
<script src="data-loader.js"></script>
|
||||||
|
<script src="menu.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
mainmenu.mp3
Normal file
BIN
mainmenu.mp3
Normal file
Binary file not shown.
409
menu.js
Normal file
409
menu.js
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
// ============================================================
|
||||||
|
// MENU.JS — Стартовый экран: анимация, слоты, выбор класса
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
let _menuSlot = null;
|
||||||
|
|
||||||
|
window.onload = async function () {
|
||||||
|
try {
|
||||||
|
await DataLoader.load();
|
||||||
|
} catch (e) {
|
||||||
|
// Ошибка уже показана баннером в DataLoader._showError()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Audio.init();
|
||||||
|
menuBuildClassGrid();
|
||||||
|
menuBuildSlots();
|
||||||
|
// Показываем сплэш-экран; start-screen скрыт через style="display:none" в HTML
|
||||||
|
splashStartAnim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Переход со сплэша в главное меню ──────────────────────
|
||||||
|
function splashEnter() {
|
||||||
|
const splash = document.getElementById('splash-screen');
|
||||||
|
if (!splash) return;
|
||||||
|
// Запускаем музыку (первый клик пользователя — браузер разрешает)
|
||||||
|
const bgm = document.getElementById('menu-bgm');
|
||||||
|
if (bgm) bgm.play().catch(() => {});
|
||||||
|
// Сначала показываем главное меню позади сплэша — чтобы не мелькал игровой интерфейс
|
||||||
|
const ss = document.getElementById('start-screen');
|
||||||
|
if (ss) ss.style.display = '';
|
||||||
|
menuStartAnim();
|
||||||
|
// Затем плавно убираем сплэш поверх уже готового меню
|
||||||
|
splash.style.opacity = '0';
|
||||||
|
splash.style.pointerEvents = 'none';
|
||||||
|
setTimeout(() => { splash.style.display = 'none'; }, 750);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Анимация сплэш-экрана ─────────────────────────────────
|
||||||
|
function splashStartAnim() {
|
||||||
|
const mc = document.getElementById('splash-canvas');
|
||||||
|
if (!mc) return;
|
||||||
|
const ctx = mc.getContext('2d');
|
||||||
|
mc.width = 900; mc.height = 600;
|
||||||
|
|
||||||
|
const RUNES = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚷ','ᚹ','ᛗ','ᛟ','ᚾ','ᛁ','ᛃ','ᚲ','ᛏ','ᛚ'];
|
||||||
|
|
||||||
|
// Три слоя звёзд (параллакс)
|
||||||
|
const starLayers = [
|
||||||
|
Array.from({length: 110}, () => ({ x: Math.random()*900, y: Math.random()*600, r: Math.random()*0.7+0.1, v: 0.04, a: Math.random()*0.4+0.1 })),
|
||||||
|
Array.from({length: 55}, () => ({ x: Math.random()*900, y: Math.random()*600, r: Math.random()*1.1+0.3, v: 0.10, a: Math.random()*0.5+0.2 })),
|
||||||
|
Array.from({length: 22}, () => ({ x: Math.random()*900, y: Math.random()*600, r: Math.random()*1.6+0.5, v: 0.18, a: Math.random()*0.6+0.3 })),
|
||||||
|
];
|
||||||
|
|
||||||
|
const runes = [];
|
||||||
|
let lastRune = 0;
|
||||||
|
let angle = 0;
|
||||||
|
|
||||||
|
function frame(ts) {
|
||||||
|
const el = document.getElementById('splash-screen');
|
||||||
|
if (!el || el.style.display === 'none' || el.style.opacity === '0') return;
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
|
||||||
|
// Фон
|
||||||
|
ctx.fillStyle = '#02020a';
|
||||||
|
ctx.fillRect(0, 0, 900, 600);
|
||||||
|
|
||||||
|
// Центральное свечение
|
||||||
|
const cx = 450, cy = 288;
|
||||||
|
const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, 420);
|
||||||
|
glow.addColorStop(0, 'rgba(35,8,75,0.55)');
|
||||||
|
glow.addColorStop(0.45,'rgba(15,4,38,0.28)');
|
||||||
|
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
|
ctx.fillStyle = glow;
|
||||||
|
ctx.fillRect(0, 0, 900, 600);
|
||||||
|
|
||||||
|
// Звёзды
|
||||||
|
starLayers.forEach((layer, li) => {
|
||||||
|
layer.forEach(s => {
|
||||||
|
s.y -= s.v;
|
||||||
|
if (s.y < -2) { s.y = 602; s.x = Math.random() * 900; }
|
||||||
|
const tw = 0.65 + Math.sin(ts / 900 + s.x * 0.05) * 0.35;
|
||||||
|
ctx.fillStyle = li === 2
|
||||||
|
? `rgba(255,220,140,${s.a * tw})`
|
||||||
|
: `rgba(190,190,255,${s.a * tw})`;
|
||||||
|
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Магический круг
|
||||||
|
angle += 0.0015;
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(cx, cy);
|
||||||
|
|
||||||
|
// Внешнее кольцо
|
||||||
|
ctx.rotate(angle);
|
||||||
|
ctx.beginPath(); ctx.arc(0, 0, 215, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = 'rgba(80,40,160,0.18)'; ctx.lineWidth = 1; ctx.stroke();
|
||||||
|
|
||||||
|
// Внутреннее кольцо
|
||||||
|
ctx.rotate(-angle * 2.3);
|
||||||
|
ctx.beginPath(); ctx.arc(0, 0, 155, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = 'rgba(100,55,185,0.14)'; ctx.lineWidth = 1; ctx.stroke();
|
||||||
|
|
||||||
|
// Шестиугольник
|
||||||
|
ctx.rotate(angle * 1.5);
|
||||||
|
ctx.strokeStyle = 'rgba(110,65,195,0.10)'; ctx.lineWidth = 0.8;
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const a1 = (i / 6) * Math.PI * 2;
|
||||||
|
const a2 = ((i + 2) / 6) * Math.PI * 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(Math.cos(a1) * 155, Math.sin(a1) * 155);
|
||||||
|
ctx.lineTo(Math.cos(a2) * 155, Math.sin(a2) * 155);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Точки на кольце
|
||||||
|
ctx.rotate(-angle * 0.5);
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const a = (i / 8) * Math.PI * 2 + angle * 3;
|
||||||
|
const px = Math.cos(a) * 215, py = Math.sin(a) * 215;
|
||||||
|
ctx.beginPath(); ctx.arc(px, py, 2.5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(150,80,220,${0.4 + Math.sin(ts/600 + i) * 0.2})`;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Лучи света
|
||||||
|
ctx.save();
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const a = (i / 8) * Math.PI * 2 + angle * 1.8;
|
||||||
|
const pulse = 0.06 + Math.sin(ts / 1100 + i) * 0.02;
|
||||||
|
const grad = ctx.createLinearGradient(cx, cy,
|
||||||
|
cx + Math.cos(a) * 380, cy + Math.sin(a) * 380);
|
||||||
|
grad.addColorStop(0, `rgba(110,50,210,${pulse})`);
|
||||||
|
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx, cy);
|
||||||
|
const w = 0.07;
|
||||||
|
ctx.arc(cx, cy, 380, a - w, a + w);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Пульсирующая сфера в центре
|
||||||
|
const orbP = Math.sin(ts / 800);
|
||||||
|
const orb = ctx.createRadialGradient(cx, cy, 0, cx, cy, 65 + orbP * 6);
|
||||||
|
orb.addColorStop(0, `rgba(190,130,255,${0.14 + orbP * 0.05})`);
|
||||||
|
orb.addColorStop(0.5, `rgba(90,40,190,${0.07 + orbP * 0.02})`);
|
||||||
|
orb.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
|
ctx.fillStyle = orb;
|
||||||
|
ctx.beginPath(); ctx.arc(cx, cy, 65 + orbP * 6, 0, Math.PI * 2); ctx.fill();
|
||||||
|
|
||||||
|
// Силуэты мечей
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.09 + Math.sin(ts / 3200) * 0.02;
|
||||||
|
ctx.strokeStyle = '#c8a020'; ctx.lineWidth = 1.6;
|
||||||
|
|
||||||
|
// Левый меч
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(185, 295);
|
||||||
|
ctx.rotate(-0.28 + Math.sin(ts / 4000) * 0.015);
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(0, 110); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(-22, -55); ctx.lineTo(22, -55); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(-6, -118); ctx.moveTo(0, -140); ctx.lineTo(6, -118); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Правый меч
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(715, 295);
|
||||||
|
ctx.rotate(0.28 - Math.sin(ts / 4000) * 0.015);
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(0, 110); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(-22, -55); ctx.lineTo(22, -55); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(-6, -118); ctx.moveTo(0, -140); ctx.lineTo(6, -118); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Руны
|
||||||
|
if (ts - lastRune > 420) {
|
||||||
|
lastRune = ts;
|
||||||
|
const col = Math.random() < 0.35 ? '#c8a020' : Math.random() < 0.5 ? '#9966cc' : '#4a4aaa';
|
||||||
|
runes.push({
|
||||||
|
x: 40 + Math.random() * 820,
|
||||||
|
y: 620,
|
||||||
|
ch: RUNES[Math.floor(Math.random() * RUNES.length)],
|
||||||
|
a: 0.75,
|
||||||
|
vy: -(0.38 + Math.random() * 0.52),
|
||||||
|
sz: 10 + Math.random() * 15,
|
||||||
|
col,
|
||||||
|
dx: (Math.random() - 0.5) * 0.28,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runes.forEach(r => { r.y += r.vy; r.x += r.dx; r.a -= 0.0022; });
|
||||||
|
for (let i = runes.length - 1; i >= 0; i--) {
|
||||||
|
const r = runes[i];
|
||||||
|
if (r.a <= 0 || r.y < -10) { runes.splice(i, 1); continue; }
|
||||||
|
ctx.globalAlpha = r.a;
|
||||||
|
ctx.fillStyle = r.col;
|
||||||
|
ctx.font = `${r.sz}px serif`;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.fillText(r.ch, r.x, r.y);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
}
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызывается из любой точки взаимодействия с меню (браузер разрешает play() только после клика)
|
||||||
|
function _menuStartMusic() {
|
||||||
|
const el = document.getElementById('menu-bgm');
|
||||||
|
if (el && el.paused) el.play().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызывается из game.js при старте игры
|
||||||
|
function _stopMenuBgm() {
|
||||||
|
const el = document.getElementById('menu-bgm');
|
||||||
|
if (el && !el.paused) { el.pause(); el.currentTime = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Анимация фона главного меню ───────────────────────────
|
||||||
|
function menuStartAnim() {
|
||||||
|
const mc = document.getElementById('menu-canvas');
|
||||||
|
if (!mc) return;
|
||||||
|
const ctx = mc.getContext('2d');
|
||||||
|
mc.width = 900; mc.height = 600;
|
||||||
|
|
||||||
|
const stars = Array.from({ length: 180 }, () => ({
|
||||||
|
x: Math.random() * 900,
|
||||||
|
y: Math.random() * 600,
|
||||||
|
r: Math.random() * 1.4 + 0.2,
|
||||||
|
vx: (Math.random() - .5) * 0.1,
|
||||||
|
vy: (Math.random() - .5) * 0.05,
|
||||||
|
a: Math.random() * 0.7 + 0.2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const runes = [];
|
||||||
|
const RUNES = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚷ','ᚹ','ᛗ','ᛟ','ᚾ','ᛁ','ᛃ','ᚲ','ᛏ','ᛚ'];
|
||||||
|
let _lr = 0;
|
||||||
|
|
||||||
|
function frame(ts) {
|
||||||
|
const ss = document.getElementById('start-screen');
|
||||||
|
if (!ss || ss.style.display === 'none') return;
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
|
||||||
|
ctx.fillStyle = '#03030b';
|
||||||
|
ctx.fillRect(0, 0, 900, 600);
|
||||||
|
|
||||||
|
const grd = ctx.createRadialGradient(450, 300, 0, 450, 300, 430);
|
||||||
|
grd.addColorStop(0, 'rgba(50,20,90,0.28)');
|
||||||
|
grd.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
|
ctx.fillStyle = grd;
|
||||||
|
ctx.fillRect(0, 0, 900, 600);
|
||||||
|
|
||||||
|
// Звёзды
|
||||||
|
stars.forEach(s => {
|
||||||
|
s.x = (s.x + s.vx + 900) % 900;
|
||||||
|
s.y = (s.y + s.vy + 600) % 600;
|
||||||
|
ctx.fillStyle = `rgba(200,200,255,${s.a})`;
|
||||||
|
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Руны
|
||||||
|
if (ts - _lr > 650) {
|
||||||
|
_lr = ts;
|
||||||
|
runes.push({
|
||||||
|
x: 80 + Math.random() * 740,
|
||||||
|
y: 590 + Math.random() * 20,
|
||||||
|
ch: RUNES[Math.floor(Math.random() * RUNES.length)],
|
||||||
|
a: 0.65,
|
||||||
|
vy: -(0.35 + Math.random() * 0.35),
|
||||||
|
sz: 11 + Math.random() * 13,
|
||||||
|
col: Math.random() < 0.3 ? '#c8a020' : '#4848a8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runes.forEach(r => { r.y += r.vy; r.a -= 0.0025; });
|
||||||
|
for (let i = runes.length - 1; i >= 0; i--) {
|
||||||
|
const r = runes[i];
|
||||||
|
if (r.a <= 0) { runes.splice(i, 1); continue; }
|
||||||
|
ctx.globalAlpha = r.a;
|
||||||
|
ctx.fillStyle = r.col;
|
||||||
|
ctx.font = `${r.sz}px serif`;
|
||||||
|
ctx.fillText(r.ch, r.x, r.y);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Слоты сохранений ──────────────────────────────────────
|
||||||
|
function menuBuildSlots() {
|
||||||
|
const cont = document.getElementById('s-slots');
|
||||||
|
if (!cont) return;
|
||||||
|
cont.innerHTML = '';
|
||||||
|
for (let sl = 0; sl < 3; sl++) {
|
||||||
|
const meta = RPG.getSaveMeta(sl);
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'slot-card ' + (meta ? 'filled' : 'empty');
|
||||||
|
card.id = 'slot-card-' + sl;
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="sc-num">Слот ${sl + 1}</div>
|
||||||
|
<div class="sc-icon">${meta.icon}</div>
|
||||||
|
<div class="sc-class">${meta.className}</div>
|
||||||
|
<div class="sc-info">
|
||||||
|
⭐ Уровень ${meta.level}<br>
|
||||||
|
📍 ${meta.mapName} · День ${meta.days}<br>
|
||||||
|
⚔️ Убийств: ${meta.kills}<br>
|
||||||
|
🕐 ${meta.playTime}
|
||||||
|
</div>
|
||||||
|
<div class="sc-date">${meta.date} ${meta.saveTime}</div>
|
||||||
|
<div class="sc-play">▶ Играть</div>
|
||||||
|
<div class="sc-del" onclick="menuDeleteSlot(${sl}, event)">🗑 Удалить</div>`;
|
||||||
|
card.onclick = e => {
|
||||||
|
if (e.target.classList.contains('sc-del')) return;
|
||||||
|
_menuStartMusic();
|
||||||
|
Game.loadAndStart(sl);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="sc-num">Слот ${sl + 1}</div>
|
||||||
|
<div class="sc-icon" style="opacity:.3">📂</div>
|
||||||
|
<div class="sc-class" style="color:#444;font-size:12px">Пусто</div>
|
||||||
|
<div class="sc-info" style="margin-top:10px;color:#333">Нажмите чтобы<br>начать новую игру</div>`;
|
||||||
|
card.onclick = () => menuShowClassSelect(sl);
|
||||||
|
}
|
||||||
|
cont.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Выбор класса ──────────────────────────────────────────
|
||||||
|
function menuBuildClassGrid() {
|
||||||
|
const grid = document.getElementById('cls-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
Object.entries(RPG.CLASSES).forEach(([id, cls]) => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'cls-btn';
|
||||||
|
btn.innerHTML = `
|
||||||
|
<div class="cb-icon">${cls.icon}</div>
|
||||||
|
<div class="cb-name">${cls.name}</div>
|
||||||
|
<div class="cb-desc">${cls.desc}</div>
|
||||||
|
<div class="cb-stats">HP:${cls.hp} MP:${cls.mp} СИЛ:${cls.str} ЗАЩ:${cls.def}</div>`;
|
||||||
|
btn.onclick = () => {
|
||||||
|
const slot = _menuSlot !== null ? _menuSlot : _pickFreeSlot();
|
||||||
|
Game.start(id, slot);
|
||||||
|
};
|
||||||
|
grid.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pickFreeSlot() {
|
||||||
|
for (let i = 0; i < 3; i++) if (!RPG.hasSave(i)) return i;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Навигация меню ────────────────────────────────────────
|
||||||
|
function menuShowClassSelect(slot) {
|
||||||
|
_menuStartMusic();
|
||||||
|
_menuSlot = slot;
|
||||||
|
document.getElementById('menu-main').style.display = 'none';
|
||||||
|
document.getElementById('menu-class').style.display = 'flex';
|
||||||
|
const hint = document.getElementById('cls-slot-hint');
|
||||||
|
if (hint) hint.textContent = slot !== null ? `Новая игра — Слот ${slot + 1}` : 'Выберите класс';
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuBack() {
|
||||||
|
_menuStartMusic();
|
||||||
|
document.getElementById('menu-class').style.display = 'none';
|
||||||
|
document.getElementById('menu-main').style.display = 'flex';
|
||||||
|
_menuSlot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuDeleteSlot(slot, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!confirm(`Удалить сохранение в слоте ${slot + 1}?`)) return;
|
||||||
|
RPG.deleteSave(slot);
|
||||||
|
menuBuildSlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Выбор папки сохранений ────────────────────────────────
|
||||||
|
async function menuSelectSaveFolder() {
|
||||||
|
if (typeof SaveFS === 'undefined') {
|
||||||
|
alert('File System API недоступен в этом браузере.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!SaveFS.isSupported()) {
|
||||||
|
alert('Ваш браузер не поддерживает сохранение в файлы. Используйте Chrome/Edge.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await SaveFS.selectDir();
|
||||||
|
if (ok) {
|
||||||
|
const name = SaveFS.getDirName();
|
||||||
|
const btn = document.getElementById('btn-save-folder');
|
||||||
|
if (btn) btn.textContent = '📁 ' + name;
|
||||||
|
menuBuildSlots(); // обновить слоты из файлов
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспортировать сохранения в уже выбранную папку
|
||||||
|
async function menuExportSaves() {
|
||||||
|
if (typeof SaveFS === 'undefined' || !SaveFS.hasDir()) {
|
||||||
|
alert('Сначала выберите папку!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await SaveFS.exportAll();
|
||||||
|
alert('Сохранения экспортированы в папку: ' + SaveFS.getDirName());
|
||||||
|
}
|
||||||
3407
package-lock.json
generated
Normal file
3407
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "chronicles-of-eydon",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Хроники Эйдона — изометрическая RPG на HTML5 Canvas",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "serve . -p 8080 --no-clipboard",
|
||||||
|
"dev": "live-server --port=8080 --open=index.html"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"live-server": "^1.2.2",
|
||||||
|
"serve": "^14.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
2173
renderer.js
Normal file
2173
renderer.js
Normal file
File diff suppressed because it is too large
Load Diff
97
saves.js
Normal file
97
saves.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// ============================================================
|
||||||
|
// SAVES.JS — JSON-сохранения в отдельной папке (File System API)
|
||||||
|
// ============================================================
|
||||||
|
// Приоритет: если выбрана папка — пишем JSON-файлы.
|
||||||
|
// Всегда также пишем в localStorage как кэш/резерв.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const SaveFS = {
|
||||||
|
_dir: null, // FileSystemDirectoryHandle
|
||||||
|
|
||||||
|
isSupported() {
|
||||||
|
return 'showDirectoryPicker' in window;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasDir() {
|
||||||
|
return !!this._dir;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDirName() {
|
||||||
|
return this._dir ? this._dir.name : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Открыть диалог выбора папки, затем синхронизировать JSON → localStorage
|
||||||
|
async selectDir() {
|
||||||
|
try {
|
||||||
|
this._dir = await window.showDirectoryPicker({ id: 'eidon-saves', mode: 'readwrite' });
|
||||||
|
await this._syncFromDir();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name !== 'AbortError') console.warn('SaveFS.selectDir:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Записать слот в JSON-файл
|
||||||
|
async writeSlot(slot, data) {
|
||||||
|
if (!this._dir) return false;
|
||||||
|
try {
|
||||||
|
const fh = await this._dir.getFileHandle('save_' + slot + '.json', { create: true });
|
||||||
|
const w = await fh.createWritable();
|
||||||
|
await w.write(JSON.stringify(data, null, 2));
|
||||||
|
await w.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('SaveFS.writeSlot:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Прочитать слот из JSON-файла
|
||||||
|
async readSlot(slot) {
|
||||||
|
if (!this._dir) return null;
|
||||||
|
try {
|
||||||
|
const fh = await this._dir.getFileHandle('save_' + slot + '.json');
|
||||||
|
const file = await fh.getFile();
|
||||||
|
return JSON.parse(await file.text());
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Удалить JSON-файл слота
|
||||||
|
async deleteSlot(slot) {
|
||||||
|
if (!this._dir) return false;
|
||||||
|
try {
|
||||||
|
await this._dir.removeEntry('save_' + slot + '.json');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Прочитать все 3 JSON-файла из папки → обновить localStorage
|
||||||
|
// Если файла нет — слот в localStorage тоже очищается (папка — источник истины)
|
||||||
|
async _syncFromDir() {
|
||||||
|
for (let sl = 0; sl < 3; sl++) {
|
||||||
|
const data = await this.readSlot(sl);
|
||||||
|
if (data) {
|
||||||
|
localStorage.setItem(RPG.SAVE_PREFIX + sl, JSON.stringify(data));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(RPG.SAVE_PREFIX + sl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Экспортировать все сохранения из localStorage → JSON-файлы
|
||||||
|
async exportAll() {
|
||||||
|
for (let sl = 0; sl < 3; sl++) {
|
||||||
|
const raw = localStorage.getItem(RPG.SAVE_PREFIX + sl);
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
await this.writeSlot(sl, JSON.parse(raw));
|
||||||
|
} catch (e) { /* skip */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
791
style.css
Normal file
791
style.css
Normal file
@@ -0,0 +1,791 @@
|
|||||||
|
/* ============================================================
|
||||||
|
STYLE.CSS — Хроники Эйдона RPG
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #07070f;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||||
|
color: #e0e0e0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-container {
|
||||||
|
position: relative;
|
||||||
|
width: 900px;
|
||||||
|
height: 600px;
|
||||||
|
border: 2px solid #2a2a4a;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 50px rgba(80,80,200,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas { display: block; }
|
||||||
|
|
||||||
|
/* ───── HUD ───── */
|
||||||
|
#hud {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 46px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: linear-gradient(to bottom, rgba(5,5,15,0.92), transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.hpill {
|
||||||
|
background: rgba(15,15,30,0.85);
|
||||||
|
border: 1px solid #2a2a4a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.hpill b { color: #ffd700; }
|
||||||
|
.hbar { width: 90px; height: 10px; background: #0d0d1a; border-radius: 5px; border: 1px solid #2a2a4a; overflow: hidden; }
|
||||||
|
.hbar-hp { height: 100%; background: linear-gradient(to right, #c0392b, #e74c3c); border-radius: 5px; transition: width .3s; }
|
||||||
|
.hbar-mp { height: 100%; background: linear-gradient(to right, #1a6aa0, #3498db); border-radius: 5px; transition: width .3s; }
|
||||||
|
.hbar-exp { height: 100%; background: linear-gradient(to right, #6c3483, #9b59b6); border-radius: 5px; transition: width .3s; }
|
||||||
|
#hud-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
||||||
|
|
||||||
|
/* ───── Панели (общее) ───── */
|
||||||
|
.panel {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(8,8,18,0.97);
|
||||||
|
border: 2px solid #28284a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
z-index: 50;
|
||||||
|
box-shadow: 0 4px 40px rgba(0,0,0,0.9);
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.18s ease, visibility 0s linear 0.18s;
|
||||||
|
}
|
||||||
|
.panel.open {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: opacity 0.18s ease;
|
||||||
|
animation: panelIn 0.18s ease forwards;
|
||||||
|
}
|
||||||
|
@keyframes panelIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
.ph {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-bottom: 1px solid #1e1e38;
|
||||||
|
background: rgba(20,20,45,0.6);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
.ph-title { font-size: 16px; font-weight: bold; color: #ffd700; }
|
||||||
|
.ph-close {
|
||||||
|
background: none; border: 1px solid #444; color: #888;
|
||||||
|
width: 22px; height: 22px; border-radius: 4px; cursor: pointer;
|
||||||
|
font-size: 15px; line-height: 20px; text-align: center;
|
||||||
|
}
|
||||||
|
.ph-close:hover { background: #3a1a1a; color: #ff6666; border-color: #f66; }
|
||||||
|
|
||||||
|
/* ───── ИНВЕНТАРЬ ───── */
|
||||||
|
#inventory-panel { top: 25px; left: 50%; transform: translateX(-50%); width: 800px; max-height: 565px; overflow-y: auto; }
|
||||||
|
.inv-body { padding: 10px 12px; }
|
||||||
|
.inv-stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
|
||||||
|
.inv-stat { background: rgba(20,20,40,0.7); border: 1px solid #1e1e38; border-radius: 5px; padding: 3px 8px; font-size: 11px; }
|
||||||
|
.sec-title { font-size: 10px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin: 7px 0 5px; }
|
||||||
|
|
||||||
|
.eq-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-bottom: 10px; }
|
||||||
|
.eq-slot {
|
||||||
|
background: rgba(15,15,35,0.9); border: 2px solid #1e1e38; border-radius: 5px;
|
||||||
|
padding: 6px; cursor: pointer; min-height: 52px; transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.eq-slot:hover { border-color: #ffd700; }
|
||||||
|
.eq-slot.filled { border-color: #3a3a7a; }
|
||||||
|
.eq-label { font-size: 9px; color: #555; text-transform: uppercase; }
|
||||||
|
.eq-name { font-size: 11px; color: #ddd; margin-top: 3px; }
|
||||||
|
.eq-val { font-size: 10px; color: #888; }
|
||||||
|
|
||||||
|
.inv-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
|
||||||
|
.inv-slot {
|
||||||
|
background: rgba(15,15,35,0.9); border: 2px solid #1e1e38; border-radius: 5px;
|
||||||
|
padding: 4px; cursor: pointer; min-height: 52px; position: relative;
|
||||||
|
transition: all .15s; text-align: center;
|
||||||
|
}
|
||||||
|
.inv-slot:hover { border-color: #ffd700; background: rgba(35,35,65,0.95); transform: scale(1.04); }
|
||||||
|
.is-icon { font-size: 17px; margin-bottom: 1px; }
|
||||||
|
.is-name { font-size: 8px; color: #ccc; line-height: 1.2; }
|
||||||
|
.is-val { font-size: 8px; color: #888; }
|
||||||
|
.is-qty { position: absolute; top: 2px; right: 3px; font-size: 8px; color: #ffd700; font-weight: bold; }
|
||||||
|
/* Редкость */
|
||||||
|
.r-common { border-color: #3a3a3a !important; }
|
||||||
|
.r-uncommon { border-color: #2a7a2a !important; }
|
||||||
|
.r-rare { border-color: #2a2a9a !important; }
|
||||||
|
.r-epic { border-color: #7a2a9a !important; }
|
||||||
|
.r-legendary { border-color: #9a6a00 !important; }
|
||||||
|
|
||||||
|
/* ───── МАГАЗИН ───── */
|
||||||
|
#shop-panel { top: 35px; left: 50%; transform: translateX(-50%); width: 660px; }
|
||||||
|
.shop-gold { padding: 6px 14px; text-align: center; color: #ffd700; font-size: 13px; border-bottom: 1px solid #1e1e38; }
|
||||||
|
.shop-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; padding: 10px; }
|
||||||
|
.shop-item {
|
||||||
|
background: rgba(10,20,10,0.85); border: 2px solid #1e381e; border-radius: 6px;
|
||||||
|
padding: 7px; cursor: pointer; transition: all .18s;
|
||||||
|
}
|
||||||
|
.shop-item:hover { background: rgba(25,50,25,0.9); border-color: #4f4; }
|
||||||
|
.si-name { font-size: 12px; font-weight: bold; }
|
||||||
|
.si-price { color: #ffd700; font-size: 11px; margin-top: 2px; }
|
||||||
|
.si-stat { color: #888; font-size: 10px; }
|
||||||
|
|
||||||
|
/* ───── КВЕСТЫ ───── */
|
||||||
|
#quest-panel { top: 35px; left: 50%; transform: translateX(-50%); width: 560px; max-height: 530px; overflow-y: auto; }
|
||||||
|
.quest-list { padding: 8px 10px; }
|
||||||
|
.q-sec { color: #666; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; margin: 7px 0 4px; }
|
||||||
|
.q-card {
|
||||||
|
background: rgba(15,15,32,0.8); border: 1px solid #1e1e38; border-radius: 5px;
|
||||||
|
padding: 7px 9px; margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.q-card.active { border-left: 3px solid #ffd700; }
|
||||||
|
.q-card.completed { border-left: 3px solid #2ecc71; opacity: 0.65; }
|
||||||
|
.q-name { font-size: 12px; font-weight: bold; }
|
||||||
|
.q-desc { font-size: 10px; color: #888; margin-top: 1px; }
|
||||||
|
.q-pbar { height: 4px; background: #0d0d1a; border-radius: 2px; margin-top: 5px; overflow: hidden; }
|
||||||
|
.q-pfill { height: 100%; background: linear-gradient(to right, #6c3483, #ffd700); border-radius: 2px; transition: width .3s; }
|
||||||
|
.q-reward { font-size: 9px; color: #ffd700; margin-top: 3px; }
|
||||||
|
|
||||||
|
/* ───── ДИАЛОГ ───── */
|
||||||
|
#dialog-panel { bottom: 18px; left: 50%; transform: translateX(-50%); width: 600px; border-color: #3a3a6a; }
|
||||||
|
.dlg-name { padding: 7px 14px; color: #ffd700; font-size: 15px; font-weight: bold; border-bottom: 1px solid #1e1e38; }
|
||||||
|
.dlg-text { padding: 9px 14px; font-size: 13px; line-height: 1.55; color: #ddd; }
|
||||||
|
.dlg-options { display: flex; flex-wrap: wrap; gap: 5px; padding: 7px 14px; border-top: 1px solid #1e1e38; }
|
||||||
|
.dlg-opt {
|
||||||
|
background: rgba(20,20,50,0.85); border: 1px solid #3a3a6a; border-radius: 4px;
|
||||||
|
color: #ddd; padding: 4px 10px; cursor: pointer; font-size: 11px; transition: all .15s;
|
||||||
|
}
|
||||||
|
.dlg-opt:hover { background: rgba(50,50,90,0.9); border-color: #ffd700; color: #ffd700; }
|
||||||
|
|
||||||
|
/* ───── БОЕВАЯ ПАНЕЛЬ ───── */
|
||||||
|
#combat-panel {
|
||||||
|
bottom: 0; left: 0; right: 0;
|
||||||
|
border-radius: 0; border-bottom: none; border-left: none; border-right: none;
|
||||||
|
border-top-color: #5a1a1a;
|
||||||
|
background: rgba(8,3,8,0.97);
|
||||||
|
}
|
||||||
|
.cbt-head {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 5px 14px; background: rgba(35,5,5,0.7); border-bottom: 1px solid #3a1010;
|
||||||
|
}
|
||||||
|
.cbt-title { color: #e74c3c; font-size: 13px; font-weight: bold; }
|
||||||
|
#cbt-status { color: #aaa; font-size: 11px; }
|
||||||
|
.cbt-body { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 8px 14px; }
|
||||||
|
.cbt-fighter { background: rgba(15,10,15,0.8); border-radius: 5px; padding: 7px; }
|
||||||
|
.cf-name { font-size: 12px; font-weight: bold; }
|
||||||
|
.cf-hpbar { height: 9px; background: #0a0a0f; border-radius: 4px; margin: 4px 0; overflow: hidden; }
|
||||||
|
.cf-hpfill { height: 100%; border-radius: 4px; transition: width .3s; }
|
||||||
|
.cf-enemy { background: linear-gradient(to right, #6a0000, #c0392b); }
|
||||||
|
.cf-player { background: linear-gradient(to right, #0a3a1a, #27ae60); }
|
||||||
|
.cf-hptext { font-size: 9px; color: #888; }
|
||||||
|
.cf-status { font-size: 9px; color: #e67e22; margin-top: 2px; }
|
||||||
|
.cbt-actions { display: flex; flex-wrap: wrap; gap: 5px; padding: 5px 14px 8px; }
|
||||||
|
.cbt-btn {
|
||||||
|
background: rgba(20,12,20,0.9); border: 1px solid #3a2a3a; border-radius: 4px;
|
||||||
|
color: #ddd; padding: 5px 10px; cursor: pointer; font-size: 11px; transition: all .15s;
|
||||||
|
}
|
||||||
|
.cbt-btn:hover:not(.disabled) { transform: scale(1.06); }
|
||||||
|
.cbt-btn.b-atk { border-color: #8a2020; }
|
||||||
|
.cbt-btn.b-atk:hover { background: rgba(60,10,10,0.9); }
|
||||||
|
.cbt-btn.b-spl { border-color: #1a4a8a; }
|
||||||
|
.cbt-btn.b-spl:hover { background: rgba(10,25,60,0.9); }
|
||||||
|
.cbt-btn.b-itm { border-color: #1a6a2a; }
|
||||||
|
.cbt-btn.b-itm:hover { background: rgba(10,45,15,0.9); }
|
||||||
|
.cbt-btn.b-fle { border-color: #444; }
|
||||||
|
.cbt-btn.b-fle:hover { background: rgba(30,30,30,0.9); }
|
||||||
|
.cbt-btn.disabled { opacity: 0.35; cursor: default; pointer-events: none; }
|
||||||
|
.cbt-btn:active:not(.disabled) { transform: scale(0.93) !important; filter: brightness(1.4); }
|
||||||
|
.cbt-log { padding: 4px 14px 8px; font-size: 11px; min-height: 80px; border-top: 1px solid #1a0f1a; overflow-y: auto; max-height: 90px; display: flex; flex-direction: column-reverse; }
|
||||||
|
.cbt-log div { margin-bottom: 1px; line-height: 1.35; }
|
||||||
|
|
||||||
|
/* ───── ПОРТРЕТ В БОЮ ───── */
|
||||||
|
.portrait-canvas { display:block; margin:0 auto 4px; border:1px solid #2a2a4a; border-radius:4px; background:#08080f; }
|
||||||
|
|
||||||
|
/* ───── СКИЛЛ-ПАНЕЛЬ (левел-ап) ───── */
|
||||||
|
#skill-panel { top: 50%; left: 50%; transform: translate(-50%,-50%); width: 480px; z-index: 100; }
|
||||||
|
.sk-intro { padding: 8px 14px; color: #999; font-size: 11px; border-bottom: 1px solid #1e1e38; }
|
||||||
|
.sk-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 12px; }
|
||||||
|
.sk-card {
|
||||||
|
background: rgba(15,15,35,0.9); border: 2px solid #2a2a5a; border-radius: 7px;
|
||||||
|
padding: 9px; cursor: pointer; text-align: center; transition: all .18s;
|
||||||
|
}
|
||||||
|
.sk-card:hover { border-color: #ffd700; transform: scale(1.04); box-shadow: 0 0 12px rgba(255,215,0,.2); }
|
||||||
|
.sk-icon { font-size: 22px; margin-bottom: 4px; }
|
||||||
|
.sk-name { font-size: 12px; font-weight: bold; color: #ffd700; }
|
||||||
|
.sk-desc { font-size: 9px; color: #888; margin-top: 3px; line-height: 1.4; }
|
||||||
|
|
||||||
|
/* ───── ДЕРЕВО ПЕРКОВ ───── */
|
||||||
|
#perk-panel { top: 22px; left: 50%; transform: translateX(-50%); width: 840px; max-height: 580px; }
|
||||||
|
#perk-branches { display: grid; grid-template-columns: repeat(4,1fr); gap: 0; padding: 10px 8px; overflow-y: auto; max-height: 490px; }
|
||||||
|
.perk-branch { border-right: 1px solid #1a1a30; padding: 0 8px; }
|
||||||
|
.perk-branch:last-child { border-right: none; }
|
||||||
|
.perk-branch-title { font-size: 10px; color: #666; text-transform: uppercase; letter-spacing: 1px; text-align: center; padding: 4px 0 8px; border-bottom: 1px solid #1a1a30; margin-bottom: 8px; }
|
||||||
|
.perk-tier-line { width: 2px; height: 12px; background: #1e1e38; margin: 0 auto; }
|
||||||
|
.perk-card { background: rgba(12,12,30,0.9); border: 2px solid #1e1e38; border-radius: 6px; padding: 7px 6px; margin-bottom: 4px; cursor: pointer; text-align: center; transition: all .15s; position: relative; }
|
||||||
|
.perk-card:hover:not(.locked) { border-color: #ffd700; transform: scale(1.04); }
|
||||||
|
.perk-card.learned { border-color: #27ae60; background: rgba(8,35,18,0.9); }
|
||||||
|
.perk-card.available { border-color: #3a3a7a; }
|
||||||
|
.perk-card.locked { opacity: 0.38; cursor: not-allowed; }
|
||||||
|
.perk-icon { font-size: 17px; margin-bottom: 2px; }
|
||||||
|
.perk-name { font-size: 9px; font-weight: bold; color: #ffd700; line-height: 1.3; }
|
||||||
|
.perk-desc { font-size: 8px; color: #666; margin-top: 2px; line-height: 1.3; }
|
||||||
|
.perk-tier-badge { position: absolute; top: 2px; right: 3px; font-size: 7px; color: #444; }
|
||||||
|
|
||||||
|
/* ───── КРАФТИНГ ───── */
|
||||||
|
#craft-panel { top: 30px; left: 50%; transform: translateX(-50%); width: 660px; }
|
||||||
|
.craft-tabs { display: flex; flex-wrap: wrap; gap: 4px; padding: 7px 10px; border-bottom: 1px solid #1e1e38; }
|
||||||
|
.craft-tab { background: rgba(12,12,28,0.8); border: 1px solid #2a2a4a; border-radius: 4px; color: #777; padding: 4px 10px; font-size: 10px; cursor: pointer; transition: all .15s; }
|
||||||
|
.craft-tab.active { background: rgba(38,38,78,0.9); border-color: #ffd700; color: #ffd700; }
|
||||||
|
.craft-tab:hover:not(.active) { border-color: #4a4a7a; color: #aaa; }
|
||||||
|
.craft-recipe-btn { display: block; width: 100%; text-align: left; background: rgba(10,10,25,0.8); border: 1px solid #1a1a35; border-radius: 4px; color: #bbb; padding: 5px 9px; font-size: 11px; cursor: pointer; margin-bottom: 3px; transition: all .15s; }
|
||||||
|
.craft-recipe-btn:hover { border-color: #3a3a7a; color: #eee; }
|
||||||
|
.craft-recipe-btn.can-craft { border-color: #27ae60; color: #6f6; }
|
||||||
|
.craft-recipe-btn.selected { border-color: #ffd700; background: rgba(28,28,58,0.9); }
|
||||||
|
.craft-ing { display: flex; justify-content: space-between; align-items: center; padding: 3px 0; border-bottom: 1px solid #0a0a18; font-size: 10px; }
|
||||||
|
.craft-ing.have { color: #4f4; }
|
||||||
|
.craft-ing.missing { color: #f44; }
|
||||||
|
|
||||||
|
/* ───── БЕСТИАРИЙ ───── */
|
||||||
|
#bestiary-panel { top: 22px; left: 50%; transform: translateX(-50%); width: 820px; max-height: 575px; }
|
||||||
|
#bestiary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 10px; overflow-y: auto; max-height: 495px; }
|
||||||
|
.beast-card { background: rgba(12,12,28,0.9); border: 2px solid #1e1e38; border-radius: 7px; padding: 8px; text-align: center; transition: border-color .15s; }
|
||||||
|
.beast-card.seen { border-color: #2a3a6a; cursor: default; }
|
||||||
|
.beast-card.seen:hover { border-color: #ffd700; }
|
||||||
|
.beast-card.unseen { opacity: 0.35; filter: grayscale(1); }
|
||||||
|
.beast-canvas { display:block; margin:0 auto 4px; border:1px solid #1a1a35; border-radius:3px; background:#06060e; }
|
||||||
|
.beast-name { font-size: 11px; font-weight: bold; color: #ffd700; }
|
||||||
|
.beast-lore { font-size: 8px; color: #666; margin: 3px 0; line-height: 1.4; text-align: left; }
|
||||||
|
.beast-kills { font-size: 9px; color: #4f4; margin-top: 2px; }
|
||||||
|
.beast-weak { font-size: 8px; color: #e67e22; margin-top: 1px; }
|
||||||
|
.beast-resist{ font-size: 8px; color: #3498db; }
|
||||||
|
|
||||||
|
/* ───── КНОПКА MUTE ───── */
|
||||||
|
#btn-mute {
|
||||||
|
background: rgba(15,15,30,0.85); border: 1px solid #2a2a4a;
|
||||||
|
border-radius: 8px; padding: 2px 6px; font-size: 13px;
|
||||||
|
cursor: pointer; pointer-events: all; user-select: none;
|
||||||
|
}
|
||||||
|
#btn-mute:hover { border-color: #ffd700; }
|
||||||
|
|
||||||
|
/* ───── СООБЩЕНИЯ ───── */
|
||||||
|
#msg-overlay { position: absolute; top: 54px; left: 50%; transform: translateX(-50%); pointer-events: none; z-index: 30; min-width: 200px; text-align: center; }
|
||||||
|
.msg-pop {
|
||||||
|
background: rgba(8,8,18,0.88); border: 1px solid #2a2a4a;
|
||||||
|
border-radius: 16px; padding: 4px 14px; font-size: 12px; color: #fff;
|
||||||
|
margin-bottom: 3px; animation: msgFade 2.8s forwards; display: inline-block;
|
||||||
|
}
|
||||||
|
@keyframes msgFade {
|
||||||
|
0% { opacity: 0; transform: translateY(8px); }
|
||||||
|
12% { opacity: 1; transform: translateY(0); }
|
||||||
|
72% { opacity: 1; }
|
||||||
|
100% { opacity: 0; transform: translateY(-12px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── ТОСТ ДОСТИЖЕНИЯ ───── */
|
||||||
|
#ach-toast {
|
||||||
|
position: absolute; top: 58px; right: 8px;
|
||||||
|
background: rgba(255,215,0,.12); border: 2px solid #ffd700;
|
||||||
|
border-radius: 7px; padding: 7px 11px; font-size: 11px; color: #ffd700;
|
||||||
|
z-index: 60; display: none; max-width: 190px;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
#ach-toast.show {
|
||||||
|
display: block;
|
||||||
|
animation: achToastIn .3s ease;
|
||||||
|
}
|
||||||
|
@keyframes achToastIn {
|
||||||
|
from { opacity: 0; transform: translateX(20px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── СПЛЭШ ЭКРАН ───── */
|
||||||
|
#splash-screen {
|
||||||
|
position: absolute; inset: 0; z-index: 300; overflow: hidden;
|
||||||
|
background: #02020a;
|
||||||
|
transition: opacity 0.75s ease;
|
||||||
|
}
|
||||||
|
#splash-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||||
|
.splash-content {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.splash-eyeline {
|
||||||
|
font-size: 11px; color: #6644aa; letter-spacing: 8px;
|
||||||
|
text-transform: uppercase; margin-bottom: 16px;
|
||||||
|
opacity: 0; animation: splashFadeUp 1.2s 0.6s ease forwards;
|
||||||
|
}
|
||||||
|
.splash-title {
|
||||||
|
font-size: 68px; font-weight: 900; line-height: 1.05;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
color: #ffd700; text-align: center; letter-spacing: 3px;
|
||||||
|
text-shadow: 0 0 60px rgba(255,215,0,0.85), 0 0 130px rgba(255,140,0,0.45), 0 3px 6px rgba(0,0,0,0.95);
|
||||||
|
opacity: 0; animation: splashTitleIn 1.6s 0.9s ease forwards;
|
||||||
|
}
|
||||||
|
.splash-subtitle {
|
||||||
|
font-size: 11px; color: #4433aa; letter-spacing: 5px;
|
||||||
|
text-transform: uppercase; margin-top: 12px; margin-bottom: 58px;
|
||||||
|
opacity: 0; animation: splashFadeUp 1.2s 1.4s ease forwards;
|
||||||
|
}
|
||||||
|
.splash-btn {
|
||||||
|
pointer-events: auto;
|
||||||
|
background: linear-gradient(160deg, #100828 0%, #1c0948 50%, #100828 100%);
|
||||||
|
border: 2px solid #6644aa; border-radius: 8px;
|
||||||
|
color: #c8aaee; padding: 15px 48px;
|
||||||
|
font-size: 14px; letter-spacing: 3px; text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0; animation: splashFadeUp 1.2s 1.9s ease forwards, splashPulse 2.8s 3.5s ease-in-out infinite;
|
||||||
|
transition: background 0.25s, border-color 0.25s, color 0.25s, box-shadow 0.25s;
|
||||||
|
}
|
||||||
|
.splash-btn:hover {
|
||||||
|
background: linear-gradient(160deg, #1c0948 0%, #2e1268 50%, #1c0948 100%);
|
||||||
|
border-color: #ffd700; color: #ffd700;
|
||||||
|
box-shadow: 0 0 50px rgba(255,215,0,0.22), inset 0 0 20px rgba(255,215,0,0.06);
|
||||||
|
}
|
||||||
|
.splash-hint {
|
||||||
|
font-size: 10px; color: #2a1840; letter-spacing: 3px; margin-top: 22px;
|
||||||
|
opacity: 0; animation: splashHintBlink 2.4s 3.2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes splashFadeUp {
|
||||||
|
from { opacity: 0; transform: translateY(14px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes splashTitleIn {
|
||||||
|
from { opacity: 0; transform: scale(0.9) translateY(10px); filter: blur(8px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); filter: blur(0); }
|
||||||
|
}
|
||||||
|
@keyframes splashPulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(100,60,180,0); }
|
||||||
|
50% { box-shadow: 0 0 24px 8px rgba(100,60,180,0.18); }
|
||||||
|
}
|
||||||
|
@keyframes splashHintBlink {
|
||||||
|
0%, 100% { opacity: 0.55; }
|
||||||
|
50% { opacity: 0.12; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── СТАРТОВЫЙ ЭКРАН ───── */
|
||||||
|
#start-screen {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
z-index: 200; overflow: hidden;
|
||||||
|
}
|
||||||
|
#menu-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||||
|
.menu-view { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; width: 100%; }
|
||||||
|
|
||||||
|
.s-title {
|
||||||
|
font-size: 36px; font-weight: bold; color: #ffd700;
|
||||||
|
text-shadow: 0 0 40px rgba(255,215,0,.6), 0 0 80px rgba(255,215,0,.2);
|
||||||
|
margin-bottom: 4px; letter-spacing: 2px;
|
||||||
|
animation: titlePulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes titlePulse {
|
||||||
|
0%,100% { text-shadow: 0 0 30px rgba(255,215,0,.5), 0 0 60px rgba(255,215,0,.15); }
|
||||||
|
50% { text-shadow: 0 0 50px rgba(255,215,0,.9), 0 0 100px rgba(255,215,0,.3); }
|
||||||
|
}
|
||||||
|
.s-subtitle { font-size: 12px; color: #556; letter-spacing: 3px; text-transform: uppercase; margin-bottom: 26px; }
|
||||||
|
|
||||||
|
/* Слоты */
|
||||||
|
.s-slots { display: flex; gap: 14px; margin-bottom: 18px; }
|
||||||
|
.slot-card {
|
||||||
|
width: 220px; min-height: 130px;
|
||||||
|
background: rgba(8,8,20,0.85); border: 2px solid #1e1e3a; border-radius: 10px;
|
||||||
|
padding: 14px 12px; cursor: pointer; transition: all .22s; text-align: center;
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.slot-card:hover { border-color: #ffd700; transform: translateY(-4px); box-shadow: 0 8px 28px rgba(255,215,0,.18); }
|
||||||
|
.slot-card.empty { border-style: dashed; opacity: 0.55; }
|
||||||
|
.slot-card.empty:hover { opacity: 0.9; }
|
||||||
|
.sc-num { font-size: 9px; color: #444; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
||||||
|
.sc-icon { font-size: 30px; margin-bottom: 6px; }
|
||||||
|
.sc-class { font-size: 14px; font-weight: bold; color: #ffd700; }
|
||||||
|
.sc-info { font-size: 10px; color: #778; margin-top: 4px; line-height: 1.7; }
|
||||||
|
.sc-date { font-size: 9px; color: #445; margin-top: 6px; }
|
||||||
|
.sc-del { font-size: 9px; color: #c44; cursor: pointer; margin-top: 8px; padding: 2px 8px; border: 1px solid #622; border-radius: 3px; transition: all .15s; }
|
||||||
|
.sc-del:hover { background: rgba(200,50,50,.18); color: #f66; border-color: #f44; }
|
||||||
|
.sc-play { font-size: 11px; color: #4f4; border: 1px solid #282; border-radius: 4px; padding: 4px 14px; margin-top: 8px; transition: all .15s; }
|
||||||
|
.sc-play:hover { background: rgba(50,200,50,.15); }
|
||||||
|
.s-new-btn {
|
||||||
|
background: rgba(18,18,42,0.88); border: 2px solid #3a3a6a; border-radius: 6px;
|
||||||
|
color: #aab; padding: 9px 32px; cursor: pointer; font-size: 13px; transition: all .22s; letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.s-new-btn:hover { border-color: #ffd700; color: #ffd700; transform: scale(1.05); }
|
||||||
|
|
||||||
|
/* Выбор класса */
|
||||||
|
.cls-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 14px; max-width: 840px; }
|
||||||
|
.cls-btn {
|
||||||
|
background: linear-gradient(135deg, #0f0f22, #080813);
|
||||||
|
border: 2px solid #2a2a4a; border-radius: 8px;
|
||||||
|
color: #e0e0e0; padding: 10px 8px; cursor: pointer; transition: all .22s; text-align: center;
|
||||||
|
}
|
||||||
|
.cls-btn:hover { transform: translateY(-3px); box-shadow: 0 4px 18px rgba(255,215,0,.22); border-color: #ffd700; }
|
||||||
|
.cb-icon { font-size: 26px; margin-bottom: 5px; }
|
||||||
|
.cb-name { font-size: 14px; font-weight: bold; color: #ffd700; }
|
||||||
|
.cb-desc { font-size: 9px; color: #666; margin-top: 3px; line-height: 1.35; }
|
||||||
|
.cb-stats { font-size: 9px; color: #888; margin-top: 4px; }
|
||||||
|
.s-back-btn { background: rgba(12,12,28,0.85); border: 1px solid #333; border-radius: 5px; color: #778; padding: 5px 14px; cursor: pointer; font-size: 11px; transition: all .15s; }
|
||||||
|
.s-back-btn:hover { border-color: #888; color: #ddd; }
|
||||||
|
.s-folder-btn { background: rgba(12,12,28,0.85); border: 1px solid #2a3a5a; border-radius: 5px; color: #6688aa; padding: 5px 14px; cursor: pointer; font-size: 11px; transition: all .15s; }
|
||||||
|
.s-folder-btn:hover { border-color: #4488cc; color: #88bbee; }
|
||||||
|
.cls-slot-hint { font-size: 11px; color: #556; margin: 0 0 10px; letter-spacing: 1px; }
|
||||||
|
|
||||||
|
/* ───── ИНДИКАТОР СОХРАНЕНИЯ ───── */
|
||||||
|
#save-ind {
|
||||||
|
position: absolute; bottom: 52px; right: 10px;
|
||||||
|
background: rgba(10,40,15,0.92); border: 1px solid #27ae60;
|
||||||
|
border-radius: 6px; padding: 5px 10px; font-size: 10px; color: #4f4;
|
||||||
|
z-index: 25; opacity: 0; transition: opacity .35s; pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── ХИНТ ВЗАИМОДЕЙСТВИЯ ───── */
|
||||||
|
#interact-hint {
|
||||||
|
position: absolute; bottom: 70px; left: 50%; transform: translateX(-50%);
|
||||||
|
background: rgba(8, 8, 20, 0.88); border: 1px solid #3a3a7a;
|
||||||
|
border-radius: 8px; padding: 7px 18px;
|
||||||
|
font-size: 12px; color: #ccc; pointer-events: none;
|
||||||
|
opacity: 0; transition: opacity 0.22s ease, transform 0.22s ease;
|
||||||
|
white-space: nowrap; z-index: 50;
|
||||||
|
transform: translateX(-50%) translateY(6px);
|
||||||
|
box-shadow: 0 2px 12px rgba(60,0,120,0.25);
|
||||||
|
}
|
||||||
|
#interact-hint.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
.hint-key {
|
||||||
|
display: inline-block; background: #1e1e4a; border: 1px solid #6a6aaa;
|
||||||
|
border-radius: 4px; padding: 1px 8px; font-size: 11px; font-weight: bold;
|
||||||
|
color: #ffd700; margin-right: 7px; font-family: monospace;
|
||||||
|
box-shadow: 0 1px 3px rgba(100,0,200,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── СКРОЛЛБАР ───── */
|
||||||
|
::-webkit-scrollbar { width: 4px; }
|
||||||
|
::-webkit-scrollbar-track { background: #0a0a14; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 2px; }
|
||||||
|
|
||||||
|
/* ───── СЕТЫ ЭКИПИРОВКИ ───── */
|
||||||
|
.set-bonus-bar {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 5px;
|
||||||
|
margin: 6px 0 2px; padding: 5px 6px;
|
||||||
|
background: rgba(255,215,0,0.05); border: 1px solid rgba(255,215,0,0.18);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.set-bonus-card {
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
background: rgba(20,20,40,0.8); border: 1px solid rgba(255,215,0,0.3);
|
||||||
|
border-radius: 4px; padding: 3px 7px; font-size: 10px;
|
||||||
|
}
|
||||||
|
.sb-icon { font-size: 13px; }
|
||||||
|
.sb-name { color: #ffd700; font-weight: bold; margin-right: 3px; }
|
||||||
|
.sb-desc { color: #a8c; font-size: 9px; }
|
||||||
|
.enc-tag { font-size: 10px; margin-left: 3px; }
|
||||||
|
|
||||||
|
/* ───── ПАНЕЛЬ ЗАЧАРОВАНИЯ ───── */
|
||||||
|
#enchant-panel { top: 30px; left: 50%; transform: translateX(-50%); width: 620px; }
|
||||||
|
.enchant-body {
|
||||||
|
display: flex; height: 430px;
|
||||||
|
}
|
||||||
|
.enchant-left {
|
||||||
|
width: 200px; border-right: 1px solid #1e1e38;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.enchant-right {
|
||||||
|
flex: 1; overflow-y: auto; padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.enchant-col-title {
|
||||||
|
font-size: 9px; color: #555; letter-spacing: 1px; text-transform: uppercase;
|
||||||
|
padding: 6px 10px 4px; border-bottom: 1px solid #1a1a30;
|
||||||
|
}
|
||||||
|
#enchant-item-list {
|
||||||
|
overflow-y: auto; flex: 1; padding: 6px;
|
||||||
|
}
|
||||||
|
.enchant-item-btn {
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
padding: 6px 8px; border-radius: 5px; cursor: pointer;
|
||||||
|
font-size: 10px; color: #99a; margin-bottom: 3px;
|
||||||
|
border: 1px solid transparent; transition: all .15s;
|
||||||
|
}
|
||||||
|
.enchant-item-btn:hover { background: rgba(80,80,180,0.15); border-color: #3a3a6a; color: #ccd; }
|
||||||
|
.enchant-item-btn.active { background: rgba(100,80,200,0.22); border-color: #5a5aaa; color: #ddf; }
|
||||||
|
.eib-icon { font-size: 14px; }
|
||||||
|
.eib-name { flex: 1; }
|
||||||
|
.eib-ench { font-size: 12px; }
|
||||||
|
.eib-src { font-size: 9px; color: #445; }
|
||||||
|
|
||||||
|
.ench-item-header {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 7px 4px 8px; border-bottom: 1px solid #1e1e38; margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.eih-icon { font-size: 20px; }
|
||||||
|
.eih-name { font-size: 13px; color: #dde; font-weight: bold; flex: 1; }
|
||||||
|
.eih-cur { font-size: 10px; color: #a8c; background: rgba(150,80,200,0.15); padding: 2px 7px; border-radius: 10px; }
|
||||||
|
|
||||||
|
.enchant-card {
|
||||||
|
background: linear-gradient(135deg, #0f0f22, #080813);
|
||||||
|
border: 1px solid #2a2a4a; border-radius: 6px;
|
||||||
|
padding: 8px 10px; margin-bottom: 6px; cursor: pointer; transition: all .18s;
|
||||||
|
}
|
||||||
|
.enchant-card:hover:not(.disabled) { border-color: #8844cc; background: rgba(80,40,140,0.25); transform: translateX(2px); }
|
||||||
|
.enchant-card.disabled { opacity: 0.5; cursor: default; }
|
||||||
|
.enchant-card.current { border-color: #5580cc; background: rgba(40,60,140,0.25); }
|
||||||
|
.ec-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
||||||
|
.ec-icon { font-size: 16px; }
|
||||||
|
.ec-name { font-size: 11px; color: #ccaaff; font-weight: bold; flex: 1; }
|
||||||
|
.ec-desc { font-size: 10px; color: #88aadd; }
|
||||||
|
.ec-cost { font-size: 9px; color: #667; }
|
||||||
|
|
||||||
|
.ench-empty { color: #445; font-size: 11px; padding: 20px; text-align: center; }
|
||||||
|
|
||||||
|
/* ───── ЖУРНАЛ ЛОРА ───── */
|
||||||
|
#lore-panel { top: 30px; left: 50%; transform: translateX(-50%); width: 560px; }
|
||||||
|
#lore-list { max-height: 480px; overflow-y: auto; padding: 10px 12px; }
|
||||||
|
.lore-group { margin-bottom: 14px; }
|
||||||
|
.lore-group-title {
|
||||||
|
font-size: 10px; text-transform: uppercase; letter-spacing: 1px;
|
||||||
|
color: #ffd700; border-bottom: 1px solid #2a2a4a;
|
||||||
|
padding-bottom: 4px; margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
.lore-card {
|
||||||
|
background: rgba(10,10,22,0.9); border: 1px solid #2a2a4a;
|
||||||
|
border-radius: 6px; padding: 8px 10px; margin-bottom: 6px;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.lore-card:hover { border-color: #5566aa; }
|
||||||
|
.lc-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
|
||||||
|
.lc-icon { font-size: 16px; }
|
||||||
|
.lc-title { font-size: 12px; color: #ccccff; font-weight: bold; }
|
||||||
|
.lc-text { font-size: 11px; color: #889; line-height: 1.5; font-style: italic; }
|
||||||
|
.lc-hint { font-size: 11px; color: #ffdd44; margin-top: 5px; padding: 4px 6px;
|
||||||
|
background: rgba(255,221,68,0.08); border-left: 2px solid #ffdd44; border-radius: 3px; }
|
||||||
|
.lore-map-status { font-size: 11px; margin-top: 6px; padding: 3px 0; text-align: right; }
|
||||||
|
.lore-empty { color: #445; font-size: 12px; padding: 30px; text-align: center; line-height: 1.7; }
|
||||||
|
|
||||||
|
/* ── Boss HP Bar ─────────────────────────────────── */
|
||||||
|
#boss-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 580px;
|
||||||
|
z-index: 60;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: bossBarIn .4s ease;
|
||||||
|
}
|
||||||
|
@keyframes bossBarIn {
|
||||||
|
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||||
|
}
|
||||||
|
#boss-bar-name {
|
||||||
|
color: #ff6644;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
text-shadow: 0 0 10px #ff440088;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
#boss-bar-track {
|
||||||
|
height: 14px;
|
||||||
|
background: #110505;
|
||||||
|
border: 1px solid #660000;
|
||||||
|
border-radius: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 14px #cc110044, inset 0 0 6px #00000088;
|
||||||
|
}
|
||||||
|
#boss-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(90deg, #6b0000 0%, #cc2200 60%, #ff4400 100%);
|
||||||
|
border-radius: 7px;
|
||||||
|
transition: width .5s ease;
|
||||||
|
box-shadow: inset 0 1px 0 #ff660044;
|
||||||
|
}
|
||||||
|
#boss-bar-text {
|
||||||
|
color: #aa6655;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── АНИМАЦИЯ ПОРТРЕТОВ (дыхание + моргание + вспышка) ───── */
|
||||||
|
#portrait-player { animation: portrait-breathe 3.2s ease-in-out infinite; }
|
||||||
|
#portrait-enemy { animation: portrait-breathe 2.4s ease-in-out infinite; }
|
||||||
|
@keyframes portrait-flash {
|
||||||
|
0% { filter: brightness(1) saturate(1); }
|
||||||
|
25% { filter: brightness(3) saturate(0); }
|
||||||
|
100% { filter: brightness(1) saturate(1); }
|
||||||
|
}
|
||||||
|
.portrait-hit { animation: portrait-flash 0.28s ease forwards !important; }
|
||||||
|
@keyframes portrait-breathe {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-2px); }
|
||||||
|
}
|
||||||
|
.portrait-wrap { position: relative; display: inline-block; }
|
||||||
|
.portrait-blink {
|
||||||
|
position: absolute; bottom: 28px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 32px; height: 4px;
|
||||||
|
background: transparent; pointer-events: none; transition: background .05s;
|
||||||
|
}
|
||||||
|
.portrait-blink.blinking { background: #08080f; }
|
||||||
|
|
||||||
|
/* ───── DRAG & DROP ИНВЕНТАРЬ ───── */
|
||||||
|
.inv-slot[draggable="true"] { cursor: grab; }
|
||||||
|
.inv-slot.dragging { opacity: 0.35; cursor: grabbing; }
|
||||||
|
.eq-slot.drag-over, .inv-slot.drag-over {
|
||||||
|
border-color: #4488ff !important;
|
||||||
|
box-shadow: 0 0 8px #4488ff55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── ДОСТИЖЕНИЯ ───── */
|
||||||
|
#achiev-panel { top: 30px; left: 50%; transform: translateX(-50%); width: 500px; }
|
||||||
|
#achiev-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 6px; padding: 10px; max-height: 400px; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.ach-card {
|
||||||
|
background: #0d0d1a; border: 1px solid #1a1a2a;
|
||||||
|
border-radius: 6px; padding: 8px; text-align: center;
|
||||||
|
}
|
||||||
|
.ach-card.unlocked {
|
||||||
|
border-color: #ffd700; background: #1a1400;
|
||||||
|
box-shadow: 0 0 6px #ffd70033;
|
||||||
|
}
|
||||||
|
.ach-card.locked { opacity: 0.35; filter: grayscale(0.7); }
|
||||||
|
.ach-icon { font-size: 20px; display: block; margin-bottom: 3px; }
|
||||||
|
.ach-name { font-size: 10px; color: #ccccff; font-weight: bold; }
|
||||||
|
.ach-desc { font-size: 9px; color: #556; margin-top: 2px; line-height: 1.3; }
|
||||||
|
|
||||||
|
/* ───── ПРОГРЕСС ДОСТИЖЕНИЙ ───── */
|
||||||
|
.ach-progress { height: 3px; background: #1a1a2a; border-radius: 2px; margin: 5px 2px 1px; overflow: hidden; }
|
||||||
|
.ach-prog-fill { height: 100%; background: linear-gradient(to right, #3a3a8a, #6666cc); border-radius: 2px; transition: width .3s; }
|
||||||
|
.ach-prog-text { font-size: 8px; color: #445; text-align: center; margin-top: 1px; }
|
||||||
|
|
||||||
|
/* ───── ЭКИПИРОВАННЫЕ ПРЕДМЕТЫ ───── */
|
||||||
|
.inv-slot.equipped { border-color: #ffd700 !important; box-shadow: 0 0 8px #ffd70044; }
|
||||||
|
.inv-slot.equipped::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute; top: 2px; right: 4px;
|
||||||
|
color: #ffd700; font-size: 12px; font-weight: bold;
|
||||||
|
text-shadow: 0 0 4px #ffd700;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── МАГАЗИН — недоступные предметы ───── */
|
||||||
|
.shop-item { position: relative; }
|
||||||
|
.shop-item.cant-afford { opacity: 0.40; cursor: not-allowed; pointer-events: none; }
|
||||||
|
.shop-item.cant-afford .si-price { color: #e74c3c; }
|
||||||
|
|
||||||
|
/* ───── КАРТА МИРА ───── */
|
||||||
|
#worldmap-panel { top: 50%; left: 50%; transform: translate(-50%, -50%); width: 520px; }
|
||||||
|
|
||||||
|
/* ───── SVG-ЛИНИИ ПЕРКОВ ───── */
|
||||||
|
.perk-tier-line { display: none; }
|
||||||
|
.perk-svg-line { display: block; margin: 2px auto; }
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════
|
||||||
|
МЕНЮ ПАУЗЫ
|
||||||
|
════════════════════════════════════════════ */
|
||||||
|
.pause-overlay {
|
||||||
|
position: fixed !important; top: 0 !important; left: 0 !important;
|
||||||
|
width: 100vw !important; height: 100vh !important; transform: none !important;
|
||||||
|
background: rgba(0,0,0,0.75);
|
||||||
|
display: flex !important; align-items: center; justify-content: center; z-index: 9999;
|
||||||
|
}
|
||||||
|
.pause-box {
|
||||||
|
background: #0d0d1e; border: 1px solid #2a2a6a; border-radius: 10px;
|
||||||
|
padding: 32px 44px; display: flex; flex-direction: column; align-items: center; gap: 14px;
|
||||||
|
min-width: 270px; box-shadow: 0 0 50px rgba(80,0,180,0.45);
|
||||||
|
}
|
||||||
|
.pause-title {
|
||||||
|
font-size: 22px; color: #ffd700; letter-spacing: 5px; font-weight: bold;
|
||||||
|
margin-bottom: 8px; text-shadow: 0 0 12px #ffd70066;
|
||||||
|
}
|
||||||
|
.pause-btn {
|
||||||
|
width: 100%; padding: 9px 0; background: #14143a; border: 1px solid #3a3a7a;
|
||||||
|
border-radius: 6px; color: #ccc; font-size: 13px; cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.pause-btn:hover { background: #1e1e5a; color: #fff; border-color: #6644cc; }
|
||||||
|
.pause-exit { border-color: #6a1a1a !important; color: #e88; margin-top: 4px; }
|
||||||
|
.pause-exit:hover { background: #2a0a0a !important; color: #faa; border-color: #cc4444 !important; }
|
||||||
|
.pause-vol { width: 100%; display: flex; flex-direction: column; gap: 6px; align-items: center; }
|
||||||
|
.pause-vol label { font-size: 11px; color: #778; }
|
||||||
|
.pause-vol input[type=range] { width: 90%; accent-color: #6644cc; cursor: pointer; }
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════
|
||||||
|
ВКЛАДКИ ИНВЕНТАРЯ
|
||||||
|
════════════════════════════════════════════ */
|
||||||
|
.inv-tabs { display:flex; border-bottom:1px solid #1e1e38; flex-shrink:0; }
|
||||||
|
.inv-tab {
|
||||||
|
flex:1; padding:7px 4px; background:none; border:none;
|
||||||
|
border-bottom:2px solid transparent; color:#556; font-size:11px; cursor:pointer;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.inv-tab:hover { color:#aaa; }
|
||||||
|
.inv-tab.active { color:#ffd700; border-bottom-color:#ffd700; }
|
||||||
|
.inv-tab-pane { display:none; }
|
||||||
|
.inv-tab-pane.active { display:block; }
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════
|
||||||
|
БУМАЖНАЯ КУКЛА (PAPER DOLL)
|
||||||
|
════════════════════════════════════════════ */
|
||||||
|
.paperdoll {
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
gap:8px; padding:10px 8px 4px;
|
||||||
|
}
|
||||||
|
.pd-col { display:flex; flex-direction:column; gap:6px; }
|
||||||
|
.pd-portrait { border-radius:6px; border:1px solid #2a2a5a; background:#06060e; display:block; }
|
||||||
|
.pd-slot {
|
||||||
|
width:64px; min-height:64px; background:#0e0e22; border:1px solid #2a2a4a;
|
||||||
|
border-radius:6px; display:flex; flex-direction:column;
|
||||||
|
align-items:center; justify-content:center; font-size:10px; color:#445; cursor:pointer;
|
||||||
|
text-align:center; padding:4px; transition: border-color 0.15s, background 0.15s;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
.pd-slot:hover { border-color:#4a4a8a; background:#141430; }
|
||||||
|
.pd-slot.filled { border-color:#3a5a8a; background:#0e1a2a; color:#ccc; }
|
||||||
|
.pd-slot.filled:hover { border-color:#6699ff; background:#101e32; }
|
||||||
|
.pd-slot.drag-over { border-color:#ffd700; background:#1a1808; }
|
||||||
|
.pd-slot small { font-size:8px; color:#334; display:block; margin-top:1px; }
|
||||||
|
.pd-item-icon { font-size:18px; line-height:1.1; }
|
||||||
|
.pd-item-name { font-size:9px; color:#99aacc; margin-top:2px; max-width:60px;
|
||||||
|
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
.pd-item-stat { font-size:8px; color:#667; margin-top:1px; }
|
||||||
|
.pd-acc-row { display:flex; justify-content:center; padding:0 8px 8px; }
|
||||||
|
.pd-slot-wide { width:180px; min-height:40px; flex-direction:row; gap:8px; min-width:0; }
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════
|
||||||
|
ДЕТАЛЬНЫЕ СТАТЫ
|
||||||
|
════════════════════════════════════════════ */
|
||||||
|
#inv-stats-detail { padding:10px 12px; font-size:11px; overflow-y:auto; max-height:360px; }
|
||||||
|
.stat-group { margin-bottom:12px; }
|
||||||
|
.stat-group-title {
|
||||||
|
color:#ffd700; font-size:10px; letter-spacing:1px;
|
||||||
|
text-transform:uppercase; border-bottom:1px solid #1e1e38;
|
||||||
|
padding-bottom:3px; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.stat-row { display:flex; align-items:baseline; gap:6px; margin-bottom:4px; flex-wrap:wrap; }
|
||||||
|
.stat-label { color:#778; width:115px; flex-shrink:0; }
|
||||||
|
.stat-value { color:#ddd; font-weight:bold; min-width:32px; }
|
||||||
|
.stat-breakdown { color:#445; font-size:9px; flex:1; min-width:0; }
|
||||||
|
.stat-breakdown b { color:#667; }
|
||||||
|
.stat-bar { height:3px; background:#1a1a2a; border-radius:2px; margin-top:2px; margin-bottom:5px; }
|
||||||
|
.stat-bar-fill { height:100%; background:linear-gradient(to right,#3a3a8a,#6644cc); border-radius:2px; transition:width .3s; }
|
||||||
Reference in New Issue
Block a user