1
0
Fork 0

Pause log processing during ship and weapon animations

This commit is contained in:
Michaël Lemaire 2017-02-14 01:30:50 +01:00
parent 5b7b01d85e
commit 8cb165e1a4
16 changed files with 245 additions and 81 deletions

19
TODO
View file

@ -1,9 +1,13 @@
* Ensure that tweens and particle emitters get destroyed once animation is done
* Stop processing the log during a ship move animation, or a weapon animation
* Highlight ships that will be included as target of current action
* Fix action tooltip sometimes not being hidden when the mouse goes out of action icon
* Quick loading does not cancel pending "setTimeout"s.
* Drones: add tooltip
* Drones: add hull points and take area damage
* Drones: change the sprite angle for deploy animation
* Drones: add animation for each activation
* Drones: fix not being removed when owner is in statis (owner's turn is skipped)
* Do not apply effects on ships in stasis
* Add a battle log display
* Organize arena objects and information in layers
* Prevent arena effects information (eg. "shield -36") to overflow out of the arena
@ -23,13 +27,14 @@
* Mobile: targetting in two times, using a draggable target indicator
* AI: apply safety distances to move actions
* AI: bully AI crashes when winning a battle (trying to move toward null ship!)
* AI: sometimes faces a cardinal point, then is stuck in an infinite thinking loop
* Add a defeat screen (game over for now)
* Add a victory screen, with loot display
* Add retreat from battle
* Map : restore fog of war
* Map : add information on current star/location + information on hovered location
* Map : add stores and shipyards
* Map : remove jump links that cross the radius of other systems
* Map : improve performance
* Menu : fix background stars aggregating at right side when the game is not focused
* Map: restore fog of war
* Map: add information on current star/location + information on hovered location
* Map: add stores and shipyards
* Map: remove jump links that cross the radius of other systems
* Map: improve performance
* Menu: fix background stars aggregating at right side when the game is not focused
* Multiplayer

View file

@ -248,7 +248,9 @@ module TS.SpaceTac {
battle.drones = [drone];
battle.log.events = [];
battle.injectInitialEvents();
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone)]);
let expected = new DroneDeployedEvent(drone);
expected.initial = true;
expect(battle.log.events).toEqual([expected]);
});
});
}

View file

