From 787945703597e4a422af189672b8c54017a7aab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Mon, 11 Jun 2018 00:58:42 +0200 Subject: [PATCH] Refactored animation system to be able to control the playback speed --- TODO.md | 1 - src/common | 2 +- src/ui/battle/Arena.ts | 37 ++--- src/ui/battle/ArenaDrone.ts | 6 +- src/ui/battle/ArenaShip.ts | 120 +++++++-------- src/ui/battle/LogProcessor.ts | 29 ++-- src/ui/battle/ShipList.spec.ts | 8 +- src/ui/battle/ShipList.ts | 15 +- src/ui/battle/WeaponEffect.spec.ts | 56 +++---- src/ui/battle/WeaponEffect.ts | 225 ++++++++++++++++------------- src/ui/common/Animations.spec.ts | 3 +- src/ui/common/Animations.ts | 83 +++++++---- src/ui/common/Audio.ts | 7 +- 13 files changed, 316 insertions(+), 276 deletions(-) diff --git a/TODO.md b/TODO.md index be256b1..40809a3 100644 --- a/TODO.md +++ b/TODO.md @@ -59,7 +59,6 @@ Battle * Add targetting shortcuts for "previous target", "next enemy" and "next ally" * Area targetting should include the hotkeyed ship at best (apply exclusion and power limit), not necessarily center on it * Add shortcut to perform only the "move" part of a move+fire simulation -* Fix delay of shield/hull impact effects (should depend on weapon animation, and ship location) * Add a turn count marker in the ship list * BattleChecks should be done proactively when all diffs have been simulated by an action, in addition to reactively after applying diff --git a/src/common b/src/common index 71b3097..1425cb0 160000 --- a/src/common +++ b/src/common @@ -1 +1 @@ -Subproject commit 71b309744d7760ffc4ecdc14a1f68dbe2a25d419 +Subproject commit 1425cb08935dd996a4c7a644ab793ff3b8355c9b diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index 51423e1..5773158 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -67,7 +67,7 @@ module TK.SpaceTac.UI { this.range_hint.setLayer(this.layer_hints); this.addShipSprites(); - view.battle.drones.list().forEach(drone => this.addDrone(drone, false)); + view.battle.drones.list().forEach(drone => this.addDrone(drone, 0)); view.log_processor.register(diff => this.checkDroneDeployed(diff)); view.log_processor.register(diff => this.checkDroneRecalled(diff)); @@ -218,10 +218,8 @@ module TK.SpaceTac.UI { /** * Spawn a new drone - * - * Return the duration of deploy animation */ - addDrone(drone: Drone, animate = true): number { + async addDrone(drone: Drone, speed = 1): Promise { if (!this.findDrone(drone)) { let sprite = new ArenaDrone(this.view, drone); let owner = this.view.battle.getShip(drone.owner) || new Ship(); @@ -229,39 +227,32 @@ module TK.SpaceTac.UI { this.layer_drones.add(sprite); this.drone_sprites.push(sprite); - if (animate) { + if (speed) { sprite.radius.setAlpha(0); sprite.setPosition(owner.arena_x, owner.arena_y); sprite.sprite.setRotation(owner.arena_angle); - let move_duration = this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite); - this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500, "Cubic.easeIn", move_duration); - - return move_duration + 500; + await this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite, speed); + await this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500 / speed, "Cubic.easeIn"); } else { sprite.setPosition(drone.x, drone.y); sprite.setRotation(angle); - return 0; } } else { console.error("Drone added twice to arena", drone); - return 0; } } /** * Remove a destroyed drone - * - * Return the duration of deploy animation */ - removeDrone(drone: Drone): number { + async removeDrone(drone: Drone, speed = 1): Promise { let sprite = this.findDrone(drone); if (sprite) { remove(this.drone_sprites, sprite); return sprite.setDestroyed(); } else { console.error("Drone not found in arena for removal", drone); - return 0; } } @@ -290,14 +281,11 @@ module TK.SpaceTac.UI { private checkDroneDeployed(diff: BaseBattleDiff): LogProcessorDelegate { if (diff instanceof DroneDeployedDiff) { return { - foreground: async (animate) => { - let duration = this.addDrone(diff.drone, animate); - if (duration) { + foreground: async (speed: number) => { + if (speed) { this.view.gameui.audio.playOnce("battle-drone-deploy"); - if (animate) { - await this.view.timer.sleep(duration); - } } + await this.addDrone(diff.drone, speed); } } } else { @@ -311,12 +299,11 @@ module TK.SpaceTac.UI { private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate { if (diff instanceof DroneRecalledDiff) { return { - foreground: async () => { - let duration = this.removeDrone(diff.drone); - if (duration) { + foreground: async (speed: number) => { + if (speed) { this.view.gameui.audio.playOnce("battle-drone-destroy"); - await this.view.timer.sleep(duration); } + await this.removeDrone(diff.drone, speed); } } } else { diff --git a/src/ui/battle/ArenaDrone.ts b/src/ui/battle/ArenaDrone.ts index 129417a..afadd13 100644 --- a/src/ui/battle/ArenaDrone.ts +++ b/src/ui/battle/ArenaDrone.ts @@ -59,10 +59,10 @@ module TK.SpaceTac.UI { * * Return the animation duration */ - setDestroyed(): number { + async setDestroyed(): Promise { this.view.animations.addAnimation(this, { alpha: 0.3 }, 300, undefined, 200); - this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500).then(() => this.destroy()); - return 500; + await this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500); + this.destroy(); } /** diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index c921e96..56800f0 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -104,9 +104,9 @@ module TK.SpaceTac.UI { // Set location if (this.battleview.battle.cycle == 1 && this.battleview.battle.play_index == 0 && ship.alive && this.battleview.player.is(ship.fleet.player)) { this.setPosition(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle)); - this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle); + this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, 1); } else { - this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, false); + this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, 0); } // Log processing @@ -132,86 +132,88 @@ module TK.SpaceTac.UI { * Process a ship diff */ private processShipDiff(diff: BaseBattleShipDiff): LogProcessorDelegate { + let timer = this.battleview.timer; + if (diff instanceof ShipEffectAddedDiff || diff instanceof ShipEffectRemovedDiff) { return { background: async () => this.updateActiveEffects() } } else if (diff instanceof ShipValueDiff) { return { - background: async (animate, timer) => { - if (animate) { + background: async (speed: number) => { + if (speed) { this.toggle_hsp.manipulate("value")(true); } if (diff.code == "hull") { - if (animate) { + if (speed) { this.updateHull(this.ship.getValue("hull") - diff.diff, diff.diff); - await timer.sleep(1000); + await timer.sleep(1000 / speed); this.updateHull(this.ship.getValue("hull")); - await timer.sleep(500); + await timer.sleep(500 / speed); } else { this.updateHull(this.ship.getValue("hull")); } } else if (diff.code == "shield") { - if (animate) { + if (speed) { this.updateShield(this.ship.getValue("shield") - diff.diff, diff.diff); - await timer.sleep(1000); + await timer.sleep(1000 / speed); this.updateShield(this.ship.getValue("shield")); - await timer.sleep(500); + await timer.sleep(500 / speed); } else { this.updateShield(this.ship.getValue("shield")); } } else if (diff.code == "power") { this.power_text.setText(`${this.ship.getValue("power")}`); - if (animate) { - await this.battleview.animations.blink(this.power_text); + if (speed) { + await this.battleview.animations.blink(this.power_text, { speed: speed }); } } - if (animate) { - await timer.sleep(500); + if (speed) { + await timer.sleep(500 / speed); this.toggle_hsp.manipulate("value")(false); } } } } else if (diff instanceof ShipAttributeDiff) { return { - background: async (animate, timer) => { - if (animate) { - this.displayAttributeChanged(diff); + background: async (speed: number) => { + if (speed) { + this.displayAttributeChanged(diff, speed); if (diff.code == "evasion") { // TODO diff this.updateEvasion(this.ship.getAttribute("evasion")); - this.toggle_hsp.manipulate("attribute")(2000); + this.toggle_hsp.manipulate("attribute")(2000 / speed); } - await timer.sleep(2000); + await timer.sleep(2000 / speed); } } } } else if (diff instanceof ShipDamageDiff) { return { - background: async (animate, timer) => { - if (animate) { - await this.displayEffect(`${diff.theoretical} damage`, false); - await timer.sleep(1000); + background: async (speed: number) => { + if (speed) { + await this.displayEffect(`${diff.theoretical} damage`, false, speed); + await timer.sleep(1000 / speed); } } } } else if (diff instanceof ShipActionToggleDiff) { return { - foreground: async (animate, timer) => { + foreground: async (speed: number) => { let action = this.ship.actions.getById(diff.action); if (action) { - if (animate) { + if (speed) { if (diff.activated) { - await this.displayEffect(`${action.name} ON`, true); + await this.displayEffect(`${action.name} ON`, true, speed); } else { - await this.displayEffect(`${action.name} OFF`, false); + await this.displayEffect(`${action.name} OFF`, false, speed); } } this.updateEffectsRadius(); - await timer.sleep(500); + await timer.sleep(500 / speed); } } } @@ -220,20 +222,20 @@ module TK.SpaceTac.UI { if (action) { if (action instanceof EndTurnAction) { return { - foreground: async (animate, timer) => { - if (animate) { - await this.displayEffect("End turn", true); - await timer.sleep(500); + foreground: async (speed: number) => { + if (speed) { + await this.displayEffect("End turn", true, speed); + await timer.sleep(500 / speed); } } } } else if (!(action instanceof ToggleAction)) { let action_name = action.name; return { - foreground: async (animate, timer) => { - if (animate) { - await this.displayEffect(action_name, true); - await timer.sleep(300); + foreground: async (speed: number) => { + if (speed) { + await this.displayEffect(action_name, true, speed); + await timer.sleep(300 / speed); } } } @@ -244,11 +246,12 @@ module TK.SpaceTac.UI { return {}; } } else if (diff instanceof ShipMoveDiff) { - let func = async (animate: boolean, timer: Timer) => { - this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, false); - let duration = this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, animate, !!diff.engine); - if (duration && animate) { - await timer.sleep(duration); + let func = async (speed: number) => { + if (speed) { + await this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, 0); + await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, speed, !!diff.engine); + } else { + await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, 0); } }; if (diff.engine) { @@ -259,10 +262,10 @@ module TK.SpaceTac.UI { } else if (diff instanceof VigilanceAppliedDiff) { let action = this.ship.actions.getById(diff.action); return { - foreground: async (animate, timer) => { - if (animate && action) { - await this.displayEffect(`${action.name} (vigilance)`, true); - await timer.sleep(300); + foreground: async (speed: number) => { + if (speed && action) { + await this.displayEffect(`${action.name} (vigilance)`, true, speed); + await timer.sleep(300 / speed); } } } @@ -315,7 +318,7 @@ module TK.SpaceTac.UI { //this.displayEffect("stasis", false); this.stasis.visible = true; this.stasis.alpha = 0; - this.battleview.animations.blink(this.stasis, 0.9, 0.7); + this.battleview.animations.blink(this.stasis, { alpha_on: 0.9, alpha_off: 0.7 }); } else { this.stasis.visible = false; } @@ -327,30 +330,31 @@ module TK.SpaceTac.UI { * * Return the duration of animation */ - moveToArenaLocation(x: number, y: number, facing_angle: number, animate = true, engine = true): number { - if (animate) { + async moveToArenaLocation(x: number, y: number, facing_angle: number, speed = 1, engine = true): Promise { + if (speed) { let animation = bound(this.arena.view.animations, engine ? "moveInSpace" : "moveTo"); - let duration = animation(this, x, y, facing_angle, this.sprite); - return duration; + await animation(this, x, y, facing_angle, this.sprite, speed); } else { this.x = x; this.y = y; this.sprite.rotation = facing_angle; - return 0; } } /** * Briefly show an effect on this ship */ - async displayEffect(message: string, beneficial: boolean) { + async displayEffect(message: string, beneficial: boolean, speed: number) { if (!this.effects_messages.visible) { this.effects_messages.removeAll(true); } - let builder = new UIBuilder(this.arena.view, this.effects_messages); + if (!speed) { + return; + } - let text = builder.text(message, 0, 20 * this.effects_messages.length, { + let builder = new UIBuilder(this.arena.view, this.effects_messages); + builder.text(message, 0, 20 * this.effects_messages.length, { color: beneficial ? "#afe9c6" : "#e9afaf" }); @@ -360,19 +364,19 @@ module TK.SpaceTac.UI { (this.ship.arena_y < arena.height * 0.9) ? 60 : (-60 - this.effects_messages.height) ); - this.effects_messages_toggle.manipulate("added")(1400); - await this.battleview.timer.sleep(1500); + this.effects_messages_toggle.manipulate("added")(1400 / speed); + await this.battleview.timer.sleep(1500 / speed); } /** * Display interesting changes in ship attributes */ - displayAttributeChanged(event: ShipAttributeDiff) { + displayAttributeChanged(event: ShipAttributeDiff, speed = 1) { // TODO show final diff, not just cumulative one let diff = (event.added.cumulative || 0) - (event.removed.cumulative || 0); if (diff) { let name = SHIP_VALUES_NAMES[event.code]; - this.displayEffect(`${name} ${diff < 0 ? "-" : "+"}${Math.abs(diff)}`, diff >= 0); + this.displayEffect(`${name} ${diff < 0 ? "-" : "+"}${Math.abs(diff)}`, diff >= 0, speed); } } diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts index 27708f1..188c891 100644 --- a/src/ui/battle/LogProcessor.ts +++ b/src/ui/battle/LogProcessor.ts @@ -155,15 +155,14 @@ module TK.SpaceTac.UI { let ship = this.view.battle.playing_ship; if (ship) { let result = callback(ship); - let timer = new Timer(true); if (result.foreground) { - let promise = result.foreground(false, timer); + let promise = result.foreground(0); if (result.background) { let next = result.background; - promise.then(() => next(false, timer)); + promise.then(() => next(0)); } } else if (result.background) { - result.background(false, timer); + result.background(0); } } } @@ -176,7 +175,7 @@ module TK.SpaceTac.UI { if (this.debug) { console.log("Battle diff", diff); } - let timer = timed ? this.view.timer : new Timer(true); + let speed = timed ? 1 : 0; // TODO add priority to sort the delegates let delegates = this.subscriber.map(subscriber => subscriber(diff)); @@ -189,11 +188,11 @@ module TK.SpaceTac.UI { this.background_promises = []; } - let promises = foregrounds.map(foreground => foreground(timed, timer)); + let promises = foregrounds.map(foreground => foreground(speed)); await Promise.all(promises); } - let promises = backgrounds.map(background => background(timed, timed ? this.view.timer : new Timer(true))); + let promises = backgrounds.map(background => background(speed)); this.background_promises = this.background_promises.concat(promises); } @@ -263,9 +262,9 @@ module TK.SpaceTac.UI { if (action && action instanceof TriggerAction) { let effect = new WeaponEffect(this.view.arena, ship, diff.target, action); return { - foreground: async (animate, timer) => { - if (animate) { - await this.view.timer.sleep(effect.start()) + foreground: async (speed: number) => { + if (speed) { + await effect.start(speed); } } } @@ -286,14 +285,14 @@ module TK.SpaceTac.UI { if (ship) { let dead_ship = ship; return { - foreground: async (animate) => { + foreground: async (speed: number) => { if (dead_ship.is(this.view.ship_hovered)) { this.view.setShipHovered(null); } this.view.arena.markAsDead(dead_ship); this.view.ship_list.refresh(); - if (animate) { - await this.view.timer.sleep(2000); + if (speed) { + await this.view.timer.sleep(2000 / speed); } } } @@ -324,8 +323,8 @@ module TK.SpaceTac.UI { * *background* is started when no other foreground delegate is working or pending */ export type LogProcessorDelegate = { - foreground?: (animate: boolean, timer: Timer) => Promise, - background?: (animate: boolean, timer: Timer) => Promise, + foreground?: (speed: number) => Promise, + background?: (speed: number) => Promise, } /** diff --git a/src/ui/battle/ShipList.spec.ts b/src/ui/battle/ShipList.spec.ts index 4a99c20..9455197 100644 --- a/src/ui/battle/ShipList.spec.ts +++ b/src/ui/battle/ShipList.spec.ts @@ -35,7 +35,7 @@ module TK.SpaceTac.UI.Specs { }); battle.throwInitiative(); - list.refresh(false); + list.refresh(0); check.in("ship now in play order", check => { check.equals(list.items[0].visible, true, "ship card visible"); }); @@ -51,14 +51,14 @@ module TK.SpaceTac.UI.Specs { }); battle.setPlayingShip(battle.play_order[0]); - list.refresh(false); + list.refresh(0); check.in("started", check => { check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); }); battle.advanceToNextShip(); - list.refresh(false); + list.refresh(0); check.in("end turn", check => { check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); check.equals(nn(list.findItem(battle.play_order[1])).location, { x: -14, y: 962 }, "second ship position"); @@ -78,7 +78,7 @@ module TK.SpaceTac.UI.Specs { let dead = battle.play_order[1]; dead.setDead(); - list.refresh(false); + list.refresh(0); check.in("ship dead", check => { check.equals(list.items.length, 3, "item count"); check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); diff --git a/src/ui/battle/ShipList.ts b/src/ui/battle/ShipList.ts index 086c1e1..d192a51 100644 --- a/src/ui/battle/ShipList.ts +++ b/src/ui/battle/ShipList.ts @@ -62,7 +62,7 @@ module TK.SpaceTac.UI { setShipsFromBattle(battle: Battle, animate = true): void { this.clearAll(); iforeach(battle.iships(true), ship => this.addShip(ship)); - this.refresh(animate); + this.refresh(animate ? 1 : 0); } /** @@ -71,8 +71,8 @@ module TK.SpaceTac.UI { bindToLog(log: LogProcessor): void { log.watchForShipChange(ship => { return { - foreground: async (animate) => { - this.refresh(animate) + foreground: async (speed: number) => { + this.refresh(speed); } } }); @@ -114,7 +114,8 @@ module TK.SpaceTac.UI { /** * Update the locations of all items */ - refresh(animate = true): void { + refresh(speed = 1): void { + let duration = speed ? 1000 / speed : 0; this.items.forEach(item => { if (item.ship.alive) { let position = this.battle.getPlayOrder(item.ship); @@ -122,16 +123,16 @@ module TK.SpaceTac.UI { item.visible = false; } else { if (position == 0) { - item.moveAt(-14, 962, animate ? 1000 : 0); + item.moveAt(-14, 962, duration); } else { - item.moveAt(2, 942 - position * 99, animate ? 1000 : 0); + item.moveAt(2, 942 - position * 99, duration); } item.visible = true; item.setZ(99 - position); } } else { item.setZ(100); - item.moveAt(200, item.y, animate ? 1000 : 0); + item.moveAt(200, item.y, duration); } }); } diff --git a/src/ui/battle/WeaponEffect.spec.ts b/src/ui/battle/WeaponEffect.spec.ts index 9520665..b9a4525 100644 --- a/src/ui/battle/WeaponEffect.spec.ts +++ b/src/ui/battle/WeaponEffect.spec.ts @@ -12,22 +12,19 @@ module TK.SpaceTac.UI.Specs { battleview.timer = new Timer(); let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new TriggerAction("weapon")); - effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 500, 3000, true); + effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 1, true); let layer = battleview.arena.layer_weapon_effects; - check.equals(layer.length, 1); - - testgame.clockForward(600); check.equals(layer.length, 2); - let child = layer.list[0]; - if (check.instance(child, UIImage, "first child is an image")) { + check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); + + let child = layer.list[1]; + if (check.instance(child, UIImage, "second child is an image")) { check.nears(child.rotation, -2.677945044588987, 10); check.equals(child.x, 20, "x"); check.equals(child.y, 15, "y"); } - - check.instance(layer.list[1], Phaser.GameObjects.Particles.ParticleEmitterManager, "second child is an emitter"); }); test.case("displays gatling gun effect", check => { @@ -36,14 +33,14 @@ module TK.SpaceTac.UI.Specs { let ship = nn(battleview.battle.playing_ship); let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new TriggerAction("weapon")); - effect.gunEffect(); + effect.bulletsExecutor(1); let layer = battleview.arena.layer_weapon_effects; check.equals(layer.length, 1); check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); }); - test.case("displays shield and hull effect on impacted ships", check => { + test.acase("displays shield and hull effect on impacted ships", async check => { let battleview = testgame.view; battleview.timer = new Timer(); @@ -55,22 +52,27 @@ module TK.SpaceTac.UI.Specs { let dest = new Ship(); let effect = new WeaponEffect(battleview.arena, dest, Target.newFromShip(dest), weapon); - check.patch(effect, "getEffectForWeapon", () => (() => 100)); + check.patch(effect, "getEffectForWeapon", () => { + return { + execution: () => Promise.resolve(), + delay: () => 0 + }; + }); let mock_shield_impact = check.patch(effect, "shieldImpactEffect", null); let mock_hull_impact = check.patch(effect, "hullImpactEffect", null); ship.setValue("shield", 0); - effect.start(); + await effect.start(1); check.called(mock_shield_impact, 0); check.called(mock_hull_impact, [ - [Target.newFromShip(dest), ship.location, 40, 400] + [Target.newFromShip(dest), ship.location, 1] ]); ship.setValue("shield", 10); - effect.start(); + await effect.start(2); check.called(mock_shield_impact, [ - [Target.newFromShip(dest), ship.location, 40, 800, false] + [Target.newFromShip(dest), ship.location, 2, false] ]); check.called(mock_hull_impact, 0); }); @@ -81,12 +83,12 @@ module TK.SpaceTac.UI.Specs { let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(50, 50), new TriggerAction("weapon")); - effect.gunEffect(); + effect.bulletsExecutor(1); checkEmitters("gun effect started", 1); testgame.clockForward(6000); checkEmitters("gun effect ended", 0); - effect.hullImpactEffect({ x: 0, y: 0 }, { x: 50, y: 50 }, 1000, 2000); + effect.hullImpactEffect({ x: 0, y: 0 }, { x: 50, y: 50 }, 1); checkEmitters("hull effect started", 1); testgame.clockForward(8500); checkEmitters("hull effect ended", 0); @@ -97,9 +99,10 @@ module TK.SpaceTac.UI.Specs { battleview.timer = new Timer(); let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new TriggerAction("weapon")); - - let result = effect.angularLaser({ x: 20, y: 30 }, 300, Math.PI / 4, -Math.PI / 2, 5); - check.equals(result, 200); + effect.source = { x: 20, y: 30 }; + effect.action.angle = 90; + effect.action.range = 300; + effect.laserExecutor(5); let layer = battleview.arena.layer_weapon_effects; check.equals(layer.length, 1); @@ -109,14 +112,13 @@ module TK.SpaceTac.UI.Specs { //check.equals(image.width, 300); check.equals(image.x, 20); check.equals(image.y, 30); - check.nears(image.rotation, Math.PI / 4); - } + check.nears(image.rotation, 0.2606023917473408); - let values = battleview.animations.simulate(image, "rotation", 4); - check.nears(values[0], Math.PI / 4); - check.nears(values[1], 0); - check.nears(values[2], -Math.PI / 4); - check.nears(values[3], -Math.PI / 2); + checkTween(testgame, image, "rotation", [ + 0.2606023917473408, + 1.8313987185422373, + ]); + } }); }); } diff --git a/src/ui/battle/WeaponEffect.ts b/src/ui/battle/WeaponEffect.ts index 46d1298..bb7b1d0 100644 --- a/src/ui/battle/WeaponEffect.ts +++ b/src/ui/battle/WeaponEffect.ts @@ -1,13 +1,14 @@ module TK.SpaceTac.UI { + type WeaponEffectInfo = { + execution: (speed: number) => Promise + delay: (ship: Ship) => number + } + /** * Visual effects renderer for weapons. */ export class WeaponEffect { - // Link to game - private ui: MainUI - - // Link to arena - private arena: Arena + // Link to view private view: BattleView // Timer to use @@ -20,19 +21,17 @@ module TK.SpaceTac.UI { private builder: UIBuilder // Firing ship - private ship: Ship - private source: IArenaLocation + ship: Ship + source: IArenaLocation // Target (ship or space) - private target: Target - private destination: IArenaLocation + target: Target + destination: IArenaLocation // Weapon used - private action: TriggerAction + action: TriggerAction constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) { - this.ui = arena.game; - this.arena = arena; this.view = arena.view; this.timer = arena.view.timer; this.layer = arena.layer_weapon_effects; @@ -47,167 +46,187 @@ module TK.SpaceTac.UI { /** * Start the visual effect - * - * Returns the duration of the effect. */ - start(): number { + async start(speed: number): Promise { + if (!speed) { + return; + } + // Fire effect - let effect = this.getEffectForWeapon(this.action.code, this.action); - let duration = 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); + 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; - let damage_duration = this.damageEffect(source, ships, duration * 0.4, this.action.code == "gatlinggun"); - duration = Math.max(duration, damage_duration); + promises.push(this.damageEffect(source, ships, speed, this.action.code == "gatlinggun")); } - return duration; + await Promise.all(promises); } /** * Add a damage effect on ships impacted by a weapon */ - damageEffect(source: IArenaLocation, ships: Ship[], base_delay = 0, shield_flares = false): number { - let duration = 0; - - // TODO For each ship, delay should depend on fire effect animation - let delay = base_delay; - - ships.forEach(ship => { - if (ship.getValue("shield") > 0) { - this.shieldImpactEffect(source, ship.location, delay, 800, shield_flares); - duration = Math.max(duration, delay + 800); - } else { - this.hullImpactEffect(source, ship.location, delay, 400); - duration = Math.max(duration, delay + 400); - } + 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); + } + }); }); - return duration; + await Promise.all(promises); } /** * Get the function that will be called to start the visual effect */ - getEffectForWeapon(weapon: string, action: TriggerAction): () => number { + getEffectForWeapon(weapon: string, action: TriggerAction): WeaponEffectInfo { switch (weapon) { case "gatlinggun": - return () => this.gunEffect(); + return this.bulletsEffect(); case "prokhorovlaser": - let angle = arenaAngle(this.source, this.target); - let dangle = radians(action.angle) * 0.5; - return () => this.angularLaser(this.source, action.range, angle - dangle, angle + dangle); + return this.laserEffect(); default: - return () => this.defaultEffect(); + return this.genericEffect(); } } /** * Add a shield impact effect on a ship */ - shieldImpactEffect(from: IArenaLocation, ship: IArenaLocation, delay: number, duration: number, particles = false) { + 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); - - let tween1 = this.view.animations.addAnimation(effect, { alpha: 1 }, 100, undefined, delay); - let tween2 = this.view.animations.addAnimation(effect, { alpha: 0 }, 100, undefined, delay + duration); - tween2.then(() => effect.destroy()); - - if (particles) { - this.timer.schedule(delay, () => { - 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, - count: 50, - lifetime: 400, - 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, max: 80 } - }); - }); - } + 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 */ - hullImpactEffect(from: IArenaLocation, ship: IArenaLocation, delay: number, duration: number) { + 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, + emitDuration: 500 / speed, count: 50, - lifetime: 400, + 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, max: 260 } + speed: { min: 120 / speed, max: 260 / speed } }); + + return Promise.resolve(); // TODO } /** - * Default firing effect + * Generic weapon effect */ - defaultEffect(): number { - this.ui.audio.playOnce("battle-weapon-missile-launch"); + 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.blast; - let projectile_duration = arenaDistance(this.source, this.destination) * 1.5; - this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration || 1).then(() => { - missile.destroy(); - if (blast_radius > 0) { - this.ui.audio.playOnce("battle-weapon-missile-explosion"); + 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(); - 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); - Promise.all([ - this.view.animations.addAnimation(blast, { alpha: 0 }, 1450, "Quad.easeIn"), - this.view.animations.addAnimation(blast, { scaleX: scaling, scaleY: scaling }, 1500, "Quint.easeOut"), - ]).then(() => blast.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) { + result += 300 * Phaser.Math.Easing.Quintic.Out(arenaDistance(this.destination, ship.location) / this.action.blast); + } + return result; } - }); - - return projectile_duration + (blast_radius ? 1500 : 0); + } } /** * Laser effect, scanning from one angle to the other */ - angularLaser(source: IArenaLocation, radius: number, start_angle: number, end_angle: number, speed = 1): number { + 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"); + this.view.audio.playOnce("battle-weapon-laser", speed); - let laser = this.builder.image("battle-effects-laser", source.x, source.y); + let laser = this.builder.image("battle-effects-laser", this.source.x, this.source.y); laser.setOrigin(0, 0.5); - laser.setRotation(start_angle); - laser.setScale(radius / laser.width); - let dest_angle = laser.rotation + angularDifference(laser.rotation, end_angle); - this.view.animations.addAnimation(laser, { rotation: dest_angle }, duration).then(() => laser.destroy()); + laser.setRotation(angle - dangle); + laser.setScale(this.action.range / 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()); + } - return duration; + 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) */ - gunEffect(): number { - this.ui.audio.playOnce("battle-weapon-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); @@ -218,9 +237,8 @@ module TK.SpaceTac.UI { if (guard + 1 > distance) { guard = distance - 1; } - let speed = 2000; - let duration = 500; - let lifetime = 1000 * (distance - guard) / speed; + 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 }, @@ -229,11 +247,18 @@ module TK.SpaceTac.UI { lifetime: lifetime, direction: { minangle: angle, maxangle: angle }, scale: { min: 1, max: 1 }, - speed: { min: speed, max: speed }, + speed: { min: 2000 / speed, max: 2000 / speed }, facing: ParticleFacingMode.ALWAYS }); - return lifetime; + return this.timer.sleep(lifetime); + } + + private bulletsEffect(): WeaponEffectInfo { + return { + execution: speed => this.bulletsExecutor(speed), + delay: ship => 2000 / arenaDistance(this.source, ship.location) + } } } } diff --git a/src/ui/common/Animations.spec.ts b/src/ui/common/Animations.spec.ts index 7ead98b..832590d 100644 --- a/src/ui/common/Animations.spec.ts +++ b/src/ui/common/Animations.spec.ts @@ -57,8 +57,7 @@ module TK.SpaceTac.UI.Specs { test.case("animates rotation", check => { let obj = new UIBuilder(testgame.view).image("test"); obj.setRotation(-Math.PI * 2.5); - let result = testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear"); - check.equals(result, 750); + testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear"); let points = testgame.view.animations.simulate(obj, "rotation", 4); check.nears(points[0], -Math.PI * 0.5); check.nears(points[1], -Math.PI * 0.25); diff --git a/src/ui/common/Animations.ts b/src/ui/common/Animations.ts index 09d46ff..622caeb 100644 --- a/src/ui/common/Animations.ts +++ b/src/ui/common/Animations.ts @@ -16,6 +16,16 @@ module TK.SpaceTac.UI { freezeFrames?: boolean } + /** + * Configuration object for blink animations + */ + interface AnimationBlinkOptions { + alpha_on?: number + alpha_off?: number + times?: number + speed?: number + } + /** * Manager of all animations. * @@ -177,13 +187,23 @@ module TK.SpaceTac.UI { /** * Catch the player eye with a blink effect */ - async blink(obj: any, alpha_on = 1, alpha_off = 0.3, times = 3): Promise { + async blink(obj: { alpha: number }, config: AnimationBlinkOptions = {}): Promise { + let speed = coalesce(config.speed, 1); + let alpha_on = coalesce(config.alpha_on, 1); + + if (!speed) { + obj.alpha = alpha_on; + } + + let alpha_off = coalesce(config.alpha_off, 0.3); + let times = coalesce(config.times, 3); + if (obj.alpha != alpha_on) { - await this.addAnimation(obj, { alpha: alpha_on }, 150); + await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed); } for (let i = 0; i < times; i++) { - await this.addAnimation(obj, { alpha: alpha_off }, 150); - await this.addAnimation(obj, { alpha: alpha_on }, 150); + await this.addAnimation(obj, { alpha: alpha_off }, 150 / speed); + await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed); } } @@ -194,7 +214,7 @@ module TK.SpaceTac.UI { * * Returns the duration */ - rotationTween(obj: Phaser.GameObjects.Components.Transform, dest: number, speed = 1, easing = "Cubic.easeInOut"): number { + rotationTween(obj: Phaser.GameObjects.Components.Transform, dest: number, speed = 1, easing = "Cubic.easeInOut"): Promise { // Immediately change the object's current rotation to be in range (-pi,pi) let value = UITools.normalizeAngle(obj.rotation); obj.setRotation(value); @@ -210,9 +230,7 @@ module TK.SpaceTac.UI { let duration = distance * 1000 / speed; // Tween - this.addAnimation(obj, { rotation: dest }, duration, easing); - - return duration; + return this.addAnimation(obj, { rotation: dest }, duration, easing); } /** @@ -220,11 +238,10 @@ module TK.SpaceTac.UI { * * Returns the animation duration. */ - moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, ease = true): number { - let duration_rot = this.rotationTween(rotated_obj, angle, 0.5); + moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1, ease = true): Promise { + let duration_rot = this.rotationTween(rotated_obj, angle, 0.5 * speed); let duration_pos = arenaDistance(obj, { x: x, y: y }) * 2; - this.addAnimation(obj, { x: x, y: y }, duration_pos, ease ? "Quad.easeInOut" : "Linear"); - return Math.max(duration_rot, duration_pos); + return this.addAnimation(obj, { x: x, y: y }, duration_pos / speed, ease ? "Quad.easeInOut" : "Linear"); } /** @@ -232,39 +249,41 @@ module TK.SpaceTac.UI { * * Returns the animation duration. */ - moveInSpace(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj): number { + moveInSpace(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1): Promise { this.killPrevious(obj, ["x", "y"]); if (x == obj.x && y == obj.y) { - return this.rotationTween(rotated_obj, angle, 0.5); + return this.rotationTween(rotated_obj, angle, 0.5 * speed); } else { this.killPrevious(rotated_obj, ["rotation"]); let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y)); - let duration = Math.sqrt(distance / 1000) * 3000; + let duration = Math.sqrt(distance / 1000) * 3000 / speed; let curve_force = distance * 0.4; let prevx = obj.x; let prevy = obj.y; let xpts = [obj.x, obj.x + Math.cos(rotated_obj.rotation) * curve_force, x - Math.cos(angle) * curve_force, x]; let ypts = [obj.y, obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y]; let fobj = { t: 0 }; - this.tweens.add({ - targets: [fobj], - t: 1, - duration: duration, - ease: "Sine.easeInOut", - onUpdate: () => { - obj.setPosition( - Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]), - Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]), - ) - if (prevx != obj.x || prevy != obj.y) { - rotated_obj.setRotation(Math.atan2(obj.y - prevy, obj.x - prevx)); + return new Promise(resolve => { + this.tweens.add({ + targets: [fobj], + t: 1, + duration: duration, + ease: "Sine.easeInOut", + onComplete: resolve, + onUpdate: () => { + obj.setPosition( + Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]), + Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]), + ) + if (prevx != obj.x || prevy != obj.y) { + rotated_obj.setRotation(Math.atan2(obj.y - prevy, obj.x - prevx)); + } + prevx = obj.x; + prevy = obj.y; } - prevx = obj.x; - prevy = obj.y; - } - }) - return duration; + }) + }); } } } diff --git a/src/ui/common/Audio.ts b/src/ui/common/Audio.ts index 16d7b6a..b7a993a 100644 --- a/src/ui/common/Audio.ts +++ b/src/ui/common/Audio.ts @@ -36,7 +36,12 @@ module TK.SpaceTac.UI { /** * Play a single sound effect (fire-and-forget) */ - playOnce(key: string): void { + playOnce(key: string, speed = 1): void { + if (speed != 1) { + // TODO + return; + } + let manager = this.getManager(); if (manager) { if (this.hasCache(key)) {