Add six new enemies and spawn enemies within view
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import * as THREE from 'three';
|
||||
import type { Game } from './Game';
|
||||
import { Enemy } from './Enemy';
|
||||
import { playSound } from './SoundManager';
|
||||
|
||||
const SHOOT_INTERVAL = 2.5;
|
||||
const SHOOT_RANGE = 18;
|
||||
const BOLT_SPEED = 9;
|
||||
const BOLT_DAMAGE = 12;
|
||||
const BOLT_GRAVITY = 10;
|
||||
const CLOUD_RADIUS = 2.4;
|
||||
const CLOUD_DURATION = 3;
|
||||
const CLOUD_TICK = 0.7;
|
||||
const CLOUD_TICK_DAMAGE = 4;
|
||||
const HIT_FLASH_TIME = 0.14;
|
||||
|
||||
// Sporen-Pilz: stationär, schießt Bogen-Sporen; am Einschlag entsteht eine Sporenwolke (DoT)
|
||||
export class SporeMushroom extends Enemy {
|
||||
private model: THREE.Group;
|
||||
private stalkMat!: THREE.MeshToonMaterial;
|
||||
private capMat!: THREE.MeshToonMaterial;
|
||||
private glowMat!: THREE.MeshBasicMaterial;
|
||||
private cap!: THREE.Mesh;
|
||||
private shootTimer = 1 + Math.random();
|
||||
private swayTime = Math.random() * Math.PI * 2;
|
||||
private hitFlash = 0;
|
||||
private bolts: SporeBolt[] = [];
|
||||
private clouds: SporeCloud[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.health = 70;
|
||||
this.speed = 0;
|
||||
this.xp = 25;
|
||||
this.contactDps = 0;
|
||||
this.immovable = true;
|
||||
|
||||
this.model = new THREE.Group();
|
||||
this.fadeOutModel = this.model;
|
||||
this.body.add(this.model);
|
||||
|
||||
this.stalkMat = new THREE.MeshToonMaterial({ color: 0xe8dcc0 });
|
||||
this.capMat = new THREE.MeshToonMaterial({ color: 0x55aa44 });
|
||||
this.glowMat = new THREE.MeshBasicMaterial({ color: 0x88ff66 });
|
||||
|
||||
// Stiel
|
||||
const stalk = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.32, 0.9, 10), this.stalkMat);
|
||||
stalk.position.y = 0.45;
|
||||
stalk.castShadow = true;
|
||||
this.registerFadeMesh(stalk);
|
||||
this.model.add(stalk);
|
||||
|
||||
// Hut
|
||||
this.cap = new THREE.Mesh(new THREE.SphereGeometry(0.6, 18, 14), this.capMat);
|
||||
this.cap.scale.set(1, 0.7, 1);
|
||||
this.cap.position.y = 1.05;
|
||||
this.cap.castShadow = true;
|
||||
this.registerFadeMesh(this.cap);
|
||||
this.model.add(this.cap);
|
||||
|
||||
// Glüh-Flecken
|
||||
for (const [dx, dz] of [[0, 0], [0.3, 0.12], [-0.28, 0.18], [0.05, -0.32]] as const) {
|
||||
const spot = new THREE.Mesh(new THREE.SphereGeometry(0.09, 8, 6), this.glowMat);
|
||||
spot.scale.set(1, 0.4, 1);
|
||||
spot.position.set(dx, 1.32, dz);
|
||||
this.model.add(spot);
|
||||
}
|
||||
|
||||
// Augen am Stiel (+Z)
|
||||
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x224422 });
|
||||
for (const side of [-1, 1]) {
|
||||
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.07, 6, 5), eyeMat);
|
||||
eye.position.set(side * 0.16, 0.6, 0.3);
|
||||
this.model.add(eye);
|
||||
}
|
||||
}
|
||||
|
||||
playAnim() {
|
||||
// Prozedurale Optik
|
||||
}
|
||||
|
||||
updateAnimation(dt: number) {
|
||||
this.updateFadeDeath(dt, { duration: 0.9, sink: 0.3 });
|
||||
}
|
||||
|
||||
update(dt: number, game: Game) {
|
||||
this.swayTime += dt;
|
||||
|
||||
// Hit-Feedback
|
||||
if (this.hitFlash > 0) {
|
||||
this.hitFlash -= dt;
|
||||
this.capMat.color.set(0xffffff);
|
||||
this.stalkMat.color.set(0xffffff);
|
||||
} else {
|
||||
this.capMat.color.set(0x55aa44);
|
||||
this.stalkMat.color.set(0xe8dcc0);
|
||||
}
|
||||
|
||||
// Leichtes Schwanken + Puls
|
||||
this.model.rotation.z = Math.sin(this.swayTime * 1.5) * 0.05;
|
||||
const pulse = 1 + Math.sin(this.swayTime * 4) * 0.05;
|
||||
this.cap.scale.set(1, 0.7 * pulse, 1);
|
||||
|
||||
const playerPos = game.player.body.position;
|
||||
const toPlayer = new THREE.Vector3().subVectors(playerPos, this.body.position);
|
||||
toPlayer.y = 0;
|
||||
const dist = toPlayer.length();
|
||||
|
||||
if (dist > 0.1) {
|
||||
this.body.lookAt(
|
||||
this.body.position.x + toPlayer.x,
|
||||
this.body.position.y,
|
||||
this.body.position.z + toPlayer.z
|
||||
);
|
||||
}
|
||||
|
||||
this.shootTimer -= dt;
|
||||
if (dist < SHOOT_RANGE && this.shootTimer <= 0 && game.player.health > 0) {
|
||||
this.shootTimer = SHOOT_INTERVAL + Math.random() * 0.5;
|
||||
const from = new THREE.Vector3(this.body.position.x, 1.3, this.body.position.z);
|
||||
const target = playerPos.clone();
|
||||
const bolt = new SporeBolt(from, target);
|
||||
game.scene.add(bolt.body);
|
||||
this.bolts.push(bolt);
|
||||
playSound('spawn', 0.3);
|
||||
}
|
||||
|
||||
for (let i = this.bolts.length - 1; i >= 0; i--) {
|
||||
const bolt = this.bolts[i];
|
||||
if (!bolt.update(dt, game)) {
|
||||
this.bolts.splice(i, 1);
|
||||
if (bolt.landed) {
|
||||
const cloud = new SporeCloud(bolt.body.position, CLOUD_RADIUS, CLOUD_DURATION);
|
||||
game.scene.add(cloud.body);
|
||||
this.clouds.push(cloud);
|
||||
}
|
||||
bolt.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = this.clouds.length - 1; i >= 0; i--) {
|
||||
const cloud = this.clouds[i];
|
||||
if (!cloud.update(dt, game)) {
|
||||
this.clouds.splice(i, 1);
|
||||
cloud.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override takeDamage(
|
||||
damage: number,
|
||||
game: Game,
|
||||
withKnockback = true,
|
||||
sourcePos?: THREE.Vector3
|
||||
) {
|
||||
super.takeDamage(damage, game, withKnockback, sourcePos);
|
||||
this.hitFlash = HIT_FLASH_TIME;
|
||||
}
|
||||
|
||||
protected onDeath(_game: Game) {
|
||||
this.cleanupEffects();
|
||||
this.beginFadeDeath();
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this.cleanupEffects();
|
||||
}
|
||||
|
||||
private cleanupEffects() {
|
||||
for (const bolt of this.bolts) bolt.dispose();
|
||||
this.bolts = [];
|
||||
for (const cloud of this.clouds) cloud.dispose();
|
||||
this.clouds = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Bogen-Spore: ballistisches Projektil (parabelförmig zum Zielpunkt)
|
||||
class SporeBolt {
|
||||
body: THREE.Group;
|
||||
private vel: THREE.Vector3;
|
||||
private life = 5;
|
||||
dead = false;
|
||||
landed = false;
|
||||
|
||||
constructor(from: THREE.Vector3, target: THREE.Vector3) {
|
||||
this.body = new THREE.Group();
|
||||
this.body.position.copy(from);
|
||||
|
||||
// Ballistik: Flugzeit aus horizontaler Distanz, Abwärts-Geschwindigkeit passend
|
||||
const dx = target.x - from.x;
|
||||
const dz = target.z - from.z;
|
||||
const horiz = Math.hypot(dx, dz);
|
||||
const t = Math.max(0.35, horiz / BOLT_SPEED);
|
||||
const vy = (target.y - from.y + 0.5 * BOLT_GRAVITY * t * t) / t;
|
||||
this.vel = new THREE.Vector3(dx / t, vy, dz / t);
|
||||
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x88ff66,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
});
|
||||
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.24, 8, 6), glowMat);
|
||||
this.body.add(glow);
|
||||
const coreMat = new THREE.MeshToonMaterial({ color: 0x3a8a2a });
|
||||
const core = new THREE.Mesh(new THREE.SphereGeometry(0.13, 6, 5), coreMat);
|
||||
this.body.add(core);
|
||||
}
|
||||
|
||||
update(dt: number, game: Game): boolean {
|
||||
this.life -= dt;
|
||||
this.vel.y -= BOLT_GRAVITY * dt;
|
||||
this.body.position.addScaledVector(this.vel, dt);
|
||||
|
||||
if (this.body.position.y <= 0.1) {
|
||||
this.body.position.y = 0.1;
|
||||
this.landed = true;
|
||||
return false;
|
||||
}
|
||||
if (game.player.health > 0) {
|
||||
const dist = this.body.position.distanceTo(game.player.body.position);
|
||||
if (dist < 1.0) {
|
||||
game.player.damage(BOLT_DAMAGE, this.body.position);
|
||||
this.landed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.life <= 0) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sporenwolke: Boden-DoT wie die Tintenpfütze, aber grün
|
||||
class SporeCloud {
|
||||
body: THREE.Group;
|
||||
private life: number;
|
||||
private tickTimer = 0;
|
||||
private puffs: THREE.Mesh[] = [];
|
||||
|
||||
constructor(position: THREE.Vector3, radius: number, duration: number) {
|
||||
this.life = duration;
|
||||
this.body = new THREE.Group();
|
||||
this.body.position.set(position.x, 0.1, position.z);
|
||||
|
||||
const puffGeom = new THREE.SphereGeometry(1, 10, 8);
|
||||
const puffMat = new THREE.MeshToonMaterial({
|
||||
color: 0x55cc44,
|
||||
transparent: true,
|
||||
opacity: 0.32,
|
||||
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) * radius * 0.4,
|
||||
0.35 + Math.random() * 0.3,
|
||||
Math.sin(a) * radius * 0.4
|
||||
);
|
||||
puff.scale.setScalar(radius * (0.2 + 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.32 * 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 < 2.4) {
|
||||
this.tickTimer -= dt;
|
||||
if (this.tickTimer <= 0) {
|
||||
this.tickTimer = CLOUD_TICK;
|
||||
game.player.damage(CLOUD_TICK_DAMAGE, this.body.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.body.removeFromParent();
|
||||
for (const puff of this.puffs) {
|
||||
puff.geometry.dispose();
|
||||
(puff.material as THREE.Material).dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user