Add skill system, level-ups, and new enemy types
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
import * as THREE from 'three';
|
||||
import type { Game } from './Game';
|
||||
import { Enemy } from './Enemy';
|
||||
|
||||
export class Crab extends Enemy {
|
||||
private model: THREE.Group;
|
||||
private clawL: THREE.Mesh;
|
||||
private clawR: THREE.Mesh;
|
||||
private time = Math.random() * Math.PI * 2;
|
||||
private side = Math.random() < 0.5 ? 1 : -1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.health = 90;
|
||||
this.speed = 4.5;
|
||||
this.xp = 20;
|
||||
this.contactRadius = 2.2;
|
||||
|
||||
this.model = new THREE.Group();
|
||||
this.fadeOutModel = this.model;
|
||||
this.body.add(this.model);
|
||||
|
||||
const shellMat = new THREE.MeshToonMaterial({ color: 0xdd5533 });
|
||||
const darkMat = new THREE.MeshToonMaterial({ color: 0x993322 });
|
||||
|
||||
// Shell body
|
||||
const shell = new THREE.Mesh(new THREE.SphereGeometry(0.55, 18, 14), shellMat);
|
||||
shell.scale.set(1, 0.55, 0.85);
|
||||
shell.position.y = 0.35;
|
||||
shell.castShadow = true;
|
||||
this.registerFadeMesh(shell);
|
||||
this.model.add(shell);
|
||||
|
||||
// Shell spots
|
||||
for (const [dx, dz] of [[0, -0.15], [0.25, 0.1], [-0.25, 0.1]] as const) {
|
||||
const spot = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), darkMat);
|
||||
spot.scale.set(1, 0.4, 1);
|
||||
spot.position.set(dx, 0.62, dz);
|
||||
this.registerFadeMesh(spot);
|
||||
this.model.add(spot);
|
||||
}
|
||||
|
||||
// Eye stalks (on the armored front side, -Z)
|
||||
const stalkMat = new THREE.MeshToonMaterial({ color: 0xdd5533 });
|
||||
const eyeMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
||||
for (const side of [-1, 1]) {
|
||||
const stalk = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.22, 6), stalkMat);
|
||||
stalk.position.set(side * 0.22, 0.55, -0.42);
|
||||
this.registerFadeMesh(stalk);
|
||||
this.model.add(stalk);
|
||||
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.09, 10, 8), eyeMat);
|
||||
eye.position.set(side * 0.22, 0.68, -0.42);
|
||||
this.registerFadeMesh(eye);
|
||||
this.model.add(eye);
|
||||
}
|
||||
|
||||
// Legs
|
||||
const legGeom = new THREE.CapsuleGeometry(0.05, 0.25, 4, 8);
|
||||
for (const side of [-1, 1]) {
|
||||
for (const legZ of [-0.3, 0.3]) {
|
||||
const leg = new THREE.Mesh(legGeom, shellMat);
|
||||
leg.position.set(side * 0.45, 0.15, legZ);
|
||||
leg.rotation.z = side * 0.6;
|
||||
this.registerFadeMesh(leg);
|
||||
this.model.add(leg);
|
||||
}
|
||||
}
|
||||
|
||||
// Big claws (front, open/close)
|
||||
const clawGeom = new THREE.SphereGeometry(0.22, 12, 10);
|
||||
this.clawL = new THREE.Mesh(clawGeom, shellMat);
|
||||
this.clawR = new THREE.Mesh(clawGeom, shellMat);
|
||||
this.clawL.position.set(-0.45, 0.45, -0.5);
|
||||
this.clawR.position.set(0.45, 0.45, -0.5);
|
||||
this.clawL.scale.set(1.2, 0.7, 1.4);
|
||||
this.clawR.scale.set(1.2, 0.7, 1.4);
|
||||
this.clawL.castShadow = true;
|
||||
this.clawR.castShadow = true;
|
||||
this.registerFadeMesh(this.clawL);
|
||||
this.registerFadeMesh(this.clawR);
|
||||
this.model.add(this.clawL, this.clawR);
|
||||
}
|
||||
|
||||
playAnim() {
|
||||
// Procedural visuals only
|
||||
}
|
||||
|
||||
updateAnimation(dt: number) {
|
||||
this.updateFadeDeath(dt, { duration: 0.8, sink: 0.4 });
|
||||
}
|
||||
|
||||
update(dt: number, game: Game) {
|
||||
this.time += dt;
|
||||
|
||||
// Claw pinch
|
||||
const pinch = 1 + Math.sin(this.time * 4) * 0.35;
|
||||
this.clawL.scale.set(1.2, 0.7 * pinch, 1.4);
|
||||
this.clawR.scale.set(1.2, 0.7 * pinch, 1.4);
|
||||
|
||||
const toPlayer = new THREE.Vector3()
|
||||
.subVectors(game.player.body.position, this.body.position);
|
||||
toPlayer.y = 0;
|
||||
const dist = toPlayer.length();
|
||||
|
||||
// Face the player
|
||||
if (dist > 0.1) {
|
||||
this.body.lookAt(
|
||||
this.body.position.x + toPlayer.x,
|
||||
this.body.position.y,
|
||||
this.body.position.z + toPlayer.z
|
||||
);
|
||||
}
|
||||
|
||||
// Strafe sideways around the player
|
||||
if (dist > 3) {
|
||||
const perp = new THREE.Vector3(-toPlayer.z, 0, toPlayer.x).normalize();
|
||||
if (this.knockback > 0) {
|
||||
this.knockback -= dt;
|
||||
const away = toPlayer.clone().normalize().multiplyScalar(-this.speed);
|
||||
this.body.position.x += away.x * dt;
|
||||
this.body.position.z += away.z * dt;
|
||||
} else {
|
||||
// Flip side occasionally to circle
|
||||
if (Math.random() < dt * 0.3) this.side *= -1;
|
||||
this.body.position.x += perp.x * this.side * this.speed * dt;
|
||||
this.body.position.z += perp.z * this.side * this.speed * dt;
|
||||
}
|
||||
game.clampToArena(this.body.position);
|
||||
} else if (dist > 0.1) {
|
||||
const dir = toPlayer.normalize();
|
||||
if (this.knockback > 0) {
|
||||
this.knockback -= dt;
|
||||
this.body.position.x -= dir.x * this.speed * dt;
|
||||
this.body.position.z -= dir.z * this.speed * dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
takeDamage(
|
||||
damage: number,
|
||||
game: Game,
|
||||
withKnockback = true,
|
||||
sourcePos?: THREE.Vector3
|
||||
) {
|
||||
// Armored from the front: reduce damage coming from the facing side
|
||||
if (sourcePos) {
|
||||
const front = new THREE.Vector3(0, 0, -1)
|
||||
.applyQuaternion(this.body.quaternion);
|
||||
front.y = 0;
|
||||
front.normalize();
|
||||
const incoming = new THREE.Vector3()
|
||||
.subVectors(sourcePos, this.body.position);
|
||||
incoming.y = 0;
|
||||
incoming.normalize();
|
||||
if (front.dot(incoming) > 0.3) {
|
||||
damage *= 0.4;
|
||||
}
|
||||
}
|
||||
super.takeDamage(damage, game, withKnockback, sourcePos);
|
||||
}
|
||||
|
||||
protected onDeath(_game: Game) {
|
||||
this.beginFadeDeath();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user