120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
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();
|
|
}
|
|
}
|