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,110 @@
import { LoadoutBlueprint, UnitBlueprint } from "interfaces";
import { UnitEditor } from "./uniteditor";
import { LoadoutEditor } from "./loadouteditor";
import { addCheckboxInput, addDropdownInput, addLoadoutsScroll, addNewElementInput, addStringInput } from "./utils";
/** Database editor for Air Units, both Aircraft and Helicopter since they are identical in terms of datbase entries.
*
*/
export class AirUnitEditor extends UnitEditor {
#loadoutEditor: LoadoutEditor | null = null;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
super(contentDiv1, contentDiv2, contentDiv3);
/* The loadout editor allows to edit the loadout (who could have thought eh?) */
this.#loadoutEditor = new LoadoutEditor(this.contentDiv3);
/* Refresh the loadout editor if needed */
this.contentDiv3.addEventListener("refresh", () => {
if (this.visible)
this.#loadoutEditor?.show();
});
}
/** Sets a unit blueprint as the currently active one
*
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint: UnitBlueprint) {
this.blueprint = blueprint;
if (this.blueprint !== null) {
this.contentDiv2.replaceChildren();
var title = document.createElement("label");
title.innerText = "Unit properties";
this.contentDiv2.appendChild(title);
addStringInput(this.contentDiv2, "Name", blueprint.name, "text", (value: string) => { blueprint.name = value; }, true);
addStringInput(this.contentDiv2, "Label", blueprint.label, "text", (value: string) => { blueprint.label = value; });
addStringInput(this.contentDiv2, "Short label", blueprint.shortLabel, "text", (value: string) => { blueprint.shortLabel = value; });
addDropdownInput(this.contentDiv2, "Coalition", blueprint.coalition, ["", "blue", "red"], (value: string) => {blueprint.coalition = value; });
addDropdownInput(this.contentDiv2, "Era", blueprint.era, ["WW2", "Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], (value: string) => {blueprint.era = value; });
addStringInput(this.contentDiv2, "Filename", blueprint.filename ?? "", "text", (value: string) => { blueprint.filename = value; });
addStringInput(this.contentDiv2, "Cost", String(blueprint.cost) ?? "", "number", (value: string) => { blueprint.cost = parseFloat(value); });
addCheckboxInput(this.contentDiv2, "Can target point", blueprint.canTargetPoint ?? false, (value: boolean) => {blueprint.canTargetPoint = value;})
addStringInput(this.contentDiv2, "Description", blueprint.description ?? "", "text", (value: string) => {blueprint.description = value; });
addStringInput(this.contentDiv2, "Tags", blueprint.tags ?? "", "text", (value: string) => {blueprint.tags = value; });
/* Add a scrollable list of loadouts that the user can edit */
var title = document.createElement("label");
title.innerText = "Loadouts";
this.contentDiv2.appendChild(title);
addLoadoutsScroll(this.contentDiv2, blueprint.loadouts ?? [], (loadout: LoadoutBlueprint) => {
this.#loadoutEditor?.setLoadout(loadout);
this.#loadoutEditor?.show();
});
addNewElementInput(this.contentDiv2, (ev: MouseEvent, input: HTMLInputElement) => { this.addLoadout(input.value); });
this.#loadoutEditor?.hide();
}
}
/** Add a new empty blueprint
*
* @param key Blueprint key
*/
addBlueprint(key: string) {
if (this.database != null) {
this.database.blueprints[key] = {
name: key,
coalition: "",
label: "",
shortLabel: "",
era: "",
loadouts: [],
enabled: true
}
this.show();
this.setBlueprint(this.database.blueprints[key]);
}
}
/** Add a new empty loadout to the currently active blueprint
*
* @param loadoutName The name of the new loadout
*/
addLoadout(loadoutName: string) {
if (loadoutName && this.blueprint !== null) {
this.blueprint.loadouts?.push({
name: loadoutName,
code: "",
fuel: 1,
items: [],
roles: [],
enabled: true
})
this.setBlueprint(this.blueprint);
}
}
/** Hide the editor
*
*/
hide() {
super.hide();
this.#loadoutEditor?.hide();
}
}

View File

