Add asteroids module

This commit is contained in:
Aelita4 2025-01-25 22:27:06 +01:00
parent ebec253e50
commit 75d936b78a
Signed by: Aelita4
GPG Key ID: E44490C2025906C1
14 changed files with 494 additions and 16 deletions

View File

@ -0,0 +1,20 @@
import Asteroid from '../../../types/Asteroid';
import SystemManager from './SystemManager';
import { updateSystemAsteroids } from '../../db/systems';
export default class AsteroidManager {
asteroids: Array<Asteroid> = [];
system: SystemManager;
constructor(system: SystemManager) {
this.system = system;
}
init(asteroids: Array<Asteroid>) {
this.asteroids = asteroids;
}
async sync() {
await updateSystemAsteroids(this.system.data._id, this.asteroids);
}
}

View File

@ -20,7 +20,8 @@ export type Fleet = {
returning: boolean,
mission: MissionType,
ships: Array<{ id: string, amount: number }>,
cargo: Array<{ id: string, amount: number }>
cargo: Array<{ id: string, amount: number }>,
additionalData?: string
}
export type BattleFleet = {
@ -33,7 +34,7 @@ export type BattleFleet = {
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 }>) {
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,
@ -43,7 +44,8 @@ export default class FleetManager {
returning,
mission,
ships,
cargo
cargo,
additionalData
}
}
@ -148,6 +150,30 @@ export default class FleetManager {
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;
}
}
@ -467,7 +493,8 @@ export default class FleetManager {
returning: this.data.returning,
mission: this.data.mission,
ships: this.data.ships,
cargo: this.data.cargo
cargo: this.data.cargo,
additionalData: this.data.additionalData
}
await updateFleet(data);

View File

@ -94,7 +94,8 @@ class LocationManager {
resources: null,
//@ts-ignore
ships: null,
planets: []
planets: [],
asteroids: [],
};
const s = await new SystemManager(systemObject).fillData(systemData);
@ -139,7 +140,8 @@ class LocationManager {
fleet.returning,
fleet.mission,
fleet.ships,
fleet.cargo
fleet.cargo,
fleet.additionalData
));
}
}

View File

