Files
Wackelpeter/web/src/Sword.ts
T

134 lines
4.0 KiB
TypeScript

import * as THREE from 'three';
import { Weapon } from './Weapon';
import { Player } from './Player';
import { loadGLB } from './MeshLoader';
import { playSound } from './SoundManager';
import { SlashEffect, isInSlashArc } from './SwordTrail';
export class Sword extends Weapon {
private mixer: THREE.AnimationMixer | null = null;
private clips: Map<string, THREE.AnimationAction> = new Map();
constructor() {
super();
this.COOLDOWN1_TIME = 0.5;
this.mesh.position.set(-0.35, 0.3, 0.35);
this.mesh.scale.set(0.2, 0.2, 0.2);
this.mesh.rotation.set(0.3, 0, 0.3);
// Fallback geometry
const bladeGeom = new THREE.BoxGeometry(0.08, 1.0, 0.15);
const handleGeom = new THREE.CylinderGeometry(0.05, 0.05, 0.2, 8);
const guardGeom = new THREE.BoxGeometry(0.25, 0.06, 0.06);
const metalMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.9, roughness: 0.3 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.6 });
const blade = new THREE.Mesh(bladeGeom, metalMat);
blade.position.y = 0.4;
blade.castShadow = true;
const guard = new THREE.Mesh(guardGeom, metalMat);
guard.position.y = -0.1;
const handle = new THREE.Mesh(handleGeom, darkMat);
handle.position.y = -0.25;
this.mesh.add(blade);
this.mesh.add(guard);
this.mesh.add(handle);
this._fallback = true;
}
async init() {
try {
const gltf = await loadGLB('sword');
const model = gltf.scene;
model.scale.set(1, 1, 1);
const swordTexture = new THREE.TextureLoader().load('./textures/sword.png');
swordTexture.flipY = false;
swordTexture.colorSpace = THREE.SRGBColorSpace;
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true;
child.material = new THREE.MeshToonMaterial({ map: swordTexture });
}
});
// Remove fallback geometry
while (this.mesh.children.length > 0) {
this.mesh.remove(this.mesh.children[0]);
}
this._fallback = false;
this.mesh.add(model);
if (gltf.animations.length > 0) {
this.mixer = new THREE.AnimationMixer(model);
for (const clip of gltf.animations) {
const action = this.mixer.clipAction(clip);
this.clips.set(clip.name, action);
}
const normal = this.clips.get('normal');
if (normal) {
normal.play();
}
}
} catch (e) {
console.warn('Failed to load sword model', e);
}
}
private _fallback = false;
updateAnimation(dt: number) {
if (this.mixer) this.mixer.update(dt);
}
skill1(_groundPoint: THREE.Vector3, player: Player): void {
if (this.cooldown1 > 0) return;
this.cooldown1 = this.COOLDOWN1_TIME;
playSound('sword_hit1', 0.5);
// Play skill1 animation
const action = this.clips.get('skill1');
if (action) {
action.reset();
action.setLoop(THREE.LoopOnce, 1);
action.setEffectiveTimeScale(2);
action.play();
}
const { game } = player;
const range = player.stats.swordRange;
const damage = player.stats.swordDamage;
// Aim direction: where the blob actually faces (toward the mouse point)
const aim = new THREE.Vector3()
.subVectors(game.groundPoint, player.body.position);
aim.y = 0;
if (aim.lengthSq() < 0.001) {
aim.set(0, 0, -1).applyQuaternion(player.body.quaternion);
aim.y = 0;
}
aim.normalize();
// Slash trail feedback
const slash = new SlashEffect(player.body.position, aim, game.scene, range);
game.slashEffects.push(slash);
for (const enemy of game.enemies) {
if (enemy.dead) continue;
if (!enemy.canBeSlashHit()) continue;
if (isInSlashArc(player.body.position, aim, enemy.body.position, range)) {
enemy.takeDamage(damage, game, true, player.body.position);
}
}
// Destroy enemy projectiles caught in the slash
game.destroyProjectilesInArc(player.body.position, aim, range);
}
skill2(): void {
// Not implemented
}
}