From c717b153ce4107160ca1d4a513ab3ae38c8bbe43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Fri, 26 May 2017 01:09:29 +0200 Subject: [PATCH] Added battle stats --- README.md | 2 +- TODO | 3 +- src/core/Battle.ts | 29 +++++++++------ src/core/BattleStats.spec.ts | 68 ++++++++++++++++++++++++++++++++++ src/core/BattleStats.ts | 52 ++++++++++++++++++++++++++ src/core/Ship.spec.ts | 7 ++++ src/core/Ship.ts | 10 +++-- src/core/events/MoveEvent.ts | 10 +++-- src/ui/battle/BattleView.ts | 5 +-- src/ui/battle/LogProcessor.ts | 1 + src/ui/battle/OutcomeDialog.ts | 56 +++++++++++++--------------- src/ui/common/UIComponent.ts | 38 ++++++++++++++++++- src/ui/menu/LoadDialog.ts | 4 +- 13 files changed, 228 insertions(+), 57 deletions(-) create mode 100644 src/core/BattleStats.spec.ts create mode 100644 src/core/BattleStats.ts diff --git a/README.md b/README.md index fa2fbd6..ce0756e 100644 --- a/README.md +++ b/README.md @@ -148,4 +148,4 @@ except for area damage and area effects specifically designed for drones. * 1,2,3...0 - Select action * Space - End current ship's turn -* T - Tactical mode for 5 seconds +* T - Tactical mode for 3 seconds diff --git a/TODO b/TODO index 08344b0..691331f 100644 --- a/TODO +++ b/TODO @@ -28,7 +28,6 @@ * Add actions with cost dependent of distance (like current move actions) * Keep move and drone actions out of arena borders * Find incentives to move from starting position -* Outcome: add battle statistics and/or honors * Outcome: disable the loot button if there is no loot * Ensure that tweens and particle emitters get destroyed once animation is done (or view changes) * Controls: do not focus on ship while targetting for area effects (dissociate hover and target) @@ -45,6 +44,8 @@ * Mobile: targetting in two times, using a draggable target indicator * AI: use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers * AI: apply safety distances to move actions +* AI: evaluate buffs/debuffs +* AI: abandon fight * AI: add combination of random small move and actual maneuver, as producer * AI: new duel page with producers/evaluators tweaking * AI: work in a dedicated process diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 2fed33e..dcbfe88 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -2,40 +2,42 @@ module TS.SpaceTac { // A turn-based battle between fleets export class Battle { // Flag indicating if the battle is ended - ended: boolean; + ended: boolean // Battle outcome, if *ended* is true - outcome: BattleOutcome; + outcome: BattleOutcome + + // Statistics + stats: BattleStats // Log of all battle events - log: BattleLog; + log: BattleLog // List of fleets engaged in battle - fleets: Fleet[]; + fleets: Fleet[] // List of ships, sorted by their initiative throw - play_order: Ship[]; + play_order: Ship[] // Current turn - turn: number; + turn: number // Current ship whose turn it is to play - playing_ship_index: number | null; - playing_ship: Ship | null; + playing_ship_index: number | null + playing_ship: Ship | null // List of deployed drones - drones: Drone[] = []; + drones: Drone[] = [] // Size of the battle area width: number height: number // Timer to use for scheduled things - timer = Timer.global; + timer = Timer.global // Create a battle between two fleets constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1808, height = 948) { - this.log = new BattleLog(); this.fleets = [fleet1, fleet2]; this.play_order = []; this.playing_ship_index = null; @@ -44,6 +46,9 @@ module TS.SpaceTac { this.width = width; this.height = height; + this.log = new BattleLog(); + this.stats = new BattleStats(); + this.fleets.forEach((fleet: Fleet) => { fleet.setBattle(this); }); @@ -272,7 +277,7 @@ module TS.SpaceTac { // Simulate initial ship placement this.play_order.forEach(ship => { - let event = new MoveEvent(ship, ship.arena_x, ship.arena_y); + let event = new MoveEvent(ship, ship.arena_x, ship.arena_y, 0); event.initial = true; log.add(event); }); diff --git a/src/core/BattleStats.spec.ts b/src/core/BattleStats.spec.ts new file mode 100644 index 0000000..d49fd09 --- /dev/null +++ b/src/core/BattleStats.spec.ts @@ -0,0 +1,68 @@ +module TS.SpaceTac.Specs { + describe("BattleStats", function () { + it("collects stats", function () { + let stats = new BattleStats(); + expect(stats.stats).toEqual({}); + + stats.addStat("Test", 1, true); + expect(stats.stats).toEqual({ Test: [1, 0] }); + + stats.addStat("Test", 1, true); + expect(stats.stats).toEqual({ Test: [2, 0] }); + + stats.addStat("Test", 1, false); + expect(stats.stats).toEqual({ Test: [2, 1] }); + + stats.addStat("Other Test", 10, true); + expect(stats.stats).toEqual({ Test: [2, 1], "Other Test": [10, 0] }); + }) + + it("collects damage dealt", function () { + let stats = new BattleStats(); + let battle = new Battle(); + let attacker = battle.fleets[0].addShip(); + let defender = battle.fleets[1].addShip(); + stats.watchLog(battle.log, battle.fleets[0]); + expect(stats.stats).toEqual({}); + + battle.log.add(new DamageEvent(attacker, 10, 12)); + expect(stats.stats).toEqual({ "Damage dealt": [0, 22] }); + + battle.log.add(new DamageEvent(defender, 40, 0)); + expect(stats.stats).toEqual({ "Damage dealt": [40, 22] }); + + battle.log.add(new DamageEvent(attacker, 5, 4)); + expect(stats.stats).toEqual({ "Damage dealt": [40, 31] }); + }) + + it("collects distance moved", function () { + let stats = new BattleStats(); + let battle = new Battle(); + let attacker = battle.fleets[0].addShip(); + let defender = battle.fleets[1].addShip(); + stats.watchLog(battle.log, battle.fleets[0]); + expect(stats.stats).toEqual({}); + + battle.log.add(new MoveEvent(attacker, 0, 0, 10)); + expect(stats.stats).toEqual({ "Move distance (km)": [10, 0] }); + + battle.log.add(new MoveEvent(defender, 0, 0, 58)); + expect(stats.stats).toEqual({ "Move distance (km)": [10, 58] }); + }) + + it("collects deployed drones", function () { + let stats = new BattleStats(); + let battle = new Battle(); + let attacker = battle.fleets[0].addShip(); + let defender = battle.fleets[1].addShip(); + stats.watchLog(battle.log, battle.fleets[0]); + expect(stats.stats).toEqual({}); + + battle.log.add(new DroneDeployedEvent(new Drone(attacker))); + expect(stats.stats).toEqual({ "Drones deployed": [1, 0] }); + + battle.log.add(new DroneDeployedEvent(new Drone(defender))); + expect(stats.stats).toEqual({ "Drones deployed": [1, 1] }); + }) + }) +} diff --git a/src/core/BattleStats.ts b/src/core/BattleStats.ts new file mode 100644 index 0000000..998943a --- /dev/null +++ b/src/core/BattleStats.ts @@ -0,0 +1,52 @@ +module TS.SpaceTac { + /** + * Statistics collection over a battle + */ + export class BattleStats { + stats: { [name: string]: [number, number] } = {} + + /** + * Add a value to the collector + */ + addStat(name: string, value: number, attacker: boolean) { + if (!this.stats[name]) { + this.stats[name] = [0, 0]; + } + + if (attacker) { + this.stats[name] = [this.stats[name][0] + value, this.stats[name][1]]; + } else { + this.stats[name] = [this.stats[name][0], this.stats[name][1] + value]; + } + } + + /** + * Get important stats + */ + getImportant(maxcount: number): { name: string, attacker: number, defender: number }[] { + // TODO Sort by importance + let result: { name: string, attacker: number, defender: number }[] = []; + iteritems(this.stats, (name, [attacker, defender]) => { + if (result.length < maxcount) { + result.push({ name: name, attacker: Math.round(attacker), defender: Math.round(defender) }); + } + }); + return result; + } + + /** + * Watch a battle log to automatically feed the collector + */ + watchLog(log: BattleLog, attacker: Fleet) { + log.subscribe(event => { + if (event instanceof DamageEvent) { + this.addStat("Damage dealt", event.hull + event.shield, event.ship.fleet !== attacker); + } else if (event instanceof MoveEvent) { + this.addStat("Move distance (km)", event.distance, event.ship.fleet === attacker); + } else if (event instanceof DroneDeployedEvent) { + this.addStat("Drones deployed", 1, event.ship.fleet === attacker); + } + }); + } + } +} diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index 9b0d091..6ab727d 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -33,6 +33,13 @@ module TS.SpaceTac.Specs { ship.moveTo(50, 50); expect(ship.arena_angle).toBeCloseTo(3.14159265, 0.00001); + + let battle = new Battle(); + battle.fleets[0].addShip(ship); + expect(battle.log.events).toEqual([]); + + ship.moveTo(70, 50); + expect(battle.log.events).toEqual([new MoveEvent(ship, 70, 50, 20)]); }); it("applies equipment cooldown", function () { diff --git a/src/core/Ship.ts b/src/core/Ship.ts index 9907b97..d7c49ad 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -419,7 +419,7 @@ module TS.SpaceTac { this.setArenaFacingAngle(angle); if (log) { - this.addBattleEvent(new MoveEvent(this, this.arena_x, this.arena_y)); + this.addBattleEvent(new MoveEvent(this, this.arena_x, this.arena_y, 0)); } } } @@ -427,14 +427,16 @@ module TS.SpaceTac { // Move toward a location // This does not check or consume action points moveTo(x: number, y: number, log: boolean = true): void { - if (x != this.arena_x || y != this.arena_y) { - var angle = Math.atan2(y - this.arena_y, x - this.arena_x); + let dx = x - this.arena_x; + let dy = y - this.arena_y; + if (dx != 0 || dy != 0) { + let angle = Math.atan2(dy, dx); this.setArenaFacingAngle(angle); this.setArenaPosition(x, y); if (log) { - this.addBattleEvent(new MoveEvent(this, x, y)); + this.addBattleEvent(new MoveEvent(this, x, y, Math.sqrt(dx * dx + dy * dy))); } } } diff --git a/src/core/events/MoveEvent.ts b/src/core/events/MoveEvent.ts index 5231d35..15b4ec9 100644 --- a/src/core/events/MoveEvent.ts +++ b/src/core/events/MoveEvent.ts @@ -3,12 +3,16 @@ module TS.SpaceTac { // Event logged when a ship moves export class MoveEvent extends BaseLogShipTargetEvent { - // New facing angle, in radians - facing_angle: number; + // Distance traveled + distance: number - constructor(ship: Ship, x: number, y: number) { + // New facing angle, in radians + facing_angle: number + + constructor(ship: Ship, x: number, y: number, distance: number) { super("move", ship, Target.newFromLocation(x, y)); + this.distance = distance; this.facing_angle = ship.arena_angle; } } diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index c0a63a6..68619b1 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -257,9 +257,8 @@ module TS.SpaceTac.UI { this.gameui.session.setBattleEnded(); - let dialog = new OutcomeDialog(this, this.player, this.battle.outcome); - dialog.position.set(this.getMidWidth() - dialog.width / 2, this.getMidHeight() - dialog.height / 2); - this.outcome_layer.addChild(dialog); + let dialog = new OutcomeDialog(this, this.player, this.battle.outcome, this.battle.stats); + dialog.moveToLayer(this.outcome_layer); } else { console.error("Battle not ended !"); } diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts index 5116d86..d3f5a7b 100644 --- a/src/ui/battle/LogProcessor.ts +++ b/src/ui/battle/LogProcessor.ts @@ -35,6 +35,7 @@ module TS.SpaceTac.UI { start() { this.subscription = this.log.subscribe(event => this.processBattleEvent(event)); this.battle.injectInitialEvents(); + this.battle.stats.watchLog(this.battle.log, this.view.player.fleet); } /** diff --git a/src/ui/battle/OutcomeDialog.ts b/src/ui/battle/OutcomeDialog.ts index 216f4e0..0654b0a 100644 --- a/src/ui/battle/OutcomeDialog.ts +++ b/src/ui/battle/OutcomeDialog.ts @@ -1,49 +1,45 @@ +/// + module TS.SpaceTac.UI { /** * Dialog to display battle outcome */ - export class OutcomeDialog extends Phaser.Image { - constructor(parent: BattleView, player: Player, outcome: BattleOutcome) { - super(parent.game, 0, 0, "battle-outcome-dialog"); + export class OutcomeDialog extends UIComponent { + constructor(parent: BattleView, player: Player, outcome: BattleOutcome, stats: BattleStats) { + super(parent, 1428, 1032, "battle-outcome-dialog"); let victory = outcome.winner && (outcome.winner.player == player); - let title = new Phaser.Image(this.game, 0, 0, victory ? "battle-outcome-title-victory" : "battle-outcome-title-defeat"); - title.anchor.set(0.5, 0.5); - title.position.set(this.width / 2, 164); - this.addChild(title); + this.addImage(714, 164, victory ? "battle-outcome-title-victory" : "battle-outcome-title-defeat"); if (victory) { - let button = new Phaser.Button(this.game, 344, 842, "battle-outcome-button-loot", () => { - // Open loot screen - if (outcome.winner) { - parent.character_sheet.show(outcome.winner.ships[0]); - parent.character_sheet.setLoot(outcome.loot); - } - }) - parent.tooltip.bindStaticText(button, "Open character sheet to loot equipment from defeated fleet"); - this.addChild(button); + this.addButton(502, 871, () => { + parent.character_sheet.show(nn(outcome.winner).ships[0]); + parent.character_sheet.setLoot(outcome.loot); + }, "battle-outcome-button-loot", undefined, "Open character sheet to loot equipment from defeated fleet"); - button = new Phaser.Button(this.game, 766, 842, "battle-outcome-button-map", () => { - // Exit battle and go back to map + this.addButton(924, 871, () => { parent.exitBattle(); - }); - parent.tooltip.bindStaticText(button, "Exit the battle and go back to the map"); - this.addChild(button); + }, "battle-outcome-button-map", undefined, "Exit the battle and go back to the map"); } else { - let button = new Phaser.Button(this.game, 344, 842, "battle-outcome-button-revert", () => { - // Revert just before battle + this.addButton(502, 871, () => { parent.revertBattle(); - }); - parent.tooltip.bindStaticText(button, "Go back to where the fleet was before the battle happened"); - this.addChild(button); + }, "battle-outcome-button-revert", undefined, "Go back to where the fleet was before the battle happened"); - button = new Phaser.Button(this.game, 766, 842, "battle-outcome-button-menu", () => { + this.addButton(924, 871, () => { // Quit the game, and go back to menu parent.gameui.quitGame(); - }); - parent.tooltip.bindStaticText(button, "Quit the game, and go back to main menu"); - this.addChild(button); + }, "battle-outcome-button-menu", undefined, "Quit the game, and go back to main menu"); } + + this.addText(780, 270, "You", "#ffffff", 20); + this.addText(980, 270, "Enemy", "#ffffff", 20); + stats.getImportant(10).forEach((stat, index) => { + this.addText(500, 314 + 40 * index, stat.name, "#ffffff", 20); + this.addText(780, 314 + 40 * index, stat.attacker.toString(), "#8ba883", 20, true); + this.addText(980, 314 + 40 * index, stat.defender.toString(), "#cd6767", 20, true); + }); + + this.setPositionInsideParent(0.5, 0.5); } } } diff --git a/src/ui/common/UIComponent.ts b/src/ui/common/UIComponent.ts index 6fc8295..0c2c1d9 100644 --- a/src/ui/common/UIComponent.ts +++ b/src/ui/common/UIComponent.ts @@ -39,6 +39,13 @@ module TS.SpaceTac.UI { return this.view.gameui; } + /** + * Move the a parent's layer + */ + moveToLayer(layer: Phaser.Group) { + layer.add(this.container); + } + /** * Create the internal phaser node */ @@ -127,13 +134,42 @@ module TS.SpaceTac.UI { /** * Add a button in the component, positioning its center. */ - addButton(x: number, y: number, on_click: Function, bg_normal: string, bg_hover = bg_normal, angle = 0) { + addButton(x: number, y: number, on_click: Function, bg_normal: string, bg_hover = bg_normal, tooltip = "", angle = 0) { let button = new Phaser.Button(this.view.game, x, y, bg_normal, on_click); button.anchor.set(0.5, 0.5); button.angle = angle; + if (tooltip) { + this.view.tooltip.bindStaticText(button, tooltip); + } this.addInternalChild(button); } + /** + * Add a static text. + */ + addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0): void { + let style = { font: `${bold ? "bold " : ""}${size}pt Arial`, fill: color, align: center ? "center" : "left" }; + let text = new Phaser.Text(this.view.game, x, y, content, style); + if (center) { + text.anchor.set(0.5, 0.5); + } + if (width) { + text.wordWrap = true; + text.wordWrapWidth = width; + } + this.addInternalChild(text); + } + + /** + * Add a static image, positioning its center. + */ + addImage(x: number, y: number, key: string, scale = 1): void { + let image = new Phaser.Image(this.container.game, x, y, key); + image.anchor.set(0.5, 0.5); + image.scale.set(scale); + this.addInternalChild(image); + } + /** * Set the keyboard focus on this component. */ diff --git a/src/ui/menu/LoadDialog.ts b/src/ui/menu/LoadDialog.ts index 4617b3d..89cf620 100644 --- a/src/ui/menu/LoadDialog.ts +++ b/src/ui/menu/LoadDialog.ts @@ -13,8 +13,8 @@ module TS.SpaceTac.UI { constructor(parent: MainMenu) { super(parent, 1344, 566, "menu-load-bg"); - this.addButton(600, 115, () => this.paginateSave(-1), "common-arrow", "common-arrow", 180); - this.addButton(1038, 115, () => this.paginateSave(1), "common-arrow", "common-arrow", 0); + this.addButton(600, 115, () => this.paginateSave(-1), "common-arrow", "common-arrow", "Scroll to newer saves", 180); + this.addButton(1038, 115, () => this.paginateSave(1), "common-arrow", "common-arrow", "Scroll to older saves", 0); this.addButton(1224, 115, () => this.load(), "common-button-ok"); this.addButton(1224, 341, () => this.join(), "common-button-ok");