2017-02-09 00:00:35 +00:00
|
|
|
module TS.SpaceTac {
|
2014-12-29 00:00:00 +00:00
|
|
|
// A turn-based battle between fleets
|
2017-02-07 18:54:53 +00:00
|
|
|
export class Battle {
|
2015-02-09 00:00:00 +00:00
|
|
|
// Flag indicating if the battle is ended
|
|
|
|
ended: boolean;
|
|
|
|
|
2015-02-13 00:00:00 +00:00
|
|
|
// Battle outcome, if *ended* is true
|
|
|
|
outcome: BattleOutcome;
|
|
|
|
|
2014-12-31 00:00:00 +00:00
|
|
|
// Log of all battle events
|
|
|
|
log: BattleLog;
|
|
|
|
|
2014-12-29 00:00:00 +00:00
|
|
|
// List of fleets engaged in battle
|
|
|
|
fleets: Fleet[];
|
|
|
|
|
|
|
|
// List of ships, sorted by their initiative throw
|
|
|
|
play_order: Ship[];
|
|
|
|
|
2017-02-21 21:16:18 +00:00
|
|
|
// Current turn
|
|
|
|
turn: number;
|
|
|
|
|
2014-12-31 00:00:00 +00:00
|
|
|
// Current ship whose turn it is to play
|
2017-03-09 17:11:00 +00:00
|
|
|
playing_ship_index: number | null;
|
|
|
|
playing_ship: Ship | null;
|
2014-12-31 00:00:00 +00:00
|
|
|
|
2017-02-06 21:46:55 +00:00
|
|
|
// List of deployed drones
|
|
|
|
drones: Drone[] = [];
|
|
|
|
|
2017-01-09 00:37:15 +00:00
|
|
|
// Size of the battle area
|
2017-02-19 16:54:19 +00:00
|
|
|
width: number
|
|
|
|
height: number
|
2017-01-09 00:37:15 +00:00
|
|
|
|
2017-02-16 22:59:41 +00:00
|
|
|
// Timer to use for scheduled things
|
|
|
|
timer = Timer.global;
|
|
|
|
|
2014-12-29 00:00:00 +00:00
|
|
|
// Create a battle between two fleets
|
2017-03-09 17:11:00 +00:00
|
|
|
constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1780, height = 948) {
|
2014-12-31 00:00:00 +00:00
|
|
|
this.log = new BattleLog();
|
2017-03-09 17:11:00 +00:00
|
|
|
this.fleets = [fleet1, fleet2];
|
2014-12-30 00:00:00 +00:00
|
|
|
this.play_order = [];
|
2014-12-31 00:00:00 +00:00
|
|
|
this.playing_ship_index = null;
|
|
|
|
this.playing_ship = null;
|
2015-02-09 00:00:00 +00:00
|
|
|
this.ended = false;
|
2017-02-19 16:54:19 +00:00
|
|
|
this.width = width;
|
|
|
|
this.height = height;
|
2015-01-29 00:00:00 +00:00
|
|
|
|
|
|
|
this.fleets.forEach((fleet: Fleet) => {
|
|
|
|
fleet.setBattle(this);
|
|
|
|
});
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2015-01-07 00:00:00 +00:00
|
|
|
// Create a quick random battle, for testing purposes
|
2017-05-02 22:49:35 +00:00
|
|
|
static newQuickRandom(start = true, level = 1, shipcount = 5): Battle {
|
|
|
|
var player1 = Player.newQuickRandom("John", level, shipcount, true);
|
|
|
|
var player2 = Player.newQuickRandom("Carl", level, shipcount, true);
|
2015-01-07 00:00:00 +00:00
|
|
|
|
|
|
|
var result = new Battle(player1.fleet, player2.fleet);
|
2017-02-16 22:59:41 +00:00
|
|
|
if (start) {
|
|
|
|
result.start();
|
2015-02-16 00:00:00 +00:00
|
|
|
}
|
2015-01-07 00:00:00 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-02-22 01:14:14 +00:00
|
|
|
/**
|
|
|
|
* Return an iterator over all ships engaged in the battle
|
|
|
|
*/
|
|
|
|
iships(): Iterator<Ship> {
|
|
|
|
return ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships)));
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
|
|
|
* Return an iterator over ships enemy of a player
|
|
|
|
*/
|
|
|
|
ienemies(player: Player, alive_only = false): Iterator<Ship> {
|
|
|
|
return ifilter(this.iships(), ship => ship.getPlayer() != player && (ship.alive || !alive_only));
|
|
|
|
}
|
|
|
|
|
2015-02-16 00:00:00 +00:00
|
|
|
// Check if a player is able to play
|
|
|
|
// This can be used by the UI to determine if player interaction is allowed
|
|
|
|
canPlay(player: Player): boolean {
|
|
|
|
if (this.ended) {
|
|
|
|
return false;
|
2017-02-16 18:24:21 +00:00
|
|
|
} else if (this.playing_ship && this.playing_ship.getPlayer() == player) {
|
|
|
|
return this.playing_ship.isAbleToPlay(false);
|
2015-02-16 00:00:00 +00:00
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Create play order, performing an initiative throw
|
2014-12-30 00:00:00 +00:00
|
|
|
throwInitiative(gen: RandomGenerator = new RandomGenerator()): void {
|
2014-12-30 00:00:00 +00:00
|
|
|
var play_order: Ship[] = [];
|
|
|
|
|
|
|
|
// Throw each ship's initiative
|
2015-01-07 00:00:00 +00:00
|
|
|
this.fleets.forEach(function (fleet: Fleet) {
|
|
|
|
fleet.ships.forEach(function (ship: Ship) {
|
2014-12-30 00:00:00 +00:00
|
|
|
ship.throwInitiative(gen);
|
|
|
|
play_order.push(ship);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Sort by throw result
|
2014-12-30 00:00:00 +00:00
|
|
|
play_order.sort(function (ship1: Ship, ship2: Ship) {
|
2017-02-07 19:15:21 +00:00
|
|
|
return (ship2.play_priority - ship1.play_priority);
|
2014-12-30 00:00:00 +00:00
|
|
|
});
|
|
|
|
this.play_order = play_order;
|
2014-12-29 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Defines the initial ship positions of all engaged fleets
|
|
|
|
placeShips(): void {
|
2017-01-09 00:37:15 +00:00
|
|
|
this.placeFleetShips(this.fleets[0], this.width * 0.05, this.height * 0.5, 0);
|
|
|
|
this.placeFleetShips(this.fleets[1], this.width * 0.95, this.height * 0.5, Math.PI);
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2015-02-09 00:00:00 +00:00
|
|
|
// Count the number of fleets still alive
|
|
|
|
countAliveFleets(): number {
|
|
|
|
var result = 0;
|
|
|
|
this.fleets.forEach((fleet: Fleet) => {
|
|
|
|
if (fleet.isAlive()) {
|
|
|
|
result += 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-03-11 00:00:00 +00:00
|
|
|
// Collect all ships within a given radius of a target
|
2017-02-15 22:34:27 +00:00
|
|
|
collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] {
|
2017-02-27 00:42:12 +00:00
|
|
|
return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius));
|
2015-03-11 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-04-24 17:59:16 +00:00
|
|
|
/**
|
|
|
|
* Ends a battle and sets the outcome
|
|
|
|
*/
|
2017-03-14 22:28:07 +00:00
|
|
|
endBattle(winner: Fleet | null, log = true) {
|
2017-05-09 17:19:26 +00:00
|
|
|
this.ended = true;
|
|
|
|
this.outcome = new BattleOutcome(winner);
|
|
|
|
|
|
|
|
// Apply experience
|
|
|
|
this.outcome.grantExperience(this.fleets);
|
|
|
|
|
|
|
|
// Broadcast
|
2015-02-13 00:00:00 +00:00
|
|
|
if (log && this.log) {
|
|
|
|
this.log.add(new EndBattleEvent(this.outcome));
|
|
|
|
}
|
2017-05-10 17:16:57 +00:00
|
|
|
|
|
|
|
// Apply to all ships
|
|
|
|
iforeach(this.iships(), ship => ship.endBattle(this.turn));
|
2015-02-13 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2015-02-09 00:00:00 +00:00
|
|
|
// Checks end battle conditions, returns true if the battle ended
|
|
|
|
checkEndBattle(log: boolean = true) {
|
|
|
|
if (this.ended) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
var alive_fleets = this.countAliveFleets();
|
|
|
|
|
2015-02-09 00:00:00 +00:00
|
|
|
if (alive_fleets === 0) {
|
2015-02-09 00:00:00 +00:00
|
|
|
// It's a draw
|
2015-02-13 00:00:00 +00:00
|
|
|
this.endBattle(null, log);
|
2015-02-09 00:00:00 +00:00
|
|
|
} else if (alive_fleets === 1) {
|
2015-02-09 00:00:00 +00:00
|
|
|
// We have a winner
|
2017-03-09 17:11:00 +00:00
|
|
|
var winner: Fleet | null = null;
|
2015-02-09 00:00:00 +00:00
|
|
|
this.fleets.forEach((fleet: Fleet) => {
|
|
|
|
if (fleet.isAlive()) {
|
2015-02-13 00:00:00 +00:00
|
|
|
winner = fleet;
|
2015-02-09 00:00:00 +00:00
|
|
|
}
|
|
|
|
});
|
2015-02-13 00:00:00 +00:00
|
|
|
this.endBattle(winner, log);
|
2015-02-09 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return this.ended;
|
|
|
|
}
|
|
|
|
|
2014-12-31 00:00:00 +00:00
|
|
|
// End the current ship turn, passing control to the next one in play order
|
|
|
|
// If at the end of the play order, next turn will start automatically
|
|
|
|
// Member 'play_order' must be defined before calling this function
|
2015-01-07 00:00:00 +00:00
|
|
|
advanceToNextShip(log: boolean = true): void {
|
2014-12-31 00:00:00 +00:00
|
|
|
var previous_ship = this.playing_ship;
|
|
|
|
|
2015-02-09 00:00:00 +00:00
|
|
|
if (this.checkEndBattle(log)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-01-20 00:02:18 +00:00
|
|
|
if (this.playing_ship && this.playing_ship.playing) {
|
|
|
|
this.playing_ship.endTurn();
|
|
|
|
}
|
|
|
|
|
2015-01-07 00:00:00 +00:00
|
|
|
if (this.play_order.length === 0) {
|
2014-12-31 00:00:00 +00:00
|
|
|
this.playing_ship_index = null;
|
|
|
|
this.playing_ship = null;
|
|
|
|
} else {
|
2017-02-16 22:59:41 +00:00
|
|
|
if (this.playing_ship_index == null) {
|
|
|
|
this.playing_ship_index = 0;
|
|
|
|
} else {
|
|
|
|
this.playing_ship_index = (this.playing_ship_index + 1) % this.play_order.length;
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
2017-02-16 22:59:41 +00:00
|
|
|
this.playing_ship = this.play_order[this.playing_ship_index];
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
2014-12-31 00:00:00 +00:00
|
|
|
|
2015-01-22 00:00:00 +00:00
|
|
|
if (this.playing_ship) {
|
2017-02-21 21:16:18 +00:00
|
|
|
if (this.playing_ship_index == 0) {
|
|
|
|
this.turn += 1;
|
|
|
|
}
|
2017-01-12 00:36:34 +00:00
|
|
|
this.playing_ship.startTurn();
|
2015-01-22 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-03-09 17:11:00 +00:00
|
|
|
if (log && previous_ship && this.playing_ship) {
|
2014-12-31 00:00:00 +00:00
|
|
|
this.log.add(new ShipChangeEvent(previous_ship, this.playing_ship));
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-02-16 22:59:41 +00:00
|
|
|
/**
|
|
|
|
* Make an AI play the current ship
|
|
|
|
*/
|
|
|
|
playAI(ai: AbstractAI | null = null) {
|
2017-03-09 17:11:00 +00:00
|
|
|
if (this.playing_ship) {
|
|
|
|
if (!ai) {
|
|
|
|
// TODO Use an AI adapted to the fleet
|
2017-05-09 23:20:05 +00:00
|
|
|
ai = new TacticalAI(this.playing_ship, this.timer);
|
2017-03-09 17:11:00 +00:00
|
|
|
}
|
|
|
|
ai.play();
|
2017-02-16 22:59:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Start the battle
|
|
|
|
// This will call all necessary initialization steps (initiative, placement...)
|
2014-12-31 00:00:00 +00:00
|
|
|
// This will not add any event to the battle log
|
2014-12-30 00:00:00 +00:00
|
|
|
start(): void {
|
2015-02-09 00:00:00 +00:00
|
|
|
this.ended = false;
|
2017-02-21 21:16:18 +00:00
|
|
|
this.turn = 0;
|
2014-12-30 00:00:00 +00:00
|
|
|
this.placeShips();
|
|
|
|
this.throwInitiative();
|
2017-02-27 00:42:12 +00:00
|
|
|
iforeach(this.iships(), ship => ship.startBattle());
|
2015-02-16 00:00:00 +00:00
|
|
|
this.advanceToNextShip();
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Force an injection of events in the battle log to simulate the initial state
|
|
|
|
// For instance, this may be called after 'start', to use the log subscription system
|
|
|
|
// to initialize a battle UI
|
2014-12-31 00:00:00 +00:00
|
|
|
// Attributes 'play_order' and 'playing_ship' should be defined before calling this
|
2014-12-31 00:00:00 +00:00
|
|
|
injectInitialEvents(): void {
|
2014-12-31 00:00:00 +00:00
|
|
|
var log = this.log;
|
|
|
|
|
|
|
|
// Simulate initial ship placement
|
2017-02-14 00:30:50 +00:00
|
|
|
this.play_order.forEach(ship => {
|
|
|
|
let event = new MoveEvent(ship, ship.arena_x, ship.arena_y);
|
|
|
|
event.initial = true;
|
|
|
|
log.add(event);
|
2014-12-31 00:00:00 +00:00
|
|
|
});
|
|
|
|
|
2017-02-15 22:34:27 +00:00
|
|
|
// Indicate emergency stasis
|
|
|
|
this.play_order.forEach(ship => {
|
|
|
|
if (!ship.alive) {
|
|
|
|
let event = new DeathEvent(ship);
|
|
|
|
event.initial = true;
|
|
|
|
log.add(event);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-02-06 21:46:55 +00:00
|
|
|
// Simulate drones deployment
|
|
|
|
this.drones.forEach(drone => {
|
2017-02-14 00:30:50 +00:00
|
|
|
let event = new DroneDeployedEvent(drone);
|
|
|
|
event.initial = true;
|
|
|
|
log.add(event);
|
2017-02-06 21:46:55 +00:00
|
|
|
});
|
|
|
|
|
2014-12-31 00:00:00 +00:00
|
|
|
// Simulate game turn
|
2015-04-07 00:00:00 +00:00
|
|
|
if (this.playing_ship) {
|
2017-02-14 00:30:50 +00:00
|
|
|
let event = new ShipChangeEvent(this.playing_ship, this.playing_ship);
|
|
|
|
event.initial = true;
|
|
|
|
log.add(event);
|
2015-04-07 00:00:00 +00:00
|
|
|
}
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2015-01-07 00:00:00 +00:00
|
|
|
// Defines the initial ship positions for one fleet
|
|
|
|
// x and y are the center of the fleet placement
|
|
|
|
// facing_angle is the forward angle in radians
|
|
|
|
private placeFleetShips(fleet: Fleet, x: number, y: number, facing_angle: number): void {
|
|
|
|
var side_angle = facing_angle + Math.PI * 0.5;
|
2017-01-09 00:37:15 +00:00
|
|
|
var spacing = this.height * 0.2;
|
2015-01-07 00:00:00 +00:00
|
|
|
var total_length = spacing * (fleet.ships.length - 1);
|
|
|
|
var dx = Math.cos(side_angle);
|
|
|
|
var dy = Math.sin(side_angle);
|
|
|
|
x -= dx * total_length * 0.5;
|
|
|
|
y -= dy * total_length * 0.5;
|
|
|
|
for (var i = 0; i < fleet.ships.length; i++) {
|
|
|
|
fleet.ships[i].setArenaPosition(x + i * dx * spacing, y + i * dy * spacing);
|
|
|
|
fleet.ships[i].setArenaFacingAngle(facing_angle);
|
|
|
|
}
|
2014-12-29 00:00:00 +00:00
|
|
|
}
|
2017-02-06 21:46:55 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a drone to the battle
|
|
|
|
*/
|
|
|
|
addDrone(drone: Drone, log = true) {
|
|
|
|
if (add(this.drones, drone)) {
|
|
|
|
if (log) {
|
|
|
|
this.log.add(new DroneDeployedEvent(drone));
|
|
|
|
}
|
2017-02-14 00:30:50 +00:00
|
|
|
drone.onDeploy(this.play_order);
|
2017-02-06 21:46:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a drone from the battle
|
|
|
|
*/
|
|
|
|
removeDrone(drone: Drone, log = true) {
|
|
|
|
if (remove(this.drones, drone)) {
|
|
|
|
if (log) {
|
|
|
|
this.log.add(new DroneDestroyedEvent(drone));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-12-29 00:00:00 +00:00
|
|
|
}
|
2015-01-07 00:00:00 +00:00
|
|
|
}
|