diff --git a/client/demo.js b/client/demo.js index 5db2660c..5d7dfc7c 100644 --- a/client/demo.js +++ b/client/demo.js @@ -53,7 +53,7 @@ class DemoDataGenerator { } - /* + // UNCOMMENT TO TEST ALL UNITS **************** var databases = Object.assign({}, aircraftDatabase, helicopterDatabase, groundUnitDatabase, navyUnitDatabase); @@ -70,6 +70,7 @@ class DemoDataGenerator { DEMO_UNIT_DATA[idx].groupName = `Group-${idx}`; DEMO_UNIT_DATA[idx].position.lat += latIdx / 5; DEMO_UNIT_DATA[idx].position.lng += lngIdx / 5; + DEMO_UNIT_DATA[idx].coalition = Math.floor(Math.random() * 3) latIdx += 1; if (latIdx === l) { @@ -89,9 +90,9 @@ class DemoDataGenerator { idx += 1; } } - */ - + + /* let idx = 1; DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData)); DEMO_UNIT_DATA[idx].name = "S_75M_Volhov"; @@ -152,7 +153,7 @@ class DemoDataGenerator { DEMO_UNIT_DATA[idx].position.lat += idx / 100; DEMO_UNIT_DATA[idx].category = "Aircraft"; DEMO_UNIT_DATA[idx].isLeader = true; - + */ this.startTime = Date.now(); } diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index c9269623..82c47702 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -1290,6 +1290,17 @@ dl.ol-data-grid dd { margin: 4px 0; } +.ol-dialog-content table th { + background-color: var(--background-grey); + color:white; + font-size:14px; + font-weight: normal; +} + +.ol-dialog-content table tbody th { + text-align: left; +} + .ol-dialog-footer { align-content: center; border-top: 1px solid var(--background-grey); @@ -1330,6 +1341,10 @@ dl.ol-data-grid dd { width: 16px; } +.ol-checkbox input[type="checkbox"]:disabled:before { + opacity: 10%; +} + .ol-checkbox input[type="checkbox"]:checked::before { background-image: url("/resources/theme/images/icons/square-check-solid.svg"); } @@ -1574,3 +1589,86 @@ input[type=number]::-webkit-outer-spin-button { .ol-log-entry { border-bottom: 1px solid #FFFFFF44; } + +.file-import-export { + max-width: 500px; +} + +.file-import-export .ol-dialog-content { + display:flex; + flex-direction: column; + justify-content: center; +} + +.file-import-export p { + background-color: var(--background-grey); + border-left:6px solid var(--secondary-blue-text); + padding:12px; +} + +.file-import-export th { + padding:4px 6px; +} + +.file-import-export tr td:first-child { + text-align: left; +} + +.file-import-export td { + color:white; + text-align: center; +} + +.file-import-export .ol-checkbox { + display:flex; + justify-content: center; +} + +.file-import-export .ol-checkbox input::before { + margin-right: 0; +} + +.file-import-export .ol-checkbox span { + display:none; +} + +.file-import-export button.start-transfer { + background-color: var(--secondary-blue-text); + border-color: var(--secondary-blue-text); +} + +.file-import-export .export-filename-container { + display: flex; + column-gap: 15px; + width: 100%; + align-items: center; + padding: 10px 0px; + color: white; + font-size: 14px; +} + +.file-import-export .export-filename-container input { + width: 100%; + background-color: var(--background-grey); + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + border-style: solid; + border: 1px solid var(--background-steel); + color: white; + font-size: 14px; + padding: 4px; +} + +.file-import-export .export-filename-container img { + height: 16px; + width: 16px; + filter: invert(100%); + margin-left: -31px; + transform: translateX(-15px); + pointer-events: none; +} + +.file-import-export .ol-dialog-footer button:first-of-type{ + margin-left: auto; +} \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/keyboard-solid.svg b/client/public/themes/olympus/images/icons/keyboard-solid.svg new file mode 100644 index 00000000..8838d567 --- /dev/null +++ b/client/public/themes/olympus/images/icons/keyboard-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index b2dec3ce..5acce533 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -167,6 +167,7 @@ export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area"; export const visibilityControls: string[] = ["human", "dcs", "aircraft", "helicopter", "groundunit-sam", "groundunit", "navyunit", "airbase"]; export const visibilityControlsTypes: string[][] = [["human"], ["dcs"], ["aircraft"], ["helicopter"], ["groundunit-sam"], ["groundunit"], ["navyunit"], ["airbase"]]; export const visibilityControlsTooltips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle helicopter visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; + export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{ "name": "Human", "image": "visibility/human.svg", @@ -213,7 +214,7 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{ export const IADSTypes = ["AAA", "MANPADS", "SAM Site", "Radar"]; export const IADSDensities: { [key: string]: number } = { "AAA": 0.8, "MANPADS": 0.3, "SAM Site": 0.1, "Radar": 0.05 }; - +export const GROUND_UNIT_AIR_DEFENCE_REGEX:RegExp = /(\b(AAA|SAM|MANPADS?|[mM]anpads?)|[sS]tinger\b)/; export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out"; export const SHOW_UNIT_LABELS = "Show unit labels (L)"; export const SHOW_UNITS_ENGAGEMENT_RINGS = "Show units threat range rings (Q)"; diff --git a/client/src/controls/unitspawnmenu.ts b/client/src/controls/unitspawnmenu.ts index 5fd5b57d..b0929848 100644 --- a/client/src/controls/unitspawnmenu.ts +++ b/client/src/controls/unitspawnmenu.ts @@ -3,7 +3,7 @@ import { Dropdown } from "./dropdown"; import { Slider } from "./slider"; import { UnitDatabase } from "../unit/databases/unitdatabase"; import { getApp } from ".."; -import { GAME_MASTER } from "../constants/constants"; +import { GAME_MASTER, GROUND_UNIT_AIR_DEFENCE_REGEX } from "../constants/constants"; import { Airbase } from "../mission/airbase"; import { ftToM } from "../other/utils"; import { aircraftDatabase } from "../unit/databases/aircraftdatabase"; @@ -616,7 +616,7 @@ export class HelicopterSpawnMenu extends UnitSpawnMenu { export class GroundUnitSpawnMenu extends UnitSpawnMenu { protected showRangeCircles: boolean = true; - protected unitTypeFilter = (unit:any) => {return !(/\bAAA|SAM\b/.test(unit.type) || /\bmanpad|stinger\b/i.test(unit.type))}; + protected unitTypeFilter = (unit:any) => {return !(GROUND_UNIT_AIR_DEFENCE_REGEX.test(unit.type))}; /** * @@ -657,7 +657,7 @@ export class GroundUnitSpawnMenu extends UnitSpawnMenu { export class AirDefenceUnitSpawnMenu extends GroundUnitSpawnMenu { - protected unitTypeFilter = (unit:any) => {return /\bAAA|SAM\b/.test(unit.type) || /\bmanpad|stinger\b/i.test(unit.type)}; + protected unitTypeFilter = (unit:any) => {return GROUND_UNIT_AIR_DEFENCE_REGEX.test(unit.type)}; /** * diff --git a/client/src/interfaces.ts b/client/src/interfaces.ts index 16f3ec57..1e8f8fb9 100644 --- a/client/src/interfaces.ts +++ b/client/src/interfaces.ts @@ -139,6 +139,7 @@ export interface Offset { export interface UnitData { category: string, + categoryDisplayName: string, ID: number; alive: boolean; human: boolean; diff --git a/client/src/mission/missionmanager.ts b/client/src/mission/missionmanager.ts index 17fd868a..139f35ae 100644 --- a/client/src/mission/missionmanager.ts +++ b/client/src/mission/missionmanager.ts @@ -35,6 +35,10 @@ export class MissionManager { this.#commandModeErasDropdown = new Dropdown("command-mode-era-options", () => {}); } + /** Update location of bullseyes + * + * @param object + */ updateBullseyes(data: BullseyesData) { const commandMode = getApp().getMissionManager().getCommandModeOptions().commandMode; for (let idx in data.bullseyes) { @@ -56,6 +60,10 @@ export class MissionManager { } } + /** Update airbase information + * + * @param object + */ updateAirbases(data: AirbasesData) { for (let idx in data.airbases) { var airbase = data.airbases[idx] @@ -75,6 +83,10 @@ export class MissionManager { } } + /** Update mission information + * + * @param object + */ updateMission(data: MissionData) { if (data.mission) { @@ -109,30 +121,63 @@ export class MissionManager { } } + /** Get the bullseyes set in this theatre + * + * @returns object + */ getBullseyes() { return this.#bullseyes; } + /** Get the airbases in this theatre + * + * @returns object + */ getAirbases() { return this.#airbases; } + /** Get the options/settings as set in the command mode + * + * @returns object + */ getCommandModeOptions() { return this.#commandModeOptions; } + /** Get the current date and time of the mission (based on local time) + * + * @returns object + */ getDateAndTime() { return this.#dateAndTime; } + /** + * Get the number of seconds left of setup time + * @returns number + */ getRemainingSetupTime() { return this.#remainingSetupTime; } + /** Get an object with the coalitions in it + * + * @returns object + */ getCoalitions() { return this.#coalitions; } + /** Get the current theatre (map) name + * + * @returns string + */ + getTheatre() { + return this.#theatre; + } + + getAvailableSpawnPoints() { if (this.getCommandModeOptions().commandMode === GAME_MASTER) return Infinity; diff --git a/client/src/olympusapp.ts b/client/src/olympusapp.ts index 71251942..4fc7102d 100644 --- a/client/src/olympusapp.ts +++ b/client/src/olympusapp.ts @@ -411,6 +411,7 @@ export class OlympusApp { // TODO: move from here in dedicated class document.addEventListener("closeDialog", (ev: CustomEventInit) => { ev.detail._element.closest(".ol-dialog").classList.add("hide"); + document.getElementById("gray-out")?.classList.toggle("hide", true); }); /* Try and connect with the Olympus REST server */ diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 3dbf2e1e..c92c2f7d 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -434,14 +434,24 @@ export function convertDateAndTimeToDate(dateAndTime: DateAndTime) { return new Date(year, month, date.Day, time.h, time.m, time.s); } -export function createCheckboxOption(value: string, text: string, checked: boolean = true, callback: CallableFunction = (ev: any) => {}) { +export function createCheckboxOption(value: string, text: string, checked: boolean = true, callback: CallableFunction = (ev: any) => {}, options?:any) { + options = { + "disabled": false, + "name": "", + "readOnly": false, + ...options + }; var div = document.createElement("div"); div.classList.add("ol-checkbox"); var label = document.createElement("label"); label.title = text; var input = document.createElement("input"); input.type = "checkbox"; - input.checked = checked; + input.checked = checked; + input.name = options.name; + input.disabled = options.disabled; + input.readOnly = options.readOnly; + input.value = value; var span = document.createElement("span"); span.innerText = value; label.appendChild(input); diff --git a/client/src/unit/importexport/unitdatafile.ts b/client/src/unit/importexport/unitdatafile.ts new file mode 100644 index 00000000..2a0c1611 --- /dev/null +++ b/client/src/unit/importexport/unitdatafile.ts @@ -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 += `${categoryMap[category as keyof typeof categoryMap]}`; + + coalitions.forEach((coalition: string) => { + if (index === 0) + headersHTML += `${coalition[0].toUpperCase() + coalition.substring(1)}`; + + 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 += `${checkboxHTML}`; + + }); + matrixHTML += ""; + }); + + const table = this.dialog.getElement().querySelector("table.categories-coalitions"); + + (table.tHead).innerHTML = ` ${headersHTML}`; + (table.querySelector(`tbody`)).innerHTML = matrixHTML; + + } + + #getCategoriesFromData() { + const categories = Object.keys(this.data); + categories.sort(); + return categories; + } + + getData() { + return this.data; + } +} \ No newline at end of file diff --git a/client/src/unit/importexport/unitdatafileexport.ts b/client/src/unit/importexport/unitdatafileexport.ts new file mode 100644 index 00000000..1bdb0d22 --- /dev/null +++ b/client/src/unit/importexport/unitdatafileexport.ts @@ -0,0 +1,97 @@ +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[]) { + 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((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(); + } + +} \ No newline at end of file diff --git a/client/src/unit/importexport/unitdatafileimport.ts b/client/src/unit/importexport/unitdatafileimport.ts new file mode 100644 index 00000000..deeea4a1 --- /dev/null +++ b/client/src/unit/importexport/unitdatafileimport.ts @@ -0,0 +1,138 @@ +import { getApp } from "../.."; +import { Dialog } from "../../dialog/dialog"; +import { UnitData } from "../../interfaces"; +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((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, true); + } + + /* + for (let groupName in groups) { + if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: UnitData) => { return unit.category == "GroundUnit"; }) || groups[groupName].every((unit: any) => { return unit.category == "NavyUnit"; }))) { + var aliveUnits = groups[groupName].filter((unit: UnitData) => { return unit.alive }); + var units = aliveUnits.map((unit: UnitData) => { + return { unitType: unit.name, location: unit.position, liveryID: "" } + }); + getApp().getUnitsManager().spawnUnits(groups[groupName][0].category, units, groups[groupName][0].coalition, true); + } + } + //*/ + } + + #showForm() { + 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); + }); + + } + + /* + groups.filter((unit:Unit) => unitCanBeImported(unit)).forEach((unit:Unit) => { + const category = unit.getCategoryLabel(); + 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(); + } + + 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) => { + this.#fileData = JSON.parse(e.target.result); + this.#showForm(); + }; + reader.readAsText(file); + }) + input.click(); + } + + #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); + } + +} \ No newline at end of file diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts index 11238d66..1cf901df 100644 --- a/client/src/unit/unit.ts +++ b/client/src/unit/unit.ts @@ -5,7 +5,7 @@ import { CustomMarker } from '../map/markers/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './databases/unitdatabase'; import { TargetMarker } from '../map/markers/targetmarker'; -import { DLINK, DataIndexes, GAME_MASTER, HIDE_GROUP_MEMBERS, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, ROEs, RWR, SHOW_UNIT_CONTACTS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, VISUAL, emissionsCountermeasures, reactionsToThreat, states, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, GROUPING_ZOOM_TRANSITION, MAX_SHOTS_SCATTER, SHOTS_SCATTER_DEGREES } from '../constants/constants'; +import { DLINK, DataIndexes, GAME_MASTER, HIDE_GROUP_MEMBERS, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, ROEs, RWR, SHOW_UNIT_CONTACTS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, VISUAL, emissionsCountermeasures, reactionsToThreat, states, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, GROUPING_ZOOM_TRANSITION, MAX_SHOTS_SCATTER, SHOTS_SCATTER_DEGREES, GROUND_UNIT_AIR_DEFENCE_REGEX } from '../constants/constants'; import { DataExtractor } from '../server/dataextractor'; import { groundUnitDatabase } from './databases/groundunitdatabase'; import { navyUnitDatabase } from './databases/navyunitdatabase'; @@ -249,6 +249,14 @@ export abstract class Unit extends CustomMarker { */ abstract getDefaultMarker(): string; + /** Get the category but for display use - for the user. (i.e. has spaces in it) + * + * @returns string + */ + getCategoryLabel() { + return ((GROUND_UNIT_AIR_DEFENCE_REGEX.test(this.getType())) ? "Air Defence" : this.getCategory()).replace(/([a-z])([A-Z])/g, "$1 $2"); + } + /********************** Unit data *************************/ /** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor * @@ -341,6 +349,7 @@ export abstract class Unit extends CustomMarker { getData(): UnitData { return { category: this.getCategory(), + categoryDisplayName: this.getCategoryLabel(), ID: this.ID, alive: this.#alive, human: this.#human, diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index da0cd331..4372bf16 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -16,6 +16,8 @@ import { HotgroupPanel } from "../panels/hotgrouppanel"; import { Contact, UnitData, UnitSpawnTable } from "../interfaces"; import { Dialog } from "../dialog/dialog"; import { Group } from "./group"; +import { UnitDataFileExport } from "./importexport/unitdatafileexport"; +import { UnitDataFileImport } from "./importexport/unitdatafileimport"; /** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only * result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows @@ -29,6 +31,8 @@ export class UnitsManager { #slowDeleteDialog!: Dialog; #units: { [ID: number]: Unit }; #groups: { [groupName: string]: Group } = {}; + #unitDataExport!:UnitDataFileExport; + #unitDataImport!:UnitDataFileImport; constructor() { this.#copiedUnits = []; @@ -1134,52 +1138,18 @@ export class UnitsManager { * TODO: Extend to aircraft and helicopters */ exportToFile() { - var unitsToExport: { [key: string]: any } = {}; - for (let ID in this.#units) { - var unit = this.#units[ID]; - if (!["Aircraft", "Helicopter"].includes(unit.getCategory())) { - var data: any = unit.getData(); - if (unit.getGroupName() in unitsToExport) - unitsToExport[unit.getGroupName()].push(data); - else - unitsToExport[unit.getGroupName()] = [data]; - } - } - var a = document.createElement("a"); - var file = new Blob([JSON.stringify(unitsToExport)], { type: 'text/plain' }); - a.href = URL.createObjectURL(file); - a.download = 'export.json'; - a.click(); + if (!this.#unitDataExport) + this.#unitDataExport = new UnitDataFileExport("unit-export-dialog"); + this.#unitDataExport.showForm(Object.values(this.#units)); } - + /** Import ground and navy units from file * TODO: extend to support aircraft and helicopters */ importFromFile() { - 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 = function (e: any) { - var contents = e.target.result; - var groups = JSON.parse(contents); - for (let groupName in groups) { - if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: UnitData) => { return unit.category == "GroundUnit"; }) || groups[groupName].every((unit: any) => { return unit.category == "NavyUnit"; }))) { - var aliveUnits = groups[groupName].filter((unit: UnitData) => { return unit.alive }); - var units = aliveUnits.map((unit: UnitData) => { - return { unitType: unit.name, location: unit.position, liveryID: "" } - }); - getApp().getUnitsManager().spawnUnits(groups[groupName][0].category, units, groups[groupName][0].coalition, true); - } - } - }; - reader.readAsText(file); - }) - input.click(); + if (!this.#unitDataImport) + this.#unitDataImport = new UnitDataFileImport("unit-import-dialog"); + this.#unitDataImport.selectFile(); } /** Spawn a new group of units diff --git a/client/views/other/dialogs.ejs b/client/views/other/dialogs.ejs index 994616a9..4fa33d6b 100644 --- a/client/views/other/dialogs.ejs +++ b/client/views/other/dialogs.ejs @@ -1,333 +1,19 @@ -
-
-
-

DCS Olympus

-

Dynamic Unit Command

-
Version v0.4.8-alpha
-
- -
-
Name
-
Server password
- -
- -

- - -
-
- -
-
- -
-

Olympus 1-1

-
- -
- - -
-
-

General settings

-
-
-
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
-
- - -
-
-

TACAN options

-
-
-
- -
- -
- - -
-
- -
- -
-
X
-
-
-
- -
- -
-
-
-
- - -
-
-

Radio options

-
-
- -
- - -
-
- -
- -
-
.000
-
-
-
-
-
- -
- - -
-
-
-
-
-
- - - -
- -
-
-
-
-
- - - -
- -
-
- -
-

Custom formation

-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
-
- -
- -
-
- -
- - -
-
- -
- -
-
-
- - -
- - -
-
- -
-

Command mode settings

-
- -
-
- -
- -
- -
- -
- - -
-
- -
- -
-
- -
- - -
-
Select eras
-
- -
-
-
- -
-

Spawn points

-
-
- -
- -
- -
-
- -
- -
- -
-
- -
- - -
- - -
-
-

Confirm deletion method

-
- -
-

You are trying to delete a large amount of units (), which can cause the server to lag for players.

-

You may: -

    -
  • delete in batches (less lag but Olympus cannot process any additional orders until
    all units have been deleted);
  • -
  • delete immediately (you can continue to give Olympus orders but players may
    experience lag while this happens);
  • -
  • cancel this instruction.
  • -

-
- - -
+<%- include('dialogs/advancedsettings.ejs') %> +<%- include('dialogs/commandmodesettings.ejs') %> +<%- include('dialogs/customformation.ejs') %> +<%- include('dialogs/importexport.ejs', { + "dialogId": "unit-export-dialog", + "submitButtonText": "Export units to file", + "textContent": "Select the unit categories you would like to export. Note: only ground and naval units can be exported at this time.", + "title": "Export", + "showFilenameInput": true +}) %> +<%- include('dialogs/importexport.ejs', { + "dialogId": "unit-import-dialog", + "submitButtonText": "Import units into mission", + "textContent": "Select the unit categories you would like to import.", + "title": "Import", + "showFilenameInput": false +}) %> +<%- include('dialogs/slowdelete.ejs') %> +<%- include('dialogs/splash.ejs') %> \ No newline at end of file diff --git a/client/views/other/dialogs/advancedsettings.ejs b/client/views/other/dialogs/advancedsettings.ejs new file mode 100644 index 00000000..ad3222e1 --- /dev/null +++ b/client/views/other/dialogs/advancedsettings.ejs @@ -0,0 +1,162 @@ +
+
+ +
+

Olympus 1-1

+
+ +
+ + +
+
+

General settings

+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + +
+
+

TACAN options

+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+
X
+
+
+
+ +
+ +
+
+
+
+ + +
+
+

Radio options

+
+
+ +
+ + +
+
+ +
+ +
+
.000
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+
+
+
+ + + +
\ No newline at end of file diff --git a/client/views/other/dialogs/commandmodesettings.ejs b/client/views/other/dialogs/commandmodesettings.ejs new file mode 100644 index 00000000..3b278812 --- /dev/null +++ b/client/views/other/dialogs/commandmodesettings.ejs @@ -0,0 +1,70 @@ +
+
+ +
+

Command mode settings

+
+ +
+
+ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+
+ +
+ + +
+
Select eras
+
+ +
+
+
+ +
+

Spawn points

+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + +
\ No newline at end of file diff --git a/client/views/other/dialogs/customformation.ejs b/client/views/other/dialogs/customformation.ejs new file mode 100644 index 00000000..b682121d --- /dev/null +++ b/client/views/other/dialogs/customformation.ejs @@ -0,0 +1,48 @@ +
+
+ +
+

Custom formation

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ +
+
+
+ + +
\ No newline at end of file diff --git a/client/views/other/dialogs/importexport.ejs b/client/views/other/dialogs/importexport.ejs new file mode 100644 index 00000000..3dfecc59 --- /dev/null +++ b/client/views/other/dialogs/importexport.ejs @@ -0,0 +1,29 @@ +
+
+

<%= title %>

+
+ +
+

<%= textContent %>

+ + <% if (showFilenameInput) { %> +
+ + + +
+ <% } %> + + + + + + +
+
+ + +
\ No newline at end of file diff --git a/client/views/other/dialogs/slowdelete.ejs b/client/views/other/dialogs/slowdelete.ejs new file mode 100644 index 00000000..372eff82 --- /dev/null +++ b/client/views/other/dialogs/slowdelete.ejs @@ -0,0 +1,21 @@ +
+
+

Confirm deletion method

+
+ +
+

You are trying to delete a large amount of units (), which can cause the server to lag for players.

+

You may: +

    +
  • delete in batches (less lag but Olympus cannot process any additional orders until
    all units have been deleted);
  • +
  • delete immediately (you can continue to give Olympus orders but players may
    experience lag while this happens);
  • +
  • cancel this instruction.
  • +

+
+ + +
\ No newline at end of file diff --git a/client/views/other/dialogs/splash.ejs b/client/views/other/dialogs/splash.ejs new file mode 100644 index 00000000..2a883842 --- /dev/null +++ b/client/views/other/dialogs/splash.ejs @@ -0,0 +1,50 @@ +
+
+
+

DCS Olympus

+

Dynamic Unit Command

+
Version v0.4.8-alpha
+
+ +
+
Username
+
Password
+ +
+ +

+ + +
+
\ No newline at end of file