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
* 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)
* 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

View file

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

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);
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 () {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,49 +1,45 @@
/// <reference path="../common/UIComponent.ts" />
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);
}
}
}

View file

@ -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.
*/

View file

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