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 = 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 } }