@ -1,4 +1,4 @@
import { ObjectId } from "mongodb";
import { ObjectId, UUID } from "mongodb";
import DBSystem from "../../../types/db/DBSystem";
import { getPlanetById } from "../../db/planets";
import User from "../User";
@ -14,6 +14,8 @@ import PlanetDefenseManager from "./PlanetDefenseManager";
import SystemDefenseManager from "./SystemDefenseManager";
import SystemEnergyManager from "./SystemEnergyManager";
import PlanetEnergyManager from "./PlanetEnergyManager";
import AsteroidManager from "./AsteroidManager";
import Asteroid from "../../../types/Asteroid";
export type System = {
_id: ObjectId,
@ -25,6 +27,7 @@ export type System = {
ships: SystemShipManager,
defenses: SystemDefenseManager,
planets: Planet[];
asteroids: Asteroid[];
}
export default class SystemManager {
@ -34,6 +37,7 @@ export default class SystemManager {
ships: SystemShipManager;
defenses: SystemDefenseManager;
energy: SystemEnergyManager;
asteroids: AsteroidManager;
data: System;
constructor(data: System) {
@ -43,6 +47,7 @@ export default class SystemManager {
this.ships = new SystemShipManager(this);
this.defenses = new SystemDefenseManager(this);
this.energy = new SystemEnergyManager(this);
this.asteroids = new AsteroidManager(this);
}
async fillData(systemData: DBSystem) {
@ -50,6 +55,7 @@ export default class SystemManager {
await this.resources.init(systemData.resources);
await this.ships.init(systemData.ships);
await this.defenses.init(systemData.defenses);
this.asteroids.init(systemData.asteroids);
this.energy.update();
await Promise.all(systemData.planets.map(async planetId => {

View File

@ -1,6 +1,7 @@
import { ObjectId } from "mongodb";
import { Systems } from "./mongodb";
import DBSystem from "../../types/db/DBSystem";
import Asteroid from "../../types/Asteroid";
export const getAllSystems = async () => {
return await (await Systems()).find({}).toArray() as DBSystem[];
@ -51,4 +52,13 @@ export const updateSystemDefenses = async (systemId: ObjectId, defenses: Array<a
defenses
}
});
}
export const updateSystemAsteroids = async (systemId: ObjectId, asteroids: Array<Asteroid>) => {
const systems = await Systems();
await systems.updateOne({ _id: systemId }, {
$set: {
asteroids
}
});
}

View File

@ -1,6 +1,6 @@
export default function parseParams(url: string) {
const rawParams = url.split("?")[1]?.split("&");
if(typeof rawParams === "undefined") return [];
if(typeof rawParams === "undefined") return {};
const params: { [key: string]: any } = {};
for(const rawParam of rawParams) {
const k = rawParam.split("=")[0];

View File

@ -1,7 +1,7 @@
import { APIRoute } from "astro";
import validateAccessToken from "../../../lib/utils/validateAccessToken";
import { getUserByAccessToken } from "../../../lib/db/users";
import { ObjectId } from "mongodb";
import { ObjectId, UUID } from "mongodb";
import locationManager from "../../../lib/classes/managers/LocationManager";
import { getAllShips } from "../../../lib/db/ships";
import DBShip from "../../../types/db/DBShip";
@ -36,7 +36,7 @@ export const POST: APIRoute = async({ request }) => {
)
}
let body: { source: string, destination: string, ships: Array<{ id: string, amount: number }>, cargo: Array<{ id: string, amount: number }>, mission: MissionType };
let body: { source: string, destination: string, ships: Array<{ id: string, amount: number }>, cargo: Array<{ id: string, amount: number }>, mission: MissionType, currentSystem?: string };
try {
body = await request.json()
} catch(e) {
@ -64,11 +64,16 @@ 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;
let dest, additionalData;
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 if(body.mission === "MINE") {
const checkAsteroid = checkAsteroidId(body.destination, body.currentSystem ?? "");
if(typeof checkAsteroid.error !== "undefined") return new Response(JSON.stringify(checkAsteroid), { status: checkAsteroid.code });
dest = checkAsteroid.system;
additionalData = checkAsteroid.asteroid;
} else {
const checkDestination = checkPlanetOrSystemId(body.destination, 'destination');
if(typeof checkDestination.error !== "undefined") return new Response(JSON.stringify(checkDestination), { status: checkDestination.code });
@ -86,7 +91,8 @@ export const POST: APIRoute = async({ request }) => {
false,
body.mission,
body.ships,
body.cargo
body.cargo,
body.mission === "MINE" ? additionalData?.id.toString() : ""
);
const resourceDiff = await source.resources.getDifference(body.cargo.map(c => ({ id: c.id, amount: c.amount })));
@ -123,7 +129,7 @@ export const POST: APIRoute = async({ request }) => {
}
function checkPlanetOrSystemId(id: string, type: string) {
if(typeof ObjectId === "undefined") return {
if(typeof id === "undefined") return {
code: 400,
message: "Bad Request",
error: `Missing '${type}' in body`
@ -155,8 +161,66 @@ function checkPlanetOrSystemId(id: string, type: string) {
}
}
function checkAsteroidId(id: string, systemId: string) {
if(typeof id === "undefined") return {
code: 400,
message: "Bad Request",
error: `Missing 'destination' in body`
}
if(systemId === "") return {
code: 400,
message: "Bad Request",
error: "Missing 'currentSystem' in body"
}
let idToCheck;
try {
idToCheck = new UUID(id);
} catch(e) {
return {
code: 400,
message: "Bad Request",
error: `Invalid UUID in 'destination'`
}
}
let systemObjectId;
try {
systemObjectId = new ObjectId(systemId);
} catch(e) {
return {
code: 400,
message: "Bad Request",
error: "Invalid ID in 'currentSystem'"
}
}
const system = locationManager.getSystem(systemObjectId);
if(!system) return {
code: 404,
message: "Not Found",
error: `Non-existent system provided in 'currentSystem'`
}
const asteroid = system.asteroids.asteroids.find(asteroid => asteroid.id.equals(idToCheck));
if(!asteroid) return {
code: 404,
message: "Not Found",
error: "Non-existent asteroid provided in 'destination'"
}
return {
code: 200,
message: "OK",
asteroid,
system
}
}
function checkSectorId(id: string) {
if(typeof ObjectId === "undefined") return {
if(typeof id === "undefined") return {
code: 400,
message: "Bad Request",
error: "Missing 'sector' in body"

View File

@ -0,0 +1,75 @@
import { type APIRoute } from "astro";
import validateAccessToken from "../../../../lib/utils/validateAccessToken";
import { getUserByAccessToken } from "../../../../lib/db/users";
import locationManager from "../../../../lib/classes/managers/LocationManager";
import { ObjectId, UUID } from "mongodb";
import Asteroid from "../../../../types/Asteroid";
export const POST: APIRoute = async({ request }) => {
const response = await validateAccessToken(request);
if(response instanceof Response) return response;
const user = await getUserByAccessToken(response);
if(user === null) {
return new Response(
JSON.stringify({
code: 401,
message: "Unauthorized"
}), { status: 401 }
)
}
const cookies = request.headers.get("Cookie")?.split(";").map((x) => x.trim().split("=")) ?? [];
const systemId = cookies.filter((x) => x[0] === "currentSystem")[0]?.[1];
const userSystem = locationManager.getSystem(new ObjectId(systemId));
if(!userSystem) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Invalid system ID"
}), { status: 400 }
)
}
if(userSystem.asteroids.asteroids.length >= 5) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "You have reached the maximum number of asteroids in this system"
}), { status: 400 }
)
}
const asteroidUUID = new UUID();
const asteroid: Asteroid = {
id: asteroidUUID,
name: `AS-${asteroidUUID}`,
resources: [
{
id: "coal",
amount: Math.floor(Math.random() * 100) + 1
}, {
id: "iron",
amount: Math.floor(Math.random() * 100) + 1
}, {
id: "gold",
amount: Math.floor(Math.random() * 100) + 1
}
]
}
userSystem.asteroids.asteroids.push(asteroid);
await userSystem.asteroids.sync();
return new Response(
JSON.stringify({
code: 200,
message: "OK",
asteroid
}), { status: 200 }
);
}

View File

@ -0,0 +1,90 @@
---
import { ObjectId } from "mongodb";
import LoggedIn from "../../../../layouts/LoggedIn.astro";
import locationManager from "../../../../lib/classes/managers/LocationManager";
const { token, lang } = Astro.locals;
const currentSystemId = Astro.cookies.get('currentSystem')?.value ?? null;
if(currentSystemId === null) return Astro.redirect('/game/systemManager/select');
const currentSystem = locationManager.getSystem(new ObjectId(currentSystemId));
if(currentSystem === undefined) {
Astro.cookies.delete('currentSystem');
return Astro.redirect('/game/systemManager/select');
}
const discoveredAsteroids = currentSystem.asteroids.asteroids;
---
<LoggedIn id="systemManager" title="System Manager">
<h1>Selected system: {currentSystem.data.name} <a href="/game/systemManager/select">(change)</a></h1>
<div class="system-links">
<a href="/game/systemManager">Overview</a>
<a href="/game/systemManager/structures">System-wide structures</a>
<a href="/game/systemManager/spaceStations">Space stations</a>
<a href="/game/systemManager/asteroids">Asteroids</a>
</div>
<div>
<h2>Avaliable asteroids <button id="scan">Scan for new</button></h2>
<div>
{discoveredAsteroids.map((asteroid) => <div>
<h3>{asteroid.name} <a href={`/game/systemManager/asteroids/send?dest=${asteroid.id}`}>Send</a></h3>
<h4>Resources left: </h4>
{asteroid.resources.map((resource) => <div><p>{resource.id} - {resource.amount}</p></div>)}
</div>)}
</div>
</div>
</LoggedIn>
<style>
* {
color: white;
}
h1 {
text-align: center;
color: white;
}
h1 a {
color: lime;
}
button, a {
color: white;
background-color: #555;
padding: 0.5rem;
border-radius: 5px;
border: none;
cursor: pointer;
}
.system-links {
display: flex;
flex-direction: row;
justify-content: center;
margin-bottom: 1rem;
}
.system-links a {
color: white;
background-color: #555;
padding: 0.5rem;
margin: 0 1rem;
border-radius: 5px;
text-decoration: none;
}
</style>
<script>
async function scan() {
await fetch('/api/system/asteroids/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
window.location.reload();
}
document.getElementById('scan')?.addEventListener('click', scan);
</script>

View File

@ -0,0 +1,165 @@
---
import { ObjectId } from "mongodb";
import ItemCard from "../../../../components/ItemCard.astro";
import LoggedIn from "../../../../layouts/LoggedIn.astro";
import locationManager from "../../../../lib/classes/managers/LocationManager";
import { getName, getObj } from "../../../../lib/utils/langDriver";
import parseParams from "../../../../lib/utils/parseParams";
import { getAllSystems } from "../../../../lib/db/systems";
const { user, token, lang } = Astro.locals;
const currentSystemId = Astro.cookies.get('currentSystem')?.value ?? null;
if(currentSystemId === null) return Astro.redirect('/game/systemManager/select');
const currentSystem = locationManager.getSystem(new ObjectId(currentSystemId));
if(currentSystem === undefined) {
Astro.cookies.delete('currentSystem');
return Astro.redirect('/game/systemManager/select');
}
if(Astro.request.method === "POST") {
const form = await Astro.request.formData();
const source = form.get('toSystem') ?
form.get('source-system')?.toString() :
form.get('source-planet')?.toString();
const fleetData = {
source,
destination: form.get('asteroid-id')?.toString(),
mission: "MINE",
ships: [{ id: "asteroid-miner", amount: 1 }],
cargo: [],
currentSystem: form.get('current-system')?.toString()
}
const response = await fetch(`${Astro.url.origin}/api/fleet/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(fleetData)
});
console.log(await response.json());
return Astro.redirect('/game/systemManager/asteroids');
}
const params = parseParams(Astro.request.url);
const systems = locationManager.getSystemsOwnedBy(user.id).map(sys => { return { id: sys.data._id, name: sys.data.name, hasAsteroidMiner: sys.ships.ships.find(ship => ship.data.id === "asteroid-miner") !== undefined, planets: sys.planets.map(planet => { return { id: planet._id, name: planet.name, hasAsteroidMiner: planet.ships.ships.find(ship => ship.data.id === "asteroid-miner") !== undefined } }) } });
---
<LoggedIn id="systemManager" title="System Manager">
<h1><a href="/game/systemManager/asteroids">&lt;= go back</a>Sending</h1>
<form method="post">
<input type="hidden" name="asteroid-id" value={params.dest} />
<input type="hidden" name="current-system" value={currentSystemId} />
<label><input type="checkbox" name="fromSystem" />Send from system</label>
<h3>System</h3>
<select id="source-system" name="source-system"></select>
<h3>Planet</h3>
<select id="source-planet" name="source-planet"></select>
<hr />
<button type="submit">Send fleet</button>
</form>
</LoggedIn>
<style>
* {
color: white;
}
h1 {
text-align: center;
color: white;
}
h1 a {
color: lime;
}
button {
color: white;
background-color: #555;
padding: 0.5rem;
border-radius: 5px;
border: none;
cursor: pointer;
}
.system-links {
display: flex;
flex-direction: row;
justify-content: center;
margin-bottom: 1rem;
}
a {
color: white;
background-color: #555;
padding: 0.5rem;
margin: 0 1rem;
border-radius: 5px;
text-decoration: none;
}
select {
width: 10rem;
height: 3rem;
color: black;
background-color: darkgray;
}
</style>
<script define:vars={{ systems }}>
const sourceSystem = document.getElementById('source-system');
const sourcePlanet = document.getElementById('source-planet');
const fromSystemCheckbox = document.querySelector('input[name="fromSystem"]');
if(!sourceSystem || !sourcePlanet || !fromSystemCheckbox) {
console.error('Could not find all elements');
return;
}
for(const system of systems) {
const opt = document.createElement('option');
opt.value = system.id;
opt.innerText = system.hasAsteroidMiner ? system.name : `${system.name} (no miner)`;
sourceSystem.appendChild(opt);
}
sourceSystem.addEventListener('change', () => {
const system = systems.find(system => system.id === sourceSystem.value);
if(!system) {
sourcePlanet.innerHTML = '';
const opt = document.createElement('option');
opt.value = '';
opt.innerText = 'No planets';
sourcePlanet.appendChild(opt);
return;
}
sourcePlanet.innerHTML = '';
for(const planet of system.planets) {
const opt = document.createElement('option');
opt.value = planet.id;
opt.innerText = planet.hasAsteroidMiner ? planet.name : `${planet.name} (no miner)`;
sourcePlanet.appendChild(opt);
}
if(sourcePlanet.children.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.innerText = 'No planets';
sourcePlanet.appendChild(opt);
}
});
sourceSystem.dispatchEvent(new Event('change'));
fromSystemCheckbox.addEventListener('change', () => {
sourcePlanet.disabled = fromSystemCheckbox.checked;
});
</script>

10
src/types/Asteroid.ts Normal file
View File

@ -0,0 +1,10 @@
import { UUID } from "mongodb";
export default interface Asteroid {
id: UUID;
name: string;
resources: {
id: string;
amount: number;
}[];
}

View File

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

View File

@ -11,4 +11,5 @@ export default interface DBFleet {
mission: MissionType;
ships: Array<{ id: string, amount: number }>;
cargo: Array<{ id: string, amount: number }>;
additionalData?: string;
}

View File

@ -1,4 +1,4 @@
import { ObjectId } from "mongodb";
import { ObjectId, UUID } from "mongodb";
export default interface DBSystem {
_id: ObjectId;
@ -21,4 +21,12 @@ export default interface DBSystem {
id: string,
amount: number
}>;
asteroids: Array<{
id: UUID,
name: string,
resources: Array<{
id: string,
amount: number
}>
}>;
}