@@ -0,0 +1,424 @@
import { OlympusPlugin, UnitBlueprint } from "interfaces";
import { AirUnitEditor } from "./airuniteditor";
import { OlympusApp } from "olympusapp";
import { GroundUnitEditor } from "./grounduniteditor";
import { PrimaryToolbar } from "toolbars/primarytoolbar";
import { NavyUnitEditor } from "./navyuniteditor";
/** Database Manager
*
* This database provides a user interface to allow easier and convenient unit databases manipulation. It allows to edit all the fields of the units databases, save them
* on the server, and restore the defaults.
*
* TODO:
* Add ability to manage liveries
*
*/
export class DatabaseManagerPlugin implements OlympusPlugin {
#app!: OlympusApp;
#element: HTMLElement;
#mainContentContainer: HTMLElement;
#contentDiv1: HTMLElement;
#contentDiv2: HTMLElement;
#contentDiv3: HTMLElement;
/* Upper tab buttons */
#button1: HTMLButtonElement;
#button2: HTMLButtonElement;
#button3: HTMLButtonElement;
#button4: HTMLButtonElement;
/* Lower operation buttons */
#button5: HTMLButtonElement;
#button6: HTMLButtonElement;
#button7: HTMLButtonElement;
#button8: HTMLButtonElement;
#button9: HTMLButtonElement;
/* Database editors */
#aircraftEditor: AirUnitEditor;
#helicopterEditor: AirUnitEditor;
#groundUnitEditor: GroundUnitEditor;
#navyUnitEditor: NavyUnitEditor;
constructor() {
/* Create main HTML element */
this.#element = document.createElement("div");
this.#element.id = "database-manager-panel";
this.#element.oncontextmenu = () => { return false; }
this.#element.classList.add("ol-dialog");
document.body.appendChild(this.#element);
/* Start hidden */
this.toggle(false);
/* Create the top tab buttons container and buttons */
let topButtonContainer = document.createElement("div");
this.#button1 = document.createElement("button");
this.#button1.classList.add("tab-button");
this.#button1.textContent = "Aircraft database";
this.#button1.onclick = () => { this.#hideAll(); this.#aircraftEditor.show(); this.#button1.classList.add("selected"); };
topButtonContainer.appendChild(this.#button1);
this.#button2 = document.createElement("button");
this.#button2.classList.add("tab-button");
this.#button2.textContent = "Helicopter database";
this.#button2.onclick = () => { this.#hideAll(); this.#helicopterEditor.show(); this.#button2.classList.add("selected"); };
topButtonContainer.appendChild(this.#button2);
this.#button3 = document.createElement("button");
this.#button3.classList.add("tab-button");
this.#button3.textContent = "Ground Unit database";
this.#button3.onclick = () => { this.#hideAll(); this.#groundUnitEditor.show(); this.#button3.classList.add("selected"); };
topButtonContainer.appendChild(this.#button3);
this.#button4 = document.createElement("button");
this.#button4.classList.add("tab-button");
this.#button4.textContent = "Navy Unit database";
this.#button4.onclick = () => { this.#hideAll(); this.#navyUnitEditor.show(); this.#button4.classList.add("selected"); };
topButtonContainer.appendChild(this.#button4);
this.#element.appendChild(topButtonContainer);
/* Create the container for the database editor elements and the elements themselves */
this.#mainContentContainer = document.createElement("div");
this.#mainContentContainer.classList.add("dm-container");
this.#element.appendChild(this.#mainContentContainer);
this.#contentDiv1 = document.createElement("div");
this.#contentDiv1.classList.add("dm-content-container", "ol-scrollable");
this.#mainContentContainer.appendChild(this.#contentDiv1);
this.#contentDiv2 = document.createElement("div");
this.#contentDiv2.classList.add("dm-content-container", "ol-scrollable");
this.#mainContentContainer.appendChild(this.#contentDiv2);
this.#contentDiv3 = document.createElement("div");
this.#contentDiv3.classList.add("dm-content-container", "ol-scrollable");
this.#mainContentContainer.appendChild(this.#contentDiv3);
/* Create the database editors, which use the three divs created before */
this.#aircraftEditor = new AirUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
this.#helicopterEditor = new AirUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
this.#groundUnitEditor = new GroundUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
this.#navyUnitEditor = new NavyUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
/* Create the bottom buttons container. These buttons allow to save, restore, reset, and discard the changes */
let bottomButtonContainer = document.createElement("div");
this.#button5 = document.createElement("button");
this.#button5.textContent = "Save";
this.#button5.title = "Save the changes on the server"
this.#button5.onclick = () => { this.#saveDatabases();};
bottomButtonContainer.appendChild(this.#button5);
this.#button6 = document.createElement("button");
this.#button6.textContent = "Discard";
this.#button6.title = "Discard all changes and reload the database from the server";
this.#button6.onclick = () => { this.#loadDatabases(); };
bottomButtonContainer.appendChild(this.#button6);
this.#button7 = document.createElement("button");
this.#button7.textContent = "Reset defaults";
this.#button7.onclick = () => { this.#resetToDefaultDatabases(); };
this.#button7.title = "Reset the databases to the default values";
bottomButtonContainer.appendChild(this.#button7);
this.#button8 = document.createElement("button");
this.#button8.textContent = "Restore previous";
this.#button8.onclick = () => { this.#restoreToPreviousDatabases(); };
this.#button8.title = "Restore the previously saved databases. Use this if you saved a database by mistake.";
bottomButtonContainer.appendChild(this.#button8);
this.#button9 = document.createElement("button");
this.#button9.textContent = "Close";
this.#button9.title = "Close the Database Manager"
this.#button9.onclick = () => { this.toggle(false); };
bottomButtonContainer.appendChild(this.#button9);
this.#element.appendChild(bottomButtonContainer);
}
/**
*
* @returns The name of the plugin
*/
getName() {
return "Database Control Plugin"
}
/** Initialize the plugin
*
* @param app The OlympusApp singleton
* @returns True if successfull
*/
initialize(app: any) {
this.#app = app;
const contextManager = this.#app.getContextManager();
contextManager.add( "databaseManager", {
"allowUnitCopying": false,
"allowUnitPasting": false,
"useSpawnMenu": false,
"useUnitControlPanel": false,
"useUnitInfoPanel": false
});
/* Load the databases and initialize the editors */
this.#loadDatabases();
/* Add a button to the main Olympus App to allow the users to open the dialog */
var mainButtonDiv = document.createElement("div");
var mainButton = document.createElement("button");
mainButton.textContent = "Database manager";
mainButtonDiv.appendChild(mainButton);
var toolbar: PrimaryToolbar = this.#app?.getToolbarsManager().get("primaryToolbar") as PrimaryToolbar;
var elements = toolbar.getMainDropdown().getOptionElements();
var arr = Array.prototype.slice.call(elements);
arr.splice(arr.length - 3, 0, mainButtonDiv);
toolbar.getMainDropdown().setOptionsElements(arr);
mainButton.onclick = () => {
toolbar.getMainDropdown().close();
if (this.#app?.getMissionManager().getCommandModeOptions().commandMode === "Game master")
this.toggle();
}
return true;
}
/**
*
* @returns The main container element
*/
getElement() {
return this.#element;
}
/** Toggles the visibility of the dialog
*
* @param bool Force a specific visibility state
*/
toggle(bool?: boolean) {
if (bool)
this.getElement().classList.toggle("hide", !bool);
else
this.getElement().classList.toggle("hide");
if ( this.#app )
this.#app.getContextManager().setContext( this.getElement().classList.contains("hide") ? "olympus" : "databaseManager" );
}
/** Hide all the editors
*
*/
#hideAll() {
this.#aircraftEditor.hide();
this.#helicopterEditor.hide();
this.#groundUnitEditor.hide();
this.#navyUnitEditor.hide();
this.#button1.classList.remove("selected");
this.#button2.classList.remove("selected");
this.#button3.classList.remove("selected");
this.#button4.classList.remove("selected");
}
/** Load the databases from the app to the editor. Note, this does not reload the databases from the server to the app
*
*/
#loadDatabases() {
var aircraftDatabase = this.#app?.getAircraftDatabase();
if (aircraftDatabase != null)
this.#aircraftEditor.setDatabase(aircraftDatabase);
var helicopterDatabase = this.#app?.getHelicopterDatabase();
if (helicopterDatabase != null)
this.#helicopterEditor.setDatabase(helicopterDatabase);
var groundUnitDatabase = this.#app?.getGroundUnitDatabase();
if (groundUnitDatabase != null)
this.#groundUnitEditor.setDatabase(groundUnitDatabase);
var navyUnitDatabase = this.#app?.getNavyUnitDatabase();
if (navyUnitDatabase != null)
this.#navyUnitEditor.setDatabase(navyUnitDatabase);
this.#hideAll();
this.#aircraftEditor.show();
this.#button1.classList.add("selected");
}
/** Save the databases on the server and reloads it to apply the changes
*
*/
#saveDatabases() {
var aircraftDatabase = this.#aircraftEditor.getDatabase();
if (aircraftDatabase){
this.#uploadDatabase(aircraftDatabase, "aircraftdatabase", "Aircraft database", () => {
var helicopterDatabase = this.#helicopterEditor.getDatabase();
if (helicopterDatabase) {
this.#uploadDatabase(helicopterDatabase, "helicopterDatabase", "Helicopter database", () => {
var groundUnitDatabase = this.#groundUnitEditor.getDatabase();
if (groundUnitDatabase) {
this.#uploadDatabase(groundUnitDatabase, "groundUnitDatabase", "Ground Unit database", () => {
var navyUnitDatabase = this.#navyUnitEditor.getDatabase();
if (navyUnitDatabase) {
this.#uploadDatabase(navyUnitDatabase, "navyUnitDatabase", "Navy Unit database", () => {
this.#app?.getAircraftDatabase().load(() => {});
this.#app?.getHelicopterDatabase().load(() => {});
this.#app?.getGroundUnitDatabase().load(() => {});
this.#app?.getNavyUnitDatabase().load(() => {});
this.#app?.getServerManager().reloadDatabases(() => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("Olympus core databases reloaded");
})
});
}
});
}
});
}
});
}
}
/** Resets the databases to the default values
*
*/
#resetToDefaultDatabases() {
this.#resetToDefaultDatabase("aircraftdatabase", "Aircraft database", () => {
this.#app?.getAircraftDatabase().load(() => {
this.#resetToDefaultDatabase("helicopterdatabase", "Helicopter database", () => {
this.#app?.getHelicopterDatabase().load(() => {
this.#resetToDefaultDatabase("groundunitdatabase", "Ground Unit database", () => {
this.#app?.getGroundUnitDatabase().load(() => {
this.#resetToDefaultDatabase("navyunitdatabase", "Navy Unit database", () => {
this.#app?.getNavyUnitDatabase().load(() => {
this.#loadDatabases();
this.#app?.getServerManager().reloadDatabases(() => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("Olympus core databases reloaded");
})
this.#hideAll();
this.#aircraftEditor.show();
this.#button1.classList.add("selected");
});
});
});
});
});
});
});
});
}
/** Restores the databases to the previous saved values. This is useful if you saved the databases by mistake and want to undo the error.
*
*/
#restoreToPreviousDatabases() {
this.#restoreToPreviousDatabase("aircraftdatabase", "Aircraft database", () => {
this.#app?.getAircraftDatabase().load(() => {
this.#restoreToPreviousDatabase("helicopterdatabase", "Helicopter database", () => {
this.#app?.getHelicopterDatabase().load(() => {
this.#restoreToPreviousDatabase("groundunitdatabase", "Ground Unit database", () => {
this.#app?.getGroundUnitDatabase().load(() => {
this.#restoreToPreviousDatabase("navyunitdatabase", "Navy Unit database", () => {
this.#app?.getNavyUnitDatabase().load(() => {
this.#loadDatabases();
this.#app?.getServerManager().reloadDatabases(() => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("Olympus core databases reloaded");
})
this.#hideAll();
this.#aircraftEditor.show();
this.#button1.classList.add("selected");
});
});
});
});
});
});
});
});
}
/** Upload a single database to the server
*
* @param database The database
* @param name The name of the database as it will be saved on the server
* @param label A label used in the info popup
*/
#uploadDatabase(database: { blueprints: { [key: string]: UnitBlueprint } }, name: string, label: string, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", "/api/databases/save/units/" + name);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) {
this.#app?.getPopupsManager().get("infoPopup")?.setText(label + " saved successfully");
callback();
}
else {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while saving the " + label);
}
};
xmlHttp.onerror = (res: any) => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while saving the " + label);
}
xmlHttp.send(JSON.stringify(database));
}
/** Resets a database to its default values on the server. NOTE: this only resets the database on the server, it will not reload it in the app.
*
* @param name The name of the database as it is saved on the server
* @param label A label used in the info popup
* @param callback Called when the operation is completed
*/
#resetToDefaultDatabase(name: string, label: string, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", "/api/databases/reset/units/" + name);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) {
this.#app?.getPopupsManager().get("infoPopup")?.setText(label + " reset successfully");
callback();
}
else {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while resetting the " + label);
}
};
xmlHttp.onerror = (res: any) => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while resetting the " + label)
}
xmlHttp.send("");
}
/** Restores a database to its previously saved values on the server. NOTE: this only restores the database on the server, it will not reload it in the app.
*
* @param name The name of the database as it is saved on the server
* @param label A label used in the info popup
* @param callback Called when the operation is completed
*/
#restoreToPreviousDatabase(name: string, label: string, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", "/api/databases/restore/units/" + name);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) {
this.#app?.getPopupsManager().get("infoPopup")?.setText(label + " restored successfully");
callback();
}
else {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while restoring the " + label);
}
};
xmlHttp.onerror = (res: any) => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while restoring the " + label)
}
xmlHttp.send("");
}
}

