Split client into frontend website and server

This commit is contained in:
Pax1601
2024-02-08 22:04:23 +01:00
parent 55f3bd5adb
commit 5ca6c97cbe
792 changed files with 149898 additions and 13872 deletions

View File

@@ -0,0 +1,60 @@
import { Unit } from "./unit";
export interface ContextActionOptions {
isScenic?: boolean
}
export class ContextAction {
#id: string = "";
#label: string = "";
#description: string = "";
#callback: CallableFunction | null = null;
#units: Unit[] = [];
#hideContextAfterExecution: boolean = true
#options: ContextActionOptions;
constructor(id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options: ContextActionOptions) {
this.#id = id;
this.#label = label;
this.#description = description;
this.#callback = callback;
this.#hideContextAfterExecution = hideContextAfterExecution;
this.#options = {
"isScenic": false,
...options
}
}
addUnit(unit: Unit) {
this.#units.push(unit);
}
getId() {
return this.#id;
}
getLabel() {
return this.#label;
}
getOptions() {
return this.#options;
}
getDescription() {
return this.#description;
}
getCallback() {
return this.#callback;
}
executeCallback() {
if (this.#callback)
this.#callback(this.#units);
}
getHideContextAfterExecution() {
return this.#hideContextAfterExecution;
}
}

View File

@@ -0,0 +1,25 @@
import { ContextAction, ContextActionOptions } from "./contextaction";
import { Unit } from "./unit";
export class ContextActionSet {
#contextActions: {[key: string]: ContextAction} = {};
constructor() {
}
addContextAction(unit: Unit, id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options?:ContextActionOptions) {
options = options || {};
if (!(id in this.#contextActions)) {
this.#contextActions[id] = new ContextAction(id, label, description, callback, hideContextAfterExecution, options);
}
this.#contextActions[id].addUnit(unit);
}
getContextActions() {
return this.#contextActions;
}
}

View File

@@ -0,0 +1,37 @@
import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class AircraftDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/aircraftdatabase');
}
getCategory() {
return "Aircraft";
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
}
export var aircraftDatabase = new AircraftDatabase();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class GroundUnitDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/groundunitdatabase');
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
getCategory() {
return "GroundUnit";
}
}
export var groundUnitDatabase = new GroundUnitDatabase();

View File

@@ -0,0 +1,37 @@
import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class HelicopterDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/helicopterdatabase');
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
getCategory() {
return "Helicopter";
}
}
export var helicopterDatabase = new HelicopterDatabase();

View File

@@ -0,0 +1,36 @@
import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class NavyUnitDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/navyunitdatabase');
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
getCategory() {
return "NavyUnit";
}
}
export var navyUnitDatabase = new NavyUnitDatabase();

View File

