From 9cc6e9e790ff10c41d871a5cce7e6841d204d39c Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sat, 4 Nov 2023 09:57:18 +0100 Subject: [PATCH] More work on miss on purpose --- client/@types/olympus/index.d.ts | 103 +++++++++--- client/plugins/databasemanager/index.js | 50 +++--- .../src/databasemanagerplugin.ts | 2 +- .../databasemanager/src/grounduniteditor.ts | 5 + .../databasemanager/src/loadouteditor.ts | 8 +- .../plugins/databasemanager/src/uniteditor.ts | 42 +++-- client/plugins/databasemanager/src/utils.ts | 68 +++++--- client/plugins/databasemanager/style.css | 11 +- .../databases/units/groundunitdatabase.json | 13 +- .../public/stylesheets/panels/unitcontrol.css | 23 +-- client/public/stylesheets/panels/unitinfo.css | 4 +- client/public/stylesheets/style/style.css | 1 + .../images/buttons/emissions/attack.svg | 1 + .../images/buttons/emissions/defend.svg | 3 +- .../olympus/images/buttons/emissions/free.svg | 3 +- .../images/buttons/emissions/silent.svg | 3 +- .../olympus/images/buttons/intensity/1.svg | 45 +++-- .../olympus/images/buttons/intensity/2.svg | 49 ++++-- .../olympus/images/buttons/intensity/3.svg | 70 ++++---- .../olympus/images/buttons/roe/designated.svg | 3 +- .../olympus/images/buttons/roe/free.svg | 3 +- .../olympus/images/buttons/roe/hold.svg | 3 +- .../olympus/images/buttons/roe/return.svg | 3 +- .../olympus/images/buttons/scatter/1.svg | 6 +- .../olympus/images/buttons/scatter/2.svg | 6 +- .../olympus/images/buttons/scatter/3.svg | 6 +- .../olympus/images/buttons/threat/evade.svg | 12 +- .../images/buttons/threat/manoeuvre.svg | 3 +- .../olympus/images/buttons/threat/none.svg | 3 +- .../olympus/images/buttons/threat/passive.svg | 15 +- .../images/icons/circle-question-regular.svg | 1 + client/public/themes/olympus/theme.css | 1 + client/src/constants/constants.ts | 12 +- client/src/interfaces.ts | 5 + client/src/unit/unit.ts | 11 +- client/views/panels/unitcontrol.ejs | 14 +- scripts/OlympusCommand.lua | 8 + src/core/include/groundunit.h | 2 + src/core/include/unit.h | 1 + src/core/src/groundunit.cpp | 156 ++++++++++++------ 40 files changed, 515 insertions(+), 263 deletions(-) create mode 100644 client/public/themes/olympus/images/icons/circle-question-regular.svg diff --git a/client/@types/olympus/index.d.ts b/client/@types/olympus/index.d.ts index faa3f853..2b8e73ad 100644 --- a/client/@types/olympus/index.d.ts +++ b/client/@types/olympus/index.d.ts @@ -107,6 +107,8 @@ declare module "constants/constants" { export const ROEDescriptions: string[]; export const reactionsToThreatDescriptions: string[]; export const emissionsCountermeasuresDescriptions: string[]; + export const shotsScatterDescriptions: string[]; + export const shotsIntensityDescriptions: string[]; export const minSpeedValues: { [key: string]: number; }; @@ -221,31 +223,35 @@ declare module "constants/constants" { hasTask = 12, position = 13, speed = 14, - heading = 15, - isActiveTanker = 16, - isActiveAWACS = 17, - onOff = 18, - followRoads = 19, - fuel = 20, - desiredSpeed = 21, - desiredSpeedType = 22, - desiredAltitude = 23, - desiredAltitudeType = 24, - leaderID = 25, - formationOffset = 26, - targetID = 27, - targetPosition = 28, - ROE = 29, - reactionToThreat = 30, - emissionsCountermeasures = 31, - TACAN = 32, - radio = 33, - generalSettings = 34, - ammo = 35, - contacts = 36, - activePath = 37, - isLeader = 38, - operateAs = 39, + horizontalVelocity = 15, + verticalVelocity = 16, + heading = 17, + isActiveTanker = 18, + isActiveAWACS = 19, + onOff = 20, + followRoads = 21, + fuel = 22, + desiredSpeed = 23, + desiredSpeedType = 24, + desiredAltitude = 25, + desiredAltitudeType = 26, + leaderID = 27, + formationOffset = 28, + targetID = 29, + targetPosition = 30, + ROE = 31, + reactionToThreat = 32, + emissionsCountermeasures = 33, + TACAN = 34, + radio = 35, + generalSettings = 36, + ammo = 37, + contacts = 38, + activePath = 39, + isLeader = 40, + operateAs = 41, + shotsScatter = 42, + shotsIntensity = 43, endOfData = 255 } export const MGRS_PRECISION_10KM = 2; @@ -253,6 +259,8 @@ declare module "constants/constants" { export const MGRS_PRECISION_100M = 4; export const MGRS_PRECISION_10M = 5; export const MGRS_PRECISION_1M = 6; + export const DELETE_CYCLE_TIME = 0.05; + export const DELETE_SLOW_THRESHOLD = 50; } declare module "map/markers/custommarker" { import { Map, Marker } from "leaflet"; @@ -303,7 +311,7 @@ declare module "controls/dropdown" { #private; constructor(ID: string | null, callback: CallableFunction, options?: string[] | null, defaultText?: string); getContainer(): HTMLElement; - setOptions(optionsList: string[], sortAlphabetically?: boolean): void; + setOptions(optionsList: string[], sort?: "" | "string" | "number"): void; setOptionsElements(optionsElements: HTMLElement[]): void; getOptionElements(): HTMLCollection; addOptionElement(optionElement: HTMLElement): void; @@ -498,6 +506,8 @@ declare module "interfaces" { hasTask: boolean; position: LatLng; speed: number; + horizontalVelocity: number; + verticalVelocity: number; heading: number; isActiveTanker: boolean; isActiveAWACS: boolean; @@ -523,6 +533,8 @@ declare module "interfaces" { activePath: LatLng[]; isLeader: boolean; operateAs: string; + shotsScatter: number; + shotsIntensity: number; } export interface LoadoutItemBlueprint { name: string; @@ -558,12 +570,17 @@ declare module "interfaces" { muzzleVelocity?: number; aimTime?: number; shotsToFire?: number; + shotsBaseInterval?: number; + shotsBaseScatter?: number; description?: string; abilities?: string; acquisitionRange?: number; engagementRange?: number; + targetingRange?: number; canTargetPoint?: boolean; canRearm?: boolean; + canAAA?: boolean; + indirectFire?: boolean; } export interface UnitSpawnOptions { roleType: string; @@ -786,9 +803,11 @@ declare module "controls/unitspawnmenu" { import { UnitSpawnOptions } from "interfaces"; export class UnitSpawnMenu { #private; - spawnOptions: UnitSpawnOptions; + protected showRangeCircles: boolean; + protected spawnOptions: UnitSpawnOptions; constructor(ID: string, unitDatabase: UnitDatabase, orderByRole: boolean); getContainer(): HTMLElement; + getVisible(): boolean; reset(): void; setCountries(): void; refreshOptions(): void; @@ -824,6 +843,7 @@ declare module "controls/unitspawnmenu" { deployUnits(spawnOptions: UnitSpawnOptions, unitsCount: number): void; } export class GroundUnitSpawnMenu extends UnitSpawnMenu { + protected showRangeCircles: boolean; /** * * @param ID - the ID of the HTML element which will contain the context menu @@ -1036,6 +1056,8 @@ declare module "unit/unit" { getHasTask(): boolean; getPosition(): LatLng; getSpeed(): number; + getHorizontalVelocity(): number; + getVerticalVelocity(): number; getHeading(): number; getIsActiveTanker(): boolean; getIsActiveAWACS(): boolean; @@ -1061,6 +1083,8 @@ declare module "unit/unit" { getActivePath(): LatLng[]; getIsLeader(): boolean; getOperateAs(): string; + getShotsScatter(): number; + getShotsIntensity(): number; static getConstructor(type: string): typeof GroundUnit | undefined; constructor(ID: number); getCategory(): string; @@ -1097,6 +1121,8 @@ declare module "unit/unit" { isInViewport(): boolean; canTargetPoint(): boolean; canRearm(): boolean; + canAAA(): boolean; + indirectFire(): boolean; isTanker(): boolean; isAWACS(): boolean; /********************** Unit commands *************************/ @@ -1132,6 +1158,8 @@ declare module "unit/unit" { scenicAAA(): void; missOnPurpose(): void; landAtPoint(latlng: LatLng): void; + setShotsScatter(shotsScatter: number): void; + setShotsIntensity(shotsIntensity: number): void; /***********************************************/ getActions(): { [key: string]: { @@ -1647,6 +1675,14 @@ declare module "unit/citiesDatabase" { pop: number; }[]; } +declare module "dialog/dialog" { + import { Panel } from "panels/panel"; + export class Dialog extends Panel { + constructor(element: string); + hide(): void; + show(): void; + } +} declare module "unit/unitsmanager" { import { LatLng, LatLngBounds } from "leaflet"; import { Unit } from "unit/unit"; @@ -1876,6 +1912,16 @@ declare module "unit/unitsmanager" { * @param latlng Point where to land */ selectedUnitsLandAtPoint(latlng: LatLng): void; + /** Set a specific shots scatter to all the selected units + * + * @param shotsScatter Value to set + */ + selectedUnitsSetShotsScatter(shotsScatter: number): void; + /** Set a specific shots intensity to all the selected units + * + * @param shotsScatter Value to set + */ + selectedUnitsSetShotsIntensity(shotsIntensity: number): void; /*********************** Control operations on selected units ************************/ /** See getUnitsCategories for more info * @@ -2066,6 +2112,8 @@ declare module "server/servermanager" { scenicAAA(ID: number, coalition: string, callback?: CallableFunction): void; missOnPurpose(ID: number, coalition: string, callback?: CallableFunction): void; landAtPoint(ID: number, latlng: LatLng, callback?: CallableFunction): void; + setShotsScatter(ID: number, shotsScatter: number, callback?: CallableFunction): void; + setShotsIntensity(ID: number, shotsIntensity: number, callback?: CallableFunction): void; setAdvacedOptions(ID: number, isActiveTanker: boolean, isActiveAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback?: CallableFunction): void; setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: { blue: number; @@ -2106,6 +2154,7 @@ declare module "olympusapp" { export class OlympusApp { #private; constructor(); + getDialogManager(): Manager; getMap(): Map; getServerManager(): ServerManager; getPanelsManager(): Manager; diff --git a/client/plugins/databasemanager/index.js b/client/plugins/databasemanager/index.js index 82738524..432c8142 100644 --- a/client/plugins/databasemanager/index.js +++ b/client/plugins/databasemanager/index.js @@ -508,7 +508,7 @@ class GroundUnitEditor extends uniteditor_1.UnitEditor { * @param blueprint The blueprint to edit */ setBlueprint(blueprint) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s; __classPrivateFieldSet(this, _GroundUnitEditor_blueprint, blueprint, "f"); if (__classPrivateFieldGet(this, _GroundUnitEditor_blueprint, "f") !== null) { this.contentDiv2.replaceChildren(); @@ -525,14 +525,19 @@ class GroundUnitEditor extends uniteditor_1.UnitEditor { (0, utils_1.addStringInput)(this.contentDiv2, "Cost", (_b = String(blueprint.cost)) !== null && _b !== void 0 ? _b : "", "number", (value) => { blueprint.cost = parseFloat(value); }); (0, utils_1.addStringInput)(this.contentDiv2, "Acquisition range [m]", (_c = String(blueprint.acquisitionRange)) !== null && _c !== void 0 ? _c : "", "number", (value) => { blueprint.acquisitionRange = parseFloat(value); }); (0, utils_1.addStringInput)(this.contentDiv2, "Engagement range [m]", (_d = String(blueprint.engagementRange)) !== null && _d !== void 0 ? _d : "", "number", (value) => { blueprint.engagementRange = parseFloat(value); }); - (0, utils_1.addStringInput)(this.contentDiv2, "Barrel height [m]", (_e = String(blueprint.barrelHeight)) !== null && _e !== void 0 ? _e : "", "number", (value) => { blueprint.barrelHeight = parseFloat(value); }); - (0, utils_1.addStringInput)(this.contentDiv2, "Muzzle velocity [m/s]", (_f = String(blueprint.muzzleVelocity)) !== null && _f !== void 0 ? _f : "", "number", (value) => { blueprint.muzzleVelocity = parseFloat(value); }); - (0, utils_1.addStringInput)(this.contentDiv2, "Aim time [s]", (_g = String(blueprint.aimTime)) !== null && _g !== void 0 ? _g : "", "number", (value) => { blueprint.aimTime = parseFloat(value); }); - (0, utils_1.addStringInput)(this.contentDiv2, "Burst quantity", (_h = String(blueprint.shotsToFire)) !== null && _h !== void 0 ? _h : "", "number", (value) => { blueprint.shotsToFire = Math.round(parseFloat(value)); }); - (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can target point", (_j = blueprint.canTargetPoint) !== null && _j !== void 0 ? _j : false, (value) => { blueprint.canTargetPoint = value; }); - (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can rearm", (_k = blueprint.canRearm) !== null && _k !== void 0 ? _k : false, (value) => { blueprint.canRearm = value; }); - (0, utils_1.addStringInput)(this.contentDiv2, "Description", (_l = blueprint.description) !== null && _l !== void 0 ? _l : "", "text", (value) => { blueprint.description = value; }); - (0, utils_1.addStringInput)(this.contentDiv2, "Abilities", (_m = blueprint.abilities) !== null && _m !== void 0 ? _m : "", "text", (value) => { blueprint.abilities = value; }); + (0, utils_1.addStringInput)(this.contentDiv2, "Targeting range [m]", (_e = String(blueprint.targetingRange)) !== null && _e !== void 0 ? _e : "", "number", (value) => { blueprint.targetingRange = parseFloat(value); }); + (0, utils_1.addStringInput)(this.contentDiv2, "Barrel height [m]", (_f = String(blueprint.barrelHeight)) !== null && _f !== void 0 ? _f : "", "number", (value) => { blueprint.barrelHeight = parseFloat(value); }); + (0, utils_1.addStringInput)(this.contentDiv2, "Muzzle velocity [m/s]", (_g = String(blueprint.muzzleVelocity)) !== null && _g !== void 0 ? _g : "", "number", (value) => { blueprint.muzzleVelocity = parseFloat(value); }); + (0, utils_1.addStringInput)(this.contentDiv2, "Aim time [s]", (_h = String(blueprint.aimTime)) !== null && _h !== void 0 ? _h : "", "number", (value) => { blueprint.aimTime = parseFloat(value); }); + (0, utils_1.addStringInput)(this.contentDiv2, "Burst quantity", (_j = String(blueprint.shotsToFire)) !== null && _j !== void 0 ? _j : "", "number", (value) => { blueprint.shotsToFire = Math.round(parseFloat(value)); }); + (0, utils_1.addStringInput)(this.contentDiv2, "Burst base interval [s]", (_k = String(blueprint.shotsBaseInterval)) !== null && _k !== void 0 ? _k : "", "number", (value) => { blueprint.shotsBaseInterval = Math.round(parseFloat(value)); }); + (0, utils_1.addStringInput)(this.contentDiv2, "Base scatter [°]", (_l = String(blueprint.shotsBaseScatter)) !== null && _l !== void 0 ? _l : "", "number", (value) => { blueprint.shotsBaseScatter = Math.round(parseFloat(value)); }); + (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can target point", (_m = blueprint.canTargetPoint) !== null && _m !== void 0 ? _m : false, (value) => { blueprint.canTargetPoint = value; }); + (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can rearm", (_o = blueprint.canRearm) !== null && _o !== void 0 ? _o : false, (value) => { blueprint.canRearm = value; }); + (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can operate as AAA", (_p = blueprint.canAAA) !== null && _p !== void 0 ? _p : false, (value) => { blueprint.canAAA = value; }); + (0, utils_1.addCheckboxInput)(this.contentDiv2, "Indirect fire (e.g. mortar)", (_q = blueprint.indirectFire) !== null && _q !== void 0 ? _q : false, (value) => { blueprint.indirectFire = value; }); + (0, utils_1.addStringInput)(this.contentDiv2, "Description", (_r = blueprint.description) !== null && _r !== void 0 ? _r : "", "text", (value) => { blueprint.description = value; }); + (0, utils_1.addStringInput)(this.contentDiv2, "Abilities", (_s = blueprint.abilities) !== null && _s !== void 0 ? _s : "", "text", (value) => { blueprint.abilities = value; }); } } /** Add a new empty blueprint @@ -613,10 +618,10 @@ class LoadoutEditor { title.innerText = "Loadout properties"; __classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f").appendChild(title); if (__classPrivateFieldGet(this, _LoadoutEditor_loadout, "f")) { - var laodout = __classPrivateFieldGet(this, _LoadoutEditor_loadout, "f"); - (0, utils_1.addStringInput)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), "Name", laodout.name, "text", (value) => { laodout.name = value; __classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f").dispatchEvent(new Event("refresh")); }); - (0, utils_1.addStringInput)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), "Code", laodout.code, "text", (value) => { laodout.code = value; }); - (0, utils_1.addStringInput)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), "Roles", (0, utils_1.arrayToString)(laodout.roles), "text", (value) => { laodout.roles = (0, utils_1.stringToArray)(value); }); + var loadout = __classPrivateFieldGet(this, _LoadoutEditor_loadout, "f"); + (0, utils_1.addStringInput)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), "Name", loadout.name, "text", (value) => { loadout.name = value; __classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f").dispatchEvent(new Event("refresh")); }); + (0, utils_1.addStringInput)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), "Code", loadout.code, "text", (value) => { loadout.code = value; }); + (0, utils_1.addStringInput)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), "Roles", (0, utils_1.arrayToString)(loadout.roles), "text", (value) => { loadout.roles = (0, utils_1.stringToArray)(value); }); (0, utils_1.addLoadoutItemsEditor)(__classPrivateFieldGet(this, _LoadoutEditor_contentDiv, "f"), __classPrivateFieldGet(this, _LoadoutEditor_loadout, "f")); } } @@ -1034,25 +1039,12 @@ exports.addLoadoutsScroll = addLoadoutsScroll; * @returns The string */ function arrayToString(array) { - var value = "["; - var firstRole = true; - array.forEach((role) => { - value += firstRole ? "" : ", "; - firstRole = false; - value += role; - }); - value += "]"; - return value; + return "[" + array.join(", ") + "]"; } exports.arrayToString = arrayToString; function stringToArray(input) { - input = input.replace("[", "").replace("]", ""); - var values = input.split(","); - var result = []; - values.forEach((value) => { - result.push(value.trim()); - }); - return result; + var _a; + return (_a = input.match(/(\w)+/g)) !== null && _a !== void 0 ? _a : []; } exports.stringToArray = stringToArray; diff --git a/client/plugins/databasemanager/src/databasemanagerplugin.ts b/client/plugins/databasemanager/src/databasemanagerplugin.ts index a842acb2..690beba5 100644 --- a/client/plugins/databasemanager/src/databasemanagerplugin.ts +++ b/client/plugins/databasemanager/src/databasemanagerplugin.ts @@ -86,7 +86,7 @@ export class DatabaseManagerPlugin implements OlympusPlugin { /* Create the container for the database editor elements and the elements themselves */ this.#mainContentContainer = document.createElement("div"); this.#mainContentContainer.classList.add("dm-container"); - this.#element.appendChild(this.#mainContentContainer) + this.#element.appendChild(this.#mainContentContainer); this.#contentDiv1 = document.createElement("div"); this.#contentDiv1.classList.add("dm-content-container"); diff --git a/client/plugins/databasemanager/src/grounduniteditor.ts b/client/plugins/databasemanager/src/grounduniteditor.ts index f5e49d9f..f8172a75 100644 --- a/client/plugins/databasemanager/src/grounduniteditor.ts +++ b/client/plugins/databasemanager/src/grounduniteditor.ts @@ -36,12 +36,17 @@ export class GroundUnitEditor extends UnitEditor { addStringInput(this.contentDiv2, "Cost", String(blueprint.cost)?? "", "number", (value: string) => {blueprint.cost = parseFloat(value); }); addStringInput(this.contentDiv2, "Acquisition range [m]", String(blueprint.acquisitionRange)?? "", "number", (value: string) => {blueprint.acquisitionRange = parseFloat(value); }); addStringInput(this.contentDiv2, "Engagement range [m]", String(blueprint.engagementRange)?? "", "number", (value: string) => {blueprint.engagementRange = parseFloat(value); }); + addStringInput(this.contentDiv2, "Targeting range [m]", String(blueprint.targetingRange)?? "", "number", (value: string) => {blueprint.targetingRange = parseFloat(value); }); addStringInput(this.contentDiv2, "Barrel height [m]", String(blueprint.barrelHeight)?? "", "number", (value: string) => {blueprint.barrelHeight = parseFloat(value); }); addStringInput(this.contentDiv2, "Muzzle velocity [m/s]", String(blueprint.muzzleVelocity)?? "", "number", (value: string) => {blueprint.muzzleVelocity = parseFloat(value); }); addStringInput(this.contentDiv2, "Aim time [s]", String(blueprint.aimTime)?? "", "number", (value: string) => {blueprint.aimTime = parseFloat(value); }); addStringInput(this.contentDiv2, "Burst quantity", String(blueprint.shotsToFire)?? "", "number", (value: string) => {blueprint.shotsToFire = Math.round(parseFloat(value)); }); + addStringInput(this.contentDiv2, "Burst base interval [s]", String(blueprint.shotsBaseInterval)?? "", "number", (value: string) => {blueprint.shotsBaseInterval = Math.round(parseFloat(value)); }); + addStringInput(this.contentDiv2, "Base scatter [°]", String(blueprint.shotsBaseScatter)?? "", "number", (value: string) => {blueprint.shotsBaseScatter = Math.round(parseFloat(value)); }); addCheckboxInput(this.contentDiv2, "Can target point", blueprint.canTargetPoint ?? false, (value: boolean) => {blueprint.canTargetPoint = value;}) addCheckboxInput(this.contentDiv2, "Can rearm", blueprint.canRearm ?? false, (value: boolean) => {blueprint.canRearm = value;}) + addCheckboxInput(this.contentDiv2, "Can operate as AAA", blueprint.canAAA ?? false, (value: boolean) => {blueprint.canAAA = value;}) + addCheckboxInput(this.contentDiv2, "Indirect fire (e.g. mortar)", blueprint.indirectFire ?? false, (value: boolean) => {blueprint.indirectFire = value;}) addStringInput(this.contentDiv2, "Description", blueprint.description ?? "", "text", (value: string) => {blueprint.description = value; }); addStringInput(this.contentDiv2, "Abilities", blueprint.abilities ?? "", "text", (value: string) => {blueprint.abilities = value; }); } diff --git a/client/plugins/databasemanager/src/loadouteditor.ts b/client/plugins/databasemanager/src/loadouteditor.ts index 73fe32e1..5f07c605 100644 --- a/client/plugins/databasemanager/src/loadouteditor.ts +++ b/client/plugins/databasemanager/src/loadouteditor.ts @@ -37,10 +37,10 @@ export class LoadoutEditor { this.#contentDiv.appendChild(title); if (this.#loadout) { - var laodout = this.#loadout; - addStringInput(this.#contentDiv, "Name", laodout.name, "text", (value: string) => {laodout.name = value; this.#contentDiv.dispatchEvent(new Event("refresh"));}); - addStringInput(this.#contentDiv, "Code", laodout.code, "text", (value: string) => {laodout.code = value; }); - addStringInput(this.#contentDiv, "Roles", arrayToString(laodout.roles), "text", (value: string) => {laodout.roles = stringToArray(value);}); + var loadout = this.#loadout; + addStringInput(this.#contentDiv, "Name", loadout.name, "text", (value: string) => {loadout.name = value; this.#contentDiv.dispatchEvent(new Event("refresh"));}); + addStringInput(this.#contentDiv, "Code", loadout.code, "text", (value: string) => {loadout.code = value; }); + addStringInput(this.#contentDiv, "Roles", arrayToString(loadout.roles), "text", (value: string) => {loadout.roles = stringToArray(value);}); addLoadoutItemsEditor(this.#contentDiv, this.#loadout); } } diff --git a/client/plugins/databasemanager/src/uniteditor.ts b/client/plugins/databasemanager/src/uniteditor.ts index b031a2c4..00c59fc9 100644 --- a/client/plugins/databasemanager/src/uniteditor.ts +++ b/client/plugins/databasemanager/src/uniteditor.ts @@ -49,9 +49,9 @@ export abstract class UnitEditor { } /** Show the editor - * + * @param filter String filter */ - show() { + show(filter: string = "") { this.visible = true; this.contentDiv1.replaceChildren(); this.contentDiv2.replaceChildren(); @@ -62,20 +62,20 @@ export abstract class UnitEditor { var title = document.createElement("label"); title.innerText = "Units list"; this.contentDiv1.appendChild(title); - - addBlueprintsScroll(this.contentDiv1, this.database, (key: string) => { - if (this.database != null) - this.setBlueprint(this.database.blueprints[key]) - }) - addNewElementInput(this.contentDiv1, (ev: MouseEvent, input: HTMLInputElement) => { - if (input.value != "") - this.addBlueprint((input).value); - }); + var filterInput = document.createElement("input"); + filterInput.value = filter; + this.contentDiv1.appendChild(filterInput); + + filterInput.onchange = (e: Event) => { + this.show((e.target as HTMLInputElement).value); + } + + this.addBlueprints(filter); } } - /** Hid the editor + /** Hide the editor * */ hide() { @@ -93,6 +93,24 @@ export abstract class UnitEditor { return this.database; } + /** + * + * @param filter String filter + */ + addBlueprints(filter: string = "") { + if (this.database) { + addBlueprintsScroll(this.contentDiv1, this.database, filter, (key: string) => { + if (this.database != null) + this.setBlueprint(this.database.blueprints[key]) + }); + + addNewElementInput(this.contentDiv1, (ev: MouseEvent, input: HTMLInputElement) => { + if (input.value != "") + this.addBlueprint((input).value); + }); + } + } + /* Abstract methods which will depend on the specific type of units */ abstract setBlueprint(blueprint: UnitBlueprint): void; abstract addBlueprint(key: string): void; diff --git a/client/plugins/databasemanager/src/utils.ts b/client/plugins/databasemanager/src/utils.ts index 19cec041..004a8b84 100644 --- a/client/plugins/databasemanager/src/utils.ts +++ b/client/plugins/databasemanager/src/utils.ts @@ -174,40 +174,60 @@ export function addNewElementInput(div: HTMLElement, callback: CallableFunction) * * @param div The HTMLElement that will contain the list * @param database The database that will be used to fill the list of blueprints + * @param filter A string filter that will be executed to filter the blueprints to add * @param callback Callback called when the user clicks on one of the elements */ -export function addBlueprintsScroll(div: HTMLElement, database: {blueprints: {[key: string]: UnitBlueprint}}, callback: CallableFunction) { +export function addBlueprintsScroll(div: HTMLElement, database: {blueprints: {[key: string]: UnitBlueprint}}, filter: string, callback: CallableFunction) { var scrollDiv = document.createElement("div"); scrollDiv.classList.add("dm-scroll-container"); if (database !== null) { var blueprints: {[key: string]: UnitBlueprint} = database.blueprints; for (let key of Object.keys(blueprints).sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}))) { - var rowDiv = document.createElement("div"); - scrollDiv.appendChild(rowDiv); - - var text = document.createElement("label"); - text.textContent = key; - text.onclick = () => callback(key); - rowDiv.appendChild(text); - - let checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.checked = blueprints[key].enabled; - checkbox.onclick = () => { - console.log(checkbox.checked); - blueprints[key].enabled = checkbox.checked; + var addKey = true; + if (filter !== "") { + try { + var blueprint = blueprints[key]; + addKey = eval(filter); + } catch { + console.error("An error has occurred evaluating the blueprint filter") + } } - rowDiv.appendChild(checkbox); - /* This button allows to remove an element from the list. It requires a refresh. */ - var button = document.createElement("button"); - button.innerText = "X"; - button.onclick = () => { - delete blueprints[key]; - div.dispatchEvent(new Event("refresh")); + if (addKey) { + var rowDiv = document.createElement("div"); + scrollDiv.appendChild(rowDiv); + + let text = document.createElement("label"); + text.textContent = key; + text.onclick = () => { + callback(key); + const collection = document.getElementsByClassName("blueprint-selected"); + for (let i = 0; i < collection.length; i++) { + collection[i].classList.remove("blueprint-selected"); + } + text.classList.add("blueprint-selected"); + } + rowDiv.appendChild(text); + + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = blueprints[key].enabled; + checkbox.onclick = () => { + console.log(checkbox.checked); + blueprints[key].enabled = checkbox.checked; + } + rowDiv.appendChild(checkbox); + + /* This button allows to remove an element from the list. It requires a refresh. */ + var button = document.createElement("button"); + button.innerText = "X"; + button.onclick = () => { + delete blueprints[key]; + div.dispatchEvent(new Event("refresh")); + } + rowDiv.appendChild(button); } - rowDiv.appendChild(button); } } div.appendChild(scrollDiv); @@ -269,5 +289,5 @@ export function arrayToString(array: string[]) { export function stringToArray(input: string) { - return input.match( /(\w)+/g ); + return input.match( /(\w)+/g ) ?? []; } \ No newline at end of file diff --git a/client/plugins/databasemanager/style.css b/client/plugins/databasemanager/style.css index 9ba77136..094ddb22 100644 --- a/client/plugins/databasemanager/style.css +++ b/client/plugins/databasemanager/style.css @@ -115,6 +115,10 @@ font-weight: bold; } +#database-manager-panel input { + font-weight: bold; +} + .dm-scroll-container>div:nth-child(even) { background-color: gainsboro; } @@ -131,11 +135,16 @@ } .dm-scroll-container>div *:nth-child(1):hover { - background-color: var(--secondary-blue-text); + background-color: var(--accent-dark-blue); color: white; cursor: pointer; } +.blueprint-selected { + background-color: var(--accent-light-blue) !important; + color: white; +} + .dm-scroll-container>div { display: flex; align-items: center; diff --git a/client/public/databases/units/groundunitdatabase.json b/client/public/databases/units/groundunitdatabase.json index cca675d9..cf7ca6af 100644 --- a/client/public/databases/units/groundunitdatabase.json +++ b/client/public/databases/units/groundunitdatabase.json @@ -1029,13 +1029,19 @@ } }, "barrelHeight": 2.35, - "muzzleVelocity": 1440, + "muzzleVelocity": 1000, "acquisitionRange": 15000, "engagementRange": 4000, "description": "Tracked self-propelled anti-aircraft 35mm guns", "abilities": "", "canTargetPoint": true, - "canRearm": false + "canRearm": false, + "canAAA": true, + "aimTime": 5, + "shotsToFire": 2, + "shotsBaseInterval": 5, + "shotsBaseScatter": 2, + "targetingRange": 800 }, "Grad-URAL": { "name": "Grad-URAL", @@ -2282,7 +2288,8 @@ "description": "Single infantry carrying AK-74", "abilities": "", "canTargetPoint": true, - "canRearm": false + "canRearm": false, + "canAAA": true }, "KAMAZ Truck": { "name": "KAMAZ Truck", diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css index 770109f6..99509503 100644 --- a/client/public/stylesheets/panels/unitcontrol.css +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -166,20 +166,23 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { #unit-control-panel .switch-control { align-items: center; - display: grid; - grid-template-columns: 1.35fr 0.65fr; -} - -#unit-control-panel .switch-control>*:nth-child(2) { - justify-self: end; -} - -#unit-control-panel .switch-control>*:nth-child(3) { - color: var(--secondary-semitransparent-white); + display: flex; + width: 100%; + justify-content: space-between; } #unit-control-panel .switch-control h4 { margin: 0px; + display: flex; + align-items: center; +} + +#unit-control-panel .switch-control h4 img { + height: 15px; + margin-left: 10px; + cursor: pointer; + filter: invert(100%); + opacity: 80%; } #unit-control-panel .switch-control .ol-switch { diff --git a/client/public/stylesheets/panels/unitinfo.css b/client/public/stylesheets/panels/unitinfo.css index 50c90faf..b970dfa4 100644 --- a/client/public/stylesheets/panels/unitinfo.css +++ b/client/public/stylesheets/panels/unitinfo.css @@ -91,9 +91,9 @@ #loadout-silhouette { filter: invert(100%); - height: 100px; + height: 75px; margin-right: 25px; - width: 100px; + width: 75px; } #loadout-items { diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index 0a49d7a1..e78dfc8a 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -664,6 +664,7 @@ nav.ol-panel> :last-child { #unit-control-panel .ol-option-button button.selected svg * { fill: var(--background-steel); + stroke: var(--background-steel); } #rapid-controls { diff --git a/client/public/themes/olympus/images/buttons/emissions/attack.svg b/client/public/themes/olympus/images/buttons/emissions/attack.svg index 0846e4e6..0e1bd78d 100644 --- a/client/public/themes/olympus/images/buttons/emissions/attack.svg +++ b/client/public/themes/olympus/images/buttons/emissions/attack.svg @@ -38,6 +38,7 @@ inkscape:window-maximized="1" inkscape:current-layer="svg4" /> diff --git a/client/public/themes/olympus/images/buttons/emissions/defend.svg b/client/public/themes/olympus/images/buttons/emissions/defend.svg index 5c884188..6d13f0de 100644 --- a/client/public/themes/olympus/images/buttons/emissions/defend.svg +++ b/client/public/themes/olympus/images/buttons/emissions/defend.svg @@ -39,5 +39,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/emissions/free.svg b/client/public/themes/olympus/images/buttons/emissions/free.svg index f07b2db6..19fd8b25 100644 --- a/client/public/themes/olympus/images/buttons/emissions/free.svg +++ b/client/public/themes/olympus/images/buttons/emissions/free.svg @@ -39,5 +39,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/emissions/silent.svg b/client/public/themes/olympus/images/buttons/emissions/silent.svg index f735bc21..9ecb6737 100644 --- a/client/public/themes/olympus/images/buttons/emissions/silent.svg +++ b/client/public/themes/olympus/images/buttons/emissions/silent.svg @@ -39,5 +39,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/intensity/1.svg b/client/public/themes/olympus/images/buttons/intensity/1.svg index 2321bf1e..428226dc 100644 --- a/client/public/themes/olympus/images/buttons/intensity/1.svg +++ b/client/public/themes/olympus/images/buttons/intensity/1.svg @@ -7,7 +7,7 @@ version="1.1" id="svg4" sodipodi:docname="1.svg" - inkscape:version="1.2.2 (732a01da63, 2022-12-09)" + inkscape:version="1.1 (c68e22c387, 2021-05-23)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -28,19 +28,42 @@ fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" - inkscape:zoom="23.771428" - inkscape:cx="2.6712741" - inkscape:cy="12.283654" - inkscape:window-width="2560" - inkscape:window-height="1377" - inkscape:window-x="1912" + inkscape:zoom="16.808938" + inkscape:cx="4.2536893" + inkscape:cy="-1.2195892" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4" inkscape:showpageshadow="2" inkscape:deskcolor="#d1d1d1" /> - + + + diff --git a/client/public/themes/olympus/images/buttons/intensity/2.svg b/client/public/themes/olympus/images/buttons/intensity/2.svg index 55286ebb..63b73775 100644 --- a/client/public/themes/olympus/images/buttons/intensity/2.svg +++ b/client/public/themes/olympus/images/buttons/intensity/2.svg @@ -7,7 +7,7 @@ version="1.1" id="svg4" sodipodi:docname="2.svg" - inkscape:version="1.2.2 (732a01da63, 2022-12-09)" + inkscape:version="1.1 (c68e22c387, 2021-05-23)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -28,23 +28,42 @@ fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" - inkscape:zoom="47.542857" - inkscape:cx="7.0147236" - inkscape:cy="7.9717549" - inkscape:window-width="2560" - inkscape:window-height="1377" - inkscape:window-x="1912" + inkscape:zoom="16.808938" + inkscape:cx="4.2536893" + inkscape:cy="-1.2195892" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4" inkscape:showpageshadow="2" inkscape:deskcolor="#d1d1d1" /> - - + + + diff --git a/client/public/themes/olympus/images/buttons/intensity/3.svg b/client/public/themes/olympus/images/buttons/intensity/3.svg index 167f17fe..8241e7d9 100644 --- a/client/public/themes/olympus/images/buttons/intensity/3.svg +++ b/client/public/themes/olympus/images/buttons/intensity/3.svg @@ -2,20 +2,20 @@ + id="defs12" /> - - - + inkscape:current-layer="svg8" /> + + + diff --git a/client/public/themes/olympus/images/buttons/roe/designated.svg b/client/public/themes/olympus/images/buttons/roe/designated.svg index 0846e4e6..23107028 100644 --- a/client/public/themes/olympus/images/buttons/roe/designated.svg +++ b/client/public/themes/olympus/images/buttons/roe/designated.svg @@ -40,5 +40,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/roe/free.svg b/client/public/themes/olympus/images/buttons/roe/free.svg index f07b2db6..19fd8b25 100644 --- a/client/public/themes/olympus/images/buttons/roe/free.svg +++ b/client/public/themes/olympus/images/buttons/roe/free.svg @@ -39,5 +39,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/roe/hold.svg b/client/public/themes/olympus/images/buttons/roe/hold.svg index f735bc21..9ecb6737 100644 --- a/client/public/themes/olympus/images/buttons/roe/hold.svg +++ b/client/public/themes/olympus/images/buttons/roe/hold.svg @@ -39,5 +39,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/roe/return.svg b/client/public/themes/olympus/images/buttons/roe/return.svg index 5c884188..6d13f0de 100644 --- a/client/public/themes/olympus/images/buttons/roe/return.svg +++ b/client/public/themes/olympus/images/buttons/roe/return.svg @@ -39,5 +39,6 @@ + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/scatter/1.svg b/client/public/themes/olympus/images/buttons/scatter/1.svg index cbd96e07..3981c876 100644 --- a/client/public/themes/olympus/images/buttons/scatter/1.svg +++ b/client/public/themes/olympus/images/buttons/scatter/1.svg @@ -40,7 +40,7 @@ inkscape:showpageshadow="2" inkscape:deskcolor="#d1d1d1" /> + d="m 1.880341,3.4754242 a 11.467682,10.686775 0 0 1 11.467682,2e-7 L 7.614182,12.730443 Z" + fill="#5ca7ff" + stroke="#5ca7ff" /> diff --git a/client/public/themes/olympus/images/buttons/scatter/2.svg b/client/public/themes/olympus/images/buttons/scatter/2.svg index e9c1e883..3957af5a 100644 --- a/client/public/themes/olympus/images/buttons/scatter/2.svg +++ b/client/public/themes/olympus/images/buttons/scatter/2.svg @@ -40,7 +40,7 @@ inkscape:showpageshadow="2" inkscape:deskcolor="#d1d1d1" /> + d="m 3.6920035,2.6881593 a 11.467682,10.686775 0 0 1 7.8443565,-2e-7 L 7.614182,12.730443 Z" + fill="#5ca7ff" + stroke="#5ca7ff" /> diff --git a/client/public/themes/olympus/images/buttons/scatter/3.svg b/client/public/themes/olympus/images/buttons/scatter/3.svg index ac0086e1..446228c2 100644 --- a/client/public/themes/olympus/images/buttons/scatter/3.svg +++ b/client/public/themes/olympus/images/buttons/scatter/3.svg @@ -40,7 +40,7 @@ inkscape:showpageshadow="2" inkscape:deskcolor="#d1d1d1" /> + d="m 5.6228404,2.2060238 a 11.467682,10.686775 0 0 1 3.9826836,10e-8 L 7.614182,12.730443 Z" + fill="#5ca7ff" + stroke="#5ca7ff" /> diff --git a/client/public/themes/olympus/images/buttons/threat/evade.svg b/client/public/themes/olympus/images/buttons/threat/evade.svg index 54d7b49f..fb5dc6ba 100644 --- a/client/public/themes/olympus/images/buttons/threat/evade.svg +++ b/client/public/themes/olympus/images/buttons/threat/evade.svg @@ -39,7 +39,8 @@ + id="path2" + style="stroke: none"/> + id="path6" + style="stroke: none"/> + id="path8" + style="stroke: none"/> + id="path10" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg b/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg index fcc9376b..81fc8534 100644 --- a/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg +++ b/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg @@ -39,7 +39,8 @@ + id="path2" + style="stroke: none"/> + id="path2" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/buttons/threat/passive.svg b/client/public/themes/olympus/images/buttons/threat/passive.svg index ac558fef..c7a9d0d0 100644 --- a/client/public/themes/olympus/images/buttons/threat/passive.svg +++ b/client/public/themes/olympus/images/buttons/threat/passive.svg @@ -39,7 +39,8 @@ + id="path2" + style="stroke: none"/> + id="path6" + style="stroke: none"/> + id="path8" + style="stroke: none"/> + id="path10" + style="stroke: none"/> + id="path12" + style="stroke: none"/> diff --git a/client/public/themes/olympus/images/icons/circle-question-regular.svg b/client/public/themes/olympus/images/icons/circle-question-regular.svg new file mode 100644 index 00000000..588be4cf --- /dev/null +++ b/client/public/themes/olympus/images/icons/circle-question-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/theme.css b/client/public/themes/olympus/theme.css index df1c6244..41953812 100644 --- a/client/public/themes/olympus/theme.css +++ b/client/public/themes/olympus/theme.css @@ -23,6 +23,7 @@ --accent-amber: #ffd828; --accent-green: #8bff63; --accent-light-blue: #5ca7ff; + --accent-dark-blue: #017DC1; --transparent-accent-light-blue: rgba(92, 167, 255, .33); --accent-light-red: #F5B6B6; diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 37a1f040..98fc38b4 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -48,15 +48,15 @@ export const emissionsCountermeasuresDescriptions: string[] = [ ]; export const shotsScatterDescriptions: string[] = [ - "Large scatter", - "Medium scatter", - "Small scatter (Radar guided units will track shots when the enemy unit is close)" + "When performing scenic shooting tasks like simulated firefights, will shoot with a large scatter", + "When performing scenic shooting tasks like simulated firefights, will shoot with a medium scatter", + "When performing scenic shooting tasks like simulated firefights, will shoot with a small scatter (Radar guided units will track shots when the enemy unit is close)" ]; export const shotsIntensityDescriptions: string[] = [ - "Low intensity", - "Medium intensity", - "High intensity" + "When performing scenic shooting tasks like simulated firefights, will shoot with a low rate of fire", + "When performing scenic shooting tasks like simulated firefights, will shoot with a medium rate of fire", + "When performing scenic shooting tasks like simulated firefights, will shoot with a high rate of fire" ]; export const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 }; diff --git a/client/src/interfaces.ts b/client/src/interfaces.ts index e795091d..8ff8102b 100644 --- a/client/src/interfaces.ts +++ b/client/src/interfaces.ts @@ -214,12 +214,17 @@ export interface UnitBlueprint { muzzleVelocity?: number; aimTime?: number; shotsToFire?: number; + shotsBaseInterval?: number; + shotsBaseScatter?: number; description?: string; abilities?: string; acquisitionRange?: number; engagementRange?: number; + targetingRange?: number; canTargetPoint?: boolean; canRearm?: boolean; + canAAA?: boolean; + indirectFire?: boolean; } export interface UnitSpawnOptions { diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts index 4e75929f..fbc89a6b 100644 --- a/client/src/unit/unit.ts +++ b/client/src/unit/unit.ts @@ -447,7 +447,6 @@ export class Unit extends CustomMarker { return this.getDatabase()?.getSpawnPointsByName(this.getName()); } - /********************** Icon *************************/ createIcon(): void { /* Set the icon */ @@ -654,6 +653,14 @@ export class Unit extends CustomMarker { return this.getDatabase()?.getByName(this.#name)?.canRearm === true; } + canAAA() { + return this.getDatabase()?.getByName(this.#name)?.canAAA === true; + } + + indirectFire() { + return this.getDatabase()?.getByName(this.#name)?.indirectFire === true; + } + isTanker() { return this.canFulfillRole("Tanker"); } @@ -1462,7 +1469,7 @@ export class GroundUnit extends Unit { options["group-ground"] = { text: "Create group", tooltip: "Create a group from the selected units", type: "and" }; } - if (["AAA", "flak"].includes(this.getType())) { + if (this.canAAA()) { options["scenic-aaa"] = { text: "Scenic AAA", tooltip: "Shoot AAA in the air without aiming at any target, when a enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" }; options["miss-aaa"] = { text: "Miss on purpose AAA", tooltip: "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" }; } diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs index 0ff44f4f..85f287ac 100644 --- a/client/views/panels/unitcontrol.ejs +++ b/client/views/panels/unitcontrol.ejs @@ -76,31 +76,27 @@
-

Enable tanker

+

Enable tanker

-
Instructs the unit to operate as AAR tanker. A/A TACAN, radio frequency and callsign set in Settings dialog.
-

Airborne Early Warning

+

Airborne Early Warning

-
Enables datalink and AI radio calls. Radio frequency and callsign set in Settings dialog.
-

Operate as

+

Operate as

-
Determines if the unit will target red or blue units when performing scenic tasks.
-

Unit active

+

Unit active

-
Toggling this disables unit AI completely. It will no longer move, react or emit radio waves.
-

Follow roads

+

Follow roads

diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 87bf92d5..7be6dc31 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -269,6 +269,14 @@ function Olympus.buildTask(groupName, options) point = {x = point.x, y = point.z}, } } + -- Attack unit + elseif options['id'] == 'AttackUnit' and options['unitID'] then + task = { + id = 'AttackUnit', + params = { + unitId = options['unitID'], + } + } end end return task diff --git a/src/core/include/groundunit.h b/src/core/include/groundunit.h index 8ef0fb35..22a4a455 100644 --- a/src/core/include/groundunit.h +++ b/src/core/include/groundunit.h @@ -17,6 +17,8 @@ public: virtual void setOnOff(bool newOnOff, bool force = false); virtual void setFollowRoads(bool newFollowRoads, bool force = false); + void aimAtPoint(Coords aimTarget, double horizontalScatterMultiplier = 1, double verticalScatterMultiplier = 1); + protected: virtual void AIloop(); static json::value database; diff --git a/src/core/include/unit.h b/src/core/include/unit.h index a428dcbe..c85cbf44 100644 --- a/src/core/include/unit.h +++ b/src/core/include/unit.h @@ -204,6 +204,7 @@ protected: /********** Other **********/ unsigned int taskCheckCounter = 0; unsigned int internalCounter = 0; + Unit* missOnPurposeTarget = nullptr; bool hasTaskAssigned = false; double initialFuel = 0; map updateTimeMap; diff --git a/src/core/src/groundunit.cpp b/src/core/src/groundunit.cpp index 0ee297a3..2b8016dc 100644 --- a/src/core/src/groundunit.cpp +++ b/src/core/src/groundunit.cpp @@ -148,6 +148,8 @@ void GroundUnit::AIloop() break; } case State::REACH_DESTINATION: { + setTask("Reaching destination"); + string enrouteTask = ""; bool looping = false; @@ -189,37 +191,8 @@ void GroundUnit::AIloop() case State::SIMULATE_FIRE_FIGHT: { setTask("Simulating fire fight"); - if (!getHasTask() || internalCounter == 0) { - double dist; - double bearing1; - double bearing2; - Geodesic::WGS84().Inverse(position.lat, position.lng, targetPosition.lat, targetPosition.lng, dist, bearing1, bearing2); - - double r = 15; /* m */ - /* Default gun values */ - double barrelHeight = 1.0; /* m */ - double muzzleVelocity = 860; /* m/s */ - if (database.has_object_field(to_wstring(name))) { - json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) { - barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double(); - muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double(); - } - } - - double barrelElevation = r * (9.81 * dist / (2 * muzzleVelocity * muzzleVelocity) + (targetPosition.alt - (position.alt + barrelHeight)) / dist); /* m */ - - double lat = 0; - double lng = 0; - double randomBearing = bearing1 + (((double)(rand()) / (double)(RAND_MAX) - 0.5) * 2) * 0; // TODO put defined constant here - Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng); - - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation + barrelHeight << ", radius = 0.001}"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - setHasTask(true); + if (internalCounter == 0) { + aimAtPoint(targetPosition, 3.0, 0.0); } if (internalCounter == 0) @@ -264,6 +237,17 @@ void GroundUnit::AIloop() case State::MISS_ON_PURPOSE: { setTask("Missing on purpose"); + /* Check that the unit can perform AAA duties */ + bool canAAA = false; + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_boolean_field(L"canAAA")) + canAAA = databaseEntry[L"canAAA"].as_bool(); + } + + if (!canAAA) + setState(State::IDLE); + /* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */ if (internalCounter == 0 && getOperateAs() > 0) { double distance = 0; @@ -276,8 +260,9 @@ void GroundUnit::AIloop() double aimTime = 10; /* s */ unsigned int shotsToFire = 10; double shotsBaseInterval = 15; /* s */ - double shotsBaseScatter = 5; /* degs */ + double shotsBaseScatter = 2; /* degs */ double engagementRange = 10000; /* m */ + double targetingRange = 0; /* m */ if (database.has_object_field(to_wstring(name))) { json::value databaseEntry = database[to_wstring(name)]; @@ -295,38 +280,61 @@ void GroundUnit::AIloop() shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double(); if (databaseEntry.has_number_field(L"shotsBaseScatter")) shotsBaseScatter = databaseEntry[L"shotsBaseScatter"].as_number().to_double(); + if (databaseEntry.has_number_field(L"targetingRange")) + targetingRange = databaseEntry[L"targetingRange"].as_number().to_double(); } /* Only do if we have a valid target close enough for AAA */ - if (target != nullptr && distance < 10000 /* m */) { + if (target != nullptr && distance < 3 * engagementRange) { /* Approximate the flight time */ if (muzzleVelocity != 0) aimTime += distance / muzzleVelocity; internalCounter = (aimTime + (3 - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL; - /* Compute where the target will be in aimTime seconds. */ - double aimDistance = target->getHorizontalVelocity() * aimTime; - double aimLat = 0; - double aimLng = 0; - Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getHeading() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util function */ - double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * aimTime; + /* If the target is in targeting range and we are in highest precision mode, target it */ + if (distance < targetingRange && shotsScatter == 3) { + /* Send the command */ + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + setHasTask(true); + } + /* Else, do miss on purpose */ + else { + /* Compute where the target will be in aimTime seconds. */ + double aimDistance = target->getHorizontalVelocity() * aimTime; + double aimLat = 0; + double aimLng = 0; + Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getHeading() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util function */ + double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * aimTime; - /* Compute a random scattering depending on the distance and the selected shots scatter */ - double scatterDistance = distance * tan(shotsBaseScatter * (3 - shotsScatter) / 57.29577) * RANDOM_MINUS_ONE_TO_ONE; - double scatterAngle = 180 * RANDOM_MINUS_ONE_TO_ONE; - Geodesic::WGS84().Direct(aimLat, aimLng, scatterAngle, scatterDistance, aimLat, aimLng); /* TODO make util function */ - aimAlt = aimAlt + distance * tan(shotsBaseScatter * (3 - shotsScatter) / 57.29577) * RANDOM_MINUS_ONE_TO_ONE; + /* Send the command */ + if (distance > engagementRange) { + aimAtPoint(Coords(aimLat, aimLng, aimAlt)); + log("Aiming at point!"); + } + else { + /* Compute a random scattering depending on the distance and the selected shots scatter */ + double scatterDistance = distance * tan(shotsBaseScatter * (3 - shotsScatter) / 57.29577) * RANDOM_MINUS_ONE_TO_ONE; + double scatterAngle = 180 * RANDOM_MINUS_ONE_TO_ONE; + Geodesic::WGS84().Direct(aimLat, aimLng, scatterAngle, scatterDistance, aimLat, aimLng); /* TODO make util function */ + aimAlt = aimAlt + distance * tan(shotsBaseScatter * (3 - shotsScatter) / 57.29577) * RANDOM_MINUS_ONE_TO_ONE; - /* Send the command */ - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001, expendQty = " << shotsToFire << " }"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - setHasTask(true); + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001, expendQty = " << shotsToFire << " }"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + setHasTask(true); + } - setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); + setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); + } + + missOnPurposeTarget = target; } else { if (getHasTask()) @@ -343,6 +351,8 @@ void GroundUnit::AIloop() alertnessTimeConstant = databaseEntry[L"alertnessTimeConstant"].as_number().to_double(); } internalCounter = (5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) / FRAMERATE_TIME_INTERVAL; + missOnPurposeTarget = nullptr; + setTargetPosition(Coords(NULL)); } internalCounter--; @@ -382,3 +392,45 @@ void GroundUnit::setFollowRoads(bool newFollowRoads, bool force) resetActiveDestination(); /* Reset active destination to apply option*/ } } + +void GroundUnit::aimAtPoint(Coords aimTarget, double horizontalScatterMultiplier, double verticalScatterMultiplier) { + double dist; + double bearing1; + double bearing2; + Geodesic::WGS84().Inverse(position.lat, position.lng, aimTarget.lat, aimTarget.lng, dist, bearing1, bearing2); + + double r = 15; /* m */ + /* Default gun values */ + double barrelHeight = 1.0; /* m */ + double muzzleVelocity = 860; /* m/s */ + double shotsBaseScatter = 5; /* degs */ + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) { + barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double(); + muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double(); + } + if (databaseEntry.has_number_field(L"shotsBaseScatter")) + shotsBaseScatter = databaseEntry[L"shotsBaseScatter"].as_number().to_double(); + } + + //double barrelElevation = r * (9.81 * dist / (2 * muzzleVelocity * muzzleVelocity) + (aimTarget.alt - (position.alt + barrelHeight)) / dist); /* m */ + double deltaHeight = (aimTarget.alt - (position.alt + barrelHeight)); + double alpha = 9.81 / 2 * dist * dist / (muzzleVelocity * muzzleVelocity); + double inner = dist * dist - 4 * alpha * (alpha + deltaHeight); + if (inner > 0) { + double barrelElevation = r * tan(atan((dist - sqrt(inner)) / (2 * alpha)) + RANDOM_MINUS_ONE_TO_ONE * (3 - shotsScatter) * verticalScatterMultiplier); + + double lat = 0; + double lng = 0; + double randomBearing = bearing1 + RANDOM_MINUS_ONE_TO_ONE * (3 - shotsScatter) * horizontalScatterMultiplier; + Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng); + + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation + barrelHeight << ", radius = 0.001}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + setHasTask(true); + } +}