View File

@@ -0,0 +1,77 @@
import { UnitBlueprint } from "interfaces";
import { UnitEditor } from "./uniteditor";
import { addCheckboxInput, addDropdownInput, addStringInput } from "./utils";
/** Database editor for ground units
*
*/
export class GroundUnitEditor extends UnitEditor {
#blueprint: UnitBlueprint | null = null;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
super(contentDiv1, contentDiv2, contentDiv3);
}
/** Sets a unit blueprint as the currently active one
*
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint: UnitBlueprint) {
this.#blueprint = blueprint;
if (this.#blueprint !== null) {
this.contentDiv2.replaceChildren();
var title = document.createElement("label");
title.innerText = "Unit properties";
this.contentDiv2.appendChild(title);
addStringInput(this.contentDiv2, "Name", blueprint.name, "text", (value: string) => {blueprint.name = value; }, true);
addStringInput(this.contentDiv2, "Label", blueprint.label, "text", (value: string) => {blueprint.label = value; });
addStringInput(this.contentDiv2, "Short label", blueprint.shortLabel, "text", (value: string) => {blueprint.shortLabel = value; });
addStringInput(this.contentDiv2, "Type", blueprint.type?? "", "text", (value: string) => {blueprint.type = value; });
addStringInput(this.contentDiv2, "Unit when grouped", blueprint.unitWhenGrouped?? "", "text", (value: string) => {blueprint.unitWhenGrouped = value; });
addDropdownInput(this.contentDiv2, "Coalition", blueprint.coalition, ["", "blue", "red"], (value: string) => {blueprint.coalition = value; });
addDropdownInput(this.contentDiv2, "Era", blueprint.era, ["WW2", "Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], (value: string) => {blueprint.era = value; });
//addStringInput(this.contentDiv2, "Filename", blueprint.filename?? "", "text", (value: string) => {blueprint.filename = value; });
addStringInput(this.contentDiv2, "Cost", String(blueprint.cost)?? "", "number", (value: string) => {blueprint.cost = parseFloat(value); });
addStringInput(this.contentDiv2, "Acquisition range [m]", String(blueprint.acquisitionRange)?? "", "number", (value: string) => {blueprint.acquisitionRange = parseFloat(value); });
addStringInput(this.contentDiv2, "Engagement range [m]", String(blueprint.engagementRange)?? "", "number", (value: string) => {blueprint.engagementRange = parseFloat(value); });
addStringInput(this.contentDiv2, "Targeting range [m]", String(blueprint.targetingRange)?? "", "number", (value: string) => {blueprint.targetingRange = parseFloat(value); });
addStringInput(this.contentDiv2, "Aim method range [m]", String(blueprint.aimMethodRange)?? "", "number", (value: string) => {blueprint.aimMethodRange = parseFloat(value); });
addStringInput(this.contentDiv2, "Barrel height [m]", String(blueprint.barrelHeight)?? "", "number", (value: string) => {blueprint.barrelHeight = parseFloat(value); });
addStringInput(this.contentDiv2, "Muzzle velocity [m/s]", String(blueprint.muzzleVelocity)?? "", "number", (value: string) => {blueprint.muzzleVelocity = parseFloat(value); });
addStringInput(this.contentDiv2, "Aim time [s]", String(blueprint.aimTime)?? "", "number", (value: string) => {blueprint.aimTime = parseFloat(value); });
addStringInput(this.contentDiv2, "Shots to fire", String(blueprint.shotsToFire)?? "", "number", (value: string) => {blueprint.shotsToFire = Math.round(parseFloat(value)); });
addStringInput(this.contentDiv2, "Shots base interval [s]", String(blueprint.shotsBaseInterval)?? "", "number", (value: string) => {blueprint.shotsBaseInterval = Math.round(parseFloat(value)); });
addStringInput(this.contentDiv2, "Shots base scatter [°]", String(blueprint.shotsBaseScatter)?? "", "number", (value: string) => {blueprint.shotsBaseScatter = Math.round(parseFloat(value)); });
addStringInput(this.contentDiv2, "Alertness time constant [s]", String(blueprint.alertnessTimeConstant)?? "", "number", (value: string) => {blueprint.alertnessTimeConstant = Math.round(parseFloat(value)); });
addCheckboxInput(this.contentDiv2, "Can target point", blueprint.canTargetPoint ?? false, (value: boolean) => {blueprint.canTargetPoint = value;})
addCheckboxInput(this.contentDiv2, "Can rearm", blueprint.canRearm ?? false, (value: boolean) => {blueprint.canRearm = value;})
addCheckboxInput(this.contentDiv2, "Can operate as AAA", blueprint.canAAA ?? false, (value: boolean) => {blueprint.canAAA = value;})
addCheckboxInput(this.contentDiv2, "Indirect fire (e.g. mortar)", blueprint.indirectFire ?? false, (value: boolean) => {blueprint.indirectFire = value;})
addStringInput(this.contentDiv2, "Description", blueprint.description ?? "", "text", (value: string) => {blueprint.description = value; });
addStringInput(this.contentDiv2, "Tags", blueprint.tags ?? "", "text", (value: string) => {blueprint.tags = value; });
addStringInput(this.contentDiv2, "Marker file", blueprint.markerFile ?? "", "text", (value: string) => {blueprint.markerFile = value; });
}
}
/** Add a new empty blueprint
*
* @param key Blueprint key
*/
addBlueprint(key: string) {
if (this.database != null) {
this.database.blueprints[key] = {
name: key,
coalition: "",
label: "",
shortLabel: "",
era: "",
enabled: true
}
this.show();
this.setBlueprint(this.database.blueprints[key]);
}
}
}