@@ -0,0 +1,252 @@
import { LatLng } from "leaflet";
import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitBlueprint } from "../../interfaces";
export abstract class UnitDatabase {
blueprints: { [key: string]: UnitBlueprint } = {};
#url: string;
constructor(url: string = "") {
this.#url = url;
this.load(() => {});
}
load(callback: CallableFunction) {
if (this.#url !== "") {
var xhr = new XMLHttpRequest();
xhr.open('GET', this.#url, true);
xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
xhr.responseType = 'json';
xhr.onload = () => {
var status = xhr.status;
if (status === 200) {
this.blueprints = xhr.response;
callback();
} else {
console.error(`Error retrieving database from ${this.#url}`)
}
};
xhr.send();
}
}
abstract getCategory(): string;
/* Gets a specific blueprint by name */
getByName(name: string) {
if (name in this.blueprints)
return this.blueprints[name];
return null;
}
/* Gets a specific blueprint by label */
getByLabel(label: string) {
for (let unit in this.blueprints) {
if (this.blueprints[unit].label === label)
return this.blueprints[unit];
}
return null;
}
getBlueprints(includeDisabled: boolean = false) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) {
var filteredBlueprints: { [key: string]: UnitBlueprint } = {};
for (let unit in this.blueprints) {
const blueprint = this.blueprints[unit];
if (blueprint.enabled || includeDisabled)
filteredBlueprints[unit] = blueprint;
}
return filteredBlueprints;
}
else {
var filteredBlueprints: { [key: string]: UnitBlueprint } = {};
for (let unit in this.blueprints) {
const blueprint = this.blueprints[unit];
if ((blueprint.enabled || includeDisabled) && this.getSpawnPointsByName(blueprint.name) <= getApp().getMissionManager().getAvailableSpawnPoints() &&
getApp().getMissionManager().getCommandModeOptions().eras.includes(blueprint.era) &&
(!getApp().getMissionManager().getCommandModeOptions().restrictToCoalition || blueprint.coalition === getApp().getMissionManager().getCommandedCoalition() || blueprint.coalition === undefined)) {
filteredBlueprints[unit] = blueprint;
}
}
return filteredBlueprints;
}
}
/* Returns a list of all possible roles in a database */
getRoles() {
var roles: string[] = [];
var filteredBlueprints = this.getBlueprints();
for (let unit in filteredBlueprints) {
var loadouts = filteredBlueprints[unit].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
for (let role of loadout.roles) {
if (role !== "" && !roles.includes(role))
roles.push(role);
}
}
}
}
return roles;
}
/* Returns a list of all possible types in a database */
getTypes(unitFilter?:CallableFunction) {
var filteredBlueprints = this.getBlueprints();
var types: string[] = [];
for (let unit in filteredBlueprints) {
if ( typeof unitFilter === "function" && !unitFilter(filteredBlueprints[unit]))
continue;
var type = filteredBlueprints[unit].type;
if (type && type !== "" && !types.includes(type))
types.push(type);
}
return types;
}
/* Returns a list of all possible periods in a database */
getEras() {
var filteredBlueprints = this.getBlueprints();
var eras: string[] = [];
for (let unit in filteredBlueprints) {
var era = filteredBlueprints[unit].era;
if (era && era !== "" && !eras.includes(era))
eras.push(era);
}
return eras;
}
/* Get all blueprints by range */
getByRange(range: string) {
var filteredBlueprints = this.getBlueprints();
var unitswithrange = [];
var minRange = 0;
var maxRange = 0;
if (range === "Short range") {
minRange = 0;
maxRange = 10000;
}
else if (range === "Medium range") {
minRange = 10000;
maxRange = 100000;
}
else {
minRange = 100000;
maxRange = 999999;
}
for (let unit in filteredBlueprints) {
var engagementRange = filteredBlueprints[unit].engagementRange;
if (engagementRange !== undefined) {
if (engagementRange >= minRange && engagementRange < maxRange) {
unitswithrange.push(filteredBlueprints[unit]);
}
}
}
return unitswithrange;
}
/* Get all blueprints by type */
getByType(type: string) {
var filteredBlueprints = this.getBlueprints();
var units = [];
for (let unit in filteredBlueprints) {
if (filteredBlueprints[unit].type === type) {
units.push(filteredBlueprints[unit]);
}
}
return units;
}
/* Get all blueprints by role */
getByRole(role: string) {
var filteredBlueprints = this.getBlueprints();
var units = [];
for (let unit in filteredBlueprints) {
var loadouts = filteredBlueprints[unit].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes(role.toLowerCase())) {
units.push(filteredBlueprints[unit])
break;
}
}
}
}
return units;
}
/* Get the names of all the loadouts for a specific unit and for a specific role */
getLoadoutNamesByRole(name: string, role: string) {
var filteredBlueprints = this.getBlueprints();
var loadoutsByRole = [];
var loadouts = filteredBlueprints[name].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes("")) {
loadoutsByRole.push(loadout.name)
}
}
}
return loadoutsByRole;
}
/* Get the livery names for a specific unit */
getLiveryNamesByName(name: string) {
var liveries = this.blueprints[name].liveries;
if (liveries !== undefined)
return Object.values(liveries);
else
return [];
}
/* Get the loadout content from the unit name and loadout name */
getLoadoutByName(name: string, loadoutName: string) {
var loadouts = this.blueprints[name].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
if (loadout.name === loadoutName)
return loadout;
}
}
return null;
}
generateTestGrid(initialPosition: LatLng) {
var filteredBlueprints = this.getBlueprints();
const step = 0.01;
var nUnits = Object.values(filteredBlueprints).length;
var gridSize = Math.ceil(Math.sqrt(nUnits));
Object.values(filteredBlueprints).forEach((unitBlueprint: UnitBlueprint, idx: number) => {
var row = Math.floor(idx / gridSize);
var col = idx - row * gridSize;
var location = new LatLng(initialPosition.lat + col * step, initialPosition.lng + row * step)
getApp().getUnitsManager().spawnUnits(this.getCategory(), [{unitType: unitBlueprint.name, location: location, altitude: 1000, loadout: "", liveryID: ""}]);
})
}
getSpawnPointsByLabel(label: string) {
var blueprint = this.getByLabel(label);
if (blueprint)
return this.getSpawnPointsByName(blueprint.name);
else
return Infinity;
}
getSpawnPointsByName(name: string) {
return Infinity;
}
getUnkownUnit(name: string): UnitBlueprint {
return {
name: name,
enabled: true,
coalition: 'neutral',
era: 'N/A',
label: name,
shortLabel: ''
}
}
}

