module TS.SpaceTac { // A turn-based battle between fleets export class Battle { // Flag indicating if the battle is ended ended: boolean // Battle outcome, if *ended* is true outcome: BattleOutcome // Battle cheats cheats: BattleCheats // Statistics stats: BattleStats // Log of all battle events log: BattleLog // List of fleets engaged in battle fleets: Fleet[] // List of ships, sorted by their initiative throw play_order: Ship[] // Current turn turn: number // Current ship whose turn it is to play playing_ship_index: number | null playing_ship: Ship | null // List of deployed drones drones: Drone[] = [] // Size of the battle area width: number height: number border = 50 ship_separation = 100 // Timer to use for scheduled things timer = Timer.global // Indicator that an AI is playing ai_playing = false // Create a battle between two fleets constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1808, height = 948) { this.fleets = [fleet1, fleet2]; this.play_order = []; this.playing_ship_index = null; this.playing_ship = null; this.ended = false; this.width = width; this.height = height; this.log = new BattleLog(); this.stats = new BattleStats(); this.cheats = new BattleCheats(this, fleet1.player); this.fleets.forEach((fleet: Fleet) => { fleet.setBattle(this); }); } postUnserialize() { this.ai_playing = false; } // Create a quick random battle, for testing purposes static newQuickRandom(start = true, level = 1, shipcount = 5): Battle { var player1 = Player.newQuickRandom("Player", level, shipcount, true); var player2 = Player.newQuickRandom("Enemy", level, shipcount, true); var result = new Battle(player1.fleet, player2.fleet); if (start) { result.start(); } return result; } /** * Get the number of turns in a game cycle. */ getCycleLength(): number { return this.play_order.length; } /** * Get the number of turns before a specific ship plays (currently playing ship will return 0). */ getTurnsBefore(ship: Ship): number { let pos = this.play_order.indexOf(ship) - (this.playing_ship_index || 0); if (pos < 0) { pos += this.play_order.length; } return pos; } /** * Return an iterator over all ships engaged in the battle */ iships(alive_only = false): Iterator { 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 { return ifilter(this.iships(alive_only), ship => ship.getPlayer() === player); } /** * Return an iterator over ships enemy of a player */ ienemies(player: Player, alive_only = false): Iterator { return ifilter(this.iships(alive_only), ship => ship.getPlayer() !== player); } // 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; } else if (this.playing_ship && this.playing_ship.getPlayer() == player) { return this.playing_ship.isAbleToPlay(false); } else { return false; } } // Create play order, performing an initiative throw throwInitiative(gen: RandomGenerator = new RandomGenerator()): void { var play_order: Ship[] = []; // Throw each ship's initiative this.fleets.forEach(function (fleet: Fleet) { fleet.ships.forEach(function (ship: Ship) { ship.throwInitiative(gen); play_order.push(ship); }); }); // Sort by throw result play_order.sort(function (ship1: Ship, ship2: Ship) { return (ship2.play_priority - ship1.play_priority); }); this.play_order = play_order; } // Defines the initial ship positions of all engaged fleets 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); } } // Count the number of fleets still alive countAliveFleets(): number { var result = 0; this.fleets.forEach((fleet: Fleet) => { if (fleet.isAlive()) { result += 1; } }); return result; } // Collect all ships within a given radius of a target collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] { return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius)); } /** * Ends a battle and sets the outcome */ endBattle(winner: Fleet | null, log = true) { this.ended = true; this.outcome = new BattleOutcome(winner); // Apply experience this.outcome.grantExperience(this.fleets); // Broadcast if (log && this.log) { this.log.add(new EndBattleEvent(this.outcome)); } // Apply to all ships iforeach(this.iships(), ship => ship.endBattle(this.turn)); } // Checks end battle conditions, returns true if the battle ended checkEndBattle(log: boolean = true) { if (this.ended) { return true; } var alive_fleets = this.countAliveFleets(); if (alive_fleets === 0) { // It's a draw this.endBattle(null, log); } else if (alive_fleets === 1) { // We have a winner var winner: Fleet | null = null; this.fleets.forEach((fleet: Fleet) => { if (fleet.isAlive()) { winner = fleet; } }); this.endBattle(winner, log); } return this.ended; } // 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 advanceToNextShip(log: boolean = true): void { var previous_ship = this.playing_ship; if (this.playing_ship && this.playing_ship.playing) { this.playing_ship.endTurn(); } if (this.checkEndBattle(log)) { return; } this.drones.forEach(drone => drone.activate()); if (this.play_order.length === 0) { this.playing_ship_index = null; this.playing_ship = null; } else { 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; } this.playing_ship = this.play_order[this.playing_ship_index]; } if (this.playing_ship) { if (this.playing_ship_index == 0) { this.turn += 1; } this.playing_ship.startTurn(); } this.ai_playing = false; if (log && previous_ship && this.playing_ship) { this.log.add(new ShipChangeEvent(previous_ship, this.playing_ship)); } } /** * Make an AI play the current ship */ playAI(ai: AbstractAI | null = null): boolean { if (this.playing_ship && !this.ai_playing) { this.ai_playing = true; if (!ai) { // TODO Use an AI adapted to the fleet ai = new TacticalAI(this.playing_ship, this.timer); } ai.play(); return true; } else { return false; } } // Start the battle // This will call all necessary initialization steps (initiative, placement...) // This will not add any event to the battle log start(): void { this.ended = false; this.turn = 0; this.placeShips(); this.throwInitiative(); iforeach(this.iships(), ship => ship.startBattle()); this.advanceToNextShip(); // For now, we inject bootstrap events in the log this.getBootstrapEvents().forEach(event => this.log.add(event)); } /** * Get a set of minimal events required to reach current state from an empty state. */ getBootstrapEvents(): BaseBattleEvent[] { let result: BaseBattleEvent[] = []; // Simulate initial ship placement this.play_order.forEach(ship => { let event = new MoveEvent(ship, ship.location, ship.location); event.initial = true; result.push(event); }); // Simulate active effects this.play_order.forEach(ship => { if (ship.alive) { let event = ship.getActiveEffects(); event.initial = true; result.push(event); } }); // Indicate emergency stasis this.play_order.forEach(ship => { if (!ship.alive) { let event = new DeathEvent(ship); event.initial = true; result.push(event); } }); // Simulate drones deployment this.drones.forEach(drone => { let event = new DroneDeployedEvent(drone); event.initial = true; result.push(event); }); // Simulate game turn if (this.playing_ship) { let event = new ShipChangeEvent(this.playing_ship, this.playing_ship); event.initial = true; result.push(event); } return result; } /** * 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 { var side_angle = facing_angle + Math.PI * 0.5; var spacing = width * 0.2; 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); } } /** * Add a drone to the battle */ addDrone(drone: Drone, log = true) { if (add(this.drones, drone)) { if (log) { this.log.add(new DroneDeployedEvent(drone)); } } } /** * Remove a drone from the battle */ removeDrone(drone: Drone, log = true) { if (remove(this.drones, drone)) { if (log) { this.log.add(new DroneDestroyedEvent(drone)); } } } /** * Get the list of area effects at a given location */ iAreaEffects(x: number, y: number): Iterator { let drones_in_range = ifilter(iarray(this.drones), drone => drone.isInRange(x, y)); return ichain( ichainit(imap(drones_in_range, drone => iarray(drone.effects))), ichainit(imap(this.iships(), ship => ship.iAreaEffects(x, y))) ); } } }