Add skill system, level-ups, and new enemy types

This commit is contained in:
2026-08-17 16:38:58 +02:00
parent 4751d428a7
commit 6f207ba1a0
23 changed files with 2192 additions and 83 deletions
+165
View File
@@ -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();
}
}