View File

@@ -0,0 +1,45 @@
import { Unit } from "./unit";
export class Group {
#members: Unit[] = [];
#name: string;
constructor(name: string) {
this.#name = name;
document.addEventListener("unitDeath", (e: any) => {
if (this.#members.includes(e.detail))
this.getLeader()?.onGroupChanged(e.detail);
});
}
getName() {
return this.#name;
}
addMember(member: Unit) {
if (!this.#members.includes(member)) {
this.#members.push(member);
member.setGroup(this);
this.getLeader()?.onGroupChanged(member);
}
}
removeMember(member: Unit) {
if (this.#members.includes(member)) {
delete this.#members[this.#members.indexOf(member)];
member.setGroup(null);
this.getLeader()?.onGroupChanged(member);
}
}
getMembers() {
return this.#members;
}
getLeader() {
return this.#members.find((unit: Unit) => { return (unit.getIsLeader() && unit.getAlive())})
}
}

View File

@@ -0,0 +1,65 @@
import { Dialog } from "../../dialog/dialog";
import { createCheckboxOption } from "../../other/utils";
var categoryMap = {
"Aircraft": "Aircraft",
"Helicopter": "Helicopter",
"GroundUnit": "Ground units",
"NavyUnit": "Naval units"
}
export abstract class UnitDataFile {
protected data: any;
protected dialog!: Dialog;
constructor() { }
buildCategoryCoalitionTable() {
const categories = this.#getCategoriesFromData();
const coalitions = ["blue", "neutral", "red"];
let headersHTML: string = ``;
let matrixHTML: string = ``;
categories.forEach((category: string, index) => {
matrixHTML += `<tr><td>${categoryMap[category as keyof typeof categoryMap]}</td>`;
coalitions.forEach((coalition: string) => {
if (index === 0)
headersHTML += `<th data-coalition="${coalition}">${coalition[0].toUpperCase() + coalition.substring(1)}</th>`;
const optionIsValid = this.data[category].hasOwnProperty(coalition);
let checkboxHTML = createCheckboxOption(`${category}:${coalition}`, category, optionIsValid, () => { }, {
"disabled": !optionIsValid,
"name": "category-coalition-selection",
"readOnly": !optionIsValid
}).outerHTML;
if (optionIsValid)
checkboxHTML = checkboxHTML.replace(`"checkbox"`, `"checkbox" checked`); // inner and outerHTML screw default checked up
matrixHTML += `<td data-coalition="${coalition}">${checkboxHTML}</td>`;
});
matrixHTML += "</tr>";
});
const table = <HTMLTableElement>this.dialog.getElement().querySelector("table.categories-coalitions");
(<HTMLElement>table.tHead).innerHTML = `<tr><td>&nbsp;</td>${headersHTML}</tr>`;
(<HTMLElement>table.querySelector(`tbody`)).innerHTML = matrixHTML;
}
#getCategoriesFromData() {
const categories = Object.keys(this.data);
categories.sort();
return categories;
}
getData() {
return this.data;
}
}

