2017-09-24 22:23:22 +00:00
|
|
|
module TK.SpaceTac {
|
2017-11-14 00:07:06 +00:00
|
|
|
/**
|
|
|
|
* A turn-based battle between fleets
|
|
|
|
*/
|
2017-02-07 18:54:53 +00:00
|
|
|
export class Battle {
|
2017-11-14 00:07:06 +00:00
|
|
|
// Battle outcome, if the battle has ended
|
|
|
|
outcome: BattleOutcome | null = null
|
2017-05-25 23:09:29 +00:00
|
|
|
|
2017-08-17 17:51:22 +00:00
|
|
|
// Battle cheats
|
|
|
|
cheats: BattleCheats
|
|
|
|
|
2017-05-25 23:09:29 +00:00
|
|
|
// Statistics
|
|
|
|
stats: BattleStats
|
2015-02-13 00:00:00 +00:00
|
|
|
|
2014-12-31 00:00:00 +00:00
|
|
|
// Log of all battle events
|
2017-05-25 23:09:29 +00:00
|
|
|
log: BattleLog
|
2014-12-31 00:00:00 +00:00
|
|
|
|
2014-12-29 00:00:00 +00:00
|
|
|
// List of fleets engaged in battle
|
2017-05-25 23:09:29 +00:00
|
|
|
fleets: Fleet[]
|
2014-12-29 00:00:00 +00:00
|
|
|
|
2017-10-25 22:45:53 +00:00
|
|
|
// Container of all engaged ships
|
|
|
|
ships: RObjectContainer<Ship>
|
2014-12-29 00:00:00 +00:00
|
|
|
|
2017-10-25 22:45:53 +00:00
|
|
|
// List of playing ships, sorted by their initiative throw
|
|
|
|
play_order: Ship[]
|
|
|
|
play_index = -1
|
2017-02-21 21:16:18 +00:00
|
|
|
|
2017-10-25 22:45:53 +00:00
|
|
|
// Current battle "cycle" (one cycle is one turn done for all ships in the play order)
|
2017-11-14 00:07:06 +00:00
|
|
|
cycle = 0
|
2014-12-31 00:00:00 +00:00
|
|
|
|
2017-02-06 21:46:55 +00:00
|
|
|
// List of deployed drones
|
2017-11-14 00:07:06 +00:00
|
|
|
drones = new RObjectContainer<Drone>()
|
2017-02-06 21:46:55 +00:00
|
|
|
|
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-08-17 17:51:22 +00:00
|
|
|
border = 50
|
|
|
|
ship_separation = 100
|
2017-01-09 00:37:15 +00:00
|
|
|
|
2017-02-16 22:59:41 +00:00
|
|
|
// Timer to use for scheduled things
|
2017-05-25 23:09:29 +00:00
|
|
|
timer = Timer.global
|
2017-02-16 22:59:41 +00:00
|
|
|
|
2017-05-30 18:23:35 +00:00
|
|
|
// Indicator that an AI is playing
|
|
|
|
ai_playing = false
|
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
constructor(fleet1 = new Fleet(new Player(undefined, "Attacker")), fleet2 = new Fleet(new Player(undefined, "Defender")), width = 1808, height = 948) {
|
2017-03-09 17:11:00 +00:00
|
|
|
this.fleets = [fleet1, fleet2];
|
2017-10-25 22:45:53 +00:00
|
|
|
this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships));
|
2014-12-30 00:00:00 +00:00
|
|
|
this.play_order = [];
|
2017-02-19 16:54:19 +00:00
|
|
|
this.width = width;
|
|
|
|
this.height = height;
|
2015-01-29 00:00:00 +00:00
|
|
|
|
2017-05-25 23:09:29 +00:00
|
|
|
this.log = new BattleLog();
|
|
|
|
this.stats = new BattleStats();
|
2017-08-17 17:51:22 +00:00
|
|
|
this.cheats = new BattleCheats(this, fleet1.player);
|
2017-05-25 23:09:29 +00:00
|
|
|
|
2015-01-29 00:00:00 +00:00
|
|
|
this.fleets.forEach((fleet: Fleet) => {
|
|
|
|
fleet.setBattle(this);
|
|
|
|
});
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-07-20 15:49:47 +00:00
|
|
|
postUnserialize() {
|
|
|
|
this.ai_playing = false;
|
|
|
|
}
|
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
/**
|
|
|
|
* Property is true if the battle has ended
|
|
|
|
*/
|
|
|
|
get ended(): boolean {
|
|
|
|
return bool(this.outcome);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply a list of diffs to the game state, and add them to the log.
|
|
|
|
*
|
|
|
|
* This should be the main way to modify the game state.
|
|
|
|
*/
|
|
|
|
applyDiffs(diffs: BaseBattleDiff[]): void {
|
|
|
|
let client = new BattleLogClient(this, this.log);
|
|
|
|
diffs.forEach(diff => client.add(diff));
|
|
|
|
}
|
|
|
|
|
2017-10-02 16:02:30 +00:00
|
|
|
/**
|
|
|
|
* Create a quick random battle, for testing purposes, or quick skirmish
|
|
|
|
*/
|
2017-05-02 22:49:35 +00:00
|
|
|
static newQuickRandom(start = true, level = 1, shipcount = 5): Battle {
|
2017-10-02 16:02:30 +00:00
|
|
|
let player1 = Player.newQuickRandom("Player", level, shipcount, true);
|
|
|
|
let player2 = Player.newQuickRandom("Enemy", level, shipcount, true);
|
2015-01-07 00:00:00 +00:00
|
|
|
|
2017-10-02 16:02:30 +00:00
|
|
|
let 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-05-10 22:52:16 +00:00
|
|
|
/**
|
2017-10-25 22:45:53 +00:00
|
|
|
* Get the currently playing ship
|
2017-05-10 22:52:16 +00:00
|
|
|
*/
|
2017-10-25 22:45:53 +00:00
|
|
|
get playing_ship(): Ship | null {
|
|
|
|
return this.play_order[this.play_index] || null;
|
2017-05-10 22:52:16 +00:00
|
|
|
}
|
|
|
|
|
2017-05-14 23:00:36 +00:00
|
|
|
/**
|
2017-10-25 22:45:53 +00:00
|
|
|
* Get a ship by its ID.
|
2017-05-14 23:00:36 +00:00
|
|
|
*/
|
2017-11-14 00:07:06 +00:00
|
|
|
getShip(id: RObjectId | null): Ship | null {
|
|
|
|
if (id === null) {
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
return this.ships.get(id);
|
|
|
|
}
|
2017-05-14 23:00:36 +00:00
|
|
|
}
|
|
|
|
|
2017-02-22 01:14:14 +00:00
|
|
|
/**
|
|
|
|
* Return an iterator over all ships engaged in the battle
|
|
|
|
*/
|
2017-05-17 23:17:07 +00:00
|
|
|
iships(alive_only = false): Iterator<Ship> {
|
|
|
|
let result = ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships)));
|
|
|
|
return alive_only ? ifilter(result, ship => ship.alive) : result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an iterator over ships allies of (or owned by) a player
|
|
|
|
*/
|
|
|
|
iallies(player: Player, alive_only = false): Iterator<Ship> {
|
|
|
|
return ifilter(this.iships(alive_only), ship => ship.getPlayer() === player);
|
2017-02-22 01:14:14 +00:00
|
|
|
}
|
|
|
|
|
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> {
|
2017-05-17 23:17:07 +00:00
|
|
|
return ifilter(this.iships(alive_only), ship => ship.getPlayer() !== player);
|
2017-02-27 00:42:12 +00:00
|
|
|
}
|
|
|
|
|
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;
|
2017-10-25 22:45:53 +00:00
|
|
|
this.play_index = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the number of turns before a specific ship plays (currently playing ship will return 0).
|
|
|
|
*
|
|
|
|
* Returns -1 if the ship is not in the play list.
|
|
|
|
*/
|
|
|
|
getPlayOrder(ship: Ship): number {
|
|
|
|
let index = this.play_order.indexOf(ship);
|
|
|
|
if (index < 0) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
let result = index - this.play_index;
|
2017-11-14 00:07:06 +00:00
|
|
|
return (result < 0) ? (result + this.play_order.length) : result;
|
2017-10-25 22:45:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a ship in the play order list
|
|
|
|
*/
|
|
|
|
removeFromPlayOrder(idx: number): void {
|
|
|
|
this.play_order.splice(idx, 1);
|
|
|
|
if (idx <= this.play_index) {
|
|
|
|
this.play_index -= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a ship from the play order list
|
|
|
|
*/
|
|
|
|
insertInPlayOrder(idx: number, ship: Ship): void {
|
|
|
|
this.play_order.splice(idx, 0, ship);
|
|
|
|
if (idx <= this.play_index) {
|
|
|
|
this.play_index += 1;
|
|
|
|
}
|
2014-12-29 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
/**
|
|
|
|
* Set the currently playing ship
|
|
|
|
*/
|
|
|
|
setPlayingShip(ship: Ship): void {
|
|
|
|
let current = this.playing_ship;
|
|
|
|
if (current) {
|
|
|
|
current.playing = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.play_index = this.play_order.indexOf(ship);
|
|
|
|
this.ai_playing = false;
|
|
|
|
|
|
|
|
current = this.playing_ship;
|
|
|
|
if (current) {
|
|
|
|
current.playing = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Defines the initial ship positions of all engaged fleets
|
2017-05-30 10:33:07 +00:00
|
|
|
placeShips(vertical = true): void {
|
|
|
|
if (vertical) {
|
|
|
|
this.placeFleetShips(this.fleets[0], this.width * 0.25, this.height * 0.5, 0, this.height);
|
|
|
|
this.placeFleetShips(this.fleets[1], this.width * 0.75, this.height * 0.5, Math.PI, this.height);
|
|
|
|
} else {
|
|
|
|
this.placeFleetShips(this.fleets[0], this.width * 0.5, this.height * 0.90, -Math.PI / 2, this.width);
|
|
|
|
this.placeFleetShips(this.fleets[1], this.width * 0.5, this.height * 0.10, Math.PI / 2, this.width);
|
|
|
|
}
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
/**
|
2017-11-14 00:07:06 +00:00
|
|
|
* Ends the battle and sets the outcome
|
2017-04-24 17:59:16 +00:00
|
|
|
*/
|
2017-11-14 00:07:06 +00:00
|
|
|
endBattle(winner: Fleet | null) {
|
|
|
|
this.applyDiffs([new EndBattleDiff(winner, this.cycle)]);
|
2015-02-09 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
/**
|
|
|
|
* Get the next playing ship
|
|
|
|
*/
|
|
|
|
getNextShip(): Ship {
|
|
|
|
return this.play_order[(this.play_index + 1) % this.play_order.length];
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-02-16 22:59:41 +00:00
|
|
|
/**
|
|
|
|
* Make an AI play the current ship
|
|
|
|
*/
|
2017-08-23 17:59:22 +00:00
|
|
|
playAI(ai: AbstractAI | null = null, debug = false): boolean {
|
2017-05-30 18:23:35 +00:00
|
|
|
if (this.playing_ship && !this.ai_playing) {
|
|
|
|
this.ai_playing = true;
|
2017-03-09 17:11:00 +00:00
|
|
|
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
|
|
|
}
|
2017-08-23 17:59:22 +00:00
|
|
|
ai.play(debug);
|
2017-05-30 18:23:35 +00:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
2017-02-16 22:59:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
/**
|
|
|
|
* Start the battle
|
|
|
|
*
|
|
|
|
* This will call all necessary initialization steps (initiative, placement...)
|
|
|
|
*
|
|
|
|
* This should not put any diff in the log
|
|
|
|
*/
|
2014-12-30 00:00:00 +00:00
|
|
|
start(): void {
|
2017-11-14 00:07:06 +00:00
|
|
|
this.outcome = null;
|
|
|
|
this.cycle = 1;
|
2014-12-30 00:00:00 +00:00
|
|
|
this.placeShips();
|
2017-09-14 22:24:53 +00:00
|
|
|
this.stats.onBattleStart(this.fleets[0], this.fleets[1]);
|
2014-12-30 00:00:00 +00:00
|
|
|
this.throwInitiative();
|
2017-11-14 00:07:06 +00:00
|
|
|
iforeach(this.iships(), ship => ship.restoreInitialState());
|
|
|
|
this.setPlayingShip(this.play_order[0]);
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-05-29 23:15:32 +00:00
|
|
|
/**
|
2017-11-14 00:07:06 +00:00
|
|
|
* Force current ship's turn to end, then advance to the next one
|
2017-05-29 23:15:32 +00:00
|
|
|
*/
|
2017-11-14 00:07:06 +00:00
|
|
|
advanceToNextShip(): void {
|
2015-04-07 00:00:00 +00:00
|
|
|
if (this.playing_ship) {
|
2017-11-14 00:07:06 +00:00
|
|
|
this.applyOneAction(EndTurnAction.SINGLETON);
|
|
|
|
} else if (this.play_order.length) {
|
|
|
|
this.setPlayingShip(this.play_order[0]);
|
2015-04-07 00:00:00 +00:00
|
|
|
}
|
2017-10-25 22:45:53 +00:00
|
|
|
}
|
|
|
|
|
2017-05-30 10:33:07 +00:00
|
|
|
/**
|
|
|
|
* Defines the initial ship positions for one fleet
|
|
|
|
*
|
|
|
|
* *x* and *y* are the center of the fleet formation
|
|
|
|
* *facing_angle* is the forward angle in radians
|
|
|
|
* *width* is the formation width
|
|
|
|
*/
|
|
|
|
private placeFleetShips(fleet: Fleet, x: number, y: number, facing_angle: number, width: number): void {
|
2015-01-07 00:00:00 +00:00
|
|
|
var side_angle = facing_angle + Math.PI * 0.5;
|
2017-05-30 10:33:07 +00:00
|
|
|
var spacing = width * 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
|
|
|
|
*/
|
2017-11-14 00:07:06 +00:00
|
|
|
addDrone(drone: Drone) {
|
|
|
|
this.drones.add(drone);
|
2017-02-06 21:46:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a drone from the battle
|
|
|
|
*/
|
2017-11-14 00:07:06 +00:00
|
|
|
removeDrone(drone: Drone) {
|
|
|
|
this.drones.remove(drone);
|
2017-02-06 21:46:55 +00:00
|
|
|
}
|
2017-06-12 22:28:54 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the list of area effects at a given location
|
|
|
|
*/
|
|
|
|
iAreaEffects(x: number, y: number): Iterator<BaseEffect> {
|
2017-11-14 00:07:06 +00:00
|
|
|
let drones_in_range = ifilter(this.drones.iterator(), drone => drone.isInRange(x, y));
|
2017-06-12 22:28:54 +00:00
|
|
|
|
|
|
|
return ichain(
|
|
|
|
ichainit(imap(drones_in_range, drone => iarray(drone.effects))),
|
|
|
|
ichainit(imap(this.iships(), ship => ship.iAreaEffects(x, y)))
|
|
|
|
);
|
|
|
|
}
|
2017-11-14 00:07:06 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform all battle checks to ensure the state is consistent
|
|
|
|
*/
|
|
|
|
performChecks(): void {
|
|
|
|
let checks = new BattleChecks(this);
|
|
|
|
checks.apply();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply one action to the battle state
|
|
|
|
*
|
|
|
|
* At the end of the action, some checks will be applied to ensure the battle state is consistent
|
|
|
|
*/
|
|
|
|
applyOneAction(action: BaseAction, target?: Target): boolean {
|
|
|
|
let ship = this.playing_ship;
|
|
|
|
if (ship) {
|
|
|
|
if (action.apply(this, ship, target)) {
|
|
|
|
this.performChecks();
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error("Cannot apply action - ship not playing", action, this);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Revert the last applied action
|
|
|
|
*
|
|
|
|
* This will remove diffs from the log, so pay attention to other log clients!
|
|
|
|
*/
|
|
|
|
revertOneAction(): void {
|
|
|
|
let client = new BattleLogClient(this, this.log);
|
|
|
|
while (!client.atStart() && !(client.getCurrent() instanceof ShipActionUsedDiff)) {
|
|
|
|
client.backward();
|
|
|
|
}
|
|
|
|
if (!client.atStart()) {
|
|
|
|
client.backward();
|
|
|
|
}
|
|
|
|
client.truncate();
|
|
|
|
}
|
2014-12-29 00:00:00 +00:00
|
|
|
}
|
2015-01-07 00:00:00 +00:00
|
|
|
}
|