View File

@@ -0,0 +1,5 @@
import { DatabaseManagerPlugin } from "./databasemanagerplugin";
globalThis.getOlympusPlugin = () => {
return new DatabaseManagerPlugin();
}

View File

@@ -0,0 +1,55 @@
import { LoadoutBlueprint } from "interfaces";
import { addLoadoutItemsEditor, addStringInput, arrayToString, stringToArray } from "./utils";
/** The LoadoutEditor allows the user to edit a loadout
*
*/
export class LoadoutEditor {
#contentDiv: HTMLElement;
#loadout: LoadoutBlueprint | null = null;
#visible: boolean = false;
constructor(contentDiv: HTMLElement) {
this.#contentDiv = contentDiv;
this.#contentDiv.addEventListener("refresh", () => {
if (this.#visible)
this.show();
})
}
/** Set the loadout to edit
*
* @param loadout The loadout to edit
*/
setLoadout(loadout: LoadoutBlueprint) {
this.#loadout = loadout;
}
/** Show the editor
*
*/
show() {
this.#visible = true;
this.#contentDiv.replaceChildren();
var title = document.createElement("label");
title.innerText = "Loadout properties";
this.#contentDiv.appendChild(title);
if (this.#loadout) {
var loadout = this.#loadout;
addStringInput(this.#contentDiv, "Name", loadout.name, "text", (value: string) => {loadout.name = value; this.#contentDiv.dispatchEvent(new Event("refresh"));});
addStringInput(this.#contentDiv, "Code", loadout.code, "text", (value: string) => {loadout.code = value; });
addStringInput(this.#contentDiv, "Roles", arrayToString(loadout.roles), "text", (value: string) => {loadout.roles = stringToArray(value);});
addLoadoutItemsEditor(this.#contentDiv, this.#loadout);
}
}
/** Hide the editor
*
*/
hide() {
this.#visible = false;
this.#contentDiv.replaceChildren();
}
}

View File

@@ -0,0 +1,60 @@
import { UnitBlueprint } from "interfaces";
import { UnitEditor } from "./uniteditor";
import { addDropdownInput, addStringInput } from "./utils";
/** Database editor for navy units
*
*/
export class NavyUnitEditor extends UnitEditor {
#blueprint: UnitBlueprint | null = null;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
super(contentDiv1, contentDiv2, contentDiv3);
}
/** Sets a unit blueprint as the currently active one
*
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint: UnitBlueprint) {
this.#blueprint = blueprint;
if (this.#blueprint !== null) {
this.contentDiv2.replaceChildren();
var title = document.createElement("label");
title.innerText = "Unit properties";
this.contentDiv2.appendChild(title);
addStringInput(this.contentDiv2, "Name", blueprint.name, "text", (value: string) => {blueprint.name = value; }, true);
addStringInput(this.contentDiv2, "Label", blueprint.label, "text", (value: string) => {blueprint.label = value; });
addStringInput(this.contentDiv2, "Short label", blueprint.shortLabel, "text", (value: string) => {blueprint.shortLabel = value; });
addStringInput(this.contentDiv2, "Type", blueprint.type?? "", "text", (value: string) => {blueprint.type = value; });
addDropdownInput(this.contentDiv2, "Coalition", blueprint.coalition, ["", "blue", "red"], (value: string) => {blueprint.coalition = value; });
addDropdownInput(this.contentDiv2, "Era", blueprint.era, ["WW2", "Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], (value: string) => {blueprint.era = value; });
//addStringInput(this.contentDiv2, "Filename", blueprint.filename?? "", "text", (value: string) => {blueprint.filename = value; });
addStringInput(this.contentDiv2, "Cost", String(blueprint.cost)?? "", "number", (value: string) => {blueprint.cost = parseFloat(value); });
addStringInput(this.contentDiv2, "Barrel height [m]", String(blueprint.barrelHeight)?? "", "number", (value: string) => {blueprint.barrelHeight = parseFloat(value); });
addStringInput(this.contentDiv2, "Muzzle velocity [m/s]", String(blueprint.muzzleVelocity)?? "", "number", (value: string) => {blueprint.muzzleVelocity = parseFloat(value); });
}
}
/** Add a new empty blueprint
*
* @param key Blueprint key
*/
addBlueprint(key: string) {
if (this.database != null) {
this.database.blueprints[key] = {
name: key,
coalition: "",
label: "",
shortLabel: "",
era: "",
enabled: true
}
this.show();
this.setBlueprint(this.database.blueprints[key]);
}
}
}

View File

@@ -0,0 +1,117 @@
import { UnitBlueprint } from "interfaces";
import { UnitDatabase } from "unit/databases/unitdatabase";
import { addBlueprintsScroll, addNewElementInput } from "./utils";
/** Base abstract class of Unit database editors
*
*/
export abstract class UnitEditor {
blueprint: UnitBlueprint | null = null;
database: {blueprints: {[key: string]: UnitBlueprint}} | null = null;
visible: boolean = false;
contentDiv1: HTMLElement;
contentDiv2: HTMLElement;
contentDiv3: HTMLElement;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
this.contentDiv1 = contentDiv1;
this.contentDiv2 = contentDiv2;
this.contentDiv3 = contentDiv3;
/* Refresh the list of units if it changes */
this.contentDiv1.addEventListener("refresh", () => {
if (this.visible)
this.show();
})
/* If the unit properties or loadout are edited, reload the editor */
this.contentDiv2.addEventListener("refresh", () => {
if (this.visible) {
if (this.blueprint !== null)
this.setBlueprint(this.blueprint);
}
});
this.contentDiv3.addEventListener("refresh", () => {
if (this.visible) {
if (this.blueprint !== null)
this.setBlueprint(this.blueprint);
}
});
}
/**
*
* @param database The database that the editor will operate on
*/
setDatabase(database: UnitDatabase) {
this.database = JSON.parse(JSON.stringify({blueprints: database.getBlueprints(true)}));
}
/** Show the editor
* @param filter String filter
*/
show(filter: string = "") {
this.visible = true;
this.contentDiv1.replaceChildren();
this.contentDiv2.replaceChildren();
this.contentDiv3.replaceChildren();
/* Create the list of units. Each unit is clickable to activate the editor on it */
if (this.database != null) {
var title = document.createElement("label");
title.innerText = "Units list";
this.contentDiv1.appendChild(title);
var filterInput = document.createElement("input");
filterInput.value = filter;
this.contentDiv1.appendChild(filterInput);
filterInput.onchange = (e: Event) => {
this.show((e.target as HTMLInputElement).value);
}
this.addBlueprints(filter);
}
}
/** Hide the editor
*
*/
hide() {
this.visible = false;
this.contentDiv1.replaceChildren();
this.contentDiv2.replaceChildren();
this.contentDiv3.replaceChildren();
}
/**
*
* @returns The edited database
*/
getDatabase() {
return this.database;
}
/**
*
* @param filter String filter
*/
addBlueprints(filter: string = "") {
if (this.database) {
addBlueprintsScroll(this.contentDiv1, this.database, filter, (key: string) => {
if (this.database != null)
this.setBlueprint(this.database.blueprints[key])
});
addNewElementInput(this.contentDiv1, (ev: MouseEvent, input: HTMLInputElement) => {
if (input.value != "")
this.addBlueprint((input).value);
});
}
}
/* Abstract methods which will depend on the specific type of units */
abstract setBlueprint(blueprint: UnitBlueprint): void;
abstract addBlueprint(key: string): void;
}

View File

@@ -0,0 +1,297 @@
import { LoadoutBlueprint, LoadoutItemBlueprint, UnitBlueprint } from "interfaces";
/** This file contains a set of utility functions that are reused in the various editors and allows to declutter the classes
*
*/
/** Add a string input in the form of String: [ value ]
*
* @param div The HTMLElement that will contain the input
* @param key The key of the input, which will be used as label
* @param value The initial value of the input
* @param type The type of the input, e.g. "Text" or "Number" as per html standard
* @param callback Callback called when the user enters a new value
* @param disabled If true, the input will be disabled and read only
*/
export function addStringInput(div: HTMLElement, key: string, value: string, type: string, callback: CallableFunction, disabled?: boolean) {
var row = document.createElement("div");
var dt = document.createElement("dt");
var dd = document.createElement("dd");
dt.innerText = key;
var input = document.createElement("input");
input.value = value;
input.textContent = value;
input.type = type?? "text";
input.disabled = disabled?? false;
input.onchange = () => callback(input.value);
dd.appendChild(input);
row.appendChild(dt);
row.appendChild(dd);
row.classList.add("input-row");
div.appendChild(row);
}
/** Add a dropdown (select) input
*
* @param div The HTMLElement that will contain the input
* @param key The key of the input, which will be used as label
* @param value The initial value of the input
* @param options The dropdown options
*/
export function addDropdownInput(div: HTMLElement, key: string, value: string, options: string[], callback: CallableFunction, disabled?: boolean) {
var row = document.createElement("div");
var dt = document.createElement("dt");
var dd = document.createElement("dd");
dt.innerText = key;
var select = document.createElement("select");
options.forEach((option: string) => {
var el = document.createElement("option");
el.value = option;
el.innerText = option;
select.appendChild(el);
});
select.value = value;
select.disabled = disabled?? false;
select.onchange = () => callback(select.value);
dd.appendChild(select);
row.appendChild(dt);
row.appendChild(dd);
row.classList.add("input-row");
div.appendChild(row);
}
/** Add a checkbox input in the form of String: [ value ]
*
* @param div The HTMLElement that will contain the input
* @param key The key of the input, which will be used as label
* @param value The initial value of the input
* @param callback Callback called when the user enters a new value
* @param disabled If true, the input will be disabled and read only
*/
export function addCheckboxInput(div: HTMLElement, key: string, value: boolean, callback: CallableFunction, disabled?: boolean) {
var row = document.createElement("div");
var dt = document.createElement("dt");
var dd = document.createElement("dd");
dt.innerText = key;
var input = document.createElement("input");
input.checked = value;
input.type = "checkbox";
input.disabled = disabled?? false;
input.onchange = () => callback(input.checked);
dd.appendChild(input);
row.appendChild(dt);
row.appendChild(dd);
row.classList.add("input-row");
div.appendChild(row);
}
/** Create a loadout items editor. This editor allows to add or remove loadout items, as well as changing their name and quantity
*
* @param div The HTMLElement that will contain the editor
* @param loadout The loadout to edit
*/
export function addLoadoutItemsEditor(div: HTMLElement, loadout: LoadoutBlueprint) {
var itemsEl = document.createElement("div");
itemsEl.classList.add("dm-scroll-container", "dm-items-container");
/* Create a row for each loadout item to allow and change the name and quantity of the item itself */
loadout.items.sort((a: LoadoutItemBlueprint, b: LoadoutItemBlueprint) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}));
loadout.items.forEach((item: LoadoutItemBlueprint, index: number) => {
var rowDiv = document.createElement("div");
var nameLabel = document.createElement("label");
nameLabel.innerText = "Name"
rowDiv.appendChild(nameLabel);
var nameInput = document.createElement("input");
rowDiv.appendChild(nameInput);
nameInput.textContent = item.name;
nameInput.value = item.name
nameInput.onchange = () => { loadout.items[index].name = nameInput.value; }
var quantityLabel = document.createElement("label");
quantityLabel.innerText = "Quantity"
rowDiv.appendChild(quantityLabel);
var quantityInput = document.createElement("input");
rowDiv.appendChild(quantityInput);
quantityInput.textContent = String(item.quantity);
quantityInput.value = String(item.quantity);
quantityInput.type = "number";
quantityInput.step = "1";
quantityInput.onchange = () => { loadout.items[index].quantity = parseInt(quantityInput.value); }
/* This button allows to remove the item */
var button = document.createElement("button");
button.innerText = "X";
button.onclick = () => {
loadout.items.splice(index, 1);
div.dispatchEvent(new Event("refresh"));
}
rowDiv.appendChild(button);
itemsEl.appendChild(rowDiv);
})
div.appendChild(itemsEl);
/* Button to add a new item to the loadout */
var inputDiv = document.createElement("div");
inputDiv.classList.add("dm-new-item-input");
var button = document.createElement("button");
button.innerText = "Add";
inputDiv.appendChild(button);
div.appendChild(inputDiv);
button.addEventListener("click", (ev: MouseEvent) => {
loadout?.items.push({
name: "",
quantity: 1
})
div.dispatchEvent(new Event("refresh"));
});
}
/** Add a input and button to create a new element in a list. It uses a generic callback to actually add the element.
*
* @param div The HTMLElement that will contain the input and button
* @param callback Callback called when the user clicks on "Add"
*/
export function addNewElementInput(div: HTMLElement, callback: CallableFunction) {
var inputDiv = document.createElement("div");
inputDiv.classList.add("dm-new-element-input");
var input = document.createElement("input");
inputDiv.appendChild(input);
var button = document.createElement("button");
button.innerText = "Add";
button.addEventListener("click", (ev: MouseEvent) => callback(ev, input));
inputDiv.appendChild(button);
div.appendChild(inputDiv);
}
/** Add a scrollable list of blueprints
*
* @param div The HTMLElement that will contain the list
* @param database The database that will be used to fill the list of blueprints
* @param filter A string filter that will be executed to filter the blueprints to add
* @param callback Callback called when the user clicks on one of the elements
*/
export function addBlueprintsScroll(div: HTMLElement, database: {blueprints: {[key: string]: UnitBlueprint}}, filter: string, callback: CallableFunction) {
var scrollDiv = document.createElement("div");
scrollDiv.classList.add("dm-scroll-container");
if (database !== null) {
var blueprints: {[key: string]: UnitBlueprint} = database.blueprints;
for (let key of Object.keys(blueprints).sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}))) {
var addKey = true;
if (filter !== "") {
try {
var blueprint = blueprints[key];
addKey = eval(filter);
} catch {
console.error("An error has occurred evaluating the blueprint filter")
}
}
if (addKey) {
var rowDiv = document.createElement("div");
scrollDiv.appendChild(rowDiv);
let text = document.createElement("div");
text.innerHTML = `<div>${key}</div> <div>${blueprints[key].label}</div>`;
text.onclick = () => {
callback(key);
const collection = document.getElementsByClassName("blueprint-selected");
for (let i = 0; i < collection.length; i++) {
collection[i].classList.remove("blueprint-selected");
}
text.classList.add("blueprint-selected");
}
rowDiv.appendChild(text);
let checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = blueprints[key].enabled;
checkbox.onclick = () => {
console.log(checkbox.checked);
blueprints[key].enabled = checkbox.checked;
}
rowDiv.appendChild(checkbox);
/* This button allows to remove an element from the list. It requires a refresh. */
var button = document.createElement("button");
button.innerText = "X";
button.onclick = () => {
delete blueprints[key];
div.dispatchEvent(new Event("refresh"));
}
rowDiv.appendChild(button);
}
}
}
div.appendChild(scrollDiv);
}
/** Add a scrollable list of loadouts
*
* @param div The HTMLElement that will contain the list
* @param loadouts The loadouts that will be used to fill the list
* @param callback Callback called when the user clicks on one of the elements
*/
export function addLoadoutsScroll(div: HTMLElement, loadouts: LoadoutBlueprint[], callback: CallableFunction) {
var loadoutsEl = document.createElement("div");
loadoutsEl.classList.add("dm-scroll-container", "dm-loadout-container")
loadouts.sort((a: LoadoutBlueprint, b: LoadoutBlueprint) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}));
loadouts.forEach((loadout: LoadoutBlueprint, index: number) => {
var rowDiv = document.createElement("div");
loadoutsEl.appendChild(rowDiv);
var text = document.createElement("label");
text.textContent = loadout.name;
text.onclick = () => { callback(loadout) };
rowDiv.appendChild(text);
/* The "Empty loadout" can not be removed */
if (loadout.name !== "Empty loadout") {
let checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = loadout.enabled;
checkbox.onclick = () => {
console.log(checkbox.checked);
loadout.enabled = checkbox.checked;
}
rowDiv.appendChild(checkbox);
/* This button allows to remove an element from the list. It requires a refresh. */
var button = document.createElement("button");
button.innerText = "X";
button.onclick = () => {
loadouts.splice(index, 1);
div.dispatchEvent(new Event("refresh"));
}
rowDiv.appendChild(button);
}
});
div.appendChild(loadoutsEl);
}
/** Converts an array of string into a single string like [val1, val2, val3]
*
* @param array The input array of strings
* @returns The string
*/
export function arrayToString(array: string[]) {
return "[" + array.join( ", " ) + "]";
}
/** Converts an a single string like [val1, val2, val3] into an array
*
* @param input The input string
* @returns The array
*/
export function stringToArray(input: string) {
return input.match( /(\w)+/g ) ?? [];
}