134 lines
4.0 KiB
TypeScript
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
|
|
}
|
|
}
|