Add Krake boss with ink, pull marks and sweep tentacles plus boss mode

This commit is contained in:
2026-08-19 13:59:39 +02:00
parent 1a4cccda6e
commit 496c4af50e
12 changed files with 911 additions and 53 deletions
+158 -45
View File
@@ -9,6 +9,7 @@ import { BeeSwarm } from './BeeSwarm';
import { Slime } from './Slime';
import { Brute } from './Brute';
import { Golem } from './Golem';
import { Krake } from './Krake';
import { Crab } from './Crab';
import { Turret } from './Turret';
import { EnemyProjectile } from './EnemyProjectile';
@@ -74,8 +75,11 @@ export class Game {
spawns = 0;
killed = 0;
// Boss-Wellen: Welle -> Boss-Factory (aktuell nur Welle 10; 20/30 folgen)
private bossWaves = new Map<number, () => Enemy>([[10, () => new Golem()]]);
// Boss-Wellen: Welle -> Boss (Name fuer Boss-Modus-UI, Factory fuer Spawn)
private bossWaves = new Map<number, { name: string; spawn: () => Enemy }>([
[10, { name: 'Golem', spawn: () => new Golem() }],
[20, { name: 'Krake', spawn: () => new Krake() }],
]);
private bossFightActive = false;
// Debug hitzone visualization
@@ -104,6 +108,9 @@ export class Game {
private uiGameOverStats!: HTMLElement;
private uiMainMenu!: HTMLElement;
private uiSeedInput!: HTMLInputElement;
private uiBossMode!: HTMLElement;
private uiBossModeButtons!: HTMLElement;
private cheatBuffer = '';
private uiLevelLabel!: HTMLElement;
private uiSeedLabel!: HTMLElement;
private uiXpBarFill!: HTMLElement;
@@ -118,6 +125,10 @@ export class Game {
private uiOfferDescs: HTMLElement[] = [];
private uiDamageVignette!: HTMLElement;
private uiLowHpVignette!: HTMLElement;
private uiInkVignette!: HTMLElement;
private uiBossBar!: HTMLElement;
private uiBossBarFill!: HTMLElement;
private uiBossBarLabel!: HTMLElement;
private uiTrapContainer!: HTMLElement;
private uiTrapCD!: HTMLElement;
private uiGasContainer!: HTMLElement;
@@ -218,6 +229,13 @@ export class Game {
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
this.uiDamageVignette = document.getElementById('damage-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.uiTrapCD = document.querySelector('#trap-cooldown .cooldown-fill')!;
this.uiGasContainer = document.getElementById('gas-cooldown')!;
@@ -278,13 +296,26 @@ export class Game {
e.preventDefault();
return;
}
if (e.code === 'Space' && this.state === 'playing') {
if (e.code === 'Space' && this.state === 'playing' && this.player.pullTime <= 0) {
this.player.jump();
e.preventDefault();
}
if (e.code === 'KeyH') {
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);
if (slotIdx >= 0 && this.state === 'playing' && this.hotbar[slotIdx]) {
this.triggerHotbarSkill(this.hotbar[slotIdx]!);
@@ -498,7 +529,11 @@ export class Game {
}
}
this.resetWorld();
this.logState('playing', 'startGame');
this.beginRun();
}
private beginRun() {
this.logState('playing', 'beginRun');
this.state = 'playing';
this.paused = false;
this.hidePauseOverlay();
@@ -514,6 +549,51 @@ export class Game {
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() {
const el = document.activeElement;
if (el instanceof HTMLElement) el.blur();
@@ -728,15 +808,19 @@ export class Game {
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() {
if (this.state !== 'levelup' || this.selectedOffer < 0) return;
const offer = this.offers[this.selectedOffer];
this.selectedOffer = -1;
this.skillSystem.apply(offer.id);
if (HOTBAR_SKILLS.includes(offer.id) && !this.hotbar.includes(offer.id)) {
const free = this.hotbar.indexOf(null);
if (free >= 0) this.hotbar[free] = offer.id;
}
this.assignHotbarSkill(offer.id);
this.player.applySkillSystem(this.skillSystem);
this.syncSkillVisibility();
this.blurFocus();
@@ -841,11 +925,12 @@ export class Game {
}
private spawnBossForWave(wave: number) {
const factory = this.bossWaves.get(wave);
if (!factory) return;
const x = (Math.random() - 0.5) * 80;
const z = (Math.random() - 0.5) * 80;
this.spawnEnemyInstance(factory(), x, z, 0.15);
const entry = this.bossWaves.get(wave);
if (!entry) return;
const enemy = entry.spawn();
const x = enemy.spawnAtCenter ? 0 : (Math.random() - 0.5) * 80;
const z = enemy.spawnAtCenter ? 0 : (Math.random() - 0.5) * 80;
this.spawnEnemyInstance(enemy, x, z, 0.15);
playSound('spawn', 0.8);
}
@@ -908,49 +993,60 @@ export class Game {
const camLeft = new THREE.Vector3();
camLeft.crossVectors(new THREE.Vector3(0, 1, 0), camFwd).normalize();
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);
let speed = this.player.stats.moveSpeed;
const p = this.player;
let speed = p.stats.moveSpeed;
if (p.slowTimer > 0) speed *= p.slowFactor;
let isMoving = false;
if (moveDir.length() > 0) {
moveDir.normalize();
this.player.velocity.set(moveDir.x * speed, 0, moveDir.z * speed);
isMoving = true;
if (p.pullTime > 0) {
// Tentakel-Griff: Spieler wird komplett zum Boss gezogen
p.pullTime -= dt;
p.velocity.copy(p.pullDir).multiplyScalar(20);
p.onGround = true;
p.body.position.y = 0.5;
} 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
if (isMoving && this.player.getCurrentAnimation() !== 'walk') {
this.player.playAnimation('walk');
} else if (!isMoving && this.player.getCurrentAnimation() !== 'stand') {
this.player.playAnimation('stand');
if (isMoving && p.getCurrentAnimation() !== 'walk') {
p.playAnimation('walk');
} else if (!isMoving && p.getCurrentAnimation() !== 'stand') {
p.playAnimation('stand');
}
// Jump / gravity
if (!this.player.onGround) {
this.player.jumpVelocity -= 20 * dt;
this.player.body.position.y += this.player.jumpVelocity * dt;
if (this.player.body.position.y <= 0.5) {
this.player.body.position.y = 0.5;
this.player.jumpVelocity = 0;
this.player.onGround = true;
if (!p.onGround && p.pullTime <= 0) {
p.jumpVelocity -= 20 * dt;
p.body.position.y += p.jumpVelocity * dt;
if (p.body.position.y <= 0.5) {
p.body.position.y = 0.5;
p.jumpVelocity = 0;
p.onGround = true;
}
}
// Apply horizontal movement
this.player.body.position.x += this.player.velocity.x * dt;
this.player.body.position.z += this.player.velocity.z * dt;
this.clampToArena(this.player.body.position);
p.body.position.x += p.velocity.x * dt;
p.body.position.z += p.velocity.z * dt;
this.clampToArena(p.body.position);
// Look at ground point
this.player.body.lookAt(
p.body.lookAt(
this.groundPoint.x,
this.player.body.position.y,
p.body.position.y,
this.groundPoint.z
);
}
@@ -1042,6 +1138,17 @@ export class Game {
? ''
: `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();
this.uiXpBarFill.style.width = `${Math.min(1, xpPct) * 100}%`;
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) {
const el = this.uiDamageVignette;
el.classList.remove('hurt');
@@ -1335,12 +1449,11 @@ export class Game {
}
if (this.state === 'playing' || this.state === 'levelup') {
const leveled = this.skillSystem.addXp(enemy.xp);
if (leveled) {
const levels = this.skillSystem.addXp(enemy.xp);
if (levels > 0) {
this.pendingPicks += levels;
if (this.state === 'playing') {
this.openLevelUp();
} else {
this.pendingPicks++;
}
}
}