Add six new enemies and spawn enemies within view

This commit is contained in:
2026-08-20 10:54:51 +02:00
parent 2bffefb02c
commit e6a370f952
10 changed files with 1524 additions and 13 deletions
+93 -11
View File
@@ -17,6 +17,12 @@ import { Necromancer } from './Necromancer';
import { Dragon } from './Dragon';
import { Crab } from './Crab';
import { Turret } from './Turret';
import { Shaman } from './Shaman';
import { Archer } from './Archer';
import { Bomber } from './Bomber';
import { Wisp } from './Wisp';
import { Turtle } from './Turtle';
import { SporeMushroom } from './SporeMushroom';
import { EnemyProjectile } from './EnemyProjectile';
import { Sword } from './Sword';
import { Staff } from './Staff';
@@ -82,6 +88,13 @@ export class Game {
spawns = 0;
killed = 0;
// Stationäre Gegner, die zu lange außerhalb des Bildschirms sind, werden
// wieder in die Nähe des Spielers teleportiert (Off-Screen-Reset).
private offscreenTimers = new Map<Enemy, number>();
private static readonly OFFSCREEN_REPOSITION_TIME = 8;
private static readonly SPAWN_VIEW_MIN = 10;
private static readonly SPAWN_VIEW_MAX = 24;
// 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() }],
@@ -765,6 +778,7 @@ export class Game {
this.spawns = 0;
this.killed = 0;
this.bossFightActive = false;
this.offscreenTimers.clear();
this.shakeTime = 0;
this.pendingPicks = 0;
this.hideLevelUp();
@@ -931,8 +945,13 @@ export class Game {
}
private spawnEnemy() {
const x = (Math.random() - 0.5) * 80;
const z = (Math.random() - 0.5) * 80;
// Immer im Sichtbereich spawnen (Ring um den Spieler), damit auch
// stationäre Gegner nicht auf der Arena gesucht werden müssen.
const p = this.player.body.position;
const a = Math.random() * Math.PI * 2;
const r = Game.SPAWN_VIEW_MIN + Math.random() * (Game.SPAWN_VIEW_MAX - Game.SPAWN_VIEW_MIN);
const x = Math.max(-46, Math.min(46, p.x + Math.cos(a) * r));
const z = Math.max(-46, Math.min(46, p.z + Math.sin(a) * r));
const weights = this.enemyWeights(this.spawns);
let total = 0;
@@ -957,22 +976,33 @@ export class Game {
case 'brute': this.spawnEnemyInstance(new Brute(), x, z, 0.15); break;
case 'crab': this.spawnEnemyInstance(new Crab(), x, z, 0.1); break;
case 'turret': this.spawnEnemyInstance(new Turret(), x, z, 0.1); break;
case 'shaman': this.spawnEnemyInstance(new Shaman(), x, z, 0.1); break;
case 'archer': this.spawnEnemyInstance(new Archer(), x, z, 0.1); break;
case 'bomber': this.spawnEnemyInstance(new Bomber(), x, z, 0.1); break;
case 'wisp': this.spawnEnemyInstance(new Wisp(), x, z, 1.0); break;
case 'turtle': this.spawnEnemyInstance(new Turtle(), x, z, 0.1); break;
case 'spore': this.spawnEnemyInstance(new SporeMushroom(), x, z, 0.1); break;
}
}
private enemyWeights(wave: number): Map<string, number> {
const w = new Map<string, number>();
if (wave >= 7) {
w.set('spider', 25).set('ghost', 15).set('frog', 15).set('slime', 10)
.set('bat', 10).set('turret', 10).set('crab', 10).set('bee', 5);
w.set('spider', 20).set('ghost', 12).set('frog', 12).set('slime', 8)
.set('bat', 8).set('turret', 8).set('crab', 8).set('bee', 5)
.set('archer', 6).set('bomber', 6).set('shaman', 4).set('wisp', 4)
.set('turtle', 4).set('spore', 5);
} else if (wave >= 6) {
w.set('spider', 35).set('ghost', 20).set('frog', 15).set('slime', 10)
.set('bat', 10).set('turret', 10);
w.set('spider', 28).set('ghost', 16).set('frog', 12).set('slime', 8)
.set('bat', 8).set('turret', 8)
.set('archer', 7).set('bomber', 7).set('wisp', 4).set('spore', 5);
} else if (wave >= 5) {
w.set('spider', 45).set('ghost', 20).set('frog', 15).set('slime', 10)
.set('bat', 10);
w.set('spider', 35).set('ghost', 16).set('frog', 12).set('slime', 8)
.set('bat', 8)
.set('archer', 7).set('bomber', 7).set('spore', 5);
} else if (wave >= 4) {
w.set('spider', 55).set('ghost', 25).set('frog', 20);
w.set('spider', 45).set('ghost', 20).set('frog', 16)
.set('archer', 6).set('bomber', 6);
} else if (wave >= 3) {
w.set('spider', 75).set('ghost', 25);
} else {
@@ -989,8 +1019,18 @@ export class Game {
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;
let x = 0;
let z = 0;
if (enemy.spawnAtCenter) {
// Stationäre Bosse (Krake, Spinnenkönigin) spawnen in der Arena-Mitte
} else {
// Alle anderen Bosse im Sichtbereich spawnen
const p = this.player.body.position;
const a = Math.random() * Math.PI * 2;
const r = Game.SPAWN_VIEW_MIN + Math.random() * (Game.SPAWN_VIEW_MAX - Game.SPAWN_VIEW_MIN);
x = Math.max(-46, Math.min(46, p.x + Math.cos(a) * r));
z = Math.max(-46, Math.min(46, p.z + Math.sin(a) * r));
}
this.spawnEnemyInstance(enemy, x, z, 0.15);
playSound('spawn', 0.8);
}
@@ -1123,6 +1163,47 @@ export class Game {
pos.z = Math.max(-47, Math.min(47, pos.z));
}
// Stationäre Gegner (Turret, Sporen-Pilz, Krake, Spinnenkönigin), die zu
// lange außerhalb des Bildschirms sind, werden wieder in Sicht teleportiert.
private updateOffscreenReposition(dt: number) {
for (const enemy of this.enemies) {
if (enemy.dead) {
this.offscreenTimers.delete(enemy);
continue;
}
if (!enemy.immovable) continue;
if (this.isOnScreen(enemy.body.position)) {
this.offscreenTimers.delete(enemy);
} else {
const t = (this.offscreenTimers.get(enemy) ?? 0) + dt;
if (t >= Game.OFFSCREEN_REPOSITION_TIME) {
this.offscreenTimers.delete(enemy);
this.repositionToView(enemy);
} else {
this.offscreenTimers.set(enemy, t);
}
}
}
}
// NDC-Projektion mit Toleranz: |x|,|y| < 1.15 gilt als im Bild
private isOnScreen(pos: THREE.Vector3): boolean {
const v = pos.clone().project(this.camera);
return v.x > -1.15 && v.x < 1.15 && v.y > -1.15 && v.y < 1.15 && v.z < 1;
}
private repositionToView(enemy: Enemy) {
const p = this.player.body.position;
const a = Math.random() * Math.PI * 2;
const r = Game.SPAWN_VIEW_MIN + Math.random() * (Game.SPAWN_VIEW_MAX - Game.SPAWN_VIEW_MIN);
enemy.body.position.set(
Math.max(-46, Math.min(46, p.x + Math.cos(a) * r)),
enemy.body.position.y,
Math.max(-46, Math.min(46, p.z + Math.sin(a) * r))
);
playSound('spawn', 0.5);
}
private updateEnemies(dt: number) {
for (const enemy of this.enemies) {
if (enemy.dead || this.state !== 'playing') {
@@ -1369,6 +1450,7 @@ export class Game {
this.updateGroundPoint();
this.updatePlayerMovement(dt);
this.updateEnemies(dt);
this.updateOffscreenReposition(dt);
this.updateFireballs(dt);
this.updateHitzones();