1
0
Fork 0

Added battle stats

This commit is contained in:
Michaël Lemaire 2017-05-26 01:09:29 +02:00
parent 39389bdf43
commit c717b153ce
13 changed files with 228 additions and 57 deletions

View file

@ -148,4 +148,4 @@ except for area damage and area effects specifically designed for drones.
* 1,2,3...0 - Select action * 1,2,3...0 - Select action
* Space - End current ship's turn * Space - End current ship's turn
* T - Tactical mode for 5 seconds * T - Tactical mode for 3 seconds

3
TODO
View file

@ -28,7 +28,6 @@
* Add actions with cost dependent of distance (like current move actions) * Add actions with cost dependent of distance (like current move actions)
* Keep move and drone actions out of arena borders * Keep move and drone actions out of arena borders
* Find incentives to move from starting position * Find incentives to move from starting position
* Outcome: add battle statistics and/or honors
* Outcome: disable the loot button if there is no loot * 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) * 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) * 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 * 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: 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: 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: add combination of random small move and actual maneuver, as producer
* AI: new duel page with producers/evaluators tweaking * AI: new duel page with producers/evaluators tweaking
* AI: work in a dedicated process * AI: work in a dedicated process

View file

@ -2,40 +2,42 @@ module TS.SpaceTac {
// A turn-based battle between fleets // A turn-based battle between fleets
export class Battle { export class Battle {
// Flag indicating if the battle is ended // Flag indicating if the battle is ended
ended: boolean; ended: boolean
// Battle outcome, if *ended* is true // Battle outcome, if *ended* is true
outcome: BattleOutcome; outcome: BattleOutcome
// Statistics
stats: BattleStats
// Log of all battle events // Log of all battle events
log: BattleLog; log: BattleLog
// List of fleets engaged in battle // List of fleets engaged in battle
fleets: Fleet[]; fleets: Fleet[]
// List of ships, sorted by their initiative throw // List of ships, sorted by their initiative throw
play_order: Ship[]; play_order: Ship[]
// Current turn // Current turn
turn: number; turn: number
// Current ship whose turn it is to play // Current ship whose turn it is to play
playing_ship_index: number | null; playing_ship_index: number | null
playing_ship: Ship | null; playing_ship: Ship | null
// List of deployed drones // List of deployed drones
drones: Drone[] = []; drones: Drone[] = []
// Size of the battle area // Size of the battle area
width: number width: number
height: number height: number
// Timer to use for scheduled things // Timer to use for scheduled things
timer = Timer.global; timer = Timer.global
// Create a battle between two fleets // Create a battle between two fleets
constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1808, height = 948) { constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1808, height = 948) {
this.log = new BattleLog();
this.fleets = [fleet1, fleet2]; this.fleets = [fleet1, fleet2];
this.play_order = []; this.play_order = [];
this.playing_ship_index = null; this.playing_ship_index = null;
@ -44,6 +46,9 @@ module TS.SpaceTac {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.log = new BattleLog();
this.stats = new BattleStats();
this.fleets.forEach((fleet: Fleet) => { this.fleets.forEach((fleet: Fleet) => {
fleet.setBattle(this); fleet.setBattle(this);
}); });
@ -272,7 +277,7 @@ module TS.SpaceTac {
// Simulate initial ship placement // Simulate initial ship placement
this.play_order.forEach(ship => { 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; event.initial = true;
log.add(event); log.add(event);
}); });

View file

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

52
src/core/BattleStats.ts Normal file
View file

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

View file

@ -33,6 +33,13 @@ module TS.SpaceTac.Specs {
ship.moveTo(50, 50); ship.moveTo(50, 50);
expect(ship.arena_angle).toBeCloseTo(3.14159265, 0.00001); 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 () { it("applies equipment cooldown", function () {

View file

@ -419,7 +419,7 @@ module TS.SpaceTac {
this.setArenaFacingAngle(angle); this.setArenaFacingAngle(angle);
if (log) { 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 // Move toward a location
// This does not check or consume action points // This does not check or consume action points
moveTo(x: number, y: number, log: boolean = true): void { moveTo(x: number, y: number, log: boolean = true): void {
if (x != this.arena_x || y != this.arena_y) { let dx = x - this.arena_x;
var angle = Math.atan2(y - this.arena_y, 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.setArenaFacingAngle(angle);
this.setArenaPosition(x, y); this.setArenaPosition(x, y);
if (log) { if (log) {
this.addBattleEvent(new MoveEvent(this, x, y)); this.addBattleEvent(new MoveEvent(this, x, y, Math.sqrt(dx * dx + dy * dy)));
} }
} }
} }

View file

@ -3,12 +3,16 @@
module TS.SpaceTac { module TS.SpaceTac {
// Event logged when a ship moves // Event logged when a ship moves
export class MoveEvent extends BaseLogShipTargetEvent { export class MoveEvent extends BaseLogShipTargetEvent {
// New facing angle, in radians // Distance traveled
facing_angle: number; 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)); super("move", ship, Target.newFromLocation(x, y));
this.distance = distance;
this.facing_angle = ship.arena_angle; this.facing_angle = ship.arena_angle;
} }
} }

View file

@ -257,9 +257,8 @@ module TS.SpaceTac.UI {
this.gameui.session.setBattleEnded(); this.gameui.session.setBattleEnded();
let dialog = new OutcomeDialog(this, this.player, this.battle.outcome); let dialog = new OutcomeDialog(this, this.player, this.battle.outcome, this.battle.stats);
dialog.position.set(this.getMidWidth() - dialog.width / 2, this.getMidHeight() - dialog.height / 2); dialog.moveToLayer(this.outcome_layer);
this.outcome_layer.addChild(dialog);
} else { } else {
console.error("Battle not ended !"); console.error("Battle not ended !");
} }

View file

@ -35,6 +35,7 @@ module TS.SpaceTac.UI {
start() { start() {
this.subscription = this.log.subscribe(event => this.processBattleEvent(event)); this.subscription = this.log.subscribe(event => this.processBattleEvent(event));
this.battle.injectInitialEvents(); this.battle.injectInitialEvents();
this.battle.stats.watchLog(this.battle.log, this.view.player.fleet);
} }
/** /**

View file

@ -1,49 +1,45 @@
/// <reference path="../common/UIComponent.ts" />
module TS.SpaceTac.UI { module TS.SpaceTac.UI {
/** /**
* Dialog to display battle outcome * Dialog to display battle outcome
*/ */
export class OutcomeDialog extends Phaser.Image { export class OutcomeDialog extends UIComponent {
constructor(parent: BattleView, player: Player, outcome: BattleOutcome) { constructor(parent: BattleView, player: Player, outcome: BattleOutcome, stats: BattleStats) {
super(parent.game, 0, 0, "battle-outcome-dialog"); super(parent, 1428, 1032, "battle-outcome-dialog");
let victory = outcome.winner && (outcome.winner.player == player); 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"); this.addImage(714, 164, 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);
if (victory) { if (victory) {
let button = new Phaser.Button(this.game, 344, 842, "battle-outcome-button-loot", () => { this.addButton(502, 871, () => {
// Open loot screen parent.character_sheet.show(nn(outcome.winner).ships[0]);
if (outcome.winner) { parent.character_sheet.setLoot(outcome.loot);
parent.character_sheet.show(outcome.winner.ships[0]); }, "battle-outcome-button-loot", undefined, "Open character sheet to loot equipment from defeated fleet");
parent.character_sheet.setLoot(outcome.loot);
}
})
parent.tooltip.bindStaticText(button, "Open character sheet to loot equipment from defeated fleet");
this.addChild(button);
button = new Phaser.Button(this.game, 766, 842, "battle-outcome-button-map", () => { this.addButton(924, 871, () => {
// Exit battle and go back to map
parent.exitBattle(); parent.exitBattle();
}); }, "battle-outcome-button-map", undefined, "Exit the battle and go back to the map");
parent.tooltip.bindStaticText(button, "Exit the battle and go back to the map");
this.addChild(button);
} else { } else {
let button = new Phaser.Button(this.game, 344, 842, "battle-outcome-button-revert", () => { this.addButton(502, 871, () => {
// Revert just before battle
parent.revertBattle(); parent.revertBattle();
}); }, "battle-outcome-button-revert", undefined, "Go back to where the fleet was before the battle happened");
parent.tooltip.bindStaticText(button, "Go back to where the fleet was before the battle happened");
this.addChild(button);
button = new Phaser.Button(this.game, 766, 842, "battle-outcome-button-menu", () => { this.addButton(924, 871, () => {
// Quit the game, and go back to menu // Quit the game, and go back to menu
parent.gameui.quitGame(); parent.gameui.quitGame();
}); }, "battle-outcome-button-menu", undefined, "Quit the game, and go back to main menu");
parent.tooltip.bindStaticText(button, "Quit the game, and go back to main menu");
this.addChild(button);
} }
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);
} }
} }
} }

View file

@ -39,6 +39,13 @@ module TS.SpaceTac.UI {
return this.view.gameui; return this.view.gameui;
} }
/**
* Move the a parent's layer
*/
moveToLayer(layer: Phaser.Group) {
layer.add(this.container);
}
/** /**
* Create the internal phaser node * Create the internal phaser node
*/ */
@ -127,13 +134,42 @@ module TS.SpaceTac.UI {
/** /**
* Add a button in the component, positioning its center. * 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); let button = new Phaser.Button(this.view.game, x, y, bg_normal, on_click);
button.anchor.set(0.5, 0.5); button.anchor.set(0.5, 0.5);
button.angle = angle; button.angle = angle;
if (tooltip) {
this.view.tooltip.bindStaticText(button, tooltip);
}
this.addInternalChild(button); 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. * Set the keyboard focus on this component.
*/ */

View file

@ -13,8 +13,8 @@ module TS.SpaceTac.UI {
constructor(parent: MainMenu) { constructor(parent: MainMenu) {
super(parent, 1344, 566, "menu-load-bg"); super(parent, 1344, 566, "menu-load-bg");
this.addButton(600, 115, () => this.paginateSave(-1), "common-arrow", "common-arrow", 180); 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", 0); 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, 115, () => this.load(), "common-button-ok");
this.addButton(1224, 341, () => this.join(), "common-button-ok"); this.addButton(1224, 341, () => this.join(), "common-button-ok");