Add Krake boss with ink, pull marks and sweep tentacles plus boss mode
This commit is contained in:
@@ -34,7 +34,9 @@ npm run dev # Dev-Server mit HMR
|
|||||||
| `Trapper.ts` | Falle (skill1) + Gaswolke (skill2) |
|
| `Trapper.ts` | Falle (skill1) + Gaswolke (skill2) |
|
||||||
| `Aura.ts` / `Boomerang.ts` | Passive Skills, lesen Level live aus `game.skillSystem` |
|
| `Aura.ts` / `Boomerang.ts` | Passive Skills, lesen Level live aus `game.skillSystem` |
|
||||||
| `Trap.ts` / `GasCloud.ts` | Platzierbare Effekte; `update(dt, game): boolean` (false = entfernen), `dispose()` |
|
| `Trap.ts` / `GasCloud.ts` | Platzierbare Effekte; `update(dt, game): boolean` (false = entfernen), `dispose()` |
|
||||||
| `*Enemy*.ts` (Spider, Ghost, Frog, Bat, BeeSwarm, Slime, Brute/Boss, Crab, Turret) | Prozedurale Gegner-Modelle; je `update(dt, game)` |
|
| `Golem.ts` / `Krake.ts` | Bosse (Welle 10 / 20), registriert in `Game.bossWaves` (`{name, spawn}`). Krake: stationär (`spawnAtCenter`), Tinte (`InkBlob.ts`/`InkPuddle.ts`: Slow + DoT), Tentakel-Griff (5 rote Boden-Marken um den Oktopus → Pull via `player.pullTime`), Sweep (roter Tentakel kreist um den Oktopus, überspringbar), Enrage <50% HP |
|
||||||
|
| `InkBlob.ts` / `InkPuddle.ts` | Krake-Effekte: Tintenprojektil → Pfütze mit Slow (`player.slowTimer`) + DoT + `game.triggerInkVignette()` |
|
||||||
|
| `*Enemy*.ts` (Spider, Ghost, Frog, Bat, BeeSwarm, Slime, Brute, Crab, Turret) | Prozedurale Gegner-Modelle; je `update(dt, game)` |
|
||||||
| `SoundManager.ts` | Vorab geladene .ogg-Buffer (`playSound(name, volume)`); Sounds: damage, spawn, fireball, explosion, teleport, gameover, sword_hit1, spiderwalking |
|
| `SoundManager.ts` | Vorab geladene .ogg-Buffer (`playSound(name, volume)`); Sounds: damage, spawn, fireball, explosion, teleport, gameover, sword_hit1, spiderwalking |
|
||||||
| `MeshLoader.ts` | `loadGLB(name)` → `web/public/models/*.glb`; Texturen in `web/public/textures/` |
|
| `MeshLoader.ts` | `loadGLB(name)` → `web/public/models/*.glb`; Texturen in `web/public/textures/` |
|
||||||
|
|
||||||
@@ -47,5 +49,7 @@ npm run dev # Dev-Server mit HMR
|
|||||||
- **Skill-Arten**: Aktive = `Weapon`-Subklassen mit Cooldowns (UI-Balken in `index.html`, Update in `updateUI`). Passive = Stats in `Player.applySkillSystem` ODER Game-seitige Objekte (Aura/Boomerang), die in `syncSkillVisibility` erzeugt/zersstört und im Loop geupdated werden.
|
- **Skill-Arten**: Aktive = `Weapon`-Subklassen mit Cooldowns (UI-Balken in `index.html`, Update in `updateUI`). Passive = Stats in `Player.applySkillSystem` ODER Game-seitige Objekte (Aura/Boomerang), die in `syncSkillVisibility` erzeugt/zersstört und im Loop geupdated werden.
|
||||||
- **Effekt-Lifecycle**: Neue Effekte mit eigenem `update(dt, game)`-Rückgabewert (false = aufräumen) und `dispose()`; in `Game.resetWorld()` ALLES aufräumen (sonst Geister-Objekte nach „Nochmal spielen").
|
- **Effekt-Lifecycle**: Neue Effekte mit eigenem `update(dt, game)`-Rückgabewert (false = aufräumen) und `dispose()`; in `Game.resetWorld()` ALLES aufräumen (sonst Geister-Objekte nach „Nochmal spielen").
|
||||||
- **Gegner-Festhalten**: `enemy.rootTime > 0` friert `enemy.update()` ein (siehe `Game.updateEnemies`) — so funktioniert die Falle, ohne jede Gegner-Klasse anzufassen.
|
- **Gegner-Festhalten**: `enemy.rootTime > 0` friert `enemy.update()` ein (siehe `Game.updateEnemies`) — so funktioniert die Falle, ohne jede Gegner-Klasse anzufassen.
|
||||||
|
- **Boss-Wellen & Boss-Modus**: `Game.bossWaves` (Map: Welle → `{name, spawn}`) steuert Boss-Wellen (10, 20 …) — Boss-Welle startet erst, wenn die Arena geräumt ist (`enemies.every(e => e.dead)`); nach Boss-Tod läuft die nächste normale Welle an. Cheat-Code **„idkfa"** im Hauptmenü blendet den Boss-Modus ein (`#boss-mode`, Buttons werden aus `bossWaves` erzeugt) → `startBossFight(wave)` mit Zufalls-Skill-Boost (`grantRandomSkills`). `spawnAtCenter`-Flag lässt stationäre Bosse in der Arena-Mitte spawnen. Effekte, die ein Boss selbst verwaltet (Tinten-Blobs/-Pfützen, Ringe), müssen in `onDeath`/`dispose` aufgeräumt werden (Dead-Enemy-Removal ruft `dispose` NICHT auf).
|
||||||
|
- **Spieler-Status-Effekte** (in `Player.ts` + `Game.updatePlayerMovement`): `slowTimer`/`slowFactor` (Tinten-Slow, Lila-Tönung) und `pullTime`/`pullDir` (Tentakel-Griff: Eingabe wird ignoriert, Geschwindigkeit = Pull-Richtung). Beides wird in `Player.reset()` zurückgesetzt.
|
||||||
- **Kontaktschaden** (Gegner berührt Spieler) läuft in `Game.ts` (Close-Encounter-Loop) mit `contactDps * dt`; Dornen-Skill ebendort als Gegenschlag.
|
- **Kontaktschaden** (Gegner berührt Spieler) läuft in `Game.ts` (Close-Encounter-Loop) mit `contactDps * dt`; Dornen-Skill ebendort als Gegenschlag.
|
||||||
- **`playSound`** erzeugt pro Aufruf einen AudioBufferSourceNode — bei hochfrequenten Effekten (Aura-Ticks, Bumerang-Hits) ggf. throtteln.
|
- **`playSound`** erzeugt pro Aufruf einen AudioBufferSourceNode — bei hochfrequenten Effekten (Aura-Ticks, Bumerang-Hits) ggf. throtteln.
|
||||||
|
|||||||
@@ -37,6 +37,34 @@
|
|||||||
0%, 100% { opacity: 0.25; }
|
0%, 100% { opacity: 0.25; }
|
||||||
50% { opacity: 0.6; }
|
50% { opacity: 0.6; }
|
||||||
}
|
}
|
||||||
|
#ink-vignette {
|
||||||
|
position: absolute; inset: 0; opacity: 0; pointer-events: none;
|
||||||
|
background: radial-gradient(ellipse at center,
|
||||||
|
rgba(120,40,180,0) 35%, rgba(120,40,180,0.4) 100%);
|
||||||
|
box-shadow: inset 0 0 140px 50px rgba(100,30,160,0.55);
|
||||||
|
}
|
||||||
|
#ink-vignette.ink { animation: ink-flash 0.5s ease-out; }
|
||||||
|
@keyframes ink-flash {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
#boss-bar {
|
||||||
|
position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
|
||||||
|
width: 420px; display: none; text-align: center; font-family: monospace;
|
||||||
|
}
|
||||||
|
#boss-bar-label {
|
||||||
|
color: #ffcc00; font-size: 16px; text-shadow: 1px 1px 3px #000;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
#boss-bar-bg {
|
||||||
|
height: 18px; background: rgba(0,0,0,0.6);
|
||||||
|
border: 2px solid #aa3366;
|
||||||
|
}
|
||||||
|
#boss-bar-fill {
|
||||||
|
height: 100%; width: 100%;
|
||||||
|
background: linear-gradient(#ff5577, #bb2244);
|
||||||
|
transition: width 0.15s linear;
|
||||||
|
}
|
||||||
#health-bar-bg {
|
#health-bar-bg {
|
||||||
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
||||||
background: rgba(0,0,0,0.6); border: 2px solid #555;
|
background: rgba(0,0,0,0.6); border: 2px solid #555;
|
||||||
@@ -235,6 +263,11 @@
|
|||||||
<div id="ui-overlay">
|
<div id="ui-overlay">
|
||||||
<div id="damage-vignette"></div>
|
<div id="damage-vignette"></div>
|
||||||
<div id="low-hp-vignette"></div>
|
<div id="low-hp-vignette"></div>
|
||||||
|
<div id="ink-vignette"></div>
|
||||||
|
<div id="boss-bar">
|
||||||
|
<div id="boss-bar-label">BOSS</div>
|
||||||
|
<div id="boss-bar-bg"><div id="boss-bar-fill"></div></div>
|
||||||
|
</div>
|
||||||
<div id="health-bar-bg"></div>
|
<div id="health-bar-bg"></div>
|
||||||
<div id="health-bar"></div>
|
<div id="health-bar"></div>
|
||||||
<div id="health-text">100</div>
|
<div id="health-text">100</div>
|
||||||
@@ -334,6 +367,10 @@
|
|||||||
Q, E, 1–4: Skills (werden nach Erwerb belegt)
|
Q, E, 1–4: Skills (werden nach Erwerb belegt)
|
||||||
</div>
|
</div>
|
||||||
<input id="seed-input" type="text" placeholder="Seed (leer = zufällig)" />
|
<input id="seed-input" type="text" placeholder="Seed (leer = zufällig)" />
|
||||||
|
<div id="boss-mode" style="opacity:0; pointer-events:none; visibility:hidden; display:flex; flex-direction:column; align-items:center; gap:10px;">
|
||||||
|
<div class="subtitle">BOSS-MODUS</div>
|
||||||
|
<div id="boss-mode-buttons" style="display:flex; flex-direction:column; gap:8px; align-items:center;"></div>
|
||||||
|
</div>
|
||||||
<button id="btn-start" class="menu-button">Start</button>
|
<button id="btn-start" class="menu-button">Start</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export class Aura {
|
|||||||
const dist = enemy.body.position.distanceTo(center);
|
const dist = enemy.body.position.distanceTo(center);
|
||||||
if (dist > radius + 0.4) continue;
|
if (dist > radius + 0.4) continue;
|
||||||
enemy.takeDamage(damage, game, false, center);
|
enemy.takeDamage(damage, game, false, center);
|
||||||
|
// Stationaere Gegner (Bosse, Tuerme) werden nur geschaedigt, nicht geschoben
|
||||||
|
if (enemy.immovable) continue;
|
||||||
const dir = new THREE.Vector3()
|
const dir = new THREE.Vector3()
|
||||||
.subVectors(enemy.body.position, center);
|
.subVectors(enemy.body.position, center);
|
||||||
dir.y = 0;
|
dir.y = 0;
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ export abstract class Enemy {
|
|||||||
knockback = 0;
|
knockback = 0;
|
||||||
rootTime = 0;
|
rootTime = 0;
|
||||||
health = 100;
|
health = 100;
|
||||||
|
maxHealth = 100;
|
||||||
|
isBoss = false;
|
||||||
|
displayName = '';
|
||||||
speed = 6;
|
speed = 6;
|
||||||
xp = 10;
|
xp = 10;
|
||||||
contactDps = 25;
|
contactDps = 25;
|
||||||
contactRadius = 2;
|
contactRadius = 2;
|
||||||
guaranteedDrop = false;
|
guaranteedDrop = false;
|
||||||
|
spawnAtCenter = false;
|
||||||
|
immovable = false;
|
||||||
grace = 0;
|
grace = 0;
|
||||||
mixer: THREE.AnimationMixer | null = null;
|
mixer: THREE.AnimationMixer | null = null;
|
||||||
protected clips: Map<string, THREE.AnimationAction> = new Map();
|
protected clips: Map<string, THREE.AnimationAction> = new Map();
|
||||||
|
|||||||
+158
-45
@@ -9,6 +9,7 @@ import { BeeSwarm } from './BeeSwarm';
|
|||||||
import { Slime } from './Slime';
|
import { Slime } from './Slime';
|
||||||
import { Brute } from './Brute';
|
import { Brute } from './Brute';
|
||||||
import { Golem } from './Golem';
|
import { Golem } from './Golem';
|
||||||
|
import { Krake } from './Krake';
|
||||||
import { Crab } from './Crab';
|
import { Crab } from './Crab';
|
||||||
import { Turret } from './Turret';
|
import { Turret } from './Turret';
|
||||||
import { EnemyProjectile } from './EnemyProjectile';
|
import { EnemyProjectile } from './EnemyProjectile';
|
||||||
@@ -74,8 +75,11 @@ export class Game {
|
|||||||
spawns = 0;
|
spawns = 0;
|
||||||
killed = 0;
|
killed = 0;
|
||||||
|
|
||||||
// Boss-Wellen: Welle -> Boss-Factory (aktuell nur Welle 10; 20/30 folgen)
|
// Boss-Wellen: Welle -> Boss (Name fuer Boss-Modus-UI, Factory fuer Spawn)
|
||||||
private bossWaves = new Map<number, () => Enemy>([[10, () => new Golem()]]);
|
private bossWaves = new Map<number, { name: string; spawn: () => Enemy }>([
|
||||||
|
[10, { name: 'Golem', spawn: () => new Golem() }],
|
||||||
|
[20, { name: 'Krake', spawn: () => new Krake() }],
|
||||||
|
]);
|
||||||
private bossFightActive = false;
|
private bossFightActive = false;
|
||||||
|
|
||||||
// Debug hitzone visualization
|
// Debug hitzone visualization
|
||||||
@@ -104,6 +108,9 @@ export class Game {
|
|||||||
private uiGameOverStats!: HTMLElement;
|
private uiGameOverStats!: HTMLElement;
|
||||||
private uiMainMenu!: HTMLElement;
|
private uiMainMenu!: HTMLElement;
|
||||||
private uiSeedInput!: HTMLInputElement;
|
private uiSeedInput!: HTMLInputElement;
|
||||||
|
private uiBossMode!: HTMLElement;
|
||||||
|
private uiBossModeButtons!: HTMLElement;
|
||||||
|
private cheatBuffer = '';
|
||||||
private uiLevelLabel!: HTMLElement;
|
private uiLevelLabel!: HTMLElement;
|
||||||
private uiSeedLabel!: HTMLElement;
|
private uiSeedLabel!: HTMLElement;
|
||||||
private uiXpBarFill!: HTMLElement;
|
private uiXpBarFill!: HTMLElement;
|
||||||
@@ -118,6 +125,10 @@ export class Game {
|
|||||||
private uiOfferDescs: HTMLElement[] = [];
|
private uiOfferDescs: HTMLElement[] = [];
|
||||||
private uiDamageVignette!: HTMLElement;
|
private uiDamageVignette!: HTMLElement;
|
||||||
private uiLowHpVignette!: HTMLElement;
|
private uiLowHpVignette!: HTMLElement;
|
||||||
|
private uiInkVignette!: HTMLElement;
|
||||||
|
private uiBossBar!: HTMLElement;
|
||||||
|
private uiBossBarFill!: HTMLElement;
|
||||||
|
private uiBossBarLabel!: HTMLElement;
|
||||||
private uiTrapContainer!: HTMLElement;
|
private uiTrapContainer!: HTMLElement;
|
||||||
private uiTrapCD!: HTMLElement;
|
private uiTrapCD!: HTMLElement;
|
||||||
private uiGasContainer!: HTMLElement;
|
private uiGasContainer!: HTMLElement;
|
||||||
@@ -218,6 +229,13 @@ export class Game {
|
|||||||
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
|
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
|
||||||
this.uiDamageVignette = document.getElementById('damage-vignette')!;
|
this.uiDamageVignette = document.getElementById('damage-vignette')!;
|
||||||
this.uiLowHpVignette = document.getElementById('low-hp-vignette')!;
|
this.uiLowHpVignette = document.getElementById('low-hp-vignette')!;
|
||||||
|
this.uiInkVignette = document.getElementById('ink-vignette')!;
|
||||||
|
this.uiBossBar = document.getElementById('boss-bar')!;
|
||||||
|
this.uiBossBarFill = document.getElementById('boss-bar-fill')!;
|
||||||
|
this.uiBossBarLabel = document.getElementById('boss-bar-label')!;
|
||||||
|
this.uiBossMode = document.getElementById('boss-mode')!;
|
||||||
|
this.uiBossModeButtons = document.getElementById('boss-mode-buttons')!;
|
||||||
|
this.setupBossModeUI();
|
||||||
this.uiTrapContainer = document.getElementById('trap-cooldown')!;
|
this.uiTrapContainer = document.getElementById('trap-cooldown')!;
|
||||||
this.uiTrapCD = document.querySelector('#trap-cooldown .cooldown-fill')!;
|
this.uiTrapCD = document.querySelector('#trap-cooldown .cooldown-fill')!;
|
||||||
this.uiGasContainer = document.getElementById('gas-cooldown')!;
|
this.uiGasContainer = document.getElementById('gas-cooldown')!;
|
||||||
@@ -278,13 +296,26 @@ export class Game {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code === 'Space' && this.state === 'playing') {
|
if (e.code === 'Space' && this.state === 'playing' && this.player.pullTime <= 0) {
|
||||||
this.player.jump();
|
this.player.jump();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (e.code === 'KeyH') {
|
if (e.code === 'KeyH') {
|
||||||
this.showHitzones = !this.showHitzones;
|
this.showHitzones = !this.showHitzones;
|
||||||
}
|
}
|
||||||
|
// Cheat-Code "idkfa" im Hauptmenue: Boss-Modus freischalten
|
||||||
|
if (this.state === 'menu') {
|
||||||
|
const active = document.activeElement;
|
||||||
|
const typing = active instanceof HTMLInputElement
|
||||||
|
|| active instanceof HTMLTextAreaElement;
|
||||||
|
if (!typing) {
|
||||||
|
this.cheatBuffer = (this.cheatBuffer + e.key).slice(-5).toLowerCase();
|
||||||
|
if (this.cheatBuffer === 'idkfa') {
|
||||||
|
this.cheatBuffer = '';
|
||||||
|
this.showBossMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const slotIdx = HOTBAR_KEY_CODES.indexOf(e.code);
|
const slotIdx = HOTBAR_KEY_CODES.indexOf(e.code);
|
||||||
if (slotIdx >= 0 && this.state === 'playing' && this.hotbar[slotIdx]) {
|
if (slotIdx >= 0 && this.state === 'playing' && this.hotbar[slotIdx]) {
|
||||||
this.triggerHotbarSkill(this.hotbar[slotIdx]!);
|
this.triggerHotbarSkill(this.hotbar[slotIdx]!);
|
||||||
@@ -498,7 +529,11 @@ export class Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.resetWorld();
|
this.resetWorld();
|
||||||
this.logState('playing', 'startGame');
|
this.beginRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
private beginRun() {
|
||||||
|
this.logState('playing', 'beginRun');
|
||||||
this.state = 'playing';
|
this.state = 'playing';
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
this.hidePauseOverlay();
|
this.hidePauseOverlay();
|
||||||
@@ -514,6 +549,51 @@ export class Game {
|
|||||||
this.blurFocus();
|
this.blurFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupBossModeUI() {
|
||||||
|
this.uiBossModeButtons.innerHTML = '';
|
||||||
|
for (const [wave, entry] of this.bossWaves) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'menu-button';
|
||||||
|
btn.textContent = `${entry.name} (Welle ${wave})`;
|
||||||
|
btn.style.fontSize = '14px';
|
||||||
|
btn.style.padding = '8px 20px';
|
||||||
|
btn.addEventListener('click', () => this.startBossFight(wave));
|
||||||
|
this.uiBossModeButtons.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showBossMode() {
|
||||||
|
this.uiBossMode.style.opacity = '1';
|
||||||
|
this.uiBossMode.style.pointerEvents = 'auto';
|
||||||
|
this.uiBossMode.style.visibility = 'visible';
|
||||||
|
playSound('spawn', 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
startBossFight(wave: number) {
|
||||||
|
if (this.state === 'playing' || this.state === 'levelup') return;
|
||||||
|
this.seed = Math.floor(Math.random() * 1000000);
|
||||||
|
this.resetWorld();
|
||||||
|
this.spawns = wave;
|
||||||
|
this.grantRandomSkills(wave);
|
||||||
|
this.spawnBossForWave(wave);
|
||||||
|
this.bossFightActive = true;
|
||||||
|
this.spawnTimer = 0;
|
||||||
|
this.beginRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
private grantRandomSkills(wave: number) {
|
||||||
|
const picks = Math.round(wave * 0.7);
|
||||||
|
for (let i = 0; i < picks; i++) {
|
||||||
|
const offers = this.skillSystem.rollOffers();
|
||||||
|
if (offers.length === 0) break;
|
||||||
|
const pick = offers[Math.floor(Math.random() * offers.length)];
|
||||||
|
this.skillSystem.apply(pick.id);
|
||||||
|
this.assignHotbarSkill(pick.id);
|
||||||
|
}
|
||||||
|
this.player.applySkillSystem(this.skillSystem);
|
||||||
|
this.syncSkillVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
private blurFocus() {
|
private blurFocus() {
|
||||||
const el = document.activeElement;
|
const el = document.activeElement;
|
||||||
if (el instanceof HTMLElement) el.blur();
|
if (el instanceof HTMLElement) el.blur();
|
||||||
@@ -728,15 +808,19 @@ export class Game {
|
|||||||
this.uiConfirmBtn.disabled = false;
|
this.uiConfirmBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assignHotbarSkill(skillId: string) {
|
||||||
|
if (HOTBAR_SKILLS.includes(skillId) && !this.hotbar.includes(skillId)) {
|
||||||
|
const free = this.hotbar.indexOf(null);
|
||||||
|
if (free >= 0) this.hotbar[free] = skillId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private confirmOffer() {
|
private confirmOffer() {
|
||||||
if (this.state !== 'levelup' || this.selectedOffer < 0) return;
|
if (this.state !== 'levelup' || this.selectedOffer < 0) return;
|
||||||
const offer = this.offers[this.selectedOffer];
|
const offer = this.offers[this.selectedOffer];
|
||||||
this.selectedOffer = -1;
|
this.selectedOffer = -1;
|
||||||
this.skillSystem.apply(offer.id);
|
this.skillSystem.apply(offer.id);
|
||||||
if (HOTBAR_SKILLS.includes(offer.id) && !this.hotbar.includes(offer.id)) {
|
this.assignHotbarSkill(offer.id);
|
||||||
const free = this.hotbar.indexOf(null);
|
|
||||||
if (free >= 0) this.hotbar[free] = offer.id;
|
|
||||||
}
|
|
||||||
this.player.applySkillSystem(this.skillSystem);
|
this.player.applySkillSystem(this.skillSystem);
|
||||||
this.syncSkillVisibility();
|
this.syncSkillVisibility();
|
||||||
this.blurFocus();
|
this.blurFocus();
|
||||||
@@ -841,11 +925,12 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private spawnBossForWave(wave: number) {
|
private spawnBossForWave(wave: number) {
|
||||||
const factory = this.bossWaves.get(wave);
|
const entry = this.bossWaves.get(wave);
|
||||||
if (!factory) return;
|
if (!entry) return;
|
||||||
const x = (Math.random() - 0.5) * 80;
|
const enemy = entry.spawn();
|
||||||
const z = (Math.random() - 0.5) * 80;
|
const x = enemy.spawnAtCenter ? 0 : (Math.random() - 0.5) * 80;
|
||||||
this.spawnEnemyInstance(factory(), x, z, 0.15);
|
const z = enemy.spawnAtCenter ? 0 : (Math.random() - 0.5) * 80;
|
||||||
|
this.spawnEnemyInstance(enemy, x, z, 0.15);
|
||||||
playSound('spawn', 0.8);
|
playSound('spawn', 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,49 +993,60 @@ export class Game {
|
|||||||
const camLeft = new THREE.Vector3();
|
const camLeft = new THREE.Vector3();
|
||||||
camLeft.crossVectors(new THREE.Vector3(0, 1, 0), camFwd).normalize();
|
camLeft.crossVectors(new THREE.Vector3(0, 1, 0), camFwd).normalize();
|
||||||
|
|
||||||
const moveDir = new THREE.Vector3();
|
const p = this.player;
|
||||||
if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) moveDir.add(camFwd);
|
let speed = p.stats.moveSpeed;
|
||||||
if (this.keys.has('KeyS') || this.keys.has('ArrowDown')) moveDir.sub(camFwd);
|
if (p.slowTimer > 0) speed *= p.slowFactor;
|
||||||
if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) moveDir.add(camLeft);
|
|
||||||
if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) moveDir.sub(camLeft);
|
|
||||||
|
|
||||||
let speed = this.player.stats.moveSpeed;
|
|
||||||
let isMoving = false;
|
let isMoving = false;
|
||||||
if (moveDir.length() > 0) {
|
|
||||||
moveDir.normalize();
|
if (p.pullTime > 0) {
|
||||||
this.player.velocity.set(moveDir.x * speed, 0, moveDir.z * speed);
|
// Tentakel-Griff: Spieler wird komplett zum Boss gezogen
|
||||||
isMoving = true;
|
p.pullTime -= dt;
|
||||||
|
p.velocity.copy(p.pullDir).multiplyScalar(20);
|
||||||
|
p.onGround = true;
|
||||||
|
p.body.position.y = 0.5;
|
||||||
} else {
|
} else {
|
||||||
this.player.velocity.set(0, 0, 0);
|
const moveDir = new THREE.Vector3();
|
||||||
|
if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) moveDir.add(camFwd);
|
||||||
|
if (this.keys.has('KeyS') || this.keys.has('ArrowDown')) moveDir.sub(camFwd);
|
||||||
|
if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) moveDir.add(camLeft);
|
||||||
|
if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) moveDir.sub(camLeft);
|
||||||
|
|
||||||
|
if (moveDir.length() > 0) {
|
||||||
|
moveDir.normalize();
|
||||||
|
p.velocity.set(moveDir.x * speed, 0, moveDir.z * speed);
|
||||||
|
isMoving = true;
|
||||||
|
} else {
|
||||||
|
p.velocity.set(0, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play walk/stand animations
|
// Play walk/stand animations
|
||||||
if (isMoving && this.player.getCurrentAnimation() !== 'walk') {
|
if (isMoving && p.getCurrentAnimation() !== 'walk') {
|
||||||
this.player.playAnimation('walk');
|
p.playAnimation('walk');
|
||||||
} else if (!isMoving && this.player.getCurrentAnimation() !== 'stand') {
|
} else if (!isMoving && p.getCurrentAnimation() !== 'stand') {
|
||||||
this.player.playAnimation('stand');
|
p.playAnimation('stand');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jump / gravity
|
// Jump / gravity
|
||||||
if (!this.player.onGround) {
|
if (!p.onGround && p.pullTime <= 0) {
|
||||||
this.player.jumpVelocity -= 20 * dt;
|
p.jumpVelocity -= 20 * dt;
|
||||||
this.player.body.position.y += this.player.jumpVelocity * dt;
|
p.body.position.y += p.jumpVelocity * dt;
|
||||||
if (this.player.body.position.y <= 0.5) {
|
if (p.body.position.y <= 0.5) {
|
||||||
this.player.body.position.y = 0.5;
|
p.body.position.y = 0.5;
|
||||||
this.player.jumpVelocity = 0;
|
p.jumpVelocity = 0;
|
||||||
this.player.onGround = true;
|
p.onGround = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply horizontal movement
|
// Apply horizontal movement
|
||||||
this.player.body.position.x += this.player.velocity.x * dt;
|
p.body.position.x += p.velocity.x * dt;
|
||||||
this.player.body.position.z += this.player.velocity.z * dt;
|
p.body.position.z += p.velocity.z * dt;
|
||||||
this.clampToArena(this.player.body.position);
|
this.clampToArena(p.body.position);
|
||||||
|
|
||||||
// Look at ground point
|
// Look at ground point
|
||||||
this.player.body.lookAt(
|
p.body.lookAt(
|
||||||
this.groundPoint.x,
|
this.groundPoint.x,
|
||||||
this.player.body.position.y,
|
p.body.position.y,
|
||||||
this.groundPoint.z
|
this.groundPoint.z
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1042,6 +1138,17 @@ export class Game {
|
|||||||
? ''
|
? ''
|
||||||
: `Seed: ${this.seed}`;
|
: `Seed: ${this.seed}`;
|
||||||
|
|
||||||
|
// Boss-Healthbar (sichtbar, solange ein Boss lebt)
|
||||||
|
const boss = this.enemies.find(e => e.isBoss && !e.dead);
|
||||||
|
if (boss) {
|
||||||
|
this.uiBossBar.style.display = 'block';
|
||||||
|
this.uiBossBarLabel.textContent = boss.displayName;
|
||||||
|
this.uiBossBarFill.style.width =
|
||||||
|
`${Math.max(0, (boss.health / boss.maxHealth) * 100)}%`;
|
||||||
|
} else {
|
||||||
|
this.uiBossBar.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
const xpPct = this.skillSystem.xpProgress();
|
const xpPct = this.skillSystem.xpProgress();
|
||||||
this.uiXpBarFill.style.width = `${Math.min(1, xpPct) * 100}%`;
|
this.uiXpBarFill.style.width = `${Math.min(1, xpPct) * 100}%`;
|
||||||
this.uiXpBarLabel.textContent = this.skillSystem.hasAnyUpgradeLeft()
|
this.uiXpBarLabel.textContent = this.skillSystem.hasAnyUpgradeLeft()
|
||||||
@@ -1093,6 +1200,13 @@ export class Game {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
triggerInkVignette() {
|
||||||
|
const el = this.uiInkVignette;
|
||||||
|
el.classList.remove('ink');
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add('ink');
|
||||||
|
}
|
||||||
|
|
||||||
onPlayerHurt(sourcePos: THREE.Vector3) {
|
onPlayerHurt(sourcePos: THREE.Vector3) {
|
||||||
const el = this.uiDamageVignette;
|
const el = this.uiDamageVignette;
|
||||||
el.classList.remove('hurt');
|
el.classList.remove('hurt');
|
||||||
@@ -1335,12 +1449,11 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state === 'playing' || this.state === 'levelup') {
|
if (this.state === 'playing' || this.state === 'levelup') {
|
||||||
const leveled = this.skillSystem.addXp(enemy.xp);
|
const levels = this.skillSystem.addXp(enemy.xp);
|
||||||
if (leveled) {
|
if (levels > 0) {
|
||||||
|
this.pendingPicks += levels;
|
||||||
if (this.state === 'playing') {
|
if (this.state === 'playing') {
|
||||||
this.openLevelUp();
|
this.openLevelUp();
|
||||||
} else {
|
|
||||||
this.pendingPicks++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export class Golem extends Brute {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.health = 800;
|
this.health = 800;
|
||||||
|
this.maxHealth = 800;
|
||||||
|
this.isBoss = true;
|
||||||
|
this.displayName = 'Golem';
|
||||||
this.speed = 3;
|
this.speed = 3;
|
||||||
this.xp = 80;
|
this.xp = 80;
|
||||||
this.contactDps = 50;
|
this.contactDps = 50;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { Game } from './Game';
|
||||||
|
|
||||||
|
const SPEED = 8;
|
||||||
|
const PLAYER_PROXIMITY = 1.4;
|
||||||
|
|
||||||
|
export class InkBlob {
|
||||||
|
body: THREE.Group;
|
||||||
|
target: THREE.Vector3;
|
||||||
|
private vel: THREE.Vector3;
|
||||||
|
private life = 6;
|
||||||
|
dead = false;
|
||||||
|
nearPlayer = false;
|
||||||
|
|
||||||
|
constructor(from: THREE.Vector3, target: THREE.Vector3) {
|
||||||
|
this.target = target.clone();
|
||||||
|
this.body = new THREE.Group();
|
||||||
|
this.body.position.copy(from);
|
||||||
|
|
||||||
|
this.vel = new THREE.Vector3().subVectors(target, from);
|
||||||
|
this.vel.y = 0;
|
||||||
|
const dist = this.vel.length();
|
||||||
|
if (dist > 0.01) {
|
||||||
|
this.vel.normalize().multiplyScalar(SPEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const glowMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xaa44ff,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
depthWrite: false,
|
||||||
|
});
|
||||||
|
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.5, 12, 12), glowMat);
|
||||||
|
this.body.add(glow);
|
||||||
|
|
||||||
|
const coreMat = new THREE.MeshToonMaterial({ color: 0x3a1060 });
|
||||||
|
const core = new THREE.Mesh(new THREE.SphereGeometry(0.28, 10, 10), coreMat);
|
||||||
|
this.body.add(core);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt: number, game: Game): boolean {
|
||||||
|
this.life -= dt;
|
||||||
|
this.body.position.x += this.vel.x * dt;
|
||||||
|
this.body.position.z += this.vel.z * dt;
|
||||||
|
this.body.position.y = 1.2 + Math.sin(this.life * 4) * 0.1;
|
||||||
|
|
||||||
|
if (game.player.health > 0 && !this.nearPlayer) {
|
||||||
|
const dist = this.body.position.distanceTo(game.player.body.position);
|
||||||
|
if (dist < PLAYER_PROXIMITY) {
|
||||||
|
this.nearPlayer = true;
|
||||||
|
game.triggerInkVignette();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = this.target.x - this.body.position.x;
|
||||||
|
const dz = this.target.z - this.body.position.z;
|
||||||
|
if (this.life <= 0 || dx * dx + dz * dz < 0.25) {
|
||||||
|
this.dead = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.body.removeFromParent();
|
||||||
|
for (const child of [...this.body.children]) {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.geometry.dispose();
|
||||||
|
(child.material as THREE.Material).dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { Game } from './Game';
|
||||||
|
|
||||||
|
const TICK = 0.5;
|
||||||
|
const TICK_DAMAGE = 4;
|
||||||
|
const SLOW_REFRESH = 0.15;
|
||||||
|
|
||||||
|
export class InkPuddle {
|
||||||
|
body: THREE.Group;
|
||||||
|
private radius: number;
|
||||||
|
private life: number;
|
||||||
|
private tickTimer = 0;
|
||||||
|
private puffs: THREE.Mesh[] = [];
|
||||||
|
|
||||||
|
constructor(position: THREE.Vector3, radius = 3.5, duration = 6) {
|
||||||
|
this.radius = radius;
|
||||||
|
this.life = duration;
|
||||||
|
this.body = new THREE.Group();
|
||||||
|
this.body.position.set(position.x, 0.1, position.z);
|
||||||
|
|
||||||
|
const puffGeom = new THREE.SphereGeometry(1, 12, 10);
|
||||||
|
const puffMat = new THREE.MeshToonMaterial({
|
||||||
|
color: 0x552277,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.35,
|
||||||
|
depthWrite: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const puff = new THREE.Mesh(puffGeom, puffMat);
|
||||||
|
const a = (i / 6) * Math.PI * 2;
|
||||||
|
puff.position.set(
|
||||||
|
Math.cos(a) * this.radius * 0.4,
|
||||||
|
0.3 + Math.random() * 0.3,
|
||||||
|
Math.sin(a) * this.radius * 0.4
|
||||||
|
);
|
||||||
|
puff.scale.setScalar(this.radius * (0.22 + Math.random() * 0.12));
|
||||||
|
this.body.add(puff);
|
||||||
|
this.puffs.push(puff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt: number, game: Game): boolean {
|
||||||
|
this.life -= dt;
|
||||||
|
if (this.life <= 0) return false;
|
||||||
|
|
||||||
|
const fade = Math.min(1, this.life / 0.6);
|
||||||
|
for (const puff of this.puffs) {
|
||||||
|
(puff.material as THREE.MeshToonMaterial).opacity = 0.35 * fade;
|
||||||
|
puff.rotation.y += dt * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.player.health > 0) {
|
||||||
|
const dist = Math.hypot(
|
||||||
|
game.player.body.position.x - this.body.position.x,
|
||||||
|
game.player.body.position.z - this.body.position.z
|
||||||
|
);
|
||||||
|
if (dist < this.radius) {
|
||||||
|
game.player.slowTimer = SLOW_REFRESH;
|
||||||
|
this.tickTimer -= dt;
|
||||||
|
if (this.tickTimer <= 0) {
|
||||||
|
this.tickTimer = TICK;
|
||||||
|
game.player.damage(TICK_DAMAGE, this.body.position);
|
||||||
|
game.triggerInkVignette();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.body.removeFromParent();
|
||||||
|
for (const puff of this.puffs) {
|
||||||
|
puff.geometry.dispose();
|
||||||
|
(puff.material as THREE.Material).dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { Game } from './Game';
|
||||||
|
import { Enemy } from './Enemy';
|
||||||
|
import { playSound } from './SoundManager';
|
||||||
|
import { InkBlob } from './InkBlob';
|
||||||
|
import { InkPuddle } from './InkPuddle';
|
||||||
|
|
||||||
|
const MAX_HEALTH = 3200;
|
||||||
|
const INK_COOLDOWN = 4.5;
|
||||||
|
const PULL_COOLDOWN = 7.5;
|
||||||
|
const PULL_MARK_COUNT = 5;
|
||||||
|
const PULL_MARK_RADIUS = 2.2;
|
||||||
|
const PULL_MARK_DIST = 6.5;
|
||||||
|
const PULL_CHARGE_TIME = 2.5;
|
||||||
|
const PULL_DAMAGE = 25;
|
||||||
|
const PULL_TIME = 0.6;
|
||||||
|
const SLAM_TIME = 0.5;
|
||||||
|
const SWEEP_RADIUS = 11;
|
||||||
|
const SWEEP_DURATION = 3.6;
|
||||||
|
const SWEEP_HIT_WIDTH = 1.2;
|
||||||
|
const SWEEP_DAMAGE = 25;
|
||||||
|
const ENRAGE_RATIO = 0.3;
|
||||||
|
const INK_RADIUS = 3.5;
|
||||||
|
const INK_DURATION = 6;
|
||||||
|
|
||||||
|
export class Krake extends Enemy {
|
||||||
|
spawnAtCenter = true;
|
||||||
|
private model: THREE.Group;
|
||||||
|
private game: Game | null = null;
|
||||||
|
private waveTime = 0;
|
||||||
|
private tentacles: THREE.Group[] = [];
|
||||||
|
private inkTimer = 2;
|
||||||
|
private pullTimer = 3.5;
|
||||||
|
private sweepTimer = 5;
|
||||||
|
private enraged = false;
|
||||||
|
private effectsReady = false;
|
||||||
|
|
||||||
|
private slamState: 'none' | 'grab' | 'slam' = 'none';
|
||||||
|
private slamChain!: THREE.Group;
|
||||||
|
private slamTime = 0;
|
||||||
|
private slamRetractFrom = 1;
|
||||||
|
private pullState: 'idle' | 'charge' = 'idle';
|
||||||
|
private pullChargeTime = 0;
|
||||||
|
private markOffsetAngle = 0;
|
||||||
|
private pullMarks: {
|
||||||
|
group: THREE.Group;
|
||||||
|
ringMat: THREE.MeshBasicMaterial;
|
||||||
|
discMat: THREE.MeshBasicMaterial;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
private sweepTents: { group: THREE.Group; angle: number; dir: number; progress: number }[] = [];
|
||||||
|
private sweepActive = false;
|
||||||
|
private sweepHit = false;
|
||||||
|
|
||||||
|
private inkBlobs: InkBlob[] = [];
|
||||||
|
private puddles: InkPuddle[] = [];
|
||||||
|
private skinMat!: THREE.MeshToonMaterial;
|
||||||
|
private darkMat!: THREE.MeshToonMaterial;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.health = MAX_HEALTH;
|
||||||
|
this.maxHealth = MAX_HEALTH;
|
||||||
|
this.isBoss = true;
|
||||||
|
this.displayName = 'Krake';
|
||||||
|
this.immovable = true;
|
||||||
|
this.speed = 0;
|
||||||
|
this.xp = 150;
|
||||||
|
this.contactDps = 0;
|
||||||
|
this.contactRadius = 0;
|
||||||
|
this.guaranteedDrop = true;
|
||||||
|
|
||||||
|
this.model = new THREE.Group();
|
||||||
|
this.fadeOutModel = this.model;
|
||||||
|
this.body.add(this.model);
|
||||||
|
|
||||||
|
const skinMat = new THREE.MeshToonMaterial({ color: 0x8a4a9a });
|
||||||
|
const darkMat = new THREE.MeshToonMaterial({ color: 0x5a2a6a });
|
||||||
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffee44 });
|
||||||
|
this.skinMat = skinMat;
|
||||||
|
this.darkMat = darkMat;
|
||||||
|
|
||||||
|
// Kopf
|
||||||
|
const head = new THREE.Mesh(new THREE.SphereGeometry(2.2, 24, 18), skinMat);
|
||||||
|
head.scale.set(1, 0.85, 1);
|
||||||
|
head.position.y = 3.1;
|
||||||
|
head.castShadow = true;
|
||||||
|
this.registerFadeMesh(head);
|
||||||
|
this.model.add(head);
|
||||||
|
|
||||||
|
// Augen auf +Z (Blickrichtung)
|
||||||
|
for (const side of [-1, 1]) {
|
||||||
|
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.4, 10, 8), eyeMat);
|
||||||
|
eye.position.set(side * 0.8, 3.45, 1.9);
|
||||||
|
this.registerFadeMesh(eye);
|
||||||
|
this.model.add(eye);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tentakel um den Kopf herum
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const chain = new THREE.Group();
|
||||||
|
const a = (i / 8) * Math.PI * 2;
|
||||||
|
for (let j = 0; j < 7; j++) {
|
||||||
|
const seg = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.42 - j * 0.04, 12, 10),
|
||||||
|
j % 2 === 0 ? skinMat : darkMat
|
||||||
|
);
|
||||||
|
seg.position.set(j * 0.55, -j * 0.28, 0);
|
||||||
|
this.registerFadeMesh(seg);
|
||||||
|
chain.add(seg);
|
||||||
|
}
|
||||||
|
chain.position.set(Math.cos(a) * 1.7, 1.1, Math.sin(a) * 1.7);
|
||||||
|
chain.rotation.y = -a;
|
||||||
|
this.model.add(chain);
|
||||||
|
this.tentacles.push(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weltraum-Effekte (Pull-Marken, Slam-Tentakel, Sweep-Tentakel)
|
||||||
|
this.buildEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildEffects() {
|
||||||
|
// 5 rote Boden-Marken (Arme) rund um den Oktopus
|
||||||
|
for (let i = 0; i < PULL_MARK_COUNT; i++) {
|
||||||
|
const ringMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xff3333,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthWrite: false,
|
||||||
|
});
|
||||||
|
const discMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xff2222,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0,
|
||||||
|
depthWrite: false,
|
||||||
|
});
|
||||||
|
const ring = new THREE.Mesh(
|
||||||
|
new THREE.RingGeometry(PULL_MARK_RADIUS - 0.15, PULL_MARK_RADIUS + 0.15, 32),
|
||||||
|
ringMat
|
||||||
|
);
|
||||||
|
ring.rotation.x = -Math.PI / 2;
|
||||||
|
ring.position.y = 0.05;
|
||||||
|
const disc = new THREE.Mesh(new THREE.CircleGeometry(PULL_MARK_RADIUS, 32), discMat);
|
||||||
|
disc.rotation.x = -Math.PI / 2;
|
||||||
|
disc.position.y = 0.03;
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.add(ring, disc);
|
||||||
|
group.visible = false;
|
||||||
|
this.pullMarks.push({ group, ringMat, discMat });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.slamChain = new THREE.Group();
|
||||||
|
for (let j = 1; j <= 10; j++) {
|
||||||
|
const seg = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.3, 8, 6),
|
||||||
|
new THREE.MeshToonMaterial({ color: 0x7a3a8a })
|
||||||
|
);
|
||||||
|
seg.position.set(0, 0, j * 0.5);
|
||||||
|
this.slamChain.add(seg);
|
||||||
|
}
|
||||||
|
this.slamChain.visible = false;
|
||||||
|
|
||||||
|
// Kreisende Sweep-Tentakel (dicker roter Arm mit Saugnaepfen, ueber den man springen kann)
|
||||||
|
// Zwei Exemplare: Phase 2 nutzt beide in entgegengesetzte Richtungen
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const group = this.buildSweepTentacle();
|
||||||
|
group.visible = false;
|
||||||
|
this.sweepTents.push({ group, angle: 0, dir: i === 0 ? 1 : -1, progress: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSweepTentacle(): THREE.Group {
|
||||||
|
const sweepMat = new THREE.MeshToonMaterial({ color: 0xdd4455 });
|
||||||
|
const cupMat = new THREE.MeshToonMaterial({ color: 0xffb3a0 });
|
||||||
|
const cupGeom = new THREE.SphereGeometry(1, 10, 6);
|
||||||
|
const group = new THREE.Group();
|
||||||
|
for (let j = 1; j <= 18; j++) {
|
||||||
|
const r = Math.max(0.22, 0.42 - j * 0.012);
|
||||||
|
const seg = new THREE.Mesh(new THREE.SphereGeometry(r, 12, 10), sweepMat);
|
||||||
|
seg.position.set(j * 0.6, 0.3, 0);
|
||||||
|
group.add(seg);
|
||||||
|
|
||||||
|
// Saugnapf: flache Scheibe, abwechselnd links/rechts
|
||||||
|
const side = j % 2 === 0 ? 1 : -1;
|
||||||
|
const cup = new THREE.Mesh(cupGeom, cupMat);
|
||||||
|
cup.scale.set(r * 0.5, r * 0.18, r * 0.5);
|
||||||
|
cup.position.set(j * 0.6, 0.3, side * (r + 0.02));
|
||||||
|
group.add(cup);
|
||||||
|
}
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
playAnim() {
|
||||||
|
// Prozedurale Optik
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnimation(dt: number) {
|
||||||
|
this.updateFadeDeath(dt, { duration: 1.2, sink: 0.5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureEffects(scene: THREE.Scene) {
|
||||||
|
if (this.effectsReady) return;
|
||||||
|
for (const mark of this.pullMarks) scene.add(mark.group);
|
||||||
|
scene.add(this.slamChain);
|
||||||
|
for (const tent of this.sweepTents) scene.add(tent.group);
|
||||||
|
this.effectsReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt: number, game: Game) {
|
||||||
|
this.game = game;
|
||||||
|
this.ensureEffects(game.scene);
|
||||||
|
this.waveTime += dt;
|
||||||
|
|
||||||
|
// Tentakel wippen
|
||||||
|
for (let i = 0; i < this.tentacles.length; i++) {
|
||||||
|
this.tentacles[i].rotation.z = Math.sin(this.waveTime * 2.2 + i * 0.9) * 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zum Spieler drehen
|
||||||
|
const playerPos = game.player.body.position;
|
||||||
|
const toPlayer = new THREE.Vector3().subVectors(playerPos, this.body.position);
|
||||||
|
toPlayer.y = 0;
|
||||||
|
if (toPlayer.length() > 0.1) {
|
||||||
|
this.body.lookAt(
|
||||||
|
this.body.position.x + toPlayer.x,
|
||||||
|
this.body.position.y,
|
||||||
|
this.body.position.z + toPlayer.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrage unter 50% HP
|
||||||
|
if (!this.enraged && this.health < MAX_HEALTH * ENRAGE_RATIO) {
|
||||||
|
this.enraged = true;
|
||||||
|
// Phase 2: Krake wird dunkelrot
|
||||||
|
this.skinMat.color.set(0x8a2233);
|
||||||
|
this.darkMat.color.set(0x551520);
|
||||||
|
playSound('damage', 0.8);
|
||||||
|
}
|
||||||
|
const inkCd = this.enraged ? INK_COOLDOWN * 0.6 : INK_COOLDOWN;
|
||||||
|
const pullCd = this.enraged ? PULL_COOLDOWN * 0.65 : PULL_COOLDOWN;
|
||||||
|
|
||||||
|
// Tinte spucken
|
||||||
|
this.inkTimer -= dt;
|
||||||
|
if (this.inkTimer <= 0) {
|
||||||
|
this.inkTimer = inkCd;
|
||||||
|
this.spitInk(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tentakel-Griff
|
||||||
|
if (this.pullState === 'idle') {
|
||||||
|
this.pullTimer -= dt;
|
||||||
|
if (this.pullTimer <= 0) {
|
||||||
|
this.pullTimer = pullCd;
|
||||||
|
this.startPullCharge(game);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.updatePullCharge(dt, game);
|
||||||
|
}
|
||||||
|
if (this.slamState === 'grab') {
|
||||||
|
// Tentakel bleibt am Spieler haengen, solange er gezogen wird
|
||||||
|
if (game.player.pullTime > 0) {
|
||||||
|
const playerPos = game.player.body.position;
|
||||||
|
this.slamChain.position.set(this.body.position.x, 3.2, this.body.position.z);
|
||||||
|
this.slamChain.lookAt(playerPos.x, 1, playerPos.z);
|
||||||
|
const dist = Math.max(1, Math.hypot(
|
||||||
|
playerPos.x - this.body.position.x,
|
||||||
|
playerPos.z - this.body.position.z
|
||||||
|
));
|
||||||
|
this.slamChain.scale.set(1, 1, Math.min(dist / 5, 2.5));
|
||||||
|
} else {
|
||||||
|
// Zug vorbei -> Tentakel zieht sich zurueck
|
||||||
|
this.slamRetractFrom = this.slamChain.scale.z;
|
||||||
|
this.slamState = 'slam';
|
||||||
|
this.slamTime = SLAM_TIME;
|
||||||
|
}
|
||||||
|
} else if (this.slamState === 'slam') {
|
||||||
|
this.updateSlam(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tentakel-Sweep
|
||||||
|
this.updateSweep(dt, game);
|
||||||
|
|
||||||
|
// Tinten-Blobs aktualisieren
|
||||||
|
for (let i = this.inkBlobs.length - 1; i >= 0; i--) {
|
||||||
|
const blob = this.inkBlobs[i];
|
||||||
|
if (!blob.update(dt, game)) {
|
||||||
|
this.inkBlobs.splice(i, 1);
|
||||||
|
const puddle = new InkPuddle(
|
||||||
|
blob.body.position,
|
||||||
|
this.enraged ? INK_RADIUS + 0.5 : INK_RADIUS,
|
||||||
|
INK_DURATION
|
||||||
|
);
|
||||||
|
game.scene.add(puddle.body);
|
||||||
|
this.puddles.push(puddle);
|
||||||
|
blob.dispose();
|
||||||
|
playSound('explosion', 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tintenpfützen aktualisieren
|
||||||
|
for (let i = this.puddles.length - 1; i >= 0; i--) {
|
||||||
|
const puddle = this.puddles[i];
|
||||||
|
if (!puddle.update(dt, game)) {
|
||||||
|
this.puddles.splice(i, 1);
|
||||||
|
puddle.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private spitInk(game: Game) {
|
||||||
|
const from = new THREE.Vector3(this.body.position.x, 3.0, this.body.position.z);
|
||||||
|
const target = game.player.body.position.clone();
|
||||||
|
target.y = 0;
|
||||||
|
|
||||||
|
if (this.enraged) {
|
||||||
|
const t2 = target.clone().add(
|
||||||
|
new THREE.Vector3((Math.random() - 0.5) * 8, 0, (Math.random() - 0.5) * 8)
|
||||||
|
);
|
||||||
|
const blob2 = new InkBlob(from, t2);
|
||||||
|
game.scene.add(blob2.body);
|
||||||
|
this.inkBlobs.push(blob2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new InkBlob(from, target);
|
||||||
|
game.scene.add(blob.body);
|
||||||
|
this.inkBlobs.push(blob);
|
||||||
|
playSound('spawn', 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startPullCharge(game: Game) {
|
||||||
|
this.pullState = 'charge';
|
||||||
|
this.pullChargeTime = PULL_CHARGE_TIME;
|
||||||
|
this.markOffsetAngle = Math.random() * Math.PI * 2;
|
||||||
|
const cx = this.body.position.x;
|
||||||
|
const cz = this.body.position.z;
|
||||||
|
for (let i = 0; i < PULL_MARK_COUNT; i++) {
|
||||||
|
const a = this.markOffsetAngle + (i / PULL_MARK_COUNT) * Math.PI * 2;
|
||||||
|
const mark = this.pullMarks[i];
|
||||||
|
mark.group.position.set(
|
||||||
|
cx + Math.cos(a) * PULL_MARK_DIST,
|
||||||
|
0.06,
|
||||||
|
cz + Math.sin(a) * PULL_MARK_DIST
|
||||||
|
);
|
||||||
|
mark.group.visible = true;
|
||||||
|
}
|
||||||
|
playSound('spawn', 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePullCharge(dt: number, game: Game) {
|
||||||
|
this.pullChargeTime -= dt;
|
||||||
|
const pulse = Math.abs(Math.sin(this.waveTime * 9));
|
||||||
|
for (const mark of this.pullMarks) {
|
||||||
|
mark.ringMat.opacity = 0.3 + 0.5 * pulse;
|
||||||
|
mark.discMat.opacity = 0.1 + 0.08 * pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pullChargeTime <= 0) {
|
||||||
|
this.pullState = 'idle';
|
||||||
|
const playerPos = game.player.body.position;
|
||||||
|
|
||||||
|
// Spieler in einer Marke? -> heranziehen
|
||||||
|
let grabbed = false;
|
||||||
|
for (const mark of this.pullMarks) {
|
||||||
|
const dx = playerPos.x - mark.group.position.x;
|
||||||
|
const dz = playerPos.z - mark.group.position.z;
|
||||||
|
if (Math.hypot(dx, dz) < PULL_MARK_RADIUS) {
|
||||||
|
grabbed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const mark of this.pullMarks) {
|
||||||
|
mark.group.visible = false;
|
||||||
|
mark.ringMat.opacity = 0;
|
||||||
|
mark.discMat.opacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grabbed && playerPos.y <= 0.8) {
|
||||||
|
// Slam-Tentakel greift den Spieler und bleibt haengen
|
||||||
|
this.slamChain.position.set(this.body.position.x, 3.2, this.body.position.z);
|
||||||
|
this.slamChain.lookAt(playerPos.x, 1, playerPos.z);
|
||||||
|
this.slamChain.scale.set(1, 1, 1);
|
||||||
|
this.slamChain.visible = true;
|
||||||
|
this.slamState = 'grab';
|
||||||
|
|
||||||
|
game.player.damage(PULL_DAMAGE, this.body.position);
|
||||||
|
const dir = new THREE.Vector3().subVectors(this.body.position, playerPos);
|
||||||
|
dir.y = 0;
|
||||||
|
if (dir.lengthSq() < 0.01) dir.set(0, 0, 1);
|
||||||
|
dir.normalize();
|
||||||
|
game.player.pullDir.copy(dir);
|
||||||
|
game.player.pullTime = PULL_TIME;
|
||||||
|
playSound('damage', 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSlam(dt: number) {
|
||||||
|
this.slamTime -= dt;
|
||||||
|
const s = Math.max(0, this.slamTime / SLAM_TIME);
|
||||||
|
this.slamChain.scale.set(1, 1, this.slamRetractFrom * s);
|
||||||
|
|
||||||
|
if (this.slamTime <= 0) {
|
||||||
|
this.slamChain.visible = false;
|
||||||
|
this.slamState = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSweep(dt: number, game: Game) {
|
||||||
|
if (!this.sweepActive) {
|
||||||
|
this.sweepTimer -= dt;
|
||||||
|
const cd = this.enraged ? 3.2 : 4.5;
|
||||||
|
if (this.sweepTimer <= 0) {
|
||||||
|
this.sweepTimer = cd;
|
||||||
|
this.sweepActive = true;
|
||||||
|
this.sweepHit = false;
|
||||||
|
const start = Math.random() * Math.PI * 2;
|
||||||
|
for (let i = 0; i < this.sweepTents.length; i++) {
|
||||||
|
const tent = this.sweepTents[i];
|
||||||
|
tent.angle = start + i * Math.PI;
|
||||||
|
tent.progress = 0;
|
||||||
|
tent.group.position.set(this.body.position.x, 0.06, this.body.position.z);
|
||||||
|
// Tentakel liegt auf lokaler +X; rotation.y = -Winkel zeigt auf (cos, sin)
|
||||||
|
tent.group.rotation.y = -tent.angle;
|
||||||
|
tent.group.visible = i === 0 || this.enraged;
|
||||||
|
}
|
||||||
|
playSound('spawn', 0.6);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotSpeed = ((Math.PI * 2) / (this.enraged ? 3.0 : SWEEP_DURATION)) * dt;
|
||||||
|
|
||||||
|
for (const tent of this.sweepTents) {
|
||||||
|
// Phase 2: zweiter Tentakel kreist gegensinnig
|
||||||
|
if (tent.dir === -1 && !this.enraged) {
|
||||||
|
tent.group.visible = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tent.angle += tent.dir * rotSpeed;
|
||||||
|
tent.progress += rotSpeed;
|
||||||
|
tent.group.rotation.y = -tent.angle;
|
||||||
|
if (!this.sweepHit) {
|
||||||
|
const playerPos = game.player.body.position;
|
||||||
|
const dx = playerPos.x - this.body.position.x;
|
||||||
|
const dz = playerPos.z - this.body.position.z;
|
||||||
|
const dist = Math.hypot(dx, dz);
|
||||||
|
if (dist > 0.5 && dist < SWEEP_RADIUS + 0.5) {
|
||||||
|
const dirX = Math.cos(tent.angle);
|
||||||
|
const dirZ = Math.sin(tent.angle);
|
||||||
|
// Abstand des Spielers zur Tentakel-Linie (radial vom Boss)
|
||||||
|
const perp = Math.abs(dx * dirZ - dz * dirX);
|
||||||
|
const along = dx * dirX + dz * dirZ;
|
||||||
|
// Ueberspringbar: nur Treffer, solange der Spieler am Boden ist
|
||||||
|
if (along > 0.3 && perp < SWEEP_HIT_WIDTH && playerPos.y <= 0.6) {
|
||||||
|
this.sweepHit = true;
|
||||||
|
game.player.damage(SWEEP_DAMAGE, this.body.position);
|
||||||
|
playSound('damage', 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sweepTents.every(t => t.progress >= Math.PI * 2)) {
|
||||||
|
this.sweepActive = false;
|
||||||
|
for (const tent of this.sweepTents) {
|
||||||
|
tent.group.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDeath(_game: Game) {
|
||||||
|
this.cleanupEffects();
|
||||||
|
this.beginFadeDeath();
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispose() {
|
||||||
|
super.dispose();
|
||||||
|
this.cleanupEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupEffects() {
|
||||||
|
for (const blob of this.inkBlobs) {
|
||||||
|
blob.dispose();
|
||||||
|
}
|
||||||
|
this.inkBlobs = [];
|
||||||
|
for (const puddle of this.puddles) {
|
||||||
|
puddle.dispose();
|
||||||
|
}
|
||||||
|
this.puddles = [];
|
||||||
|
if (this.game) {
|
||||||
|
for (const mark of this.pullMarks) {
|
||||||
|
this.game.scene.remove(mark.group);
|
||||||
|
}
|
||||||
|
this.game.scene.remove(this.slamChain);
|
||||||
|
for (const tent of this.sweepTents) {
|
||||||
|
this.game.scene.remove(tent.group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const mark of this.pullMarks) {
|
||||||
|
for (const child of mark.group.children) {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.geometry.dispose();
|
||||||
|
(child.material as THREE.Material).dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pullMarks = [];
|
||||||
|
for (const tent of this.sweepTents) {
|
||||||
|
for (const child of tent.group.children) {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.geometry.dispose();
|
||||||
|
(child.material as THREE.Material).dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const child of this.slamChain.children) {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.geometry.dispose();
|
||||||
|
(child.material as THREE.Material).dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-2
@@ -30,6 +30,10 @@ export class Player {
|
|||||||
onGround = true;
|
onGround = true;
|
||||||
shieldHp = 0;
|
shieldHp = 0;
|
||||||
shieldTimer = 0;
|
shieldTimer = 0;
|
||||||
|
slowTimer = 0;
|
||||||
|
slowFactor = 0.6;
|
||||||
|
pullTime = 0;
|
||||||
|
pullDir = new THREE.Vector3();
|
||||||
stats: PlayerStats = {
|
stats: PlayerStats = {
|
||||||
maxHealth: 100,
|
maxHealth: 100,
|
||||||
regen: 0,
|
regen: 0,
|
||||||
@@ -50,6 +54,7 @@ export class Player {
|
|||||||
private flashTime = 0;
|
private flashTime = 0;
|
||||||
private static readonly BASE_COLOR = 0x44aa44;
|
private static readonly BASE_COLOR = 0x44aa44;
|
||||||
private static readonly HURT_COLOR = 0xff5555;
|
private static readonly HURT_COLOR = 0xff5555;
|
||||||
|
private static readonly SLOW_COLOR = 0x9944cc;
|
||||||
private static readonly HURT_FLASH_TIME = 0.3;
|
private static readonly HURT_FLASH_TIME = 0.3;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -217,14 +222,23 @@ export class Player {
|
|||||||
updateAnimations(dt: number) {
|
updateAnimations(dt: number) {
|
||||||
if (this.mixer) this.mixer.update(dt);
|
if (this.mixer) this.mixer.update(dt);
|
||||||
if (this.hurtCooldown > 0) this.hurtCooldown -= dt;
|
if (this.hurtCooldown > 0) this.hurtCooldown -= dt;
|
||||||
|
if (this.slowTimer > 0) this.slowTimer -= dt;
|
||||||
|
|
||||||
|
let color: THREE.Color;
|
||||||
if (this.flashTime > 0) {
|
if (this.flashTime > 0) {
|
||||||
this.flashTime -= dt;
|
this.flashTime -= dt;
|
||||||
const t = Math.max(0, this.flashTime) / Player.HURT_FLASH_TIME;
|
const t = Math.max(0, this.flashTime) / Player.HURT_FLASH_TIME;
|
||||||
const color = new THREE.Color(Player.BASE_COLOR).lerp(
|
color = new THREE.Color(Player.BASE_COLOR).lerp(
|
||||||
new THREE.Color(Player.HURT_COLOR), t
|
new THREE.Color(Player.HURT_COLOR), t
|
||||||
);
|
);
|
||||||
for (const mat of this.bodyMaterials) mat.color.copy(color);
|
} else if (this.slowTimer > 0) {
|
||||||
|
color = new THREE.Color(Player.BASE_COLOR).lerp(
|
||||||
|
new THREE.Color(Player.SLOW_COLOR), 0.55
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
color = new THREE.Color(Player.BASE_COLOR);
|
||||||
}
|
}
|
||||||
|
for (const mat of this.bodyMaterials) mat.color.copy(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGame(game: Game) {
|
setGame(game: Game) {
|
||||||
@@ -316,6 +330,8 @@ export class Player {
|
|||||||
this.health = 100;
|
this.health = 100;
|
||||||
this.shieldHp = 0;
|
this.shieldHp = 0;
|
||||||
this.shieldTimer = 0;
|
this.shieldTimer = 0;
|
||||||
|
this.slowTimer = 0;
|
||||||
|
this.pullTime = 0;
|
||||||
this.body.position.set(0, 0.5, 0);
|
this.body.position.set(0, 0.5, 0);
|
||||||
this.velocity.set(0, 0, 0);
|
this.velocity.set(0, 0, 0);
|
||||||
this.jumpVelocity = 0;
|
this.jumpVelocity = 0;
|
||||||
|
|||||||
+6
-5
@@ -233,15 +233,16 @@ export class SkillSystem {
|
|||||||
return this.xp / this.xpNeeded();
|
return this.xp / this.xpNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
addXp(amount: number): boolean {
|
addXp(amount: number): number {
|
||||||
this.xp += amount;
|
this.xp += amount;
|
||||||
if (!this.hasAnyUpgradeLeft()) return false;
|
if (!this.hasAnyUpgradeLeft()) return 0;
|
||||||
if (this.xp >= this.xpNeeded()) {
|
let leveled = 0;
|
||||||
|
while (this.xp >= this.xpNeeded()) {
|
||||||
this.xp -= this.xpNeeded();
|
this.xp -= this.xpNeeded();
|
||||||
this.level++;
|
this.level++;
|
||||||
return true;
|
leveled++;
|
||||||
}
|
}
|
||||||
return false;
|
return leveled;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPendingLevelUp(): boolean {
|
hasPendingLevelUp(): boolean {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class Turret extends Enemy {
|
|||||||
this.speed = 0;
|
this.speed = 0;
|
||||||
this.xp = 25;
|
this.xp = 25;
|
||||||
this.contactDps = 0;
|
this.contactDps = 0;
|
||||||
|
this.immovable = true;
|
||||||
|
|
||||||
this.model = new THREE.Group();
|
this.model = new THREE.Group();
|
||||||
this.fadeOutModel = this.model;
|
this.fadeOutModel = this.model;
|
||||||
|
|||||||
Reference in New Issue
Block a user