Add Krake boss with ink, pull marks and sweep tentacles plus boss mode
This commit is contained in:
+158
-45
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user