diff --git a/TODO b/TODO index ef42360..4ffbd8d 100644 --- a/TODO +++ b/TODO @@ -4,16 +4,14 @@ * 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) +* More sound effects * 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 * Allow to cancel last moves * Identify ships in emergency stasis more clearly -* Add more visual effects to weapons, hits and explosions * Effect should be random in a range (eg. "damage target 50-75") * Add an overload/cooling system * Add auto-move to attack @@ -37,4 +35,6 @@ * 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 +* Missions/quests system +* Main story arc * Multiplayer \ No newline at end of file diff --git a/src/core/Drone.spec.ts b/src/core/Drone.spec.ts index 593b27c..d853017 100644 --- a/src/core/Drone.spec.ts +++ b/src/core/Drone.spec.ts @@ -123,5 +123,22 @@ module TS.SpaceTac { drone.onTurnStart(owner); expect(removeDrone).toHaveBeenCalledWith(drone); }); + + it("logs each activation", function () { + let battle = new Battle(); + let ship = new Ship(); + ship.fleet.setBattle(battle); + let other = new Ship(); + + let drone = new Drone(ship); + drone.apply([ship, other]); + drone.apply([]); + drone.apply([other]); + + expect(battle.log.events).toEqual([ + new DroneAppliedEvent(drone, [ship, other]), + new DroneAppliedEvent(drone, [other]) + ]); + }); }); } diff --git a/src/core/Drone.ts b/src/core/Drone.ts index 19e11e3..60cfc93 100644 --- a/src/core/Drone.ts +++ b/src/core/Drone.ts @@ -32,30 +32,32 @@ module TS.SpaceTac { } /** - * Call a function for each ship in radius. + * Filter the list of ships in radius. */ - forEachInRadius(ships: Ship[], callback: (ship: Ship) => any) { - ships.forEach(ship => { - if (ship.isInCircle(this.x, this.y, this.radius)) { - callback(ship); - } - }); + filterShipsInRadius(ships: Ship[]): Ship[] { + return ships.filter(ship => ship.isInCircle(this.x, this.y, this.radius)); } /** - * Apply the effects on a single ship. + * Apply the effects on a list of ships * - * This does not check if the ship is in range. + * This does not check if the ships are in range. */ - singleApply(ship: Ship) { - this.effects.forEach(effect => effect.applyOnShip(ship)); + apply(ships: Ship[], log = true) { + if (ships.length > 0) { + let battle = this.owner.getBattle(); + if (battle && log) { + battle.log.add(new DroneAppliedEvent(this, ships)); + } + ships.forEach(ship => this.effects.forEach(effect => effect.applyOnShip(ship))); + } } /** * Called when the drone is first deployed. */ onDeploy(ships: Ship[]) { - this.forEachInRadius(ships, ship => this.singleApply(ship)); + this.apply(this.filterShipsInRadius(ships)); } /** @@ -89,7 +91,7 @@ module TS.SpaceTac { */ onTurnEnd(ship: Ship) { if (this.duration > 0 && ship.isInCircle(this.x, this.y, this.radius) && contains(this.inside_at_start, ship)) { - this.singleApply(ship); + this.apply([ship]); } } @@ -99,7 +101,7 @@ module TS.SpaceTac { onShipMove(ship: Ship) { if (this.duration > 0 && ship.isInCircle(this.x, this.y, this.radius)) { if (add(this.inside, ship)) { - this.singleApply(ship); + this.apply([ship]); } } else { remove(this.inside, ship); diff --git a/src/core/equipments/RepairDrone.spec.ts b/src/core/equipments/RepairDrone.spec.ts index e1d76c0..9c001e3 100644 --- a/src/core/equipments/RepairDrone.spec.ts +++ b/src/core/equipments/RepairDrone.spec.ts @@ -18,9 +18,9 @@ module TS.SpaceTac.Equipments { expect(drone.duration).toBe(1); ship.setAttribute("hull_capacity", 100); ship.setValue("hull", 85); - drone.singleApply(ship); + drone.apply([ship]); expect(ship.getValue("hull")).toBe(95); - drone.singleApply(ship); + drone.apply([ship]); expect(ship.getValue("hull")).toBe(100); }); }); diff --git a/src/core/events/DroneAppliedEvent.ts b/src/core/events/DroneAppliedEvent.ts new file mode 100644 index 0000000..82ba163 --- /dev/null +++ b/src/core/events/DroneAppliedEvent.ts @@ -0,0 +1,21 @@ +/// + +module TS.SpaceTac { + /** + * Event logged when a drone applies its effects + */ + export class DroneAppliedEvent extends BaseLogEvent { + // Pointer to the drone + drone: Drone; + + // List of impacted ships + ships: Ship[]; + + constructor(drone: Drone, ships: Ship[]) { + super("droneapply", drone.owner); + + this.drone = drone; + this.ships = ships; + } + } +} diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index 3d0011a..1af728b 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -139,22 +139,38 @@ module TS.SpaceTac.UI { this.battleview.gameui.audio.playOnce("battle-ship-change"); } + /** + * Find an ArenaDrone displaying a Drone. + */ + findDrone(drone: Drone): ArenaDrone | null { + return first(this.drone_sprites, sprite => sprite.drone == drone); + } + /** * Spawn a new drone * * Return the duration of deploy animation */ - addDrone(drone: Drone): number { - if (!any(this.drone_sprites, sprite => sprite.drone == drone)) { + addDrone(drone: Drone, animate = true): number { + if (!this.findDrone(drone)) { let sprite = new ArenaDrone(this.battleview, drone); + let angle = Math.atan2(drone.y - drone.owner.arena_y, drone.x - drone.owner.arena_x); this.addChild(sprite); this.drone_sprites.push(sprite); - 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); + if (animate) { + sprite.position.set(drone.owner.arena_x, drone.owner.arena_y); + sprite.rotation = drone.owner.arena_angle; + let move_duration = Animation.moveInSpace(sprite, drone.x, drone.y, angle); + this.game.tweens.create(sprite.radius).from({ alpha: 0 }, 500, Phaser.Easing.Cubic.In, true, move_duration); + + return move_duration + 500; + } else { + sprite.position.set(drone.x, drone.y); + sprite.rotation = angle; + return 0; + } - return 2000; } else { console.error("Drone added twice to arena", drone); return 0; @@ -163,7 +179,7 @@ module TS.SpaceTac.UI { // Remove a destroyed drone removeDrone(drone: Drone): void { - let sprite = first(this.drone_sprites, sprite => sprite.drone == drone); + let sprite = this.findDrone(drone); if (sprite) { remove(this.drone_sprites, sprite); sprite.destroy(); diff --git a/src/ui/battle/ArenaDrone.ts b/src/ui/battle/ArenaDrone.ts index 51ccf6b..486053a 100644 --- a/src/ui/battle/ArenaDrone.ts +++ b/src/ui/battle/ArenaDrone.ts @@ -7,27 +7,52 @@ module TS.SpaceTac.UI { drone: Drone; // Sprite - sprite: Phaser.Button; + sprite: Phaser.Image; // Radius radius: Phaser.Graphics; + // Activation effect + activation: Phaser.Graphics; + constructor(battleview: BattleView, drone: Drone) { super(battleview.game); this.drone = drone; this.radius = new Phaser.Graphics(this.game, 0, 0); - this.radius.lineStyle(3, 0xe9f2f9, 0.5); + this.radius.lineStyle(2, 0xe9f2f9, 0.3); this.radius.beginFill(0xe9f2f9, 0.0); this.radius.drawCircle(0, 0, drone.radius * 2); this.radius.endFill(); this.addChild(this.radius); - this.sprite = new Phaser.Button(this.game, 0, 0, `battle-actions-deploy-${drone.code}`); + this.activation = new Phaser.Graphics(this.game, 0, 0); + this.activation.lineStyle(2, 0xe9f2f9, 0.7); + this.activation.beginFill(0xe9f2f9, 0.0); + this.activation.drawCircle(0, 0, drone.radius * 2); + this.activation.endFill(); + this.activation.visible = false; + this.addChild(this.activation); + + this.sprite = new Phaser.Image(this.game, 0, 0, `battle-actions-deploy-${drone.code}`); this.sprite.anchor.set(0.5, 0.5); this.sprite.scale.set(0.1, 0.1); this.addChild(this.sprite); } + + /** + * Start the activation animation + * + * Return the animation duration + */ + setApplied(): number { + this.activation.scale.set(0.001, 0.001); + this.activation.visible = true; + let tween = this.game.tweens.create(this.activation.scale).to({ x: 1, y: 1 }, 500); + tween.onComplete.addOnce(() => this.activation.visible = false); + tween.start(); + return 500; + } } } diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index 131e52d..b102c97 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -19,10 +19,6 @@ 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); @@ -56,17 +52,15 @@ 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) { + /*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; + this.prevy = this.y;*/ } // Set the hovered state on this ship @@ -88,24 +82,7 @@ module TS.SpaceTac.UI { */ moveTo(x: number, y: number, facing_angle: number, animate = true): number { if (animate) { - 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; - } + return Animation.moveInSpace(this, x, y, facing_angle, this.sprite); } else { this.x = x; this.y = y; diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index accdd50..21eb828 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -161,15 +161,13 @@ 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); } } diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts index 225ad65..abb6ae0 100644 --- a/src/ui/battle/LogProcessor.ts +++ b/src/ui/battle/LogProcessor.ts @@ -79,6 +79,8 @@ module TS.SpaceTac.UI { this.processDroneDeployedEvent(event); } else if (event instanceof DroneDestroyedEvent) { this.processDroneDestroyedEvent(event); + } else if (event instanceof DroneAppliedEvent) { + this.processDroneAppliedEvent(event); } else if (event.code == "effectadd" || event.code == "effectduration" || event.code == "effectdel") { this.processEffectEvent(event); } @@ -183,5 +185,14 @@ module TS.SpaceTac.UI { private processDroneDestroyedEvent(event: DroneDestroyedEvent): void { this.view.arena.removeDrone(event.drone); } + + // Drone applied + private processDroneAppliedEvent(event: DroneAppliedEvent): void { + let drone = this.view.arena.findDrone(event.drone); + if (drone) { + let duration = drone.setApplied(); + this.delayNextEvents(duration); + } + } } } diff --git a/src/ui/common/Animation.ts b/src/ui/common/Animation.ts index 3f72555..905c0ce 100644 --- a/src/ui/common/Animation.ts +++ b/src/ui/common/Animation.ts @@ -1,4 +1,11 @@ module TS.SpaceTac.UI { + interface PhaserGraphics { + x: number; + y: number; + rotation: number; + game: Phaser.Game; + }; + /** * Utility functions for animation */ @@ -60,5 +67,40 @@ module TS.SpaceTac.UI { return duration; } + + /** + * Make an object move toward a location in space, with a ship-like animation. + * + * Returns the animation duration. + */ + static moveInSpace(obj: PhaserGraphics, x: number, y: number, angle: number, rotated_obj = obj): number { + if (x == obj.x && y == obj.y) { + let tween = obj.game.tweens.create(rotated_obj); + let duration = Animation.rotationTween(tween, angle, 0.3); + tween.start(); + return duration; + } else { + let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y)); + var tween = obj.game.tweens.create(obj); + let duration = Math.sqrt(distance / 1000) * 3000; + let curve_force = distance * 0.4; + tween.to({ + x: [obj.x + Math.cos(rotated_obj.rotation) * curve_force, x - Math.cos(angle) * curve_force, x], + y: [obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y] + }, duration, Phaser.Easing.Sinusoidal.InOut); + tween.interpolation((v, k) => Phaser.Math.bezierInterpolation(v, k)); + let prevx = obj.x; + let prevy = obj.y; + tween.onUpdateCallback(() => { + if (prevx != obj.x || prevy != obj.y) { + rotated_obj.rotation = Math.atan2(obj.y - prevy, obj.x - prevx); + } + prevx = obj.x; + prevy = obj.y; + }); + tween.start(); + return duration; + } + } } }