Files
Wackelpeter/web/src/Bat.ts
T

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