feat(integrations): add retro doom web engine and agy proxy nginx config
This commit is contained in:
1 parent
b8db5a123d
commit
50eadb4b5e
15 files changed
+6397
No files matched your search
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
# 📜 План разработки проекта DOOM 3D (Full 3D WebGL)
|
||||||
|
|
||||||
|
**Цель:** Создать полноценный браузерный 3D ретро-шутер в стиле DOOM / Quake на базе **Three.js** (с поддержкой полной трехмерности: многоуровневые карты, перепады высот, лестницы, прыжки, объемные комнаты, освещение и физика коллизий) с аутентичным 2D ретро-HUD.
|
||||||
|
|
||||||
|
Доступ по URL: [ai.qpid.pro/doom/](https://ai.qpid.pro/doom/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Стек технологий
|
||||||
|
- **3D Графика & Рендеринг:** Three.js (WebGL / WebGPU) — полноценный 3D пайплайн, шейдеры, динамический свет, тени, партиклы.
|
||||||
|
- **Физика и Коллизии:** Octree / Capsule Collider (быстрые коллизии для FPS со сложной геометрией, ступенями, склонами и прыжками) или Rapier3D.
|
||||||
|
- **2D HUD / UI:** 2D Canvas / DOM Overlay в аутентичном стиле Doom (панель состояния, анимированное лицо Doomguy, прицел, всплывающие сообщения).
|
||||||
|
- **Звук:** Web Audio API (пространственный 3D позиционный звук, ретро-SFX, саундтрек).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Этапы разработки (Roadmap)
|
||||||
|
|
||||||
|
### 🔹 Этап 1: 3D Движок и Базовая физика перемещения
|
||||||
|
- [ ] Инициализация Three.js сцены, камеры от первого лица (FPS Camera) и рендерера с поддержкой адаптивного разрешения и пикселизации (ретро-эффект).
|
||||||
|
- [ ] Захват курсора (Pointer Lock API) и плавный обзор мышью с настраиваемой чувствительностью.
|
||||||
|
- [ ] Физический контроллер игрока (FPS Controller):
|
||||||
|
- Перемещение WASD (ходьба, бег с зажатым Shift)
|
||||||
|
- Прыжки, гравитация, падение
|
||||||
|
- Подъем по ступеням и пандусам (step-climbing)
|
||||||
|
- Collision Detection на базе Octree / Capsule против сложной 3D геометрии.
|
||||||
|
- [ ] Мобильное управление: виртуальные стики перемещения и обзора + кнопки стрельбы и прыжка.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 2: 3D Уровни и Геометрия (Многоэтажность и Архитектура)
|
||||||
|
- [ ] Модульный 3D генератор / загрузчик уровней:
|
||||||
|
- Многоуровневые здания, мосты, тоннели, шахты лифтов, балконы и открытые арены.
|
||||||
|
- Поддержка форматов GLTF / OBJ / кастомных воксельных и блочных карт.
|
||||||
|
- [ ] Освещение и атмосфера:
|
||||||
|
- Динамические точечные источники света (факелы, мигающие лампы, лава).
|
||||||
|
- Ambient Occlusion, туман (Fog) и затемнение глубины.
|
||||||
|
- [ ] Интерактивные 3D объекты:
|
||||||
|
- Раздвижные и подъемные двери с коллизиями.
|
||||||
|
- Настенные переключатели и рубильники.
|
||||||
|
- Платформы и движущиеся лифты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 3: Оружие от 1-го лица и Боевая система
|
||||||
|
- [ ] Рендеринг оружия в руках (3D модели / анимированные спрайты с эффектом присутствия):
|
||||||
|
- Кулак / Бензопила
|
||||||
|
- Пистолет (Pistol)
|
||||||
|
- Дробовик (Shotgun / Super Shotgun)
|
||||||
|
- Скорострельный пулемет (Chaingun)
|
||||||
|
- Ракетница (Rocket Launcher)
|
||||||
|
- Плазменная пушка (Plasma Gun / BFG 9000).
|
||||||
|
- [ ] Визуальные и физические эффекты стрельбы:
|
||||||
|
- Отдача камеры, покачивание оружия при ходьбе (Weapon Bobbing).
|
||||||
|
- Вспышки дула (Muzzle Flash) с динамическим освещением комнат.
|
||||||
|
- Hitscan лучи с трассерами и искрами/следами от пуль на стенах (Bullet Decals).
|
||||||
|
- Физические 3D снаряды (ракеты, плазменные шары) с частицами дыма и взрывами с радиусом поражения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 4: 3D Враги и ИИ (AI)
|
||||||
|
- [ ] Модели / 3D-биллборды классических монстров (Зомби-стрелки, Импы, Демоны, Какодемоны).
|
||||||
|
- [ ] Пространственный 3D ИИ с навигацией (Raycast Line of Sight, NavMesh / 3D Waypoints):
|
||||||
|
- Поиск пути с учетом высот и лестниц.
|
||||||
|
- Состояния: Патруль $\rightarrow$ Обнаружение $\rightarrow$ Преследование $\rightarrow$ Атака (дальняя/ближняя) $\rightarrow$ Уклонение $\rightarrow$ Смерть.
|
||||||
|
- [ ] 3D Ragdoll / физика отлетающих частиц и анимации гибели монстров.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 5: Предметы, Лут и Интерактивность
|
||||||
|
- [ ] 3D вращающиеся предметы с мягким свечением:
|
||||||
|
- Аптечки и стимуляторы (Health).
|
||||||
|
- Броня и сферы бессмертия (Armor / Soulsphere).
|
||||||
|
- Коробки с патронами, гильзами и батареями.
|
||||||
|
- Цветные ключ-карты доступа (Red, Blue, Yellow).
|
||||||
|
- [ ] Взрывающиеся токсичные бочки (Explosive Barrels) с цепной детонацией.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 6: Аутентичный HUD и Интерфейс
|
||||||
|
- [ ] Классическая статус-панель в стиле DOOM:
|
||||||
|
- Значения здоровья (HEALTH), брони (ARMOR), патронов (AMMO).
|
||||||
|
- Анимированный аватар Doomguy (эмоции, ранения, оскал при стрельбе).
|
||||||
|
- Индикаторы собранных ключей доступа.
|
||||||
|
- [ ] Прицел, всплывающие уведомления о подборе предметов («Picked up a shotgun!») и экран получения урона (красная вспышка).
|
||||||
|
- [ ] Экран паузы, меню настроек (FOV, чувствительность, звук, ретро-фильтры).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 7: Пространственный Звук и Саундтрек
|
||||||
|
- [ ] Web Audio 3D Positional Audio (звук шагов и рычания монстров позиционируется в 3D пространстве).
|
||||||
|
- [ ] Звуки выстрелов, перезарядки, взрывов, открывания тяжелых железных дверей.
|
||||||
|
- [ ] Динамичный рок / метал саундтрек.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Этап 8: Оптимизация, Полировка и Деплой
|
||||||
|
- [ ] Стабильные 60+ FPS (Frustum Culling, Instanced Meshes для повторяющихся элементов).
|
||||||
|
- [ ] Шейдер пикселизации / CRT-монитора (опциональный ретро-фильтр).
|
||||||
|
- [ ] Проверка и тестирование по адресу `https://ai.qpid.pro/doom/`.
|
||||||
Executable
+483
@@ -0,0 +1,483 @@
|
|||||||
|
/* ===================================================
|
||||||
|
DOOM 3D - Authentic Retro CSS Stylesheet
|
||||||
|
CRT Scanlines, Retro Doom Status Bar, LED numbers,
|
||||||
|
Responsive Canvas, Mobile Controls & Modals.
|
||||||
|
=================================================== */
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Share+Tech+Mono&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #050203;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First Person Weapon Overlay Canvas (CS:GO / Right-Handed Viewmodel) */
|
||||||
|
#weapon-canvas {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 85px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 85px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screen Damage & Pickup Flash Tint */
|
||||||
|
#damage-flash {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
transition: background-color 0.05s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRT Monitor Scanline Filter */
|
||||||
|
#crt-scanlines {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 25;
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.45) 50%);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center Crosshair */
|
||||||
|
#crosshair {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
#crosshair::before, #crosshair::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background: #00ff66;
|
||||||
|
box-shadow: 0 0 4px #00ff66;
|
||||||
|
}
|
||||||
|
#crosshair::before {
|
||||||
|
top: 8px;
|
||||||
|
left: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
#crosshair::after {
|
||||||
|
top: 0;
|
||||||
|
left: 8px;
|
||||||
|
width: 2px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Notification Ticker */
|
||||||
|
#hud-ticker {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ffff44;
|
||||||
|
text-shadow: 2px 2px #b00, -2px -2px #000;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 30;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid #880000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================
|
||||||
|
AUTHENTIC RETRO DOOM STATUS BAR (BOTTOM HUD)
|
||||||
|
=================================================== */
|
||||||
|
|
||||||
|
#doom-hud {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 90px;
|
||||||
|
background: #2b2b2b;
|
||||||
|
border-top: 4px solid #5a5a5a;
|
||||||
|
box-shadow: inset 0 3px 0 #8c8c8c, 0 -4px 10px rgba(0,0,0,0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 30;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #1f1f1f 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #1f1f1f 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #1f1f1f 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #1f1f1f 75%);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-panel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 78px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 3px solid #404040;
|
||||||
|
box-shadow: inset 2px 2px 0 #000, inset -2px -2px 0 #555;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-stat-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 90px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-right: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #aa2222;
|
||||||
|
text-shadow: 1px 1px #000;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-value {
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff2200;
|
||||||
|
text-shadow: 0 0 8px #ff0000, 2px 2px #500;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Doomguy Animated Face Box */
|
||||||
|
.hud-face-box {
|
||||||
|
width: 76px;
|
||||||
|
height: 74px;
|
||||||
|
background: #111;
|
||||||
|
border: 2px solid #555;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
#doomguy-face {
|
||||||
|
width: 64px;
|
||||||
|
height: 68px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ARMS Matrix (1-7) */
|
||||||
|
.hud-arms-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-right: 2px solid #333;
|
||||||
|
}
|
||||||
|
.arms-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 16px);
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.arm-num {
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
.arm-num.active {
|
||||||
|
color: #ffff00;
|
||||||
|
text-shadow: 0 0 4px #ffaa00;
|
||||||
|
}
|
||||||
|
.arm-num.selected {
|
||||||
|
color: #00ff66;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keys Matrix */
|
||||||
|
.hud-keys-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.keys-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.key-slot {
|
||||||
|
width: 14px;
|
||||||
|
height: 18px;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
#key-blue.has-key {
|
||||||
|
background: #0088ff;
|
||||||
|
box-shadow: 0 0 8px #0088ff;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
#key-red.has-key {
|
||||||
|
background: #ff1122;
|
||||||
|
box-shadow: 0 0 8px #ff1122;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
#key-yellow.has-key {
|
||||||
|
background: #ffbb00;
|
||||||
|
box-shadow: 0 0 8px #ffbb00;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================
|
||||||
|
MODALS, OVERLAYS & MENUS
|
||||||
|
=================================================== */
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #1c1515;
|
||||||
|
border: 4px solid #8b0000;
|
||||||
|
box-shadow: 0 0 25px rgba(255, 0, 0, 0.4), inset 0 0 15px #000;
|
||||||
|
padding: 30px 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doom-title {
|
||||||
|
font-size: 34px;
|
||||||
|
color: #ff2200;
|
||||||
|
text-shadow: 4px 4px #4a0000, -2px -2px #000, 0 0 15px #ff5500;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doom-subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ffaa44;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-box {
|
||||||
|
background: #0d0808;
|
||||||
|
border: 2px solid #441111;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #bbb;
|
||||||
|
line-height: 1.8;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-box span {
|
||||||
|
color: #ffff44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-doom {
|
||||||
|
display: inline-block;
|
||||||
|
background: #8b0000;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 3px solid #ff4444;
|
||||||
|
padding: 14px 28px;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 4px 4px 0 #3a0000;
|
||||||
|
text-shadow: 2px 2px #000;
|
||||||
|
transition: transform 0.1s, background 0.1s;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-doom:hover {
|
||||||
|
background: #b30000;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-doom:active {
|
||||||
|
transform: translateY(2px);
|
||||||
|
box-shadow: 2px 2px 0 #3a0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Form */
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row input[type="range"] {
|
||||||
|
width: 160px;
|
||||||
|
accent-color: #ff2200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================
|
||||||
|
MOBILE VIRTUAL CONTROLS
|
||||||
|
=================================================== */
|
||||||
|
|
||||||
|
#mobile-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 90px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 28;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-controls.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Virtual Joystick */
|
||||||
|
#joystick-base {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 30px;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#joystick-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: rgba(255, 50, 50, 0.6);
|
||||||
|
border: 2px solid #ff4444;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Swipe Look Zone */
|
||||||
|
#mobile-look-zone {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 60%;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Action Buttons */
|
||||||
|
.mobile-action-buttons {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25px;
|
||||||
|
right: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mobile-act {
|
||||||
|
background: rgba(40, 40, 40, 0.7);
|
||||||
|
border: 2px solid #ff4444;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mobile-fire {
|
||||||
|
background: rgba(200, 0, 0, 0.85);
|
||||||
|
border-color: #ffbb00;
|
||||||
|
padding: 18px 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-weapon-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#doom-hud {
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
.hud-stat-box {
|
||||||
|
min-width: 65px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.hud-value {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.hud-face-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
#doomguy-face {
|
||||||
|
width: 50px;
|
||||||
|
height: 54px;
|
||||||
|
}
|
||||||
|
.doom-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+178
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>DOOM 3D - WebGL Retro FPS</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css?v=2">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game-container">
|
||||||
|
<!-- Center Aiming Reticle -->
|
||||||
|
<div id="crosshair"></div>
|
||||||
|
|
||||||
|
<!-- Dynamic Damage & Slime Screen Flash -->
|
||||||
|
<div id="damage-flash"></div>
|
||||||
|
|
||||||
|
<!-- Retro CRT Scanline Shader Overlay -->
|
||||||
|
<div id="crt-scanlines"></div>
|
||||||
|
|
||||||
|
<!-- HUD Message Ticker -->
|
||||||
|
<div id="hud-ticker"></div>
|
||||||
|
|
||||||
|
<!-- Authentic DOOM Status Bar (Bottom HUD) -->
|
||||||
|
<div id="doom-hud">
|
||||||
|
<div class="hud-panel">
|
||||||
|
<!-- AMMO -->
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<div class="hud-label">AMMO</div>
|
||||||
|
<div id="stat-ammo" class="hud-value">50</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HEALTH -->
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<div class="hud-label">HEALTH</div>
|
||||||
|
<div id="stat-health" class="hud-value">100%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ARMS MATRIX (1-7) -->
|
||||||
|
<div class="hud-arms-box">
|
||||||
|
<div class="hud-label">ARMS</div>
|
||||||
|
<div class="arms-grid">
|
||||||
|
<div id="arm-slot-1" class="arm-num active">1</div>
|
||||||
|
<div id="arm-slot-2" class="arm-num active selected">2</div>
|
||||||
|
<div id="arm-slot-3" class="arm-num active">3</div>
|
||||||
|
<div id="arm-slot-4" class="arm-num inactive">4</div>
|
||||||
|
<div id="arm-slot-5" class="arm-num inactive">5</div>
|
||||||
|
<div id="arm-slot-6" class="arm-num inactive">6</div>
|
||||||
|
<div id="arm-slot-7" class="arm-num inactive">7</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DOOMGUY ANIMATED FACE -->
|
||||||
|
<div class="hud-face-box">
|
||||||
|
<canvas id="doomguy-face"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ARMOR -->
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<div class="hud-label">ARMOR</div>
|
||||||
|
<div id="stat-armor" class="hud-value">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KEYS -->
|
||||||
|
<div class="hud-keys-box">
|
||||||
|
<div class="hud-label">KEYS</div>
|
||||||
|
<div class="keys-grid">
|
||||||
|
<div id="key-blue" class="key-slot" title="Blue Keycard"></div>
|
||||||
|
<div id="key-red" class="key-slot" title="Red Keycard"></div>
|
||||||
|
<div id="key-yellow" class="key-slot" title="Yellow Keycard"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Touch Virtual Controls -->
|
||||||
|
<div id="mobile-controls" class="hidden">
|
||||||
|
<div id="joystick-base">
|
||||||
|
<div id="joystick-knob"></div>
|
||||||
|
</div>
|
||||||
|
<div id="mobile-look-zone"></div>
|
||||||
|
<div class="mobile-action-buttons">
|
||||||
|
<div class="mobile-weapon-row">
|
||||||
|
<button id="btn-mobile-prev" class="btn-mobile-act">◀ WPN</button>
|
||||||
|
<button id="btn-mobile-next" class="btn-mobile-act">WPN ▶</button>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-weapon-row">
|
||||||
|
<button id="btn-mobile-use" class="btn-mobile-act">USE [E]</button>
|
||||||
|
<button id="btn-mobile-jump" class="btn-mobile-act">JUMP</button>
|
||||||
|
</div>
|
||||||
|
<button id="btn-mobile-fire" class="btn-mobile-act btn-mobile-fire">FIRE</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Start Game Screen Modal -->
|
||||||
|
<div id="start-screen" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h1 class="doom-title">DOOM 3D</h1>
|
||||||
|
<p class="doom-subtitle">FULL 3D WEBGL RETRO FPS</p>
|
||||||
|
|
||||||
|
<div class="instructions-box">
|
||||||
|
<p>🕹️ <span>W, A, S, D</span> : Move around</p>
|
||||||
|
<p>🖱️ <span>MOUSE</span> : Aim / Look around</p>
|
||||||
|
<p>💥 <span>LEFT CLICK</span> : Fire weapon</p>
|
||||||
|
<p>🚀 <span>SPACE</span> : Jump / Ascend stairs</p>
|
||||||
|
<p>⚡ <span>SHIFT</span> : Sprint run (2x speed)</p>
|
||||||
|
<p>🚪 <span>E</span> : Open doors & interact</p>
|
||||||
|
<p>🔫 <span>1 - 7</span> : Switch Weapons</p>
|
||||||
|
<p>⚙️ <span>ESC</span> : Pause / Settings</p>
|
||||||
|
<p>☠️ Cheats: Type <span>IDDQD</span>, <span>IDKFA</span>, <span>IDCLIP</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="btn-start-game" class="btn-doom">CLICK TO PLAY</button>
|
||||||
|
<button id="btn-settings" class="btn-doom" style="background:#444; border-color:#888;">SETTINGS</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pause Menu Modal -->
|
||||||
|
<div id="pause-menu" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h1 class="doom-title">PAUSED</h1>
|
||||||
|
<p class="doom-subtitle">MISSION IN PROGRESS</p>
|
||||||
|
<button id="btn-resume" class="btn-doom">RESUME GAME</button>
|
||||||
|
<button id="btn-restart" class="btn-doom">RESTART LEVEL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Over Modal -->
|
||||||
|
<div id="game-over-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h1 class="doom-title" style="color:#ff0000; text-shadow: 0 0 20px #ff0000;">YOU DIED</h1>
|
||||||
|
<p class="doom-subtitle">THE DEMONS OVERWHELMED YOU...</p>
|
||||||
|
<button id="btn-restart" class="btn-doom" onclick="window.game && window.game.restartGame()">RESPAWN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div id="settings-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2 class="doom-title" style="font-size:24px;">SETTINGS</h2>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>SFX VOLUME</span>
|
||||||
|
<input type="range" id="slider-sfx" min="0" max="1" step="0.05" value="0.8">
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>MUSIC VOLUME</span>
|
||||||
|
<input type="range" id="slider-music" min="0" max="1" step="0.05" value="0.5">
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>MOUSE SENSITIVITY</span>
|
||||||
|
<input type="range" id="slider-sens" min="0.5" max="3" step="0.1" value="1.0">
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>CRT SCANLINES</span>
|
||||||
|
<input type="checkbox" id="toggle-crt" checked style="width:20px; height:20px; accent-color:#ff2200;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="btn-close-settings" class="btn-doom" style="margin-top:20px;">BACK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Standalone Three.js r128 Library -->
|
||||||
|
<script src="js/lib/three.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Game Modules -->
|
||||||
|
<script src="js/audio.js?v=2"></script>
|
||||||
|
<script src="js/engine.js?v=2"></script>
|
||||||
|
<script src="js/player.js?v=2"></script>
|
||||||
|
<script src="js/weapons.js?v=2"></script>
|
||||||
|
<script src="js/map.js?v=2"></script>
|
||||||
|
<script src="js/enemies.js?v=2"></script>
|
||||||
|
<script src="js/items.js?v=2"></script>
|
||||||
|
<script src="js/hud.js?v=2"></script>
|
||||||
|
<script src="js/mobile.js?v=2"></script>
|
||||||
|
<script src="js/main.js?v=2"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Executable
+728
@@ -0,0 +1,728 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Retro Sound Synthesizer & Music Sequencer (Web Audio API)
|
||||||
|
* Generates authentic 90s DOOM-inspired sound effects and the iconic E1M1 metal riff.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SoundSystem {
|
||||||
|
constructor() {
|
||||||
|
this.ctx = null;
|
||||||
|
this.masterGain = null;
|
||||||
|
this.sfxGain = null;
|
||||||
|
this.musicGain = null;
|
||||||
|
this.initialized = false;
|
||||||
|
|
||||||
|
this.sfxVolume = 0.8;
|
||||||
|
this.musicVolume = 0.5;
|
||||||
|
this.masterVolume = 1.0;
|
||||||
|
|
||||||
|
this.musicPlaying = false;
|
||||||
|
this.musicStep = 0;
|
||||||
|
this.musicTimer = null;
|
||||||
|
this.musicBpm = 135;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
try {
|
||||||
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
|
this.ctx = new AudioContext();
|
||||||
|
|
||||||
|
this.masterGain = this.ctx.createGain();
|
||||||
|
this.masterGain.gain.value = this.masterVolume;
|
||||||
|
this.masterGain.connect(this.ctx.destination);
|
||||||
|
|
||||||
|
this.sfxGain = this.ctx.createGain();
|
||||||
|
this.sfxGain.gain.value = this.sfxVolume;
|
||||||
|
this.sfxGain.connect(this.masterGain);
|
||||||
|
|
||||||
|
this.musicGain = this.ctx.createGain();
|
||||||
|
this.musicGain.gain.value = this.musicVolume;
|
||||||
|
this.musicGain.connect(this.masterGain);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
this.startMusic();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Web Audio API not supported or blocked:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
if (this.ctx && this.ctx.state === 'suspended') {
|
||||||
|
this.ctx.resume();
|
||||||
|
}
|
||||||
|
if (!this.initialized) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMasterVolume(val) {
|
||||||
|
this.masterVolume = Math.max(0, Math.min(1, val));
|
||||||
|
if (this.masterGain) this.masterGain.gain.setValueAtTime(this.masterVolume, this.ctx.currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSfxVolume(val) {
|
||||||
|
this.sfxVolume = Math.max(0, Math.min(1, val));
|
||||||
|
if (this.sfxGain) this.sfxGain.gain.setValueAtTime(this.sfxVolume, this.ctx.currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMusicVolume(val) {
|
||||||
|
this.musicVolume = Math.max(0, Math.min(1, val));
|
||||||
|
if (this.musicGain) this.musicGain.gain.setValueAtTime(this.musicVolume, this.ctx.currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Create White Noise buffer
|
||||||
|
createNoiseBuffer(duration = 1) {
|
||||||
|
if (!this.ctx) return null;
|
||||||
|
const bufferSize = this.ctx.sampleRate * duration;
|
||||||
|
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||||||
|
const data = buffer.getChannelData(0);
|
||||||
|
for (let i = 0; i < bufferSize; i++) {
|
||||||
|
data[i] = Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distortion curve for heavy metal tone
|
||||||
|
makeDistortionCurve(amount = 50) {
|
||||||
|
const k = typeof amount === 'number' ? amount : 50;
|
||||||
|
const n_samples = 44100;
|
||||||
|
const curve = new Float32Array(n_samples);
|
||||||
|
const deg = Math.PI / 180;
|
||||||
|
for (let i = 0; i < n_samples; ++i) {
|
||||||
|
const x = (i * 2) / n_samples - 1;
|
||||||
|
curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
|
||||||
|
}
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// WEAPON SOUND EFFECTS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
playFist() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
// Swoosh
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(120, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(40, t + 0.15);
|
||||||
|
gain.gain.setValueAtTime(0.6, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.16);
|
||||||
|
|
||||||
|
// Impact punch
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.12);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'lowpass';
|
||||||
|
filter.frequency.setValueAtTime(800, t);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(100, t + 0.12);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(0.8, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playPistol() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
// Punch osc
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(320, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(45, t + 0.18);
|
||||||
|
oGain.gain.setValueAtTime(0.7, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.18);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.19);
|
||||||
|
|
||||||
|
// Noise crack
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.2);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(1800, t);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(300, t + 0.2);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(0.9, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playShotgun() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
// Low boom
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(200, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(30, t + 0.35);
|
||||||
|
oGain.gain.setValueAtTime(1.0, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.36);
|
||||||
|
|
||||||
|
// Heavy blast noise
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.35);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'lowpass';
|
||||||
|
filter.frequency.setValueAtTime(2800, t);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(400, t + 0.35);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(1.2, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
|
||||||
|
// Pump action slide (clack-clack) after 0.35s
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t2 = this.ctx.currentTime;
|
||||||
|
const p1 = this.ctx.createOscillator();
|
||||||
|
const pGain = this.ctx.createGain();
|
||||||
|
p1.type = 'square';
|
||||||
|
p1.frequency.setValueAtTime(400, t2);
|
||||||
|
p1.frequency.setValueAtTime(650, t2 + 0.06);
|
||||||
|
pGain.gain.setValueAtTime(0.3, t2);
|
||||||
|
pGain.gain.exponentialRampToValueAtTime(0.01, t2 + 0.15);
|
||||||
|
p1.connect(pGain);
|
||||||
|
pGain.connect(this.sfxGain);
|
||||||
|
p1.start(t2);
|
||||||
|
p1.stop(t2 + 0.16);
|
||||||
|
}, 320);
|
||||||
|
}
|
||||||
|
|
||||||
|
playChaingun() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(260 + Math.random() * 40, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(50, t + 0.12);
|
||||||
|
oGain.gain.setValueAtTime(0.65, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.13);
|
||||||
|
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.12);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(2200, t);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(0.7, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playRocketLaunch() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
// Swoosh tone
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(100, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(450, t + 0.25);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(120, t + 0.5);
|
||||||
|
oGain.gain.setValueAtTime(0.7, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.5);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.51);
|
||||||
|
|
||||||
|
// Rocket booster hiss
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.4);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(800, t);
|
||||||
|
filter.frequency.linearRampToValueAtTime(1600, t + 0.4);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(0.8, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playExplosion(volumeMultiplier = 1.0) {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
// Sub bass boom
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(160, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(25, t + 0.7);
|
||||||
|
oGain.gain.setValueAtTime(1.2 * volumeMultiplier, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.7);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.71);
|
||||||
|
|
||||||
|
// Crumbling explosion noise
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.8);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'lowpass';
|
||||||
|
filter.frequency.setValueAtTime(1400, t);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(120, t + 0.8);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(1.4 * volumeMultiplier, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.8);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playPlasma() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(950, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(180, t + 0.16);
|
||||||
|
oGain.gain.setValueAtTime(0.7, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.16);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.17);
|
||||||
|
|
||||||
|
const osc2 = this.ctx.createOscillator();
|
||||||
|
const oGain2 = this.ctx.createGain();
|
||||||
|
osc2.type = 'sine';
|
||||||
|
osc2.frequency.setValueAtTime(1400, t);
|
||||||
|
osc2.frequency.exponentialRampToValueAtTime(300, t + 0.14);
|
||||||
|
oGain2.gain.setValueAtTime(0.5, t);
|
||||||
|
oGain2.gain.exponentialRampToValueAtTime(0.01, t + 0.14);
|
||||||
|
osc2.connect(oGain2);
|
||||||
|
oGain2.connect(this.sfxGain);
|
||||||
|
osc2.start(t);
|
||||||
|
osc2.stop(t + 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
playBFGCharge() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(100, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.8);
|
||||||
|
gain.gain.setValueAtTime(0.1, t);
|
||||||
|
gain.gain.linearRampToValueAtTime(0.9, t + 0.8);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
playBFGFire() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
|
||||||
|
// Big apocalyptic energy discharge
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const oGain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(800, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(60, t + 0.9);
|
||||||
|
oGain.gain.setValueAtTime(1.3, t);
|
||||||
|
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.9);
|
||||||
|
osc.connect(oGain);
|
||||||
|
oGain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.92);
|
||||||
|
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(1.0);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(3000, t);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(200, t + 1.0);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(1.5, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 1.0);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// MONSTER & WORLD SOUND EFFECTS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
playEnemyAlert(type = 'zombie') {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
|
||||||
|
if (type === 'pinky' || type === 'caco') {
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(140, t);
|
||||||
|
osc.frequency.linearRampToValueAtTime(220, t + 0.15);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(60, t + 0.4);
|
||||||
|
gain.gain.setValueAtTime(0.7, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.42);
|
||||||
|
} else {
|
||||||
|
// Zombie / Imp grunt
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(220, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(90, t + 0.28);
|
||||||
|
gain.gain.setValueAtTime(0.6, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.28);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playEnemyPain() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(180, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(70, t + 0.18);
|
||||||
|
gain.gain.setValueAtTime(0.6, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.18);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
playEnemyDeath() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(130, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(30, t + 0.45);
|
||||||
|
gain.gain.setValueAtTime(0.8, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.45);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.46);
|
||||||
|
|
||||||
|
// Gurgle
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.35);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(600, t);
|
||||||
|
const nGain = this.ctx.createGain();
|
||||||
|
nGain.gain.setValueAtTime(0.7, t);
|
||||||
|
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(nGain);
|
||||||
|
nGain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playFireball() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.3);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(1200, t);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(300, t + 0.3);
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
gain.gain.setValueAtTime(0.8, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.3);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
playItemPickup(type = 'ammo') {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sine';
|
||||||
|
|
||||||
|
if (type === 'health' || type === 'armor') {
|
||||||
|
osc.frequency.setValueAtTime(440, t);
|
||||||
|
osc.frequency.setValueAtTime(554.37, t + 0.06);
|
||||||
|
osc.frequency.setValueAtTime(659.25, t + 0.12);
|
||||||
|
gain.gain.setValueAtTime(0.5, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.26);
|
||||||
|
} else if (type === 'weapon' || type === 'key') {
|
||||||
|
// Fanfare chime
|
||||||
|
osc.frequency.setValueAtTime(330, t);
|
||||||
|
osc.frequency.setValueAtTime(440, t + 0.08);
|
||||||
|
osc.frequency.setValueAtTime(660, t + 0.16);
|
||||||
|
osc.frequency.setValueAtTime(880, t + 0.24);
|
||||||
|
gain.gain.setValueAtTime(0.7, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.45);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.46);
|
||||||
|
} else {
|
||||||
|
// Ammo / click
|
||||||
|
osc.frequency.setValueAtTime(600, t);
|
||||||
|
osc.frequency.setValueAtTime(900, t + 0.05);
|
||||||
|
gain.gain.setValueAtTime(0.4, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.13);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playDoor(open = true) {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
const startFreq = open ? 90 : 150;
|
||||||
|
const endFreq = open ? 160 : 70;
|
||||||
|
osc.frequency.setValueAtTime(startFreq, t);
|
||||||
|
osc.frequency.linearRampToValueAtTime(endFreq, t + 0.6);
|
||||||
|
gain.gain.setValueAtTime(0.4, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6);
|
||||||
|
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'lowpass';
|
||||||
|
filter.frequency.setValueAtTime(450, t);
|
||||||
|
|
||||||
|
osc.connect(filter);
|
||||||
|
filter.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
playPlayerPain() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(200, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(80, t + 0.22);
|
||||||
|
gain.gain.setValueAtTime(0.9, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.22);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + 0.23);
|
||||||
|
}
|
||||||
|
|
||||||
|
playSlimeHurt() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const noise = this.ctx.createBufferSource();
|
||||||
|
noise.buffer = this.createNoiseBuffer(0.2);
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
filter.type = 'bandpass';
|
||||||
|
filter.frequency.setValueAtTime(3200, t);
|
||||||
|
filter.Q.setValueAtTime(3, t);
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
gain.gain.setValueAtTime(0.5, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(gain);
|
||||||
|
gain.connect(this.sfxGain);
|
||||||
|
noise.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// E1M1 RETRO SYNTHESIZER SOUNDTRACK
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
startMusic() {
|
||||||
|
if (this.musicPlaying) return;
|
||||||
|
this.musicPlaying = true;
|
||||||
|
this.musicStep = 0;
|
||||||
|
|
||||||
|
// E1M1 Notes in Hz (E Minor riff)
|
||||||
|
const E1 = 41.20;
|
||||||
|
const E2 = 82.41;
|
||||||
|
const G2 = 98.00;
|
||||||
|
const A2 = 110.00;
|
||||||
|
const Bb2 = 116.54;
|
||||||
|
const B2 = 123.47;
|
||||||
|
const D3 = 146.83;
|
||||||
|
const E3 = 164.81;
|
||||||
|
|
||||||
|
// 32-step bass riff pattern for classic E1M1
|
||||||
|
// E E E3 E E D3 E E | C3 E E Bb2 E E B2 C3
|
||||||
|
const riffNotes = [
|
||||||
|
E2, E2, E3, E2, E2, D3, E2, E2,
|
||||||
|
E3, E2, E2, D3, E2, E2, Bb2, B2,
|
||||||
|
E2, E2, E3, E2, E2, D3, E2, E2,
|
||||||
|
E3, E2, E2, G2, E2, A2, Bb2, B2
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepDuration = (60 / this.musicBpm) / 4; // 16th notes
|
||||||
|
|
||||||
|
const scheduleStep = () => {
|
||||||
|
if (!this.musicPlaying || !this.ctx) return;
|
||||||
|
const now = this.ctx.currentTime;
|
||||||
|
|
||||||
|
const note = riffNotes[this.musicStep % riffNotes.length];
|
||||||
|
const isHeavyBeat = (this.musicStep % 4 === 0);
|
||||||
|
const isSnareBeat = (this.musicStep % 8 === 4);
|
||||||
|
|
||||||
|
// Synth Distortion Bass/Guitar
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const dist = this.ctx.createWaveShaper();
|
||||||
|
const filter = this.ctx.createBiquadFilter();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(note, now);
|
||||||
|
dist.curve = this.makeDistortionCurve(30);
|
||||||
|
dist.oversample = '4x';
|
||||||
|
|
||||||
|
filter.type = 'lowpass';
|
||||||
|
filter.frequency.setValueAtTime(1400, now);
|
||||||
|
filter.frequency.exponentialRampToValueAtTime(350, now + stepDuration * 0.9);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.35, now);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, now + stepDuration * 0.95);
|
||||||
|
|
||||||
|
osc.connect(dist);
|
||||||
|
dist.connect(filter);
|
||||||
|
filter.connect(gain);
|
||||||
|
gain.connect(this.musicGain);
|
||||||
|
|
||||||
|
osc.start(now);
|
||||||
|
osc.stop(now + stepDuration);
|
||||||
|
|
||||||
|
// Kick Drum on quarter notes
|
||||||
|
if (isHeavyBeat) {
|
||||||
|
const kick = this.ctx.createOscillator();
|
||||||
|
const kGain = this.ctx.createGain();
|
||||||
|
kick.type = 'sine';
|
||||||
|
kick.frequency.setValueAtTime(140, now);
|
||||||
|
kick.frequency.exponentialRampToValueAtTime(35, now + 0.12);
|
||||||
|
kGain.gain.setValueAtTime(0.7, now);
|
||||||
|
kGain.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
|
||||||
|
kick.connect(kGain);
|
||||||
|
kGain.connect(this.musicGain);
|
||||||
|
kick.start(now);
|
||||||
|
kick.stop(now + 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snare Drum on 2 and 4
|
||||||
|
if (isSnareBeat) {
|
||||||
|
const snare = this.ctx.createBufferSource();
|
||||||
|
snare.buffer = this.createNoiseBuffer(0.15);
|
||||||
|
const sFilter = this.ctx.createBiquadFilter();
|
||||||
|
sFilter.type = 'highpass';
|
||||||
|
sFilter.frequency.setValueAtTime(1000, now);
|
||||||
|
const sGain = this.ctx.createGain();
|
||||||
|
sGain.gain.setValueAtTime(0.45, now);
|
||||||
|
sGain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
|
||||||
|
snare.connect(sFilter);
|
||||||
|
sFilter.connect(sGain);
|
||||||
|
sGain.connect(this.musicGain);
|
||||||
|
snare.start(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed Hi-hat every 8th note
|
||||||
|
if (this.musicStep % 2 === 0) {
|
||||||
|
const hat = this.ctx.createBufferSource();
|
||||||
|
hat.buffer = this.createNoiseBuffer(0.04);
|
||||||
|
const hFilter = this.ctx.createBiquadFilter();
|
||||||
|
hFilter.type = 'highpass';
|
||||||
|
hFilter.frequency.setValueAtTime(7000, now);
|
||||||
|
const hGain = this.ctx.createGain();
|
||||||
|
hGain.gain.setValueAtTime(0.15, now);
|
||||||
|
hGain.gain.exponentialRampToValueAtTime(0.01, now + 0.04);
|
||||||
|
hat.connect(hFilter);
|
||||||
|
hFilter.connect(hGain);
|
||||||
|
hGain.connect(this.musicGain);
|
||||||
|
hat.start(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.musicStep++;
|
||||||
|
this.musicTimer = setTimeout(scheduleStep, stepDuration * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
scheduleStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopMusic() {
|
||||||
|
this.musicPlaying = false;
|
||||||
|
if (this.musicTimer) {
|
||||||
|
clearTimeout(this.musicTimer);
|
||||||
|
this.musicTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMusic() {
|
||||||
|
if (this.musicPlaying) {
|
||||||
|
this.stopMusic();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.startMusic();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SoundSystem = SoundSystem;
|
||||||
Executable
+689
@@ -0,0 +1,689 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Enemies & Monster AI System
|
||||||
|
* 5 Iconic Doom Demons: Zombieman, Shotgunner, Imp, Pinky Demon, Cacodemon.
|
||||||
|
* 3D Billboards, multi-state animations, line-of-sight tracking, attacks, and blood FX.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomEnemies {
|
||||||
|
constructor(engine, player, sound, map) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.player = player;
|
||||||
|
this.sound = sound;
|
||||||
|
this.map = map;
|
||||||
|
|
||||||
|
this.enemies = [];
|
||||||
|
this.monsterProjectiles = [];
|
||||||
|
this.textures = {};
|
||||||
|
|
||||||
|
this.generateMonsterTextures();
|
||||||
|
this.spawnMapEnemies();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// PROCEDURAL HIGH-DEFINITION RETRO SPRITE TEXTURES
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
generateMonsterTextures() {
|
||||||
|
this.textures.zombie = this.createZombieSprites();
|
||||||
|
this.textures.shotgunner = this.createShotgunnerSprites();
|
||||||
|
this.textures.imp = this.createImpSprites();
|
||||||
|
this.textures.pinky = this.createPinkySprites();
|
||||||
|
this.textures.caco = this.createCacoSprites();
|
||||||
|
}
|
||||||
|
|
||||||
|
createZombieSprites() {
|
||||||
|
return {
|
||||||
|
walk1: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ff3300', 'rifle', 1),
|
||||||
|
walk2: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ff3300', 'rifle', 2),
|
||||||
|
attack: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ffea00', 'rifle_fire', 1),
|
||||||
|
pain: this.drawHumanoidSprite('#882222', '#c06060', '#ffffff', 'rifle', 1),
|
||||||
|
dead: this.drawDeadCorpseSprite('#3d6b2c', '#990000')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createShotgunnerSprites() {
|
||||||
|
return {
|
||||||
|
walk1: this.drawHumanoidSprite('#22252a', '#9098a0', '#ff0000', 'shotgun', 1),
|
||||||
|
walk2: this.drawHumanoidSprite('#22252a', '#9098a0', '#ff0000', 'shotgun', 2),
|
||||||
|
attack: this.drawHumanoidSprite('#22252a', '#9098a0', '#ffaa00', 'shotgun_fire', 1),
|
||||||
|
pain: this.drawHumanoidSprite('#6b1a1a', '#aa4444', '#ffffff', 'shotgun', 1),
|
||||||
|
dead: this.drawDeadCorpseSprite('#22252a', '#bb0000')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createImpSprites() {
|
||||||
|
return {
|
||||||
|
walk1: this.drawImpSprite(1),
|
||||||
|
walk2: this.drawImpSprite(2),
|
||||||
|
attack: this.drawImpSprite(3), // casting fireball
|
||||||
|
pain: this.drawImpSprite(4),
|
||||||
|
dead: this.drawDeadCorpseSprite('#6e4428', '#aa1100')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createPinkySprites() {
|
||||||
|
return {
|
||||||
|
walk1: this.drawPinkySprite(1),
|
||||||
|
walk2: this.drawPinkySprite(2),
|
||||||
|
attack: this.drawPinkySprite(3), // wide jaws bite
|
||||||
|
pain: this.drawPinkySprite(4),
|
||||||
|
dead: this.drawDeadCorpseSprite('#a82838', '#880000')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createCacoSprites() {
|
||||||
|
return {
|
||||||
|
walk1: this.drawCacoSprite(1),
|
||||||
|
walk2: this.drawCacoSprite(2),
|
||||||
|
attack: this.drawCacoSprite(3), // blue lightning maw
|
||||||
|
pain: this.drawCacoSprite(4),
|
||||||
|
dead: this.drawDeadCorpseSprite('#c41c1c', '#00d4ff')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHumanoidSprite(armorColor, skinColor, eyeColor, weaponType, frame) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 160;
|
||||||
|
c.height = 180;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Shadow under feet
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(80, 165, 38, 12, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Boots / Legs
|
||||||
|
ctx.fillStyle = '#181b16';
|
||||||
|
if (frame === 1) {
|
||||||
|
ctx.fillRect(58, 105, 20, 58);
|
||||||
|
ctx.fillRect(82, 105, 20, 58);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(48, 105, 22, 58);
|
||||||
|
ctx.fillRect(90, 105, 22, 58);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Torso / Tactical Armor Vest
|
||||||
|
ctx.fillStyle = armorColor;
|
||||||
|
ctx.fillRect(50, 48, 60, 60);
|
||||||
|
ctx.fillStyle = '#1c201a';
|
||||||
|
ctx.fillRect(56, 54, 48, 18); // Chest plate detail
|
||||||
|
ctx.fillRect(54, 94, 52, 10); // Utility belt
|
||||||
|
|
||||||
|
// Muscular / Zombie Flesh Head
|
||||||
|
ctx.fillStyle = skinColor;
|
||||||
|
ctx.fillRect(62, 16, 36, 34);
|
||||||
|
|
||||||
|
// Glowing Demonic Eyes
|
||||||
|
ctx.fillStyle = eyeColor;
|
||||||
|
ctx.fillRect(68, 26, 8, 7);
|
||||||
|
ctx.fillRect(84, 26, 8, 7);
|
||||||
|
|
||||||
|
// Sinister Mouth
|
||||||
|
ctx.fillStyle = '#330000';
|
||||||
|
ctx.fillRect(72, 38, 16, 6);
|
||||||
|
|
||||||
|
// Zombie Helmet / Hair
|
||||||
|
ctx.fillStyle = '#33382e';
|
||||||
|
ctx.fillRect(58, 10, 44, 12);
|
||||||
|
|
||||||
|
// Weapon
|
||||||
|
if (weaponType.includes('rifle')) {
|
||||||
|
ctx.fillStyle = '#111316';
|
||||||
|
ctx.fillRect(75, 62, 60, 14);
|
||||||
|
ctx.fillStyle = '#444c54';
|
||||||
|
ctx.fillRect(82, 58, 24, 8);
|
||||||
|
|
||||||
|
if (weaponType === 'rifle_fire') {
|
||||||
|
ctx.fillStyle = '#ffea00';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(142, 69, 20, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(142, 69, 10, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
} else if (weaponType.includes('shotgun')) {
|
||||||
|
ctx.fillStyle = '#1a1c20';
|
||||||
|
ctx.fillRect(70, 64, 70, 18);
|
||||||
|
ctx.fillStyle = '#7a3e1d'; // Wooden pump
|
||||||
|
ctx.fillRect(92, 70, 24, 14);
|
||||||
|
|
||||||
|
if (weaponType === 'shotgun_fire') {
|
||||||
|
ctx.fillStyle = '#ffaa00';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(148, 73, 26, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(148, 73, 14, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawImpSprite(pose) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 160;
|
||||||
|
c.height = 180;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(80, 165, 42, 12, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Brown Spiky Demon Body
|
||||||
|
ctx.fillStyle = pose === 4 ? '#8a2d1d' : '#6b4124';
|
||||||
|
// Torso
|
||||||
|
ctx.fillRect(52, 45, 56, 62);
|
||||||
|
|
||||||
|
// Demonic Spikes on shoulders & head
|
||||||
|
ctx.fillStyle = '#e8d8b8';
|
||||||
|
ctx.fillRect(42, 38, 12, 14);
|
||||||
|
ctx.fillRect(106, 38, 12, 14);
|
||||||
|
ctx.fillRect(56, 8, 10, 18);
|
||||||
|
ctx.fillRect(94, 8, 10, 18);
|
||||||
|
|
||||||
|
// Head
|
||||||
|
ctx.fillStyle = pose === 4 ? '#9c3622' : '#7c4d2d';
|
||||||
|
ctx.fillRect(58, 18, 44, 35);
|
||||||
|
|
||||||
|
// Glowing Fiery Red Eyes
|
||||||
|
ctx.fillStyle = '#ff0000';
|
||||||
|
ctx.fillRect(66, 28, 8, 8);
|
||||||
|
ctx.fillRect(86, 28, 8, 8);
|
||||||
|
ctx.fillStyle = '#ffff00';
|
||||||
|
ctx.fillRect(68, 30, 4, 4);
|
||||||
|
ctx.fillRect(88, 30, 4, 4);
|
||||||
|
|
||||||
|
// Sharp Demonic Fangs
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
ctx.fillRect(68 + i * 7, 42, 4, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legs with clawed hooves
|
||||||
|
ctx.fillStyle = '#523018';
|
||||||
|
if (pose === 1) {
|
||||||
|
ctx.fillRect(52, 107, 22, 56);
|
||||||
|
ctx.fillRect(86, 107, 22, 56);
|
||||||
|
} else if (pose === 2) {
|
||||||
|
ctx.fillRect(44, 107, 22, 56);
|
||||||
|
ctx.fillRect(94, 107, 22, 56);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(50, 107, 24, 56);
|
||||||
|
ctx.fillRect(86, 107, 24, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fireball casting pose
|
||||||
|
if (pose === 3) {
|
||||||
|
// Glowing Fireball in hands
|
||||||
|
ctx.fillStyle = '#ff5500';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(125, 75, 24, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = '#ffff00';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(125, 75, 14, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPinkySprite(frame) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 180;
|
||||||
|
c.height = 180;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(90, 165, 52, 14, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Massive Demonic Muscle Beast (Pinky)
|
||||||
|
ctx.fillStyle = frame === 4 ? '#781822' : '#b82e44';
|
||||||
|
ctx.fillRect(40, 40, 100, 70);
|
||||||
|
|
||||||
|
// Demon Horns
|
||||||
|
ctx.fillStyle = '#e6d8c8';
|
||||||
|
ctx.fillRect(28, 22, 18, 24);
|
||||||
|
ctx.fillRect(134, 22, 18, 24);
|
||||||
|
|
||||||
|
// Huge Demon Jaws & Snout
|
||||||
|
ctx.fillStyle = '#8a1d2e';
|
||||||
|
ctx.fillRect(55, 50, 70, 55);
|
||||||
|
|
||||||
|
// Glowing Yellow Eyes
|
||||||
|
ctx.fillStyle = '#ffff33';
|
||||||
|
ctx.fillRect(60, 55, 12, 10);
|
||||||
|
ctx.fillRect(108, 55, 12, 10);
|
||||||
|
|
||||||
|
// Terrifying Sharp Teeth
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
ctx.fillRect(62 + i * 10, 85, 6, 12);
|
||||||
|
if (frame === 3) {
|
||||||
|
// Open jaw
|
||||||
|
ctx.fillRect(62 + i * 10, 70, 6, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heavy Stomping Legs
|
||||||
|
ctx.fillStyle = '#6e1422';
|
||||||
|
if (frame === 1) {
|
||||||
|
ctx.fillRect(42, 110, 32, 54);
|
||||||
|
ctx.fillRect(106, 110, 32, 54);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(30, 110, 32, 54);
|
||||||
|
ctx.fillRect(118, 110, 32, 54);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCacoSprite(frame) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 180;
|
||||||
|
c.height = 180;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Giant Red Floating Sphere Demon
|
||||||
|
ctx.fillStyle = '#c41c1c';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(90, 90, 72, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Spikes on head
|
||||||
|
ctx.fillStyle = '#6e0c0c';
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const angle = Math.PI + (i * Math.PI) / 7;
|
||||||
|
const hx = 90 + Math.cos(angle) * 70;
|
||||||
|
const hy = 90 + Math.sin(angle) * 70;
|
||||||
|
ctx.fillRect(hx - 6, hy - 6, 12, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Massive Emerald Eye
|
||||||
|
ctx.fillStyle = '#00ff55';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(90, 65, 24, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillStyle = '#003311';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(90, 65, 10, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Gaping Maw
|
||||||
|
ctx.fillStyle = '#111116';
|
||||||
|
ctx.fillRect(50, 105, 80, 34);
|
||||||
|
|
||||||
|
if (frame === 3) {
|
||||||
|
// Blue Lightning Ball inside mouth
|
||||||
|
ctx.fillStyle = '#00d4ff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(90, 122, 22, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
} else {
|
||||||
|
// Teeth
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
ctx.fillRect(55 + i * 10, 105, 6, 10);
|
||||||
|
ctx.fillRect(55 + i * 10, 128, 6, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawDeadCorpseSprite(bodyColor, bloodColor) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 160;
|
||||||
|
c.height = 90;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Blood Pool on Floor
|
||||||
|
ctx.fillStyle = bloodColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(80, 58, 68, 24, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Collapsed Demonic Body
|
||||||
|
ctx.fillStyle = bodyColor;
|
||||||
|
ctx.fillRect(35, 36, 95, 28);
|
||||||
|
|
||||||
|
// Skull / Slain Head
|
||||||
|
ctx.fillStyle = '#d0a080';
|
||||||
|
ctx.fillRect(20, 42, 20, 18);
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// SPAWNING ENEMIES
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
spawnMapEnemies() {
|
||||||
|
const spawns = (this.map && this.map.spawns) ? this.map.spawns.enemies : (window.game && window.game.map ? window.game.map.spawns.enemies : []);
|
||||||
|
if (!spawns || spawns.length === 0) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < spawns.length; i++) {
|
||||||
|
const s = spawns[i];
|
||||||
|
this.createEnemy(s.type, s.x, s.y, s.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEnemy(type, x, y, z) {
|
||||||
|
let maxHp = 30;
|
||||||
|
let speed = 4.5;
|
||||||
|
let scaleW = 2.4;
|
||||||
|
let scaleH = 2.6;
|
||||||
|
let attackRange = 28;
|
||||||
|
let isFlying = false;
|
||||||
|
|
||||||
|
if (type === 'zombie') {
|
||||||
|
maxHp = 30;
|
||||||
|
speed = 3.6;
|
||||||
|
attackRange = 30;
|
||||||
|
} else if (type === 'shotgunner') {
|
||||||
|
maxHp = 45;
|
||||||
|
speed = 4.0;
|
||||||
|
attackRange = 24;
|
||||||
|
} else if (type === 'imp') {
|
||||||
|
maxHp = 65;
|
||||||
|
speed = 4.6;
|
||||||
|
attackRange = 35;
|
||||||
|
} else if (type === 'pinky') {
|
||||||
|
maxHp = 140;
|
||||||
|
speed = 7.5;
|
||||||
|
attackRange = 2.8;
|
||||||
|
scaleW = 2.8;
|
||||||
|
scaleH = 2.8;
|
||||||
|
} else if (type === 'caco') {
|
||||||
|
maxHp = 220;
|
||||||
|
speed = 4.0;
|
||||||
|
attackRange = 35;
|
||||||
|
scaleW = 3.4;
|
||||||
|
scaleH = 3.4;
|
||||||
|
isFlying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprite Material - depthTest true, depthWrite false for proper transparency
|
||||||
|
const spriteMat = new THREE.SpriteMaterial({
|
||||||
|
map: this.textures[type].walk1,
|
||||||
|
transparent: true,
|
||||||
|
depthTest: true,
|
||||||
|
depthWrite: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const sprite = new THREE.Sprite(spriteMat);
|
||||||
|
const spawnY = isFlying ? (y + 3.5) : (y + scaleH / 2);
|
||||||
|
sprite.position.set(x, spawnY, z);
|
||||||
|
sprite.scale.set(scaleW, scaleH, 1);
|
||||||
|
this.engine.scene.add(sprite);
|
||||||
|
|
||||||
|
const enemyObj = {
|
||||||
|
type,
|
||||||
|
mesh: sprite,
|
||||||
|
material: spriteMat,
|
||||||
|
hp: maxHp,
|
||||||
|
maxHp: maxHp,
|
||||||
|
speed: speed,
|
||||||
|
scaleW: scaleW,
|
||||||
|
scaleH: scaleH,
|
||||||
|
radius: scaleW * 0.45,
|
||||||
|
attackRange: attackRange,
|
||||||
|
isFlying: isFlying,
|
||||||
|
state: 'idle',
|
||||||
|
stateTimer: 0,
|
||||||
|
animTimer: 0,
|
||||||
|
animFrame: 1,
|
||||||
|
attackCooldown: 0.8 + Math.random() * 0.8,
|
||||||
|
isDead: false,
|
||||||
|
takeDamage: (dmg, hitPos) => this.damageEnemy(enemyObj, dmg, hitPos)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.enemies.push(enemyObj);
|
||||||
|
return enemyObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
damageEnemy(e, amount, hitPos) {
|
||||||
|
if (e.isDead) return;
|
||||||
|
|
||||||
|
e.hp -= amount;
|
||||||
|
if (this.sound) this.sound.playEnemyPain();
|
||||||
|
|
||||||
|
// Alert enemy and nearby monsters!
|
||||||
|
e.state = 'chase';
|
||||||
|
this.alertNearbyMonsters(e.mesh.position, 35);
|
||||||
|
|
||||||
|
if (e.hp <= 0) {
|
||||||
|
this.killEnemy(e);
|
||||||
|
} else {
|
||||||
|
// Pain stagger frame
|
||||||
|
e.state = 'pain';
|
||||||
|
e.stateTimer = 0.22;
|
||||||
|
e.material.map = this.textures[e.type].pain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
killEnemy(e) {
|
||||||
|
e.isDead = true;
|
||||||
|
e.state = 'dead';
|
||||||
|
e.mesh.scale.set(e.scaleW * 1.3, e.scaleH * 0.5, 1);
|
||||||
|
if (!e.isFlying) {
|
||||||
|
e.mesh.position.y = Math.max(0.1, e.mesh.position.y - e.scaleH * 0.35);
|
||||||
|
}
|
||||||
|
e.material.map = this.textures[e.type].dead;
|
||||||
|
if (this.sound) this.sound.playEnemyDeath();
|
||||||
|
|
||||||
|
// Drop loot
|
||||||
|
if (e.type === 'shotgunner') {
|
||||||
|
if (window.game && window.game.items) {
|
||||||
|
window.game.items.spawnItem('shotgun', e.mesh.position.x, e.mesh.position.y, e.mesh.position.z);
|
||||||
|
}
|
||||||
|
} else if (e.type === 'zombie') {
|
||||||
|
if (window.game && window.game.items && Math.random() > 0.4) {
|
||||||
|
window.game.items.spawnItem('ammo_clip', e.mesh.position.x, e.mesh.position.y, e.mesh.position.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alertNearbyMonsters(pos, radius) {
|
||||||
|
for (let i = 0; i < this.enemies.length; i++) {
|
||||||
|
const other = this.enemies[i];
|
||||||
|
if (!other.isDead && other.state === 'idle') {
|
||||||
|
if (other.mesh.position.distanceTo(pos) < radius) {
|
||||||
|
other.state = 'chase';
|
||||||
|
if (this.sound) this.sound.playEnemyAlert(other.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// ENEMY AI & PROJECTILES UPDATE LOOP
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
const playerPos = this.player.position;
|
||||||
|
|
||||||
|
// 1. Update Demon AI
|
||||||
|
for (let i = 0; i < this.enemies.length; i++) {
|
||||||
|
const e = this.enemies[i];
|
||||||
|
if (e.isDead) continue;
|
||||||
|
|
||||||
|
const dist = e.mesh.position.distanceTo(playerPos);
|
||||||
|
|
||||||
|
// Alert enemy if player is close or in line of sight
|
||||||
|
if (e.state === 'idle' && dist < 28) {
|
||||||
|
e.state = 'chase';
|
||||||
|
if (this.sound) this.sound.playEnemyAlert(e.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// State Handling
|
||||||
|
if (e.state === 'pain') {
|
||||||
|
e.stateTimer -= delta;
|
||||||
|
if (e.stateTimer <= 0) {
|
||||||
|
e.state = 'chase';
|
||||||
|
}
|
||||||
|
} else if (e.state === 'attack') {
|
||||||
|
e.stateTimer -= delta;
|
||||||
|
if (e.stateTimer <= 0) {
|
||||||
|
e.state = 'chase';
|
||||||
|
e.attackCooldown = 1.2 + Math.random() * 0.8;
|
||||||
|
}
|
||||||
|
} else if (e.state === 'chase') {
|
||||||
|
e.attackCooldown -= delta;
|
||||||
|
|
||||||
|
// Move towards player
|
||||||
|
const dir = new THREE.Vector3().subVectors(playerPos, e.mesh.position);
|
||||||
|
if (!e.isFlying) dir.y = 0;
|
||||||
|
dir.normalize();
|
||||||
|
|
||||||
|
// Check collision with other enemies to prevent stacking
|
||||||
|
const moveDist = e.speed * delta;
|
||||||
|
e.mesh.position.addScaledVector(dir, moveDist);
|
||||||
|
|
||||||
|
// Animation walking frames
|
||||||
|
e.animTimer += delta * 6;
|
||||||
|
if (e.animTimer >= 1.0) {
|
||||||
|
e.animTimer = 0;
|
||||||
|
e.animFrame = e.animFrame === 1 ? 2 : 1;
|
||||||
|
e.material.map = (e.animFrame === 1) ? this.textures[e.type].walk1 : this.textures[e.type].walk2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack Player if within range and cooldown is ready
|
||||||
|
if (dist <= e.attackRange && e.attackCooldown <= 0) {
|
||||||
|
this.triggerEnemyAttack(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update Monster Flying Projectiles (Fireballs & Plasma balls)
|
||||||
|
for (let i = this.monsterProjectiles.length - 1; i >= 0; i--) {
|
||||||
|
const p = this.monsterProjectiles[i];
|
||||||
|
p.mesh.position.addScaledVector(p.velocity, delta);
|
||||||
|
p.light.position.copy(p.mesh.position);
|
||||||
|
p.life -= delta;
|
||||||
|
|
||||||
|
// Check hit against player
|
||||||
|
const pDist = p.mesh.position.distanceTo(this.player.position);
|
||||||
|
if (pDist < 1.4) {
|
||||||
|
this.player.takeDamage(p.damage, p.mesh.position);
|
||||||
|
this.engine.createExplosionFX(p.mesh.position, p.isCaco ? 0x00d4ff : 0xff5500);
|
||||||
|
this.destroyMonsterProjectile(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.life <= 0) {
|
||||||
|
this.destroyMonsterProjectile(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEnemyAttack(e) {
|
||||||
|
e.state = 'attack';
|
||||||
|
e.stateTimer = 0.5;
|
||||||
|
e.material.map = this.textures[e.type].attack;
|
||||||
|
|
||||||
|
const playerPos = this.player.position;
|
||||||
|
|
||||||
|
if (e.type === 'pinky') {
|
||||||
|
// Melee Bite Attack
|
||||||
|
if (e.mesh.position.distanceTo(playerPos) < 3.2) {
|
||||||
|
this.player.takeDamage(18, e.mesh.position);
|
||||||
|
}
|
||||||
|
} else if (e.type === 'zombie') {
|
||||||
|
// Hitscan rifle shot
|
||||||
|
if (this.sound) this.sound.playPistol();
|
||||||
|
this.engine.createMuzzleLight(e.mesh.position, 0xffcc00);
|
||||||
|
this.player.takeDamage(8, e.mesh.position);
|
||||||
|
} else if (e.type === 'shotgunner') {
|
||||||
|
// Hitscan shotgun blast
|
||||||
|
if (this.sound) this.sound.playShotgun();
|
||||||
|
this.engine.createMuzzleLight(e.mesh.position, 0xff9900);
|
||||||
|
this.player.takeDamage(18, e.mesh.position);
|
||||||
|
} else if (e.type === 'imp') {
|
||||||
|
// Launch Fireball projectile
|
||||||
|
this.spawnFireball(e.mesh.position, playerPos, 16, 22.0);
|
||||||
|
} else if (e.type === 'caco') {
|
||||||
|
// Launch Blue Lightning Plasma projectile
|
||||||
|
this.spawnCacoBall(e.mesh.position, playerPos, 28, 25.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnFireball(from, to, damage, speed) {
|
||||||
|
const geo = new THREE.SphereGeometry(0.35, 8, 8);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: 0xff4400 });
|
||||||
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
|
mesh.position.copy(from);
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
const light = new THREE.PointLight(0xff5500, 3.0, 10, 2);
|
||||||
|
light.position.copy(from);
|
||||||
|
this.engine.scene.add(light);
|
||||||
|
|
||||||
|
const target = new THREE.Vector3(to.x, to.y + 1.2, to.z);
|
||||||
|
const dir = new THREE.Vector3().subVectors(target, from).normalize();
|
||||||
|
|
||||||
|
this.monsterProjectiles.push({
|
||||||
|
mesh,
|
||||||
|
light,
|
||||||
|
velocity: dir.multiplyScalar(speed),
|
||||||
|
damage,
|
||||||
|
life: 4.0,
|
||||||
|
isCaco: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnCacoBall(from, to, damage, speed) {
|
||||||
|
const geo = new THREE.SphereGeometry(0.45, 8, 8);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: 0x00d4ff });
|
||||||
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
|
mesh.position.copy(from);
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
const light = new THREE.PointLight(0x00d4ff, 3.5, 12, 2);
|
||||||
|
light.position.copy(from);
|
||||||
|
this.engine.scene.add(light);
|
||||||
|
|
||||||
|
const target = new THREE.Vector3(to.x, to.y + 1.2, to.z);
|
||||||
|
const dir = new THREE.Vector3().subVectors(target, from).normalize();
|
||||||
|
|
||||||
|
this.monsterProjectiles.push({
|
||||||
|
mesh,
|
||||||
|
light,
|
||||||
|
velocity: dir.multiplyScalar(speed),
|
||||||
|
damage,
|
||||||
|
life: 4.0,
|
||||||
|
isCaco: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyMonsterProjectile(index) {
|
||||||
|
const p = this.monsterProjectiles[index];
|
||||||
|
this.engine.scene.remove(p.mesh);
|
||||||
|
this.engine.scene.remove(p.light);
|
||||||
|
this.monsterProjectiles.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomEnemies = DoomEnemies;
|
||||||
Executable
+852
@@ -0,0 +1,852 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Procedural Texture Generator, Particle System & Three.js Engine Setup
|
||||||
|
* High performance WebGL engine with authentic 90s DOOM textures and visual effects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomEngine {
|
||||||
|
constructor(canvasContainer) {
|
||||||
|
this.container = canvasContainer;
|
||||||
|
this.scene = null;
|
||||||
|
this.camera = null;
|
||||||
|
this.renderer = null;
|
||||||
|
this.clock = new THREE.Clock();
|
||||||
|
|
||||||
|
// Procedural Textures Cache
|
||||||
|
this.textures = {};
|
||||||
|
this.materials = {};
|
||||||
|
|
||||||
|
// Particle System
|
||||||
|
this.particles = [];
|
||||||
|
this.decals = [];
|
||||||
|
this.animatedTextures = [];
|
||||||
|
|
||||||
|
// Lights
|
||||||
|
this.lights = [];
|
||||||
|
this.dynamicLights = [];
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 1. Scene & Atmospheric Fog
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0x1a0505);
|
||||||
|
this.scene.fog = new THREE.FogExp2(0x1a0505, 0.015);
|
||||||
|
|
||||||
|
// 2. Camera
|
||||||
|
const aspect = window.innerWidth / window.innerHeight;
|
||||||
|
this.camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
|
||||||
|
this.camera.position.set(0, 1.8, 0);
|
||||||
|
|
||||||
|
// 3. Renderer
|
||||||
|
this.renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||||
|
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
||||||
|
this.renderer.shadowMap.enabled = true;
|
||||||
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
this.container.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
// 4. Generate Procedural Textures & Materials
|
||||||
|
this.generateTextures();
|
||||||
|
this.buildSkybox();
|
||||||
|
|
||||||
|
// 5. Global Lighting
|
||||||
|
this.setupLighting();
|
||||||
|
|
||||||
|
// 6. Window Resize Handler
|
||||||
|
window.addEventListener('resize', () => this.onResize());
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize() {
|
||||||
|
if (!this.camera || !this.renderer) return;
|
||||||
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLighting() {
|
||||||
|
// Ambient base light (dark demonic tone)
|
||||||
|
const ambient = new THREE.AmbientLight(0x382222, 0.9);
|
||||||
|
this.scene.add(ambient);
|
||||||
|
this.lights.push(ambient);
|
||||||
|
|
||||||
|
// Sun / Directional red overhead light
|
||||||
|
const sun = new THREE.DirectionalLight(0xff6644, 0.8);
|
||||||
|
sun.position.set(30, 60, 20);
|
||||||
|
sun.castShadow = true;
|
||||||
|
sun.shadow.mapSize.width = 1024;
|
||||||
|
sun.shadow.mapSize.height = 1024;
|
||||||
|
sun.shadow.camera.near = 0.5;
|
||||||
|
sun.shadow.camera.far = 200;
|
||||||
|
sun.shadow.camera.left = -50;
|
||||||
|
sun.shadow.camera.right = 50;
|
||||||
|
sun.shadow.camera.top = 50;
|
||||||
|
sun.shadow.camera.bottom = -50;
|
||||||
|
this.scene.add(sun);
|
||||||
|
this.lights.push(sun);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFlickeringLight(x, y, z, color = 0xffaa44, intensity = 2.0, distance = 25) {
|
||||||
|
const light = new THREE.PointLight(color, intensity, distance, 2);
|
||||||
|
light.position.set(x, y, z);
|
||||||
|
this.scene.add(light);
|
||||||
|
this.dynamicLights.push({
|
||||||
|
light,
|
||||||
|
baseIntensity: intensity,
|
||||||
|
flickerSpeed: 10 + Math.random() * 20,
|
||||||
|
phase: Math.random() * 10
|
||||||
|
});
|
||||||
|
return light;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// PROCEDURAL TEXTURE GENERATION
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
generateTextures() {
|
||||||
|
this.textures.metal = this.createMetalTexture();
|
||||||
|
this.textures.tech = this.createTechScreenTexture();
|
||||||
|
this.textures.brick = this.createBrickTexture();
|
||||||
|
this.textures.hex = this.createHexMetalTexture();
|
||||||
|
this.textures.grate = this.createGratingTexture();
|
||||||
|
this.textures.slime = this.createSlimeTexture();
|
||||||
|
this.textures.doorRegular = this.createDoorTexture('normal');
|
||||||
|
this.textures.doorBlue = this.createDoorTexture('blue');
|
||||||
|
this.textures.doorRed = this.createDoorTexture('red');
|
||||||
|
this.textures.doorYellow = this.createDoorTexture('yellow');
|
||||||
|
this.textures.ceiling = this.createCeilingTexture();
|
||||||
|
this.textures.barrel = this.createBarrelTexture();
|
||||||
|
|
||||||
|
// Standard Doom Materials
|
||||||
|
this.materials.metal = new THREE.MeshStandardMaterial({ map: this.textures.metal, roughness: 0.6, metalness: 0.4 });
|
||||||
|
this.materials.tech = new THREE.MeshStandardMaterial({ map: this.textures.tech, roughness: 0.4, metalness: 0.2, emissive: 0x112233, emissiveIntensity: 0.4 });
|
||||||
|
this.materials.brick = new THREE.MeshStandardMaterial({ map: this.textures.brick, roughness: 0.85, metalness: 0.1 });
|
||||||
|
this.materials.hex = new THREE.MeshStandardMaterial({ map: this.textures.hex, roughness: 0.5, metalness: 0.6 });
|
||||||
|
this.materials.grate = new THREE.MeshStandardMaterial({ map: this.textures.grate, roughness: 0.7, metalness: 0.5 });
|
||||||
|
this.materials.slime = new THREE.MeshStandardMaterial({ map: this.textures.slime.texture, roughness: 0.2, metalness: 0.1, emissive: 0x00ff22, emissiveIntensity: 0.6 });
|
||||||
|
this.materials.doorRegular = new THREE.MeshStandardMaterial({ map: this.textures.doorRegular, roughness: 0.5, metalness: 0.5 });
|
||||||
|
this.materials.doorBlue = new THREE.MeshStandardMaterial({ map: this.textures.doorBlue, roughness: 0.5, metalness: 0.5, emissive: 0x0044ff, emissiveIntensity: 0.3 });
|
||||||
|
this.materials.doorRed = new THREE.MeshStandardMaterial({ map: this.textures.doorRed, roughness: 0.5, metalness: 0.5, emissive: 0xff0022, emissiveIntensity: 0.3 });
|
||||||
|
this.materials.doorYellow = new THREE.MeshStandardMaterial({ map: this.textures.doorYellow, roughness: 0.5, metalness: 0.5, emissive: 0xffbb00, emissiveIntensity: 0.3 });
|
||||||
|
this.materials.ceiling = new THREE.MeshStandardMaterial({ map: this.textures.ceiling, roughness: 0.7, metalness: 0.3 });
|
||||||
|
this.materials.barrel = new THREE.MeshStandardMaterial({ map: this.textures.barrel, roughness: 0.6, metalness: 0.3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
createMetalTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Steel base
|
||||||
|
ctx.fillStyle = '#484440';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Heavy steel panel borders
|
||||||
|
ctx.strokeStyle = '#22201e';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.strokeRect(4, 4, 248, 248);
|
||||||
|
ctx.strokeRect(10, 10, 114, 114);
|
||||||
|
ctx.strokeRect(132, 10, 114, 114);
|
||||||
|
ctx.strokeRect(10, 132, 114, 114);
|
||||||
|
ctx.strokeRect(132, 132, 114, 114);
|
||||||
|
|
||||||
|
// Rivets / Bolts
|
||||||
|
ctx.fillStyle = '#1c1b18';
|
||||||
|
const rivetPoints = [
|
||||||
|
[16, 16], [118, 16], [16, 118], [118, 118],
|
||||||
|
[138, 16], [240, 16], [138, 118], [240, 118],
|
||||||
|
[16, 138], [118, 138], [16, 240], [118, 240],
|
||||||
|
[138, 138], [240, 138], [138, 240], [240, 240]
|
||||||
|
];
|
||||||
|
rivetPoints.forEach(([x, y]) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = '#6a6560';
|
||||||
|
ctx.fillRect(x - 1, y - 1, 2, 2);
|
||||||
|
ctx.fillStyle = '#1c1b18';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Grime and scratches
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
|
||||||
|
for (let i = 0; i < 600; i++) {
|
||||||
|
ctx.fillRect(Math.random() * 256, Math.random() * 256, Math.random() * 3 + 1, Math.random() * 3 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rust patches
|
||||||
|
ctx.fillStyle = 'rgba(120, 60, 20, 0.25)';
|
||||||
|
ctx.fillRect(10, 110, 80, 14);
|
||||||
|
ctx.fillRect(150, 10, 20, 90);
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTechScreenTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Dark tech casing
|
||||||
|
ctx.fillStyle = '#202428';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Terminal Screen 1 (Top Blue Terminal)
|
||||||
|
ctx.fillStyle = '#081420';
|
||||||
|
ctx.fillRect(16, 16, 224, 100);
|
||||||
|
ctx.strokeStyle = '#3a4450';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeRect(16, 16, 224, 100);
|
||||||
|
|
||||||
|
// Oscilloscope / Data wave
|
||||||
|
ctx.strokeStyle = '#00ffff';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let x = 20; x < 230; x += 5) {
|
||||||
|
const y = 66 + Math.sin(x * 0.1) * 20 + (Math.random() * 6 - 3);
|
||||||
|
if (x === 20) ctx.moveTo(x, y);
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Terminal Text lines
|
||||||
|
ctx.fillStyle = '#44ff88';
|
||||||
|
ctx.font = '10px monospace';
|
||||||
|
ctx.fillText('UAC MAINFRAME // LEVEL 1', 24, 34);
|
||||||
|
ctx.fillStyle = '#00aaff';
|
||||||
|
ctx.fillText('SYSTEM STATUS: CRITICAL', 24, 48);
|
||||||
|
ctx.fillStyle = '#ff4444';
|
||||||
|
ctx.fillText('REACTOR OVERHEAT: 88.4%', 24, 98);
|
||||||
|
|
||||||
|
// Lower Control Panel with LED buttons and sliders
|
||||||
|
ctx.fillStyle = '#16181b';
|
||||||
|
ctx.fillRect(16, 128, 224, 112);
|
||||||
|
ctx.strokeStyle = '#40454c';
|
||||||
|
ctx.strokeRect(16, 128, 224, 112);
|
||||||
|
|
||||||
|
// Blinking LEDs
|
||||||
|
const ledColors = ['#ff0000', '#00ff00', '#ffff00', '#00e1ff', '#ff00ff'];
|
||||||
|
for (let row = 0; row < 3; row++) {
|
||||||
|
for (let col = 0; col < 7; col++) {
|
||||||
|
ctx.fillStyle = ledColors[(row * 7 + col) % ledColors.length];
|
||||||
|
ctx.fillRect(28 + col * 28, 142 + row * 26, 14, 14);
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(28 + col * 28, 142 + row * 26, 14, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBrickTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Dark demonic brick mortar
|
||||||
|
ctx.fillStyle = '#180e0c';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Red/brown stone bricks
|
||||||
|
const brickH = 32;
|
||||||
|
const brickW = 64;
|
||||||
|
const rows = 8;
|
||||||
|
const cols = 4;
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
const offset = (r % 2) * (brickW / 2);
|
||||||
|
for (let c = -1; c <= cols; c++) {
|
||||||
|
const x = c * brickW + offset + 2;
|
||||||
|
const y = r * brickH + 2;
|
||||||
|
const shade = 70 + Math.floor(Math.random() * 40);
|
||||||
|
const redShade = shade + 40;
|
||||||
|
ctx.fillStyle = `rgb(${redShade}, ${Math.floor(shade * 0.4)}, ${Math.floor(shade * 0.35)})`;
|
||||||
|
ctx.fillRect(x, y, brickW - 4, brickH - 4);
|
||||||
|
|
||||||
|
// Brick cracks & texture
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||||
|
ctx.fillRect(x + Math.random() * 20, y + 4, Math.random() * 25 + 5, 2);
|
||||||
|
ctx.fillRect(x + 4, y + Math.random() * 15, 2, Math.random() * 10 + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createHexMetalTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
ctx.fillStyle = '#26292d';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Hexagonal grid
|
||||||
|
const size = 32;
|
||||||
|
const h = size * Math.sqrt(3);
|
||||||
|
|
||||||
|
const drawHex = (x, y, radius) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const angle = (Math.PI / 3) * i;
|
||||||
|
const hx = x + radius * Math.cos(angle);
|
||||||
|
const hy = y + radius * Math.sin(angle);
|
||||||
|
if (i === 0) ctx.moveTo(hx, hy);
|
||||||
|
else ctx.lineTo(hx, hy);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let row = -1; row < 10; row++) {
|
||||||
|
for (let col = -1; col < 10; col++) {
|
||||||
|
const x = col * size * 1.5;
|
||||||
|
const y = row * h + ((col % 2) * h / 2);
|
||||||
|
ctx.fillStyle = (row + col) % 2 === 0 ? '#34383e' : '#2b2f34';
|
||||||
|
drawHex(x, y, size * 0.9);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = '#181a1d';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Center glowing dot
|
||||||
|
if ((row + col) % 3 === 0) {
|
||||||
|
ctx.fillStyle = '#ff6600';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createGratingTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 128;
|
||||||
|
c.height = 128;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
ctx.fillStyle = '#1c1c1f';
|
||||||
|
ctx.fillRect(0, 0, 128, 128);
|
||||||
|
|
||||||
|
// Grate lines
|
||||||
|
ctx.fillStyle = '#3a3a40';
|
||||||
|
for (let i = 0; i < 128; i += 16) {
|
||||||
|
ctx.fillRect(i, 0, 4, 128);
|
||||||
|
ctx.fillRect(0, i, 128, 4);
|
||||||
|
}
|
||||||
|
// Shadow pits
|
||||||
|
ctx.fillStyle = '#0a0a0c';
|
||||||
|
for (let x = 4; x < 128; x += 16) {
|
||||||
|
for (let y = 4; y < 128; y += 16) {
|
||||||
|
ctx.fillRect(x + 2, y + 2, 8, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createSlimeTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
|
||||||
|
const animObj = {
|
||||||
|
canvas: c,
|
||||||
|
ctx: ctx,
|
||||||
|
texture: tex,
|
||||||
|
time: 0,
|
||||||
|
update: (delta) => {
|
||||||
|
animObj.time += delta * 2;
|
||||||
|
const t = animObj.time;
|
||||||
|
// Radioactive green gradient
|
||||||
|
ctx.fillStyle = '#154408';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Bubbles and ripples
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const bx = 128 + Math.cos(t * 0.8 + i * 1.3) * 90 + Math.sin(i * 4) * 20;
|
||||||
|
const by = 128 + Math.sin(t * 0.9 + i * 1.5) * 90 + Math.cos(i * 3) * 20;
|
||||||
|
const rad = 10 + Math.sin(t * 3 + i) * 6;
|
||||||
|
|
||||||
|
const grad = ctx.createRadialGradient(bx, by, 2, bx, by, rad);
|
||||||
|
grad.addColorStop(0, '#88ff22');
|
||||||
|
grad.addColorStop(0.7, '#22aa00');
|
||||||
|
grad.addColorStop(1, 'transparent');
|
||||||
|
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(bx, by, rad, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swirling slime streaks
|
||||||
|
ctx.strokeStyle = 'rgba(100, 255, 30, 0.3)';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let x = 0; x < 256; x += 10) {
|
||||||
|
const y = 128 + Math.sin(x * 0.05 + t) * 40 + Math.cos(x * 0.02 - t) * 30;
|
||||||
|
if (x === 0) ctx.moveTo(x, y);
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animatedTextures.push(animObj);
|
||||||
|
return animObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
createDoorTexture(type = 'normal') {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 512;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Blast door base
|
||||||
|
ctx.fillStyle = '#3a3a40';
|
||||||
|
ctx.fillRect(0, 0, 256, 512);
|
||||||
|
|
||||||
|
// Outer heavy steel frame
|
||||||
|
ctx.strokeStyle = '#18181a';
|
||||||
|
ctx.lineWidth = 12;
|
||||||
|
ctx.strokeRect(6, 6, 244, 500);
|
||||||
|
|
||||||
|
// Center seam split
|
||||||
|
ctx.strokeStyle = '#111';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(128, 12);
|
||||||
|
ctx.lineTo(128, 500);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Hazard Stripes at top & bottom
|
||||||
|
const drawHazard = (yStart, h) => {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(12, yStart, 232, h);
|
||||||
|
ctx.clip();
|
||||||
|
ctx.fillStyle = '#ffaa00';
|
||||||
|
ctx.fillRect(12, yStart, 232, h);
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
for (let x = -50; x < 300; x += 30) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, yStart);
|
||||||
|
ctx.lineTo(x + 20, yStart);
|
||||||
|
ctx.lineTo(x - 10, yStart + h);
|
||||||
|
ctx.lineTo(x - 30, yStart + h);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
drawHazard(12, 40);
|
||||||
|
drawHazard(460, 40);
|
||||||
|
|
||||||
|
// Center Keycard Lock Indicator
|
||||||
|
ctx.fillStyle = '#1e2024';
|
||||||
|
ctx.fillRect(88, 220, 80, 72);
|
||||||
|
ctx.strokeStyle = '#555';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeRect(88, 220, 80, 72);
|
||||||
|
|
||||||
|
if (type === 'blue') {
|
||||||
|
ctx.fillStyle = '#0088ff';
|
||||||
|
ctx.fillRect(96, 230, 64, 52);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.font = 'bold 12px monospace';
|
||||||
|
ctx.fillText('BLUE', 110, 260);
|
||||||
|
} else if (type === 'red') {
|
||||||
|
ctx.fillStyle = '#ff1122';
|
||||||
|
ctx.fillRect(96, 230, 64, 52);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.font = 'bold 12px monospace';
|
||||||
|
ctx.fillText('RED', 114, 260);
|
||||||
|
} else if (type === 'yellow') {
|
||||||
|
ctx.fillStyle = '#ffbb00';
|
||||||
|
ctx.fillRect(96, 230, 64, 52);
|
||||||
|
ctx.fillStyle = '#111111';
|
||||||
|
ctx.font = 'bold 12px monospace';
|
||||||
|
ctx.fillText('YELW', 110, 260);
|
||||||
|
} else {
|
||||||
|
// Normal door UAC symbol
|
||||||
|
ctx.fillStyle = '#888';
|
||||||
|
ctx.font = 'bold 16px monospace';
|
||||||
|
ctx.fillText('UAC', 112, 262);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createCeilingTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
ctx.fillStyle = '#222428';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Ceiling grid
|
||||||
|
ctx.strokeStyle = '#121316';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.strokeRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Fluorescent light strip
|
||||||
|
ctx.fillStyle = '#e8f4ff';
|
||||||
|
ctx.fillRect(40, 112, 176, 32);
|
||||||
|
ctx.strokeStyle = '#556677';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(40, 112, 176, 32);
|
||||||
|
|
||||||
|
// Light grill
|
||||||
|
ctx.strokeStyle = '#8899aa';
|
||||||
|
for (let x = 50; x < 210; x += 15) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 112);
|
||||||
|
ctx.lineTo(x, 144);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.wrapS = THREE.RepeatWrapping;
|
||||||
|
tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBarrelTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 256;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Toxic green barrel metal
|
||||||
|
ctx.fillStyle = '#3a6624';
|
||||||
|
ctx.fillRect(0, 0, 256, 256);
|
||||||
|
|
||||||
|
// Metal ribs
|
||||||
|
ctx.fillStyle = '#224412';
|
||||||
|
ctx.fillRect(0, 20, 256, 16);
|
||||||
|
ctx.fillRect(0, 120, 256, 16);
|
||||||
|
ctx.fillRect(0, 220, 256, 16);
|
||||||
|
|
||||||
|
// Biohazard / Radiation Symbol
|
||||||
|
ctx.fillStyle = '#ffff00';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(128, 70, 30, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(128, 70, 8, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const angle = (i * 2 * Math.PI) / 3;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(128, 70, 25, angle - 0.5, angle + 0.5);
|
||||||
|
ctx.lineTo(128, 70);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toxic Slime leak
|
||||||
|
ctx.fillStyle = '#88ff00';
|
||||||
|
ctx.fillRect(80, 140, 16, 80);
|
||||||
|
ctx.fillRect(160, 160, 22, 60);
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// RETRO DOOM SKYBOX
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
buildSkybox() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 1024;
|
||||||
|
c.height = 512;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
// Dark apocalyptic red/purple gradient
|
||||||
|
const grad = ctx.createLinearGradient(0, 0, 0, 512);
|
||||||
|
grad.addColorStop(0, '#100208');
|
||||||
|
grad.addColorStop(0.3, '#4a0814');
|
||||||
|
grad.addColorStop(0.65, '#9e1e12');
|
||||||
|
grad.addColorStop(0.85, '#e04a1b');
|
||||||
|
grad.addColorStop(1, '#ff8833');
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.fillRect(0, 0, 1024, 512);
|
||||||
|
|
||||||
|
// Stars
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
for (let i = 0; i < 150; i++) {
|
||||||
|
ctx.fillRect(Math.random() * 1024, Math.random() * 250, Math.random() * 2 + 1, Math.random() * 2 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Burning Hell Moon
|
||||||
|
const moonGrad = ctx.createRadialGradient(750, 140, 10, 750, 140, 60);
|
||||||
|
moonGrad.addColorStop(0, '#ffffff');
|
||||||
|
moonGrad.addColorStop(0.3, '#ffcc44');
|
||||||
|
moonGrad.addColorStop(0.7, '#ff3300');
|
||||||
|
moonGrad.addColorStop(1, 'transparent');
|
||||||
|
ctx.fillStyle = moonGrad;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(750, 140, 60, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Jagged Volcanic Mountains silhouette
|
||||||
|
ctx.fillStyle = '#1c0406';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 512);
|
||||||
|
let curY = 360;
|
||||||
|
for (let x = 0; x <= 1024; x += 32) {
|
||||||
|
curY = 320 + Math.sin(x * 0.02) * 60 + Math.cos(x * 0.05) * 40 + (Math.random() * 20 - 10);
|
||||||
|
ctx.lineTo(x, curY);
|
||||||
|
}
|
||||||
|
ctx.lineTo(1024, 512);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
const skyTexture = new THREE.CanvasTexture(c);
|
||||||
|
skyTexture.mapping = THREE.EquirectangularReflectionMapping;
|
||||||
|
|
||||||
|
// Big Dome Sphere
|
||||||
|
const skyGeo = new THREE.SphereGeometry(400, 32, 24);
|
||||||
|
const skyMat = new THREE.MeshBasicMaterial({
|
||||||
|
map: skyTexture,
|
||||||
|
side: THREE.BackSide,
|
||||||
|
depthWrite: false
|
||||||
|
});
|
||||||
|
const skyMesh = new THREE.Mesh(skyGeo, skyMat);
|
||||||
|
this.scene.add(skyMesh);
|
||||||
|
this.skyboxMesh = skyMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// PARTICLE & EXPLOSION ENGINE
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
spawnBloodBurst(pos, normal = new THREE.Vector3(0, 1, 0), count = 18, color = 0xcc0000) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const size = 0.08 + Math.random() * 0.12;
|
||||||
|
const geo = new THREE.BoxGeometry(size, size, size);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: color });
|
||||||
|
const p = new THREE.Mesh(geo, mat);
|
||||||
|
p.position.copy(pos);
|
||||||
|
|
||||||
|
const vel = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 6 + normal.x * 3,
|
||||||
|
Math.random() * 5 + 2,
|
||||||
|
(Math.random() - 0.5) * 6 + normal.z * 3
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scene.add(p);
|
||||||
|
this.particles.push({
|
||||||
|
mesh: p,
|
||||||
|
velocity: vel,
|
||||||
|
gravity: -16,
|
||||||
|
lifetime: 0.6 + Math.random() * 0.4,
|
||||||
|
age: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnSparks(pos, normal = new THREE.Vector3(0, 1, 0), count = 12) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const size = 0.04 + Math.random() * 0.05;
|
||||||
|
const geo = new THREE.BoxGeometry(size, size, size);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: 0xffcc33 });
|
||||||
|
const p = new THREE.Mesh(geo, mat);
|
||||||
|
p.position.copy(pos);
|
||||||
|
|
||||||
|
const vel = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 8 + normal.x * 4,
|
||||||
|
Math.random() * 6 + 1 + normal.y * 3,
|
||||||
|
(Math.random() - 0.5) * 8 + normal.z * 4
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scene.add(p);
|
||||||
|
this.particles.push({
|
||||||
|
mesh: p,
|
||||||
|
velocity: vel,
|
||||||
|
gravity: -12,
|
||||||
|
lifetime: 0.25 + Math.random() * 0.25,
|
||||||
|
age: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnExplosion(pos, radius = 3.5, color = 0xff6600) {
|
||||||
|
// 1. Expanding Core Sphere
|
||||||
|
const geo = new THREE.SphereGeometry(0.5, 12, 8);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.9 });
|
||||||
|
const core = new THREE.Mesh(geo, mat);
|
||||||
|
core.position.copy(pos);
|
||||||
|
this.scene.add(core);
|
||||||
|
|
||||||
|
// 2. Dynamic Explosion Flash Light
|
||||||
|
const expLight = new THREE.PointLight(color, 6.0, radius * 4, 1.5);
|
||||||
|
expLight.position.copy(pos);
|
||||||
|
this.scene.add(expLight);
|
||||||
|
|
||||||
|
this.particles.push({
|
||||||
|
mesh: core,
|
||||||
|
light: expLight,
|
||||||
|
isExplosionCore: true,
|
||||||
|
maxRadius: radius,
|
||||||
|
lifetime: 0.45,
|
||||||
|
age: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Smoke & Fire Debris
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const size = 0.2 + Math.random() * 0.35;
|
||||||
|
const dGeo = new THREE.BoxGeometry(size, size, size);
|
||||||
|
const dMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: Math.random() > 0.4 ? 0xff4400 : 0x444444,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.9
|
||||||
|
});
|
||||||
|
const d = new THREE.Mesh(dGeo, dMat);
|
||||||
|
d.position.copy(pos);
|
||||||
|
|
||||||
|
const vel = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 14,
|
||||||
|
Math.random() * 10 + 2,
|
||||||
|
(Math.random() - 0.5) * 14
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scene.add(d);
|
||||||
|
this.particles.push({
|
||||||
|
mesh: d,
|
||||||
|
velocity: vel,
|
||||||
|
gravity: -8,
|
||||||
|
lifetime: 0.7 + Math.random() * 0.5,
|
||||||
|
age: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnPlasmaImpact(pos, normal = new THREE.Vector3(0, 1, 0), color = 0x00d4ff) {
|
||||||
|
const light = new THREE.PointLight(color, 3.0, 8, 2);
|
||||||
|
light.position.copy(pos);
|
||||||
|
this.scene.add(light);
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const size = 0.08 + Math.random() * 0.08;
|
||||||
|
const geo = new THREE.BoxGeometry(size, size, size);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: color });
|
||||||
|
const p = new THREE.Mesh(geo, mat);
|
||||||
|
p.position.copy(pos);
|
||||||
|
|
||||||
|
const vel = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 7 + normal.x * 3,
|
||||||
|
Math.random() * 5 + 1,
|
||||||
|
(Math.random() - 0.5) * 7 + normal.z * 3
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scene.add(p);
|
||||||
|
this.particles.push({
|
||||||
|
mesh: p,
|
||||||
|
velocity: vel,
|
||||||
|
gravity: -5,
|
||||||
|
lifetime: 0.3 + Math.random() * 0.2,
|
||||||
|
age: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.scene.remove(light);
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// UPDATE GAME LOOP
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
// 1. Update animated textures (Toxic Slime)
|
||||||
|
for (let i = 0; i < this.animatedTextures.length; i++) {
|
||||||
|
this.animatedTextures[i].update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Flickering dynamic lights
|
||||||
|
const t = this.clock.getElapsedTime();
|
||||||
|
for (let i = 0; i < this.dynamicLights.length; i++) {
|
||||||
|
const dl = this.dynamicLights[i];
|
||||||
|
const noise = Math.sin(t * dl.flickerSpeed + dl.phase) * 0.35 + (Math.random() * 0.2 - 0.1);
|
||||||
|
dl.light.intensity = Math.max(0.2, dl.baseIntensity + noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Keep Skybox centered on player camera
|
||||||
|
if (this.skyboxMesh && this.camera) {
|
||||||
|
this.skyboxMesh.position.copy(this.camera.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update Particle physics & lifetimes
|
||||||
|
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||||
|
const p = this.particles[i];
|
||||||
|
p.age += delta;
|
||||||
|
|
||||||
|
if (p.age >= p.lifetime) {
|
||||||
|
this.scene.remove(p.mesh);
|
||||||
|
if (p.light) this.scene.remove(p.light);
|
||||||
|
this.particles.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = p.age / p.lifetime;
|
||||||
|
|
||||||
|
if (p.isExplosionCore) {
|
||||||
|
const scale = 1 + progress * (p.maxRadius * 2);
|
||||||
|
p.mesh.scale.set(scale, scale, scale);
|
||||||
|
p.mesh.material.opacity = 1.0 - progress;
|
||||||
|
if (p.light) {
|
||||||
|
p.light.intensity = 6.0 * (1.0 - progress);
|
||||||
|
}
|
||||||
|
} else if (p.velocity) {
|
||||||
|
p.velocity.y += p.gravity * delta;
|
||||||
|
p.mesh.position.addScaledVector(p.velocity, delta);
|
||||||
|
if (p.mesh.material.transparent) {
|
||||||
|
p.mesh.material.opacity = 1.0 - progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomEngine = DoomEngine;
|
||||||
Executable
+270
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Authentic Retro Status Bar & HUD
|
||||||
|
* LED segmented numbers, animated Doomguy face canvas reacting to HP/pain/godmode,
|
||||||
|
* weapon slot matrix, keycard inventory, ticker notifications, and damage screen flashes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomHUD {
|
||||||
|
constructor(player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
// Container elements
|
||||||
|
this.hudContainer = document.getElementById('doom-hud');
|
||||||
|
this.damageOverlay = document.getElementById('damage-flash');
|
||||||
|
this.ticker = document.getElementById('hud-ticker');
|
||||||
|
this.faceCanvas = document.getElementById('doomguy-face');
|
||||||
|
this.faceCtx = this.faceCanvas ? this.faceCanvas.getContext('2d') : null;
|
||||||
|
|
||||||
|
// Doomguy Face animation states
|
||||||
|
this.faceLookDir = 0; // -1 = left, 0 = forward, 1 = right
|
||||||
|
this.faceTimer = 0;
|
||||||
|
this.painTimer = 0;
|
||||||
|
this.grinTimer = 0;
|
||||||
|
|
||||||
|
// Notification ticker
|
||||||
|
this.tickerTimer = 0;
|
||||||
|
|
||||||
|
// Flash states
|
||||||
|
this.flashAlpha = 0;
|
||||||
|
this.flashColor = 'rgba(255, 0, 0, ';
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.faceCanvas) {
|
||||||
|
this.faceCanvas.width = 64;
|
||||||
|
this.faceCanvas.height = 72;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(text) {
|
||||||
|
if (!this.ticker) return;
|
||||||
|
this.ticker.innerText = text.toUpperCase();
|
||||||
|
this.ticker.style.opacity = '1';
|
||||||
|
this.tickerTimer = 3.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerDamageFlash(color = 'red') {
|
||||||
|
this.flashAlpha = 0.55;
|
||||||
|
this.painTimer = 0.5;
|
||||||
|
if (color === 'green') {
|
||||||
|
this.flashColor = 'rgba(50, 255, 0, ';
|
||||||
|
} else if (color === 'yellow') {
|
||||||
|
this.flashColor = 'rgba(255, 220, 0, ';
|
||||||
|
} else {
|
||||||
|
this.flashColor = 'rgba(220, 0, 0, ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerGrin() {
|
||||||
|
this.grinTimer = 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
showGameOver() {
|
||||||
|
const overlay = document.getElementById('game-over-modal');
|
||||||
|
if (overlay) overlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideGameOver() {
|
||||||
|
const overlay = document.getElementById('game-over-modal');
|
||||||
|
if (overlay) overlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// DOOMGUY FACE RENDERING (Dynamic Procedural Pixel Art)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
renderDoomguyFace(delta) {
|
||||||
|
if (!this.faceCtx) return;
|
||||||
|
const ctx = this.faceCtx;
|
||||||
|
ctx.clearRect(0, 0, 64, 72);
|
||||||
|
|
||||||
|
const hp = this.player.health;
|
||||||
|
const isDead = this.player.isDead;
|
||||||
|
const godMode = this.player.godMode;
|
||||||
|
|
||||||
|
// Update look direction timer
|
||||||
|
this.faceTimer += delta;
|
||||||
|
if (this.faceTimer > 1.8) {
|
||||||
|
this.faceTimer = 0;
|
||||||
|
const r = Math.random();
|
||||||
|
this.faceLookDir = r < 0.3 ? -1 : (r > 0.7 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.painTimer > 0) this.painTimer -= delta;
|
||||||
|
if (this.grinTimer > 0) this.grinTimer -= delta;
|
||||||
|
|
||||||
|
// 1. Background / Skin Base
|
||||||
|
let skinTone = '#d49b78';
|
||||||
|
if (hp <= 40) skinTone = '#c28766';
|
||||||
|
if (hp <= 20) skinTone = '#b07255';
|
||||||
|
if (isDead) skinTone = '#808a70'; // Pale zombie/corpse skin
|
||||||
|
|
||||||
|
// Jaw / Head Contour
|
||||||
|
ctx.fillStyle = skinTone;
|
||||||
|
ctx.fillRect(12, 10, 40, 52);
|
||||||
|
ctx.fillRect(16, 62, 32, 6);
|
||||||
|
|
||||||
|
// Brown Hair & Crewcut
|
||||||
|
ctx.fillStyle = '#4a2c18';
|
||||||
|
ctx.fillRect(10, 4, 44, 14);
|
||||||
|
ctx.fillRect(8, 12, 6, 20);
|
||||||
|
ctx.fillRect(50, 12, 6, 20);
|
||||||
|
|
||||||
|
if (isDead) {
|
||||||
|
// Dead Doomguy: X-eyes or blank white eyes + blood
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(18, 30, 10, 4);
|
||||||
|
ctx.fillRect(36, 30, 10, 4);
|
||||||
|
|
||||||
|
ctx.fillStyle = '#aa0000';
|
||||||
|
ctx.fillRect(26, 44, 12, 18);
|
||||||
|
ctx.fillRect(16, 52, 32, 6);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Eyes
|
||||||
|
const eyeOffset = this.faceLookDir * 2;
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(18, 28, 10, 8);
|
||||||
|
ctx.fillRect(36, 28, 10, 8);
|
||||||
|
|
||||||
|
// Pupils
|
||||||
|
if (godMode) {
|
||||||
|
// Glowing Yellow God Mode Eyes
|
||||||
|
ctx.fillStyle = '#ffff00';
|
||||||
|
ctx.fillRect(18, 28, 10, 8);
|
||||||
|
ctx.fillRect(36, 28, 10, 8);
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = '#3a6688'; // Blue eyes
|
||||||
|
ctx.fillRect(21 + eyeOffset, 30, 4, 4);
|
||||||
|
ctx.fillRect(39 + eyeOffset, 30, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eyebrows
|
||||||
|
ctx.fillStyle = '#3a2010';
|
||||||
|
if (this.painTimer > 0) {
|
||||||
|
// Pain / Anguished eyebrows
|
||||||
|
ctx.fillRect(16, 24, 12, 3);
|
||||||
|
ctx.fillRect(36, 24, 12, 3);
|
||||||
|
} else if (this.grinTimer > 0) {
|
||||||
|
// Evil smirk eyebrows
|
||||||
|
ctx.fillRect(16, 26, 12, 3);
|
||||||
|
ctx.fillRect(36, 23, 12, 3);
|
||||||
|
} else {
|
||||||
|
// Normal stern eyebrows
|
||||||
|
ctx.fillRect(16, 25, 12, 3);
|
||||||
|
ctx.fillRect(36, 25, 12, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Nose
|
||||||
|
ctx.fillStyle = '#a66e4e';
|
||||||
|
ctx.fillRect(29, 36, 6, 12);
|
||||||
|
if (hp <= 60) {
|
||||||
|
// Bloody nose
|
||||||
|
ctx.fillStyle = '#aa0000';
|
||||||
|
ctx.fillRect(29, 44, 6, 6);
|
||||||
|
ctx.fillRect(31, 50, 3, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Mouth / Expressions
|
||||||
|
if (this.painTimer > 0) {
|
||||||
|
// Ouch open screaming mouth
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(24, 52, 16, 10);
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(26, 52, 12, 3);
|
||||||
|
} else if (this.grinTimer > 0) {
|
||||||
|
// Evil grin
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(22, 53, 20, 6);
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(24, 54, 16, 3);
|
||||||
|
} else if (hp <= 30) {
|
||||||
|
// Bruised grimace
|
||||||
|
ctx.fillStyle = '#441111';
|
||||||
|
ctx.fillRect(24, 54, 16, 4);
|
||||||
|
} else {
|
||||||
|
// Classic Doomguy smirk
|
||||||
|
ctx.fillStyle = '#6b3e28';
|
||||||
|
ctx.fillRect(24, 54, 16, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Blood wounds for low health
|
||||||
|
if (hp <= 80) {
|
||||||
|
ctx.fillStyle = '#aa0000';
|
||||||
|
ctx.fillRect(44, 18, 4, 8); // Forehead cut
|
||||||
|
}
|
||||||
|
if (hp <= 40) {
|
||||||
|
ctx.fillStyle = '#aa0000';
|
||||||
|
ctx.fillRect(14, 38, 8, 10); // Cheek wound
|
||||||
|
ctx.fillRect(40, 56, 6, 8);
|
||||||
|
}
|
||||||
|
if (hp <= 20) {
|
||||||
|
ctx.fillStyle = '#880000';
|
||||||
|
ctx.fillRect(16, 28, 14, 10); // Black eye
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// UPDATE STATUS BAR METRICS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
// 1. Ticker fadeout
|
||||||
|
if (this.tickerTimer > 0) {
|
||||||
|
this.tickerTimer -= delta;
|
||||||
|
if (this.tickerTimer <= 0 && this.ticker) {
|
||||||
|
this.ticker.style.opacity = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Damage Screen Flash fadeout
|
||||||
|
if (this.flashAlpha > 0) {
|
||||||
|
this.flashAlpha = Math.max(0, this.flashAlpha - delta * 2.0);
|
||||||
|
if (this.damageOverlay) {
|
||||||
|
this.damageOverlay.style.backgroundColor = `${this.flashColor}${this.flashAlpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Render Animated Doomguy Face
|
||||||
|
this.renderDoomguyFace(delta);
|
||||||
|
|
||||||
|
// 4. Update HUD Numerical Values
|
||||||
|
const currentWeapon = (window.game && window.game.weapons) ? window.game.weapons.current : null;
|
||||||
|
const ammoCount = (currentWeapon && currentWeapon.ammoType) ? this.player.ammo[currentWeapon.ammoType] : '---';
|
||||||
|
|
||||||
|
const ammoElem = document.getElementById('stat-ammo');
|
||||||
|
const hpElem = document.getElementById('stat-health');
|
||||||
|
const armorElem = document.getElementById('stat-armor');
|
||||||
|
|
||||||
|
if (ammoElem) ammoElem.innerText = ammoCount;
|
||||||
|
if (hpElem) hpElem.innerText = `${Math.max(0, this.player.health)}%`;
|
||||||
|
if (armorElem) armorElem.innerText = `${this.player.armor}%`;
|
||||||
|
|
||||||
|
// 5. Update ARMS Numbers (1-7)
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
const weapons = window.game.weapons;
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const armSlot = document.getElementById(`arm-slot-${i + 1}`);
|
||||||
|
if (armSlot) {
|
||||||
|
const isUnlocked = weapons.unlocked[i];
|
||||||
|
const isCurrent = (weapons.currentIndex === i);
|
||||||
|
armSlot.className = `arm-num ${isUnlocked ? 'active' : 'inactive'} ${isCurrent ? 'selected' : ''}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Update KEYS Indicators (Blue, Red, Yellow)
|
||||||
|
const blueKeyElem = document.getElementById('key-blue');
|
||||||
|
const redKeyElem = document.getElementById('key-red');
|
||||||
|
const yellowKeyElem = document.getElementById('key-yellow');
|
||||||
|
|
||||||
|
if (blueKeyElem) blueKeyElem.className = `key-slot ${this.player.keysInventory.blue ? 'has-key' : ''}`;
|
||||||
|
if (redKeyElem) redKeyElem.className = `key-slot ${this.player.keysInventory.red ? 'has-key' : ''}`;
|
||||||
|
if (yellowKeyElem) yellowKeyElem.className = `key-slot ${this.player.keysInventory.yellow ? 'has-key' : ''}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomHUD = DoomHUD;
|
||||||
Executable
+335
@@ -0,0 +1,335 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Pickups, Keycards & Explosive Barrels System
|
||||||
|
* 3D rotating items, bobbing animations, and chain reaction explosive toxic barrels.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomItems {
|
||||||
|
constructor(engine, player, sound, map) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.player = player;
|
||||||
|
this.sound = sound;
|
||||||
|
this.map = map;
|
||||||
|
|
||||||
|
this.items = [];
|
||||||
|
this.barrels = [];
|
||||||
|
this.itemTextures = {};
|
||||||
|
|
||||||
|
this.generateItemTextures();
|
||||||
|
this.spawnMapItems();
|
||||||
|
this.spawnMapBarrels();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateItemTextures() {
|
||||||
|
this.itemTextures.medikit = this.createMedikitTexture();
|
||||||
|
this.itemTextures.armorGreen = this.createArmorTexture('#22aa22');
|
||||||
|
this.itemTextures.armorBlue = this.createArmorTexture('#0088ff');
|
||||||
|
this.itemTextures.ammoClip = this.createAmmoTexture('CLIP');
|
||||||
|
this.itemTextures.ammoShells = this.createAmmoTexture('SHELLS');
|
||||||
|
this.itemTextures.ammoRocket = this.createAmmoTexture('ROCKETS');
|
||||||
|
this.itemTextures.ammoCell = this.createAmmoTexture('CELLS');
|
||||||
|
}
|
||||||
|
|
||||||
|
createMedikitTexture() {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 64;
|
||||||
|
c.height = 64;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.fillStyle = '#eeeeee';
|
||||||
|
ctx.fillRect(0, 0, 64, 64);
|
||||||
|
ctx.fillStyle = '#cc0000';
|
||||||
|
// Red cross
|
||||||
|
ctx.fillRect(24, 10, 16, 44);
|
||||||
|
ctx.fillRect(10, 24, 44, 16);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createArmorTexture(color) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 64;
|
||||||
|
c.height = 64;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(8, 8, 48, 48);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(18, 18, 28, 28);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createAmmoTexture(label) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 64;
|
||||||
|
c.height = 64;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.fillStyle = '#3a3a30';
|
||||||
|
ctx.fillRect(0, 0, 64, 64);
|
||||||
|
ctx.fillStyle = '#ddaa00';
|
||||||
|
ctx.font = 'bold 12px monospace';
|
||||||
|
ctx.fillText(label, 6, 36);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// SPAWNING ITEMS & KEYCARDS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
spawnMapItems() {
|
||||||
|
const spawns = (this.map && this.map.spawns) ? this.map.spawns.items : (window.game && window.game.map ? window.game.map.spawns.items : []);
|
||||||
|
if (!spawns || spawns.length === 0) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < spawns.length; i++) {
|
||||||
|
const s = spawns[i];
|
||||||
|
this.spawnItem(s.type, s.x, s.y, s.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnItem(type, x, y, z) {
|
||||||
|
let mesh = null;
|
||||||
|
let light = null;
|
||||||
|
let scale = 1.0;
|
||||||
|
|
||||||
|
if (type.startsWith('key_')) {
|
||||||
|
// 3D Keycard with colored glowing point light
|
||||||
|
const colorName = type.replace('key_', '');
|
||||||
|
let colorHex = 0x0088ff;
|
||||||
|
if (colorName === 'red') colorHex = 0xff1122;
|
||||||
|
if (colorName === 'yellow') colorHex = 0xffbb00;
|
||||||
|
|
||||||
|
const geo = new THREE.BoxGeometry(0.8, 1.2, 0.15);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: colorHex,
|
||||||
|
emissive: colorHex,
|
||||||
|
emissiveIntensity: 0.8
|
||||||
|
});
|
||||||
|
mesh = new THREE.Mesh(geo, mat);
|
||||||
|
light = new THREE.PointLight(colorHex, 2.5, 6, 2);
|
||||||
|
light.position.set(x, y + 0.6, z);
|
||||||
|
this.engine.scene.add(light);
|
||||||
|
} else if (type === 'soulsphere') {
|
||||||
|
// Blue glowing soulsphere
|
||||||
|
const geo = new THREE.SphereGeometry(0.6, 16, 16);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x00d4ff,
|
||||||
|
emissive: 0x00d4ff,
|
||||||
|
emissiveIntensity: 0.9,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.85
|
||||||
|
});
|
||||||
|
mesh = new THREE.Mesh(geo, mat);
|
||||||
|
light = new THREE.PointLight(0x00d4ff, 3.5, 10, 2);
|
||||||
|
light.position.set(x, y + 0.6, z);
|
||||||
|
this.engine.scene.add(light);
|
||||||
|
} else if (type === 'medikit') {
|
||||||
|
const geo = new THREE.BoxGeometry(0.8, 0.6, 0.8);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({ map: this.itemTextures.medikit });
|
||||||
|
mesh = new THREE.Mesh(geo, mat);
|
||||||
|
} else if (type.startsWith('armor_')) {
|
||||||
|
const isBlue = type.includes('blue');
|
||||||
|
const geo = new THREE.BoxGeometry(0.9, 0.9, 0.9);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
map: isBlue ? this.itemTextures.armorBlue : this.itemTextures.armorGreen,
|
||||||
|
emissive: isBlue ? 0x0044ff : 0x008800,
|
||||||
|
emissiveIntensity: 0.3
|
||||||
|
});
|
||||||
|
mesh = new THREE.Mesh(geo, mat);
|
||||||
|
} else {
|
||||||
|
// Ammo / Weapon box
|
||||||
|
const geo = new THREE.BoxGeometry(0.7, 0.5, 0.7);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({ map: this.itemTextures.ammoClip });
|
||||||
|
mesh = new THREE.Mesh(geo, mat);
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.position.set(x, y + 0.4, z);
|
||||||
|
mesh.castShadow = true;
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
const itemObj = {
|
||||||
|
type,
|
||||||
|
mesh,
|
||||||
|
light,
|
||||||
|
baseY: y + 0.4,
|
||||||
|
rotationSpeed: 1.5,
|
||||||
|
bobTimer: Math.random() * 5,
|
||||||
|
isCollected: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.items.push(itemObj);
|
||||||
|
return itemObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// EXPLOSIVE TOXIC BARRELS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
spawnMapBarrels() {
|
||||||
|
const spawns = (this.map && this.map.spawns) ? this.map.spawns.barrels : (window.game && window.game.map ? window.game.map.spawns.barrels : []);
|
||||||
|
if (!spawns || spawns.length === 0) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < spawns.length; i++) {
|
||||||
|
const s = spawns[i];
|
||||||
|
this.spawnBarrel(s.x, s.y, s.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnBarrel(x, y, z) {
|
||||||
|
const geo = new THREE.CylinderGeometry(0.55, 0.55, 1.4, 16);
|
||||||
|
const mat = this.engine.materials.barrel;
|
||||||
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
|
mesh.position.set(x, y + 0.7, z);
|
||||||
|
mesh.castShadow = true;
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
// Add to map colliders
|
||||||
|
const min = new THREE.Vector3(x - 0.55, y, z - 0.55);
|
||||||
|
const max = new THREE.Vector3(x + 0.55, y + 1.4, z + 0.55);
|
||||||
|
const box = new THREE.Box3(min, max);
|
||||||
|
this.engine.scene.colliders = this.engine.scene.colliders || [];
|
||||||
|
|
||||||
|
const barrelObj = {
|
||||||
|
mesh,
|
||||||
|
box,
|
||||||
|
hp: 20,
|
||||||
|
isExploded: false,
|
||||||
|
takeDamage: (dmg) => this.damageBarrel(barrelObj, dmg)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.barrels.push(barrelObj);
|
||||||
|
return barrelObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
damageBarrel(b, amount) {
|
||||||
|
if (b.isExploded) return;
|
||||||
|
b.hp -= amount;
|
||||||
|
if (b.hp <= 0) {
|
||||||
|
this.explodeBarrel(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
explodeBarrel(b) {
|
||||||
|
b.isExploded = true;
|
||||||
|
const pos = b.mesh.position;
|
||||||
|
|
||||||
|
this.sound.playExplosion(1.5);
|
||||||
|
this.engine.spawnExplosion(pos, 5.5, 0x88ff00); // Toxic radioactive green blast!
|
||||||
|
|
||||||
|
// Remove mesh
|
||||||
|
this.engine.scene.remove(b.mesh);
|
||||||
|
|
||||||
|
// Area Damage & Chain Reactions
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
window.game.weapons.applySplashDamage(pos, 180, 5.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// ITEM COLLECTION & UPDATE
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
const playerPos = this.player.position;
|
||||||
|
|
||||||
|
for (let i = this.items.length - 1; i >= 0; i--) {
|
||||||
|
const item = this.items[i];
|
||||||
|
if (item.isCollected) continue;
|
||||||
|
|
||||||
|
// Rotation & Bobbing Animation
|
||||||
|
item.bobTimer += delta * 3.0;
|
||||||
|
item.mesh.rotation.y += item.rotationSpeed * delta;
|
||||||
|
item.mesh.position.y = item.baseY + Math.sin(item.bobTimer) * 0.15;
|
||||||
|
if (item.light) {
|
||||||
|
item.light.position.copy(item.mesh.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player proximity pickup check
|
||||||
|
const dist = item.mesh.position.distanceTo(playerPos);
|
||||||
|
if (dist < 1.6) {
|
||||||
|
this.collectItem(item, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectItem(item, index) {
|
||||||
|
let collected = false;
|
||||||
|
const type = item.type;
|
||||||
|
|
||||||
|
if (type === 'medikit') {
|
||||||
|
collected = this.player.heal(25);
|
||||||
|
if (collected) this.sound.playItemPickup('health');
|
||||||
|
} else if (type === 'soulsphere') {
|
||||||
|
collected = this.player.heal(100, true);
|
||||||
|
if (collected) {
|
||||||
|
this.sound.playItemPickup('weapon');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification('SOULSPHERE! +100 HP');
|
||||||
|
}
|
||||||
|
} else if (type === 'armor_green') {
|
||||||
|
collected = this.player.addArmor(100);
|
||||||
|
if (collected) this.sound.playItemPickup('armor');
|
||||||
|
} else if (type === 'armor_blue') {
|
||||||
|
collected = this.player.addArmor(200, true);
|
||||||
|
if (collected) this.sound.playItemPickup('armor');
|
||||||
|
} else if (type === 'ammo_clip') {
|
||||||
|
collected = this.player.addAmmo('bullets', 10);
|
||||||
|
if (collected) this.sound.playItemPickup('ammo');
|
||||||
|
} else if (type === 'ammo_box') {
|
||||||
|
collected = this.player.addAmmo('bullets', 50);
|
||||||
|
if (collected) this.sound.playItemPickup('ammo');
|
||||||
|
} else if (type === 'ammo_shells') {
|
||||||
|
collected = this.player.addAmmo('shells', 8);
|
||||||
|
if (collected) this.sound.playItemPickup('ammo');
|
||||||
|
} else if (type === 'ammo_rocket') {
|
||||||
|
collected = this.player.addAmmo('rockets', 5);
|
||||||
|
if (collected) this.sound.playItemPickup('ammo');
|
||||||
|
} else if (type === 'ammo_cell') {
|
||||||
|
collected = this.player.addAmmo('cells', 40);
|
||||||
|
if (collected) this.sound.playItemPickup('ammo');
|
||||||
|
} else if (type === 'shotgun') {
|
||||||
|
this.player.addAmmo('shells', 8);
|
||||||
|
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(2);
|
||||||
|
collected = true;
|
||||||
|
this.sound.playItemPickup('weapon');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE SHOTGUN!');
|
||||||
|
} else if (type === 'chaingun') {
|
||||||
|
this.player.addAmmo('bullets', 40);
|
||||||
|
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(3);
|
||||||
|
collected = true;
|
||||||
|
this.sound.playItemPickup('weapon');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE CHAINGUN!');
|
||||||
|
} else if (type === 'rocket_launcher') {
|
||||||
|
this.player.addAmmo('rockets', 5);
|
||||||
|
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(4);
|
||||||
|
collected = true;
|
||||||
|
this.sound.playItemPickup('weapon');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE ROCKET LAUNCHER!');
|
||||||
|
} else if (type === 'plasma') {
|
||||||
|
this.player.addAmmo('cells', 50);
|
||||||
|
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(5);
|
||||||
|
collected = true;
|
||||||
|
this.sound.playItemPickup('weapon');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE PLASMA RIFLE!');
|
||||||
|
} else if (type === 'bfg') {
|
||||||
|
this.player.addAmmo('cells', 100);
|
||||||
|
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(6);
|
||||||
|
collected = true;
|
||||||
|
this.sound.playItemPickup('weapon');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE BFG 9000! OH YES.');
|
||||||
|
} else if (type.startsWith('key_')) {
|
||||||
|
const keyColor = type.replace('key_', '');
|
||||||
|
this.player.keysInventory[keyColor] = true;
|
||||||
|
collected = true;
|
||||||
|
this.sound.playItemPickup('key');
|
||||||
|
if (window.game && window.game.hud) window.game.hud.showNotification(`PICKED UP THE ${keyColor.toUpperCase()} KEYCARD`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collected) {
|
||||||
|
item.isCollected = true;
|
||||||
|
this.engine.scene.remove(item.mesh);
|
||||||
|
if (item.light) this.engine.scene.remove(item.light);
|
||||||
|
this.items.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomItems = DoomItems;
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2010-2021 Three.js Authors
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="128",n=100,i=300,r=301,s=302,a=303,o=304,l=306,c=307,h=1e3,u=1001,d=1002,p=1003,m=1004,f=1005,g=1006,v=1007,y=1008,x=1009,_=1012,w=1014,b=1015,M=1016,S=1020,T=1022,E=1023,A=1026,L=1027,R=33776,C=33777,P=33778,D=33779,I=35840,N=35841,B=35842,z=35843,F=37492,O=37496,H=2300,G=2301,U=2302,k=2400,V=2401,W=2402,j=2500,q=2501,X=3e3,Y=3001,Z=3007,J=3002,Q=3004,K=3005,$=3006,tt=7680,et=35044,nt=35048,it="300 es";class rt{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e<i;e++)n[e].call(this,t);t.target=null}}}const st=[];for(let t=0;t<256;t++)st[t]=(t<16?"0":"")+t.toString(16);let at=1234567;const ot=Math.PI/180,lt=180/Math.PI;function ct(){const t=4294967295*Math.random()|0,e=4294967295*Math.random()|0,n=4294967295*Math.random()|0,i=4294967295*Math.random()|0;return(st[255&t]+st[t>>8&255]+st[t>>16&255]+st[t>>24&255]+"-"+st[255&e]+st[e>>8&255]+"-"+st[e>>16&15|64]+st[e>>24&255]+"-"+st[63&n|128]+st[n>>8&255]+"-"+st[n>>16&255]+st[n>>24&255]+st[255&i]+st[i>>8&255]+st[i>>16&255]+st[i>>24&255]).toUpperCase()}function ht(t,e,n){return Math.max(e,Math.min(n,t))}function ut(t,e){return(t%e+e)%e}function dt(t,e,n){return(1-n)*t+n*e}function pt(t){return 0==(t&t-1)&&0!==t}function mt(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))}function ft(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}var gt=Object.freeze({__proto__:null,DEG2RAD:ot,RAD2DEG:lt,generateUUID:ct,clamp:ht,euclideanModulo:ut,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:dt,damp:function(t,e,n,i){return dt(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(ut(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){return void 0!==t&&(at=t%2147483647),at=16807*at%2147483647,(at-1)/2147483646},degToRad:function(t){return t*ot},radToDeg:function(t){return t*lt},isPowerOfTwo:pt,ceilPowerOfTwo:mt,floorPowerOfTwo:ft,setQuaternionFromProperEuler:function(t,e,n,i,r){const s=Math.cos,a=Math.sin,o=s(n/2),l=a(n/2),c=s((e+i)/2),h=a((e+i)/2),u=s((e-i)/2),d=a((e-i)/2),p=s((i-e)/2),m=a((i-e)/2);switch(r){case"XYX":t.set(o*h,l*u,l*d,o*c);break;case"YZY":t.set(l*d,o*h,l*u,o*c);break;case"ZXZ":t.set(l*u,l*d,o*h,o*c);break;case"XZX":t.set(o*h,l*m,l*p,o*c);break;case"YXY":t.set(l*p,o*h,l*m,o*c);break;case"ZYZ":t.set(l*m,l*p,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}}});class vt{constructor(t=0,e=0){this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t,e){return void 0!==e?(console.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this)}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t,e){return void 0!==e?(console.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this)}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=tLine truncated
|
||||||
Executable
+179
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Master Game Manager & Application Entry Point
|
||||||
|
* Orchestrates loop, state transitions, settings, audio sync, and rendering.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomGame {
|
||||||
|
constructor() {
|
||||||
|
this.container = document.getElementById('game-container');
|
||||||
|
this.state = 'start'; // 'start', 'playing', 'paused', 'gameover'
|
||||||
|
|
||||||
|
this.sound = new SoundSystem();
|
||||||
|
this.engine = new DoomEngine(this.container);
|
||||||
|
this.map = new DoomMap(this.engine, this.sound);
|
||||||
|
this.player = new DoomPlayer(this.engine, this.sound);
|
||||||
|
this.weapons = new DoomWeapons(this.engine, this.player, this.sound);
|
||||||
|
this.enemies = new DoomEnemies(this.engine, this.player, this.sound, this.map);
|
||||||
|
this.items = new DoomItems(this.engine, this.player, this.sound, this.map);
|
||||||
|
this.hud = new DoomHUD(this.player);
|
||||||
|
this.mobile = new DoomMobileControls(this.player);
|
||||||
|
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
this.crtFilterEnabled = true;
|
||||||
|
|
||||||
|
this.setupUI();
|
||||||
|
this.loop = this.loop.bind(this);
|
||||||
|
requestAnimationFrame(this.loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupUI() {
|
||||||
|
// Start Game Button
|
||||||
|
const startBtn = document.getElementById('btn-start-game');
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
this.startGame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart Game Button
|
||||||
|
const restartBtn = document.getElementById('btn-restart');
|
||||||
|
if (restartBtn) {
|
||||||
|
restartBtn.addEventListener('click', () => {
|
||||||
|
this.restartGame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause Menu Resume Button
|
||||||
|
const resumeBtn = document.getElementById('btn-resume');
|
||||||
|
if (resumeBtn) {
|
||||||
|
resumeBtn.addEventListener('click', () => {
|
||||||
|
this.resumeGame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings Modal Open/Close
|
||||||
|
const settingsBtn = document.getElementById('btn-settings');
|
||||||
|
const settingsModal = document.getElementById('settings-modal');
|
||||||
|
const closeSettingsBtn = document.getElementById('btn-close-settings');
|
||||||
|
|
||||||
|
if (settingsBtn && settingsModal) {
|
||||||
|
settingsBtn.addEventListener('click', () => {
|
||||||
|
settingsModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (closeSettingsBtn && settingsModal) {
|
||||||
|
closeSettingsBtn.addEventListener('click', () => {
|
||||||
|
settingsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings Sliders
|
||||||
|
const sfxSlider = document.getElementById('slider-sfx');
|
||||||
|
const musicSlider = document.getElementById('slider-music');
|
||||||
|
const sensSlider = document.getElementById('slider-sens');
|
||||||
|
const crtCheck = document.getElementById('toggle-crt');
|
||||||
|
|
||||||
|
if (sfxSlider) {
|
||||||
|
sfxSlider.addEventListener('input', (e) => {
|
||||||
|
this.sound.setSfxVolume(parseFloat(e.target.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (musicSlider) {
|
||||||
|
musicSlider.addEventListener('input', (e) => {
|
||||||
|
this.sound.setMusicVolume(parseFloat(e.target.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sensSlider) {
|
||||||
|
sensSlider.addEventListener('input', (e) => {
|
||||||
|
this.player.mouseSensitivity = parseFloat(e.target.value) * 0.002;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (crtCheck) {
|
||||||
|
crtCheck.addEventListener('change', (e) => {
|
||||||
|
this.toggleCRTFilter(e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC key to toggle pause
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.code === 'Escape') {
|
||||||
|
if (this.state === 'playing') {
|
||||||
|
this.pauseGame();
|
||||||
|
} else if (this.state === 'paused') {
|
||||||
|
this.resumeGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startGame() {
|
||||||
|
this.state = 'playing';
|
||||||
|
const startOverlay = document.getElementById('start-screen');
|
||||||
|
if (startOverlay) startOverlay.classList.add('hidden');
|
||||||
|
|
||||||
|
this.sound.init();
|
||||||
|
this.player.requestPointerLock();
|
||||||
|
this.hud.showNotification('ENTER THE TECH BASE. RIP AND TEAR!');
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseGame() {
|
||||||
|
this.state = 'paused';
|
||||||
|
this.player.exitPointerLock();
|
||||||
|
const pauseOverlay = document.getElementById('pause-menu');
|
||||||
|
if (pauseOverlay) pauseOverlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeGame() {
|
||||||
|
this.state = 'playing';
|
||||||
|
const pauseOverlay = document.getElementById('pause-menu');
|
||||||
|
if (pauseOverlay) pauseOverlay.classList.add('hidden');
|
||||||
|
this.player.requestPointerLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
restartGame() {
|
||||||
|
this.hud.hideGameOver();
|
||||||
|
this.player.respawn(this.map.spawns.player);
|
||||||
|
this.state = 'playing';
|
||||||
|
this.player.requestPointerLock();
|
||||||
|
this.hud.showNotification('RESPAWNED! GET READY TO FIGHT.');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCRTFilter(enabled) {
|
||||||
|
this.crtFilterEnabled = enabled;
|
||||||
|
const scanlineElem = document.getElementById('crt-scanlines');
|
||||||
|
if (scanlineElem) {
|
||||||
|
if (enabled) scanlineElem.classList.remove('hidden');
|
||||||
|
else scanlineElem.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// MAIN GAME LOOP
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
loop(currentTime) {
|
||||||
|
requestAnimationFrame(this.loop);
|
||||||
|
|
||||||
|
const delta = Math.min(0.1, (currentTime - this.lastTime) / 1000.0);
|
||||||
|
this.lastTime = currentTime;
|
||||||
|
|
||||||
|
if (this.state === 'playing') {
|
||||||
|
// Update all game subsystems
|
||||||
|
this.player.update(delta);
|
||||||
|
this.map.update(delta);
|
||||||
|
this.weapons.update(delta);
|
||||||
|
this.enemies.update(delta);
|
||||||
|
this.items.update(delta);
|
||||||
|
this.engine.update(delta);
|
||||||
|
this.hud.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always render 3D Scene
|
||||||
|
this.engine.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global bootstrap
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.game = new DoomGame();
|
||||||
|
});
|
||||||
Executable
+458
@@ -0,0 +1,458 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Multi-Level 3D Map & Architecture
|
||||||
|
* Multi-story buildings, stairs, ramps, catwalks, toxic slime pits,
|
||||||
|
* elevators, flickering lights, and keycard-locked blast doors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomMap {
|
||||||
|
constructor(engine, sound) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.sound = sound;
|
||||||
|
|
||||||
|
// Level Geometry Colliders
|
||||||
|
this.colliders = [];
|
||||||
|
this.slimeZones = [];
|
||||||
|
this.doors = [];
|
||||||
|
this.elevators = [];
|
||||||
|
this.spawns = {
|
||||||
|
player: new THREE.Vector3(0, 0, 0),
|
||||||
|
enemies: [],
|
||||||
|
items: [],
|
||||||
|
barrels: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.buildMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
getColliders() {
|
||||||
|
return this.colliders;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInSlimePit(pos) {
|
||||||
|
for (let i = 0; i < this.slimeZones.length; i++) {
|
||||||
|
const sz = this.slimeZones[i];
|
||||||
|
if (pos.x >= sz.min.x && pos.x <= sz.max.x &&
|
||||||
|
pos.z >= sz.min.z && pos.z <= sz.max.z &&
|
||||||
|
pos.y >= sz.min.y - 0.5 && pos.y <= sz.max.y + 1.5) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to add Box Mesh + Collider Box
|
||||||
|
addBlock(x, y, z, w, h, d, material, castShadow = true, receiveShadow = true) {
|
||||||
|
const geo = new THREE.BoxGeometry(w, h, d);
|
||||||
|
const mesh = new THREE.Mesh(geo, material);
|
||||||
|
mesh.position.set(x, y + h / 2, z);
|
||||||
|
mesh.castShadow = castShadow;
|
||||||
|
mesh.receiveShadow = receiveShadow;
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
// AABB collider
|
||||||
|
const min = new THREE.Vector3(x - w / 2, y, z - d / 2);
|
||||||
|
const max = new THREE.Vector3(x + w / 2, y + h, z + d / 2);
|
||||||
|
const box = new THREE.Box3(min, max);
|
||||||
|
this.colliders.push(box);
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for Stairs (multiple small step boxes)
|
||||||
|
addStaircase(startX, startY, startZ, width, totalHeight, depth, numSteps, dir = 'z') {
|
||||||
|
const stepH = totalHeight / numSteps;
|
||||||
|
const stepD = depth / numSteps;
|
||||||
|
|
||||||
|
for (let i = 0; i < numSteps; i++) {
|
||||||
|
const h = stepH * (i + 1);
|
||||||
|
let curX = startX;
|
||||||
|
let curY = startY;
|
||||||
|
let curZ = startZ;
|
||||||
|
|
||||||
|
if (dir === 'z') {
|
||||||
|
curZ = startZ + (i * stepD);
|
||||||
|
this.addBlock(curX, curY, curZ, width, h, stepD + 0.1, this.engine.materials.metal);
|
||||||
|
} else if (dir === '-z') {
|
||||||
|
curZ = startZ - (i * stepD);
|
||||||
|
this.addBlock(curX, curY, curZ, width, h, stepD + 0.1, this.engine.materials.metal);
|
||||||
|
} else if (dir === 'x') {
|
||||||
|
curX = startX + (i * stepD);
|
||||||
|
this.addBlock(curX, curY, curZ, stepD + 0.1, h, width, this.engine.materials.metal);
|
||||||
|
} else if (dir === '-x') {
|
||||||
|
curX = startX - (i * stepD);
|
||||||
|
this.addBlock(curX, curY, curZ, stepD + 0.1, h, width, this.engine.materials.metal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// LEVEL CONSTRUCTION (Inspired by E1M1 & E1M2)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
buildMap() {
|
||||||
|
const mat = this.engine.materials;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. STARTING ROOM (Hangar / Briefing Room)
|
||||||
|
// ==========================================
|
||||||
|
// Floor
|
||||||
|
this.addBlock(0, -1, 0, 24, 1, 24, mat.metal);
|
||||||
|
// Ceiling
|
||||||
|
this.addBlock(0, 7, 0, 24, 1, 24, mat.ceiling);
|
||||||
|
// North Wall (with door opening in middle)
|
||||||
|
this.addBlock(-8, 0, -12, 8, 7, 2, mat.tech);
|
||||||
|
this.addBlock(8, 0, -12, 8, 7, 2, mat.tech);
|
||||||
|
this.addBlock(0, 5, -12, 8, 2, 2, mat.tech); // door lintel
|
||||||
|
// South Wall
|
||||||
|
this.addBlock(0, 0, 12, 24, 7, 2, mat.brick);
|
||||||
|
// West Wall
|
||||||
|
this.addBlock(-12, 0, 0, 2, 7, 24, mat.tech);
|
||||||
|
// East Wall
|
||||||
|
this.addBlock(12, 0, 0, 2, 7, 24, mat.metal);
|
||||||
|
|
||||||
|
// Starting Room Pillars & Consoles
|
||||||
|
this.addBlock(-5, 0, -4, 2, 7, 2, mat.hex);
|
||||||
|
this.addBlock(5, 0, -4, 2, 7, 2, mat.hex);
|
||||||
|
this.addBlock(0, 0, 6, 6, 1.4, 2, mat.tech);
|
||||||
|
|
||||||
|
// Flickering light in start room
|
||||||
|
this.engine.addFlickeringLight(0, 5.5, 0, 0xffeedd, 2.2, 18);
|
||||||
|
|
||||||
|
// Spawn points in start room
|
||||||
|
this.spawns.player.set(0, 0, 6);
|
||||||
|
this.spawns.items.push({ type: 'medikit', x: -9, y: 0, z: -8 });
|
||||||
|
this.spawns.items.push({ type: 'armor_green', x: 9, y: 0, z: -8 });
|
||||||
|
this.spawns.items.push({ type: 'shotgun', x: 0, y: 0, z: 2 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_shells', x: -2, y: 0, z: 2 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_shells', x: 2, y: 0, z: 2 });
|
||||||
|
|
||||||
|
// Door 1: Start Room to Main Corridor (Sliding blast door)
|
||||||
|
this.createDoor(0, 0, -12, 7.8, 5, 1.2, 'normal', null);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. CENTRAL CORRIDOR & CROSSROAD
|
||||||
|
// ==========================================
|
||||||
|
// Floor & Ceiling
|
||||||
|
this.addBlock(0, -1, -26, 12, 1, 26, mat.grate);
|
||||||
|
this.addBlock(0, 7, -26, 12, 1, 26, mat.ceiling);
|
||||||
|
// Walls
|
||||||
|
this.addBlock(-6, 0, -26, 1, 7, 26, mat.metal);
|
||||||
|
this.addBlock(6, 0, -26, 1, 7, 26, mat.metal);
|
||||||
|
this.engine.addFlickeringLight(0, 5.5, -24, 0xff8833, 2.0, 16);
|
||||||
|
|
||||||
|
this.spawns.enemies.push({ type: 'zombie', x: -2, y: 0, z: -20 });
|
||||||
|
this.spawns.enemies.push({ type: 'zombie', x: 2, y: 0, z: -24 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_clip', x: 4, y: 0, z: -28 });
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. TOXIC SLIME ARENA & COURTYARD (Outdoor)
|
||||||
|
// ==========================================
|
||||||
|
// Floor around slime
|
||||||
|
this.addBlock(-25, -1, -65, 60, 1, 50, mat.brick);
|
||||||
|
// Toxic Slime Basin (recessed pool in middle)
|
||||||
|
this.addBlock(-25, -2.2, -65, 34, 1, 26, mat.slime);
|
||||||
|
// Slime Hazard Zone definition
|
||||||
|
this.slimeZones.push(new THREE.Box3(
|
||||||
|
new THREE.Vector3(-42, -2.5, -78),
|
||||||
|
new THREE.Vector3(-8, 0.2, -52)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Stepping stone platforms across the slime river
|
||||||
|
this.addBlock(-32, -0.8, -65, 4, 1.2, 4, mat.hex);
|
||||||
|
this.addBlock(-25, -0.8, -65, 4, 1.2, 4, mat.hex);
|
||||||
|
this.addBlock(-18, -0.8, -65, 4, 1.2, 4, mat.hex);
|
||||||
|
|
||||||
|
// Slime pit lighting
|
||||||
|
this.engine.addFlickeringLight(-25, 2.0, -65, 0x33ff22, 3.5, 30);
|
||||||
|
|
||||||
|
// Courtyard Perimeter Walls
|
||||||
|
this.addBlock(-55, 0, -65, 2, 12, 50, mat.brick); // West high wall
|
||||||
|
this.addBlock(5, 0, -65, 2, 12, 50, mat.brick); // East high wall
|
||||||
|
this.addBlock(-25, 0, -90, 60, 12, 2, mat.brick); // North high wall
|
||||||
|
|
||||||
|
// Blue Keycard Island on the slime pit!
|
||||||
|
this.spawns.items.push({ type: 'key_blue', x: -25, y: 0.6, z: -65 });
|
||||||
|
this.spawns.enemies.push({ type: 'imp', x: -35, y: 0, z: -80 });
|
||||||
|
this.spawns.enemies.push({ type: 'imp', x: -15, y: 0, z: -80 });
|
||||||
|
this.spawns.enemies.push({ type: 'shotgunner', x: -25, y: 0, z: -84 });
|
||||||
|
this.spawns.barrels.push({ x: -10, y: 0, z: -75 });
|
||||||
|
this.spawns.barrels.push({ x: -8, y: 0, z: -75 });
|
||||||
|
this.spawns.barrels.push({ x: -38, y: 0, z: -75 });
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. MULTI-STORY TECH TOWER & CATWALKS (Level 2 & 3)
|
||||||
|
// ==========================================
|
||||||
|
// Staircase leading from Courtyard (y=0) to 2nd Floor Catwalk (y=6)
|
||||||
|
this.addStaircase(-2, 0, -45, 6, 6, 16, 15, '-z');
|
||||||
|
|
||||||
|
// 2nd Floor Catwalk (y=6)
|
||||||
|
this.addBlock(20, 5, -65, 28, 1, 50, mat.grate);
|
||||||
|
this.addBlock(20, 15, -65, 28, 1, 50, mat.ceiling);
|
||||||
|
// Catwalk Railing/Balcony facing courtyard (with opening for elevator at z=-45)
|
||||||
|
this.addBlock(6, 6, -72, 1, 1.8, 34, mat.metal);
|
||||||
|
this.addBlock(6, 6, -37, 1, 1.8, 16, mat.metal);
|
||||||
|
// East Wall
|
||||||
|
this.addBlock(34, 6, -65, 2, 9, 50, mat.tech);
|
||||||
|
// North Wall
|
||||||
|
this.addBlock(20, 6, -90, 30, 9, 2, mat.tech);
|
||||||
|
|
||||||
|
// Catwalk Monsters & Items
|
||||||
|
this.spawns.enemies.push({ type: 'shotgunner', x: 18, y: 6, z: -55 });
|
||||||
|
this.spawns.enemies.push({ type: 'pinky', x: 22, y: 6, z: -75 });
|
||||||
|
this.spawns.items.push({ type: 'chaingun', x: 28, y: 6, z: -80 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_box', x: 28, y: 6, z: -76 });
|
||||||
|
|
||||||
|
// Catwalk Light
|
||||||
|
this.engine.addFlickeringLight(20, 11, -65, 0x4488ff, 2.8, 22);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. BLUE KEYCARD ARMORED VAULT (Secret Armory)
|
||||||
|
// ==========================================
|
||||||
|
// Door at North of Catwalk (x=20, z=-90)
|
||||||
|
this.createDoor(20, 6, -90, 8, 6, 1.2, 'blue', 'blue');
|
||||||
|
|
||||||
|
// Secret Armory Room behind Blue Door
|
||||||
|
this.addBlock(20, 5, -105, 20, 1, 28, mat.hex);
|
||||||
|
this.addBlock(20, 15, -105, 20, 1, 28, mat.ceiling);
|
||||||
|
this.addBlock(10, 6, -105, 1, 9, 28, mat.tech);
|
||||||
|
this.addBlock(30, 6, -105, 1, 9, 28, mat.tech);
|
||||||
|
this.addBlock(20, 6, -119, 20, 9, 2, mat.tech);
|
||||||
|
|
||||||
|
// Armory Rewards
|
||||||
|
this.spawns.items.push({ type: 'plasma', x: 20, y: 6, z: -110 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_cell', x: 16, y: 6, z: -110 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_cell', x: 24, y: 6, z: -110 });
|
||||||
|
this.spawns.items.push({ type: 'soulsphere', x: 20, y: 6, z: -115 });
|
||||||
|
this.spawns.items.push({ type: 'key_red', x: 20, y: 6, z: -105 }); // Red Keycard inside armory!
|
||||||
|
|
||||||
|
this.engine.addFlickeringLight(20, 12, -105, 0x00aaff, 3.2, 18);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 6. NUCLEAR REACTOR CHAMBER (Guarded by Red Door)
|
||||||
|
// ==========================================
|
||||||
|
// Door leading from West Courtyard (x=-55, z=-65)
|
||||||
|
this.createDoor(-55, 0, -65, 1.2, 6, 8, 'red', 'red');
|
||||||
|
|
||||||
|
// Nuclear Reactor Room
|
||||||
|
this.addBlock(-80, -1, -65, 48, 1, 48, mat.metal);
|
||||||
|
this.addBlock(-80, 16, -65, 48, 1, 48, mat.ceiling);
|
||||||
|
this.addBlock(-104, 0, -65, 2, 16, 48, mat.brick);
|
||||||
|
this.addBlock(-80, 0, -89, 48, 16, 2, mat.brick);
|
||||||
|
this.addBlock(-80, 0, -41, 48, 16, 2, mat.brick);
|
||||||
|
|
||||||
|
// Central Glowing Reactor Core
|
||||||
|
this.addBlock(-80, 0, -65, 10, 12, 10, mat.slime);
|
||||||
|
this.slimeZones.push(new THREE.Box3(
|
||||||
|
new THREE.Vector3(-86, -1, -71),
|
||||||
|
new THREE.Vector3(-74, 1, -59)
|
||||||
|
));
|
||||||
|
this.engine.addFlickeringLight(-80, 7, -65, 0x00ff22, 4.5, 35);
|
||||||
|
|
||||||
|
// Reactor Boss Monsters & Flying Cacodemons!
|
||||||
|
this.spawns.enemies.push({ type: 'caco', x: -75, y: 5, z: -60 });
|
||||||
|
this.spawns.enemies.push({ type: 'caco', x: -85, y: 7, z: -70 });
|
||||||
|
this.spawns.enemies.push({ type: 'pinky', x: -90, y: 0, z: -65 });
|
||||||
|
this.spawns.enemies.push({ type: 'shotgunner', x: -70, y: 0, z: -50 });
|
||||||
|
|
||||||
|
// Reactor Loot & Yellow Keycard
|
||||||
|
this.spawns.items.push({ type: 'rocket_launcher', x: -92, y: 0, z: -65 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_rocket', x: -92, y: 0, z: -61 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_rocket', x: -92, y: 0, z: -69 });
|
||||||
|
this.spawns.items.push({ type: 'armor_blue', x: -80, y: 0, z: -80 });
|
||||||
|
this.spawns.items.push({ type: 'key_yellow', x: -80, y: 0, z: -50 });
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 7. FINAL BOSS ARENA / EXIT VAULT (Yellow Door)
|
||||||
|
// ==========================================
|
||||||
|
// Yellow Blast Door at North of Reactor room (x=-80, z=-89)
|
||||||
|
this.createDoor(-80, 0, -89, 8, 6, 1.2, 'yellow', 'yellow');
|
||||||
|
|
||||||
|
// BFG Sanctum & Exit Chamber
|
||||||
|
this.addBlock(-80, -1, -115, 32, 1, 48, mat.brick);
|
||||||
|
this.addBlock(-80, 18, -115, 32, 1, 48, mat.ceiling);
|
||||||
|
this.addBlock(-96, 0, -115, 2, 18, 48, mat.brick);
|
||||||
|
this.addBlock(-64, 0, -115, 2, 18, 48, mat.brick);
|
||||||
|
this.addBlock(-80, 0, -139, 32, 18, 2, mat.tech);
|
||||||
|
|
||||||
|
// BFG 9000 Altar
|
||||||
|
this.addBlock(-80, 0, -125, 4, 1.5, 4, mat.hex);
|
||||||
|
this.spawns.items.push({ type: 'bfg', x: -80, y: 1.8, z: -125 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_cell', x: -75, y: 0, z: -125 });
|
||||||
|
this.spawns.items.push({ type: 'ammo_cell', x: -85, y: 0, z: -125 });
|
||||||
|
this.spawns.items.push({ type: 'soulsphere', x: -80, y: 0, z: -132 });
|
||||||
|
|
||||||
|
// Final Arena Demonic Horde
|
||||||
|
this.spawns.enemies.push({ type: 'caco', x: -80, y: 6, z: -120 });
|
||||||
|
this.spawns.enemies.push({ type: 'pinky', x: -75, y: 0, z: -115 });
|
||||||
|
this.spawns.enemies.push({ type: 'pinky', x: -85, y: 0, z: -115 });
|
||||||
|
this.spawns.enemies.push({ type: 'imp', x: -72, y: 0, z: -130 });
|
||||||
|
this.spawns.enemies.push({ type: 'imp', x: -88, y: 0, z: -130 });
|
||||||
|
|
||||||
|
this.engine.addFlickeringLight(-80, 10, -125, 0xff0022, 4.0, 30);
|
||||||
|
|
||||||
|
// Motorized Elevator Platform in Courtyard (moves between ground y=0 and catwalk y=6)
|
||||||
|
this.createElevator(12, 0, -45, 6, 6, -0.6, 5.2, 2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// INTERACTIVE DOORS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
createDoor(x, y, z, w, h, d, type = 'normal', requiredKey = null) {
|
||||||
|
let mat = this.engine.materials.doorRegular;
|
||||||
|
if (type === 'blue') mat = this.engine.materials.doorBlue;
|
||||||
|
if (type === 'red') mat = this.engine.materials.doorRed;
|
||||||
|
if (type === 'yellow') mat = this.engine.materials.doorYellow;
|
||||||
|
|
||||||
|
const geo = new THREE.BoxGeometry(w, h, d);
|
||||||
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
|
mesh.position.set(x, y + h / 2, z);
|
||||||
|
mesh.castShadow = true;
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
const min = new THREE.Vector3(x - w / 2, y, z - d / 2);
|
||||||
|
const max = new THREE.Vector3(x + w / 2, y + h, z + d / 2);
|
||||||
|
const box = new THREE.Box3(min, max);
|
||||||
|
this.colliders.push(box);
|
||||||
|
|
||||||
|
const doorObj = {
|
||||||
|
mesh,
|
||||||
|
box,
|
||||||
|
x, y, z, w, h, d,
|
||||||
|
type,
|
||||||
|
requiredKey,
|
||||||
|
state: 'closed', // 'closed', 'opening', 'open', 'closing'
|
||||||
|
openHeight: h * 0.95,
|
||||||
|
currentOffset: 0,
|
||||||
|
autoCloseTimer: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.doors.push(doorObj);
|
||||||
|
return doorObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
interactWithDoors(playerPos, keysInventory) {
|
||||||
|
for (let i = 0; i < this.doors.length; i++) {
|
||||||
|
const door = this.doors[i];
|
||||||
|
const dist = playerPos.distanceTo(door.mesh.position);
|
||||||
|
if (dist < 5.5) {
|
||||||
|
if (door.requiredKey && !keysInventory[door.requiredKey]) {
|
||||||
|
if (window.game && window.game.hud) {
|
||||||
|
window.game.hud.showNotification(`YOU NEED THE ${door.requiredKey.toUpperCase()} KEY`);
|
||||||
|
}
|
||||||
|
if (this.sound) this.sound.playSlimeHurt();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (door.state === 'closed' || door.state === 'closing') {
|
||||||
|
door.state = 'opening';
|
||||||
|
if (this.sound) this.sound.playDoor(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// MOTORIZED ELEVATOR PLATFORMS
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
createElevator(x, y, z, w, d, minY, maxY, speed = 2.0) {
|
||||||
|
const geo = new THREE.BoxGeometry(w, 0.8, d);
|
||||||
|
const mesh = new THREE.Mesh(geo, this.engine.materials.hex);
|
||||||
|
mesh.position.set(x, y + 0.4, z);
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
this.engine.scene.add(mesh);
|
||||||
|
|
||||||
|
const min = new THREE.Vector3(x - w / 2, y, z - d / 2);
|
||||||
|
const max = new THREE.Vector3(x + w / 2, y + 0.8, z + d / 2);
|
||||||
|
const box = new THREE.Box3(min, max);
|
||||||
|
this.colliders.push(box);
|
||||||
|
|
||||||
|
this.elevators.push({
|
||||||
|
mesh,
|
||||||
|
box,
|
||||||
|
x, z, w, d,
|
||||||
|
currentY: y,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
speed,
|
||||||
|
dir: 1, // 1 = up, -1 = down
|
||||||
|
pauseTimer: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// MAP GAME LOOP UPDATE
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
// 1. Update Doors
|
||||||
|
for (let i = 0; i < this.doors.length; i++) {
|
||||||
|
const door = this.doors[i];
|
||||||
|
if (door.state === 'opening') {
|
||||||
|
door.currentOffset += delta * 4.0;
|
||||||
|
if (door.currentOffset >= door.openHeight) {
|
||||||
|
door.currentOffset = door.openHeight;
|
||||||
|
door.state = 'open';
|
||||||
|
door.autoCloseTimer = 5.0; // stay open for 5 sec
|
||||||
|
}
|
||||||
|
door.mesh.position.y = (door.y + door.h / 2) + door.currentOffset;
|
||||||
|
// Move collider out of way
|
||||||
|
door.box.min.y = door.y + door.currentOffset;
|
||||||
|
door.box.max.y = door.y + door.h + door.currentOffset;
|
||||||
|
} else if (door.state === 'open') {
|
||||||
|
door.autoCloseTimer -= delta;
|
||||||
|
if (door.autoCloseTimer <= 0) {
|
||||||
|
door.state = 'closing';
|
||||||
|
if (this.sound) this.sound.playDoor(false);
|
||||||
|
}
|
||||||
|
} else if (door.state === 'closing') {
|
||||||
|
door.currentOffset -= delta * 4.0;
|
||||||
|
if (door.currentOffset <= 0) {
|
||||||
|
door.currentOffset = 0;
|
||||||
|
door.state = 'closed';
|
||||||
|
}
|
||||||
|
door.mesh.position.y = (door.y + door.h / 2) + door.currentOffset;
|
||||||
|
door.box.min.y = door.y + door.currentOffset;
|
||||||
|
door.box.max.y = door.y + door.h + door.currentOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update Motorized Elevators
|
||||||
|
for (let i = 0; i < this.elevators.length; i++) {
|
||||||
|
const el = this.elevators[i];
|
||||||
|
if (el.pauseTimer > 0) {
|
||||||
|
el.pauseTimer -= delta;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.currentY += el.dir * el.speed * delta;
|
||||||
|
if (el.currentY >= el.maxY) {
|
||||||
|
el.currentY = el.maxY;
|
||||||
|
el.dir = -1;
|
||||||
|
el.pauseTimer = 3.0;
|
||||||
|
} else if (el.currentY <= el.minY) {
|
||||||
|
el.currentY = el.minY;
|
||||||
|
el.dir = 1;
|
||||||
|
el.pauseTimer = 3.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.mesh.position.y = el.currentY + 0.4;
|
||||||
|
el.box.min.y = el.currentY;
|
||||||
|
el.box.max.y = el.currentY + 0.8;
|
||||||
|
|
||||||
|
// Lift player if player is on elevator
|
||||||
|
if (window.game && window.game.player) {
|
||||||
|
const p = window.game.player;
|
||||||
|
if (p.position.x >= el.box.min.x && p.position.x <= el.box.max.x &&
|
||||||
|
p.position.z >= el.box.min.z && p.position.z <= el.box.max.z &&
|
||||||
|
Math.abs(p.position.y - el.box.max.y) < 0.25) {
|
||||||
|
p.position.y = el.box.max.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomMap = DoomMap;
|
||||||
Executable
+214
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - Mobile Touch Controls
|
||||||
|
* Virtual thumbstick for movement, swipe aiming, and action buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomMobileControls {
|
||||||
|
constructor(player) {
|
||||||
|
this.player = player;
|
||||||
|
this.container = document.getElementById('mobile-controls');
|
||||||
|
this.joystickBase = document.getElementById('joystick-base');
|
||||||
|
this.joystickKnob = document.getElementById('joystick-knob');
|
||||||
|
this.lookZone = document.getElementById('mobile-look-zone');
|
||||||
|
|
||||||
|
this.touchMoveId = null;
|
||||||
|
this.touchLookId = null;
|
||||||
|
|
||||||
|
this.joystickCenter = { x: 0, y: 0 };
|
||||||
|
this.maxRadius = 50;
|
||||||
|
|
||||||
|
this.lastLookX = 0;
|
||||||
|
this.lastLookY = 0;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
||||||
|
if (isTouch && this.container) {
|
||||||
|
this.container.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupJoystick();
|
||||||
|
this.setupLookZone();
|
||||||
|
this.setupActionButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupJoystick() {
|
||||||
|
if (!this.joystickBase || !this.joystickKnob) return;
|
||||||
|
|
||||||
|
const onTouchStart = (e) => {
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
if (this.touchMoveId === null) {
|
||||||
|
this.touchMoveId = touch.identifier;
|
||||||
|
const rect = this.joystickBase.getBoundingClientRect();
|
||||||
|
this.joystickCenter.x = rect.left + rect.width / 2;
|
||||||
|
this.joystickCenter.y = rect.top + rect.height / 2;
|
||||||
|
this.handleJoystickMove(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchMove = (e) => {
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
if (touch.identifier === this.touchMoveId) {
|
||||||
|
this.handleJoystickMove(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = (e) => {
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
if (touch.identifier === this.touchMoveId) {
|
||||||
|
this.touchMoveId = null;
|
||||||
|
this.joystickKnob.style.transform = 'translate(-50%, -50%)';
|
||||||
|
this.player.keys.forward = false;
|
||||||
|
this.player.keys.backward = false;
|
||||||
|
this.player.keys.left = false;
|
||||||
|
this.player.keys.right = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.joystickBase.addEventListener('touchstart', onTouchStart, { passive: false });
|
||||||
|
window.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||||
|
window.addEventListener('touchend', onTouchEnd, { passive: false });
|
||||||
|
window.addEventListener('touchcancel', onTouchEnd, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleJoystickMove(clientX, clientY) {
|
||||||
|
const dx = clientX - this.joystickCenter.x;
|
||||||
|
const dy = clientY - this.joystickCenter.y;
|
||||||
|
const dist = Math.hypot(dx, dy);
|
||||||
|
|
||||||
|
const clampedDist = Math.min(dist, this.maxRadius);
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
|
||||||
|
const knobX = Math.cos(angle) * clampedDist;
|
||||||
|
const knobY = Math.sin(angle) * clampedDist;
|
||||||
|
|
||||||
|
this.joystickKnob.style.transform = `translate(calc(-50% + ${knobX}px), calc(-50% + ${knobY}px))`;
|
||||||
|
|
||||||
|
// Normalize inputs (-1 to 1)
|
||||||
|
const normX = knobX / this.maxRadius;
|
||||||
|
const normY = knobY / this.maxRadius;
|
||||||
|
|
||||||
|
const threshold = 0.25;
|
||||||
|
this.player.keys.forward = normY < -threshold;
|
||||||
|
this.player.keys.backward = normY > threshold;
|
||||||
|
this.player.keys.left = normX < -threshold;
|
||||||
|
this.player.keys.right = normX > threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLookZone() {
|
||||||
|
if (!this.lookZone) return;
|
||||||
|
|
||||||
|
this.lookZone.addEventListener('touchstart', (e) => {
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
if (this.touchLookId === null) {
|
||||||
|
this.touchLookId = touch.identifier;
|
||||||
|
this.lastLookX = touch.clientX;
|
||||||
|
this.lastLookY = touch.clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', (e) => {
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
if (touch.identifier === this.touchLookId) {
|
||||||
|
const dx = touch.clientX - this.lastLookX;
|
||||||
|
const dy = touch.clientY - this.lastLookY;
|
||||||
|
|
||||||
|
this.lastLookX = touch.clientX;
|
||||||
|
this.lastLookY = touch.clientY;
|
||||||
|
|
||||||
|
// Apply rotation to player camera
|
||||||
|
const sensitivity = this.player.mouseSensitivity * 1.5;
|
||||||
|
this.player.euler.y -= dx * sensitivity;
|
||||||
|
this.player.euler.x -= dy * sensitivity;
|
||||||
|
|
||||||
|
const maxPitch = Math.PI / 2 - 0.02;
|
||||||
|
this.player.euler.x = Math.max(-maxPitch, Math.min(maxPitch, this.player.euler.x));
|
||||||
|
this.player.camera.quaternion.setFromEuler(this.player.euler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
const endLook = (e) => {
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
if (touch.identifier === this.touchLookId) {
|
||||||
|
this.touchLookId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('touchend', endLook, { passive: false });
|
||||||
|
window.addEventListener('touchcancel', endLook, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
setupActionButtons() {
|
||||||
|
const btnFire = document.getElementById('btn-mobile-fire');
|
||||||
|
const btnJump = document.getElementById('btn-mobile-jump');
|
||||||
|
const btnUse = document.getElementById('btn-mobile-use');
|
||||||
|
const btnNext = document.getElementById('btn-mobile-next');
|
||||||
|
const btnPrev = document.getElementById('btn-mobile-prev');
|
||||||
|
|
||||||
|
if (btnFire) {
|
||||||
|
btnFire.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.player.keys.shoot = true;
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
window.game.weapons.triggerAttack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
btnFire.addEventListener('touchend', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.player.keys.shoot = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnJump) {
|
||||||
|
btnJump.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.player.keys.jump = true;
|
||||||
|
});
|
||||||
|
btnJump.addEventListener('touchend', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.player.keys.jump = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnUse) {
|
||||||
|
btnUse.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.player.checkInteractions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnNext) {
|
||||||
|
btnNext.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
window.game.weapons.nextWeapon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnPrev) {
|
||||||
|
btnPrev.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
window.game.weapons.prevWeapon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomMobileControls = DoomMobileControls;
|
||||||
Executable
+498
@@ -0,0 +1,498 @@
|
|||||||
|
/**
|
||||||
|
* DOOM 3D - First Person Player Controller & Physics
|
||||||
|
* Smooth movement, Pointer Lock look, step-climbing physics, stats, inventory and cheat codes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DoomPlayer {
|
||||||
|
constructor(engine, sound) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.sound = sound;
|
||||||
|
this.camera = engine.camera;
|
||||||
|
|
||||||
|
// Position & Physics
|
||||||
|
this.position = new THREE.Vector3(0, 2.0, 0);
|
||||||
|
this.velocity = new THREE.Vector3();
|
||||||
|
this.euler = new THREE.Euler(0, 0, 0, 'YXZ');
|
||||||
|
this.isGrounded = false;
|
||||||
|
this.height = 1.8;
|
||||||
|
this.radius = 0.5;
|
||||||
|
this.stepHeight = 0.95;
|
||||||
|
|
||||||
|
// Speeds
|
||||||
|
this.walkSpeed = 11.0;
|
||||||
|
this.runSpeed = 19.0;
|
||||||
|
this.jumpForce = 12.0;
|
||||||
|
this.gravity = 26.0;
|
||||||
|
this.mouseSensitivity = 0.0022;
|
||||||
|
|
||||||
|
// Controls Input State
|
||||||
|
this.keys = {
|
||||||
|
forward: false,
|
||||||
|
backward: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
sprint: false,
|
||||||
|
jump: false,
|
||||||
|
interact: false,
|
||||||
|
shoot: false
|
||||||
|
};
|
||||||
|
this.isPointerLocked = false;
|
||||||
|
|
||||||
|
// Player Stats
|
||||||
|
this.maxHealth = 100;
|
||||||
|
this.health = 100;
|
||||||
|
this.maxArmor = 100;
|
||||||
|
this.armor = 0;
|
||||||
|
this.isDead = false;
|
||||||
|
this.godMode = false;
|
||||||
|
this.noclip = false;
|
||||||
|
|
||||||
|
// Ammo Reserves
|
||||||
|
this.ammo = {
|
||||||
|
bullets: 50,
|
||||||
|
shells: 0,
|
||||||
|
rockets: 0,
|
||||||
|
cells: 0
|
||||||
|
};
|
||||||
|
this.maxAmmo = {
|
||||||
|
bullets: 200,
|
||||||
|
shells: 50,
|
||||||
|
rockets: 50,
|
||||||
|
cells: 300
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keys & Inventory
|
||||||
|
this.keysInventory = {
|
||||||
|
blue: false,
|
||||||
|
red: false,
|
||||||
|
yellow: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Head Bobbing
|
||||||
|
this.bobTimer = 0;
|
||||||
|
this.bobOffset = new THREE.Vector2();
|
||||||
|
|
||||||
|
// Slime Damage Timer
|
||||||
|
this.slimeTimer = 0;
|
||||||
|
|
||||||
|
// Cheat Input Buffer
|
||||||
|
this.cheatBuffer = '';
|
||||||
|
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Keyboard down
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
this.onKeyDown(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard up
|
||||||
|
window.addEventListener('keyup', (e) => {
|
||||||
|
this.onKeyUp(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse move (Pointer Lock)
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!this.isPointerLocked || this.isDead) return;
|
||||||
|
const movementX = e.movementX || e.mozMovementX || e.webkitMovementX || 0;
|
||||||
|
const movementY = e.movementY || e.mozMovementY || e.webkitMovementY || 0;
|
||||||
|
|
||||||
|
this.euler.y -= movementX * this.mouseSensitivity;
|
||||||
|
this.euler.x -= movementY * this.mouseSensitivity;
|
||||||
|
|
||||||
|
// Clamp pitch look angle to [-89 deg, +89 deg]
|
||||||
|
const maxPitch = Math.PI / 2 - 0.02;
|
||||||
|
this.euler.x = Math.max(-maxPitch, Math.min(maxPitch, this.euler.x));
|
||||||
|
|
||||||
|
this.camera.quaternion.setFromEuler(this.euler);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pointer lock state changes
|
||||||
|
document.addEventListener('pointerlockchange', () => {
|
||||||
|
this.isPointerLocked = (document.pointerLockElement === this.engine.renderer.domElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Canvas click request pointer lock
|
||||||
|
this.engine.renderer.domElement.addEventListener('click', () => {
|
||||||
|
if (!this.isPointerLocked && !this.isDead) {
|
||||||
|
this.requestPointerLock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse click to shoot
|
||||||
|
window.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button === 0 && this.isPointerLocked && !this.isDead) {
|
||||||
|
this.keys.shoot = true;
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
window.game.weapons.triggerAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', (e) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
this.keys.shoot = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPointerLock() {
|
||||||
|
try {
|
||||||
|
this.engine.renderer.domElement.requestPointerLock();
|
||||||
|
if (this.sound) this.sound.resume();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Pointer lock request error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitPointerLock() {
|
||||||
|
try {
|
||||||
|
document.exitPointerLock();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
const code = e.code;
|
||||||
|
if (code === 'KeyW' || code === 'ArrowUp') this.keys.forward = true;
|
||||||
|
if (code === 'KeyS' || code === 'ArrowDown') this.keys.backward = true;
|
||||||
|
if (code === 'KeyA' || code === 'ArrowLeft') this.keys.left = true;
|
||||||
|
if (code === 'KeyD' || code === 'ArrowRight') this.keys.right = true;
|
||||||
|
if (code === 'ShiftLeft' || code === 'ShiftRight') this.keys.sprint = true;
|
||||||
|
if (code === 'Space') {
|
||||||
|
this.keys.jump = true;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (code === 'KeyE' || code === 'Space') {
|
||||||
|
this.keys.interact = true;
|
||||||
|
this.checkInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weapon hotkeys (1-7)
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
if (code === 'Digit1') window.game.weapons.selectWeapon(0); // Fists
|
||||||
|
if (code === 'Digit2') window.game.weapons.selectWeapon(1); // Pistol
|
||||||
|
if (code === 'Digit3') window.game.weapons.selectWeapon(2); // Shotgun
|
||||||
|
if (code === 'Digit4') window.game.weapons.selectWeapon(3); // Chaingun
|
||||||
|
if (code === 'Digit5') window.game.weapons.selectWeapon(4); // Rocket Launcher
|
||||||
|
if (code === 'Digit6') window.game.weapons.selectWeapon(5); // Plasma Gun
|
||||||
|
if (code === 'Digit7') window.game.weapons.selectWeapon(6); // BFG 9000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic DOOM Cheats
|
||||||
|
this.checkCheatCode(e.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyUp(e) {
|
||||||
|
const code = e.code;
|
||||||
|
if (code === 'KeyW' || code === 'ArrowUp') this.keys.forward = false;
|
||||||
|
if (code === 'KeyS' || code === 'ArrowDown') this.keys.backward = false;
|
||||||
|
if (code === 'KeyA' || code === 'ArrowLeft') this.keys.left = false;
|
||||||
|
if (code === 'KeyD' || code === 'ArrowRight') this.keys.right = false;
|
||||||
|
if (code === 'ShiftLeft' || code === 'ShiftRight') this.keys.sprint = false;
|
||||||
|
if (code === 'Space') this.keys.jump = false;
|
||||||
|
if (code === 'KeyE') this.keys.interact = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCheatCode(char) {
|
||||||
|
if (!char || char.length > 1) return;
|
||||||
|
this.cheatBuffer = (this.cheatBuffer + char.toLowerCase()).slice(-8);
|
||||||
|
|
||||||
|
// IDDQD (God Mode)
|
||||||
|
if (this.cheatBuffer.endsWith('iddqd')) {
|
||||||
|
this.godMode = !this.godMode;
|
||||||
|
this.health = 100;
|
||||||
|
if (window.game && window.game.hud) {
|
||||||
|
window.game.hud.showNotification(this.godMode ? 'DEGNAMELESS INVULNERABILITY ON' : 'DEGNAMELESS INVULNERABILITY OFF');
|
||||||
|
}
|
||||||
|
if (this.sound) this.sound.playItemPickup('weapon');
|
||||||
|
}
|
||||||
|
// IDKFA (All Weapons, Full Ammo & Keys)
|
||||||
|
else if (this.cheatBuffer.endsWith('idkfa')) {
|
||||||
|
this.ammo.bullets = this.maxAmmo.bullets;
|
||||||
|
this.ammo.shells = this.maxAmmo.shells;
|
||||||
|
this.ammo.rockets = this.maxAmmo.rockets;
|
||||||
|
this.ammo.cells = this.maxAmmo.cells;
|
||||||
|
this.armor = 200;
|
||||||
|
this.keysInventory.blue = true;
|
||||||
|
this.keysInventory.red = true;
|
||||||
|
this.keysInventory.yellow = true;
|
||||||
|
if (window.game && window.game.weapons) {
|
||||||
|
window.game.weapons.unlocked = [true, true, true, true, true, true, true];
|
||||||
|
}
|
||||||
|
if (window.game && window.game.hud) {
|
||||||
|
window.game.hud.showNotification('VERY HAPPY AMMO ADDED');
|
||||||
|
}
|
||||||
|
if (this.sound) this.sound.playItemPickup('weapon');
|
||||||
|
}
|
||||||
|
// IDCLIP (Noclip)
|
||||||
|
else if (this.cheatBuffer.endsWith('idclip')) {
|
||||||
|
this.noclip = !this.noclip;
|
||||||
|
if (window.game && window.game.hud) {
|
||||||
|
window.game.hud.showNotification(this.noclip ? 'NOCLIP ON' : 'NOCLIP OFF');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInteractions() {
|
||||||
|
if (!window.game || !window.game.map) return;
|
||||||
|
window.game.map.interactWithDoors(this.position, this.keysInventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// DAMAGE & COMBAT LOGIC
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
takeDamage(amount, sourcePos = null) {
|
||||||
|
if (this.isDead || this.godMode) return;
|
||||||
|
|
||||||
|
// Armor absorbs 1/3 to 1/2 of damage
|
||||||
|
let dmgToHealth = amount;
|
||||||
|
if (this.armor > 0) {
|
||||||
|
const absorb = Math.min(this.armor, Math.floor(amount * 0.5));
|
||||||
|
this.armor -= absorb;
|
||||||
|
dmgToHealth = amount - absorb;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.health = Math.max(0, this.health - dmgToHealth);
|
||||||
|
|
||||||
|
if (this.sound) {
|
||||||
|
this.sound.playPlayerPain();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.game && window.game.hud) {
|
||||||
|
window.game.hud.triggerDamageFlash();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.health <= 0) {
|
||||||
|
this.die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heal(amount, isMega = false) {
|
||||||
|
if (this.isDead) return false;
|
||||||
|
const cap = isMega ? 200 : this.maxHealth;
|
||||||
|
if (this.health >= cap) return false;
|
||||||
|
this.health = Math.min(cap, this.health + amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addArmor(amount, isMega = false) {
|
||||||
|
if (this.isDead) return false;
|
||||||
|
const cap = isMega ? 200 : this.maxArmor;
|
||||||
|
if (this.armor >= cap) return false;
|
||||||
|
this.armor = Math.min(cap, this.armor + amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAmmo(type, count) {
|
||||||
|
if (!this.ammo[type]) return false;
|
||||||
|
if (this.ammo[type] >= this.maxAmmo[type]) return false;
|
||||||
|
this.ammo[type] = Math.min(this.maxAmmo[type], this.ammo[type] + count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
this.isDead = true;
|
||||||
|
this.health = 0;
|
||||||
|
if (window.game && window.game.hud) {
|
||||||
|
window.game.hud.showGameOver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respawn(spawnPos) {
|
||||||
|
this.isDead = false;
|
||||||
|
this.health = 100;
|
||||||
|
this.armor = 0;
|
||||||
|
this.ammo.bullets = 50;
|
||||||
|
this.ammo.shells = 0;
|
||||||
|
this.ammo.rockets = 0;
|
||||||
|
this.ammo.cells = 0;
|
||||||
|
this.position.copy(spawnPos || new THREE.Vector3(0, 2, 0));
|
||||||
|
this.velocity.set(0, 0, 0);
|
||||||
|
this.euler.set(0, 0, 0, 'YXZ');
|
||||||
|
this.camera.quaternion.setFromEuler(this.euler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// PHYSICS & COLLISION UPDATE
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
if (this.isDead) {
|
||||||
|
// Death cam sinking to floor
|
||||||
|
if (this.camera.position.y > this.position.y - 1.2) {
|
||||||
|
this.camera.position.y -= delta * 3;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Calculate horizontal movement direction relative to camera yaw
|
||||||
|
const moveDir = new THREE.Vector3();
|
||||||
|
if (this.keys.forward) moveDir.z -= 1;
|
||||||
|
if (this.keys.backward) moveDir.z += 1;
|
||||||
|
if (this.keys.left) moveDir.x -= 1;
|
||||||
|
if (this.keys.right) moveDir.x += 1;
|
||||||
|
|
||||||
|
if (moveDir.lengthSq() > 0) {
|
||||||
|
moveDir.normalize();
|
||||||
|
// Rotate move direction by camera's horizontal yaw (euler.y)
|
||||||
|
moveDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.euler.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed
|
||||||
|
const curSpeed = this.keys.sprint ? this.runSpeed : this.walkSpeed;
|
||||||
|
const targetVelX = moveDir.x * curSpeed;
|
||||||
|
const targetVelZ = moveDir.z * curSpeed;
|
||||||
|
|
||||||
|
// Smooth acceleration/damping
|
||||||
|
this.velocity.x += (targetVelX - this.velocity.x) * Math.min(1, delta * 14);
|
||||||
|
this.velocity.z += (targetVelZ - this.velocity.z) * Math.min(1, delta * 14);
|
||||||
|
|
||||||
|
// 2. Jump & Gravity
|
||||||
|
if (!this.noclip) {
|
||||||
|
if (this.keys.jump && this.isGrounded) {
|
||||||
|
this.velocity.y = this.jumpForce;
|
||||||
|
this.isGrounded = false;
|
||||||
|
}
|
||||||
|
this.velocity.y -= this.gravity * delta;
|
||||||
|
} else {
|
||||||
|
this.velocity.y = 0;
|
||||||
|
if (this.keys.jump) this.position.y += curSpeed * delta;
|
||||||
|
if (this.keys.sprint) this.position.y -= curSpeed * delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Collision Resolution against Level Geometry
|
||||||
|
if (!this.noclip && window.game && window.game.map) {
|
||||||
|
this.resolveCollisions(delta);
|
||||||
|
} else {
|
||||||
|
this.position.addScaledVector(this.velocity, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update Camera position with Head Bobbing
|
||||||
|
const isMoving = (this.keys.forward || this.keys.backward || this.keys.left || this.keys.right) && this.isGrounded;
|
||||||
|
if (isMoving) {
|
||||||
|
const bobSpeed = this.keys.sprint ? 16 : 11;
|
||||||
|
this.bobTimer += delta * bobSpeed;
|
||||||
|
this.bobOffset.x = Math.cos(this.bobTimer * 0.5) * 0.05;
|
||||||
|
this.bobOffset.y = Math.sin(this.bobTimer) * 0.08;
|
||||||
|
} else {
|
||||||
|
this.bobOffset.x *= 0.85;
|
||||||
|
this.bobOffset.y *= 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.camera.position.set(
|
||||||
|
this.position.x + this.bobOffset.x,
|
||||||
|
this.position.y + this.height + this.bobOffset.y,
|
||||||
|
this.position.z
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Slime Pit Hazard Damage
|
||||||
|
if (window.game && window.game.map) {
|
||||||
|
if (window.game.map.isInSlimePit(this.position)) {
|
||||||
|
this.slimeTimer += delta;
|
||||||
|
if (this.slimeTimer >= 0.6) {
|
||||||
|
this.slimeTimer = 0;
|
||||||
|
this.takeDamage(6);
|
||||||
|
if (this.sound) this.sound.playSlimeHurt();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.slimeTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveCollisions(delta) {
|
||||||
|
const colliders = window.game.map.getColliders();
|
||||||
|
|
||||||
|
// Horizontal move step X
|
||||||
|
this.position.x += this.velocity.x * delta;
|
||||||
|
this.collideHorizontal(colliders, 'x');
|
||||||
|
|
||||||
|
// Horizontal move step Z
|
||||||
|
this.position.z += this.velocity.z * delta;
|
||||||
|
this.collideHorizontal(colliders, 'z');
|
||||||
|
|
||||||
|
// Vertical move step Y
|
||||||
|
this.position.y += this.velocity.y * delta;
|
||||||
|
this.collideVertical(colliders);
|
||||||
|
}
|
||||||
|
|
||||||
|
collideHorizontal(colliders, axis) {
|
||||||
|
const pMinX = this.position.x - this.radius;
|
||||||
|
const pMaxX = this.position.x + this.radius;
|
||||||
|
const pMinZ = this.position.z - this.radius;
|
||||||
|
const pMaxZ = this.position.z + this.radius;
|
||||||
|
const pMinY = this.position.y;
|
||||||
|
const pMaxY = this.position.y + this.height;
|
||||||
|
|
||||||
|
for (let i = 0; i < colliders.length; i++) {
|
||||||
|
const box = colliders[i];
|
||||||
|
// Check AABB overlap
|
||||||
|
if (pMaxX > box.min.x && pMinX < box.max.x &&
|
||||||
|
pMaxZ > box.min.z && pMinZ < box.max.z &&
|
||||||
|
pMaxY > box.min.y && pMinY < box.max.y) {
|
||||||
|
|
||||||
|
// Check if step-climbing is possible (stairs, curbs, low ledges)
|
||||||
|
const stepUp = box.max.y - pMinY;
|
||||||
|
if (stepUp > 0 && stepUp <= this.stepHeight && (this.isGrounded || this.velocity.y <= 1.0)) {
|
||||||
|
this.position.y = box.max.y + 0.02;
|
||||||
|
this.isGrounded = true;
|
||||||
|
this.velocity.y = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push back on axis
|
||||||
|
if (axis === 'x') {
|
||||||
|
if (this.velocity.x > 0) {
|
||||||
|
this.position.x = box.min.x - this.radius;
|
||||||
|
} else if (this.velocity.x < 0) {
|
||||||
|
this.position.x = box.max.x + this.radius;
|
||||||
|
}
|
||||||
|
this.velocity.x = 0;
|
||||||
|
} else if (axis === 'z') {
|
||||||
|
if (this.velocity.z > 0) {
|
||||||
|
this.position.z = box.min.z - this.radius;
|
||||||
|
} else if (this.velocity.z < 0) {
|
||||||
|
this.position.z = box.max.z + this.radius;
|
||||||
|
}
|
||||||
|
this.velocity.z = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collideVertical(colliders) {
|
||||||
|
const pMinX = this.position.x - this.radius;
|
||||||
|
const pMaxX = this.position.x + this.radius;
|
||||||
|
const pMinZ = this.position.z - this.radius;
|
||||||
|
const pMaxZ = this.position.z + this.radius;
|
||||||
|
const pMinY = this.position.y;
|
||||||
|
const pMaxY = this.position.y + this.height;
|
||||||
|
|
||||||
|
let onGround = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < colliders.length; i++) {
|
||||||
|
const box = colliders[i];
|
||||||
|
if (pMaxX > box.min.x && pMinX < box.max.x &&
|
||||||
|
pMaxZ > box.min.z && pMinZ < box.max.z &&
|
||||||
|
pMaxY > box.min.y && pMinY < box.max.y) {
|
||||||
|
|
||||||
|
if (this.velocity.y < 0) {
|
||||||
|
// Falling onto platform / floor
|
||||||
|
this.position.y = box.max.y;
|
||||||
|
this.velocity.y = 0;
|
||||||
|
onGround = true;
|
||||||
|
} else if (this.velocity.y > 0) {
|
||||||
|
// Hitting ceiling
|
||||||
|
this.position.y = box.min.y - this.height;
|
||||||
|
this.velocity.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGrounded = onGround;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DoomPlayer = DoomPlayer;
|
||||||
Executable
+1342
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,65 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name agy.qpid.pro;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name agy.qpid.pro;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/agy.qpid.pro/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/agy.qpid.pro/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
|
||||||
|
# Gzip Compression for fast bundle loading
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml application/manifest+json;
|
||||||
|
|
||||||
|
# Certbot webroot
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Antigravity Manager — all other traffic to port 8045
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8045/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user