import * as THREE from 'three'; import type { Game } from './Game'; import { Enemy } from './Enemy'; export class Bat extends Enemy { private model: THREE.Group; private wingL: THREE.Mesh; private wingR: THREE.Mesh; private flapTime = Math.random() * Math.PI * 2; private flyTime = Math.random() * Math.PI * 2; constructor() { super(); this.health = 25; this.speed = 8; this.xp = 10; this.model = new THREE.Group(); this.fadeOutModel = this.model; this.body.add(this.model); const furMat = new THREE.MeshToonMaterial({ color: 0x5a4568 }); // Body const bodyMesh = new THREE.Mesh(new THREE.SphereGeometry(0.3, 14, 12), furMat); bodyMesh.scale.set(1, 0.9, 1.3); bodyMesh.castShadow = true; this.registerFadeMesh(bodyMesh); this.model.add(bodyMesh); // Head with ears (face toward the player: +Z) const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 10), furMat); head.position.set(0, 0.15, 0.3); this.registerFadeMesh(head); this.model.add(head); for (const side of [-1, 1]) { const ear = new THREE.Mesh(new THREE.ConeGeometry(0.07, 0.18, 6), furMat); ear.position.set(side * 0.12, 0.32, 0.25); this.registerFadeMesh(ear); this.model.add(ear); } // Eyes const eyeMat = new THREE.MeshToonMaterial({ color: 0xff2222 }); for (const side of [-1, 1]) { const eye = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 8), eyeMat); eye.position.set(side * 0.07, 0.16, 0.42); this.registerFadeMesh(eye); this.model.add(eye); } // Wings (hinged at the body, flap via rotation.z) const wingGeom = new THREE.PlaneGeometry(0.75, 0.5, 1, 4); wingGeom.translate(0.3, 0, 0); const wingMat = new THREE.MeshToonMaterial({ color: 0x443355, side: THREE.DoubleSide, transparent: true, opacity: 0.9, }); this.wingL = new THREE.Mesh(wingGeom, wingMat); this.wingR = new THREE.Mesh(wingGeom.clone(), wingMat); this.wingL.position.set(-0.3, 0.05, 0); this.wingR.position.set(0.3, 0.05, 0); this.registerFadeMesh(this.wingL); this.registerFadeMesh(this.wingR); this.model.add(this.wingL, this.wingR); } playAnim() { // Procedural visuals only } updateAnimation(dt: number) { this.updateFadeDeath(dt, { duration: 0.7, sink: 3 }); } update(dt: number, game: Game) { this.flapTime += dt * 14; this.flyTime += dt * 3; const flap = Math.sin(this.flapTime) * 0.9; this.wingL.rotation.z = -1.1 - flap; this.wingR.rotation.z = 1.1 + flap; // Bob up and down while swooping at the player this.body.position.y = 1.0 + Math.sin(this.flyTime) * 0.5; const toPlayer = new THREE.Vector3() .subVectors(game.player.body.position, this.body.position); toPlayer.y = 0; const dist = toPlayer.length(); if (dist > 0.1) { const dir = toPlayer.normalize(); this.body.lookAt( this.body.position.x + dir.x, this.body.position.y, this.body.position.z + dir.z ); // Slight zigzag const perp = new THREE.Vector3(-dir.z, 0, dir.x) .multiplyScalar(Math.sin(this.flyTime * 2.2) * 0.6); let moveDir = dir.clone().multiplyScalar(this.speed).add(perp); if (this.knockback > 0) { moveDir = dir.multiplyScalar(-this.speed); this.knockback -= dt; } this.body.position.x += moveDir.x * dt; this.body.position.z += moveDir.z * dt; game.clampToArena(this.body.position); } } protected onDeath(_game: Game) { this.beginFadeDeath(); } }