module TK.SpaceTac { /** * A turn-based battle between fleets */ export class Battle { // Battle outcome, if the battle has ended outcome: BattleOutcome | null = null // Statistics stats: BattleStats // Log of all battle events log: BattleLog // List of fleets engaged in battle fleets: Fleet[] // Container of all engaged ships ships: RObjectContainer // Current battle turn count cycle = 0 // List of deployed drones drones = new RObjectContainer() // Size of the battle area width: number height: number constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1920, height = 1080) { this.fleets = [fleet1, fleet2]; this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships)); this.width = width; this.height = height; this.log = new BattleLog(); this.stats = new BattleStats(); this.fleets.forEach((fleet: Fleet) => { fleet.setBattle(this); }); } /** * Property is true if the battle has ended */ get ended(): boolean { return bool(this.outcome); } /** * Apply a turn plan through a resolution */ applyTurnPlan(plan: TurnPlan, random?: RandomGenerator): void { const resolution = new TurnResolution(this, plan, random); resolution.resolve(); this.performChecks(); } /** * 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)); } /** * Create a quick random battle, for testing purposes, or quick skirmish */ static newQuickRandom(start = true, level = 1, shipcount = 5): Battle { let player1 = Player.newQuickRandom("Player", level, shipcount, true); let player2 = Player.newQuickRandom("Enemy", level, shipcount, true); let result = new Battle(player1.fleet, player2.fleet); if (start) { result.start(); } return result; } /** * Get a ship by its ID. */ getShip(id: RObjectId | null): Ship | null { if (id === null) { return null; } else { return this.ships.get(id); } } /** * Return an iterator over all ships engaged in the battle */ iships(alive_only = false): Iterable { 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(ship: Ship, alive_only = false): Iterable { return ifilter(this.iships(alive_only), iship => iship.fleet.player.is(ship.fleet.player)); } /** * Return an iterator over ships enemy of a player */ ienemies(ship: Ship, alive_only = false): Iterable { return ifilter(this.iships(alive_only), iship => !iship.fleet.player.is(ship.fleet.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 { return !this.ended; } // 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); } } // 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 the battle and sets the outcome */ endBattle(winner: Fleet | null) { this.applyDiffs([new EndBattleDiff(winner, this.cycle)]); } /** * Make an AI play the current turn */ playAI(debug = false): boolean { // TODO //AIWorker.process(this, debug); return false; } /** * Start the battle * * This will call all necessary initialization steps (initiative, placement...) * * This should not put any diff in the log */ start(): void { this.outcome = null; this.cycle = 1; this.placeShips(); iforeach(this.iships(), ship => ship.restoreInitialState()); } /** * 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) { this.drones.add(drone); } /** * Remove a drone from the battle */ removeDrone(drone: Drone) { this.drones.remove(drone); } /** * Get the list of area effects that are expected to apply on a given ship */ getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] { let drone_effects = this.drones.list().map(drone => { // TODO Should apply filterImpactedShips from drone action if (drone.isInRange(ship.arena_x, ship.arena_y)) { return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]); } else { return []; } }); let ships_effects = this.ships.list().map(iship => { return iship.getAreaEffects(ship).map((effect): [Ship | Drone, BaseEffect] => [iship, effect]); }); return flatten(drone_effects.concat(ships_effects)); } /** * Perform all battle checks to ensure the state is consistent * * Returns all applied diffs */ performChecks(): BaseBattleDiff[] { let checks = new BattleChecks(this); return 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_id: RObjectId, target?: Target): boolean { let ship = first(this.ships.list(), ship => ship.actions.getById(action_id) !== null); if (ship) { let action = ship.actions.getById(action_id); if (action) { if (!target) { target = action.getDefaultTarget(ship); } if (action.apply(this, ship, target)) { this.performChecks(); if (!this.ended) { this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]); } return true; } else { return false; } } else { console.error("Action not found on ship", action_id, ship); return false; } } else { console.error("Cannot apply action - ship not playing", action_id, this); return false; } } /** * Revert the last turn * * This will remove diffs from the log, so pay attention to other log clients! */ revertOneTurn(): void { let client = new BattleLogClient(this, this.log); while (!client.atStart() && !(client.getCurrent() instanceof TurnStartDiff)) { client.backward(); } if (!client.atStart()) { client.backward(); } client.truncate(); } } }