Compare commits

..

8 Commits

Author SHA1 Message Date
Aelita4 cb402476fa
Add asteroids module 2025-01-25 22:27:06 +01:00
Aelita4 ebec253e50
Add one-time research 2025-01-24 19:11:25 +01:00
Aelita4 d4dcc1e759
Implement basic energy system 2025-01-24 19:00:37 +01:00
Aelita4 b1e17b16d3
Add storage system 2025-01-18 22:00:42 +01:00
Aelita4 d6c6327661
Optimize images 2025-01-14 22:34:30 +01:00
Aelita4 5caf20c680
Improve resource bar tooltips 2025-01-14 21:41:27 +01:00
Aelita4 d6520fdee6
Improve resource bar 2025-01-14 18:55:41 +01:00
Aelita4 119cf70e86
Add defense system 2025-01-13 10:49:30 +01:00
51 changed files with 1624 additions and 165 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

View File

@ -1,4 +1,5 @@
--- ---
import { Image } from 'astro:assets';
import { getHighestWeightedLanguage, getLocales, getName } from '../lib/utils/langDriver'; import { getHighestWeightedLanguage, getLocales, getName } from '../lib/utils/langDriver';
interface Props { interface Props {
@ -16,13 +17,15 @@ interface Props {
const lang = await getLocales(Astro.cookies.get('language')?.value ?? await getHighestWeightedLanguage(Astro.request.headers.get('accept-language'))); const lang = await getLocales(Astro.cookies.get('language')?.value ?? await getHighestWeightedLanguage(Astro.request.headers.get('accept-language')));
--- ---
<div class="item-card" data-id={Astro.props.id}> <div class="item-card" data-id={Astro.props.id}>
<img class="item-card-image" src={Astro.props.image} /> <Image class="item-card-image" src={Astro.props.image} alt={Astro.props.id} width={512} height={512} />
<div class="item-card-main-field"> <div class="item-card-main-field">
<form method="post"> <form method="post">
<input type="hidden" name="id" value={Astro.props.id} /> <input type="hidden" name="id" value={Astro.props.id} />
<div class="item-card-name">{Astro.props.name} | {Astro.props.level}</div> <div class="item-card-name">{Astro.props.name} | {Astro.props.level}</div>
<div class="item-card-description">{Astro.props.description} <a href={`/wiki/${Astro.props.category}/${Astro.props.id}`}>[more]</a></div> <div class="item-card-description">{Astro.props.description} <a href={`/wiki/${Astro.props.category}/${Astro.props.id}`}>[more]</a></div>
<input type="submit" class="item-card-build" value={getName(lang, Astro.props.button_type, Astro.props.button_name)} /> {Astro.props.button_name === "nav-researched" ?
<input type="submit" class="item-card-build" value={getName(lang, Astro.props.button_type, "nav-researched")} disabled /> :
<input type="submit" class="item-card-build" value={getName(lang, Astro.props.button_type, "nav-research")} />}
<div class="item-card-info-button">i</div> <div class="item-card-info-button">i</div>
{Astro.props.has_amount_input === "true" && <input type="number" name="amount" />} {Astro.props.has_amount_input === "true" && <input type="number" name="amount" />}
</form> </form>

View File

@ -1,5 +1,6 @@
--- ---
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { Image } from 'astro:assets';
import { checkForUnreadMails } from '../lib/db/mails'; import { checkForUnreadMails } from '../lib/db/mails';
import { getHighestWeightedLanguage, getLocales, getName } from '../lib/utils/langDriver'; import { getHighestWeightedLanguage, getLocales, getName } from '../lib/utils/langDriver';
@ -66,13 +67,20 @@ const listOfElements: Array<NavElement> = [{
url: "/register", url: "/register",
show: "notLoggedInOnly", show: "notLoggedInOnly",
position: "bottom" position: "bottom"
},{ }, {
id: "overview", id: "overview",
title: getName(lang, "general", "nav-overview"), title: getName(lang, "general", "nav-overview"),
type: "simple", type: "simple",
url: "/game", url: "/game",
show: "loggedInOnly", show: "loggedInOnly",
position: "bottom" position: "bottom"
}, {
id: "energy",
title: getName(lang, "general", "nav-energy"),
type: "simple",
url: "/game/energy",
show: "loggedInOnly",
position: "bottom"
}, { }, {
id: "buildings", id: "buildings",
title: getName(lang, "general", "nav-buildings"), title: getName(lang, "general", "nav-buildings"),
@ -101,6 +109,13 @@ const listOfElements: Array<NavElement> = [{
url: "/game/fleet", url: "/game/fleet",
show: "loggedInOnly", show: "loggedInOnly",
position: "bottom" position: "bottom"
},{
id: "defenses",
title: getName(lang, "general", "nav-defenses"),
type: "simple",
url: "/game/defenses",
show: "loggedInOnly",
position: "bottom"
}, { }, {
id: "galaxyView", id: "galaxyView",
title: getName(lang, "general", "nav-galaxy-view"), title: getName(lang, "general", "nav-galaxy-view"),
@ -153,8 +168,8 @@ const hasUnreadMail = await checkForUnreadMails(new ObjectId(userId));
<li class="nav-item"><a href={element.url} class={`nav-url ${active === element.id ? "active" : ""}`}>{element.title}</a></li> <li class="nav-item"><a href={element.url} class={`nav-url ${active === element.id ? "active" : ""}`}>{element.title}</a></li>
)} )}
<li class="nav-item"><span class="nav-username nav-keep-right">{username}</span></li> <li class="nav-item"><span class="nav-username nav-keep-right">{username}</span></li>
{loggedIn === "true" ? <li class="nav-item"><img src="/gargamel.png" class="avatar-icon" /></li> : ""} {loggedIn === "true" ? <li class="nav-item"><Image src="/gargamel.png" alt="profile-pic" width={179} height={178} class="avatar-icon" /></li> : ""}
{loggedIn === "true" ? <li class="nav-item"><a href="/game/mail"><img src={`/mail${hasUnreadMail ? "-notif" : ""}.svg?${new Date().getTime()}`} class="mail-icon" /></a></li> : ""} {loggedIn === "true" ? <li class="nav-item"><a href="/game/mail"><Image src={`/mail${hasUnreadMail ? "-notif" : ""}.svg?${new Date().getTime()}`} alt="mail" width={800} height={800} class="mail-icon" /></a></li> : ""}
</ul> </ul>
</div> </div>
<div class="row"> <div class="row">
@ -218,6 +233,7 @@ nav ul {
} }
.avatar-icon { .avatar-icon {
width: 50px;
height: 50px; height: 50px;
border-radius: 50%; border-radius: 50%;
border: 1px solid white; border: 1px solid white;
@ -226,6 +242,7 @@ nav ul {
} }
.mail-icon { .mail-icon {
width: 50px;
height: 50px; height: 50px;
border-radius: 50%; border-radius: 50%;
/* border: 1px solid white; */ /* border: 1px solid white; */

View File

@ -1,5 +1,6 @@
--- ---
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { Image } from 'astro:assets';
import { getHighestWeightedLanguage, getLocales, getName } from '../lib/utils/langDriver'; import { getHighestWeightedLanguage, getLocales, getName } from '../lib/utils/langDriver';
import { getAllResources } from '../lib/db/resources'; import { getAllResources } from '../lib/db/resources';
import locationManager from '../lib/classes/managers/LocationManager'; import locationManager from '../lib/classes/managers/LocationManager';
@ -18,80 +19,146 @@ if(!planet) return;
await planet.resources.calculateCurrentAvailableResources(); await planet.resources.calculateCurrentAvailableResources();
const resourceArray: Resource[] = []; const resourceArray: (Resource & { capacity: number })[] = [];
for(const key of planet.resources.resources) { for(const key of planet.resources.resources) {
resourceArray.push(key); resourceArray.push({ ...key, capacity: 10_000 });
}
if(!(planet instanceof SystemManager)) {
const mapStorageToResource: { [key: string]: string } = {
"coal-storage": "coal",
"iron-storage": "iron",
"gold-storage": "gold",
"water-tank": "water",
"acid-tank": "sulfuric-acid",
"nitrogen-tank": "liquid-nitrogen",
"hydrogen-tank": "hydrogen",
"oxygen-tank": "oxygen",
"helium3-tank": "helium-3",
}
resourceArray.forEach(res => {
res.capacity = 10_000;
});
for(const building of planet.buildings.buildings) {
if(building.data.category === 'storage') {
const resource = mapStorageToResource[building.data.id];
if(resource) {
const res = resourceArray.find(x => x.id === resource);
if(res) res.capacity += building.level * 10_000;
}
}
}
} }
--- ---
<div id="resourcebar"> <div id="resourcebar">
<div class="resourcebar-item-identifier"> <!-- Energy: <span class={`${planet.energy.consumption > planet.energy.production ? "prod-full" : planet.energy.consumption >= (planet.energy.production * 0.9) ? "prod-almost-full" : ""}`}>{planet.energy.consumption}/{planet.energy.production}</span> -->
<div class="resourcebar-circle-id" data-type="solid"></div> <div class="resourcebar-item">
</div> <Image src="/images/resources/energy.png" alt="energy" class="icon" width={32} height={32} />
<div class="resourcebar-planetname"> <div class="text">
{planet instanceof SystemManager ? <span style="color: red;">{planet.data.name}</span> : planet.name} <span class="line1"><!--{getName(lang, 'resources', 'energy')}-->Energy</span>
</div> <span class={`line2 ${planet.energy.consumption > planet.energy.production ? "prod-full" : planet.energy.consumption >= (planet.energy.production * 0.9) ? "prod-almost-full" : ""}`}>{planet.energy.consumption}/{planet.energy.production}</span>
<div id="resourcebar-elements" class="resourcebar-elements"> </div>
{resourceArray.map(res => <div class="resourcebar-item-tooltip">
<div class="resourcebar-item" <span class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'production')}</span><span class={`resourcebar-item-tooltip-amount`}>{planet.energy.production}</span>
data-res-type={resourceTypes.find(x => x.id === res.id)?.type ?? "solid"} <span class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'consumption')}</span><span class="resourcebar-item-tooltip-amount">{planet.energy.consumption}</span>
data-res-id={res.id} <span class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'balance')}</span><span class={`resourcebar-item-tooltip-amount ${planet.energy.consumption > planet.energy.production ? "prod-full" : planet.energy.consumption >= (planet.energy.production * 0.9) ? "prod-almost-full" : ""}`}>{planet.energy.production - planet.energy.consumption}</span>
data-res-amount={res.amount} </div>
data-res-mining-rate={res.perHourMiningRate}
style={(resourceTypes.find(x => x.id === res.id)?.type ?? "solid") === "solid" ? "" : "display: none;"}
>
<div class="resourcebar-item-icon">
<img src={resourceTypes.find(x => x.id === res.id)?.icon ?? "#"} alt={res.id} />
</div>
<div class="resourcebar-item-text-wrapper" data-resname={res.id}>
<div class="resourcebar-item-text">{getName(lang, 'resources', res.id)}</div>
<div class="resourcebar-item-amount">[fetching]</div>
</div>
<div class="resourcebar-item-tooltip">
<div class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'avaliable')} - <span class="resourcebar-item-tooltip-avaliable">{Math.floor(res.amount).toString()}</span></div>
<div class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'production')} - <span class="resourcebar-item-tooltip-production">{res.perHourMiningRate?.toString() ?? "0"}</span></div>
<div class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'capacity')} - <span class="resourcebar-item-tooltip-capacity">{'21372137'}</span></div>
</div>
</div>
)}
</div> </div>
{resourceArray.map(res =>
<div class="resourcebar-item resourcebar-iterable"
data-res-type={resourceTypes.find(x => x.id === res.id)?.type ?? "solid"}
data-res-id={res.id}
data-res-amount={res.amount}
data-res-mining-rate={res.amount >= res.capacity ? 0 : res.perHourMiningRate}
data-res-capacity={res.capacity}
>
<Image src={resourceTypes.find(x => x.id === res.id)?.icon ?? "#"} alt={res.id} class="icon" width={32} height={32} />
<div class="text" data-resname={res.id}>
<span class="line1">{getName(lang, 'resources', res.id)}</span>
<span class={`line2 ${res.amount >= res.capacity ? "prod-full" : res.amount >= (res.capacity * 0.9) ? "prod-almost-full" : ""}`}></span>
</div>
<div class="resourcebar-item-tooltip">
<span class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'avaliable')}</span><span class="resourcebar-item-tooltip-amount resourcebar-item-tooltip-avaliable">{Math.floor(res.amount).toString()}</span>
<span class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'production')}</span><span class={`resourcebar-item-tooltip-amount resourcebar-item-tooltip-production ${res.amount >= res.capacity ? "prod-full" : res.amount >= (res.capacity * 0.9) ? "prod-almost-full" : ""}`}>{res.amount >= res.capacity ? "0" : res.perHourMiningRate.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") ?? "0"}</span>
<span class="resourcebar-item-tooltip-name">{getName(lang, 'general', 'capacity')}</span><span class="resourcebar-item-tooltip-amount resourcebar-item-tooltip-capacity">{res.capacity.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</span>
</div>
</div>
)}
</div> </div>
<style> <style>
#resourcebar { #resourcebar {
color: white;
background-color: blueviolet;
border-radius: 15px;
margin-top: 20px;
display: flex; display: flex;
flex-direction: row; justify-content: space-between;
align-items: center;
padding: 10px 20px;
background: linear-gradient(135deg, #565656, #4a4a4a);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1000;
} }
.resourcebar-item-identifier { .resourcebar-item {
left: 25px;
flex-shrink: 0;
margin-left: 25px;
}
.resourcebar-circle-id {
width: 50px;
height: 50px;
background-color: #0f0;
border-radius: 25px;
margin-right: 8px;
margin-top: 8px;
margin-bottom: 8px;
}
.resourcebar-planetname {
margin-top: 8px;
font-size: 2.5em;
margin-left: 25px;
}
.resourcebar-elements {
flex-grow: 1;
display: flex; display: flex;
flex-direction: row; align-items: center;
justify-content: space-evenly; margin: 0 10px;
}
.icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
.text {
display: flex;
flex-direction: column;
}
.line1 {
font-size: 14px;
font-weight: bold;
color: #fff;
}
.line2 {
font-size: 12px;
color: #b0b0b0;
}
@media (max-width: 768px) {
.status-bar {
padding: 10px 15px;
}
.icon {
width: 20px;
height: 20px;
}
.line1 {
font-size: 12px;
}
.line2 {
font-size: 10px;
}
}
@media (max-width: 480px) {
.status-bar {
flex-direction: column;
align-items: flex-start;
}
.status-item {
width: 100%;
margin: 5px 0;
}
} }
.resourcebar-item { .resourcebar-item {
@ -110,30 +177,44 @@ for(const key of planet.resources.resources) {
margin-bottom: 8px; margin-bottom: 8px;
} }
.resourcebar-item-icon img {
width: 100%;
}
.resourcebar-item-text-wrapper { .resourcebar-item-text-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.resourcebar-item-text { .resourcebar-item-tooltip-name {
font-size: 1.5em; font-size: 16px;
color: #b0b0b0;
margin-left: 5px;
margin-right: 5px;
margin-top: 5px;
} }
.resourcebar-item-amount { .resourcebar-item-tooltip-amount {
font-size: 1.2em; font-size: 14px;
color: #fff;
margin-left: 5px;
margin-bottom: 5px;
}
.prod-almost-full {
color: #ff9900;
}
.prod-full {
color: #ff0000;
} }
.resourcebar-item .resourcebar-item-tooltip { .resourcebar-item .resourcebar-item-tooltip {
position: absolute; position: absolute;
background-color: gray; display: flex;
margin-top: 70px; flex-direction: column;
width: 150%; color: #b0b0b0;
background-color: rgb(58, 58, 58);
margin-top: 220px;
border-radius: 10px; border-radius: 10px;
min-width: 134px;
transition: visibility 1s, opacity 1s; transition: visibility 1s, opacity 1s;
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
@ -153,55 +234,35 @@ for(const key of planet.resources.resources) {
return x.toString(); return x.toString();
} }
setInterval(() => { function numWithSeparator(x: number) {
const resourcebarItems = document.getElementById('resourcebar')?.querySelectorAll('.resourcebar-item'); x = Math.floor(x);
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function init() {
const resourcebarItems = document.getElementById('resourcebar')?.querySelectorAll('.resourcebar-iterable');
resourcebarItems?.forEach((item: any) => { resourcebarItems?.forEach((item: any) => {
const resourceAmount = parseFloat(item.dataset.resAmount ?? '0'); const resourceAmount = parseFloat(item.dataset.resAmount ?? '0');
const miningRate = parseInt(item.dataset.resMiningRate ?? '0') / 3600; const miningRate = parseInt(item.dataset.resMiningRate ?? '0') / 3600;
const newAmount = resourceAmount + miningRate; const newAmount = resourceAmount + miningRate;
item.dataset.resAmount = newAmount.toString(); item.dataset.resAmount = newAmount.toString();
(item.querySelector('.resourcebar-item-amount') as HTMLElement).innerHTML = numWithPrefix(newAmount); (item.querySelector('.line2') as HTMLElement).innerHTML = numWithPrefix(newAmount);
(item.querySelector('.resourcebar-item-tooltip .resourcebar-item-tooltip-avaliable') as HTMLElement).innerHTML = Math.floor(newAmount).toString(); (item.querySelector('.resourcebar-item-tooltip-avaliable') as HTMLElement).innerHTML = numWithSeparator(newAmount);
});
}, 1_000);
document.querySelector(".resourcebar-item-identifier")?.addEventListener("click", () => { if(newAmount >= parseFloat(item.dataset.resCapacity ?? '0')) {
switch((document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement)?.dataset.type) { (item.querySelector('.line2') as HTMLElement).classList.remove('prod-almost-full');
case "solid": (item.querySelector('.line2') as HTMLElement).classList.add('prod-full');
(document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement).dataset.type = "liquid"; (item.querySelector('.resourcebar-item-tooltip-production') as HTMLElement).classList.remove('prod-almost-full');
(document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement).style.backgroundColor = "#00f"; (item.querySelector('.resourcebar-item-tooltip-production') as HTMLElement).classList.add('prod-full');
(document.getElementById("resourcebar-elements")?.querySelectorAll(".resourcebar-item") as NodeListOf<HTMLElement>).forEach(item => { item.dataset.resMiningRate = '0';
if(item.dataset.resType === "liquid") { } else if(newAmount >= parseFloat(item.dataset.resCapacity ?? '0') * 0.9) {
item.style.display = ""; (item.querySelector('.line2') as HTMLElement).classList.add('prod-almost-full');
} else { (item.querySelector('.resourcebar-item-tooltip-production') as HTMLElement).classList.add('prod-almost-full');
item.style.display = "none"; }
} });
}); }
break;
case "liquid": init();
(document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement).dataset.type = "gas"; setInterval(init, 1_000);
(document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement).style.backgroundColor = "#ff0";
(document.getElementById("resourcebar-elements")?.querySelectorAll(".resourcebar-item") as NodeListOf<HTMLElement>).forEach(item => {
if(item.dataset.resType === "gas") {
item.style.display = "";
} else {
item.style.display = "none";
}
});
break;
case "gas":
(document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement).dataset.type = "solid";
(document.querySelector(".resourcebar-item-identifier")?.children[0] as HTMLElement).style.backgroundColor = "#0f0";
(document.getElementById("resourcebar-elements")?.querySelectorAll(".resourcebar-item") as NodeListOf<HTMLElement>).forEach(item => {
if(item.dataset.resType === "solid") {
item.style.display = "";
} else {
item.style.display = "none";
}
});
break;
}
});
</script> </script>

View File

@ -21,14 +21,21 @@ const { title } = Astro.props;
<title>{title}</title> <title>{title}</title>
</head> </head>
<body> <body>
<NavBar loggedIn="true" active={Astro.props.id} /> <div class="bars">
<ResourceBar /> <NavBar loggedIn="true" active={Astro.props.id} />
<ResourceBar />
</div>
<slot /> <slot />
</body> </body>
</html> </html>
<style> <style>
html { html {
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;
background: #13151a; background: #13151a;
} width: 100%;
}
.bars {
display: inline;
}
</style> </style>

View File

@ -5,11 +5,13 @@ export default class Building {
manager: BuildingManager manager: BuildingManager
data: DBBuilding; data: DBBuilding;
level: number; level: number;
activePercent: number = 100;
constructor(manager: BuildingManager, data: DBBuilding, level: number) { constructor(manager: BuildingManager, data: DBBuilding, level: number, activePercent?: number) {
this.manager = manager; this.manager = manager;
this.data = data; this.data = data;
this.level = level; this.level = level;
if(activePercent !== undefined) this.activePercent = activePercent;
} }
async checkRequiredResources(level: number): Promise<boolean> { async checkRequiredResources(level: number): Promise<boolean> {
@ -38,7 +40,7 @@ export default class Building {
const playerBuildings = this.manager.buildings; const playerBuildings = this.manager.buildings;
let playerBuildingsCanBuild = { canBuild: true, missing: "" }; let playerBuildingsCanBuild = { canBuild: true, missing: "" };
this.data.requirements.buildings.forEach((buildingReq: any) => { this.data.requirements.buildings.forEach((buildingReq: any) => {
if(playerBuildings.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0 < buildingReq.level) { if((playerBuildings.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0) < buildingReq.level) {
playerBuildingsCanBuild = { canBuild: false, missing: `${buildingReq.id} level ${buildingReq.level} required, found ${playerBuildings.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0}` }; playerBuildingsCanBuild = { canBuild: false, missing: `${buildingReq.id} level ${buildingReq.level} required, found ${playerBuildings.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0}` };
return; return;
} }

View File

@ -0,0 +1,84 @@
import DBDefenses from "../../types/db/DBDefenses";
import DefenseManager from "./managers/abstract/DefenseManager";
import SystemManager from "./managers/SystemManager";
export default class Defense {
manager: DefenseManager;
data: DBDefenses;
amount: number;
constructor(manager: DefenseManager, data: DBDefenses, amount: number) {
this.manager = manager;
this.data = data;
this.amount = amount;
}
async checkRequiredResources(): Promise<boolean> {
const resources = await this.manager.manager.resources.calculateCurrentAvailableResources();
const requirements = this.data.requirements.resources;
let canBuild = true;
for(let res of requirements) {
const resource = resources.find(r => r.id === res.id);
if(!resource) return false;
if(resource.amount < (res.amount * this.amount)) {
canBuild = false;
break;
}
}
return canBuild;
}
async checkRequirements(): Promise<{ canBuild: boolean, error: string }> {
if(!(this.manager.manager instanceof SystemManager)) {
const playerBuildings = this.manager.manager.buildings.buildings;
let playerBuildingsCanBuild = { canBuild: true, missing: "" };
this.data.requirements.buildings.forEach((buildingReq: any) => {
if((playerBuildings.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0) < buildingReq.level) {
playerBuildingsCanBuild = { canBuild: false, missing: `${buildingReq.id} level ${buildingReq.level} required, found ${playerBuildings.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0}` };
return;
}
});
if(!playerBuildingsCanBuild.canBuild) return {
canBuild: false,
error: playerBuildingsCanBuild.missing
}
} /*else { //TODO: check for structures requirements
const structures = this.manager.manager.structures.structures;
let playerStructuresCanBuild = { canBuild: true, missing: "" };
this.data.requirements.structures.forEach((buildingReq: any) => {
if((structures.filter((building) => building.data.id === buildingReq.id)[0]?.level ?? 0) < buildingReq.level) {
playerStructuresCanBuild = { canBuild: false, missing: `${buildingReq.id} level ${buildingReq.level} required, found ${structures.filter((structure) => structure.data.id === buildingReq.id)[0]?.level ?? 0}` };
return;
}
});
if(!playerStructuresCanBuild.canBuild) return {
canBuild: false,
error: playerStructuresCanBuild.missing
}
}*/
// research
const playerResearch = this.manager.manager instanceof SystemManager ? this.manager.manager.data.ownedBy.research : this.manager.manager.system.data.ownedBy.research;
let playerResearchCanBuild = { canBuild: true, missing: "" };
for(const researchReq of this.data.requirements.research) {
if(playerResearch.research.find((research) => research.id === researchReq.id)?.level ?? 0 < researchReq.level) {
playerResearchCanBuild = { canBuild: false, missing: `${researchReq.id} level ${researchReq.level} required, found ${playerResearch.research.find((research) => research.id === researchReq.id)?.level ?? 0}` };
}
};
if(!playerResearchCanBuild.canBuild) return {
canBuild: false,
error: playerResearchCanBuild.missing
}
return {
canBuild: true,
error: ""
}
}
}

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

@ -9,6 +9,7 @@ import { getRandomInRange, weightedRandom } from "../../utils/math";
import { Sector } from "./LocationManager"; import { Sector } from "./LocationManager";
import { Planet } from "./PlanetManager"; import { Planet } from "./PlanetManager";
import SystemManager from "./SystemManager"; import SystemManager from "./SystemManager";
import { getAllDefenses } from "../../db/defenses";
export type Fleet = { export type Fleet = {
id: ObjectId, id: ObjectId,
@ -19,7 +20,8 @@ export type Fleet = {
returning: boolean, returning: boolean,
mission: MissionType, mission: MissionType,
ships: Array<{ id: string, amount: number }>, ships: Array<{ id: string, amount: number }>,
cargo: Array<{ id: string, amount: number }> cargo: Array<{ id: string, amount: number }>,
additionalData?: string
} }
export type BattleFleet = { export type BattleFleet = {
@ -32,7 +34,7 @@ export type BattleFleet = {
export default class FleetManager { export default class FleetManager {
data: Fleet; 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 = { this.data = {
id, id,
source, source,
@ -42,7 +44,8 @@ export default class FleetManager {
returning, returning,
mission, mission,
ships, ships,
cargo cargo,
additionalData
} }
} }
@ -104,7 +107,7 @@ export default class FleetManager {
case 'ATTACK': case 'ATTACK':
if(!("expedition" in this.data.destination)) { if(!("expedition" in this.data.destination)) {
const enemyShips = this.data.destination.ships.ships; const enemyShips = this.data.destination.ships.ships;
return await this.battleResults(enemyShips.map(ship => { return { id: ship.data.id, amount: ship.amount } })); 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 { } else {
throw new Error("Cannot attack sector."); throw new Error("Cannot attack sector.");
} }
@ -147,6 +150,30 @@ export default class FleetManager {
case 'EXPEDITION': case 'EXPEDITION':
await this.expeditionResults(); 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; return false;
} }
} }
@ -161,18 +188,19 @@ export default class FleetManager {
await this.sync(); await this.sync();
} }
private async sendMail(title: string, description: string) { private async sendMail(user: ObjectId, title: string, description: string) {
await sendMail( await sendMail(
null, null,
this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, user,
this.data.arrivalTime, this.data.arrivalTime,
title, title,
description description
); );
} }
async battleResults(enemyFleet: { id: string, amount: number }[]) { async battleResults(enemyFleet: { id: string, amount: number }[], enemyDefenses: { id: string, amount: number }[] = []) {
const allShips = await getAllShips(); const allShips = await getAllShips();
const allDefenses = await getAllDefenses();
const playerStats = this.data.ships.reduce((acc, ship) => { const playerStats = this.data.ships.reduce((acc, ship) => {
const dbShip = allShips.find(s => s.id === ship.id); const dbShip = allShips.find(s => s.id === ship.id);
@ -183,6 +211,14 @@ export default class FleetManager {
return acc; return acc;
}, { attack: 0, defense: 0, hitpoints: 0 }); }, { 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 enemyStats = enemyFleet.reduce((acc, ship) => {
const dbShip = allShips.find(s => s.id === ship.id); const dbShip = allShips.find(s => s.id === ship.id);
if(!dbShip) return acc; if(!dbShip) return acc;
@ -190,7 +226,7 @@ export default class FleetManager {
acc.defense += dbShip.structure.defense * ship.amount; acc.defense += dbShip.structure.defense * ship.amount;
acc.hitpoints += dbShip.structure.hitpoints * ship.amount; acc.hitpoints += dbShip.structure.hitpoints * ship.amount;
return acc; return acc;
}, { attack: 0, defense: 0, hitpoints: 0 }); }, enemyDefensesStats);
const playerShipsStructure: BattleFleet[] = []; const playerShipsStructure: BattleFleet[] = [];
for(const playerShip of this.data.ships) { for(const playerShip of this.data.ships) {
@ -282,10 +318,12 @@ export default class FleetManager {
} else this.data.ships = []; } else this.data.ships = [];
await this.sendMail( 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"})`, `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 `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 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\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 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 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 Player ships left:\n${Object.keys(playerShipsLeft).map(key => `${key} - ${playerShipsLeft[key]}`).join('\n')}\n
@ -293,6 +331,22 @@ export default class FleetManager {
${playerBalance > enemyBalance ? `Resources stolen:\n${resourcesStolen.map(res => `${res.id} - ${res.amount}`).join('\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); return !(playerShipsStructure.length > 0);
} }
@ -309,7 +363,7 @@ export default class FleetManager {
if(expeditionRandom < 0.02) { // 2% chance; lost all ships, black hole if(expeditionRandom < 0.02) { // 2% chance; lost all ships, black hole
this.data.ships = []; this.data.ships = [];
await this.sendMail("Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered a black hole. All ships were lost.`); 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; return;
} }
@ -324,7 +378,7 @@ export default class FleetManager {
else ship.amount += s.amount; else ship.amount += s.amount;
} }
await this.sendMail("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.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(); await this.initiateReturn();
return; return;
} }
@ -356,7 +410,7 @@ export default class FleetManager {
else resource.amount += res.amount; else resource.amount += res.amount;
} }
await this.sendMail("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.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(); await this.initiateReturn();
return; return;
} }
@ -367,7 +421,7 @@ export default class FleetManager {
{ id: 'transporter', amount: getRandomInRange(0, 100) + valueAdded } { id: 'transporter', amount: getRandomInRange(0, 100) + valueAdded }
]; ];
await this.sendMail("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.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); await this.battleResults(pirates);
return; return;
} }
@ -416,12 +470,12 @@ export default class FleetManager {
else resource.amount += res.amount; else resource.amount += res.amount;
} }
await this.sendMail("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.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(); await this.initiateReturn();
return; return;
} }
await this.sendMail("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.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(); await this.initiateReturn();
return; return;
} }
@ -439,7 +493,8 @@ export default class FleetManager {
returning: this.data.returning, returning: this.data.returning,
mission: this.data.mission, mission: this.data.mission,
ships: this.data.ships, ships: this.data.ships,
cargo: this.data.cargo cargo: this.data.cargo,
additionalData: this.data.additionalData
} }
await updateFleet(data); await updateFleet(data);

View File

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

View File

@ -0,0 +1,17 @@
import { updatePlanetDefenses } from "../../db/planets";
import { Planet } from "./PlanetManager";
import DefenseManager from "./abstract/DefenseManager";
export default class PlanetDefenseManager extends DefenseManager {
constructor(planet: Planet) {
super(planet);
}
get manager() {
return this._manager as Planet;
}
async sync() {
await updatePlanetDefenses(this.manager._id, this.defenses.map(def => { return { id: def.data.id, amount: def.amount } }));
}
}

View File

@ -0,0 +1,29 @@
import { Planet } from "./PlanetManager";
import EnergyManager from "./abstract/EnergyManager";
export default class PlanetEnergyManager extends EnergyManager {
constructor(system: Planet) {
super(system);
}
get manager() {
return this._manager as Planet;
}
update() {
const buildings = this.manager.buildings.buildings;
this.production = 0;
this.consumption = 0;
for(const building of buildings) {
if(building.data.category === "power-plants") {
this.production += (building.data.energy * building.level);
} else {
this.consumption += (building.data.energy * building.level);
}
}
return this;
}
}

View File

@ -3,6 +3,8 @@ import BuildingManager from "./BuildingManager";
import PlanetResourceManager from "./PlanetResourceManager"; import PlanetResourceManager from "./PlanetResourceManager";
import ShipManager from "./PlanetShipManager"; import ShipManager from "./PlanetShipManager";
import SystemManager from "./SystemManager"; import SystemManager from "./SystemManager";
import PlanetDefenseManager from "./PlanetDefenseManager";
import PlanetEnergyManager from "./PlanetEnergyManager";
export type Planet = { export type Planet = {
_id: ObjectId; _id: ObjectId;
@ -12,4 +14,6 @@ export type Planet = {
resources: PlanetResourceManager; resources: PlanetResourceManager;
buildings: BuildingManager; buildings: BuildingManager;
ships: ShipManager; ships: ShipManager;
defenses: PlanetDefenseManager;
energy: PlanetEnergyManager;
} }

View File

@ -91,6 +91,33 @@ export default class PlanetResourceManager extends ResourceManager {
return this; return this;
} }
getStorageCapacities() {
const mapStorageToResource: { [key: string]: string } = {
"coal-storage": "coal",
"iron-storage": "iron",
"gold-storage": "gold",
"water-tank": "water",
"acid-tank": "sulfuric-acid",
"nitrogen-tank": "liquid-nitrogen",
"hydrogen-tank": "hydrogen",
"oxygen-tank": "oxygen",
"helium3-tank": "helium-3",
}
const output: { id: string, capacity: number }[] = [];
for(const building of this.planet.buildings.buildings) {
if(building.data.category === 'storage') {
const resource = mapStorageToResource[building.data.id];
if(resource) {
output.push({ id: resource, capacity: building.level * 10_000 });
}
}
}
return output;
}
async sync() { async sync() {
await updatePlanetResources(this.planet._id, this.resources.map(res => { await updatePlanetResources(this.planet._id, this.resources.map(res => {
return { return {

View File

@ -0,0 +1,17 @@
import { updateSystemDefenses } from "../../db/systems";
import SystemManager from "./SystemManager";
import DefenseManager from "./abstract/DefenseManager";
export default class SystemDefenseManager extends DefenseManager {
constructor(system: SystemManager) {
super(system);
}
get manager() {
return this._manager as SystemManager;
}
async sync() {
await updateSystemDefenses(this.manager.data._id, this.defenses.map(def => { return { id: def.data.id, amount: def.amount } }));
}
}

View File

@ -0,0 +1,26 @@
import EnergyManager from "./abstract/EnergyManager";
import SystemManager from "./SystemManager";
export default class SystemEnergyManager extends EnergyManager {
constructor(system: SystemManager) {
super(system);
}
get manager() {
return this._manager as SystemManager;
}
update() {
const structures = this.manager.structures.structures;
for(const structure of structures) {
if(structure.data.id === "dyson-sphere") {
this.production += (structure.data.energy * structure.level);
} else {
this.consumption += (structure.data.energy * structure.level);
}
}
return this;
}
}

View File

@ -1,4 +1,4 @@
import { ObjectId } from "mongodb"; import { ObjectId, UUID } from "mongodb";
import DBSystem from "../../../types/db/DBSystem"; import DBSystem from "../../../types/db/DBSystem";
import { getPlanetById } from "../../db/planets"; import { getPlanetById } from "../../db/planets";
import User from "../User"; import User from "../User";
@ -10,6 +10,12 @@ import PlanetShipManager from "./PlanetShipManager";
import StructureManager from "./StructureManager"; import StructureManager from "./StructureManager";
import SystemResourceManager from "./SystemResourceManager"; import SystemResourceManager from "./SystemResourceManager";
import SystemShipManager from "./SystemShipManager"; import SystemShipManager from "./SystemShipManager";
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 = { export type System = {
_id: ObjectId, _id: ObjectId,
@ -19,7 +25,9 @@ export type System = {
structures: StructureManager, structures: StructureManager,
resources: SystemResourceManager, resources: SystemResourceManager,
ships: SystemShipManager, ships: SystemShipManager,
defenses: SystemDefenseManager,
planets: Planet[]; planets: Planet[];
asteroids: Asteroid[];
} }
export default class SystemManager { export default class SystemManager {
@ -27,6 +35,9 @@ export default class SystemManager {
structures: StructureManager; structures: StructureManager;
resources: SystemResourceManager; resources: SystemResourceManager;
ships: SystemShipManager; ships: SystemShipManager;
defenses: SystemDefenseManager;
energy: SystemEnergyManager;
asteroids: AsteroidManager;
data: System; data: System;
constructor(data: System) { constructor(data: System) {
@ -34,12 +45,18 @@ export default class SystemManager {
this.structures = new StructureManager(this); this.structures = new StructureManager(this);
this.resources = new SystemResourceManager(this); this.resources = new SystemResourceManager(this);
this.ships = new SystemShipManager(this); this.ships = new SystemShipManager(this);
this.defenses = new SystemDefenseManager(this);
this.energy = new SystemEnergyManager(this);
this.asteroids = new AsteroidManager(this);
} }
async fillData(systemData: DBSystem) { async fillData(systemData: DBSystem) {
await this.structures.init(systemData.structures); await this.structures.init(systemData.structures);
await this.resources.init(systemData.resources); await this.resources.init(systemData.resources);
await this.ships.init(systemData.ships); 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 => { await Promise.all(systemData.planets.map(async planetId => {
const planet = await getPlanetById(planetId); const planet = await getPlanetById(planetId);
@ -54,12 +71,18 @@ export default class SystemManager {
//@ts-ignore //@ts-ignore
buildings: null, buildings: null,
//@ts-ignore //@ts-ignore
ships: null ships: null,
//@ts-ignore
defenses: null,
//@ts-ignore
energy: null
} }
planetObject.resources = await new PlanetResourceManager(planetObject).init(planet.resources); planetObject.resources = await new PlanetResourceManager(planetObject).init(planet.resources);
planetObject.buildings = await new BuildingManager(planetObject).init(planet.buildings); planetObject.buildings = await new BuildingManager(planetObject).init(planet.buildings);
planetObject.ships = await new PlanetShipManager(planetObject).init(planet.ships); planetObject.ships = await new PlanetShipManager(planetObject).init(planet.ships);
planetObject.defenses = await new PlanetDefenseManager(planetObject).init(planet.defenses);
planetObject.energy = new PlanetEnergyManager(planetObject).update();
this.planets.push(planetObject); this.planets.push(planetObject);
})); }));

View File

@ -73,7 +73,14 @@ export default class SystemResourceManager extends ResourceManager {
return this; return this;
} }
getStorageCapacities(): { id: string; capacity: number; }[] {
return this.resources.map(res => {
return {
id: res.id,
capacity: 10_000 //TODO: add structure for storage
}
});
}
async sync() { async sync() {
await updateSystemResources(this.system.data._id, this.resources.map(res => { await updateSystemResources(this.system.data._id, this.resources.map(res => {

View File

@ -0,0 +1,62 @@
import { Planet } from '../PlanetManager';
import SystemManager from '../SystemManager';
import Defense from '../../Defense';
import DBDefenses from '../../../../types/db/DBDefenses';
import { getAllDefenses } from '../../../db/defenses';
export default abstract class DefenseManager {
defenses: Array<Defense> = [];
defensesDB: Array<DBDefenses> = [];
protected _manager: Planet | SystemManager;
constructor(manager: Planet | SystemManager) {
this._manager = manager;
}
abstract sync(): Promise<void>;
abstract get manager(): Planet | SystemManager;
async init(defenseData: { id: string, amount: number }[]) {
this.defensesDB = await getAllDefenses();
for(const def of defenseData) {
const defToFind = this.defensesDB.find(d => d.id === def.id);
if(defToFind) this.defenses.push(new Defense(
this,
defToFind,
def.amount
))
}
return this;
}
getDefenseById(id: string) {
return this.defenses.find(def => def.data.id === id);
}
addDefenses(id: string, amount: number) {
const findDef = this.defenses.find(d => d.data.id === id);
if(!findDef) {
const defData = this.defensesDB.find(d => d.id === id);
if(!defData) return;
this.defenses.push(new Defense(
this,
defData,
amount
));
}
else findDef.amount += amount;
}
removeDefenses(id: string, amount: number) {
const findDef = this.defenses.find(d => d.data.id === id);
if(findDef) {
findDef.amount -= amount;
if(findDef.amount <= 0) this.defenses.splice(this.defenses.indexOf(findDef), 1);
}
}
}

View File

@ -0,0 +1,14 @@
import { Planet } from "../PlanetManager";
import SystemManager from "../SystemManager";
export default abstract class EnergyManager {
protected _manager: Planet | SystemManager;
production: number = 0;
consumption: number = 0;
constructor(manager: Planet | SystemManager) {
this._manager = manager;
}
abstract update(): this;
}

View File

@ -14,17 +14,23 @@ export default abstract class ResourceManager {
abstract sync(): Promise<void>; abstract sync(): Promise<void>;
abstract getStorageCapacities(): { id: string, capacity: number }[];
getResourceById(resId: string) { getResourceById(resId: string) {
return this.resources.find(res => res.id === resId); return this.resources.find(res => res.id === resId);
} }
async calculateCurrentAvailableResources() { async calculateCurrentAvailableResources() {
const storage = this.getStorageCapacities();
for(const res of this.resources) { for(const res of this.resources) {
if(!res.lastUpdated || !res.perHourMiningRate) continue; if(!res.lastUpdated || !res.perHourMiningRate) continue;
const maxStorage = 10_000 + (storage.find(s => s.id === res.id)?.capacity ?? 0);
const timeDiff = Math.abs((new Date()).getTime() - res.lastUpdated.getTime()); const timeDiff = Math.abs((new Date()).getTime() - res.lastUpdated.getTime());
const hours = timeDiff / (1000 * 60 * 60); const hours = timeDiff / (1000 * 60 * 60);
const amountToAdd = hours * res.perHourMiningRate; let amountToAdd = hours * res.perHourMiningRate;
if(res.amount + amountToAdd > maxStorage) amountToAdd = Math.max(0, maxStorage - res.amount);
res.amount += amountToAdd; res.amount += amountToAdd;
res.lastUpdated = new Date(); res.lastUpdated = new Date();
}; };

12
src/lib/db/defenses.ts Normal file
View File

@ -0,0 +1,12 @@
import DBDefenses from '../../types/db/DBDefenses';
import { Defenses } from '../db/mongodb';
export const getAllDefenses = async () => {
return (await Defenses()).find({}).toArray() as unknown as Array<DBDefenses>;
}
export const getDefenseById = async (id: string) => {
return (await Defenses()).findOne({
id
}) as unknown as DBDefenses;
}

View File

@ -8,6 +8,7 @@ export const getLang = async (language = "en") => {
ships: (await lang[2].find({}).toArray()).map(({ _id, ...rest }) => rest), ships: (await lang[2].find({}).toArray()).map(({ _id, ...rest }) => rest),
resources: (await lang[3].find({}).toArray()).map(({ _id, ...rest }) => rest), resources: (await lang[3].find({}).toArray()).map(({ _id, ...rest }) => rest),
research: (await lang[4].find({}).toArray()).map(({ _id, ...rest }) => rest), research: (await lang[4].find({}).toArray()).map(({ _id, ...rest }) => rest),
structures: (await lang[5].find({}).toArray()).map(({ _id, ...rest }) => rest) structures: (await lang[5].find({}).toArray()).map(({ _id, ...rest }) => rest),
defenses: (await lang[6].find({}).toArray()).map(({ _id, ...rest }) => rest)
} }
} }

View File

@ -67,6 +67,11 @@ export const Ships = async() => {
return db.collection('ships'); return db.collection('ships');
} }
export const Defenses = async() => {
const db = await getDB();
return db.collection('defenses');
}
export const Fleet = async() => { export const Fleet = async() => {
const db = await getDB(); const db = await getDB();
return db.collection('fleet'); return db.collection('fleet');
@ -85,7 +90,8 @@ export const Lang = async (language = "en") => {
await db.collection('ships'), await db.collection('ships'),
await db.collection('resources'), await db.collection('resources'),
await db.collection('research'), await db.collection('research'),
await db.collection('structures') await db.collection('structures'),
await db.collection('defenses')
] ]
} }

View File

@ -40,4 +40,13 @@ export const updatePlanetShips = async (planetId: ObjectId, ships: Array<{ id: s
ships ships
} }
}); });
}
export const updatePlanetDefenses = async (planetId: ObjectId, defenses: Array<{ id: string, amount: number }>) => {
const planets = await Planets();
await planets.updateOne({ _id: planetId }, {
$set: {
defenses
}
});
} }

View File

@ -1,6 +1,7 @@
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
import { Systems } from "./mongodb"; import { Systems } from "./mongodb";
import DBSystem from "../../types/db/DBSystem"; import DBSystem from "../../types/db/DBSystem";
import Asteroid from "../../types/Asteroid";
export const getAllSystems = async () => { export const getAllSystems = async () => {
return await (await Systems()).find({}).toArray() as DBSystem[]; return await (await Systems()).find({}).toArray() as DBSystem[];
@ -42,4 +43,22 @@ export const updateSystemShips = async (systemId: ObjectId, ships: Array<any>) =
ships ships
} }
}); });
}
export const updateSystemDefenses = async (systemId: ObjectId, defenses: Array<any>) => {
const systems = await Systems();
await systems.updateOne({ _id: systemId }, {
$set: {
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) { export default function parseParams(url: string) {
const rawParams = url.split("?")[1]?.split("&"); const rawParams = url.split("?")[1]?.split("&");
if(typeof rawParams === "undefined") return []; if(typeof rawParams === "undefined") return {};
const params: { [key: string]: any } = {}; const params: { [key: string]: any } = {};
for(const rawParam of rawParams) { for(const rawParam of rawParams) {
const k = rawParam.split("=")[0]; const k = rawParam.split("=")[0];

View File

@ -115,6 +115,7 @@ export const POST: APIRoute = async({ request }) => {
await userPlanet.buildings.sync(); await userPlanet.buildings.sync();
await userPlanet.resources.sync(); await userPlanet.resources.sync();
userPlanet.energy.update();
return new Response( return new Response(
JSON.stringify({ JSON.stringify({

View File

@ -0,0 +1,146 @@
import { type APIRoute } from "astro";
import validateAccessToken from "../../../lib/utils/validateAccessToken";
import { getUserByAccessToken } from "../../../lib/db/users";
import { ObjectId } from "mongodb";
import locationManager from "../../../lib/classes/managers/LocationManager";
import { getAllResources } from "../../../lib/db/resources";
import Defense from "../../../lib/classes/Defense";
import { Planet } from "../../../lib/classes/managers/PlanetManager";
import SystemManager from "../../../lib/classes/managers/SystemManager";
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 }
)
}
let body;
try {
body = await request.json()
} catch(e) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Invalid JSON body"
}), { status: 400 }
)
}
if(!body.id || !body.defense || !body.amount) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Missing required fields: id, defense, amount"
}), { status: 400 }
)
}
const amount = parseInt(body.amount);
if(isNaN(amount) || amount <= 0) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Invalid amount"
}), { status: 400 }
)
}
let id: ObjectId;
try {
id = new ObjectId(body.id);
} catch(e) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Invalid ID"
}), { status: 400 }
)
}
let planetOrSystem: Planet | SystemManager | undefined = locationManager.getPlanet(id);
if(!planetOrSystem) planetOrSystem = locationManager.getSystem(id);
if(!planetOrSystem) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Invalid ID"
}), { status: 400 }
)
}
const defenseDB = planetOrSystem.defenses.defensesDB.find(d => d.id === body.defense);
if(!defenseDB) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: "Invalid defense ID"
}), { status: 400 }
)
}
const defense = new Defense(planetOrSystem.defenses, defenseDB, amount);
const requirements = await defense.checkRequirements();
const resources = await defense.checkRequiredResources();
if(!requirements.canBuild || !resources) {
return new Response(
JSON.stringify({
code: 400,
message: "Bad Request",
error: `${requirements.error} | ${resources ? "" : "Not enough resources"}`
}), { status: 400 }
)
}
const resourcesDiff = await planetOrSystem.resources.getDifference(defense.data.requirements.resources.map(res => {
return {
id: res.id,
amount: amount * res.amount
}
}));
const resourceDB = await getAllResources();
const resourcesAfter = resourcesDiff.map(res => {
const data = resourceDB.find(r => r.id === res.id);
if(!data) throw new Error("Resource not found");
return {
id: res.id,
amount: res.amount,
lastUpdated: res.lastUpdated,
perHourMiningRate: res.perHourMiningRate,
data
}
});
planetOrSystem.resources.updateAmount(resourcesAfter.map(res => { return { id: res.id, amount: res.amount } }));
planetOrSystem.defenses.addDefenses(defense.data.id, amount);
await planetOrSystem.defenses.sync();
await planetOrSystem.resources.sync();
return new Response(
JSON.stringify({
code: 200,
message: "OK"
}), { status: 200 }
);
}

View File

@ -1,7 +1,7 @@
import { APIRoute } from "astro"; import { APIRoute } from "astro";
import validateAccessToken from "../../../lib/utils/validateAccessToken"; import validateAccessToken from "../../../lib/utils/validateAccessToken";
import { getUserByAccessToken } from "../../../lib/db/users"; import { getUserByAccessToken } from "../../../lib/db/users";
import { ObjectId } from "mongodb"; import { ObjectId, UUID } from "mongodb";
import locationManager from "../../../lib/classes/managers/LocationManager"; import locationManager from "../../../lib/classes/managers/LocationManager";
import { getAllShips } from "../../../lib/db/ships"; import { getAllShips } from "../../../lib/db/ships";
import DBShip from "../../../types/db/DBShip"; 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 { try {
body = await request.json() body = await request.json()
} catch(e) { } catch(e) {
@ -64,11 +64,16 @@ export const POST: APIRoute = async({ request }) => {
const checkCargoBody = checkCargo(body.cargo, body.ships, source, body.mission); const checkCargoBody = checkCargo(body.cargo, body.ships, source, body.mission);
if(typeof checkCargoBody.error !== "undefined") return new Response(JSON.stringify(checkCargoBody), { status: checkCargoBody.code }); if(typeof checkCargoBody.error !== "undefined") return new Response(JSON.stringify(checkCargoBody), { status: checkCargoBody.code });
let dest; let dest, additionalData;
if(body.mission === "EXPEDITION") { if(body.mission === "EXPEDITION") {
const destinationSector = checkSectorId(body.destination); const destinationSector = checkSectorId(body.destination);
if(typeof destinationSector.error !== "undefined") return new Response(JSON.stringify(destinationSector), { status: destinationSector.code }); if(typeof destinationSector.error !== "undefined") return new Response(JSON.stringify(destinationSector), { status: destinationSector.code });
dest = destinationSector.sector; 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 { } else {
const checkDestination = checkPlanetOrSystemId(body.destination, 'destination'); const checkDestination = checkPlanetOrSystemId(body.destination, 'destination');
if(typeof checkDestination.error !== "undefined") return new Response(JSON.stringify(checkDestination), { status: checkDestination.code }); 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, false,
body.mission, body.mission,
body.ships, 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 }))); 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) { function checkPlanetOrSystemId(id: string, type: string) {
if(typeof ObjectId === "undefined") return { if(typeof id === "undefined") return {
code: 400, code: 400,
message: "Bad Request", message: "Bad Request",
error: `Missing '${type}' in body` 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) { function checkSectorId(id: string) {
if(typeof ObjectId === "undefined") return { if(typeof id === "undefined") return {
code: 400, code: 400,
message: "Bad Request", message: "Bad Request",
error: "Missing 'sector' in body" 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,184 @@
---
import ItemCard from '../../components/ItemCard.astro';
import LoggedIn from '../../layouts/LoggedIn.astro';
import { Planet } from '../../lib/classes/managers/PlanetManager';
import SystemManager from '../../lib/classes/managers/SystemManager';
import { getAllDefenses } from '../../lib/db/defenses';
import { getObj } from '../../lib/utils/langDriver';
const { token, lang } = Astro.locals;
const active: SystemManager | Planet = Astro.locals.active;
const defenses = await getAllDefenses();
if(Astro.request.method === "POST") {
const body = await Astro.request.formData();
const id = body.get("id") as string;
const amount = parseInt(body.get("amount") as string ?? "1");
await fetch(Astro.url.origin + '/api/defenses/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
id: active instanceof SystemManager ? active.data._id : active._id,
defense: id,
amount
})
});
}
const modalSet: { [key: string]: { resources: Array<any>, research: Array<any>, buildings: Array<any> } } = {};
for(const def of defenses) {
modalSet[def.id] = {
resources: def.requirements.resources,
research: def.requirements.research,
buildings: def.requirements.buildings,
// energy: building.energy
};
}
const planetId = active instanceof SystemManager ? active.data._id : active._id;
---
<LoggedIn id="defenses" title="Defenses">
<div id="ship-modal-background">
<div id="ship-modal-details" data-building-id="">
<h3>Required resources</h3>
<div class="ship-modal-text" id="ship-modal-req-resources">None</div>
<h3>Required buildings</h3>
<div class="ship-modal-text" id="ship-modal-req-buildings">None</div>
<h3>Required research</h3>
<div class="ship-modal-text" id="ship-modal-req-research">None</div>
</div>
</div>
<div class="ship-cards">
{defenses.map(def => <>
<ItemCard
category="ships"
id={def.id}
name={getObj(lang, "defenses", def.id).name}
level={active.defenses.getDefenseById(def.id)?.amount.toString() ?? "0"}
description={getObj(lang, "defenses", def.id).description ?? ""}
image={`/images/defenses/${def.id}.png`}
button_type="general"
button_name="nav-build"
has_amount_input="true" />
</>)}
</div>
</LoggedIn>
<style>
.ship-cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
row-gap: 40px;
column-gap: 2%;
margin-top: 40px;
}
#ship-modal-background {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
}
#ship-modal-details {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 800px;
background: rgba(0, 0, 0, 0.9);
border-radius: 8px;
padding: 1rem;
z-index: 101;
color: white;
}
</style>
<script define:vars={{ modalSet, lang, planetId }}>
const modalResources = document.getElementById("ship-modal-req-resources");
const modalBuildings = document.getElementById("ship-modal-req-buildings");
const modalResearch = document.getElementById("ship-modal-req-research");
document.querySelectorAll('.item-card-info-button').forEach((el) => {
el.addEventListener('click', () => {
// modal
const modalDiv = document.getElementById('ship-modal-details');
if(!modalDiv) return;
modalDiv.style.display = 'block';
const reqResources = modalSet[el.parentElement.parentElement.parentElement.dataset.id]?.resources ?? [];
const reqBuildings = modalSet[el.parentElement.parentElement.parentElement.dataset.id]?.buildings ?? [];
const reqResearch = modalSet[el.parentElement.parentElement.parentElement.dataset.id]?.research ?? [];
modalResources.innerHTML = reqResources.length === 0 ? "None" : reqResources.map(resource => {
return `${lang['resources'].find(r => r.id === resource.id).name}: ${resource.amount}`;
}).join("<br />");
modalBuildings.innerHTML = reqBuildings.length === 0 ? "None" : reqBuildings.map(building => {
return `${lang['buildings'].find(b => b.id === building.id).name}: ${building.level}`;
}).join("<br />");
modalResearch.innerHTML = reqResearch.length === 0 ? "None" : reqResearch.map(research => {
return `${lang['research'].find(r => r.id === research.id).name}: ${research.level}`;
}).join("<br />");
// background
const backgroundDiv = document.getElementById('ship-modal-background');
if(!backgroundDiv) return;
backgroundDiv.style.display = 'block';
});
});
// close modal on background click
const bg = document.getElementById('ship-modal-background');
bg?.addEventListener('click', () => {
const modalDiv = document.getElementById('ship-modal-details');
if(!modalDiv) return;
modalDiv.style.display = 'none';
const backgroundDiv = document.getElementById('ship-modal-background');
if(!backgroundDiv) return;
backgroundDiv.style.display = 'none';
});
const allButtons = document.getElementsByClassName("item-card-build");
for(const shipButton of allButtons) {
shipButton.addEventListener("click", async () => {
const id = shipButton.id.split("_")[1];
const response = await fetch('/api/ships/addShip', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
planet: planetId,
ship: id,
amount: 1
})
});
if(response.status === 200) {
window.location.reload();
} else {
alert("Failed to build ship: " + JSON.stringify(await response.json()));
}
});
}
</script>

162
src/pages/game/energy.astro Normal file
View File

@ -0,0 +1,162 @@
---
import LoggedIn from '../../layouts/LoggedIn.astro';
import { Planet } from '../../lib/classes/managers/PlanetManager';
import SystemManager from '../../lib/classes/managers/SystemManager';
const { token, lang } = Astro.locals;
const active: SystemManager | Planet = Astro.locals.active;
if(active instanceof SystemManager) {
return Astro.redirect('/game');
}
if(Astro.request.method === "POST") {
const body = await Astro.request.formData();
for(const [key, value] of body.entries()) {
console.log(key, value);
}
return Astro.redirect('/game/energy');
}
const buildingsList = {
"mines": active.buildings.buildings.filter(building => building.data.category === "mines"),
"powerPlants": active.buildings.buildings.filter(building => building.data.category === "power-plants"),
"utilities": active.buildings.buildings.filter(building => building.data.category === "utilities"),
"storage": active.buildings.buildings.filter(building => building.data.category === "storage")
}
---
<LoggedIn id="energy" title="Energy">
<form method="post" class="form-container">
<table>
<tr class="table-col-header">
<th>Building name</th>
<th>Level</th>
<th>Production %</th>
<th>Production per hour</th>
<th>Energy usage</th>
</tr>
<tr><td colspan="5" class="table-separator">Mines</td></tr>
{buildingsList.mines.map(building => <tr>
<td>{building.data.id}</td>
<td>{building.level}</td>
<td><select name={`percent_${building.data.id}`}>
<option value="100" {...{ selected: (building.activePercent ?? 100) === 100 }}>100%</option>
<option value="90" {...{ selected: (building.activePercent ?? 100) === 90 }}>90%</option>
<option value="80" {...{ selected: (building.activePercent ?? 100) === 80 }}>80%</option>
<option value="70" {...{ selected: (building.activePercent ?? 100) === 70 }}>70%</option>
<option value="60" {...{ selected: (building.activePercent ?? 100) === 60 }}>60%</option>
<option value="50" {...{ selected: (building.activePercent ?? 100) === 50 }}>50%</option>
<option value="40" {...{ selected: (building.activePercent ?? 100) === 40 }}>40%</option>
<option value="30" {...{ selected: (building.activePercent ?? 100) === 30 }}>30%</option>
<option value="20" {...{ selected: (building.activePercent ?? 100) === 20 }}>20%</option>
<option value="10" {...{ selected: (building.activePercent ?? 100) === 10 }}>10%</option>
<option value="0" {...{ selected: (building.activePercent ?? 100) === 0 }}>0%</option>
</select></td>
<td>15</td>
<td class="red">-{building.level * building.data.energy}</td>
</tr>)}
<tr><td colspan="5" class="table-separator">Power plants</td></tr>
{buildingsList.powerPlants.map(building => <tr>
<td>{building.data.id}</td>
<td>{building.level}</td>
<td><select name={`percent_${building.data.id}`}>
<option value="100" {...{ selected: (building.activePercent ?? 100) === 100 }}>100%</option>
<option value="90" {...{ selected: (building.activePercent ?? 100) === 90 }}>90%</option>
<option value="80" {...{ selected: (building.activePercent ?? 100) === 80 }}>80%</option>
<option value="70" {...{ selected: (building.activePercent ?? 100) === 70 }}>70%</option>
<option value="60" {...{ selected: (building.activePercent ?? 100) === 60 }}>60%</option>
<option value="50" {...{ selected: (building.activePercent ?? 100) === 50 }}>50%</option>
<option value="40" {...{ selected: (building.activePercent ?? 100) === 40 }}>40%</option>
<option value="30" {...{ selected: (building.activePercent ?? 100) === 30 }}>30%</option>
<option value="20" {...{ selected: (building.activePercent ?? 100) === 20 }}>20%</option>
<option value="10" {...{ selected: (building.activePercent ?? 100) === 10 }}>10%</option>
<option value="0" {...{ selected: (building.activePercent ?? 100) === 0 }}>0%</option>
</select></td>
<td></td>
<td class="green">+{building.level * building.data.energy}</td>
</tr>)}
<tr><td colspan="5" class="table-separator">Utilities</td></tr>
{buildingsList.utilities.map(building => <tr>
<td>{building.data.id}</td>
<td>{building.level}</td>
<td></td>
<td></td>
<td class="red">-{building.level * building.data.energy}</td>
</tr>)}
<tr><td colspan="5" class="table-separator">Storage</td></tr>
{buildingsList.storage.map(building => <tr>
<td>{building.data.id}</td>
<td>{building.level}</td>
<td></td>
<td></td>
<td class="red">-{building.level * building.data.energy}</td>
</tr>)}
</table>
<input type="submit" value="Save" class="save-button" />
</form>
</LoggedIn>
<style>
.form-container {
width: fit-content;
margin-left: auto;
margin-right: auto;
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: white;
}
.green {
color: green;
}
.red {
color: red;
}
.table-col-header {
background-color: #444;
}
.table-separator {
background-color: #222;
font-weight: 1000;
text-align: center;
}
.table-col-header th {
border: 1px solid white;
padding: 10px;
}
th {
font-weight: 900;
}
tr {
color: white;
}
td {
padding: 10px;
border: 1px solid white;
}
.form-container .save-button {
flex-grow: 1;
margin-top: 20px;
padding: 10px;
background-color: #444;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
width: 100%;
}
</style>

View File

@ -59,11 +59,11 @@ for(const research of researchList) {
category="research" category="research"
id={research.id} id={research.id}
name={getObj(lang, "research", research.id).name} name={getObj(lang, "research", research.id).name}
level={user.research.getResearchById(research.id)?.level.toString() ?? "0"} level={research.onetime === true ? user.research.getResearchById(research.id)?.level === 1 ? "Researched" : "Not researched" : user.research.getResearchById(research.id)?.level.toString() ?? "0"}
description={getObj(lang, "research", research.id).description ?? ""} description={getObj(lang, "research", research.id).description ?? ""}
image={`/images/research/${research.id}.jpeg`} image={`/images/research/${research.id}.jpeg`}
button_type="general" button_type="general"
button_name="nav-research" /> button_name={research.onetime ? user.research.getResearchById(research.id)?.level === 1 ? "nav-researched" : "nav-research" : "nav-research"} />
</>)} </>)}
</div> </div>
</LoggedIn> </LoggedIn>

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; export default MissionType;

View File

@ -0,0 +1,18 @@
import { ObjectId } from "mongodb";
export default interface DBDefenses {
_id: ObjectId;
id: string;
requirements: {
buildings: Array<{ id: string, level: number }>,
research: Array<{ id: string, level: number }>,
resources: Array<{ id: string, amount: number }>,
};
energy: number;
time: number;
structure: {
hitpoints: number;
defense: number;
attack: number;
};
}

View File

@ -6,6 +6,7 @@ export default interface DBPlanet {
owner: ObjectId; // shouldn't be here owner: ObjectId; // shouldn't be here
fields: number; fields: number;
resources: Array<{ id: string, amount: number, lastUpdated: Date, perHourMiningRate: number }>; resources: Array<{ id: string, amount: number, lastUpdated: Date, perHourMiningRate: number }>;
buildings: Array<{ id: string, level: number }>; buildings: Array<{ id: string, level: number, activePercent?: number }>;
ships: Array<{ id: string, amount: number }>; ships: Array<{ id: string, amount: number }>;
defenses: Array<{ id: string, amount: number }>;
} }

View File

@ -10,4 +10,5 @@ export default interface DBResearch {
}; };
time: number; time: number;
multiplier: number; multiplier: number;
onetime?: boolean;
} }

View File

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