1
0
Fork 0

Refactored animation system to be able to control the playback speed

This commit is contained in:
Michaël Lemaire 2018-06-11 00:58:42 +02:00
parent 9fcea58fc9
commit 7879457035
13 changed files with 316 additions and 276 deletions

View file

@ -59,7 +59,6 @@ Battle
* Add targetting shortcuts for "previous target", "next enemy" and "next ally" * 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 * 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 * 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 * 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 * BattleChecks should be done proactively when all diffs have been simulated by an action, in addition to reactively after applying

@ -1 +1 @@
Subproject commit 71b309744d7760ffc4ecdc14a1f68dbe2a25d419 Subproject commit 1425cb08935dd996a4c7a644ab793ff3b8355c9b

View file

@ -67,7 +67,7 @@ module TK.SpaceTac.UI {
this.range_hint.setLayer(this.layer_hints); this.range_hint.setLayer(this.layer_hints);
this.addShipSprites(); 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.checkDroneDeployed(diff));
view.log_processor.register(diff => this.checkDroneRecalled(diff)); view.log_processor.register(diff => this.checkDroneRecalled(diff));
@ -218,10 +218,8 @@ module TK.SpaceTac.UI {
/** /**
* Spawn a new drone * Spawn a new drone
*
* Return the duration of deploy animation
*/ */
addDrone(drone: Drone, animate = true): number { async addDrone(drone: Drone, speed = 1): Promise<void> {
if (!this.findDrone(drone)) { if (!this.findDrone(drone)) {
let sprite = new ArenaDrone(this.view, drone); let sprite = new ArenaDrone(this.view, drone);
let owner = this.view.battle.getShip(drone.owner) || new Ship(); let owner = this.view.battle.getShip(drone.owner) || new Ship();
@ -229,39 +227,32 @@ module TK.SpaceTac.UI {
this.layer_drones.add(sprite); this.layer_drones.add(sprite);
this.drone_sprites.push(sprite); this.drone_sprites.push(sprite);
if (animate) { if (speed) {
sprite.radius.setAlpha(0); sprite.radius.setAlpha(0);
sprite.setPosition(owner.arena_x, owner.arena_y); sprite.setPosition(owner.arena_x, owner.arena_y);
sprite.sprite.setRotation(owner.arena_angle); sprite.sprite.setRotation(owner.arena_angle);
let move_duration = this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite); await this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite, speed);
this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500, "Cubic.easeIn", move_duration); await this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500 / speed, "Cubic.easeIn");
return move_duration + 500;
} else { } else {
sprite.setPosition(drone.x, drone.y); sprite.setPosition(drone.x, drone.y);
sprite.setRotation(angle); sprite.setRotation(angle);
return 0;
} }
} else { } else {
console.error("Drone added twice to arena", drone); console.error("Drone added twice to arena", drone);
return 0;
} }
} }
/** /**
* Remove a destroyed drone * Remove a destroyed drone
*
* Return the duration of deploy animation
*/ */
removeDrone(drone: Drone): number { async removeDrone(drone: Drone, speed = 1): Promise<void> {
let sprite = this.findDrone(drone); let sprite = this.findDrone(drone);
if (sprite) { if (sprite) {
remove(this.drone_sprites, sprite); remove(this.drone_sprites, sprite);
return sprite.setDestroyed(); return sprite.setDestroyed();
} else { } else {
console.error("Drone not found in arena for removal", drone); 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 { private checkDroneDeployed(diff: BaseBattleDiff): LogProcessorDelegate {
if (diff instanceof DroneDeployedDiff) { if (diff instanceof DroneDeployedDiff) {
return { return {
foreground: async (animate) => { foreground: async (speed: number) => {
let duration = this.addDrone(diff.drone, animate); if (speed) {
if (duration) {
this.view.gameui.audio.playOnce("battle-drone-deploy"); this.view.gameui.audio.playOnce("battle-drone-deploy");
if (animate) {
await this.view.timer.sleep(duration);
}
} }
await this.addDrone(diff.drone, speed);
} }
} }
} else { } else {
@ -311,12 +299,11 @@ module TK.SpaceTac.UI {
private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate { private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate {
if (diff instanceof DroneRecalledDiff) { if (diff instanceof DroneRecalledDiff) {
return { return {
foreground: async () => { foreground: async (speed: number) => {
let duration = this.removeDrone(diff.drone); if (speed) {
if (duration) {
this.view.gameui.audio.playOnce("battle-drone-destroy"); this.view.gameui.audio.playOnce("battle-drone-destroy");
await this.view.timer.sleep(duration);
} }
await this.removeDrone(diff.drone, speed);
} }
} }
} else { } else {

View file

@ -59,10 +59,10 @@ module TK.SpaceTac.UI {
* *
* Return the animation duration * Return the animation duration
*/ */
setDestroyed(): number { async setDestroyed(): Promise<void> {
this.view.animations.addAnimation<UIContainer>(this, { alpha: 0.3 }, 300, undefined, 200); this.view.animations.addAnimation<UIContainer>(this, { alpha: 0.3 }, 300, undefined, 200);
this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500).then(() => this.destroy()); await this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500);
return 500; this.destroy();
} }
/** /**

View file

@ -104,9 +104,9 @@ module TK.SpaceTac.UI {
// Set location // Set location
if (this.battleview.battle.cycle == 1 && this.battleview.battle.play_index == 0 && ship.alive && this.battleview.player.is(ship.fleet.player)) { 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.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 { } 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 // Log processing
@ -132,86 +132,88 @@ module TK.SpaceTac.UI {
* Process a ship diff * Process a ship diff
*/ */
private processShipDiff(diff: BaseBattleShipDiff): LogProcessorDelegate { private processShipDiff(diff: BaseBattleShipDiff): LogProcessorDelegate {
let timer = this.battleview.timer;
if (diff instanceof ShipEffectAddedDiff || diff instanceof ShipEffectRemovedDiff) { if (diff instanceof ShipEffectAddedDiff || diff instanceof ShipEffectRemovedDiff) {
return { return {
background: async () => this.updateActiveEffects() background: async () => this.updateActiveEffects()
} }
} else if (diff instanceof ShipValueDiff) { } else if (diff instanceof ShipValueDiff) {
return { return {
background: async (animate, timer) => { background: async (speed: number) => {
if (animate) { if (speed) {
this.toggle_hsp.manipulate("value")(true); this.toggle_hsp.manipulate("value")(true);
} }
if (diff.code == "hull") { if (diff.code == "hull") {
if (animate) { if (speed) {
this.updateHull(this.ship.getValue("hull") - diff.diff, diff.diff); 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")); this.updateHull(this.ship.getValue("hull"));
await timer.sleep(500); await timer.sleep(500 / speed);
} else { } else {
this.updateHull(this.ship.getValue("hull")); this.updateHull(this.ship.getValue("hull"));
} }
} else if (diff.code == "shield") { } else if (diff.code == "shield") {
if (animate) { if (speed) {
this.updateShield(this.ship.getValue("shield") - diff.diff, diff.diff); 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")); this.updateShield(this.ship.getValue("shield"));
await timer.sleep(500); await timer.sleep(500 / speed);
} else { } else {
this.updateShield(this.ship.getValue("shield")); this.updateShield(this.ship.getValue("shield"));
} }
} else if (diff.code == "power") { } else if (diff.code == "power") {
this.power_text.setText(`${this.ship.getValue("power")}`); this.power_text.setText(`${this.ship.getValue("power")}`);
if (animate) { if (speed) {
await this.battleview.animations.blink(this.power_text); await this.battleview.animations.blink(this.power_text, { speed: speed });
} }
} }
if (animate) { if (speed) {
await timer.sleep(500); await timer.sleep(500 / speed);
this.toggle_hsp.manipulate("value")(false); this.toggle_hsp.manipulate("value")(false);
} }
} }
} }
} else if (diff instanceof ShipAttributeDiff) { } else if (diff instanceof ShipAttributeDiff) {
return { return {
background: async (animate, timer) => { background: async (speed: number) => {
if (animate) { if (speed) {
this.displayAttributeChanged(diff); this.displayAttributeChanged(diff, speed);
if (diff.code == "evasion") { if (diff.code == "evasion") {
// TODO diff // TODO diff
this.updateEvasion(this.ship.getAttribute("evasion")); 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) { } else if (diff instanceof ShipDamageDiff) {
return { return {
background: async (animate, timer) => { background: async (speed: number) => {
if (animate) { if (speed) {
await this.displayEffect(`${diff.theoretical} damage`, false); await this.displayEffect(`${diff.theoretical} damage`, false, speed);
await timer.sleep(1000); await timer.sleep(1000 / speed);
} }
} }
} }
} else if (diff instanceof ShipActionToggleDiff) { } else if (diff instanceof ShipActionToggleDiff) {
return { return {
foreground: async (animate, timer) => { foreground: async (speed: number) => {
let action = this.ship.actions.getById(diff.action); let action = this.ship.actions.getById(diff.action);
if (action) { if (action) {
if (animate) { if (speed) {
if (diff.activated) { if (diff.activated) {
await this.displayEffect(`${action.name} ON`, true); await this.displayEffect(`${action.name} ON`, true, speed);
} else { } else {
await this.displayEffect(`${action.name} OFF`, false); await this.displayEffect(`${action.name} OFF`, false, speed);
} }
} }
this.updateEffectsRadius(); this.updateEffectsRadius();
await timer.sleep(500); await timer.sleep(500 / speed);
} }
} }
} }
@ -220,20 +222,20 @@ module TK.SpaceTac.UI {
if (action) { if (action) {
if (action instanceof EndTurnAction) { if (action instanceof EndTurnAction) {
return { return {
foreground: async (animate, timer) => { foreground: async (speed: number) => {
if (animate) { if (speed) {
await this.displayEffect("End turn", true); await this.displayEffect("End turn", true, speed);
await timer.sleep(500); await timer.sleep(500 / speed);
} }
} }
} }
} else if (!(action instanceof ToggleAction)) { } else if (!(action instanceof ToggleAction)) {
let action_name = action.name; let action_name = action.name;
return { return {
foreground: async (animate, timer) => { foreground: async (speed: number) => {
if (animate) { if (speed) {
await this.displayEffect(action_name, true); await this.displayEffect(action_name, true, speed);
await timer.sleep(300); await timer.sleep(300 / speed);
} }
} }
} }
@ -244,11 +246,12 @@ module TK.SpaceTac.UI {
return {}; return {};
} }
} else if (diff instanceof ShipMoveDiff) { } else if (diff instanceof ShipMoveDiff) {
let func = async (animate: boolean, timer: Timer) => { let func = async (speed: number) => {
this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, false); if (speed) {
let duration = this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, animate, !!diff.engine); await this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, 0);
if (duration && animate) { await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, speed, !!diff.engine);
await timer.sleep(duration); } else {
await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, 0);
} }
}; };
if (diff.engine) { if (diff.engine) {
@ -259,10 +262,10 @@ module TK.SpaceTac.UI {
} else if (diff instanceof VigilanceAppliedDiff) { } else if (diff instanceof VigilanceAppliedDiff) {
let action = this.ship.actions.getById(diff.action); let action = this.ship.actions.getById(diff.action);
return { return {
foreground: async (animate, timer) => { foreground: async (speed: number) => {
if (animate && action) { if (speed && action) {
await this.displayEffect(`${action.name} (vigilance)`, true); await this.displayEffect(`${action.name} (vigilance)`, true, speed);
await timer.sleep(300); await timer.sleep(300 / speed);
} }
} }
} }
@ -315,7 +318,7 @@ module TK.SpaceTac.UI {
//this.displayEffect("stasis", false); //this.displayEffect("stasis", false);
this.stasis.visible = true; this.stasis.visible = true;
this.stasis.alpha = 0; 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 { } else {
this.stasis.visible = false; this.stasis.visible = false;
} }
@ -327,30 +330,31 @@ module TK.SpaceTac.UI {
* *
* Return the duration of animation * Return the duration of animation
*/ */
moveToArenaLocation(x: number, y: number, facing_angle: number, animate = true, engine = true): number { async moveToArenaLocation(x: number, y: number, facing_angle: number, speed = 1, engine = true): Promise<void> {
if (animate) { if (speed) {
let animation = bound(this.arena.view.animations, engine ? "moveInSpace" : "moveTo"); let animation = bound(this.arena.view.animations, engine ? "moveInSpace" : "moveTo");
let duration = animation(this, x, y, facing_angle, this.sprite); await animation(this, x, y, facing_angle, this.sprite, speed);
return duration;
} else { } else {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.sprite.rotation = facing_angle; this.sprite.rotation = facing_angle;
return 0;
} }
} }
/** /**
* Briefly show an effect on this ship * 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) { if (!this.effects_messages.visible) {
this.effects_messages.removeAll(true); 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" 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.ship.arena_y < arena.height * 0.9) ? 60 : (-60 - this.effects_messages.height)
); );
this.effects_messages_toggle.manipulate("added")(1400); this.effects_messages_toggle.manipulate("added")(1400 / speed);
await this.battleview.timer.sleep(1500); await this.battleview.timer.sleep(1500 / speed);
} }
/** /**
* Display interesting changes in ship attributes * Display interesting changes in ship attributes
*/ */
displayAttributeChanged(event: ShipAttributeDiff) { displayAttributeChanged(event: ShipAttributeDiff, speed = 1) {
// TODO show final diff, not just cumulative one // TODO show final diff, not just cumulative one
let diff = (event.added.cumulative || 0) - (event.removed.cumulative || 0); let diff = (event.added.cumulative || 0) - (event.removed.cumulative || 0);
if (diff) { if (diff) {
let name = SHIP_VALUES_NAMES[event.code]; 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);
} }
} }

View file

@ -155,15 +155,14 @@ module TK.SpaceTac.UI {
let ship = this.view.battle.playing_ship; let ship = this.view.battle.playing_ship;
if (ship) { if (ship) {
let result = callback(ship); let result = callback(ship);
let timer = new Timer(true);
if (result.foreground) { if (result.foreground) {
let promise = result.foreground(false, timer); let promise = result.foreground(0);
if (result.background) { if (result.background) {
let next = result.background; let next = result.background;
promise.then(() => next(false, timer)); promise.then(() => next(0));
} }
} else if (result.background) { } else if (result.background) {
result.background(false, timer); result.background(0);
} }
} }
} }
@ -176,7 +175,7 @@ module TK.SpaceTac.UI {
if (this.debug) { if (this.debug) {
console.log("Battle diff", diff); 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 // TODO add priority to sort the delegates
let delegates = this.subscriber.map(subscriber => subscriber(diff)); let delegates = this.subscriber.map(subscriber => subscriber(diff));
@ -189,11 +188,11 @@ module TK.SpaceTac.UI {
this.background_promises = []; this.background_promises = [];
} }
let promises = foregrounds.map(foreground => foreground(timed, timer)); let promises = foregrounds.map(foreground => foreground(speed));
await Promise.all(promises); 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); this.background_promises = this.background_promises.concat(promises);
} }
@ -263,9 +262,9 @@ module TK.SpaceTac.UI {
if (action && action instanceof TriggerAction) { if (action && action instanceof TriggerAction) {
let effect = new WeaponEffect(this.view.arena, ship, diff.target, action); let effect = new WeaponEffect(this.view.arena, ship, diff.target, action);
return { return {
foreground: async (animate, timer) => { foreground: async (speed: number) => {
if (animate) { if (speed) {
await this.view.timer.sleep(effect.start()) await effect.start(speed);
} }
} }
} }
@ -286,14 +285,14 @@ module TK.SpaceTac.UI {
if (ship) { if (ship) {
let dead_ship = ship; let dead_ship = ship;
return { return {
foreground: async (animate) => { foreground: async (speed: number) => {
if (dead_ship.is(this.view.ship_hovered)) { if (dead_ship.is(this.view.ship_hovered)) {
this.view.setShipHovered(null); this.view.setShipHovered(null);
} }
this.view.arena.markAsDead(dead_ship); this.view.arena.markAsDead(dead_ship);
this.view.ship_list.refresh(); this.view.ship_list.refresh();
if (animate) { if (speed) {
await this.view.timer.sleep(2000); 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 * *background* is started when no other foreground delegate is working or pending
*/ */
export type LogProcessorDelegate = { export type LogProcessorDelegate = {
foreground?: (animate: boolean, timer: Timer) => Promise<void>, foreground?: (speed: number) => Promise<void>,
background?: (animate: boolean, timer: Timer) => Promise<void>, background?: (speed: number) => Promise<void>,
} }
/** /**

View file

@ -35,7 +35,7 @@ module TK.SpaceTac.UI.Specs {
}); });
battle.throwInitiative(); battle.throwInitiative();
list.refresh(false); list.refresh(0);
check.in("ship now in play order", check => { check.in("ship now in play order", check => {
check.equals(list.items[0].visible, true, "ship card visible"); 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]); battle.setPlayingShip(battle.play_order[0]);
list.refresh(false); list.refresh(0);
check.in("started", check => { 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[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"); check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position");
}); });
battle.advanceToNextShip(); battle.advanceToNextShip();
list.refresh(false); list.refresh(0);
check.in("end turn", check => { 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[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"); 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]; let dead = battle.play_order[1];
dead.setDead(); dead.setDead();
list.refresh(false); list.refresh(0);
check.in("ship dead", check => { check.in("ship dead", check => {
check.equals(list.items.length, 3, "item count"); 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"); check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position");

View file

@ -62,7 +62,7 @@ module TK.SpaceTac.UI {
setShipsFromBattle(battle: Battle, animate = true): void { setShipsFromBattle(battle: Battle, animate = true): void {
this.clearAll(); this.clearAll();
iforeach(battle.iships(true), ship => this.addShip(ship)); 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 { bindToLog(log: LogProcessor): void {
log.watchForShipChange(ship => { log.watchForShipChange(ship => {
return { return {
foreground: async (animate) => { foreground: async (speed: number) => {
this.refresh(animate) this.refresh(speed);
} }
} }
}); });
@ -114,7 +114,8 @@ module TK.SpaceTac.UI {
/** /**
* Update the locations of all items * Update the locations of all items
*/ */
refresh(animate = true): void { refresh(speed = 1): void {
let duration = speed ? 1000 / speed : 0;
this.items.forEach(item => { this.items.forEach(item => {
if (item.ship.alive) { if (item.ship.alive) {
let position = this.battle.getPlayOrder(item.ship); let position = this.battle.getPlayOrder(item.ship);
@ -122,16 +123,16 @@ module TK.SpaceTac.UI {
item.visible = false; item.visible = false;
} else { } else {
if (position == 0) { if (position == 0) {
item.moveAt(-14, 962, animate ? 1000 : 0); item.moveAt(-14, 962, duration);
} else { } else {
item.moveAt(2, 942 - position * 99, animate ? 1000 : 0); item.moveAt(2, 942 - position * 99, duration);
} }
item.visible = true; item.visible = true;
item.setZ(99 - position); item.setZ(99 - position);
} }
} else { } else {
item.setZ(100); item.setZ(100);
item.moveAt(200, item.y, animate ? 1000 : 0); item.moveAt(200, item.y, duration);
} }
}); });
} }

View file

@ -12,22 +12,19 @@ module TK.SpaceTac.UI.Specs {
battleview.timer = new Timer(); battleview.timer = new Timer();
let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new TriggerAction("weapon")); 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; let layer = battleview.arena.layer_weapon_effects;
check.equals(layer.length, 1);
testgame.clockForward(600);
check.equals(layer.length, 2); check.equals(layer.length, 2);
let child = layer.list[0]; check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter");
if (check.instance(child, UIImage, "first child is an image")) {
let child = layer.list[1];
if (check.instance(child, UIImage, "second child is an image")) {
check.nears(child.rotation, -2.677945044588987, 10); check.nears(child.rotation, -2.677945044588987, 10);
check.equals(child.x, 20, "x"); check.equals(child.x, 20, "x");
check.equals(child.y, 15, "y"); 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 => { test.case("displays gatling gun effect", check => {
@ -36,14 +33,14 @@ module TK.SpaceTac.UI.Specs {
let ship = nn(battleview.battle.playing_ship); let ship = nn(battleview.battle.playing_ship);
let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new TriggerAction("weapon")); 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; let layer = battleview.arena.layer_weapon_effects;
check.equals(layer.length, 1); check.equals(layer.length, 1);
check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); 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; let battleview = testgame.view;
battleview.timer = new Timer(); battleview.timer = new Timer();
@ -55,22 +52,27 @@ module TK.SpaceTac.UI.Specs {
let dest = new Ship(); let dest = new Ship();
let effect = new WeaponEffect(battleview.arena, dest, Target.newFromShip(dest), weapon); 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_shield_impact = check.patch(effect, "shieldImpactEffect", null);
let mock_hull_impact = check.patch(effect, "hullImpactEffect", null); let mock_hull_impact = check.patch(effect, "hullImpactEffect", null);
ship.setValue("shield", 0); ship.setValue("shield", 0);
effect.start(); await effect.start(1);
check.called(mock_shield_impact, 0); check.called(mock_shield_impact, 0);
check.called(mock_hull_impact, [ check.called(mock_hull_impact, [
[Target.newFromShip(dest), ship.location, 40, 400] [Target.newFromShip(dest), ship.location, 1]
]); ]);
ship.setValue("shield", 10); ship.setValue("shield", 10);
effect.start(); await effect.start(2);
check.called(mock_shield_impact, [ 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); 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")); 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); checkEmitters("gun effect started", 1);
testgame.clockForward(6000); testgame.clockForward(6000);
checkEmitters("gun effect ended", 0); 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); checkEmitters("hull effect started", 1);
testgame.clockForward(8500); testgame.clockForward(8500);
checkEmitters("hull effect ended", 0); checkEmitters("hull effect ended", 0);
@ -97,9 +99,10 @@ module TK.SpaceTac.UI.Specs {
battleview.timer = new Timer(); battleview.timer = new Timer();
let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new TriggerAction("weapon")); let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new TriggerAction("weapon"));
effect.source = { x: 20, y: 30 };
let result = effect.angularLaser({ x: 20, y: 30 }, 300, Math.PI / 4, -Math.PI / 2, 5); effect.action.angle = 90;
check.equals(result, 200); effect.action.range = 300;
effect.laserExecutor(5);
let layer = battleview.arena.layer_weapon_effects; let layer = battleview.arena.layer_weapon_effects;
check.equals(layer.length, 1); check.equals(layer.length, 1);
@ -109,14 +112,13 @@ module TK.SpaceTac.UI.Specs {
//check.equals(image.width, 300); //check.equals(image.width, 300);
check.equals(image.x, 20); check.equals(image.x, 20);
check.equals(image.y, 30); 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); checkTween(testgame, image, "rotation", [
check.nears(values[0], Math.PI / 4); 0.2606023917473408,
check.nears(values[1], 0); 1.8313987185422373,
check.nears(values[2], -Math.PI / 4); ]);
check.nears(values[3], -Math.PI / 2); }
}); });
}); });
} }

View file

@ -1,13 +1,14 @@
module TK.SpaceTac.UI { module TK.SpaceTac.UI {
type WeaponEffectInfo = {
execution: (speed: number) => Promise<void>
delay: (ship: Ship) => number
}
/** /**
* Visual effects renderer for weapons. * Visual effects renderer for weapons.
*/ */
export class WeaponEffect { export class WeaponEffect {
// Link to game // Link to view
private ui: MainUI
// Link to arena
private arena: Arena
private view: BattleView private view: BattleView
// Timer to use // Timer to use
@ -20,19 +21,17 @@ module TK.SpaceTac.UI {
private builder: UIBuilder private builder: UIBuilder
// Firing ship // Firing ship
private ship: Ship ship: Ship
private source: IArenaLocation source: IArenaLocation
// Target (ship or space) // Target (ship or space)
private target: Target target: Target
private destination: IArenaLocation destination: IArenaLocation
// Weapon used // Weapon used
private action: TriggerAction action: TriggerAction
constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) { constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) {
this.ui = arena.game;
this.arena = arena;
this.view = arena.view; this.view = arena.view;
this.timer = arena.view.timer; this.timer = arena.view.timer;
this.layer = arena.layer_weapon_effects; this.layer = arena.layer_weapon_effects;
@ -47,167 +46,187 @@ module TK.SpaceTac.UI {
/** /**
* Start the visual effect * Start the visual effect
*
* Returns the duration of the effect.
*/ */
start(): number { async start(speed: number): Promise<void> {
if (!speed) {
return;
}
// Fire effect // Fire effect
let effect = this.getEffectForWeapon(this.action.code, this.action); let fire_effect = this.getEffectForWeapon(this.action.code, this.action);
let duration = effect(); let promises = [fire_effect.execution(speed)];
// Damage effect // Damage effect
let action = this.action; let action = this.action;
if (any(action.effects, effect => effect instanceof DamageEffect)) { 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 source = action.blast ? this.target : this.source;
let damage_duration = this.damageEffect(source, ships, duration * 0.4, this.action.code == "gatlinggun"); promises.push(this.damageEffect(source, ships, speed, this.action.code == "gatlinggun"));
duration = Math.max(duration, damage_duration);
} }
return duration; await Promise.all(promises);
} }
/** /**
* Add a damage effect on ships impacted by a weapon * Add a damage effect on ships impacted by a weapon
*/ */
damageEffect(source: IArenaLocation, ships: Ship[], base_delay = 0, shield_flares = false): number { async damageEffect(source: IArenaLocation, ships: [Ship, number][], speed = 1, shield_flares = false): Promise<void> {
let duration = 0; let promises = ships.map(([ship, delay]) => {
return this.timer.sleep(delay).then(() => {
// TODO For each ship, delay should depend on fire effect animation if (ship.getValue("shield") > 0) {
let delay = base_delay; return this.shieldImpactEffect(source, ship.location, speed, shield_flares);
} else {
ships.forEach(ship => { return this.hullImpactEffect(source, ship.location, speed);
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);
}
}); });
return duration; await Promise.all(promises);
} }
/** /**
* Get the function that will be called to start the visual effect * 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) { switch (weapon) {
case "gatlinggun": case "gatlinggun":
return () => this.gunEffect(); return this.bulletsEffect();
case "prokhorovlaser": case "prokhorovlaser":
let angle = arenaAngle(this.source, this.target); return this.laserEffect();
let dangle = radians(action.angle) * 0.5;
return () => this.angularLaser(this.source, action.range, angle - dangle, angle + dangle);
default: default:
return () => this.defaultEffect(); return this.genericEffect();
} }
} }
/** /**
* Add a shield impact effect on a ship * 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<void> {
let angle = Math.atan2(from.y - ship.y, from.x - ship.x); 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); let effect = this.builder.image("battle-effects-shield-impact", ship.x, ship.y, true);
effect.setAlpha(0); effect.setAlpha(0);
effect.setRotation(angle); effect.setRotation(angle);
await this.view.animations.addAnimation(effect, { alpha: 1 }, 100 / speed, undefined);
let tween1 = this.view.animations.addAnimation(effect, { alpha: 1 }, 100, undefined, delay); await this.timer.sleep(800 / speed);
let tween2 = this.view.animations.addAnimation(effect, { alpha: 0 }, 100, undefined, delay + duration); await this.view.animations.addAnimation(effect, { alpha: 0 }, 100 / speed, undefined);
tween2.then(() => effect.destroy()); 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 }
});
});
}
} }
/** /**
* Add a hull impact effect on a ship * 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<void> {
let angle = Math.atan2(from.y - ship.y, from.x - ship.x); let angle = Math.atan2(from.y - ship.y, from.x - ship.x);
this.builder.particles({ this.builder.particles({
key: "battle-effects-hot", key: "battle-effects-hot",
source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 7 }, source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 7 },
emitDuration: 500, emitDuration: 500 / speed,
count: 50, count: 50,
lifetime: 400, lifetime: 400 / speed,
fading: true, fading: true,
direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 },
scale: { min: 1, max: 2 }, 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 { async genericExecutor(speed: number): Promise<void> {
this.ui.audio.playOnce("battle-weapon-missile-launch"); this.view.audio.playOnce("battle-weapon-missile-launch", speed);
let missile = this.builder.image("battle-effects-default", this.source.x, this.source.y, true); let missile = this.builder.image("battle-effects-default", this.source.x, this.source.y, true);
missile.setRotation(arenaAngle(this.source, this.destination)); missile.setRotation(arenaAngle(this.source, this.destination));
let blast_radius = this.action.blast; let blast_radius = this.action.blast;
let projectile_duration = arenaDistance(this.source, this.destination) * 1.5; let projectile_duration = (arenaDistance(this.source, this.destination) * 1.5) || 1;
this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration || 1).then(() => { await this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration / speed);
missile.destroy(); missile.destroy();
if (blast_radius > 0) {
this.ui.audio.playOnce("battle-weapon-missile-explosion");
let blast = this.builder.image("battle-effects-blast", this.destination.x, this.destination.y, true); if (blast_radius > 0) {
let scaling = blast_radius * 2 / (blast.width * 0.9); this.view.audio.playOnce("battle-weapon-missile-explosion", speed);
blast.setScale(0.001);
Promise.all([ let blast = this.builder.image("battle-effects-blast", this.destination.x, this.destination.y, true);
this.view.animations.addAnimation(blast, { alpha: 0 }, 1450, "Quad.easeIn"), let scaling = blast_radius * 2 / (blast.width * 0.9);
this.view.animations.addAnimation(blast, { scaleX: scaling, scaleY: scaling }, 1500, "Quint.easeOut"), blast.setScale(0.001);
]).then(() => blast.destroy());
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 * 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<void> {
let duration = 1000 / speed; 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.setOrigin(0, 0.5);
laser.setRotation(start_angle); laser.setRotation(angle - dangle);
laser.setScale(radius / laser.width); laser.setScale(this.action.range / laser.width);
let dest_angle = laser.rotation + angularDifference(laser.rotation, end_angle); let dest_angle = laser.rotation + angularDifference(laser.rotation, angle + dangle);
this.view.animations.addAnimation(laser, { rotation: dest_angle }, duration).then(() => laser.destroy()); 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) * Submachine gun effect (quick chain of small bullets)
*/ */
gunEffect(): number { bulletsExecutor(speed: number): Promise<void> {
this.ui.audio.playOnce("battle-weapon-bullets"); this.view.audio.playOnce("battle-weapon-bullets", speed);
let target_ship = this.target.getShip(this.view.battle); let target_ship = this.target.getShip(this.view.battle);
let has_shield = target_ship && (target_ship.getValue("shield") > 0); let has_shield = target_ship && (target_ship.getValue("shield") > 0);
@ -218,9 +237,8 @@ module TK.SpaceTac.UI {
if (guard + 1 > distance) { if (guard + 1 > distance) {
guard = distance - 1; guard = distance - 1;
} }
let speed = 2000; let duration = 500 / speed;
let duration = 500; let lifetime = (distance - guard) / (2 * speed);
let lifetime = 1000 * (distance - guard) / speed;
this.builder.particles({ this.builder.particles({
key: "battle-effects-bullets", key: "battle-effects-bullets",
source: { x: this.source.x + Math.cos(angle) * 35, y: this.source.y + Math.sin(angle) * 35, radius: 3 }, 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, lifetime: lifetime,
direction: { minangle: angle, maxangle: angle }, direction: { minangle: angle, maxangle: angle },
scale: { min: 1, max: 1 }, scale: { min: 1, max: 1 },
speed: { min: speed, max: speed }, speed: { min: 2000 / speed, max: 2000 / speed },
facing: ParticleFacingMode.ALWAYS 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)
}
} }
} }
} }

View file

@ -57,8 +57,7 @@ module TK.SpaceTac.UI.Specs {
test.case("animates rotation", check => { test.case("animates rotation", check => {
let obj = new UIBuilder(testgame.view).image("test"); let obj = new UIBuilder(testgame.view).image("test");
obj.setRotation(-Math.PI * 2.5); obj.setRotation(-Math.PI * 2.5);
let result = testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear"); testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear");
check.equals(result, 750);
let points = testgame.view.animations.simulate(obj, "rotation", 4); let points = testgame.view.animations.simulate(obj, "rotation", 4);
check.nears(points[0], -Math.PI * 0.5); check.nears(points[0], -Math.PI * 0.5);
check.nears(points[1], -Math.PI * 0.25); check.nears(points[1], -Math.PI * 0.25);

View file

@ -16,6 +16,16 @@ module TK.SpaceTac.UI {
freezeFrames?: boolean freezeFrames?: boolean
} }
/**
* Configuration object for blink animations
*/
interface AnimationBlinkOptions {
alpha_on?: number
alpha_off?: number
times?: number
speed?: number
}
/** /**
* Manager of all animations. * Manager of all animations.
* *
@ -177,13 +187,23 @@ module TK.SpaceTac.UI {
/** /**
* Catch the player eye with a blink effect * Catch the player eye with a blink effect
*/ */
async blink(obj: any, alpha_on = 1, alpha_off = 0.3, times = 3): Promise<void> { async blink(obj: { alpha: number }, config: AnimationBlinkOptions = {}): Promise<void> {
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) { 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++) { for (let i = 0; i < times; i++) {
await this.addAnimation(obj, { alpha: alpha_off }, 150); await this.addAnimation(obj, { alpha: alpha_off }, 150 / speed);
await this.addAnimation(obj, { alpha: alpha_on }, 150); await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed);
} }
} }
@ -194,7 +214,7 @@ module TK.SpaceTac.UI {
* *
* Returns the duration * 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<void> {
// Immediately change the object's current rotation to be in range (-pi,pi) // Immediately change the object's current rotation to be in range (-pi,pi)
let value = UITools.normalizeAngle(obj.rotation); let value = UITools.normalizeAngle(obj.rotation);
obj.setRotation(value); obj.setRotation(value);
@ -210,9 +230,7 @@ module TK.SpaceTac.UI {
let duration = distance * 1000 / speed; let duration = distance * 1000 / speed;
// Tween // Tween
this.addAnimation(obj, { rotation: dest }, duration, easing); return this.addAnimation(obj, { rotation: dest }, duration, easing);
return duration;
} }
/** /**
@ -220,11 +238,10 @@ module TK.SpaceTac.UI {
* *
* Returns the animation duration. * Returns the animation duration.
*/ */
moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, ease = true): number { moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1, ease = true): Promise<void> {
let duration_rot = this.rotationTween(rotated_obj, angle, 0.5); let duration_rot = this.rotationTween(rotated_obj, angle, 0.5 * speed);
let duration_pos = arenaDistance(obj, { x: x, y: y }) * 2; let duration_pos = arenaDistance(obj, { x: x, y: y }) * 2;
this.addAnimation(obj, { x: x, y: y }, duration_pos, ease ? "Quad.easeInOut" : "Linear"); return this.addAnimation(obj, { x: x, y: y }, duration_pos / speed, ease ? "Quad.easeInOut" : "Linear");
return Math.max(duration_rot, duration_pos);
} }
/** /**
@ -232,39 +249,41 @@ module TK.SpaceTac.UI {
* *
* Returns the animation duration. * 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<void> {
this.killPrevious(obj, ["x", "y"]); this.killPrevious(obj, ["x", "y"]);
if (x == obj.x && y == obj.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 { } else {
this.killPrevious(rotated_obj, ["rotation"]); this.killPrevious(rotated_obj, ["rotation"]);
let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y)); 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 curve_force = distance * 0.4;
let prevx = obj.x; let prevx = obj.x;
let prevy = obj.y; 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 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 ypts = [obj.y, obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y];
let fobj = { t: 0 }; let fobj = { t: 0 };
this.tweens.add({ return new Promise(resolve => {
targets: [fobj], this.tweens.add({
t: 1, targets: [fobj],
duration: duration, t: 1,
ease: "Sine.easeInOut", duration: duration,
onUpdate: () => { ease: "Sine.easeInOut",
obj.setPosition( onComplete: resolve,
Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]), onUpdate: () => {
Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]), obj.setPosition(
) Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]),
if (prevx != obj.x || prevy != obj.y) { Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]),
rotated_obj.setRotation(Math.atan2(obj.y - prevy, obj.x - prevx)); )
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;
} }
} }
} }

View file

@ -36,7 +36,12 @@ module TK.SpaceTac.UI {
/** /**
* Play a single sound effect (fire-and-forget) * 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(); let manager = this.getManager();
if (manager) { if (manager) {
if (this.hasCache(key)) { if (this.hasCache(key)) {