import { ObjectId } from "mongodb"; import MissionType from "../../../types/MissionType"; import DBFleet from "../../../types/db/DBFleet"; import { updateFleet } from "../../db/fleet"; import { sendMail } from "../../db/mails"; import { getAllShips } from "../../db/ships"; import getDistanceBetween from "../../utils/getDistanceBetween"; import { getRandomInRange, weightedRandom } from "../../utils/math"; import { Sector } from "./LocationManager"; import { Planet } from "./PlanetManager"; import SystemManager from "./SystemManager"; import { getAllDefenses } from "../../db/defenses"; export type Fleet = { id: ObjectId, source: Planet | SystemManager, destination: Planet | SystemManager | Sector, departureTime: Date, arrivalTime: Date, returning: boolean, mission: MissionType, ships: Array<{ id: string, amount: number }>, cargo: Array<{ id: string, amount: number }>, additionalData?: string } export type BattleFleet = { id: string, hitpoints: number, attack: number, defense: number } export default class FleetManager { data: Fleet; constructor(id: ObjectId, source: Planet | SystemManager, destination: Planet | SystemManager | Sector, departureTime: Date, arrivalTime: Date, returning: boolean, mission: MissionType, ships: Array<{ id: string, amount: number }>, cargo: Array<{ id: string, amount: number }>, additionalData?: string) { this.data = { id, source, destination, departureTime, arrivalTime, returning, mission, ships, cargo, additionalData } } isSourcePlanet(): this is { data: { source: Planet } } { return 'resources' in this.data.source; } isSourceSystem(): this is { data: { source: SystemManager } } { return 'structures' in this.data.source; } isDestinationPlanet(): this is { data: { destination: Planet } } { return 'resources' in this.data.destination; } isDestinationSystem(): this is { data: { destination: SystemManager } } { return 'structures' in this.data.destination; } getSourceOwner() { if(this.isSourcePlanet()) return this.data.source.system.data.ownedBy; if(this.isSourceSystem()) return this.data.source.data.ownedBy; } getDestinationOwner() { if(this.isDestinationPlanet()) return this.data.destination.system.data.ownedBy; if(this.isDestinationSystem()) return this.data.destination.data.ownedBy; } async checkStatus(): Promise<{ finished: boolean, fleet: Fleet }> { if(this.data.arrivalTime.getTime() < Date.now()) { const finished = await this.finish(); return { finished, fleet: this.data }; } return { finished: false, fleet: this.data }; } async finish() { if(this.data.returning) { for(const ship of this.data.ships) { this.data.source.ships.addShips(ship.id, ship.amount); } await this.data.source.resources.updateAmount(this.data.cargo); await this.data.source.ships.sync(); await this.data.source.resources.sync(); await sendMail( null, this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, this.data.arrivalTime, "Fleet Returned", `Your fleet from ${this.data.destination instanceof SystemManager ? `${this.data.destination.data.name} system` : `${this.data.destination.name} ${"expedition" in this.data.destination ? "sector" : "planet"}`} has returned.\n Ships: ${this.data.ships.map(ship => `${ship.amount} ${ship.id}`).join(', ')}\n Cargo: ${this.data.cargo.length > 0 ? this.data.cargo.map(cargo => `${cargo.amount} ${cargo.id}`).join(', ') : 'None'}` ); return true; } else { switch(this.data.mission) { case 'ATTACK': if(!("expedition" in this.data.destination)) { const enemyShips = this.data.destination.ships.ships; return await this.battleResults(enemyShips.map(ship => { return { id: ship.data.id, amount: ship.amount } }), this.data.destination.defenses.defenses.map(defense => { return { id: defense.data.id, amount: defense.amount } })); } else { throw new Error("Cannot attack sector."); } case 'TRANSPORT': await (this.data.destination as Planet | SystemManager).resources.updateAmount(this.data.cargo); await (this.data.destination as Planet | SystemManager).resources.sync(); const cargo = JSON.parse(JSON.stringify(this.data.cargo)) as Array<{ id: string, amount: number }>; this.data.cargo = []; const arrived = new Date(this.data.arrivalTime); await this.initiateReturn(); await sendMail( null, this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, arrived, "Fleet Arrived", `Your fleet has arrived at ${this.data.destination instanceof SystemManager ? `${this.data.destination.data.name} system` : `planet ${this.data.destination.name}`}.\n Ships: ${this.data.ships.map(ship => `${ship.amount} ${ship.id}`).join(', ')}\n Cargo delivered: ${cargo.length > 0 ? cargo.map(cargo => `${cargo.amount} ${cargo.id}`).join(', ') : 'None'}\n Fleet will return at ${this.data.arrivalTime}` ); return false; case 'TRANSFER': await (this.data.destination as Planet | SystemManager).resources.updateAmount(this.data.cargo); await (this.data.destination as Planet | SystemManager).resources.sync(); for(const ship of this.data.ships) { (this.data.destination as Planet | SystemManager).ships.addShips(ship.id, ship.amount); } await (this.data.destination as Planet | SystemManager).ships.sync(); await sendMail( null, this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, this.data.arrivalTime, "Fleet Arrived", `Your fleet has arrived at ${this.data.destination instanceof SystemManager ? `${this.data.destination.data.name} system` : `planet ${this.data.destination.name}`}.\n Ships: ${this.data.ships.map(ship => `${ship.amount} ${ship.id}`).join(', ')}\n Cargo delivered: ${this.data.cargo.length > 0 ? this.data.cargo.map(cargo => `${cargo.amount} ${cargo.id}`).join(', ') : 'None'}\n Ships will stay at the destination.` ); return true; case 'EXPEDITION': await this.expeditionResults(); return false; case 'MINE': const system = this.data.destination as SystemManager; const asteroid = system.asteroids.asteroids.find(a => a.id.equals(this.data.additionalData ?? "")); if(!asteroid) throw new Error("Asteroid not found."); for(const res of asteroid.resources) { const resource = this.data.cargo.find(r => r.id === res.id); if(!resource) this.data.cargo.push(res); else resource.amount += res.amount; } await this.initiateReturn(); await sendMail( null, this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, this.data.arrivalTime, "Asteroid mined", `Your fleet has arrived at AS-${this.data.additionalData} asteroid.\n Following resources were added to the cargo inventory:\n${asteroid.resources.map(r => `${r.id} - ${r.amount}`).join('\n')}\n Fleet will return at ${this.data.arrivalTime}` ); system.asteroids.asteroids = system.asteroids.asteroids.filter(a => !a.id.equals(asteroid.id)); await system.asteroids.sync(); return false; } } } async initiateReturn(isByPlayer: boolean = false) { this.data.returning = true; const elapsedTime = Date.now() - this.data.departureTime.getTime(); this.data.departureTime = isByPlayer ? new Date() : new Date(this.data.arrivalTime); const travelTime = getDistanceBetween(this.data.destination, this.data.source); this.data.arrivalTime = new Date(this.data.departureTime.getTime() + (isByPlayer ? elapsedTime : 1000 * travelTime)); await this.sync(); } private async sendMail(user: ObjectId, title: string, description: string) { await sendMail( null, user, this.data.arrivalTime, title, description ); } async battleResults(enemyFleet: { id: string, amount: number }[], enemyDefenses: { id: string, amount: number }[] = []) { const allShips = await getAllShips(); const allDefenses = await getAllDefenses(); const playerStats = this.data.ships.reduce((acc, ship) => { const dbShip = allShips.find(s => s.id === ship.id); if(!dbShip) return acc; acc.attack += dbShip.structure.attack * ship.amount; acc.defense += dbShip.structure.defense * ship.amount; acc.hitpoints += dbShip.structure.hitpoints * ship.amount; return acc; }, { attack: 0, defense: 0, hitpoints: 0 }); const enemyDefensesStats = enemyDefenses.reduce((acc, defense) => { const dbDefense = allDefenses.find(d => d.id === defense.id); if(!dbDefense) return acc; acc.defense += dbDefense.structure.defense * defense.amount; acc.hitpoints += dbDefense.structure.hitpoints * defense.amount; return acc; }, { attack: 0, defense: 0, hitpoints: 0 }); const enemyStats = enemyFleet.reduce((acc, ship) => { const dbShip = allShips.find(s => s.id === ship.id); if(!dbShip) return acc; acc.attack += dbShip.structure.attack * ship.amount; acc.defense += dbShip.structure.defense * ship.amount; acc.hitpoints += dbShip.structure.hitpoints * ship.amount; return acc; }, enemyDefensesStats); const playerShipsStructure: BattleFleet[] = []; for(const playerShip of this.data.ships) { for(let i = 0; i < playerShip.amount; i++) playerShipsStructure.push({ id: playerShip.id, hitpoints: allShips.find(s => s.id === playerShip.id)?.structure.hitpoints ?? 0, attack: allShips.find(s => s.id === playerShip.id)?.structure.attack ?? 0, defense: allShips.find(s => s.id === playerShip.id)?.structure.defense ?? 0 }); } const enemyShipsStructure: BattleFleet[] = []; for(const enemyShip of enemyFleet) { for(let i = 0; i < enemyShip.amount; i++) enemyShipsStructure.push({ id: enemyShip.id, hitpoints: allShips.find(s => s.id === enemyShip.id)?.structure.hitpoints ?? 0, attack: allShips.find(s => s.id === enemyShip.id)?.structure.attack ?? 0, defense: allShips.find(s => s.id === enemyShip.id)?.structure.defense ?? 0 }); } if(playerStats.attack > enemyStats.defense * 2 && playerStats.defense > enemyStats.attack * 2) { enemyShipsStructure.forEach((_, index) => enemyShipsStructure.splice(index, 1)); } else if(enemyStats.attack > playerStats.defense * 2 && enemyStats.defense > playerStats.attack * 2) { playerShipsStructure.forEach((_, index) => playerShipsStructure.splice(index, 1)); } else { roundLoop: for(let i = 0; i < 3; i++) { for(const playerShip of playerShipsStructure) { const enemyShip = enemyShipsStructure[Math.floor(Math.random() * enemyShipsStructure.length)]; if(!enemyShip) break roundLoop; const typeCount = playerShipsStructure.filter(s => s.id === playerShip.id).length; const additionalShipAttack = typeCount > 1 ? Math.floor(Math.random() * typeCount) * playerShip.attack : 0; const playerDamage = Math.max(0, (playerShip.attack + additionalShipAttack) - enemyShip.defense); enemyShip.hitpoints -= playerDamage; } for(const enemyShip of enemyShipsStructure) { const playerShip = playerShipsStructure[Math.floor(Math.random() * playerShipsStructure.length)]; if(!playerShip) break roundLoop; const typeCount = enemyShipsStructure.filter(s => s.id === enemyShip.id).length; const additionalShipAttack = typeCount > 1 ? Math.floor(Math.random() * typeCount) * enemyShip.attack : 0; const enemyDamage = Math.max(0, (enemyShip.attack + additionalShipAttack) - playerShip.defense); playerShip.hitpoints -= enemyDamage; } playerShipsStructure.forEach((ship, index) => { if(ship.hitpoints <= 0) playerShipsStructure.splice(index, 1); }); enemyShipsStructure.forEach((ship, index) => { if(ship.hitpoints <= 0) enemyShipsStructure.splice(index, 1); }); } } const playerBalance = playerStats.defense - enemyStats.attack; const enemyBalance = enemyStats.defense - playerStats.attack; const playerShipsLeft = playerShipsStructure.reduce((acc, ship) => { acc[ship.id] = (acc[ship.id] ?? 0) + 1; return acc }, {} as { [key: string]: number }); const enemyShipsLeft = enemyShipsStructure.reduce((acc, ship) => { acc[ship.id] = (acc[ship.id] ?? 0) + 1; return acc }, {} as { [key: string]: number }); const resourcesStolen: { id: string, amount: number }[] = []; const previousShips = JSON.parse(JSON.stringify(this.data.ships)) as Array<{ id: string, amount: number }>; if(playerShipsStructure.length > 0) { this.data.ships = Object.keys(playerShipsLeft).map(id => { return { id, amount: playerShipsLeft[id] } }); if(playerBalance > enemyBalance) { const enemyResources = await (this.data.destination as Planet | SystemManager).resources; await enemyResources.calculateCurrentAvailableResources(); let cargoSpaceFree = this.data.ships.reduce((acc, ship) => { const dbShip = allShips.find(s => s.id === ship.id); if(!dbShip) return acc; return acc + dbShip.capacity.solid * ship.amount; }, 0); for(const res of enemyResources.resources) { if(cargoSpaceFree <= 0) break; const amount = Math.min(Math.floor(Math.random() * res.amount), cargoSpaceFree); cargoSpaceFree -= amount; this.data.cargo.push({ id: res.id, amount }); resourcesStolen.push({ id: res.id, amount }); enemyResources.setAmount([{ id: res.id, amount: res.amount - amount }]); }; } await this.initiateReturn(); } else this.data.ships = []; await this.sendMail( this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, `Battle Results (${playerBalance > enemyBalance ? "Victory" : playerBalance < enemyBalance ? "Defeat" : "Draw"})`, `Results of battle at ${(this.data.destination instanceof SystemManager ? this.data.destination.data.name : this.data.destination.name)}:\n Player ships:\n${previousShips.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n Enemy ships:\n${enemyFleet.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n Enemy defenses:\n${enemyDefenses.map(defense => `${defense.amount} ${defense.id}`).join('\n')}\n Player stats: ${playerStats.hitpoints} HP, ${playerStats.attack} ATK, ${playerStats.defense} DEF\n Enemy stats: ${enemyStats.hitpoints} HP, ${enemyStats.attack} ATK, ${enemyStats.defense} DEF\n\n Player ships left:\n${Object.keys(playerShipsLeft).map(key => `${key} - ${playerShipsLeft[key]}`).join('\n')}\n Enemy ships left:\n${Object.keys(enemyShipsLeft).map(key => `${key} - ${enemyShipsLeft[key]}`).join('\n')}\n ${playerBalance > enemyBalance ? `Resources stolen:\n${resourcesStolen.map(res => `${res.id} - ${res.amount}`).join('\n')}` : ""}` ); if(!("expedition" in this.data.destination)) { await this.sendMail( this.data.destination instanceof SystemManager ? this.data.destination.data.ownedBy.id : this.data.destination.system.data.ownedBy.id, `Battle Results (${playerBalance < enemyBalance ? "Victory" : playerBalance > enemyBalance ? "Defeat" : "Draw"})`, `Results of battle at ${(this.data.destination instanceof SystemManager ? this.data.destination.data.name : this.data.destination.name)}:\n Enemy ships:\n${previousShips.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n Player ships:\n${enemyFleet.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n Player defenses:\n${enemyDefenses.map(defense => `${defense.amount} ${defense.id}`).join('\n')}\n Enemy stats: ${playerStats.hitpoints} HP, ${playerStats.attack} ATK, ${playerStats.defense} DEF\n Player stats: ${enemyStats.hitpoints} HP, ${enemyStats.attack} ATK, ${enemyStats.defense} DEF\n\n Enemy ships left:\n${Object.keys(playerShipsLeft).map(key => `${key} - ${playerShipsLeft[key]}`).join('\n')}\n Player ships left:\n${Object.keys(enemyShipsLeft).map(key => `${key} - ${enemyShipsLeft[key]}`).join('\n')}\n ${playerBalance > enemyBalance ? `Resources stolen:\n${resourcesStolen.map(res => `${res.id} - ${res.amount}`).join('\n')}` : ""}` ); } return !(playerShipsStructure.length > 0); } async expeditionResults() { const expeditionRandom = Math.random(); //TODO: make use of "expedition" from DBSector const allShips = await getAllShips(); const shipCount = this.data.ships.reduce((acc, ship) => acc += ship.amount, 0); const totalShipCapacity = this.data.ships.reduce((acc, ship) => acc + (allShips.find(s => s.id === ship.id)?.capacity.solid ?? 0) * ship.amount, 0); const currentCargo = this.data.cargo.reduce((acc, res) => acc + res.amount, 0); const expeditionShipPositiveRandom = weightedRandom(shipCount > 100_000 ? 100_000 : shipCount); const valueAdded = Math.floor(Math.pow(Math.E * expeditionShipPositiveRandom, 4 * Math.PI) + 10) if(expeditionRandom < 0.02) { // 2% chance; lost all ships, black hole this.data.ships = []; await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered a black hole. All ships were lost.`); return; } if(expeditionRandom < 0.1) { // 8% chance; found ships const ships: { id: string, amount: number }[] = []; ships.push({ id: 'transporter', amount: getRandomInRange(0, 20) + Math.floor(valueAdded ) }); ships.push({ id: 'fighter', amount: getRandomInRange(0, 20) + Math.floor(valueAdded ) }); for(const s of ships) { const ship = this.data.ships.find((sh => sh.id === s.id)); if(!ship) this.data.ships.push(s); else ship.amount += s.amount; } await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered abandoned shipyard. Following ships were added to the fleet:\n${ships.map(s => `${s.id} - ${s.amount}`).join(', ')}`); await this.initiateReturn(); return; } if(expeditionRandom < 0.2) { // 10% chance; found resources let totalCapacityUsed = 0; let resources = []; const resToAdd = valueAdded * 3 + currentCargo > totalShipCapacity ? totalShipCapacity / 3 : valueAdded; do { resources = [{ id: "coal", amount: getRandomInRange(0, 10000) + resToAdd }, { id: "iron", amount: getRandomInRange(0, 10000) + resToAdd }, { id: "gold", amount: getRandomInRange(0, 10000) + resToAdd }]; totalCapacityUsed = resources.reduce((acc, res) => acc + res.amount, 0); } while((totalCapacityUsed + currentCargo) > totalShipCapacity); for(const res of resources) { const resource = this.data.cargo.find(r => r.id === res.id); if(!resource) this.data.cargo.push(res); else resource.amount += res.amount; } await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered resource-rich asteroid. Following resources were added to the cargo inventory:\n${resources.map(r => `${r.id} - ${r.amount}`).join('\n')}`); await this.initiateReturn(); return; } if(expeditionRandom < 0.35) { // 15% chance; pirates/aliens const pirates: { id: string, amount: number }[] = [ { id: 'fighter', amount: getRandomInRange(0, 100) + valueAdded }, { id: 'transporter', amount: getRandomInRange(0, 100) + valueAdded } ]; await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered drunk pirates. After attempting to communicate with them, they attacked your fleet.`) await this.battleResults(pirates); return; } if(expeditionRandom < 0.36) { // 1% chance; rich rouge planet const oneResourceMax = totalShipCapacity / 3; let currentCargo = this.data.cargo.reduce((acc, res) => { return acc + res.amount; }, 0); const addedResources: { id: string, amount: number }[] = [{ id: "coal", amount: 0 }, { id: "iron", amount: 0 }, { id: "gold", amount: 0 }]; while(currentCargo < totalShipCapacity) { if((addedResources.find(r => r.id === "coal")?.amount ?? 0) < oneResourceMax) { (addedResources.find(r => r.id === "coal") as { amount: number }).amount++; currentCargo++; if(currentCargo >= totalShipCapacity) break; } if((addedResources.find(r => r.id === "iron")?.amount ?? 0) < oneResourceMax) { (addedResources.find(r => r.id === "iron") as { amount: number }).amount++; currentCargo++; if(currentCargo >= totalShipCapacity) break; } if((addedResources.find(r => r.id === "gold")?.amount ?? 0) < oneResourceMax) { (addedResources.find(r => r.id === "gold") as { amount: number }).amount++; currentCargo++; if(currentCargo >= totalShipCapacity) break; } } for(const res of addedResources) { const resource = this.data.cargo.find(r => r.id === res.id); if(!resource) this.data.cargo.push(res); else resource.amount += res.amount; } await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered resource-rich rouge planet. Your fleet could not extract all resources. Following resources were added to the cargo inventory:\n${addedResources.map(r => `${r.id} - ${r.amount}`).join('\n')}`); await this.initiateReturn(); return; } await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector scanned the sector for a long time, yet it haven't found anything.`); await this.initiateReturn(); return; } async sync() { const source = this.data.source instanceof SystemManager ? this.data.source.data._id : this.data.source._id; const destination = this.data.destination instanceof SystemManager ? this.data.destination.data._id : this.data.destination._id; const data: DBFleet = { _id: this.data.id, source, destination, departureTime: this.data.departureTime, arrivalTime: this.data.arrivalTime, returning: this.data.returning, mission: this.data.mission, ships: this.data.ships, cargo: this.data.cargo, additionalData: this.data.additionalData } await updateFleet(data); } }