module TK.SpaceTac.UI { type WeaponEffectInfo = { execution: (speed: number) => Promise delay: (ship: Ship) => number } /** * Visual effects renderer for weapons. */ export class WeaponEffect { // Link to view private view: BattleView // Timer to use private timer: Timer // Display group in which to display the visual effects private layer: UIContainer // Builder for images private builder: UIBuilder // Firing ship ship: Ship source: IArenaLocation // Target (ship or space) target: Target destination: IArenaLocation // Weapon used action: TriggerAction constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) { this.view = arena.view; this.timer = arena.view.timer; this.layer = arena.layer_weapon_effects; this.builder = new UIBuilder(arena.view, this.layer); this.ship = ship; this.target = target; this.action = action; this.source = Target.newFromShip(this.ship); this.destination = this.target; } /** * Start the visual effect */ async start(speed: number): Promise { if (!speed) { return; } // Fire effect let fire_effect = this.getEffectForWeapon(this.action.code, this.action); let promises = [fire_effect.execution(speed)]; // Damage effect let action = this.action; if (any(action.effects, effect => effect instanceof DamageEffect)) { let ships = action.getImpactedShips(this.ship, this.target, this.source).map((ship): [Ship, number] => { return [ship, fire_effect.delay(ship) / speed]; }); let source = action.blast ? this.target : this.source; promises.push(this.damageEffect(source, ships, speed, this.action.code == "gatlinggun")); } await Promise.all(promises); } /** * Add a damage effect on ships impacted by a weapon */ async damageEffect(source: IArenaLocation, ships: [Ship, number][], speed = 1, shield_flares = false): Promise { let promises = ships.map(([ship, delay]) => { return this.timer.sleep(delay).then(() => { if (ship.getValue("shield") > 0) { return this.shieldImpactEffect(source, ship.location, speed, shield_flares); } else { return this.hullImpactEffect(source, ship.location, speed); } }); }); await Promise.all(promises); } /** * Get the function that will be called to start the visual effect */ getEffectForWeapon(weapon: string, action: TriggerAction): WeaponEffectInfo { switch (weapon) { case "gatlinggun": return this.bulletsEffect(); case "prokhorovlaser": return this.laserEffect(); default: return this.genericEffect(); } } /** * Add a shield impact effect on a ship */ async shieldImpactEffect(from: IArenaLocation, ship: IArenaLocation, speed = 1, particles = false): Promise { let angle = Math.atan2(from.y - ship.y, from.x - ship.x); if (particles) { this.builder.particles({ key: "battle-effects-hot", source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 10 }, emitDuration: 500 / speed, count: 50, lifetime: 400 / speed, fading: true, direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, scale: { min: 0.7, max: 1.2 }, speed: { min: 20 / speed, max: 80 / speed } }); } let effect = this.builder.image("battle-effects-shield-impact", ship.x, ship.y, true); effect.setAlpha(0); effect.setRotation(angle); await this.view.animations.addAnimation(effect, { alpha: 1 }, 100 / speed, undefined); await this.timer.sleep(800 / speed); await this.view.animations.addAnimation(effect, { alpha: 0 }, 100 / speed, undefined); effect.destroy(); } /** * Add a hull impact effect on a ship */ async hullImpactEffect(from: IArenaLocation, ship: IArenaLocation, speed = 1): Promise { let angle = Math.atan2(from.y - ship.y, from.x - ship.x); this.builder.particles({ key: "battle-effects-hot", source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 7 }, emitDuration: 500 / speed, count: 50, lifetime: 400 / speed, fading: true, direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, scale: { min: 1, max: 2 }, speed: { min: 120 / speed, max: 260 / speed } }); return Promise.resolve(); // TODO } /** * Generic weapon effect */ async genericExecutor(speed: number): Promise { this.view.audio.playOnce("battle-weapon-missile-launch", speed); let missile = this.builder.image("battle-effects-default", this.source.x, this.source.y, true); missile.setRotation(arenaAngle(this.source, this.destination)); let blast_radius = this.action.getBlastRadius(this.ship); let projectile_duration = (arenaDistance(this.source, this.destination) * 1.5) || 1; await this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration / speed); missile.destroy(); if (blast_radius > 0) { this.view.audio.playOnce("battle-weapon-missile-explosion", speed); let blast = this.builder.image("battle-effects-blast", this.destination.x, this.destination.y, true); let scaling = blast_radius * 2 / (blast.width * 0.9); blast.setScale(0.001); await Promise.all([ this.view.animations.addAnimation(blast, { alpha: 0 }, 1450 / speed, "Quad.easeIn"), this.view.animations.addAnimation(blast, { scaleX: scaling, scaleY: scaling }, 1500 / speed, "Quint.easeOut"), ]); blast.destroy(); } } private genericEffect(): WeaponEffectInfo { return { execution: speed => this.genericExecutor(speed), delay: ship => { let result = (arenaDistance(this.source, this.destination) * 1.5) || 1; if (this.action.blast) { let d = arenaDistance(this.destination, ship.location) / this.action.getBlastRadius(this.ship); result += 300 * Phaser.Math.Easing.Quintic.Out(d); } return result; } } } /** * Laser effect, scanning from one angle to the other */ laserExecutor(speed: number): Promise { let duration = 1000 / speed; let angle = arenaAngle(this.source, this.target); let dangle = radians(this.action.angle) * 0.5; this.view.audio.playOnce("battle-weapon-laser", speed); let laser = this.builder.image("battle-effects-laser", this.source.x, this.source.y); laser.setOrigin(0, 0.5); laser.setRotation(angle - dangle); laser.setScale(this.action.getRangeRadius(this.ship) / laser.width); let dest_angle = laser.rotation + angularDifference(laser.rotation, angle + dangle); return this.view.animations.addAnimation(laser, { rotation: dest_angle }, duration).then(() => laser.destroy()); } private laserEffect(): WeaponEffectInfo { return { execution: speed => this.laserExecutor(speed), delay: ship => { let angle = arenaAngle(this.source, this.target); let span = radians(this.action.angle); return 900 * Math.abs(angularDifference(angle - span * 0.5, arenaAngle(this.source, ship.location))) / span; } } } /** * Submachine gun effect (quick chain of small bullets) */ bulletsExecutor(speed: number): Promise { this.view.audio.playOnce("battle-weapon-bullets", speed); let target_ship = this.target.getShip(this.view.battle); let has_shield = target_ship && (target_ship.getValue("shield") > 0); let angle = arenaAngle(this.source, this.target); let distance = arenaDistance(this.source, this.target); let guard = 35 + (has_shield ? 80 : 40); if (guard + 1 > distance) { guard = distance - 1; } let duration = 500 / speed; let lifetime = (distance - guard) / (2 * speed); this.builder.particles({ key: "battle-effects-bullets", source: { x: this.source.x + Math.cos(angle) * 35, y: this.source.y + Math.sin(angle) * 35, radius: 3 }, emitDuration: duration, count: 50, lifetime: lifetime, direction: { minangle: angle, maxangle: angle }, scale: { min: 1, max: 1 }, speed: { min: 2000 / speed, max: 2000 / speed }, facing: ParticleFacingMode.ALWAYS }); return this.timer.sleep(lifetime); } private bulletsEffect(): WeaponEffectInfo { return { execution: speed => this.bulletsExecutor(speed), delay: ship => 2000 / arenaDistance(this.source, ship.location) } } } }