View File

@@ -0,0 +1,101 @@
import { getApp } from "../..";
import { Dialog } from "../../dialog/dialog";
import { zeroAppend } from "../../other/utils";
import { Unit } from "../unit";
import { UnitDataFile } from "./unitdatafile";
export class UnitDataFileExport extends UnitDataFile {
protected data!: any;
protected dialog: Dialog;
#element!: HTMLElement;
#filename: string = "export.json";
constructor(elementId: string) {
super();
this.dialog = new Dialog(elementId);
this.#element = this.dialog.getElement();
this.#element.querySelector(".start-transfer")?.addEventListener("click", (ev: MouseEventInit) => {
this.#doExport();
});
}
/**
* Show the form to start the export journey
*/
showForm(units: Unit[]) {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "show");
});
const data: any = {};
const unitCanBeExported = (unit: Unit) => !["Aircraft", "Helicopter"].includes(unit.getCategory());
units.filter((unit: Unit) => unit.getAlive() && unitCanBeExported(unit)).forEach((unit: Unit) => {
const category = unit.getCategory();
const coalition = unit.getCoalition();
if (!data.hasOwnProperty(category)) {
data[category] = {};
}
if (!data[category].hasOwnProperty(coalition))
data[category][coalition] = [];
data[category][coalition].push(unit);
});
this.data = data;
this.buildCategoryCoalitionTable();
this.dialog.show();
const date = new Date();
this.#filename = `olympus_${getApp().getMissionManager().getTheatre().replace(/[^\w]/gi, "").toLowerCase()}_${date.getFullYear()}${zeroAppend(date.getMonth() + 1, 2)}${zeroAppend(date.getDate(), 2)}_${zeroAppend(date.getHours(), 2)}${zeroAppend(date.getMinutes(), 2)}${zeroAppend(date.getSeconds(), 2)}.json`;
var input = this.#element.querySelector("#export-filename") as HTMLInputElement;
input.onchange = (ev: Event) => {
this.#filename = (ev.currentTarget as HTMLInputElement).value;
}
if (input)
input.value = this.#filename;
}
#doExport() {
let selectedUnits: Unit[] = [];
this.#element.querySelectorAll(`input[type="checkbox"][name="category-coalition-selection"]:checked`).forEach(<HTMLInputElement>(checkbox: HTMLInputElement) => {
if (checkbox instanceof HTMLInputElement) {
const [category, coalition] = checkbox.value.split(":"); // e.g. "category:coalition"
selectedUnits = selectedUnits.concat(this.data[category][coalition]);
}
});
if (selectedUnits.length === 0) {
alert("Please select at least one option for export.");
return;
}
var unitsToExport: { [key: string]: any } = {};
selectedUnits.forEach((unit: Unit) => {
var data: any = unit.getData();
if (unit.getGroupName() in unitsToExport)
unitsToExport[unit.getGroupName()].push(data);
else
unitsToExport[unit.getGroupName()] = [data];
});
const a = document.createElement("a");
const file = new Blob([JSON.stringify(unitsToExport)], { type: 'text/plain' });
a.href = URL.createObjectURL(file);
var filename = this.#filename;
if (!this.#filename.toLowerCase().endsWith(".json"))
filename += ".json";
a.download = filename;
a.click();
this.dialog.hide();
}
}

View File

