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(); } } }