@ -234,18 +234,24 @@ module TS.SpaceTac {
var log = this.log;
// Simulate initial ship placement
this.play_order.forEach((ship: Ship) => {
log.add(new MoveEvent(ship, ship.arena_x, ship.arena_y));
this.play_order.forEach(ship => {
let event = new MoveEvent(ship, ship.arena_x, ship.arena_y);
event.initial = true;
log.add(event);
});
// Simulate drones deployment
this.drones.forEach(drone => {
log.add(new DroneDeployedEvent(drone));
let event = new DroneDeployedEvent(drone);
event.initial = true;
log.add(event);
});
// Simulate game turn
if (this.playing_ship) {
log.add(new ShipChangeEvent(this.playing_ship, this.playing_ship));
let event = new ShipChangeEvent(this.playing_ship, this.playing_ship);
event.initial = true;
log.add(event);
}
}
@ -271,10 +277,10 @@ module TS.SpaceTac {
*/
addDrone(drone: Drone, log = true) {
if (add(this.drones, drone)) {
drone.onDeploy(this.play_order);
if (log) {
this.log.add(new DroneDeployedEvent(drone));
}
drone.onDeploy(this.play_order);
}
}

View file

@ -355,20 +355,35 @@ module TS.SpaceTac {
return distance <= radius;
}
/**
* Rotate the ship in place to face a direction
*/
rotate(angle: number, log = true) {
if (angle != this.arena_angle) {
this.setArenaFacingAngle(angle);
if (log) {
this.addBattleEvent(new MoveEvent(this, this.arena_x, this.arena_y));
}
}
}
// Move toward a location
// This does not check or consume action points
moveTo(x: number, y: number, log: boolean = true): void {
var angle = Math.atan2(y - this.arena_y, x - this.arena_x);
this.setArenaFacingAngle(angle);
if (x != this.arena_x || y != this.arena_y) {
var angle = Math.atan2(y - this.arena_y, x - this.arena_x);
this.setArenaFacingAngle(angle);
this.setArenaPosition(x, y);
this.setArenaPosition(x, y);
if (log) {
this.addBattleEvent(new MoveEvent(this, x, y));
if (log) {
this.addBattleEvent(new MoveEvent(this, x, y));
}
// Broadcast to drones
this.forEachDrone(drone => drone.onShipMove(this));
}
// Broadcast to drones
this.forEachDrone(drone => drone.onShipMove(this));
}
// Set the death status on this ship

View file

@ -39,6 +39,9 @@ module TS.SpaceTac {
var affected: Ship[] = [];
var blast = this.getBlastRadius(ship);
// Face the target
ship.rotate(Target.newFromShip(ship).getAngleTo(target));
// Collect affected ships
if (blast) {
affected = affected.concat(battle.collectShipsInCircle(target, blast));

View file

@ -8,10 +8,10 @@ module TS.SpaceTac.Specs {
TestTools.setShipAP(ship, 100);
TestTools.setShipHP(ship, 50, 30);
var enemy1 = battle.fleets[1].ships[0];
enemy1.setArenaPosition(0, 1);
enemy1.setArenaPosition(1, 0);
TestTools.setShipHP(enemy1, 50, 30);
var enemy2 = battle.fleets[1].ships[1];
enemy2.setArenaPosition(0, 2);
enemy2.setArenaPosition(2, 0);
TestTools.setShipHP(enemy2, 50, 30);
var template = new Equipments.SubMunitionMissile();
@ -47,7 +47,7 @@ module TS.SpaceTac.Specs {
battle.log.clear();
// Fire in space
t = Target.newFromLocation(0, 2.4);
t = Target.newFromLocation(2.4, 0);
expect(equipment.action.canBeUsed(battle, ship)).toBe(true);
equipment.action.apply(battle, ship, t);
checkHP(50, 10, 40, 0, 40, 0);
@ -59,7 +59,7 @@ module TS.SpaceTac.Specs {
battle.log.clear();
// Fire far away
t = Target.newFromLocation(0, 5);
t = Target.newFromLocation(5, 0);
expect(equipment.action.canBeUsed(battle, ship)).toBe(true);
equipment.action.apply(battle, ship, t);
checkHP(50, 10, 40, 0, 40, 0);

View file

@ -10,6 +10,9 @@ module TS.SpaceTac {
// Target of the event
target: Target;
// Boolean at true if the event is used to set initial battle conditions
initial = false;
constructor(code: string, ship: Ship = null, target: Target = null) {
this.code = code;
this.ship = ship;

View file

@ -98,7 +98,7 @@ module TS.SpaceTac.UI {
var sprite = this.findShipSprite(ship);
if (sprite) {
sprite.alpha = 0.5;
sprite.displayEffect("Emergency Stasis", false);
sprite.displayEffect("stasis", false);
}
}
@ -139,8 +139,12 @@ module TS.SpaceTac.UI {
this.battleview.gameui.audio.playOnce("battle-ship-change");
}
// Spawn a new drone
addDrone(drone: Drone): void {
/**
* Spawn a new drone
*
* Return the duration of deploy animation
*/
addDrone(drone: Drone): number {
if (!any(this.drone_sprites, sprite => sprite.drone == drone)) {
let sprite = new ArenaDrone(this.battleview, drone);
this.addChild(sprite);
@ -148,8 +152,12 @@ module TS.SpaceTac.UI {
sprite.position.set(drone.owner.arena_x, drone.owner.arena_y);
this.game.tweens.create(sprite.position).to({ x: drone.x, y: drone.y }, 1800, Phaser.Easing.Sinusoidal.InOut, true, 200);
this.game.tweens.create(sprite.radius.scale).from({ x: 0.01, y: 0.01 }, 1800, Phaser.Easing.Linear.None, true, 200);
return 2000;
} else {
console.error("Drone added twice to arena", drone);
return 0;
}
}

View file

@ -19,6 +19,10 @@ module TS.SpaceTac.UI {
// Effects display
effects: Phaser.Group;
// Previous position
private prevx;
private prevy;
// Create a ship sprite usable in the Arena
constructor(parent: Arena, ship: Ship) {
super(parent.game);
@ -52,9 +56,19 @@ module TS.SpaceTac.UI {
Tools.setHoverClick(this.sprite, () => battleview.cursorOnShip(ship), () => battleview.cursorOffShip(ship), () => battleview.cursorClicked());
// Set location
this.prevx = ship.arena_x;
this.prevy = ship.arena_y;
this.position.set(ship.arena_x, ship.arena_y);
}
update() {
if (this.prevx != this.x || this.prevy != this.y) {
this.sprite.rotation = Math.atan2(this.y - this.prevy, this.x - this.prevx);
}
this.prevx = this.x;
this.prevy = this.y;
}
// Set the hovered state on this ship
// This will toggle the hover effect
setHovered(hovered: boolean) {
@ -67,25 +81,36 @@ module TS.SpaceTac.UI {
this.frame.loadTexture(`battle-arena-ship-${playing ? "playing" : "normal"}-${this.enemy ? "enemy" : "own"}`);
}
// Move the sprite to a location
moveTo(x: number, y: number, facing_angle: number, animate: boolean = true, on_complete: Function | null = null) {
/**
* Move the sprite to a location
*
* Return the duration of animation
*/
moveTo(x: number, y: number, facing_angle: number, animate = true): number {
if (animate) {
var tween_group = this.game.tweens.create(this);
var tween_sprite = this.game.tweens.create(this.sprite);
tween_group.to({ x: x, y: y });
tween_group.start();
Tools.rotationTween(tween_sprite, facing_angle);
if (on_complete) {
tween_sprite.onComplete.addOnce(on_complete);
if (x == this.x && y == this.y) {
let tween = this.game.tweens.create(this.sprite);
let duration = Animation.rotationTween(tween, facing_angle, 0.3);
tween.start();
return duration;
} else {
let distance = Target.newFromLocation(this.x, this.y).getDistanceTo(Target.newFromLocation(x, y));
var tween = this.game.tweens.create(this);
let duration = Math.sqrt(distance / 1000) * 3000;
let curve_force = distance * 0.4;
tween.to({
x: [this.x + Math.cos(this.sprite.rotation) * curve_force, x - Math.cos(facing_angle) * curve_force, x],
y: [this.y + Math.sin(this.sprite.rotation) * curve_force, y - Math.sin(facing_angle) * curve_force, y]
}, duration, Phaser.Easing.Sinusoidal.InOut);
tween.interpolation((v, k) => Phaser.Math.bezierInterpolation(v, k));
tween.start();
return duration;
}
tween_sprite.start();
} else {
this.x = x;
this.y = y;
this.sprite.rotation = facing_angle;
if (on_complete) {
on_complete();
}
return 0;
}
}

View file

@ -161,13 +161,15 @@ module TS.SpaceTac.UI {
this.ship_hovered = ship;
this.arena.setShipHovered(ship);
this.ship_list.setHovered(ship);
this.ship_tooltip.setShip(ship);
if (this.targetting) {
if (ship) {
this.targetting.setTargetShip(ship);
} else {
this.targetting.unsetTarget();
}
this.ship_tooltip.setShip(null);
} else {
this.ship_tooltip.setShip(ship);
}
}

View file

@ -14,20 +14,51 @@ module TS.SpaceTac.UI {
// Subscription identifier
private subscription: any;
// Create a log processor, linked to a battleview
// Delay before processing next events
private delayed = false;
// Processing queue, when delay is active
private queue: BaseLogEvent[] = [];
constructor(view: BattleView) {
this.view = view;
this.battle = view.battle;
this.log = view.battle.log;
this.subscription = this.log.subscribe((event: BaseLogEvent) => {
this.processBattleEvent(event);
});
this.subscription = this.log.subscribe(event => this.processBattleEvent(event));
this.battle.injectInitialEvents();
}
// Process a BaseLogEvent
/**
* Introduce a delay in event processing
*/
delayNextEvents(duration: number) {
if (duration > 0 && !this.view.gameui.headless) {
this.delayed = true;
setTimeout(() => this.processQueued(), duration);
}
}
/**
* Process the events queued due to a delay
*/
processQueued() {
let events = acopy(this.queue);
this.queue = [];
this.delayed = false;
events.forEach(event => this.processBattleEvent(event));
}
/**
* Process a single event
*/
processBattleEvent(event: BaseLogEvent) {
if (this.delayed) {
this.queue.push(event);
return;
}
console.log("Battle event", event);
if (event instanceof ShipChangeEvent) {
@ -82,7 +113,8 @@ module TS.SpaceTac.UI {
private processMoveEvent(event: MoveEvent): void {
var sprite = this.view.arena.findShipSprite(event.ship);
if (sprite) {
sprite.moveTo(event.target.x, event.target.y, event.facing_angle, true);
let duration = sprite.moveTo(event.target.x, event.target.y, event.facing_angle, !event.initial);
this.delayNextEvents(duration);
}
}
@ -113,13 +145,10 @@ module TS.SpaceTac.UI {
var source = Target.newFromShip(event.ship);
var destination = event.target;
// Face the target
var attacker = this.view.arena.findShipSprite(event.ship);
var angle = source.getAngleTo(destination);
attacker.moveTo(source.x, source.y, angle, true);
var effect = new WeaponEffect(this.view.arena, source, destination, event.weapon.code);
effect.start();
let duration = effect.start();
this.delayNextEvents(duration);
}
// Battle ended (victory or defeat)
@ -146,7 +175,8 @@ module TS.SpaceTac.UI {
// New drone deployed
private processDroneDeployedEvent(event: DroneDeployedEvent): void {
this.view.arena.addDrone(event.drone);
let duration = this.view.arena.addDrone(event.drone);
this.delayNextEvents(duration);
}
// Drone destroyed

View file

@ -21,6 +21,7 @@ module TS.SpaceTac.UI.Specs {
let effect = new WeaponEffect(battleview.arena, new Target(10, 0), Target.newFromShip(ship), "test");
let mock_shield_impact = spyOn(effect, "shieldImpactEffect").and.stub();
let mock_hull_impact = spyOn(effect, "hullImpactEffect").and.stub();
effect.gunEffect();
@ -29,11 +30,14 @@ module TS.SpaceTac.UI.Specs {
expect(layer.children[0] instanceof Phaser.Particles.Arcade.Emitter).toBe(true);
expect(mock_shield_impact).toHaveBeenCalledTimes(0);
expect(mock_hull_impact).toHaveBeenCalledTimes(1);
expect(mock_hull_impact).toHaveBeenCalledWith(jasmine.objectContaining({ x: 10, y: 0 }), jasmine.objectContaining({ x: 50, y: 30 }), 100, 800);
TestTools.setShipHP(ship, 10, 10);
effect.gunEffect();
expect(mock_shield_impact).toHaveBeenCalledTimes(1);
expect(mock_shield_impact).toHaveBeenCalledWith(jasmine.objectContaining({ x: 10, y: 0 }), jasmine.objectContaining({ x: 50, y: 30 }), 100, 800);
expect(mock_hull_impact).toHaveBeenCalledTimes(1);
});
});
}

View file

@ -43,10 +43,16 @@ module TS.SpaceTac.UI {
this.effect = this.getEffectForWeapon(weapon);
}
// Start the visual effect
start(): void {
/**
* Start the visual effect
*
* Returns the duration of the effect.
*/
start(): number {
if (this.effect) {
this.effect.call(this);
return this.effect.call(this);
} else {
return 0;
}
}
@ -87,14 +93,33 @@ module TS.SpaceTac.UI {
emitter.setRotation(0, 0);
emitter.setXSpeed(-Math.cos(angle) * 20, -Math.cos(angle) * 80);
emitter.setYSpeed(-Math.sin(angle) * 20, -Math.sin(angle) * 80);
emitter.start(false, 200, 30, duration / 30);
emitter.start(false, 200, 30, duration * 0.8 / 30);
this.layer.addChild(emitter);
}
/**
* Add a hull impact effect on a ship
*/
hullImpactEffect(from: Point, ship: Point, delay: number, duration: number) {
let angle = Math.atan2(from.y - ship.y, from.x - ship.x);
let emitter = this.ui.add.emitter(ship.x + Math.cos(angle) * 10, ship.y + Math.sin(angle) * 10, 30);
emitter.minParticleScale = 1.0;
emitter.maxParticleScale = 2.0;
emitter.gravity = 0;
emitter.makeParticles("battle-weapon-hot");
emitter.setSize(15, 15);
emitter.setRotation(0, 0);
emitter.setXSpeed(-Math.cos(angle) * 120, -Math.cos(angle) * 260);
emitter.setYSpeed(-Math.sin(angle) * 120, -Math.sin(angle) * 260);
emitter.start(false, 200, 30, duration * 0.8 / 30);
this.layer.addChild(emitter);
}
/**
* Default firing effect
*/
defaultEffect(): void {
defaultEffect(): number {
var missile = new Phaser.Sprite(this.ui, this.source.x, this.source.y, "battle-weapon-default");
missile.anchor.set(0.5, 0.5);
missile.rotation = this.source.getAngleTo(this.destination);
@ -106,12 +131,14 @@ module TS.SpaceTac.UI {
missile.destroy();
});
tween.start();
return 1000;
}
/**
* Submachine gun effect (quick chain of small bullets)
*/
gunEffect(): void {
gunEffect(): number {
this.ui.audio.playOnce("battle-weapon-bullets");
let has_shield = this.destination.ship && this.destination.ship.getValue("shield") > 0;
@ -129,12 +156,20 @@ module TS.SpaceTac.UI {
emitter.setXSpeed(Math.cos(angle) * speed, Math.cos(angle) * speed);
emitter.setYSpeed(Math.sin(angle) * speed, Math.sin(angle) * speed);
emitter.makeParticles(["battle-weapon-bullets"]);
emitter.start(false, 1000 * (distance - 50 - (has_shield ? 80 : 40)) / speed, 50, 10);
let guard = 50 + (has_shield ? 80 : 40);
if (guard + 1 > distance) {
guard = distance - 1;
}
emitter.start(false, 1000 * (distance - guard) / speed, 50, 10);
this.layer.addChild(emitter);
if (has_shield) {
this.shieldImpactEffect(this.source, this.destination, 100, 800);
} else {
this.hullImpactEffect(this.source, this.destination, 100, 800);
}
return 1000;
}
}
}

View file

@ -0,0 +1,15 @@
module TS.SpaceTac.UI.Specs {
describe("Animation", () => {
ingame_it("animates rotation", function (game) {
let obj = { rotation: -Math.PI * 2.5 };
let tween = game.tweens.create(obj);
let result = Animation.rotationTween(tween, Math.PI * 0.25, 1, Phaser.Easing.Linear.None);
expect(result).toEqual(750);
expect(tween.generateData(4)).toEqual([
{ rotation: -Math.PI * 0.25 },
{ rotation: 0 },
{ rotation: Math.PI * 0.25 },
]);
});
});
}

View file

@ -28,5 +28,35 @@ module TS.SpaceTac.UI {
Animation.fadeOut(game, obj, duration);
}
}
/**
* Interpolate a rotation value
*
* This will take into account the 2*pi modulo
*
* Returns the duration
*/
static rotationTween(tween: Phaser.Tween, dest: number, speed = 1, easing = Phaser.Easing.Cubic.InOut, property = "rotation"): number {
// Immediately change the object's current rotation to be in range (-pi,pi)
let value = Tools.normalizeAngle(tween.target[property]);
tween.target[property] = value;
// Compute destination angle
dest = Tools.normalizeAngle(dest);
if (value - dest > Math.PI) {
dest += 2 * Math.PI;
} else if (value - dest < -Math.PI) {
dest -= 2 * Math.PI;
}
let distance = Math.abs(Tools.normalizeAngle(dest - value)) / Math.PI;
let duration = distance * 1000 / speed;
// Update the tween
let changes = {};
changes[property] = dest;
tween.to(changes, duration, easing);
return duration;
}
}
}

View file

@ -64,24 +64,5 @@ module TS.SpaceTac.UI {
return angle;
}
}
// Interpolate a rotation value
// This will take into account the 2*pi modulo
static rotationTween(tween: Phaser.Tween, dest: number, property: string = "rotation"): void {
// Immediately change the object's current rotation to be in range (-pi,pi)
var value = Tools.normalizeAngle(tween.target[property]);
tween.target[property] = value;
// Update the tween
dest = Tools.normalizeAngle(dest);
if (value - dest > Math.PI) {
dest += 2 * Math.PI;
} else if (value - dest < -Math.PI) {
dest -= 2 * Math.PI;
}
var changes: Object = {};
changes[property] = dest;
tween.to(changes);
}
}
}