@@ -0,0 +1,150 @@
import { getApp } from "../..";
import { Dialog } from "../../dialog/dialog";
import { UnitData } from "../../interfaces";
import { ImportFileJSONSchemaValidator } from "../../schemas/schema";
import { UnitDataFile } from "./unitdatafile";
export class UnitDataFileImport extends UnitDataFile {
protected data!: any;
protected dialog: Dialog;
#fileData!: { [key: string]: UnitData[] };
constructor(elementId: string) {
super();
this.dialog = new Dialog(elementId);
this.dialog.getElement().querySelector(".start-transfer")?.addEventListener("click", (ev: MouseEventInit) => {
this.#doImport();
this.dialog.hide();
});
}
#doImport() {
let selectedCategories: any = {};
const unitsManager = getApp().getUnitsManager();
this.dialog.getElement().querySelectorAll(`input[type="checkbox"][name="category-coalition-selection"]:checked`).forEach(<HTMLInputElement>(checkbox: HTMLInputElement) => {
if (checkbox instanceof HTMLInputElement) {
const [category, coalition] = checkbox.value.split(":"); // e.g. "category:coalition"
selectedCategories[category] = selectedCategories[category] || {};
selectedCategories[category][coalition] = true;
}
});
for (const [groupName, groupData] of Object.entries(this.#fileData)) {
if (groupName === "" || groupData.length === 0 || !this.#unitGroupDataCanBeImported(groupData))
continue;
let { category, coalition } = groupData[0];
if (!selectedCategories.hasOwnProperty(category)
|| !selectedCategories[category].hasOwnProperty(coalition)
|| selectedCategories[category][coalition] !== true)
continue;
let unitsToSpawn = groupData.filter((unitData: UnitData) => this.#unitDataCanBeImported(unitData)).map((unitData: UnitData) => {
return { unitType: unitData.name, location: unitData.position, liveryID: "" }
});
unitsManager.spawnUnits(category, unitsToSpawn, coalition, false);
}
}
selectFile() {
var input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e: any) => {
var file = e.target.files[0];
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = (e: any) => {
try {
this.#fileData = JSON.parse(e.target.result);
const validator = new ImportFileJSONSchemaValidator();
if (!validator.validate(this.#fileData)) {
const errors = validator.getErrors().reduce((acc:any, error:any) => {
let errorString = error.instancePath.substring(1) + ": " + error.message;
if (error.params) {
const {allowedValues} = error.params;
if (allowedValues)
errorString += ": " + allowedValues.join(', ');
}
acc.push(errorString);
return acc;
}, [] as string[]);
this.#showFileDataErrors(errors);
} else {
this.#showForm();
}
} catch(e:any) {
this.#showFileDataErrors([e]);
}
};
reader.readAsText(file);
})
input.click();
}
#showFileDataErrors( reasons:string[]) {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "hide");
});
const reasonsList = this.dialog.getElement().querySelector(".import-error-reasons");
if (reasonsList instanceof HTMLElement)
reasonsList.innerHTML = `<li>${reasons.join("</li><li>")}</li>`;
this.dialog.show();
}
#showForm() {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "show");
});
const data: any = {};
for (const [group, units] of Object.entries(this.#fileData)) {
if (group === "" || units.length === 0)
continue;
if (units.some((unit: UnitData) => !this.#unitDataCanBeImported(unit)))
continue;
const category = units[0].category;
if (!data.hasOwnProperty(category)) {
data[category] = {};
}
units.forEach((unit: UnitData) => {
if (!data[category].hasOwnProperty(unit.coalition))
data[category][unit.coalition] = [];
data[category][unit.coalition].push(unit);
});
}
this.data = data;
this.buildCategoryCoalitionTable();
this.dialog.show();
}
#unitDataCanBeImported(unitData: UnitData) {
return unitData.alive && this.#unitGroupDataCanBeImported([unitData]);
}
#unitGroupDataCanBeImported(unitGroupData: UnitData[]) {
return unitGroupData.every((unitData: UnitData) => {
return !["Aircraft", "Helicopter"].includes(unitData.category);
}) && unitGroupData.some((unitData: UnitData) => unitData.alive);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff