import * as THREE from 'three'; import { Weapon } from './Weapon'; import { Player } from './Player'; import { lightningDamage, lightningJumps, lightningCooldown } from './Skills'; const CHAIN_FIRST_RANGE = 14; const CHAIN_JUMP_RANGE = 8; const FLASH_TIME = 0.35; export class ChainLightning extends Weapon { constructor() { super(); this.COOLDOWN1_TIME = lightningCooldown(1); } skill1(_groundPoint: THREE.Vector3, player: Player): void { const skills = player.game.skillSystem; const level = skills.getLevel('chainlightning'); if (level <= 0) return; if (this.cooldown1 > 0) return; this.COOLDOWN1_TIME = lightningCooldown(level); this.cooldown1 = this.COOLDOWN1_TIME; const game = player.game; const damage = lightningDamage(level); const maxJumps = lightningJumps(level); // Chain through enemies, always the closest un-hit one const start = player.body.position.clone(); start.y += 0.8; const chainPoints: THREE.Vector3[] = [start]; const hit: Set = new Set(); let from = player.body.position.clone(); for (let i = 0; i < maxJumps; i++) { let best: import('./Enemy').Enemy | null = null; let bestDist = i === 0 ? CHAIN_FIRST_RANGE : CHAIN_JUMP_RANGE; for (const enemy of game.enemies) { if (enemy.dead || hit.has(enemy)) continue; const dist = enemy.body.position.distanceTo(from); if (dist < bestDist) { bestDist = dist; best = enemy; } } if (!best) break; hit.add(best); const fromPoint = from.clone(); best.takeDamage(damage, game, true, fromPoint); const point = best.body.position.clone(); point.y += 0.8; chainPoints.push(point); from = best.body.position; } if (chainPoints.length < 2) return; this.spawnVisual(game.scene, chainPoints, hit); } skill2(): void { // Not implemented } private spawnVisual( scene: THREE.Scene, points: THREE.Vector3[], hit: Set ) { const group = new THREE.Group(); scene.add(group); // Segments between chain points for (let i = 0; i < points.length - 1; i++) { const lineGeom = new THREE.BufferGeometry().setFromPoints([ points[i], points[i + 1], ]); const lineMat = new THREE.LineBasicMaterial({ color: 0x88ccff, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false, }); const line = new THREE.Line(lineGeom, lineMat); group.add(line); // Jagged inner bolt const mid = new THREE.Vector3() .addVectors(points[i], points[i + 1]) .multiplyScalar(0.5); mid.x += (Math.random() - 0.5) * 1.2; mid.y += (Math.random() - 0.5) * 1.2; mid.z += (Math.random() - 0.5) * 1.2; const boltGeom = new THREE.BufferGeometry().setFromPoints([ points[i], mid, points[i + 1], ]); const boltMat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false, }); const bolt = new THREE.Line(boltGeom, boltMat); group.add(bolt); } // Flash spheres on hit enemies const flashes: THREE.Mesh[] = []; for (const enemy of hit) { const flashGeom = new THREE.SphereGeometry(0.7, 12, 12); const flashMat = new THREE.MeshBasicMaterial({ color: 0x88ccff, transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, depthWrite: false, }); const flash = new THREE.Mesh(flashGeom, flashMat); flash.position.copy(enemy.body.position); flash.position.y += 0.8; group.add(flash); flashes.push(flash); } const startTime = performance.now(); const update = () => { const elapsed = (performance.now() - startTime) / 1000; if (elapsed > FLASH_TIME) { group.removeFromParent(); for (const child of [...group.children]) { if (child instanceof THREE.Line || child instanceof THREE.Mesh) { child.geometry.dispose(); (child.material as THREE.Material).dispose(); } } return; } const fade = 1 - elapsed / FLASH_TIME; for (const child of group.children) { if (child instanceof THREE.Line || child instanceof THREE.Mesh) { (child.material as THREE.Material & { opacity: number }).opacity = fade; } } for (const flash of flashes) { const s = 0.7 + elapsed * 3; flash.scale.set(s, s, s); } requestAnimationFrame(update); }; update(); } }