Add expedition support

This commit is contained in:
Aelita4 2024-12-09 12:54:11 +01:00
parent 393bcb1960
commit 12f022150c
Signed by: Aelita4
GPG Key ID: E44490C2025906C1
8 changed files with 248 additions and 24 deletions

View File

@ -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;

View File

@ -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;
});

View File

@ -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<DBFleet>;
@ -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<string, DBFleet>();
@ -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());
}

View File

@ -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);
}

26
src/lib/utils/math.ts Normal file
View File

@ -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;
}

View File

@ -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<DBShip>, source: Planet | SystemManager) {
if(typeof ships === "undefined") return {
code: 400,

View File

@ -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 => {
<label for="attack"><input type="radio" name="mission" value="ATTACK" id="attack" required />Attack</label>
<label for="transport"><input type="radio" name="mission" value="TRANSPORT" id="transport" />Transport</label>
<label for="transfer"><input type="radio" name="mission" value="TRANSFER" id="transfer" />Transfer</label>
<label for="expedition"><input type="radio" name="mission" value="EXPEDITION" id="expedition" />Expedition</label>
<label><input type="checkbox" name="toSystem" />Send to system</label>
<hr />
<h2>Send to:</h2>

View File

@ -1,3 +1,3 @@
type MissionType = "TRANSPORT" | "ATTACK" | "TRANSFER";
type MissionType = "TRANSPORT" | "ATTACK" | "TRANSFER" | "EXPEDITION";
export default MissionType;