From daf59cc16dcc0587474804da6ef1b652cd0cb411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 25 Jul 2017 00:02:43 +0200 Subject: [PATCH] multiplayer: WIP on spectator mode for battles --- TODO.md | 8 ++ src/core/Ship.spec.ts | 2 + src/core/actions/BaseAction.ts | 11 ++- src/core/actions/DeployDroneAction.spec.ts | 1 + src/core/actions/MoveAction.spec.ts | 9 +- .../equipments/SubMunitionMissile.spec.ts | 25 ++--- src/core/events/ActionAppliedEvent.ts | 17 ++++ src/multi/Exchange.ts | 2 +- src/ui/battle/BattleView.ts | 19 +--- src/ui/battle/MultiBattle.ts | 96 +++++++++++++++++++ src/ui/options/OptionsDialog.ts | 12 ++- 11 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 src/core/events/ActionAppliedEvent.ts create mode 100644 src/ui/battle/MultiBattle.ts diff --git a/TODO.md b/TODO.md index da60732..93bfd93 100644 --- a/TODO.md +++ b/TODO.md @@ -86,6 +86,14 @@ Common UI * Mobile: display tooltips larger and on the side of screen where the finger is not * Mobile: targetting in two times, using a draggable target indicator +Network +------- + +* Handle cancel button in invitation dialog +* Close connection on view exit +* Add timeouts to read operations +* Display connection status + Postponed --------- diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index eb020a6..282da2a 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -227,9 +227,11 @@ module TS.SpaceTac.Specs { expect(action.activated).toBe(false); expect(battle.log.events).toEqual([ + new ActionAppliedEvent(ship, action, null), new ToggleEvent(ship, action, true), new ActiveEffectsEvent(ship, [], [], [new AttributeEffect("power_capacity", 1)]), new ValueChangeEvent(ship, new ShipAttribute("power capacity", 1), 1), + new ActionAppliedEvent(ship, action, null), new ToggleEvent(ship, action, false), new ActiveEffectsEvent(ship, [], [], []), new ValueChangeEvent(ship, new ShipAttribute("power capacity", 0), -1), diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts index edf5573..578ec6d 100644 --- a/src/core/actions/BaseAction.ts +++ b/src/core/actions/BaseAction.ts @@ -1,5 +1,9 @@ module TS.SpaceTac { - // Base class for action definitions + /** + * Base class for a battle action. + * + * An action should be the only way to modify a battle state. + */ export class BaseAction { // Identifier code for the type of action code: string @@ -140,6 +144,11 @@ module TS.SpaceTac { this.equipment.cooldown.use(); } + let battle = ship.getBattle(); + if (battle) { + battle.log.add(new ActionAppliedEvent(ship, this, checked_target)); + } + this.customApply(ship, checked_target); return true; } else { diff --git a/src/core/actions/DeployDroneAction.spec.ts b/src/core/actions/DeployDroneAction.spec.ts index c268034..a179918 100644 --- a/src/core/actions/DeployDroneAction.spec.ts +++ b/src/core/actions/DeployDroneAction.spec.ts @@ -50,6 +50,7 @@ module TS.SpaceTac { expect(drone.radius).toEqual(4); expect(drone.effects).toEqual([new DamageEffect(50)]); expect(battle.log.events).toEqual([ + new ActionAppliedEvent(ship, action, Target.newFromLocation(5, 0)), new DroneDeployedEvent(drone) ]); diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index c1b7610..afec4bc 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -61,16 +61,19 @@ module TS.SpaceTac { expect(ship.arena_y).toBeCloseTo(3.535533, 0.00001); expect(ship.values.power.get()).toEqual(0); - expect(battle.log.events.length).toBe(2); + expect(battle.log.events.length).toBe(3); expect(battle.log.events[0].code).toEqual("value"); expect(battle.log.events[0].ship).toBe(ship); expect((battle.log.events[0]).value).toEqual( new ShipValue("power", 0, 20)); - expect(battle.log.events[1].code).toEqual("move"); + expect(battle.log.events[1].code).toEqual("action"); expect(battle.log.events[1].ship).toBe(ship); - let dest = (battle.log.events[1]).end; + + expect(battle.log.events[2].code).toEqual("move"); + expect(battle.log.events[2].ship).toBe(ship); + let dest = (battle.log.events[2]).end; expect(dest.x).toBeCloseTo(3.535533, 0.00001); expect(dest.y).toBeCloseTo(3.535533, 0.00001); }); diff --git a/src/core/equipments/SubMunitionMissile.spec.ts b/src/core/equipments/SubMunitionMissile.spec.ts index c7735f9..a6c5026 100644 --- a/src/core/equipments/SubMunitionMissile.spec.ts +++ b/src/core/equipments/SubMunitionMissile.spec.ts @@ -60,11 +60,12 @@ module TS.SpaceTac.Equipments { expect(equipment.action.checkCannotBeApplied(ship)).toBe(null); equipment.action.apply(ship, target); checkHP(50, 10, 50, 10, 50, 10); - expect(battle.log.events.length).toBe(4); - expect(battle.log.events[0]).toEqual(new FireEvent(ship, equipment, Target.newFromLocation(1, 0))); - expect(battle.log.events[1]).toEqual(new DamageEvent(ship, 0, 20)); - expect(battle.log.events[2]).toEqual(new DamageEvent(enemy1, 0, 20)); - expect(battle.log.events[3]).toEqual(new DamageEvent(enemy2, 0, 20)); + expect(battle.log.events.length).toBe(5); + expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, equipment.action, Target.newFromLocation(1, 0))); + expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, Target.newFromLocation(1, 0))); + expect(battle.log.events[2]).toEqual(new DamageEvent(ship, 0, 20)); + expect(battle.log.events[3]).toEqual(new DamageEvent(enemy1, 0, 20)); + expect(battle.log.events[4]).toEqual(new DamageEvent(enemy2, 0, 20)); battle.log.clear(); equipment.cooldown.cool(); @@ -74,10 +75,11 @@ module TS.SpaceTac.Equipments { expect(equipment.action.checkCannotBeApplied(ship)).toBe(null); equipment.action.apply(ship, target); checkHP(50, 10, 40, 0, 40, 0); - expect(battle.log.events.length).toBe(3); - expect(battle.log.events[0]).toEqual(new FireEvent(ship, equipment, target)); - expect(battle.log.events[1]).toEqual(new DamageEvent(enemy1, 10, 10)); - expect(battle.log.events[2]).toEqual(new DamageEvent(enemy2, 10, 10)); + expect(battle.log.events.length).toBe(4); + expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, equipment.action, target)); + expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, target)); + expect(battle.log.events[2]).toEqual(new DamageEvent(enemy1, 10, 10)); + expect(battle.log.events[3]).toEqual(new DamageEvent(enemy2, 10, 10)); battle.log.clear(); equipment.cooldown.cool(); @@ -87,8 +89,9 @@ module TS.SpaceTac.Equipments { expect(equipment.action.checkCannotBeApplied(ship)).toBe(null); equipment.action.apply(ship, target); checkHP(50, 10, 40, 0, 40, 0); - expect(battle.log.events.length).toBe(1); - expect(battle.log.events[0]).toEqual(new FireEvent(ship, equipment, target)); + expect(battle.log.events.length).toBe(2); + expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, equipment.action, target)); + expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, target)); }); }); } diff --git a/src/core/events/ActionAppliedEvent.ts b/src/core/events/ActionAppliedEvent.ts new file mode 100644 index 0000000..93e74fe --- /dev/null +++ b/src/core/events/ActionAppliedEvent.ts @@ -0,0 +1,17 @@ +/// + +module TS.SpaceTac { + /** + * Event logged when an action is used. + */ + export class ActionAppliedEvent extends BaseLogShipEvent { + // Action applied + action: BaseAction + + constructor(ship: Ship, action: BaseAction, target: Target | null) { + super("action", ship, target); + + this.action = action; + } + } +} diff --git a/src/multi/Exchange.ts b/src/multi/Exchange.ts index 9439879..6d5c101 100644 --- a/src/multi/Exchange.ts +++ b/src/multi/Exchange.ts @@ -39,7 +39,7 @@ module TS.SpaceTac.Multi { await this.writeMessage(null, true); } - console.log("Echange established", this.token, this.localpeer, this.remotepeer); + console.log("Exchange established", this.token, this.localpeer, this.remotepeer); } /** diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index c4c1929..96f8d16 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -9,8 +9,8 @@ module TS.SpaceTac.UI { // Interacting player player: Player - // Exchange (for remote session only) - exchange: Multi.Exchange | null + // Multiplayer sharing + multi: MultiBattle // Layers layer_background: Phaser.Group @@ -64,7 +64,7 @@ module TS.SpaceTac.UI { this.battle = battle; this.ship_hovered = null; this.background = null; - this.exchange = null; + this.multi = new MultiBattle(); this.battle.timer = this.timer; @@ -141,7 +141,8 @@ module TS.SpaceTac.UI { // If we are on a remote session, start the exchange if (!this.session.primary && this.gameui.session_token) { - this.setupRemoteSession(this.gameui.session_token); + // TODO handle errors or timeout + this.multi.setup(this, this.battle, this.gameui.session_token, false); } } @@ -285,15 +286,5 @@ module TS.SpaceTac.UI { this.player.revertBattle(); this.game.state.start('router'); } - - /** - * Setup exchange for remote session - */ - async setupRemoteSession(token: string) { - this.exchange = new Multi.Exchange(this.getConnection(), token); - await this.exchange.start(); - - // TODO read actions and apply them - } } } diff --git a/src/ui/battle/MultiBattle.ts b/src/ui/battle/MultiBattle.ts new file mode 100644 index 0000000..4dea16f --- /dev/null +++ b/src/ui/battle/MultiBattle.ts @@ -0,0 +1,96 @@ +module TS.SpaceTac.UI { + /** + * Tool to synchronize two players sharing a battle over network + */ + export class MultiBattle { + // Network exchange of messages + exchange: Multi.Exchange + + // True if this peer is the primary one (the one that invited the other) + primary: boolean + + // Battle being played + battle: Battle + + // Count of battle log events that were processed + processed: number + + // Serializer to use for actions + serializer: Serializer + + // Timer for scheduling + timer: Timer + + /** + * Setup the session other a token + */ + async setup(view: BaseView, battle: Battle, token: string, primary: boolean) { + if (this.exchange) { + // TODO close it + } + + this.battle = battle; + this.primary = primary; + + this.exchange = new Multi.Exchange(view.getConnection(), token, primary); + await this.exchange.start(); + + this.serializer = new Serializer(TS.SpaceTac); + this.processed = this.battle.log.events.length; + this.timer = view.timer; + + // This is voluntarily not waited on, as it is a background task + this.backgroundSync(); + } + + /** + * Background work to maintain the battle state in sync between the two peers + */ + async backgroundSync() { + while (true) { + if (this.exchange.writing) { + await this.sendActions(); + } else { + await this.receiveAction(); + } + } + } + + /** + * Send all new actions from the battle log + */ + async sendActions() { + let events = this.battle.log.events; + + if (this.processed >= events.length) { + await this.timer.sleep(500); + } else { + while (this.processed < events.length) { + let event = events[this.processed]; + this.processed++; + + if (event instanceof ActionAppliedEvent) { + let data = this.serializer.serialize(event); + // TODO "over" should be true if the current ship should be played by remote player + await this.exchange.writeMessage(data, false); + } + } + } + } + + /** + * Read and apply one action from the peer + */ + async receiveAction() { + let message = await this.exchange.readMessage(); + let received = this.serializer.unserialize(message); + if (received instanceof ActionAppliedEvent) { + console.log("Received action from exchange", received); + // TODO Find the matching action, ship and target, and apply + } else { + console.error("Exchange received something that is not an action event", received); + } + this.processed = this.battle.log.events.length; + } + } +} diff --git a/src/ui/options/OptionsDialog.ts b/src/ui/options/OptionsDialog.ts index 372ed2d..4141d13 100644 --- a/src/ui/options/OptionsDialog.ts +++ b/src/ui/options/OptionsDialog.ts @@ -60,12 +60,14 @@ module TS.SpaceTac.UI { let conn = this.view.getConnection(); try { let token = await conn.publish(this.view.session, "Multiplayer invitation"); - this.displayMultiplayerToken(token); // TODO On cancel + this.displayMultiplayerToken(token); - let exchange = new Multi.Exchange(this.view.getConnection(), token, true); - await exchange.start(); - - // TODO Setup the exchange on current view + if (this.view instanceof BattleView) { + await this.view.multi.setup(this.view, this.view.battle, token, true); + } else { + // TODO + this.displayConnectionError(); + } this.close(); } catch (err) {