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"
* 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

@ -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.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<void> {
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<void> {
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 {

View file

@ -59,10 +59,10 @@ module TK.SpaceTac.UI {
*
* 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(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();
}
/**

View file

@ -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<void> {
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);
}
}

View file

@ -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<void>,
background?: (animate: boolean, timer: Timer) => Promise<void>,
foreground?: (speed: number) => Promise<void>,
background?: (speed: number) => Promise<void>,
}
/**

View file

@ -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");

View file

@ -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);
}
});
}

View file

@ -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,
]);
}
});
});
}

View file

@ -1,13 +1,14 @@
module TK.SpaceTac.UI {
type WeaponEffectInfo = {
execution: (speed: number) => Promise<void>
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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)
}
}
}
}

View file

@ -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);

View file

@ -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<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) {
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<void> {
// 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<void> {
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<void> {
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;
})
});
}
}
}

View file

@ -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)) {