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