diff --git a/README.md b/README.md index 83953e29..eafa8299 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Important note: DCS Olympus is in alpha state. No official release has been produced yet. The first public version is planned for Q4 2023. +# Important note: DCS Olympus is in alpha state. No official release has been produced yet. The first public version is planned for mid december 2023. # DCS Olympus *A real-time web interface to spawn and control units in DCS World* @@ -6,32 +6,19 @@ ![alt text](https://github.com/Pax1601/DCSOlympus/blob/main/client/sample.png?raw=true) ### What is this? -DCS Olympus is a mod for DCS World. It allows users to spawn, control, task, group, and remove units from a DCS World server using a real-time map interface, similarly to Real Time Strategy games. The user interface also provides useful informations units, like loadouts, fuel, tasking, and so on. In the future, more features for DCS World GCI and JTAC will be available. +DCS: Olympus is a free and open-source mod for DCS that enables dynamic real-time control through a map interface. The user is able to spawn units/groups, deploy a variety of effects such as smoke, flares, or explosions, and waypoints/tasks can be given to AI units in real-time in a way similar to a classic RTS game. -### Features and how to use it -- Spawn air and ground units, with preset loadouts - - Double click on the map to spawn a blue and red units, both in the air and in the ground, with preset loadouts for air-to-air or air-to-ground tasks; -- Control units - - Select one ore more units to move them around. Hold down ctrl and click to create a route for the unit to follow; -- Attack other units - - After selecting one ore more units, double click on another unit and select "Attack" to attack it, depending on the available weapons. +Additionally Olympus is able to run several effects and unit behaviours beyond the core DCS offerings. This includes such things as napalm and white phosphosous explosions, or setting up AA units to fire at players and miss, and more. + +It even includes Red and Blue modes which limit your view and powers to just seeing what your coalition sees, with a spawning budget you could play against your friends even with no-one in the game piloting, or have a Red commander working against a squadron of blue pilots, and/or a blue commander working with them. + +Even better it requires no client mods be installed if used on a server + +The full feature list is simply too long to enumerate in a short summary but needless to say Olympus offers up a lot of unique gameplay that has previously not existed, and enhances many other elements of DCS in exciting ways ### Installing DCS Olympus A prebuilt installer will soon be released and available here -### Building DCS Olympus -DCS Olympus is comprised of two modules: - -A "core" c++ .dll module, which is run by DCS and exposes all the necessary data, and provides endpoints for commands from a REST server. A Visual Studio 2017/2019/2022 solution is provided, and requires no additional configuration. The core dll solution has two dependencies, both can be installed using vcpkg (https://vcpkg.io/en/getting-started.html): -- cpprestsdk: `vcpkg install cpprestsdk:x64-windows` -- geographiclib: `vcpkg install geographiclib:x64-windows` - - -A "client" node.js typescript web app, which can be hosted on the server using express.js. A Visual Studio Code configuration is provided for debugging. The client requires node.js to be installed for building (https://nodejs.org/en/). After installing node.js, move in the client folder and run the following commands: -- `npm install` -- `npm -g install` - - After installing all the necessary dependencies you can start a development server executing the *client/debug.bat* batch file, and visiting http:\\localhost:3000 with any modern browser (tested with updated Chrome, Firefox and Edge). However, it is highly suggested to simply run the `Launch Chrome against localhost` debug configuration in Visual Studio Code. diff --git a/client/demo.js b/client/demo.js index e27687aa..2a8ba5c7 100644 --- a/client/demo.js +++ b/client/demo.js @@ -55,7 +55,7 @@ class DemoDataGenerator { // UNCOMMENT TO TEST ALL UNITS **************** - + /* var databases = Object.assign({}, aircraftDatabase, helicopterDatabase, groundUnitDatabase, navyUnitDatabase); var t = Object.keys(databases).length; var l = Math.floor(Math.sqrt(t)); diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index a35d3624..98936a8b 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -45,6 +45,7 @@ justify-content: space-between; row-gap: 5px; width: 100%; + padding: 5px; } .contextmenu-advanced-options-toggle, @@ -61,7 +62,13 @@ .contextmenu-advanced-options-toggle:after, .contextmenu-metadata-toggle:after { content: url(/resources/theme/images/icons/chevron-down.svg); - margin: auto; + margin-left: auto; + margin-top: auto; +} + +.contextmenu-advanced-options-toggle.is-open:after, +.contextmenu-metadata-toggle.is-open:after { + transform: rotate(180deg); } .contextmenu-advanced-options-toggle div:first-child, @@ -580,6 +587,7 @@ #iads-menu { row-gap: 10px; + padding: 10px; } #coalition-area-contextmenu>div:nth-child(2) { @@ -596,6 +604,7 @@ flex-direction: column; justify-content: space-between; row-gap: 5px; + padding: 20px; } .create-iads-button { diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index 3c011a57..8a77990d 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -199,6 +199,10 @@ form { right: 10px; } +.ol-select.is-open:not(.ol-select-image)>.ol-select-value:after { + transform: rotate(180deg); +} + .ol-select>.ol-select-options { border-radius: var(--border-radius-md); max-height: 0; diff --git a/client/sample.png b/client/sample.png index 5e941cd6..4534d728 100644 Binary files a/client/sample.png and b/client/sample.png differ diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 07fba370..d3961b1f 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -220,8 +220,8 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{ "tooltip": "Toggle airbase' visibility" }]; -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 IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"]; +export const IADSDensities: { [key: string]: number } = { "AAA": 0.8, "SAM Site": 0.1, "Radar (EWR)": 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)"; diff --git a/client/src/controls/unitspawnmenu.ts b/client/src/controls/unitspawnmenu.ts index b0929848..87f7cd29 100644 --- a/client/src/controls/unitspawnmenu.ts +++ b/client/src/controls/unitspawnmenu.ts @@ -12,8 +12,14 @@ import { groundUnitDatabase } from "../unit/databases/groundunitdatabase"; import { navyUnitDatabase } from "../unit/databases/navyunitdatabase"; import { UnitBlueprint, UnitSpawnOptions, UnitSpawnTable } from "../interfaces"; -export class UnitSpawnMenu { +/** This is the common code for all the unit spawn menus. It is shown both when right clicking on the map and when spawning from airbase. + * + */ + +export abstract class UnitSpawnMenu { protected showRangeCircles: boolean = false; + protected unitTypeFilter = (unit:any) => { return true; }; + /* Default options */ protected spawnOptions: UnitSpawnOptions = { roleType: "", name: "", @@ -31,8 +37,9 @@ export class UnitSpawnMenu { #unitDatabase: UnitDatabase; #countryCodes: any; #orderByRole: boolean; - protected unitTypeFilter = (unit:any) => { return true; }; - + #showLoadout: boolean = true; + #showAltitudeSlider: boolean = true; + /* Controls */ #unitRoleTypeDropdown: Dropdown; #unitLabelDropdown: Dropdown; @@ -44,11 +51,18 @@ export class UnitSpawnMenu { /* HTML Elements */ #deployUnitButtonEl: HTMLButtonElement; + #unitCountDivider: HTMLDivElement; #unitLoadoutPreviewEl: HTMLDivElement; #unitImageEl: HTMLImageElement; #unitLoadoutListEl: HTMLDivElement; #descriptionDiv: HTMLDivElement; #abilitiesDiv: HTMLDivElement; + #advancedOptionsDiv: HTMLDivElement; + #unitInfoDiv: HTMLDivElement; + #advancedOptionsToggle: HTMLDivElement; + #advancedOptionsText: HTMLDivElement; + #unitInfoToggle: HTMLDivElement; + #unitInfoText: HTMLDivElement; /* Range circle previews */ #engagementCircle: Circle; @@ -60,20 +74,20 @@ export class UnitSpawnMenu { this.#orderByRole = orderByRole; /* Create the dropdowns and the altitude slider */ - this.#unitRoleTypeDropdown = new Dropdown(null, (roleType: string) => this.#setUnitRoleType(roleType), undefined, "Unit type"); - this.#unitLabelDropdown = new Dropdown(null, (name: string) => this.#setUnitName(name), undefined, "Unit label"); - this.#unitLoadoutDropdown = new Dropdown(null, (loadout: string) => this.#setUnitLoadout(loadout), undefined, "Unit loadout"); - this.#unitCountDropdown = new Dropdown(null, (count: string) => this.#setUnitCount(count), undefined, "Unit count"); - this.#unitCountryDropdown = new Dropdown(null, () => { /* Custom button implementation */ }, undefined, "Unit country"); - this.#unitLiveryDropdown = new Dropdown(null, (livery: string) => this.#setUnitLivery(livery), undefined, "Unit livery"); + this.#unitRoleTypeDropdown = new Dropdown(null, (roleType: string) => this.#setUnitRoleType(roleType), undefined, "Role"); + this.#unitLabelDropdown = new Dropdown(null, (name: string) => this.#setUnitName(name), undefined, "Type"); + this.#unitLoadoutDropdown = new Dropdown(null, (loadout: string) => this.#setUnitLoadout(loadout), undefined, "Loadout"); + this.#unitCountDropdown = new Dropdown(null, (count: string) => this.#setUnitCount(count), undefined, "Count"); + this.#unitCountryDropdown = new Dropdown(null, () => { /* Custom button implementation */ }, undefined, "Country"); + this.#unitLiveryDropdown = new Dropdown(null, (livery: string) => this.#setUnitLivery(livery), undefined, "Livery"); this.#unitSpawnAltitudeSlider = new Slider(null, 0, 1000, "ft", (value: number) => { this.spawnOptions.altitude = ftToM(value); }, { title: "Spawn altitude" }); /* The unit label and unit count are in the same "row" for clarity and compactness */ var unitLabelCountContainerEl = document.createElement("div"); unitLabelCountContainerEl.classList.add("unit-label-count-container"); - var divider = document.createElement("div"); - divider.innerText = "x"; - unitLabelCountContainerEl.append(this.#unitLabelDropdown.getContainer(), divider, this.#unitCountDropdown.getContainer()); + this.#unitCountDivider = document.createElement("div"); + this.#unitCountDivider.innerText = "x"; + unitLabelCountContainerEl.append(this.#unitLabelDropdown.getContainer(), this.#unitCountDivider, this.#unitCountDropdown.getContainer()); /* Create the unit image and loadout elements */ this.#unitImageEl = document.createElement("img"); @@ -84,38 +98,37 @@ export class UnitSpawnMenu { this.#unitLoadoutListEl.classList.add("unit-loadout-list"); this.#unitLoadoutPreviewEl.append(this.#unitImageEl, this.#unitLoadoutListEl); - /* Create the divider and the advanced options collapsible div */ - var advancedOptionsDiv = document.createElement("div"); - advancedOptionsDiv.classList.add("contextmenu-advanced-options", "hide"); - var advancedOptionsToggle = document.createElement("div"); - advancedOptionsToggle.classList.add("contextmenu-advanced-options-toggle"); - var advancedOptionsText = document.createElement("div"); - advancedOptionsText.innerText = "Advanced options"; - var advancedOptionsHr = document.createElement("hr"); - advancedOptionsToggle.append(advancedOptionsText, advancedOptionsHr); - advancedOptionsToggle.addEventListener("click", () => { - advancedOptionsDiv.classList.toggle("hide"); + /* Create the advanced options collapsible div */ + this.#advancedOptionsDiv = document.createElement("div"); + this.#advancedOptionsDiv.classList.add("contextmenu-advanced-options", "hide"); + this.#advancedOptionsToggle = document.createElement("div"); + this.#advancedOptionsToggle.classList.add("contextmenu-advanced-options-toggle"); + this.#advancedOptionsText = document.createElement("div"); + this.#advancedOptionsText.innerText = "Faction / Liveries"; + this.#advancedOptionsToggle.append(this.#advancedOptionsText); + this.#advancedOptionsToggle.addEventListener("click", () => { + this.#advancedOptionsToggle.classList.toggle("is-open"); + this.#advancedOptionsDiv.classList.toggle("hide"); this.#container.dispatchEvent(new Event("resize")); }); - advancedOptionsDiv.append(this.#unitCountryDropdown.getContainer(), this.#unitLiveryDropdown.getContainer(), - this.#unitSpawnAltitudeSlider.getContainer() as HTMLElement); + this.#advancedOptionsDiv.append(this.#unitCountryDropdown.getContainer(), this.#unitLiveryDropdown.getContainer()); - /* Create the divider and the metadata collapsible div */ - var metadataDiv = document.createElement("div"); - metadataDiv.classList.add("contextmenu-metadata", "hide"); - var metadataToggle = document.createElement("div"); - metadataToggle.classList.add("contextmenu-metadata-toggle"); - var metadataText = document.createElement("div"); - metadataText.innerText = "Info"; - var metadataHr = document.createElement("hr"); - metadataToggle.append(metadataText, metadataHr); - metadataToggle.addEventListener("click", () => { - metadataDiv.classList.toggle("hide"); + /* Create the unit info collapsible div */ + this.#unitInfoDiv = document.createElement("div"); + this.#unitInfoDiv.classList.add("contextmenu-metadata", "hide"); + this.#unitInfoToggle = document.createElement("div"); + this.#unitInfoToggle.classList.add("contextmenu-metadata-toggle"); + this.#unitInfoText = document.createElement("div"); + this.#unitInfoText.innerText = "Unit information"; + this.#unitInfoToggle.append(this.#unitInfoText); + this.#unitInfoToggle.addEventListener("click", () => { + this.#unitInfoToggle.classList.toggle("is-open"); + this.#unitInfoDiv.classList.toggle("hide"); this.#container.dispatchEvent(new Event("resize")); }); this.#descriptionDiv = document.createElement("div"); this.#abilitiesDiv = document.createElement("div"); - metadataDiv.append(this.#descriptionDiv, this.#abilitiesDiv); + this.#unitInfoDiv.append(this.#descriptionDiv, this.#abilitiesDiv); /* Create the unit deploy button */ this.#deployUnitButtonEl = document.createElement("button"); @@ -128,8 +141,8 @@ export class UnitSpawnMenu { }); /* Assemble all components */ - this.#container.append(this.#unitRoleTypeDropdown.getContainer(), unitLabelCountContainerEl, this.#unitLoadoutDropdown.getContainer(), - this.#unitLoadoutPreviewEl, advancedOptionsToggle, advancedOptionsDiv, metadataToggle, metadataDiv, this.#deployUnitButtonEl); + this.#container.append(this.#unitRoleTypeDropdown.getContainer(), unitLabelCountContainerEl, this.#unitLoadoutDropdown.getContainer(), this.#unitSpawnAltitudeSlider.getContainer() as HTMLElement, + this.#unitLoadoutPreviewEl, this.#advancedOptionsToggle, this.#advancedOptionsDiv, this.#unitInfoToggle, this.#unitInfoDiv, this.#deployUnitButtonEl); /* Load the country codes from the public folder */ var xhr = new XMLHttpRequest(); @@ -144,8 +157,29 @@ export class UnitSpawnMenu { }; xhr.send(); + /* Create the range circle previews */ + this.#engagementCircle = new Circle(this.spawnOptions.latlng, { radius: 0, weight: 4, opacity: 0.8, fillOpacity: 0, dashArray: "4 8", interactive: false, bubblingMouseEvents: false }); + this.#acquisitionCircle = new Circle(this.spawnOptions.latlng, { radius: 0, weight: 2, opacity: 0.8, fillOpacity: 0, dashArray: "8 12", interactive: false, bubblingMouseEvents: false }); + /* Event listeners */ this.#container.addEventListener("unitRoleTypeChanged", () => { + /* Shown the unit label and the unit count dropdowns */ + this.#unitLabelDropdown.show(); + this.#unitCountDivider.classList.remove("hide"); + this.#unitCountDropdown.show(); + + /* Hide all the other components */ + this.#unitLoadoutDropdown.hide(); + this.#unitSpawnAltitudeSlider.hide(); + this.#unitLoadoutPreviewEl.classList.add("hide"); + this.#advancedOptionsDiv.classList.add("hide"); + this.#unitInfoDiv.classList.add("hide"); + this.#advancedOptionsText.classList.add("hide"); + this.#advancedOptionsToggle.classList.add("hide"); + this.#unitInfoText.classList.add("hide"); + this.#unitInfoToggle.classList.add("hide"); + + /* Disable the spawn button */ this.#deployUnitButtonEl.disabled = true; this.#unitLabelDropdown.reset(); this.#unitLoadoutListEl.replaceChildren(); @@ -153,6 +187,7 @@ export class UnitSpawnMenu { this.#unitImageEl.classList.toggle("hide", true); this.#unitLiveryDropdown.reset(); + /* Populate the labels dropdown from the database */ var blueprints: UnitBlueprint[] = []; if (this.#orderByRole) blueprints = this.#unitDatabase.getByRole(this.spawnOptions.roleType); @@ -188,8 +223,10 @@ export class UnitSpawnMenu { } } + /* Request resizing */ this.#container.dispatchEvent(new Event("resize")); + /* Reset the spawn options */ this.spawnOptions.name = ""; this.spawnOptions.loadout = undefined; this.spawnOptions.liveryID = undefined; @@ -198,23 +235,43 @@ export class UnitSpawnMenu { }) this.#container.addEventListener("unitLabelChanged", () => { + /* If enabled, show the altitude slideer and loadouts section */ + if (this.#showAltitudeSlider) + this.#unitSpawnAltitudeSlider.show(); + + if (this.#showLoadout) { + this.#unitLoadoutDropdown.show(); + this.#unitLoadoutPreviewEl.classList.remove("hide"); + } + + /* Show the advanced options and unit info sections */ + this.#advancedOptionsText.classList.remove("hide"); + this.#advancedOptionsToggle.classList.remove("hide"); + this.#advancedOptionsToggle.classList.remove("is-open"); + this.#unitInfoText.classList.remove("hide"); + this.#unitInfoToggle.classList.remove("hide"); + this.#unitInfoToggle.classList.remove("is-open"); + + /* Enable the spawn button */ this.#deployUnitButtonEl.disabled = false; + + /* If enabled, populate the loadout dropdown */ if (!this.#unitLoadoutDropdown.isHidden()) { this.#unitLoadoutDropdown.setOptions(this.#unitDatabase.getLoadoutNamesByRole(this.spawnOptions.name, this.spawnOptions.roleType)); this.#unitLoadoutDropdown.selectValue(0); } + /* Get the unit data from the db */ var blueprint = this.#unitDatabase.getByName(this.spawnOptions.name); - + /* Shown the unit silhouette */ this.#unitImageEl.src = `images/units/${blueprint?.filename}`; - this.#unitImageEl.classList.toggle("hide", !(blueprint?.filename !== undefined)); + this.#unitImageEl.classList.toggle("hide", !(blueprint?.filename !== undefined && blueprint?.filename !== '')); + /* Set the livery options */ this.#setUnitLiveryOptions(); - this.#container.dispatchEvent(new Event("resize")); - this.#computeSpawnPoints(); - + /* Populate the description and abilities sections */ this.#descriptionDiv.replaceChildren(); this.#abilitiesDiv.replaceChildren(); @@ -235,10 +292,16 @@ export class UnitSpawnMenu { } } + /* Show the range circles */ this.showCirclesPreviews(); + + /* Request resizing */ + this.#container.dispatchEvent(new Event("resize")); + this.#computeSpawnPoints(); }) this.#container.addEventListener("unitLoadoutChanged", () => { + /* Update the loadout information */ var items = this.spawnOptions.loadout?.items.map((item: any) => { return `${item.quantity}x ${item.name}`; }); if (items != undefined) { items.length == 0 ? items.push("Empty loadout") : ""; @@ -255,10 +318,12 @@ export class UnitSpawnMenu { }) this.#container.addEventListener("unitCountChanged", () => { + /* Recompute the spawn points */ this.#computeSpawnPoints(); }) this.#container.addEventListener("unitCountryChanged", () => { + /* Get the unit liveries by country */ this.#setUnitLiveryOptions(); }) @@ -267,13 +332,13 @@ export class UnitSpawnMenu { }) document.addEventListener('activeCoalitionChanged', () => { + /* If the coalition changed, update the circle previews to set the colours */ this.showCirclesPreviews(); }); - - this.#engagementCircle = new Circle(this.spawnOptions.latlng, { radius: 0, weight: 4, opacity: 0.8, fillOpacity: 0, dashArray: "4 8", interactive: false, bubblingMouseEvents: false }); - this.#acquisitionCircle = new Circle(this.spawnOptions.latlng, { radius: 0, weight: 2, opacity: 0.8, fillOpacity: 0, dashArray: "8 12", interactive: false, bubblingMouseEvents: false }); } + abstract deployUnits(spawnOptions: UnitSpawnOptions, unitsCount: number): void; + getContainer() { return this.#container; } @@ -283,7 +348,10 @@ export class UnitSpawnMenu { } reset() { + /* Disable the spawn button */ this.#deployUnitButtonEl.disabled = true; + + /* Reset all the dropdowns */ this.#unitRoleTypeDropdown.reset(); this.#unitLabelDropdown.reset(); this.#unitLiveryDropdown.reset(); @@ -292,19 +360,37 @@ export class UnitSpawnMenu { else this.#unitRoleTypeDropdown.setOptions(this.#unitDatabase.getTypes(this.unitTypeFilter)); + /* Reset the contents of the div elements */ this.#unitLoadoutListEl.replaceChildren(); this.#unitLoadoutDropdown.reset(); this.#unitImageEl.classList.toggle("hide", true); this.#descriptionDiv.replaceChildren(); this.#abilitiesDiv.replaceChildren(); - this.setCountries(); - this.#container.dispatchEvent(new Event("resize")); + /* Hide everything but the unit type dropdown */ + this.#unitLabelDropdown.hide(); + this.#unitCountDivider.classList.add("hide"); + this.#unitCountDropdown.hide(); + this.#unitLoadoutDropdown.hide(); + this.#unitSpawnAltitudeSlider.hide(); + this.#unitLoadoutPreviewEl.classList.add("hide"); + this.#advancedOptionsDiv.classList.add("hide"); + this.#unitInfoDiv.classList.add("hide"); + this.#advancedOptionsText.classList.add("hide"); + this.#advancedOptionsToggle.classList.add("hide"); + this.#unitInfoText.classList.add("hide"); + this.#unitInfoToggle.classList.add("hide"); + /* Get the countries and clear the circle previews */ + this.setCountries(); this.clearCirclesPreviews(); + + /* Request resizing */ + this.#container.dispatchEvent(new Event("resize")); } setCountries() { + /* Create the countries dropdown elements (with the little flags) */ var coalitions = getApp().getMissionManager().getCoalitions(); var countries = Object.values(coalitions[getApp().getActiveCoalition() as keyof typeof coalitions]); this.#unitCountryDropdown.setOptionsElements(this.#createCountryButtons(this.#unitCountryDropdown, countries, (country: string) => { this.#setUnitCountry(country) })); @@ -315,13 +401,6 @@ export class UnitSpawnMenu { } } - refreshOptions() { - //if (!this.#unitDatabase.getTypes().includes(this.#unitTypeDropdown.getValue())) - // this.reset(); - //if (!this.#unitDatabase.getByType(this.#unitTypeDropdown.getValue()).map((blueprint) => { return blueprint.label }).includes(this.#unitLabelDropdown.getValue())) - // this.resetUnitLabel(); - } - showCirclesPreviews() { this.clearCirclesPreviews(); @@ -425,6 +504,14 @@ export class UnitSpawnMenu { return this.#unitSpawnAltitudeSlider; } + setShowLoadout(showLoadout: boolean) { + this.#showLoadout = showLoadout; + } + + setShowAltitudeSlider(showAltitudeSlider: boolean) { + this.#showAltitudeSlider = showAltitudeSlider; + } + #setUnitRoleType(roleType: string) { this.spawnOptions.roleType = roleType; this.#container.dispatchEvent(new Event("unitRoleTypeChanged")); @@ -482,10 +569,6 @@ export class UnitSpawnMenu { } } - deployUnits(spawnOptions: UnitSpawnOptions, unitsCount: number) { - /* Virtual function must be overloaded by inheriting classes */ - } - #createCountryButtons(parent: Dropdown, countries: string[], callback: CallableFunction) { return Object.values(countries).map((country: string) => { var el = document.createElement("div"); @@ -614,7 +697,6 @@ export class HelicopterSpawnMenu extends UnitSpawnMenu { } export class GroundUnitSpawnMenu extends UnitSpawnMenu { - protected showRangeCircles: boolean = true; protected unitTypeFilter = (unit:any) => {return !(GROUND_UNIT_AIR_DEFENCE_REGEX.test(unit.type))}; @@ -625,8 +707,8 @@ export class GroundUnitSpawnMenu extends UnitSpawnMenu { constructor(ID: string){ super(ID, groundUnitDatabase, false); this.setMaxUnitCount(20); - this.getAltitudeSlider().hide(); - this.getLoadoutDropdown().hide(); + this.setShowAltitudeSlider(false); + this.setShowLoadout(false); this.getLoadoutPreview().classList.add("hide"); } @@ -656,7 +738,6 @@ export class GroundUnitSpawnMenu extends UnitSpawnMenu { } export class AirDefenceUnitSpawnMenu extends GroundUnitSpawnMenu { - protected unitTypeFilter = (unit:any) => {return GROUND_UNIT_AIR_DEFENCE_REGEX.test(unit.type)}; /** @@ -677,9 +758,8 @@ export class NavyUnitSpawnMenu extends UnitSpawnMenu { constructor(ID: string){ super(ID, navyUnitDatabase, false); this.setMaxUnitCount(4); - this.getAltitudeSlider().hide(); - this.getLoadoutDropdown().hide(); - this.getLoadoutPreview().classList.add("hide"); + this.setShowAltitudeSlider(false); + this.setShowLoadout(false); } deployUnits(spawnOptions: UnitSpawnOptions, unitsCount: number) { diff --git a/client/src/map/map.ts b/client/src/map/map.ts index d9145a70..f6694b71 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -7,7 +7,7 @@ import { AirbaseContextMenu } from "../contextmenus/airbasecontextmenu"; import { Dropdown } from "../controls/dropdown"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { bearing, createCheckboxOption } from "../other/utils"; +import { bearing, createCheckboxOption, polyContains } from "../other/utils"; import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; @@ -559,7 +559,7 @@ export class Map extends L.Map { /* Coalition areas are ordered in the #coalitionAreas array according to their zindex. Select the upper one */ for (let coalitionArea of this.#coalitionAreas) { - if (coalitionArea.getBounds().contains(e.latlng)) { + if (polyContains(e.latlng, coalitionArea)) { if (coalitionArea.getSelected()) clickedCoalitionArea = coalitionArea; else @@ -662,7 +662,7 @@ export class Map extends L.Map { this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); this.#updateDestinationCursors(); } - else if (this.#state === COALITIONAREA_DRAW_POLYGON) { + else if (this.#state === COALITIONAREA_DRAW_POLYGON && e.latlng !== undefined) { this.#drawingCursor.setLatLng(e.latlng); /* Update the polygon being drawn with the current position of the mouse cursor */ this.getSelectedCoalitionArea()?.moveActiveVertex(e.latlng); diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index 4372bf16..1eedccbc 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -1108,6 +1108,36 @@ export class UnitsManager { const activeEras = Object.keys(eras).filter((key: string) => { return eras[key]; }); const activeRanges = Object.keys(ranges).filter((key: string) => { return ranges[key]; }); + var airbases = getApp().getMissionManager().getAirbases(); + Object.keys(airbases).forEach((airbaseName: string) => { + var airbase = airbases[airbaseName]; + /* Check if the city is inside the coalition area */ + if (polyContains(new LatLng(airbase.getLatLng().lat, airbase.getLatLng().lng), coalitionArea)) { + /* Arbitrary formula to obtain a number of units */ + var pointsNumber = 2 + 40 * density / 100; + for (let i = 0; i < pointsNumber; i++) { + /* Place the unit nearby the airbase, depending on the distribution parameter */ + var bearing = Math.random() * 360; + var distance = Math.random() * distribution * 100; + const latlng = bearingAndDistanceToLatLng(airbase.getLatLng().lat, airbase.getLatLng().lng, bearing, distance); + + /* Make sure the unit is still inside the coalition area */ + if (polyContains(latlng, coalitionArea)) { + const type = activeTypes[Math.floor(Math.random() * activeTypes.length)]; + if (Math.random() < IADSDensities[type]) { + /* Get a random blueprint depending on the selected parameters and spawn the unit */ + const unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges }); + if (unitBlueprint) { + this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "" }], coalitionArea.getCoalition(), true, "", "", (res: any) =>{ + getApp().getMap().addTemporaryMarker(latlng, unitBlueprint.name, getApp().getActiveCoalition(), res.commandHash); + }); + } + } + } + } + } + }) + citiesDatabase.forEach((city: { lat: number, lng: number, pop: number }) => { /* Check if the city is inside the coalition area */ if (polyContains(new LatLng(city.lat, city.lng), coalitionArea)) { @@ -1125,8 +1155,11 @@ export class UnitsManager { if (Math.random() < IADSDensities[type]) { /* Get a random blueprint depending on the selected parameters and spawn the unit */ const unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges }); - if (unitBlueprint) - this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "" }], coalitionArea.getCoalition(), true); + if (unitBlueprint) { + this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "" }], coalitionArea.getCoalition(), true, "", "", (res: any) =>{ + getApp().getMap().addTemporaryMarker(latlng, unitBlueprint.name, getApp().getActiveCoalition(), res.commandHash); + }); + } } } }