Added battle stats
This commit is contained in:
parent
39389bdf43
commit
c717b153ce
|
@ -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
3
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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
68
src/core/BattleStats.spec.ts
Normal file
68
src/core/BattleStats.spec.ts
Normal 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
52
src/core/BattleStats.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 () {
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 !");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
Loading…
Reference in a new issue