From a4db569fbdede9f060182ff66456e56efc2970de Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 18 Jul 2023 21:56:56 +0200 Subject: [PATCH] Performance optimizations for large unit counts --- client/demo.js | 6 +- client/public/stylesheets/layout/layout.css | 2 +- client/public/stylesheets/olympus.css | 1 + .../stylesheets/panels/connectionstatus.css | 2 +- .../olympus/images/units/groundunit-ewr.svg | 60 ++++ .../images/units/groundunit-sam-launcher.svg | 51 +++ .../images/units/groundunit-sam-radar.svg | 50 +++ client/src/@types/dom.d.ts | 1 + client/src/constants/constants.ts | 5 +- client/src/map/map.ts | 9 +- client/src/other/utils.ts | 12 +- client/src/panels/connectionstatuspanel.ts | 8 + client/src/panels/unitcontrolpanel.ts | 2 +- client/src/server/server.ts | 34 +- client/src/units/groundunitdatabase.ts | 196 +++++++---- client/src/units/unit.ts | 151 +++++--- client/src/units/unitsmanager.ts | 145 +++++--- client/views/other/contextmenus.ejs | 4 +- scripts/OlympusCommand.lua | 133 +++++--- scripts/templates.lua | 137 ++++++++ src/core/include/airunit.h | 1 + src/core/include/commands.h | 28 +- src/core/include/groundunit.h | 5 +- src/core/include/scheduler.h | 6 +- src/core/include/unit.h | 20 +- src/core/include/unitsmanager.h | 7 +- src/core/src/airunit.cpp | 20 ++ src/core/src/core.cpp | 75 ++-- src/core/src/groundunit.cpp | 34 +- src/core/src/scheduler.cpp | 6 +- src/core/src/server.cpp | 54 +-- src/core/src/unit.cpp | 322 +++++++++--------- src/core/src/unitsmanager.cpp | 98 ++---- src/dcstools/include/dcstools.h | 2 +- src/dcstools/src/dcstools.cpp | 10 +- src/logger/include/interface.h | 2 +- src/logger/include/logger.h | 4 +- src/logger/src/interface.cpp | 4 +- src/logger/src/logger.cpp | 25 +- src/luatools/include/luatools.h | 2 +- src/luatools/src/luatools.cpp | 9 +- src/olympus/src/olympus.cpp | 24 +- src/shared/include/defines.h | 1 - 43 files changed, 1188 insertions(+), 580 deletions(-) create mode 100644 client/public/themes/olympus/images/units/groundunit-ewr.svg create mode 100644 client/public/themes/olympus/images/units/groundunit-sam-launcher.svg create mode 100644 client/public/themes/olympus/images/units/groundunit-sam-radar.svg diff --git a/client/demo.js b/client/demo.js index d7fcebf0..6c7c354b 100644 --- a/client/demo.js +++ b/client/demo.js @@ -15,7 +15,7 @@ const DEMO_UNIT_DATA = { radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], - contacts: [{ID: 2, detectionMethod: 1}], + contacts: [{ID: 2, detectionMethod: 1}, {ID: 3, detectionMethod: 4}], activePath: [{lat: 38, lng: -115, alt: 0}, {lat: 38, lng: -114, alt: 0}] }, ["2"]:{ category: "Aircraft", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "FA-18C_hornet", unitName: "Cool guy 1-2", groupName: "Cool group 2", state: 1, task: "Being cool", @@ -33,7 +33,7 @@ const DEMO_UNIT_DATA = { ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], contacts: [{ID: 1, detectionMethod: 16}], activePath: [ ] - }, ["3"]:{ category: "GroundUnit", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "M-60", unitName: "Cool guy 1-3", groupName: "Cool group 3", state: 1, task: "Being cool", + }, ["3"]:{ category: "Missile", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "", unitName: "Cool guy 1-3", groupName: "Cool group 3", state: 1, task: "Being cool", hasTask: false, position: { lat: 37.1, lng: -116, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, formationOffset: { x: 0, y: 0, z: 0 }, @@ -343,7 +343,7 @@ class DemoDataGenerator { }; mission(req, res){ - var ret = {mission: {theatre: "Syria"}}; + var ret = {mission: {theatre: "Nevada"}}; ret.time = Date.now(); var auth = req.get("Authorization"); if (auth) { diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css index 7ae3c7b5..63a525e6 100644 --- a/client/public/stylesheets/layout/layout.css +++ b/client/public/stylesheets/layout/layout.css @@ -38,7 +38,7 @@ font-size: 12px; position: absolute; right: 10px; - width: 180px; + width: 250px; z-index: 9999; } diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index f35985db..b6f2db41 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -20,6 +20,7 @@ html * { font-family: 'Open Sans', sans-serif !important; + user-select: none; } body { diff --git a/client/public/stylesheets/panels/connectionstatus.css b/client/public/stylesheets/panels/connectionstatus.css index 2b7dcec4..715850a9 100644 --- a/client/public/stylesheets/panels/connectionstatus.css +++ b/client/public/stylesheets/panels/connectionstatus.css @@ -11,7 +11,7 @@ } #connection-status-panel[data-is-connected] dt::before { - content: "Connected"; + content: "Connected FPS: " attr(data-framerate) " Load: " attr(data-load); } #connection-status-panel[data-is-connected] dd::after { diff --git a/client/public/themes/olympus/images/units/groundunit-ewr.svg b/client/public/themes/olympus/images/units/groundunit-ewr.svg new file mode 100644 index 00000000..2105f061 --- /dev/null +++ b/client/public/themes/olympus/images/units/groundunit-ewr.svg @@ -0,0 +1,60 @@ + + + + + + + + diff --git a/client/public/themes/olympus/images/units/groundunit-sam-launcher.svg b/client/public/themes/olympus/images/units/groundunit-sam-launcher.svg new file mode 100644 index 00000000..1732a5b0 --- /dev/null +++ b/client/public/themes/olympus/images/units/groundunit-sam-launcher.svg @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/client/public/themes/olympus/images/units/groundunit-sam-radar.svg b/client/public/themes/olympus/images/units/groundunit-sam-radar.svg new file mode 100644 index 00000000..0472f81e --- /dev/null +++ b/client/public/themes/olympus/images/units/groundunit-sam-radar.svg @@ -0,0 +1,50 @@ + + + + + + + + diff --git a/client/src/@types/dom.d.ts b/client/src/@types/dom.d.ts index 9d260bb8..22659e3b 100644 --- a/client/src/@types/dom.d.ts +++ b/client/src/@types/dom.d.ts @@ -19,6 +19,7 @@ interface CustomEventMap { "mapStateChanged": CustomEvent, "mapContextMenu": CustomEvent<>, "visibilityModeChanged": CustomEvent, + "contactsUpdated": CustomEvent, } declare global { diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 66053969..7e47dff6 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -142,8 +142,8 @@ export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area"; export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; -export const IADSTypes = ["AAA", "MANPADS", "SAM Sites", "Radar"]; -export const IADSDensities: {[key: string]: number}= {"AAA": 0.8, "MANPADS": 0.3, "SAM Sites": 0.1, "Radar": 0.05}; +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 enum DataIndexes { startOfData = 0, @@ -184,5 +184,6 @@ export enum DataIndexes { ammo, contacts, activePath, + isLeader, endOfData = 255 }; \ No newline at end of file diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 1d3053e5..88758a00 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -67,7 +67,7 @@ export class Map extends L.Map { constructor(ID: string) { /* Init the leaflet map */ //@ts-ignore Needed because the boxSelect option is non-standard - super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); + super(ID, { preferCanvas: true, doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); this.#ID = ID; @@ -295,6 +295,10 @@ export class Map extends L.Map { } } + getCenterUnit() { + return this.#centerUnit; + } + setTheatre(theatre: string) { var bounds = new L.LatLngBounds([-90, -180], [90, 180]); var miniMapZoom = 5; @@ -424,9 +428,6 @@ export class Map extends L.Map { #onDoubleClick(e: any) { this.deselectAllCoalitionAreas(); - - var db = groundUnitDatabase; - db.generateTestGrid(this.getMouseCoordinates()) } #onContextMenu(e: any) { diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index de64dab7..36ef8280 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -280,9 +280,17 @@ export function getMarkerCategoryByName(name: string) { else if (helicopterDatabase.getByName(name) != null) return "helicopter"; else if (groundUnitDatabase.getByName(name) != null){ - // TODO this is very messy var type = groundUnitDatabase.getByName(name)?.type; - return (type?.includes("SAM")) ? "groundunit-sam" : "groundunit-other"; + if (type === "SAM") + return "groundunit-sam"; + else if (type === "SAM Search radar" || type === "SAM Track radar" || type === "SAM Search/Track radar") + return "groundunit-sam-radar"; + else if (type === "SAM Launcher") + return "groundunit-sam-launcher"; + else if (type === "Radar") + return "groundunit-ewr"; + else + return "groundunit-other"; } else return "groundunit-other"; // TODO add other unit types diff --git a/client/src/panels/connectionstatuspanel.ts b/client/src/panels/connectionstatuspanel.ts index 54a3f7f6..686595c2 100644 --- a/client/src/panels/connectionstatuspanel.ts +++ b/client/src/panels/connectionstatuspanel.ts @@ -8,4 +8,12 @@ export class ConnectionStatusPanel extends Panel { update(connected: boolean) { this.getElement().toggleAttribute( "data-is-connected", connected ); } + + setMetrics(frameRate: number, load: number) { + const dt = this.getElement().querySelector("dt"); + if (dt) { + dt.dataset["framerate"] = String(frameRate); + dt.dataset["load"] = String(load); + } + } } \ No newline at end of file diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index d25d7404..1232ce45 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -141,7 +141,7 @@ export class UnitControlPanel extends Panel { element.toggleAttribute("data-show-advanced-settings-button", units.length == 1); /* Flight controls */ - var desiredAltitude: number | undefined = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getDesiredAltitude()}); + var desiredAltitude = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getDesiredAltitude()}); var desiredAltitudeType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getDesiredAltitudeType()}); var desiredSpeed = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getDesiredSpeed()}); var desiredSpeedType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getDesiredSpeedType()}); diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 26841dfb..afa39e74 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -18,7 +18,7 @@ var username = ""; var password = ""; var sessionHash: string | null = null; -var lastUpdateTime = 0; +var lastUpdateTimes: {[key: string]: number} = {} var demoEnabled = false; export function toggleDemoEnabled() { @@ -54,9 +54,14 @@ export function GET(callback: CallableFunction, uri: string, options?: { time?: /* Success */ setConnected(true); if (xmlHttp.responseType == 'arraybuffer') - callback(xmlHttp.response); - else - callback(JSON.parse(xmlHttp.responseText)); + lastUpdateTimes[uri] = callback(xmlHttp.response); + else { + const result = JSON.parse(xmlHttp.responseText); + lastUpdateTimes[uri] = callback(result); + + if ("frameRate" in result && "load" in result) + getConnectionStatusPanel().setMetrics(result.frameRate, result.load); + } } else if (xmlHttp.status == 401) { /* Bad credentials */ console.error("Incorrect username/password"); @@ -103,10 +108,6 @@ export function setAddress(address: string, port: number) { console.log(`Setting REST address to ${REST_ADDRESS}`) } -export function setLastUpdateTime(newLastUpdateTime: number) { - lastUpdateTime = newLastUpdateTime; -} - export function getAirbases(callback: CallableFunction) { GET(callback, AIRBASES_URI); } @@ -115,8 +116,8 @@ export function getBullseye(callback: CallableFunction) { GET(callback, BULLSEYE_URI); } -export function getLogs(callback: CallableFunction) { - GET(callback, LOGS_URI); +export function getLogs(callback: CallableFunction, refresh: boolean = false) { + GET(callback, LOGS_URI, { time: refresh ? 0 : lastUpdateTimes[LOGS_URI]}); } export function getMission(callback: CallableFunction) { @@ -124,7 +125,7 @@ export function getMission(callback: CallableFunction) { } export function getUnits(callback: CallableFunction, refresh: boolean = false) { - GET(callback, `${UNITS_URI}`, { time: refresh ? 0 : lastUpdateTime }, 'arraybuffer'); + GET(callback, UNITS_URI, { time: refresh ? 0 : lastUpdateTimes[UNITS_URI] }, 'arraybuffer'); } export function addDestination(ID: number, path: any) { @@ -327,7 +328,7 @@ export function startUpdate() { getMissionData()?.update(data); checkSessionHash(data.sessionHash); }); - getUnits((buffer: ArrayBuffer) => getUnitsManager()?.update(buffer), true /* Does a full refresh */); + getUnits((buffer: ArrayBuffer) => {return getUnitsManager()?.update(buffer), true /* Does a full refresh */}); requestUpdate(); requestRefresh(); @@ -336,7 +337,7 @@ export function startUpdate() { export function requestUpdate() { /* Main update rate = 250ms is minimum time, equal to server update time. */ if (!getPaused()) { - getUnits((buffer: ArrayBuffer) => { getUnitsManager()?.update(buffer); }, false); + getUnits((buffer: ArrayBuffer) => { return getUnitsManager()?.update(buffer); }, false); } window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); @@ -348,6 +349,13 @@ export function requestRefresh() { if (!getPaused()) { getAirbases((data: AirbasesData) => getMissionData()?.update(data)); getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getLogs((data: any) => { + for (let key in data.logs) { + if (key != "requestTime") + console.log(data.logs[key]); + } + return data.time; + }); getMission((data: any) => { checkSessionHash(data.sessionHash); getMissionData()?.update(data) diff --git a/client/src/units/groundunitdatabase.ts b/client/src/units/groundunitdatabase.ts index acc10dcc..1f635904 100644 --- a/client/src/units/groundunitdatabase.ts +++ b/client/src/units/groundunitdatabase.ts @@ -12,7 +12,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "SA-2 SAM Battery", "range": "Long", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" }, "SA-3 SAM Battery": { "name": "SA-3 SAM Battery", @@ -22,7 +22,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "SA-3 SAM Battery", "range": "Medium", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" }, "SA-6 SAM Battery": { "name": "SA-6 SAM Battery", @@ -32,7 +32,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "SA-6 SAM Battery", "range": "Medium", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" }, "SA-10 SAM Battery": { "name": "SA-10 SAM Battery", @@ -42,7 +42,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "SA-10 SAM Battery", "range": "Long", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" }, "SA-11 SAM Battery": { "name": "SA-11 SAM Battery", @@ -52,7 +52,17 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "SA-11 SAM Battery", "range": "Medium", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" + }, + "SA-5 SAM Battery": { + "name": "SA-5 SAM Battery", + "coalition": "Red", + "era": "Mid Cold War", + "label": "SA-5 SAM Battery", + "shortLabel": "SA-5 SAM Battery", + "range": "Long", + "filename": "", + "type": "SAM Site" }, "Patriot site": { "name": "Patriot site", @@ -62,7 +72,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "Patriot site", "range": "Long", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" }, "Hawk SAM Battery": { "name": "Hawk SAM Battery", @@ -72,7 +82,25 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "Hawk SAM Battery", "range": "Medium", "filename": "", - "type": "SAM Sites" + "type": "SAM Site" + }, + "SNR_75V": { + "name": "SNR_75V", + "coalition": "Red", + "era": "Early Cold War", + "label": "SA-2 Fan Song", + "shortLabel": "SNR 75V", + "filename": "", + "type": "SAM Track radar" + }, + "S_75M_Volhov": { + "name": "S_75M_Volhov", + "coalition": "Red", + "era": "Early Cold War", + "label": "SA-2 Launcher", + "shortLabel": "S75M Volhov", + "filename": "", + "type": "SAM Launcher" }, "2B11 mortar": { "name": "2B11 mortar", @@ -457,61 +485,61 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Kub 2P25 ln", "coalition": "Red", "era": "Late Cold War", - "label": "SA-6 Kub 2P25 ln", + "label": "SA-6 Launcher", "shortLabel": "Kub 2P25 ln", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "5p73 s-125 ln": { "name": "5p73 s-125 ln", "coalition": "Red", "era": "Early Cold War", - "label": "SA-3 5p73 s-125 ln", + "label": "SA-3 Launcher", "shortLabel": "5p73 s-125 ln", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "S-300PS 5P85C ln": { "name": "S-300PS 5P85C ln", "coalition": "Red", "era": "Late Cold War", - "label": "SA-10 S-300PS 5P85C ln", + "label": "SA-10 Launcher (5P85C)", "shortLabel": "S-300PS 5P85C ln", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "S-300PS 5P85D ln": { "name": "S-300PS 5P85D ln", "coalition": "Red", "era": "Late Cold War", - "label": "SA-10 S-300PS 5P85D ln", + "label": "SA-10 Launcher (5P85D)", "shortLabel": "S-300PS 5P85D ln", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "SA-11 Buk LN 9A310M1": { "name": "SA-11 Buk LN 9A310M1", "coalition": "Red", "era": "Late Cold War", - "label": "SA-11 Buk LN 9A310M1", + "label": "SA-11 Launcher", "shortLabel": "SA-11 Buk LN 9A310M1", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "Osa 9A33 ln": { "name": "Osa 9A33 ln", "coalition": "Red", "era": "Mid Cold War", - "label": "SA-8 Osa 9A33 ln", + "label": "SA-8 Launcher", "shortLabel": "Osa 9A33 ln", "range": "Short", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "Tor 9A331": { "name": "Tor 9A331", @@ -547,11 +575,11 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "SA-11 Buk CC 9S470M1", "coalition": "Red", "era": "Late Cold War", - "label": "SA-11 Buk CC 9S470M1", + "label": "SA-11 Command Post", "shortLabel": "SA-11 Buk CC 9S470M1", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "SA-8 Osa LD 9T217": { "name": "SA-8 Osa LD 9T217", @@ -567,21 +595,21 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Patriot AMG", "coalition": "Blue", "era": "Modern", - "label": "Patriot AMG", + "label": "Patriot Antenna Mast Group", "shortLabel": "Patriot AMG", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "Patriot ECS": { "name": "Patriot ECS", "coalition": "Blue", "era": "Modern", - "label": "Patriot ECS", + "label": "Patriot Engagement Control Station", "shortLabel": "Patriot ECS", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "Gepard": { "name": "Gepard", @@ -596,11 +624,11 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Hawk pcp", "coalition": "Blue", "era": "Late Cold War", - "label": "Hawk pcp", + "label": "Hawk Platoon Command Post", "shortLabel": "Hawk pcp", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "SA-18 Igla manpad": { "name": "SA-18 Igla manpad", @@ -632,6 +660,36 @@ export class GroundUnitDatabase extends UnitDatabase { "filename": "", "type": "MANPADS" }, + "RPC_5N62V": { + "name": "RPC_5N62V", + "coalition": "Red", + "era": "Mid Cold War", + "label": "SA-5 Square Pair", + "shortLabel": "RPC 5N62V", + "range": "Long", + "filename": "", + "type": "SAM Track radar" + }, + "RLS_19J6": { + "name": "RLS_19J6", + "coalition": "Red", + "era": "Mid Cold War", + "label": "SA-5 Thin Shield", + "shortLabel": "RLS 19J6", + "range": "Long", + "filename": "", + "type": "SAM Search radar" + }, + "S-200_Launcher": { + "name": "S-200_Launcher", + "coalition": "Red", + "era": "Mid Cold War", + "label": "SA-5 Launcher", + "shortLabel": "S-200 Launcher", + "range": "Long", + "filename": "", + "type": "SAM Launcher" + }, "Vulcan": { "name": "Vulcan", "coalition": "Blue", @@ -645,10 +703,10 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Hawk ln", "coalition": "Blue", "era": "Late Cold War", - "label": "Hawk ln", + "label": "Hawk Launcher", "shortLabel": "Hawk ln", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "M48 Chaparral": { "name": "M48 Chaparral", @@ -672,11 +730,11 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Patriot ln", "coalition": "Blue", "era": "Late Cold War", - "label": "Patriot ln", + "label": "Patriot Launcher", "shortLabel": "Patriot ln", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Launcher" }, "M1097 Avenger": { "name": "M1097 Avenger", @@ -691,21 +749,21 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Patriot EPP", "coalition": "Blue", "era": "Late Cold War", - "label": "Patriot EPP", + "label": "Patriot Electric Power Plant", "shortLabel": "Patriot EPP", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "Patriot cp": { "name": "Patriot cp", "coalition": "Blue", "era": "Late Cold War", - "label": "Patriot cp", + "label": "Patriot Command Post", "shortLabel": "Patriot cp", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "Roland ADS": { "name": "Roland ADS", @@ -720,11 +778,11 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "S-300PS 54K6 cp", "coalition": "Red", "era": "Late Cold War", - "label": "SA-10 S-300PS 54K6 cp", + "label": "SA-10 Command Post", "shortLabel": "S-300PS 54K6 cp", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Support vehicle" }, "Stinger manpad GRG": { "name": "Stinger manpad GRG", @@ -734,7 +792,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "Stinger manpad GRG", "range": "Short", "filename": "", - "type": "SAM" + "type": "MANPADS" }, "Stinger manpad dsr": { "name": "Stinger manpad dsr", @@ -754,7 +812,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "Stinger comm dsr", "range": "Short", "filename": "", - "type": "SAM" + "type": "MANPADS" }, "Stinger manpad": { "name": "Stinger manpad", @@ -774,7 +832,7 @@ export class GroundUnitDatabase extends UnitDatabase { "shortLabel": "Stinger comm", "range": "Short", "filename": "", - "type": "SAM" + "type": "MANPADS" }, "ZSU-23-4 Shilka": { "name": "ZSU-23-4 Shilka", @@ -843,7 +901,7 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "1L13 EWR", "coalition": "Red", "era": "Late Cold War", - "label": "1L13 EWR", + "label": "Box Spring", "shortLabel": "1L13 EWR", "filename": "", "type": "Radar" @@ -852,37 +910,37 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "Kub 1S91 str", "coalition": "Red", "era": "Mid Cold War", - "label": "SA-6 Kub 1S91 str", + "label": "SA-6 Straight flush", "shortLabel": "Kub 1S91 str", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Search/Track radar" }, "S-300PS 40B6M tr": { "name": "S-300PS 40B6M tr", "coalition": "Red", "era": "Late Cold War", - "label": "SA-10 S-300PS 40B6M tr", + "label": "SA-10 Tin Shield", "shortLabel": "S-300PS 40B6M tr", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Track radar" }, "S-300PS 40B6MD sr": { "name": "S-300PS 40B6MD sr", "coalition": "Red", "era": "Late Cold War", - "label": "SA-10 S-300PS 40B6MD sr", + "label": "SA-10 Clam Shell", "shortLabel": "S-300PS 40B6MD sr", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "55G6 EWR": { "name": "55G6 EWR", "coalition": "Red", "era": "Early Cold War", - "label": "55G6 EWR", + "label": "Tall Rack", "shortLabel": "55G6 EWR", "filename": "", "type": "Radar" @@ -891,98 +949,98 @@ export class GroundUnitDatabase extends UnitDatabase { "name": "S-300PS 64H6E sr", "coalition": "Red", "era": "Late Cold War", - "label": "SA-10 S-300PS 64H6E sr", + "label": "SA-10 Big Bird", "shortLabel": "S-300PS 64H6E sr", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "SA-11 Buk SR 9S18M1": { "name": "SA-11 Buk SR 9S18M1", "coalition": "Red", "era": "Mid Cold War", - "label": "SA-11 Buk SR 9S18M1", + "label": "SA-11 Snown Drift", "shortLabel": "SA-11 Buk SR 9S18M1", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "Dog Ear radar": { "name": "Dog Ear radar", "coalition": "Red", "era": "Mid Cold War", - "label": "Dog Ear radar", + "label": "Dog Ear", "shortLabel": "Dog Ear radar", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "Hawk tr": { "name": "Hawk tr", "coalition": "Blue", "era": "Early Cold War", - "label": "Hawk tr", + "label": "Hawk Track radar", "shortLabel": "Hawk tr", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Track radar" }, "Hawk sr": { "name": "Hawk sr", "coalition": "Blue", "era": "Early Cold War", - "label": "Hawk sr", + "label": "Hawk Search radar", "shortLabel": "Hawk sr", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "Patriot str": { "name": "Patriot str", "coalition": "Blue", "era": "Late Cold War", - "label": "Patriot str", + "label": "Patriot Search/Track radar", "shortLabel": "Patriot str", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Search/Track radar" }, "Hawk cwar": { "name": "Hawk cwar", "coalition": "Blue", "era": "Early Cold War", - "label": "Hawk cwar", + "label": "Hawk Continous Wave Acquisition Radar", "shortLabel": "Hawk cwar", "range": "Long", "filename": "", - "type": "SAM" + "type": "SAM Track radar" }, "p-19 s-125 sr": { "name": "p-19 s-125 sr", "coalition": "Red", "era": "Mid Cold War", - "label": "SA-3 p-19 s-125 sr", - "shortLabel": "p-19 s-125 sr", + "label": "SA-3 Flat Face B", + "shortLabel": "Flat Face B", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "Roland Radar": { "name": "Roland Radar", "coalition": "Blue", "era": "Mid Cold War", - "label": "Roland Radar", + "label": "Roland Search radar", "shortLabel": "Roland Radar", "filename": "", - "type": "SAM" + "type": "SAM Search radar" }, "snr s-125 tr": { "name": "snr s-125 tr", "coalition": "Red", "era": "Early Cold War", - "label": "SA-3 snr s-125 tr", + "label": "SA-3 Low Blow", "shortLabel": "snr s-125 tr", "range": "Medium", "filename": "", - "type": "SAM" + "type": "SAM Track radar" }, "house1arm": { "name": "house1arm", diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index fc5f13a8..efaa85ab 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -9,6 +9,8 @@ import { TargetMarker } from '../map/targetmarker'; import { BLUE_COMMANDER, BOMBING, CARPET_BOMBING, DLINK, DataIndexes, FIRE_AT_AREA, GAME_MASTER, HIDE_ALL, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, RED_COMMANDER, ROEs, RWR, VISUAL, emissionsCountermeasures, reactionsToThreat, states } from '../constants/constants'; import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN, UnitIconOptions } from '../@types/unit'; import { DataExtractor } from './dataextractor'; +import { groundUnitDatabase } from './groundunitdatabase'; +import { navyUnitDatabase } from './navyunitdatabase'; var pathIcon = new Icon({ iconUrl: '/resources/theme/images/markers/marker-icon.png', @@ -74,6 +76,7 @@ export class Unit extends CustomMarker { #ammo: Ammo[] = []; #contacts: Contact[] = []; #activePath: LatLng[] = []; + #isLeader: boolean = false; #selectable: boolean; #selected: boolean = false; @@ -126,6 +129,7 @@ export class Unit extends CustomMarker { getAmmo() {return this.#ammo}; getContacts() {return this.#contacts}; getActivePath() {return this.#activePath}; + getIsLeader() {return this.#isLeader}; static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -162,6 +166,8 @@ export class Unit extends CustomMarker { document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); }); + + getMap().on("zoomend", () => {this.#onZoom();}) } getCategory() { @@ -212,8 +218,9 @@ export class Unit extends CustomMarker { case DataIndexes.radio: this.#radio = dataExtractor.extractRadio(); break; case DataIndexes.generalSettings: this.#generalSettings = dataExtractor.extractGeneralSettings(); break; case DataIndexes.ammo: this.#ammo = dataExtractor.extractAmmo(); break; - case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); break; + case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); document.dispatchEvent(new CustomEvent("contactsUpdated", {detail: this})); break; case DataIndexes.activePath: this.#activePath = dataExtractor.extractActivePath(); break; + case DataIndexes.isLeader: this.#isLeader = dataExtractor.extractBool(); break; } } @@ -223,25 +230,20 @@ export class Unit extends CustomMarker { if (updateMarker) this.#updateMarker(); - document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); + if (this.getSelected() || getMap().getCenterUnit() === this) + document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); } drawLines() { - // TODO dont delete the polylines of the detected units - this.#clearContacts(); - if (this.getSelected()) { - this.#drawPath(); - this.#drawContacts(); - this.#drawTarget(); - } - else { - this.#clearPath(); - this.#clearTarget(); - } + this.#drawPath(); + this.#drawContacts(); + this.#drawTarget(); } getData() { return { + category: this.getCategory(), + ID: this.ID, alive: this.#alive, human: this.#human, controlled: this.#controlled, @@ -277,7 +279,8 @@ export class Unit extends CustomMarker { generalSettings: this.#generalSettings, ammo: this.#ammo, contacts: this.#contacts, - activePath: this.#activePath + activePath: this.#activePath, + isLeader: this.#isLeader } } @@ -315,7 +318,7 @@ export class Unit extends CustomMarker { /* Only alive units can be selected. Some units are not selectable (weapons) */ if ((this.#alive || !selected) && this.getSelectable() && this.getSelected() != selected && this.belongsToCommandedCoalition()) { this.#selected = selected; - this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected", selected); + if (selected) { document.dispatchEvent(new CustomEvent("unitSelection", { detail: this })); this.#updateMarker(); @@ -326,6 +329,15 @@ export class Unit extends CustomMarker { this.#clearPath(); this.#clearTarget(); } + + this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", selected); + if (getMap().getZoom() < 13) { + if (this.#isLeader) + this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected)); + else + this.#updateMarker(); + } + } } @@ -376,6 +388,10 @@ export class Unit extends CustomMarker { return true; } + getType() { + return ""; + } + /********************** Icon *************************/ createIcon(): void { /* Set the icon */ @@ -478,21 +494,16 @@ export class Unit extends CustomMarker { /********************** Visibility *************************/ updateVisibility() { - var hidden = false; const hiddenUnits = getUnitsManager().getHiddenTypes(); - if (this.#human && hiddenUnits.includes("human")) - hidden = true; - if (this.#controlled == false && hiddenUnits.includes("dcs")) - hidden = true; - if (hiddenUnits.includes(this.getMarkerCategory())) - hidden = true; - if (hiddenUnits.includes(this.#coalition)) - hidden = true; - if (getUnitsManager().getCommandMode() === HIDE_ALL) - hidden = true; - if (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0) { - hidden = true; - } + var hidden = ((this.#human && hiddenUnits.includes("human")) || + (this.#controlled == false && hiddenUnits.includes("dcs")) || + (hiddenUnits.includes(this.getMarkerCategory())) || + (hiddenUnits.includes(this.#coalition)) || + (getUnitsManager().getCommandMode() === HIDE_ALL) || + (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0) || + (!this.#isLeader && this.getCategory() == "GroundUnit" && getMap().getZoom() < 13)) && + !(this.getSelected()); + this.setHidden(hidden || !this.#alive); } @@ -718,7 +729,7 @@ export class Unit extends CustomMarker { } else if ((selectedUnits.length > 0 && (selectedUnits.includes(this))) || selectedUnits.length == 0) { if (this.getCategory() == "Aircraft") { - options["refuel"] = { text: "Air to air refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB." }; // TODO Add some way of knowing which aircraft can AAR + options["refuel"] = { text: "Air to air refuel", tooltip: "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB." }; // TODO Add some way of knowing which aircraft can AAR } } @@ -730,10 +741,13 @@ export class Unit extends CustomMarker { } if ((selectedUnits.length === 0 && this.getCategory() == "GroundUnit") || selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) { - if (selectedUnits.concat([this]).every((unit: Unit) => { return unit.canFulfillRole(["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"]) })) + if (selectedUnits.concat([this]).every((unit: Unit) => { return ["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"].includes(this.getType()) })) options["fire-at-area"] = { text: "Fire at area", tooltip: "Fire at a large area" }; } + if (selectedUnitTypes.length === 1 && ["NavyUnit", "GroundUnit"].includes(selectedUnitTypes[0]) && getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getCoalition()}) !== undefined) + options["group"] = { text: "Create group", tooltip: "Create a group from the selected units." }; + if (Object.keys(options).length > 0) { getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); getMap().getUnitContextMenu().setOptions(options, (option: string) => { @@ -750,6 +764,8 @@ export class Unit extends CustomMarker { getUnitsManager().selectedUnitsAttackUnit(this.ID); else if (action === "refuel") getUnitsManager().selectedUnitsRefuel(); + else if (action === "group") + getUnitsManager().selectedUnitsCreateGroup(); else if (action === "follow") this.#showFollowOptions(e); else if (action === "bomb") @@ -758,6 +774,7 @@ export class Unit extends CustomMarker { getMap().setState(CARPET_BOMBING); else if (action === "fire-at-area") getMap().setState(FIRE_AT_AREA); + } #showFollowOptions(e: any) { @@ -949,10 +966,11 @@ export class Unit extends CustomMarker { } #drawContacts() { + this.#clearContacts(); for (let index in this.#contacts) { var contactData = this.#contacts[index]; var contact = getUnitsManager().getUnitByID(contactData.ID) - if (contact != null) { + if (contact != null && contact.getAlive()) { var startLatLng = new LatLng(this.#position.lat, this.#position.lng); var endLatLng: LatLng; if (contactData.detectionMethod === RWR) { @@ -1017,6 +1035,10 @@ export class Unit extends CustomMarker { if (getMap().hasLayer(this.#targetPositionPolyline)) this.#targetPositionPolyline.removeFrom(getMap()); } + + #onZoom() { + this.#updateMarker(); + } } export class AirUnit extends Unit { @@ -1079,6 +1101,11 @@ export class GroundUnit extends Unit { getCategory() { return "GroundUnit"; } + + getType() { + var blueprint = groundUnitDatabase.getByName(this.getName()); + return blueprint?.type? blueprint.type: ""; + } } export class NavyUnit extends Unit { @@ -1108,6 +1135,11 @@ export class NavyUnit extends Unit { getCategory() { return "NavyUnit"; } + + getType() { + var blueprint = navyUnitDatabase.getByName(this.getName()); + return blueprint?.type? blueprint.type: ""; + } } export class Weapon extends Unit { @@ -1115,21 +1147,6 @@ export class Weapon extends Unit { super(ID); this.setSelectable(false); } - - getIconOptions() { - return { - showState: false, - showVvi: false, - showHotgroup: false, - showUnitIcon: true, - showShortLabel: false, - showFuel: false, - showAmmo: false, - showSummary: false, - showCallsign: false, - rotateToHeading: true - }; - } } export class Missile extends Weapon { @@ -1142,7 +1159,25 @@ export class Missile extends Weapon { } getMarkerCategory() { - return "missile"; + if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)) + return "missile"; + else + return "aircraft"; + } + + getIconOptions() { + return { + showState: false, + showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showHotgroup: false, + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showCallsign: false, + rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC) + }; } } @@ -1156,6 +1191,24 @@ export class Bomb extends Weapon { } getMarkerCategory() { - return "bomb"; + if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)) + return "bomb"; + else + return "aircraft"; + } + + getIconOptions() { + return { + showState: false, + showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showHotgroup: false, + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showCallsign: false, + rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC) + }; } } diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 0041aa0d..6288918e 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -1,7 +1,7 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getHotgroupPanel, getInfoPopup, getMap } from ".."; import { Unit } from "./unit"; -import { cloneUnit, setLastUpdateTime, spawnAircrafts, spawnGroundUnits } from "../server/server"; +import { cloneUnit, deleteUnit, spawnAircrafts, spawnGroundUnits } from "../server/server"; import { bearingAndDistanceToLatLng, deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils"; import { CoalitionArea } from "../map/coalitionarea"; import { groundUnitDatabase } from "./groundunitdatabase"; @@ -12,11 +12,12 @@ import { citiesDatabase } from "./citiesdatabase"; export class UnitsManager { #units: { [ID: number]: Unit }; - #copiedUnits: Unit[]; + #copiedUnits: any[]; #selectionEventDisabled: boolean = false; #pasteDisabled: boolean = false; #hiddenTypes: string[] = []; #commandMode: string = HIDE_ALL; + #requestDetectionUpdate: boolean = false; constructor() { this.#units = {}; @@ -31,6 +32,7 @@ export class UnitsManager { document.addEventListener('keyup', (event) => this.#onKeyUp(event)); document.addEventListener('exportToFile', () => this.exportToFile()); document.addEventListener('importFromFile', () => this.importFromFile()); + document.addEventListener('contactsUpdated', (e: CustomEvent) => {this.#requestDetectionUpdate = true}); } getSelectableAircraft() { @@ -91,17 +93,21 @@ export class UnitsManager { this.#units[ID]?.setData(dataExtractor); } - for (let ID in this.#units) { - var unit = this.#units[ID]; - if (!unit.belongsToCommandedCoalition()) - unit.setDetectionMethods(this.getUnitDetectedMethods(unit)); + if (this.#requestDetectionUpdate) { + for (let ID in this.#units) { + var unit = this.#units[ID]; + if (!unit.belongsToCommandedCoalition()) + unit.setDetectionMethods(this.getUnitDetectedMethods(unit)); + } + this.#requestDetectionUpdate = false; } - setLastUpdateTime(updateTime); - for (let ID in this.#units) { - this.#units[ID].drawLines(); + if (this.#units[ID].getSelected()) + this.#units[ID].drawLines(); }; + + return updateTime; } setHiddenType(key: string, value: boolean) { @@ -188,36 +194,59 @@ export class UnitsManager { } getSelectedUnitsTypes() { - if (this.getSelectedUnits().length == 0) + const selectedUnits = this.getSelectedUnits(); + if (selectedUnits.length == 0) return []; - return this.getSelectedUnits().map((unit: Unit) => { - return unit.constructor.name + return selectedUnits.map((unit: Unit) => { + return unit.getCategory(); })?.filter((value: any, index: any, array: string[]) => { return array.indexOf(value) === index; }); }; + /* Gets the value of a variable from the selected units. If all the units have the same value, returns the value, else returns undefined */ getSelectedUnitsVariable(variableGetter: CallableFunction) { - if (this.getSelectedUnits().length == 0) + const selectedUnits = this.getSelectedUnits(); + if (selectedUnits.length == 0) return undefined; - return this.getSelectedUnits().map((unit: Unit) => { + return selectedUnits.map((unit: Unit) => { return variableGetter(unit); })?.reduce((a: any, b: any) => { - return a == b ? a : undefined + return a === b ? a : undefined }); }; - getSelectedUnitsCoalition() { - if (this.getSelectedUnits().length == 0) + const selectedUnits = this.getSelectedUnits(); + if (selectedUnits.length == 0) return undefined; - return this.getSelectedUnits().map((unit: Unit) => { + return selectedUnits.map((unit: Unit) => { return unit.getCoalition() })?.reduce((a: any, b: any) => { return a == b ? a : undefined }); }; + getByType(type: string) { + Object.values(this.getUnits()).filter((unit: Unit) => { + return unit.getType() === type; + }) + } + + getUnitDetectedMethods(unit: Unit) { + var detectionMethods: number[] = []; + for (let idx in this.#units) { + if (this.#units[idx].getAlive() && this.#units[idx].getIsLeader() && this.#units[idx].getCoalition() !== "neutral" && this.#units[idx].getCoalition() != unit.getCoalition()) + { + this.#units[idx].getContacts().forEach((contact: Contact) => { + if (contact.ID == unit.ID && !detectionMethods.includes(contact.detectionMethod)) + detectionMethods.push(contact.detectionMethod); + }); + } + } + return detectionMethods; + } + /*********************** Actions on selected units ************************/ selectedUnitsAddDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number) { var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); @@ -227,7 +256,7 @@ export class UnitsManager { if (mantainRelativePosition) unitDestinations = this.selectedUnitsComputeGroupDestination(latlng, rotation); else - selectedUnits.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng }); + selectedUnits.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng; }); for (let idx in selectedUnits) { const unit = selectedUnits[idx]; @@ -515,36 +544,66 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `unit bombing point`); } - getUnitDetectedMethods(unit: Unit) { - var detectionMethods: number[] = []; - for (let idx in this.#units) { - if (this.#units[idx].getCoalition() !== "neutral" && this.#units[idx].getCoalition() != unit.getCoalition()) - { - this.#units[idx].getContacts().forEach((contact: Contact) => { - if (this.#units[idx].getAlive() && contact.ID == unit.ID && !detectionMethods.includes(contact.detectionMethod)) - detectionMethods.push(contact.detectionMethod); - }); - } + // TODO add undo group + selectedUnitsCreateGroup() { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: false }); + var units = []; + var coalition = "neutral"; + for (let idx in selectedUnits) { + var unit = selectedUnits[idx]; + coalition = unit.getCoalition(); + deleteUnit(unit.ID, false, true); + units.push({unitType: unit.getName(), location: unit.getPosition()}); } - return detectionMethods; + const category = this.getSelectedUnitsTypes()[0]; + this.spawnUnit(category, units, coalition, true); } /***********************************************/ copyUnits() { - this.#copiedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ - this.#showActionMessage(this.#copiedUnits, `copied`); + this.#copiedUnits = JSON.parse(JSON.stringify(this.getSelectedUnits().map((unit: Unit) => {return unit.getData()}))); /* Can be applied to humans too */ + getInfoPopup().setText(`${this.#copiedUnits.length} units copied`); } pasteUnits() { if (!this.#pasteDisabled) { + /* Compute the position of the center of the copied units */ + var nUnits = this.#copiedUnits.length; + var avgLat = 0; + var avgLng = 0; for (let idx in this.#copiedUnits) { var unit = this.#copiedUnits[idx]; - //getMap().addTemporaryMarker(getMap().getMouseCoordinates()); - cloneUnit(unit.ID, getMap().getMouseCoordinates()); - this.#showActionMessage(this.#copiedUnits, `pasted`); + avgLat += unit.position.lat / nUnits; + avgLng += unit.position.lng / nUnits; } - this.#pasteDisabled = true; - window.setTimeout(() => this.#pasteDisabled = false, 250); + + /* Organize the copied units in groups */ + var groups: {[key: string]: any} = {}; + this.#copiedUnits.forEach((unit: any) => { + if (!(unit.groupName in groups)) + groups[unit.groupName] = []; + groups[unit.groupName].push(unit); + }); + + for (let groupName in groups) { + /* Paste the units as groups. Only for ground and navy units because of loadouts, TODO: find a better solution so it works for them too*/ + if (!["Aircraft", "Helicopter"].includes(groups[groupName][0].category)) { + var units = groups[groupName].map((unit: any) => { + var position = new LatLng(getMap().getMouseCoordinates().lat + unit.position.lat - avgLat, getMap().getMouseCoordinates().lng + unit.position.lng - avgLng); + getMap().addTemporaryMarker(position, unit.name, unit.coalition); + return {unitType: unit.name, location: position}; + }); + this.spawnUnit(groups[groupName][0].category, units, groups[groupName][0].coalition, true); + } + else { + groups[groupName].forEach((unit: any) => { + var position = new LatLng(getMap().getMouseCoordinates().lat + unit.position.lat - avgLat, getMap().getMouseCoordinates().lng + unit.position.lng - avgLng); + getMap().addTemporaryMarker(position, unit.name, unit.coalition); + cloneUnit(unit.ID, position); + }); + } + } + getInfoPopup().setText(`${this.#copiedUnits.length - 1} units pasted`); } } @@ -581,7 +640,6 @@ export class UnitsManager { var unit = this.#units[ID]; if (!["Aircraft", "Helicopter"].includes(unit.getCategory())) { var data: any = unit.getData(); - data.category = unit.getCategory(); if (unit.getGroupName() in unitsToExport) unitsToExport[unit.getGroupName()].push(data); else @@ -609,7 +667,7 @@ export class UnitsManager { var groups = JSON.parse(contents); for (let groupName in groups) { if (groupName !== "" && groups[groupName].length > 0 && groups[groupName].every((unit: any) => {return unit.category == "GroundUnit";})) { - var units = groups[groupName].map((unit: any) => {return {unitType: unit.name, location: unit.position}}); + var units = groups[groupName].filter((unit: any) => {return unit.alive}).map((unit: any) => {return {unitType: unit.name, location: unit.position}}); spawnGroundUnits(units, groups[groupName][0].coalition, true); } } @@ -629,8 +687,11 @@ export class UnitsManager { /***********************************************/ #onKeyUp(event: KeyboardEvent) { - if (!keyEventWasInInput(event) && event.key === "Delete" ) { - this.selectedUnitsDelete(); + if (!keyEventWasInInput(event)) { + if (event.key === "Delete") + this.selectedUnitsDelete(); + else if (event.key === "a" && event.ctrlKey) + Object.values(this.getUnits()).filter((unit: Unit) => {return !unit.getHidden()}).forEach((unit: Unit) => unit.setSelected(true)); } } @@ -661,7 +722,7 @@ export class UnitsManager { document.dispatchEvent(new CustomEvent("unitsDeselection", { detail: this.getSelectedUnits() })); } - #showActionMessage(units: Unit[], message: string) { + #showActionMessage(units: any[], message: string) { if (units.length == 1) getInfoPopup().setText(`${units[0].getUnitName()} ${message}`); else if (units.length > 1) diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs index 9bb8e91b..873216b2 100644 --- a/client/views/other/contextmenus.ejs +++ b/client/views/other/contextmenus.ejs @@ -15,10 +15,10 @@
- +