diff --git a/src/lib/classes/managers/FleetManager.ts b/src/lib/classes/managers/FleetManager.ts index 9e89497..731e0a2 100644 --- a/src/lib/classes/managers/FleetManager.ts +++ b/src/lib/classes/managers/FleetManager.ts @@ -5,11 +5,14 @@ import { updateFleet } from "../../db/fleet"; import { Planet } from "./PlanetManager"; import SystemManager, { System } from "./SystemManager"; import { sendMail } from "../../db/mails"; +import { Sector } from "./LocationManager"; +import { getRandomInRange, weightedRandom } from "../../utils/math"; +import { getAllShips } from "../../db/ships"; export type Fleet = { id: ObjectId, source: Planet | SystemManager, - destination: Planet | SystemManager, + destination: Planet | SystemManager | Sector, departureTime: Date, arrivalTime: Date, returning: boolean, @@ -21,7 +24,7 @@ export type Fleet = { export default class FleetManager { data: Fleet; - constructor(id: ObjectId, source: Planet | SystemManager, destination: Planet | SystemManager, departureTime: Date, arrivalTime: Date, returning: boolean, mission: MissionType, ships: Array<{ id: string, amount: number }>, cargo: Array<{ id: string, amount: number }>) { + 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 }>) { this.data = { id, source, @@ -83,7 +86,7 @@ export default class FleetManager { 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` : `planet ${this.data.destination.name}`} has returned.\n + `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'}` ); @@ -93,8 +96,8 @@ export default class FleetManager { case 'ATTACK': return false; case 'TRANSPORT': - await this.data.destination.resources.updateAmount(this.data.cargo); - await this.data.destination.resources.sync(); + 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); @@ -111,12 +114,12 @@ export default class FleetManager { ); return false; case 'TRANSFER': - await this.data.destination.resources.updateAmount(this.data.cargo); - await this.data.destination.resources.sync(); + 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.ships.addShips(ship.id, ship.amount); + (this.data.destination as Planet | SystemManager).ships.addShips(ship.id, ship.amount); } - await this.data.destination.ships.sync(); + 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, @@ -128,6 +131,10 @@ export default class FleetManager { Ships will stay at the destination.` ); return true; + case 'EXPEDITION': + await this.expeditionResults(); + + return false; } } } @@ -139,6 +146,143 @@ export default class FleetManager { await this.sync(); } + private async sendMail(description: string) { + await sendMail( + null, + this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, + this.data.arrivalTime, + "Expedition Results", + description + ); + } + + 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(`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(`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(`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 + //TODO: implement fight mechanic + + await this.sendMail(`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.initiateReturn(); + 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(`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(`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; diff --git a/src/lib/classes/managers/LocationManager.ts b/src/lib/classes/managers/LocationManager.ts index 29f59bc..2a2f6e1 100644 --- a/src/lib/classes/managers/LocationManager.ts +++ b/src/lib/classes/managers/LocationManager.ts @@ -246,7 +246,7 @@ class LocationManager { else found = fleet.data.source.system.data.ownedBy.id.equals(userId); if(fleet.data.destination instanceof SystemManager) found = fleet.data.destination.data.ownedBy.id.equals(userId); - else found = fleet.data.destination.system.data.ownedBy.id.equals(userId); + else if(!("expedition" in fleet.data.destination)) found = fleet.data.destination.system.data.ownedBy.id.equals(userId); return found; }); diff --git a/src/lib/db/fleet.ts b/src/lib/db/fleet.ts index a671cb1..4995461 100644 --- a/src/lib/db/fleet.ts +++ b/src/lib/db/fleet.ts @@ -1,6 +1,6 @@ import { ObjectId } from 'mongodb'; import DBFleet from '../../types/db/DBFleet'; -import { Fleet, Planets } from '../db/mongodb'; +import { Fleet, Planets, Systems } from '../db/mongodb'; export const getAllFleet = async () => { return (await Fleet()).find({}).toArray() as unknown as Array; @@ -8,6 +8,7 @@ export const getAllFleet = async () => { export const getAllFleetByUser = async (userId: ObjectId) => { const planets = await (await Planets()).find({ owner: userId }).toArray(); + const systems = await (await Systems()).find({ ownedBy: userId }).toArray(); const fleets = new Map(); @@ -22,6 +23,17 @@ export const getAllFleetByUser = async (userId: ObjectId) => { } } + for(const system of systems) { + const fleet = await (await Fleet()).find({ $or: [ + { source: system._id }, + { destination: system._id } + ] }).toArray() as unknown as DBFleet[]; + + for(const f of fleet) { + fleets.set(f._id.toString(), f); + } + } + return Array.from(fleets.values()); } diff --git a/src/lib/db/mongodb.ts b/src/lib/db/mongodb.ts index 78f2881..0409886 100644 --- a/src/lib/db/mongodb.ts +++ b/src/lib/db/mongodb.ts @@ -7,17 +7,13 @@ const options = { }; const mongo = new MongoClient(uri, options); - -export const connect = async () => { - await mongo.connect(); -} +await mongo.connect(); export const disconnect = async () => { mongo.close(); } export const getDB = async (dbName = config.MONGODB_DB) => { - await connect(); return mongo.db(dbName); } diff --git a/src/lib/utils/math.ts b/src/lib/utils/math.ts new file mode 100644 index 0000000..fe18e5c --- /dev/null +++ b/src/lib/utils/math.ts @@ -0,0 +1,26 @@ +export function getRandomGaussian(mean: number, stdDev: number) { + let u1 = Math.random(); + let u2 = Math.random(); + let z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2); + return z0 * stdDev + mean; +} + +export function getRandomInRange(min: number, max: number) { + const mean = 0; + const stdDev = 3000; + let randomNum; + + do { + randomNum = getRandomGaussian(mean, stdDev); + } while(randomNum < min || randomNum > max); + + return Math.round(randomNum); +} + +export function weightedRandom(weight: number) { + if (weight < 1 || weight > 100_000) { + throw new Error("Weight must be between 1 and 100000."); + } + + return 1 / (1 + Math.exp(-0.00005 * (weight - 40_000))) * 0.7 + Math.random() * 0.3; +} \ No newline at end of file diff --git a/src/pages/api/fleet/send.ts b/src/pages/api/fleet/send.ts index 5e5016b..8b540b8 100644 --- a/src/pages/api/fleet/send.ts +++ b/src/pages/api/fleet/send.ts @@ -50,12 +50,7 @@ export const POST: APIRoute = async({ request }) => { const checkSource = checkPlanetOrSystemId(body.source, 'source'); if(typeof checkSource.error !== "undefined") return new Response(JSON.stringify(checkSource), { status: checkSource.code }); - - const checkDestination = checkPlanetOrSystemId(body.destination, 'destination'); - if(typeof checkDestination.error !== "undefined") return new Response(JSON.stringify(checkDestination), { status: checkDestination.code }); - const source = checkSource.planetOrSystem; - const destination = checkDestination.planetOrSystem; const shipsDB = await getAllShips(); @@ -68,10 +63,21 @@ export const POST: APIRoute = async({ request }) => { const checkCargoBody = checkCargo(body.cargo, body.ships, source, body.mission); if(typeof checkCargoBody.error !== "undefined") return new Response(JSON.stringify(checkCargoBody), { status: checkCargoBody.code }); + let dest; + if(body.mission === "EXPEDITION") { + const destinationSector = checkSectorId(body.destination); + if(typeof destinationSector.error !== "undefined") return new Response(JSON.stringify(destinationSector), { status: destinationSector.code }); + dest = destinationSector.sector; + } else { + const checkDestination = checkPlanetOrSystemId(body.destination, 'destination'); + if(typeof checkDestination.error !== "undefined") return new Response(JSON.stringify(checkDestination), { status: checkDestination.code }); + dest = checkDestination.planetOrSystem; + } + const fleetManager = new FleetManager( new ObjectId(), source, - destination, + dest, new Date(), new Date(Date.now() + 1000 * 30), //TODO: calculate time based on distance false, @@ -146,6 +152,39 @@ function checkPlanetOrSystemId(id: string, type: string) { } } +function checkSectorId(id: string) { + if(typeof ObjectId === "undefined") return { + code: 400, + message: "Bad Request", + error: "Missing 'sector' in body" + } + + let idToCheck; + try { + idToCheck = new ObjectId(id); + } catch(e) { + return { + code: 400, + message: "Bad Request", + error: "Invalid ID in 'sector'" + } + } + + const sector = locationManager.getSector(idToCheck); + + if(!sector) return { + code: 404, + message: "Not Found", + error: "Non-existent sector provided in 'sector'" + } + + return { + code: 200, + message: "OK", + sector + } +} + function checkShips(ships: Array<{ id: string, amount: number }>, shipsDB: Array, source: Planet | SystemManager) { if(typeof ships === "undefined") return { code: 400, diff --git a/src/pages/game/fleet.astro b/src/pages/game/fleet.astro index f3067f8..7592964 100644 --- a/src/pages/game/fleet.astro +++ b/src/pages/game/fleet.astro @@ -30,9 +30,15 @@ if(Astro.request.method === "POST") { } }); + const destination = form.get('mission') === "EXPEDITION" ? + form.get('destination-sector')?.toString() : + form.get('toSystem') ? + form.get('destination-system')?.toString() : + form.get('destination-planet')?.toString(); + const fleetData = { source: active instanceof SystemManager ? active.data._id : active._id, - destination: form.get('toSystem') ? form.get('destination-system')?.toString() : form.get('destination-planet')?.toString(), + destination, mission: form.get('mission')?.toString() ?? "NULL", ships: ships.map(ship => { const amount = parseInt(form.get(`ship-amount-${ship.id}`)?.toString() ?? "0"); @@ -149,6 +155,7 @@ const sectorsList = galaxies.map(galaxy => { +

Send to:

diff --git a/src/types/MissionType.ts b/src/types/MissionType.ts index 6014d92..9a739f0 100644 --- a/src/types/MissionType.ts +++ b/src/types/MissionType.ts @@ -1,3 +1,3 @@ -type MissionType = "TRANSPORT" | "ATTACK" | "TRANSFER"; +type MissionType = "TRANSPORT" | "ATTACK" | "TRANSFER" | "EXPEDITION"; export default MissionType; \ No newline at end of file