95 Commits

Author SHA1 Message Date
Ambroise Garel 3fd0d39be0 Missions with higher cloud cover and wind speed now award more XP 2025-10-19 18:55:02 +02:00
Ambroise Garel a51ff5cff0 Weather reports now include cloud cover and rain 2025-10-19 18:53:57 +02:00
Ambroise Garel a7bf94de5f Added TUM.weather table 2025-10-19 18:52:58 +02:00
Ambroise Garel a66c3000c2 Added Offensive counter-air tasks 2025-10-19 17:34:16 +02:00
Ambroise Garel 2d96f23d2f Temporarily disabled Helicopter missions 2025-10-19 17:34:06 +02:00
Ambroise Garel 392f06d4e2 Commented out Library.tasks.helicopterDestroyInfantry 2025-10-19 17:33:49 +02:00
Ambroise Garel 32248a9b72 Added COLD WAR units 2025-10-19 17:33:37 +02:00
Ambroise Garel 5b2b17a8b0 Added placeholder NATO faction 2025-10-19 17:33:30 +02:00
Ambroise Garel 806421062b AWACS, bombers and transports kills on the ramp now don't suffer a -50% score penalty 2025-09-20 17:42:39 +02:00
Ambroise Garel 591ec6e10a "On landing" actions no longer triggered when player lands away from an airfield 2025-09-18 22:03:11 +02:00
Ambroise Garel 92a00160ef Increased LAND objective minimum distance from 200m to 500m 2025-09-18 21:59:06 +02:00
Ambroise Garel 085172f033 Added (disabled) helicopterPickUpInfantry task 2025-09-18 21:49:20 +02:00
Ambroise Garel 526651c6da Fixed typo in batch file menu 2025-09-18 21:34:19 +02:00
Ambroise Garel b343d700a0 LAND objectives can now only be completed by helicopters, and force removal of target objective group 2025-09-18 17:03:19 +02:00
Ambroise Garel da20586d51 Added missing net.allow_unsafe_api value in autoexec.cfg that prevented some advanced scripts from working 2025-09-18 16:58:21 +02:00
Ambroise Garel 69ea6b6d19 Added DCSEx.enums.taskFlag.FRIENDLY_TARGET 2025-09-18 15:02:34 +02:00
Ambroise Garel 5f5b940ef7 Minor fixes to helicopter-specific tasking 2025-09-18 14:50:37 +02:00
Ambroise Garel 014495246d Updated completed features in README 2025-09-18 14:44:03 +02:00
Ambroise Garel 3484e2544c AWACS aircraft now generated on mission start 2025-09-17 16:30:25 +02:00
Ambroise Garel 185685b706 Added helicopterDestroyInfantry task 2025-09-17 16:19:48 +02:00
Ambroise Garel 6b52970520 Added DCSEx.enums.taskFamily.HELICOPTER 2025-09-17 16:19:37 +02:00
Ambroise Garel 1c6b06a1a6 Replaced "ocaBomberStrike" with "ocaStrategicAircraftStrike" (can be bomber OR transport) 2025-09-17 16:08:34 +02:00
Ambroise Garel 525484385a Fixed bug with parking spot generation 2025-09-17 16:07:59 +02:00
Ambroise Garel 6ce6a8d2a1 Now checks DCSEx.enums.taskEvent.DAMAGE when checking objective completion 2025-09-17 15:32:05 +02:00
Ambroise Garel 7bfe0813d8 Parked aircraft OCA tasks now use DCSEx.enums.taskEvent.DAMAGE 2025-09-17 15:31:33 +02:00
Ambroise Garel 12e20a32ce Added DAMAGE to DCSEx.enums.taskEvent enum 2025-09-17 15:31:08 +02:00
Ambroise Garel 13d341c4a8 Check that objectives are spawned on unique parking spots 2025-09-17 14:24:11 +02:00
Ambroise Garel 0bc9780dd4 Temporarily disabled "ocaFighterStrike" task 2025-09-16 22:53:49 +02:00
Ambroise Garel aa544e6c0c Parked aircraft now explode when hit to make them easier to kill 2025-09-16 22:42:43 +02:00
Ambroise Garel c80d036feb Parked aircraft are now invisible to AI 2025-09-16 22:42:17 +02:00
Ambroise Garel c557c3d74d Added new mission types to manual and README 2025-09-16 22:19:35 +02:00
Ambroise Garel 1892d97d15 Improved wording 2025-09-16 22:19:20 +02:00
Ambroise Garel db5ee882c4 Added check so OCA missions can't be launched in zones without enemy airfields 2025-09-16 22:16:10 +02:00
Ambroise Garel 83ddfe7598 Fixed invalid parameter name in DCSEx.zones.getAirbases 2025-09-16 22:15:53 +02:00
Ambroise Garel d0355af8e3 Added "allowShips" parameters to DCSEx.zones.getAirbases 2025-09-16 22:08:04 +02:00
Ambroise Garel 211cb15015 Updated score value for helicopters 2025-09-16 19:55:57 +02:00
Ambroise Garel 1905b4061a Wingmen now use proper A/A loadout for "helo hunt" tasking 2025-09-16 19:54:39 +02:00
Ambroise Garel b93bcb4734 Updated bug fixes 2025-09-16 19:51:21 +02:00
Ambroise Garel 61e017fbe8 AWACS can now detect helicopters 2025-09-16 19:50:56 +02:00
Ambroise Garel 655b5bc6aa Added "Helicopter hunt" mission objective 2025-09-16 19:49:35 +02:00
Ambroise Garel d13d94f1bb Added "helo hunt" tasks for attack and transport helicopters 2025-09-16 19:48:58 +02:00
Ambroise Garel 0be508c42c Updated wording 2025-09-16 19:48:25 +02:00
Ambroise Garel b9ce5ef340 Added "OCA bomber strike" task 2025-09-16 19:48:01 +02:00
Ambroise Garel 3453bccf85 Added "ocaBomberStrike" task 2025-09-16 19:47:38 +02:00
Ambroise Garel b392c55828 Added function DCSEx.zones.getAirbases(zoneTable, coalition) 2025-09-16 19:05:10 +02:00
Ambroise Garel ff15793f06 Updated wording 2025-09-16 18:59:08 +02:00
Ambroise Garel e7f9ba4f92 Added getEnemyAirbaseInZone(zone) and parked aircraft target generation 2025-09-16 18:44:09 +02:00
Ambroise Garel 98baf8a3c0 Added support for parked aircraft generation 2025-09-16 18:43:50 +02:00
Ambroise Garel 4a94770322 Added ocaFighterStrike task 2025-09-16 18:43:40 +02:00
Ambroise Garel 885b14315f Temporary disabled "ocaAirbase" task 2025-09-16 18:43:14 +02:00
Ambroise Garel 4f1ad38eeb Added "airbase strike" OCA mission objective 2025-09-16 12:16:58 +02:00
Ambroise Garel 404095967d Added OCA to DCSEx.enums.taskFamily 2025-09-16 12:07:13 +02:00
Ambroise Garel 4ebbf398d2 Added AIRBASE_TARGET and PARKED_AIRCRAFT_TARGET to DCSEx.enums.taskFlag 2025-09-16 12:00:35 +02:00
Ambroise Garel babc9a183e Added out-of-bounds check on names array 2025-09-15 16:21:26 +02:00
Ambroise Garel 0b792e4b25 Fixed errors in README 2025-09-15 12:13:28 +02:00
Ambroise Garel 2eee4320d3 Updated README 2025-09-14 15:39:25 +02:00
Ambroise Garel 2d0503e44e Updated README 2025-09-14 15:39:03 +02:00
Ambroise Garel e0a2612572 Updated manual and README for version 0.3.250914 2025-09-14 15:36:07 +02:00
Ambroise Garel e357040865 Removed unused zones from the autoexec.cfg 2025-09-14 15:28:32 +02:00
Ambroise Garel 67d71334f9 Support for "Cold War Germany" theater 2025-09-14 14:55:32 +02:00
Ambroise Garel ffcbbd0402 Merge branch 'germany-theater' 2025-09-14 14:44:04 +02:00
Ambroise Garel 638985b86c Fixed wrong filename in "enemy infantry killed" messages 2025-09-14 14:18:40 +02:00
Ambroise Garel b8b5611e32 Added voiceovers for "vector to airbase" and "weather report" calls 2025-09-14 14:17:56 +02:00
Ambroise Garel 3af259f048 Added "Give weather report" radio command 2025-09-14 12:12:25 +02:00
Ambroise Garel 104fee86e9 Added DCSEx.math.getLength3D(vec3) 2025-09-14 12:06:55 +02:00
Ambroise Garel d5577ac551 Removed unused string key 2025-09-14 11:54:23 +02:00
Ambroise Garel 5f220884bd Added "Vector me to nearest airbase" ATC command 2025-09-14 11:37:01 +02:00
Ambroise Garel e522d110bb Added atcRequireNearestAirbaseNone 2025-09-14 11:09:04 +02:00
Ambroise Garel 64bde651c3 Enemy CAP respawn rate now decreases the more enemy planes are shot 2025-09-11 12:28:10 +02:00
Ambroise Garel a9edd4a819 Target coordinates radio messages now displayed for a longer time 2025-09-11 11:29:07 +02:00
Ambroise Garel b140238aa0 Added displayTimeMultiplier parameter 2025-09-11 11:28:50 +02:00
Ambroise Garel 814fbccb00 Mission now autostarts when all players have taken off 2025-09-11 11:12:57 +02:00
Ambroise Garel 81b0be5645 Changed wording 2025-09-11 10:59:02 +02:00
Ambroise Garel fdb5090e40 Added function DCSEx.world.getPlayersOnGround(side) 2025-09-11 10:58:51 +02:00
Ambroise Garel 99133326b9 Added units from Currenthill unit pack 2025-08-26 10:55:47 +02:00
Ambroise Garel 994c4d9193 Wingmen now removed when player enters a new unit in SP 2025-08-11 17:52:08 +02:00
Ambroise Garel f1a87bcfa8 Restored DCSEx.dcs.doNothing() 2025-08-07 19:07:41 +02:00
Ambroise Garel 9d64113241 Added "press key to respawn" message on single-player death 2025-08-05 17:48:39 +02:00
Ambroise Garel adee5411e1 Updated README 2025-08-05 17:38:35 +02:00
Ambroise Garel 4232ee723c Now uses "Client" instead of "Player" slots for single-player 2025-08-05 17:38:14 +02:00
Ambroise Garel c3ecc403e2 Removed references to world.getPlayer() 2025-08-05 17:22:16 +02:00
Ambroise Garel dca67aa13c Moved TUM.administrativeSettings to its own table 2025-08-05 17:10:30 +02:00
Ambroise Garel 6658dbecf9 Moved TUM.logger to its own file 2025-08-05 17:04:24 +02:00
Ambroise Garel 52ad4156a4 Merge pull request #12 from VEAF/davidp57/administrative_settings
Introduced administrative settings.
2025-08-05 16:57:28 +02:00
Ambroise Garel b4701a98e2 Merge pull request #14 from VEAF/davidp57/bug-callsigns
Updated the currentCallsigns table, because there can be 20 max callsign numbers for some callsign types.
2025-08-03 19:42:06 +02:00
David Pierron 24a73b73c7 Updated the currentCallsigns table, because there can be 20 max callsign numbers for some callsign types. 2025-08-03 19:36:21 +02:00
Ambroise Garel b94cdaa0ef Merge pull request #13 from VEAF/davidp57/logging-system
Enhanced the logging system while maintaining backward compatibility (through the use of the `TUM.log` function).
2025-08-03 19:26:15 +02:00
Ambroise Garel 6b765e7c80 Merge pull request #8 from VEAF/davidp57/issue1
VEAF MCT - error when adding AI planes to the mission
2025-08-03 19:22:51 +02:00
David Pierron de3e3df840 Enhanced the logging system while maintaining backward compatibility (through the use of the TUM.log function).
The new system defines specific functions:
- TUM.Logger.trace
- TUM.Logger.debug
- TUM.Logger.info
- TUM.Logger.warn
- TUM.Logger.error

These function can be passed any number of arguments additionnally to the message, and they'll safely format these arguments to be passed to `string.format`.

Example of use:
`TUM.Logger.trace("function  DCSEx.world.setUnitLifePercent(unitID=%s, life=%s)", unitID, life)`

Parameters are formatted based on their type (tables are fully printed for example),

This is a reduced port of the VEAF logging system, which has been used for years.
2025-08-01 23:03:27 +02:00
David Pierron c5743c993a completed the specific radio menu feature (was missing some bits) 2025-08-01 19:39:54 +02:00
David Pierron 54ff069711 Introduced administrative settings.
They are defined in `TUM.administrativeSettings`.
They have a default value in `TUM.administrativeSettingsDefaultValues`.
They can be overloaded either:
-  by script (with a call like `TUM.administrativeSettings.setValue(TUM.administrativeSettings.USE_SPECIFIC_RADIOMENU, true)`
- by a parameter named after the setting in a trigger zone called `TUM_Administrative_Settings`

Settings that are already implemented:
- USE_SPECIFIC_RADIOMENU: use a specific radio menu for the mission commands, or use the main one?
- INITIALIZE_AUTOMATICALLY: automatically initialize the mission when the script is loaded. If false, you must call TUM.initialize() manually.
- IGNORE_ZONES_STARTINGWITH: if set, ignore all zones starting with this string. This is useful to avoid conflicts with other scripts that use the same zone names.
- ONLY_ZONES_STARTINGWITH: if set, only adds zones starting with this string. This is useful to avoid conflicts with other scripts that use the same zone names.
2025-08-01 18:47:47 +02:00
Ambroise Garel e840bc3b0d Added DCSEx.world.getFirstPlayer function 2025-08-01 16:59:46 +02:00
Ambroise Garel 25ba1ccd2e Cleaned up and added comments 2025-08-01 16:57:15 +02:00
David Pierron 3e1bd1e52e VEAF MCT - error when adding AI planes to the mission
Fixes VEAF/the-universal-mission-for-dcs-world#1
2025-07-31 10:18:48 +02:00
Ambroise Garel b45caaa4c4 Added Germany theater 2025-07-22 15:04:11 +02:00
75 changed files with 1922 additions and 445 deletions
-6
View File
@@ -11,15 +11,9 @@
if not net then net = {} end
net.allow_unsafe_api = { -- this defines the secure zones where net.dostring_in() can be called from
"userhooks",
"scripting",
"gui",
}
net.allow_dostring_in = { -- and this defines the zones that should be addressed from net.dostring_in()
"mission",
"scripting",
"gui",
"export",
"config",
}
+6
View File
@@ -0,0 +1,6 @@
-- TODO
-- [DCSEx.enums.unitFamily.GROUND_APC] = { "CHAP_FV107" },
-- CHAP_IRISTSLM_CP, CHAP_IRISTSLM_LN, CHAP_IRISTSLM_STR
+51
View File
@@ -0,0 +1,51 @@
-- do
-- Library.factions.tables["NATO"] = {}
-- Library.factions.tables["NATO"].theaters = {}
-- Library.factions.tables["NATO"].timePeriods = {}
-- Library.factions.tables["NATO"].units = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.WORLD_WAR_2] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.KOREA_WAR] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.VIETNAM_WAR] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.COLD_WAR] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.MODERN] = {
-- [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = { "Gepard", "Vulcan" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = { "Soldier stinger" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*Patriot" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*HAWK", "*NASAMS" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "rapier_fsa", "Roland ADS" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = { "M6 Linebacker", "M48 Chaparral", "M1097 Avenger" },
-- [DCSEx.enums.unitFamily.GROUND_APC] = { "AAV7", "Cobra", "LAV-25", "M-2 Bradley", "M-113", "M1045 HMMWV TOW", "M1126 Stryker ICV", "M1128 Stryker MGS", "Marder", "MCV-80", "MLRS FDDM", "TPZ", "CHAP_M1130", "CHAP_MATV" },
-- [DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "M-109", "MLRS", "CHAP_M142_ATACMS_M48", "CHAP_M142_GMLRS_M31" },
-- [DCSEx.enums.unitFamily.GROUND_INFANTRY] = { "Soldier M4 GRG", "Soldier M4", "Soldier M249", "Soldier RPG" },
-- [DCSEx.enums.unitFamily.GROUND_MBT] = { "Challenger2", "Leclerc", "Leopard-2", "Leopard1A3", "M-1 Abrams", "Merkava_Mk4" },
-- [DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B" },
-- [DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Land_Rover_101_FC", "Land_Rover_109_S3", "M 818", "CHAP_M1083" },
-- [DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "AH-1W", "AH-64D", "OH-58D", "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" },
-- [DCSEx.enums.unitFamily.HELICOPTER_TRANSPORT] = { "CH-47D", "CH-53E", "SH-60B", "UH-60A" },
-- [DCSEx.enums.unitFamily.PLANE_ATTACK] = { "A-10C_2" },
-- [DCSEx.enums.unitFamily.PLANE_AWACS] = { "E-2C", "E-3A" },
-- [DCSEx.enums.unitFamily.PLANE_BOMBER] = { "B-1B Lancer", "B-52H" },
-- [DCSEx.enums.unitFamily.PLANE_FIGHTER] = { "F-16C_50", "FA-18C_hornet" },
-- [DCSEx.enums.unitFamily.PLANE_TANKER] = { "KC-135", "KC135MPRS" },
-- [DCSEx.enums.unitFamily.PLANE_TRANSPORT] = { "C-17A", "C-130" },
-- [DCSEx.enums.unitFamily.PLANE_UAV] = { "RQ-1A Predator" },
-- [DCSEx.enums.unitFamily.SHIP_CARGO] = { "Dry-cargo ship-1", "Dry-cargo ship-2", "ELNYA", "Ship_Tilde_Supply" },
-- [DCSEx.enums.unitFamily.SHIP_CARRIER] = { "CVN_71", "CVN_72", "CVN_73", "CVN_75", "hms_invincible", "LHA_Tarawa", "Stennis" },
-- [DCSEx.enums.unitFamily.SHIP_CRUISER] = { "TICONDEROG" },
-- [DCSEx.enums.unitFamily.SHIP_FRIGATE] = { "PERRY", "USS_Arleigh_Burke_IIa" },
-- [DCSEx.enums.unitFamily.SHIP_LIGHT] = { "speedboat" },
-- [DCSEx.enums.unitFamily.SHIP_MISSILE_BOAT] = { "CastleClass_01", "La_Combattante_II" },
-- [DCSEx.enums.unitFamily.SHIP_SUBMARINE] = { "santafe" },
-- -- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Bunker", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Sandbox", "Workshop A" },
-- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Workshop A" },
-- }
-- end
+5 -5
View File
@@ -24,14 +24,14 @@ do
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*SA-2", "*SA-10" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*SA-10" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*SA-3", "*SA-6", "*SA-11" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "2S6 Tunguska", "Osa 9A33 ln", "Tor 9A331" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "2S6 Tunguska", "Osa 9A33 ln", "Tor 9A331", "CHAP_PantsirS1", "CHAP_TorM2" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = { "Strela-1 9P31", "Strela-10M3" },
[DCSEx.enums.unitFamily.GROUND_APC] = { "BMD-1", "BMP-1", "BMP-2", "BMP-3", "Boman", "BRDM-2", "BTR_D", "BTR-80", "BTR-82A", "Grad_FDDM", "MTLB" },
[DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "Grad-URAL", "SAU 2-C9", "SAU Akatsia", "SAU Gvozdika", "SAU Msta", "Smerch", "SpGH_Dana", "Uragan_BM-27" },
[DCSEx.enums.unitFamily.GROUND_APC] = { "BMD-1", "BMP-1", "BMP-2", "BMP-3", "Boman", "BRDM-2", "BTR_D", "BTR-80", "BTR-82A", "Grad_FDDM", "MTLB", "CHAP_BMPT" },
[DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "Grad-URAL", "SAU 2-C9", "SAU Akatsia", "SAU Gvozdika", "SAU Msta", "Smerch", "SpGH_Dana", "Uragan_BM-27", "CHAP_TOS1A" },
[DCSEx.enums.unitFamily.GROUND_INFANTRY] = { "Infantry AK", "Infantry AK ver2", "Infantry AK ver3", "Paratrooper AKS-74", "Paratrooper RPG-16", "Soldier AK" },
[DCSEx.enums.unitFamily.GROUND_MBT] = { "T-55", "T-72B", "T-80UD", "T-90" },
[DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B" },
[DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B", "CHAP_9K720_HE" },
[DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Ural-375", "Ural-4320 APA-5D", "Ural-4320T" },
[DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "Ka-50", "Mi-24V", "Mi-28N" },
@@ -52,7 +52,7 @@ do
[DCSEx.enums.unitFamily.SHIP_CRUISER] = { "MOSCOW", "PIOTR" },
[DCSEx.enums.unitFamily.SHIP_FRIGATE] = { "NEUSTRASH", "REZKY" },
[DCSEx.enums.unitFamily.SHIP_LIGHT] = { "speedboat" },
[DCSEx.enums.unitFamily.SHIP_MISSILE_BOAT] = { "ALBATROS", "BDK-775", "MOLNIYA" },
[DCSEx.enums.unitFamily.SHIP_MISSILE_BOAT] = { "ALBATROS", "BDK-775", "MOLNIYA", "CHAP_Project22160_TorM2KM" },
[DCSEx.enums.unitFamily.SHIP_SUBMARINE] = { "IMPROVED_KILO", "KILO", "SOM" },
-- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Bunker", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Sandbox", "Workshop A" },
+42 -4
View File
@@ -10,7 +10,45 @@ do
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.WORLD_WAR_2] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.KOREA_WAR] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.VIETNAM_WAR] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.COLD_WAR] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.COLD_WAR] = {
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" },
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = { "Gepard", "Vulcan" },
[DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = { "Soldier stinger" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*Patriot" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*HAWK", "*NASAMS" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "rapier_fsa", "Roland ADS" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = { "M6 Linebacker", "M48 Chaparral", "M1097 Avenger" },
[DCSEx.enums.unitFamily.GROUND_APC] = { "AAV7", "Cobra", "LAV-25", "M-2 Bradley", "M-113", "M1045 HMMWV TOW", "M1126 Stryker ICV", "M1128 Stryker MGS", "Marder", "MCV-80", "MLRS FDDM", "TPZ", "CHAP_M1130", "CHAP_MATV" },
[DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "M-109", "MLRS", "CHAP_M142_ATACMS_M48", "CHAP_M142_GMLRS_M31" },
[DCSEx.enums.unitFamily.GROUND_INFANTRY] = { "Soldier M4 GRG", "Soldier M4", "Soldier M249", "Soldier RPG" },
[DCSEx.enums.unitFamily.GROUND_MBT] = { "Challenger2", "Leclerc", "Leopard-2", "Leopard1A3", "M-1 Abrams", "Merkava_Mk4" },
[DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B" },
[DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Land_Rover_101_FC", "Land_Rover_109_S3", "M 818", "CHAP_M1083" },
[DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "AH-1W", "AH-64D", "OH-58D", "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" },
[DCSEx.enums.unitFamily.HELICOPTER_TRANSPORT] = { "CH-47D", "CH-53E", "SH-60B", "UH-60A" },
[DCSEx.enums.unitFamily.PLANE_ATTACK] = { "A-10C_2" },
[DCSEx.enums.unitFamily.PLANE_AWACS] = { "E-2C", "E-3A" },
[DCSEx.enums.unitFamily.PLANE_BOMBER] = { "B-1B Lancer", "B-52H" },
[DCSEx.enums.unitFamily.PLANE_FIGHTER] = { "F-16C_50", "FA-18C_hornet" },
[DCSEx.enums.unitFamily.PLANE_TANKER] = { "KC-135", "KC135MPRS" },
[DCSEx.enums.unitFamily.PLANE_TRANSPORT] = { "C-17A", "C-130" },
[DCSEx.enums.unitFamily.PLANE_UAV] = { "RQ-1A Predator" },
[DCSEx.enums.unitFamily.SHIP_CARGO] = { "Dry-cargo ship-1", "Dry-cargo ship-2", "ELNYA", "Ship_Tilde_Supply" },
[DCSEx.enums.unitFamily.SHIP_CARRIER] = { "CVN_71", "CVN_72", "CVN_73", "CVN_75", "hms_invincible", "LHA_Tarawa", "Stennis" },
[DCSEx.enums.unitFamily.SHIP_CRUISER] = { "TICONDEROG" },
[DCSEx.enums.unitFamily.SHIP_FRIGATE] = { "PERRY", "USS_Arleigh_Burke_IIa" },
[DCSEx.enums.unitFamily.SHIP_LIGHT] = { "speedboat" },
[DCSEx.enums.unitFamily.SHIP_MISSILE_BOAT] = { "CastleClass_01", "La_Combattante_II" },
[DCSEx.enums.unitFamily.SHIP_SUBMARINE] = { "santafe" },
-- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Bunker", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Sandbox", "Workshop A" },
[DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Workshop A" },
}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.MODERN] = {
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" },
@@ -21,12 +59,12 @@ do
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "rapier_fsa", "Roland ADS" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = { "M6 Linebacker", "M48 Chaparral", "M1097 Avenger" },
[DCSEx.enums.unitFamily.GROUND_APC] = { "AAV7", "Cobra", "LAV-25", "M-2 Bradley", "M-113", "M1045 HMMWV TOW", "M1126 Stryker ICV", "M1128 Stryker MGS", "Marder", "MCV-80", "MLRS FDDM", "TPZ" },
[DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "M-109", "MLRS" },
[DCSEx.enums.unitFamily.GROUND_APC] = { "AAV7", "Cobra", "LAV-25", "M-2 Bradley", "M-113", "M1045 HMMWV TOW", "M1126 Stryker ICV", "M1128 Stryker MGS", "Marder", "MCV-80", "MLRS FDDM", "TPZ", "CHAP_M1130", "CHAP_MATV" },
[DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "M-109", "MLRS", "CHAP_M142_ATACMS_M48", "CHAP_M142_GMLRS_M31" },
[DCSEx.enums.unitFamily.GROUND_INFANTRY] = { "Soldier M4 GRG", "Soldier M4", "Soldier M249", "Soldier RPG" },
[DCSEx.enums.unitFamily.GROUND_MBT] = { "Challenger2", "Leclerc", "Leopard-2", "Leopard1A3", "M-1 Abrams", "Merkava_Mk4" },
[DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B" },
[DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Land_Rover_101_FC", "Land_Rover_109_S3", "M 818" },
[DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Land_Rover_101_FC", "Land_Rover_109_S3", "M 818", "CHAP_M1083" },
[DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "AH-1W", "AH-64D", "OH-58D", "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" },
[DCSEx.enums.unitFamily.HELICOPTER_TRANSPORT] = { "CH-47D", "CH-53E", "SH-60B", "UH-60A" },
@@ -0,0 +1,22 @@
-- Library.tasks.helicopterDestroyInfantry = {
-- taskFamily = DCSEx.enums.taskFamily.HELICOPTER,
-- description =
-- {
-- briefing = {
-- "",
-- },
-- short = "Neutralize enemy infantry",
-- },
-- conditions = {
-- difficultyMinimum = 0,
-- eras = {},
-- },
-- completionEvent = DCSEx.enums.taskEvent.DESTROY,
-- flags = { },
-- minimumDistance = DCSEx.converter.nmToMeters(5.0),
-- safeRadius = 100,
-- surfaceType = nil,
-- targetCount = { 6, 8 },
-- targetFamilies = { DCSEx.enums.unitFamily.GROUND_INFANTRY },
-- waypointInaccuracy = DCSEx.converter.nmToMeters(1.5)
-- }
@@ -0,0 +1,22 @@
-- Library.tasks.helicopterPickUpInfantry = {
-- taskFamily = DCSEx.enums.taskFamily.HELICOPTER,
-- description =
-- {
-- briefing = {
-- "",
-- },
-- short = "Land and pick up friendly infantry",
-- },
-- conditions = {
-- difficultyMinimum = 0,
-- eras = {},
-- },
-- completionEvent = DCSEx.enums.taskEvent.LAND,
-- flags = { DCSEx.enums.taskFlag.FRIENDLY_TARGET },
-- minimumDistance = DCSEx.converter.nmToMeters(5.0),
-- safeRadius = 100,
-- surfaceType = nil,
-- targetCount = { 6, 8 },
-- targetFamilies = { DCSEx.enums.unitFamily.GROUND_INFANTRY },
-- waypointInaccuracy = DCSEx.converter.nmToMeters(1.5)
-- }
+25
View File
@@ -0,0 +1,25 @@
Library.tasks.heloHuntAttack = {
taskFamily = DCSEx.enums.taskFamily.HELO_HUNT,
description =
{
briefing = {
"Locate and neutralize all enemy rotary-wing assets in the area.",
"Enemy attack helicopters are staging nearby, you are to eliminate them before they launch their attack.",
"Intel confirms a group of hostile gunships in the area. You must render them combat-ineffective.",
"Engage and destroy rotary assets nearby, crippling enemy air support.",
},
short = "Destroy enemy attack helicopters",
},
conditions = {
difficultyMinimum = 0,
eras = {},
},
completionEvent = DCSEx.enums.taskEvent.DESTROY,
flags = { },
minimumDistance = DCSEx.converter.nmToMeters(10.0),
safeRadius = 100,
surfaceType = nil,
targetCount = { 2, 3 },
targetFamilies = { DCSEx.enums.unitFamily.HELICOPTER_ATTACK },
waypointInaccuracy = DCSEx.converter.nmToMeters(6.0)
}
+22
View File
@@ -0,0 +1,22 @@
Library.tasks.heloHuntTransport = {
taskFamily = DCSEx.enums.taskFamily.HELO_HUNT,
description =
{
briefing = {
"",
},
short = "Destroy enemy transport helicopters",
},
conditions = {
difficultyMinimum = 0,
eras = {},
},
completionEvent = DCSEx.enums.taskEvent.DESTROY,
flags = { },
minimumDistance = DCSEx.converter.nmToMeters(10.0),
safeRadius = 100,
surfaceType = nil,
targetCount = { 2, 3 },
targetFamilies = { DCSEx.enums.unitFamily.HELICOPTER_TRANSPORT },
waypointInaccuracy = DCSEx.converter.nmToMeters(6.0)
}
+26
View File
@@ -0,0 +1,26 @@
-- Library.tasks.ocaAirbase = {
-- taskFamily = DCSEx.enums.taskFamily.OCA,
-- description =
-- {
-- briefing = {
-- "Neutralizing this enemy airbase will eliminate their ability to conduct sustained air sorties and restore local air superiority.",
-- "Taking out this airbase will disrupt their logistics and sortie tempo, buying time for our ground forces to consolidate.",
-- "This airbase is the hub of enemy reconnaissance and close air support—removing it reduces battlefield intelligence and strike pressure.",
-- "Outlasting enemy air campaign requires degrading that facility's operational capacity to prevent rotational sorties.",
-- "Disabling this node of their air network constrains their command-and-control reach and limits coordinated strikes."
-- },
-- short = "Destroy enemy airbase",
-- },
-- conditions = {
-- difficultyMinimum = 0,
-- eras = {},
-- },
-- completionEvent = DCSEx.enums.taskEvent.DESTROY,
-- flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.AIRBASE_TARGET },
-- minimumDistance = 0.0,
-- safeRadius = 0,
-- surfaceType = land.SurfaceType.LAND,
-- targetCount = { 1, 1 },
-- targetFamilies = { DCSEx.enums.unitFamily.STATIC_SCENERY },
-- waypointInaccuracy = 0.0
-- }
+25
View File
@@ -0,0 +1,25 @@
-- Library.tasks.ocaFighterStrike = {
-- taskFamily = DCSEx.enums.taskFamily.OCA,
-- description =
-- {
-- briefing = {
-- "Destroying enemy fighters on the ramp will prevent immediate sortie generation and blunt their ability to provide CAS against our advancing forces.",
-- "A ramp strike against enemy aircraft will pre-empt an imminent scramble warning we have intel for, preventing a coordinated mass launch.",
-- "Removing enemy fighters on the ramp will degrade enemy air superiority over the battlespace and protects our medevac and resupply corridors.",
-- "Priority is to eliminate immediate airborne threats on the ramp to safeguard coalition force freedom of maneuver.",
-- },
-- short = "Destroy enemy fighter on the ramp",
-- },
-- conditions = {
-- difficultyMinimum = 0,
-- eras = {},
-- },
-- completionEvent = DCSEx.enums.taskEvent.DAMAGE,
-- flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET },
-- minimumDistance = 0.0,
-- safeRadius = 0,
-- surfaceType = land.SurfaceType.LAND,
-- targetCount = { 1, 1 },
-- targetFamilies = { DCSEx.enums.unitFamily.PLANE_FIGHTER },
-- waypointInaccuracy = 0.0
-- }
@@ -0,0 +1,22 @@
Library.tasks.ocaStrategicAircraftStrike = {
taskFamily = DCSEx.enums.taskFamily.OCA,
description =
{
briefing = {
""
},
short = "Destroy enemy aircraft on the ramp",
},
conditions = {
difficultyMinimum = 0,
eras = {},
},
completionEvent = DCSEx.enums.taskEvent.DAMAGE,
flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET },
minimumDistance = 0.0,
safeRadius = 0,
surfaceType = land.SurfaceType.LAND,
targetCount = { 1, 1 },
targetFamilies = { DCSEx.enums.unitFamily.PLANE_BOMBER, DCSEx.enums.unitFamily.PLANE_TRANSPORT },
waypointInaccuracy = 0.0
}
+1 -1
View File
@@ -8,7 +8,7 @@ IF %ERRORLEVEL% NEQ 0 goto ERROR-NO-PHP
@REM CREATE SCENARIOS
@REM -------------------------------------------
set buildConfig=p
echo THE ULTIMATE MISSION BUILDER SCRIPT:
echo THE UNIVERSAL MISSION BUILDER SCRIPT:
echo - Build [P]ersianGulf debug theater only (default)
echo - Build all [D]ebug theaters
echo - Build all [R]elease theaters
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -95,7 +95,7 @@ mission =
},
},
["pictureFileNameN"] = {},
["descriptionNeutralsTask"] = "DictKey_descriptionNeutralsTask_4",
["descriptionNeutralsTask"] = "",
["pictureFileNameServer"] = {},
["weather"] =
{
+1 -1
View File
@@ -86,7 +86,7 @@
["alt"] = 13,
["alt_type"] = "BARO",
["livery_id"] = "default",
["skill"] = "Player",
["skill"] = "Client",
["speed"] = 138.88888888889,
["AddPropAircraft"] =
{
+1 -1
View File
@@ -65,7 +65,7 @@
["alt"] = 22,
["alt_type"] = "BARO",
["livery_id"] = "default",
["skill"] = "Player",
["skill"] = "Client",
["speed"] = 138.88888888889,
["type"] = "Su-25T",
["unitId"] = 1,
+45 -25
View File
@@ -1,6 +1,6 @@
# The Universal Mission for DCS World
**Current version: open beta 0.2.250729** (see the "version history" section at the end of this file for a list of the latest changes)
**Current version: open beta 0.3.250914** (see the "version history" section at the end of this file for a list of the latest changes)
**This is a BETA version, there may be bugs and there WILL be unbalanced stuff.**
@@ -11,7 +11,7 @@ The Universal Mission for DCS World is a fully dynamic single-player/PvE mission
- Can generate any kind of mission: ground attack, interception, strike, airbase attack, CAS, CAP, and more
- Completely dynamic, no two missions are ever the same
- Entirely self-contained inside a .miz file, no need for any external program
- More than 300 voiced radio messages for immersive and realistic coms
- More than 325 voiced radio messages for immersive and realistic coms
- Supports both single-player and small-scale PvE on closed servers
- Persistent single player career mode, with awards and promotions. Dying won't reset your progress, but you have to come back to base alive for your kills and completed objectives to be saved to your profile, so watch out for SAMs on your way home
- All new AI wingman system, smarter and more immersive than DCS's original wingmen
@@ -26,14 +26,13 @@ The Universal Mission for DCS World is a fully dynamic single-player/PvE mission
**Please read the "planned development" section below for more information.**
- The current version supports only modern (post-Cold War) units and Caucasus, Kola, Marianas, Persian Gulf and Syria theaters
- The current version supports only modern (post-Cold War) units and Caucasus, Germany, Kola, Marianas, Persian Gulf and Syria theaters
- Not all mission types are supported yet
- Career progress may be lost because of future updates, don't get too attached to it
<h4>Known bugs in latest release</h4>
- AWACS datalink info is now displayed on SA pages
- Some player callsigns may cause an error on mission start, stick to the "basic" DCS callsigns (Enfield, Springfield, Uzi, Colt, Dodge, Ford, Chevy and Pontiac) to avoid this problem
- AWACS datalink info not displayed properly on SA pages
## How to use/play The Universal Mission?
@@ -43,7 +42,6 @@ The Universal Mission for DCS World is a fully dynamic single-player/PvE mission
- Download the latest release from this GitHub page.
- Copy the provided autoexec.cfg file to your **[Saved Games]\DCS\Config directory**
- Please note: as of DCS 2.9.18.12899, it seems the autoexec.cfg file [is no longer needed](https://www.digitalcombatsimulator.com/en/news/changelog/release/2.9.18.12899/) but I advise you to copy it anyway, ED might change its mind again.
- Copy the .miz files for your theater(s) of choice to your **[Saved Games]\DCS\Missions directory**
- _**(Optional but strongly recommended)**_ Unsanitize the Lua IO module. You don't have to do this, but the persistent career system won't work if you don't. To do it, open the file **[DCS World installation directory]\Scripts\MissionScripting.lua** with a text editor and comment or remove the line "sanitizeModule('io')". Make sure you restart DCS World once you've modified the file.
- Please note: should you want to backup, delete or transfer it, career progress is saved in **[DCS World installation directory]\TheUniversalMission.sav**
@@ -72,31 +70,33 @@ Please also note that PvP is not supported at the moment and that the mission wi
## Planned development
<h3>Planned for next version (could be subject to change)</h3>
<h3>Planned for next version (can be subject to change)</h3>
- Additional content
- [ ] At least partial Germany map support
- AI improvements
- [ ] AI wingmen should engage tracking radars first when told to engage SAM sites, in order to disable the site ASAP
- [ ] More objectives types
- [ ] Helicopter-specific tasking (land and pick up units, suppress infantry...)
- [x] Helicopter hunt
- [x] Offensive counter-air
- Balance improvements
- [ ] Lowered enemy CAP respawn rate
- [x] Missions with higher cloud cover and wind speed now award more XP
- [ ] Tweaked XP requirements for medals/promotions
- Bug fixes
- [ ] AWACS datalinked contacts not showing on SA pages
- [x] Some player callsigns causing a script error at startup
- [x] AWACS datalinked now showing on SA pages
- [x] AWACS can now detect enemy helicopters
- [x] Added missing net.allow_unsafe_api value in autoexec.cfg that prevented some advanced scripts from working
- [x] "On landing" actions (medal/promotions check, AI wingmen removal, etc) no longer triggered when player lands away from an airfield (e.g. in a Harrier on a helicopter)
- Extras
- [x] First draft of the PDF manual
- [ ] GitHub page
- Improvements
- [x] Better weather reports (cloud cover, rain, etc)
- Misc
- [x] AWACS unit now spawned when mission is loaded
- New features
- [ ] Additional commands in the "navigation" menu
- [ ] Vector to nearest airfield
- [ ] Weather report
- [ ] Use of "Client" slot instead of "Player" slot even in single-player missions, allowing the player to respawn on death/ejection instead of having to start the whole mission again
- [ ] Single-player mission autostart on player take off
- [ ] Administrative settings menu
- [ ] Friendly air defenses
- Quality of life/minor tweaks
- [ ] AI wingmen "Two was shot down!" call when witnessing another wingman killed
- [ ] AI wingmen "Winchester!" call when out of ammo
- [x] Increased AWACS aircraft spawn altitude
- [ ] Target coordinates radio message displayed for a longer time so players have the time to write them down or enter them in their flight computer
### High priority
@@ -104,12 +104,9 @@ Please also note that PvP is not supported at the moment and that the mission wi
- More "flavor" radio messages ("fence in" when player approaches the AO, etc) so the world will feel more alive
- Better balancing of the player career awards and promotions
- Better use of context for "ambient" radio messages (should only warn of a SAM launch if an AI pilot is there to witness it, etc)
- Friendly air defenses
- GitHub page
- Improved score multiplier taking into account various aspects of mission difficulty (weather, nighttime ops...)
- Laser designation of targets by JTAC
- New objectives: helicopter (drop/pickup units...), CAP, CAS, OCA (airbase attack)
- PDF manual
- New objectives: helicopter (drop/pickup units...), CAP, CAS
- Support for all missing DCS World theaters
- Support for more factions and five different time periods (World War 2, Korea war, Vietnam war, late Cold war, Modern)
@@ -165,6 +162,29 @@ The core script is quite simple and small, I probably won't need too much help w
## Version history
- **0.3.250914** (09/14/2025)
- **MAJOR CHANGE:**
- Use of "Client" slots instead of "Player" slots even in single-player missions, allowing the player to respawn on death/ejection instead of having to start the whole mission again
- "Player" slots should not be used anymore. Single-player missions must now use a single "Client" slot instead
- Additional content
- Added new units (Currenthill unit pack) from DCS 2.9.19.13478
- Support for "Cold War Germany" theater
- Balance improvements
- Enemy CAP respawn rate now decreases as more enemy planes are shot down
- Bug fixes
- Some player callsigns were causing a script error at startup
- Fixed wrong filename in "enemy infantry killed" messages
- Removed unused zones from the "autoexec.cfg" file
- Extras
- First draft of the PDF manual
- New features
- Additional commands in the "navigation" menu
- Vector to nearest airfield
- Weather report
- Mission now autostarts (if it wasn't started yet) when all players have taken off
- Quality of life/minor tweaks
- Increased AWACS aircraft spawn altitude
- Target coordinates radio message displayed for a longer time so players have time to write them down or enter them in their flight computer
- **0.2.250729** (07/29/2025)
- **MAJOR CHANGE:** Added all new wingman system
- Far for perfect but a lot better than DCS's default wingmen
+7
View File
@@ -1,6 +1,7 @@
-- ====================================================================================
-- DCSEX.DCS - FUNCTIONS HANDLING DCS WORLD'S GAME RULES AND TABLES
-- ====================================================================================
-- DCSEx.dcs.doNothing()
-- DCSEx.dcs.getBRAA(point, refPoint, showAltitude, metricSystem, casualFormat)
-- DCSEx.dcs.getCJTFForCoalition(coalitionID)
-- DCSEx.dcs.getCoalitionAsString(coalitionID)
@@ -23,6 +24,12 @@
DCSEx.dcs = { }
-------------------------------------
-- Does nothing. Used to create commands that do nothing in the F10 menu
-------------------------------------
function DCSEx.dcs.doNothing()
end
-------------------------------------
-- Gets a BRAA (bearing, range, altitude, aspect) string about a point
-- Format is "[bearing to unit] for [distance] at [altitude]"
+22 -19
View File
@@ -29,9 +29,10 @@ DCSEx.enums.lineType = {
-- Event to check to see if a task/objective is complete
-------------------------------------
DCSEx.enums.taskEvent = {
DESTROY = 1,
DESTROY_SCENERY = 2,
LAND = 3,
DAMAGE = 1,
DESTROY = 2,
DESTROY_SCENERY = 3,
LAND = 4,
}
-------------------------------------
@@ -39,26 +40,29 @@ DCSEx.enums.taskEvent = {
-------------------------------------
DCSEx.enums.taskFamily = {
ANTISHIP = 1,
-- CAP = 2, -- TODO
-- CAS = 3, -- TODO
GROUND_ATTACK = 2, -- 4
-- HELICOPTER = XXX, -- 5
-- HELO_HUNT = XXX, -- 6
INTERCEPTION = 3, -- 7
-- OCA = XXX, -- 8
SEAD = 4, --9
STRIKE = 5, -- 10
-- CAP = XXX,
-- CAS = XXX,
GROUND_ATTACK = 2,
-- HELICOPTER = 3,
HELO_HUNT = 3,
INTERCEPTION = 4,
OCA = 5,
SEAD = 6,
STRIKE = 7,
}
-------------------------------------
-- Special events for tasks
-------------------------------------
DCSEx.enums.taskFlag = {
ALLOW_JTAC = 1,
DESTROY_TRACK_RADARS_ONLY = 2,
MOVING = 3,
ON_ROADS = 4,
SCENERY_TARGET = 5
AIRBASE_TARGET = 1,
ALLOW_JTAC = 2,
DESTROY_TRACK_RADARS_ONLY = 3,
MOVING = 4,
ON_ROADS = 5,
PARKED_AIRCRAFT_TARGET = 6,
SCENERY_TARGET = 7,
FRIENDLY_TARGET = 8
}
-------------------------------------
@@ -69,8 +73,7 @@ DCSEx.enums.timePeriod = {
KOREA_WAR = 2,
VIETNAM_WAR = 3,
COLD_WAR = 4,
GULF_WAR = 5,
MODERN = 6
MODERN = 5
}
-------------------------------------
+11
View File
@@ -7,6 +7,7 @@
-- DCSEx.math.getBearing(point, refPoint, returnAsNESWstring)
-- DCSEx.math.getDistance2D(vec2a, vec2b)
-- DCSEx.math.getDistance3D(vec3a, vec3b)
-- DCSEx.math.getLength3D(vec3)
-- DCSEx.math.getRelativeHeading(point, refObject, format)
-- DCSEx.math.getVec2FromAngle(angle)
-- DCSEx.math.isPointInsideCircle(center, radius, vec2)
@@ -119,6 +120,16 @@ function DCSEx.math.getDistance3D(vec3a, vec3b)
return math.sqrt((vec3a.x - vec3b.x) ^ 2 + (vec3a.y - vec3b.y) ^ 2 + (vec3a.z - vec3b.z) ^ 2)
end
-------------------------------------
-- Returns the length of a 3D vector
-------------------------------------
-- @param vec3 A 3D vector
-- @return Length of the vector
-------------------------------------
function DCSEx.math.getLength3D(vec3)
return math.sqrt(vec3.x ^ 2 + vec3.y ^ 2 + vec3.z ^ 2)
end
-------------------------------------
-- Returns the relative heading difference between refObject and a given point
-------------------------------------
+2 -2
View File
@@ -143,7 +143,7 @@ do
local currentCallsigns = {}
for _,i in pairs(CALLSIGN_TYPE) do
currentCallsigns[i] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 }
currentCallsigns[i] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
end
local function getCallsignTypeByUnitType(unitType)
@@ -229,7 +229,7 @@ do
for _,g in ipairs(missionGroups) do
if g.units and g.units[1] then
local unit = g.units[1]
if unit.callsign and unit.callsign[1] and unit.callsign.name then
if unit.callsign and type(unit.callsign) == "table" and unit.callsign.name then
local callsignName = unit.callsign.name:sub(1, #unit.callsign.name - 2)
incrementCallsign(callsignName, unit.callsign[2])
end
+14
View File
@@ -374,6 +374,14 @@ do
setAircraftTaskOrbit(groupTable, options)
end
-- For parked aircraft
if options.airbaseID and options.parkingID then
groupTable.route.points[1].action = "From Parking Area"
groupTable.route.points[1].airdromeId = options.airbaseID
groupTable.route.points[1].type = "TakeOffParking"
groupTable.uncontrolled = true
end
if options.callsign then
groupCallsign = options.callsign
else
@@ -449,6 +457,12 @@ do
if aircraftDB.properties then
unitTable.AddPropAircraft = DCSEx.table.deepCopy(aircraftDB.properties)
end
-- For parked aircraft
if options.airbaseID and options.parkingID then
unitTable.parking = tostring(options.parkingID)
end
-- Setup datalink
local datalinkString = tostring(dataLinkID)
if #datalinkString == 3 then
+215 -103
View File
@@ -2,19 +2,27 @@
-- DCSEX.WORLD - FUNCTIONS RELATED TO THE GAME WORLD
-- ====================================================================================
-- DCSEx.world.collidesWithScenery(vec2, radius)
-- DCSEx.world.findSpawnPoint(vec2, minRadius, maxRadius, surfaceType, radiusWithoutScenery)
-- DCSEx.world.destroyGroupByID(groupID)
-- DCSEx.world.explodeUnit(unitID, amount)
-- DCSEx.world.getAllPlayers()
-- DCSEx.world.getAllSceneryBuildings(minHealth)
-- DCSEx.world.getAllUnits(unitCategory)
-- DCSEx.world.getAllUnits(coalitionID, unitCategory)
-- DCSEx.world.getClosestPointOnRoadsVec2(vec2)
-- DCSEx.world.getCoordinatesAsString(point)
-- DCSEx.world.getCoordinatesAsString(point, hideElevation)
-- DCSEx.world.getCurrentMarkerID()
-- DCSEx.world.getFirstPlayer(side)
-- DCSEx.world.getGroupByID(groupID)
-- DCSEx.world.getGroupCenter(group)
-- DCSEx.world.getMarkerByText(text, coalition)
-- DCSEx.world.getNextMarkerID()
-- DCSEx.world.getPlayersInAir(side)
-- DCSEx.world.getPlayersOnGround(side)
-- DCSEx.world.getSceneriesInZone(center, radius, minHealth)
-- DCSEx.world.getSpawnPoint(zone, surfaceType, safeRadius)
-- DCSEx.world.getStaticObjectByID(staticID)
-- DCSEx.world.getTerrainHeightDiff(coord, searchRadius)
-- DCSEx.world.getUnitByID(unitID)
-- DCSEx.world.getUnitsCenter(units)
-- DCSEx.world.isGroupAlive(g, unitsMustBeInAir)
-- DCSEx.world.setUnitLifePercent(unitID, life)
-- ====================================================================================
@@ -25,9 +33,16 @@ do
-- TODO: get max marker already in use from envMission
local nextMarkerId = 1 -- Next map marker ID
-------------------------------------
-- Returns true if vec2 is less than radius meters away from any scenery object
-------------------------------------
-- @param vec2 A 2d point
-- @param radius A range, in meters
-- @return True if vec2 is closer than radius meters from any object, false otherwise
-------------------------------------
function DCSEx.world.collidesWithScenery(vec2, radius)
local foundOne = false
radius = radius or 8
local foundOne = false
local volS = {
id = world.VolumeType.SPHERE,
@@ -47,46 +62,30 @@ do
return foundOne
end
-- function DCSEx.world.findSpawnPoint(vec2, minRadius, maxRadius, surfaceType, radiusWithoutScenery, territorySide, expandSearch)
-- expandSearch = expandSearch or true
-------------------------------------
-- Destroys a group
-------------------------------------
-- @param groupID ID of the group to destroy
-------------------------------------
function DCSEx.world.destroyGroupByID(groupID)
if not groupID then return end
local g = DCSEx.world.getGroupByID(groupID)
if g then g:destroy() end
end
-- for _=0,16 do
-- for _=0,16 do
-- local spawnPoint = nil
-------------------------------------
-- Spawns an explosion where an unit is located
-------------------------------------
-- @param unitID ID of the unit
-- @param amount Intensity of the explosion
-------------------------------------
function DCSEx.world.explodeUnit(unitID, amount)
net.dostring_in("mission", string.format("a_explosion_unit(%d, %f)", unitID, amount))
end
-- spawnPoint = DCSEx.math.randomPointInCircle(
-- vec2,
-- DCSEx.converter.nmToMeters(maxRadius),
-- DCSEx.converter.nmToMeters(minRadius),
-- surfaceType)
-- if spawnPoint and radiusWithoutScenery then
-- if DCSEx.world.collidesWithScenery(spawnPoint, radiusWithoutScenery) then
-- spawnPoint = nil
-- end
-- end
-- if spawnPoint and territorySide then
-- if scramble.territories.getOwner(spawnPoint) ~= territorySide then
-- spawnPoint = nil
-- end
-- end
-- if spawnPoint then return spawnPoint end
-- end
-- if not expandSearch then return nil end
-- minRadius = minRadius * 0.9
-- maxRadius = maxRadius * 1.2
-- end
-- return nil
-- end
-------------------------------------
-- Returns a table of all player-controlled units currently in the game
-------------------------------------
-- @return A table of unit objects
-------------------------------------
function DCSEx.world.getAllPlayers()
@@ -104,7 +103,8 @@ do
-------------------------------------
-- Returns a table of all map scenery buildings.
-- This function is rather CPU-consuming, better run it once on mission start and store the result in table.
-- This function is rather CPU-heavy, better run it once on mission start and store the results in a table.
-------------------------------------
-- @param minHealth Minimum health a building must have to be included in the table
-- @return A table of scenery objects
-------------------------------------
@@ -134,6 +134,7 @@ do
-------------------------------------
-- Returns all units belonging to a given category
-------------------------------------
-- @param coalitionID Coalition ID (coalition.side.XXX) or nil to search all coalitions
-- @param unitCategory An unit category (Group.Category.XXX)
-- @return A table of unit tables
@@ -156,7 +157,9 @@ do
end
-------------------------------------
-- Returns the closest point to roads as a vec2
-- Returns the closest point to roads as a vec2.
-- An alternative to ED's zany land.getClosestPointOnRoads which returns two integers (!!???)
-------------------------------------
-- @param vec2 Coordinates to look for
-- @return A vec2 with the closest point on roads
-------------------------------------
@@ -168,6 +171,7 @@ do
-------------------------------------
-- Returns the LL/MGRS coordinates of a point, as a string
-- Based on code by Bushmanni - https://forums.eagle.ru/showthread.php?t=99480
-------------------------------------
-- @param point The point, as a vec2 or vec3
-- @param hideElevation (optional) Show elevation NOT be displayed? Default: false
-- @return A string
@@ -222,6 +226,7 @@ do
-------------------------------------
-- Returns the last map marker ID generated by DCSEx.world.getNextMarkerID(), if any
-------------------------------------
-- @return A numeric ID, or nil
-------------------------------------
function DCSEx.world.getCurrentMarkerID()
@@ -229,8 +234,27 @@ do
return nextMarkerId - 1
end
-------------------------------------
-- Returns the first player found
-------------------------------------
-- @param side The coalition the player must belong to, or nil to search for any player
-- @return A player unit object, or nil if no player was found
-------------------------------------
function DCSEx.world.getFirstPlayer(side)
local players = {}
if side then
players = coalition.getPlayers(side)
else
players = DCSEx.world.getAllPlayers()
end
if not players or #players == 0 then return nil end
return players[1]
end
-------------------------------------
-- Searches and return a group by its ID
-------------------------------------
-- @param groupID ID of the group
-- @return A group table, or nil if no group with this ID was found
-------------------------------------
@@ -246,7 +270,13 @@ do
return nil
end
-- TODO: description
-------------------------------------
-- Searches and return a map marker by its text (case-insensitive)
-------------------------------------
-- @param text Text to look for (case insensitive)
-- @param coalition Coalition the marker must belong to, or nil to search all coalitions
-- @return A map marker table, or nil if no marker was found
-------------------------------------
function DCSEx.world.getMarkerByText(text, coalition)
if not text then return nil end
text = text:lower()
@@ -256,7 +286,7 @@ do
local markerText = m.text or ""
markerText = markerText:lower()
if markerText == text then
if coalition == nil or m.coalition == coalition then
if not coalition or m.coalition == coalition then
return m
end
end
@@ -266,7 +296,8 @@ do
end
-------------------------------------
-- Returns a new, unique, map marker ID
-- Returns a new unique map marker ID
-------------------------------------
-- @return A numeric ID
-------------------------------------
function DCSEx.world.getNextMarkerID()
@@ -274,7 +305,12 @@ do
return nextMarkerId - 1
end
-- TODO: description, file header
-------------------------------------
-- Returns a table of all player units currently in the air (not on ramp/ground/runway)
-------------------------------------
-- @param side Coalition the players must belong to, or nil to search all coalitions
-- @return A table of player objects
-------------------------------------
function DCSEx.world.getPlayersInAir(side)
local players = {}
if side then
@@ -293,7 +329,38 @@ do
return playersInAir
end
-- TODO: description, file header
-------------------------------------
-- Returns a table of all player units currently NOT in the air (on ramp/ground/runway)
-------------------------------------
-- @param side Coalition the players must belong to, or nil to search all coalitions
-- @return A table of player objects
-------------------------------------
function DCSEx.world.getPlayersOnGround(side)
local players = {}
if side then
players = coalition.getPlayers(side)
else
players = DCSEx.world.getAllPlayers()
end
local playersOnGround = {}
for _,p in ipairs(players) do
if not p:inAir() then
table.insert(playersOnGround, p)
end
end
return playersOnGround
end
-------------------------------------
-- Returns a valid spawn point for a ground unit (not stuck in trees, buildings...) or a naval unit
-------------------------------------
-- @param zone Trigger zone in which to look for a spawn point
-- @param surface Type of surface (land.SurfaceType enum) to look for, or any to return any point (good for air units)
-- @param safeRadius Saferadius in meters from any obstacle (default: 100)
-- @return A 2D point, or nil if none was found
-------------------------------------
function DCSEx.world.getSpawnPoint(zone, surfaceType, safeRadius)
safeRadius = safeRadius or 100
@@ -336,7 +403,14 @@ do
return nil
end
-- TODO: description
-------------------------------------
-- Returns a table of all scenery objects in a given radius
-------------------------------------
-- @param center 2D point on which to center object search
-- @param radius Radius (in meters) around the center in which to search
-- @param minHealth Minimum health for a scenery object to be valid. Allow filtering of small objects like bollards (default: 0)
-- @return A table of scenery objects
-------------------------------------
function DCSEx.world.getSceneriesInZone(center, radius, minHealth)
minHealth = minHealth or 0
local sceneries = {}
@@ -362,23 +436,12 @@ do
end
-------------------------------------
-- Searches and return a static object by its ID
-- @param staticID ID of the static object
-- @return An unit, or nil if no static object with this ID was found
-- Returns the maximum height difference in a given radius around a point
-------------------------------------
-- @param coord 2D point in which to search
-- @param searchRadius Radius in meters
-- @return A numeric value, in meters
-------------------------------------
function DCSEx.world.getStaticObjectByID(staticID)
for coalitionID = 1, 2 do
for _, s in pairs(coalition.getStaticObjects(coalitionID)) do
if DCSEx.dcs.getObjectIDAsNumber(s) == staticID then
return s
end
end
end
return nil
end
-- TODO: description, update file header
function DCSEx.world.getTerrainHeightDiff(coord, searchRadius)
local samples = {}
searchRadius = searchRadius or 5
@@ -403,30 +466,37 @@ do
return tMax - tMin
end
-- TODO: description, update file header
-------------------------------------
-- Returns the 2D center of unit group
-------------------------------------
-- @param group A group of unit
-- @return The 2D point center of all units' positions or 0,0 if no units were found
-------------------------------------
function DCSEx.world.getGroupCenter(group)
return DCSEx.world.getUnitsCenter(group:getUnits())
end
-- TODO: description, update file header
function DCSEx.world.getUnitsCenter(units)
if not units or #units == 0 then return { x = 0, y = 0 } end
local center = { x = 0, y = 0 }
for _,u in pairs(units) do
local uPt2 = DCSEx.math.vec3ToVec2(u:getPoint())
center.x = center.x + uPt2.x
center.y = center.y + uPt2.y
-------------------------------------
-- Searches and return a coalition static object by its ID
-------------------------------------
-- @param unitID ID of the static object
-- @return An static object, or nil if no unit with this ID was found
-------------------------------------
function DCSEx.world.getStaticObjectByID(staticID)
for coalitionID = 1,2 do
for _,s in pairs(coalition.getStaticObjects(coalitionID)) do
if DCSEx.dcs.getObjectIDAsNumber(s) == staticID then
return s
end
end
end
center.x = center.x / #units
center.y = center.y / #units
return center
return nil
end
-------------------------------------
-- Searches and return an unit by its ID
-- Searches and returns an unit by its ID
-------------------------------------
-- @param unitID ID of the unit
-- @return An unit, or nil if no unit with this ID was found
-------------------------------------
@@ -446,23 +516,34 @@ do
end
-------------------------------------
-- Searches and return a coalition static object by its ID
-- @param unitID ID of the static object
-- @return An static object, or nil if no unit with this ID was found
-- Returns the 2D center of a number of units
-------------------------------------
function DCSEx.world.getStaticObjectByID(staticID)
for coalitionID = 1,2 do
for _,s in pairs(coalition.getStaticObjects(coalitionID)) do
if DCSEx.dcs.getObjectIDAsNumber(s) == staticID then
return s
end
end
-- @param units A table of units
-- @return The 2D point center of all units' positions or 0,0 if no units were found
-------------------------------------
function DCSEx.world.getUnitsCenter(units)
if not units or #units == 0 then return { x = 0, y = 0 } end
local center = { x = 0, y = 0 }
for _,u in pairs(units) do
local uPt2 = DCSEx.math.vec3ToVec2(u:getPoint())
center.x = center.x + uPt2.x
center.y = center.y + uPt2.y
end
return nil
center.x = center.x / #units
center.y = center.y / #units
return center
end
-- TODO: description
-------------------------------------
-- Returns true if a group exists and any of its units are alive, false otherwise
-------------------------------------
-- @param g A group
-- @param unitsMustBeInAir Are units on the ground ignored? (default: false)
-- @return True if a group exists and any of its units are alive, false otherwise
-------------------------------------
function DCSEx.world.isGroupAlive(g, unitsMustBeInAir)
if not g then return false end
if not g:isExist() then return false end
@@ -484,22 +565,16 @@ do
return atLeastOneActiveUnit
end
-- TODO: description & file header
-------------------------------------
-- Sets the health of an unit
-------------------------------------
-- @param unitID ID of the unit
-- @param life Life percentage
-------------------------------------
function DCSEx.world.setUnitLifePercent(unitID, life)
net.dostring_in("mission", string.format("a_unit_set_life_percentage(%d, %f)", unitID, life))
end
-- TODO: description & file header
function DCSEx.world.explodeUnit(unitID, amount)
net.dostring_in("mission", string.format("a_explosion_unit(%d, %f)", unitID, amount))
end
function DCSEx.world.destroyGroupByID(groupID)
if not groupID then return end
local g = DCSEx.world.getGroupByID(groupID)
if g then g:destroy() end
end
-- function DCSEx.world.destroySceneryInZone(zone, destructionPercent)
-- destructionPercent = destructionPercent or 0
-- net.dostring_in("mission", string.format("a_scenery_destruction_zone(%d, %f)", zone.zoneId, destructionPercent))
@@ -518,4 +593,41 @@ do
-- function DCSEx.world.shellingZone(zone, tnt, shellsCount)
-- net.dostring_in("mission", string.format("a_shelling_zone(%d, %f, %d)", zone.zoneId, tnt, shellsCount))
-- end
-- function DCSEx.world.findSpawnPoint(vec2, minRadius, maxRadius, surfaceType, radiusWithoutScenery, territorySide, expandSearch)
-- expandSearch = expandSearch or true
-- for _=0,16 do
-- for _=0,16 do
-- local spawnPoint = nil
-- spawnPoint = DCSEx.math.randomPointInCircle(
-- vec2,
-- DCSEx.converter.nmToMeters(maxRadius),
-- DCSEx.converter.nmToMeters(minRadius),
-- surfaceType)
-- if spawnPoint and radiusWithoutScenery then
-- if DCSEx.world.collidesWithScenery(spawnPoint, radiusWithoutScenery) then
-- spawnPoint = nil
-- end
-- end
-- if spawnPoint and territorySide then
-- if scramble.territories.getOwner(spawnPoint) ~= territorySide then
-- spawnPoint = nil
-- end
-- end
-- if spawnPoint then return spawnPoint end
-- end
-- if not expandSearch then return nil end
-- minRadius = minRadius * 0.9
-- maxRadius = maxRadius * 1.2
-- end
-- return nil
-- end
end
+38
View File
@@ -2,6 +2,7 @@
-- DCSEX.ZONES - FUNCTIONS RELATED TO MAP TRIGGER ZONES
-- ====================================================================================
-- DCSEx.zones.drawOnMap(zoneTable, lineColor, fillColor, lineType, drawName, readOnly)
-- DCSEx.zones.getAirbases(zone, coalID, allowShips)
-- DCSEx.zones.getAll()
-- DCSEx.zones.getByName(name)
-- DCSEx.zones.getCenter(zoneTable)
@@ -72,6 +73,43 @@ function DCSEx.zones.drawOnMap(zoneTable, lineColor, fillColor, lineType, drawNa
return markerID
end
-------------------------------------
-- Returns all airbases in the zone
-------------------------------------
-- @param zoneTable Table of the zone in which to search
-- @param coalID Coalition (from the coalition.side enum) the airbase must belong to. Default is nil, which means "all coalitions"
-- @param allowShips Should ships be allowed?
-- @return Table of airbases
-------------------------------------
function DCSEx.zones.getAirbases(zoneTable, coalID, allowShips)
coalID = coalID or nil
allowShips = allowShips or false
local coalitionSides = { coalition.side.RED, coalition.side.BLUE }
if coalID then coalitionSides = { coalID } end
local validAirbases = {}
for _,side in ipairs(coalitionSides) do
for _,ab in ipairs(coalition.getAirbases(side)) do
local abDesc = ab:getDesc()
local isValid = true
if ab:getDesc().category == Airbase.Category.HELIPAD then
isValid = false
elseif ab:getDesc().category == Airbase.Category.SHIP and not allowShips then
isValid = false
end
if isValid then
if DCSEx.zones.isPointInside(zoneTable, ab:getPoint()) then
table.insert(validAirbases, ab)
end
end
end
end
return validAirbases
end
-------------------------------------
-- Returns all trigger zones
-------------------------------------
File diff suppressed because one or more lines are too long
+12 -12
View File
@@ -201,16 +201,17 @@ Library.radioMessages = {
"$1, off the deck, forming up now."
},
atcRequireNearestAirbase = { -- TODO: voiceover
"Roger. Vectoring you to the nearest airbase.\n$1",
"Copy. Coordinates to nearest field inbound.\n$1",
"Roger. Guide you direct to the nearest recovery airfield.\n$1"
atcRequireNearestAirbase = {
"Roger. Vectoring you to the nearest airbase.\n\n$1",
"Copy. Coordinates to nearest field inbound.\n\n$1",
"Roger. Guide you direct to the nearest recovery airfield.\n\n$1"
},
atcWeatherUpdate = { -- TODO: voiceover
"Roger. Weather info coming up now.\n$1",
"Copy. Weather report inbound.\n$1",
"This is control, checking conditions now.\n$1",
"Copy. Weather data on the way.\n$1"
atcRequireNearestAirbaseNone = "No friendly airbase is available at the moment.",
atcWeatherUpdate = {
"Roger. Weather info coming up now.\n\n$1",
"Copy. Weather report inbound.\n\n$1",
"This is control, checking conditions now.\n\n$1",
"Copy. Weather data on the way.\n\n$1"
},
atcSafeLanding = { "Be advised: $1 is wheels down at $2 and clear of runway.", "All aircraft, $1 has landed at $2 and vacated active. Runway is open for next inbound.", "Traffic, $1 is on deck at $2 and heading to parking. Runway clear.", "All flights, $1 just rolled out at $2 and cleared the active.", "Heads up, $1 landed at $2 and moving to the ramp. Runway available for next approach." },
atcSafeLandingPlayer = { "$1, wheels on deck, welcome back. You may taxi to the parking area.", "$1, good copy on landing. Exit when able, proceed to the parking area.", "$1, touchdown confirmed. Continue to parking.", "$1, welcome home. Clear of runway and taxi to parking area.", "$1, nice landing. Taxi to parking when ready." },
@@ -306,13 +307,13 @@ Library.radioMessages = {
"$1, target already marked with smoke."
},
playerATCRequireNearestAirbase = { -- TODO: voiceover
playerATCRequireNearestAirbase = {
"Control, request vectors to nearest suitable base for recovery.",
"Control, requesting nearest friendly airfield for landing, over.",
"Control, negative on original destination, request alternate field nearest current position.",
"Control, requesting location and frequency for closest towered airfield."
},
playerATCWeatherUpdate = { -- TODO: voiceover
playerATCWeatherUpdate = {
"Control, request latest weather update, over.",
"Control, need current weather and visibility.",
"Control, what's the weather looking like out there?",
@@ -330,7 +331,6 @@ Library.radioMessages = {
"Command, mission timeline check, are we on schedule?"
},
playerCommandRequireObjectives = {
"Command, request objective $1 coordinates, over.",
"Command, send me grid for objective $1.",
+124 -111
View File
@@ -9,35 +9,38 @@ TUM.VERSION_STRING = "0.1.250722"
TUM.DEBUG_MODE = __DEBUG_MODE__
TUM.logLevel = {
INFO = 0,
WARNING = 1,
ERROR = 2
}
-------------------------------------
-- Prints and logs a debug message
-- @param message The message
-- @param logLevel Is it a warning, error or info messages (as defined in TUM.logLevel). Info messages are not printed out unless debug mode is enabled.
-- @param logLevel Is it a warning, error or info messages (as defined in TUM.logger.logLevel). Info messages are not printed out unless debug mode is enabled.
-------------------------------------
function TUM.log(message, logLevel)
logLevel = logLevel or TUM.logLevel.INFO
if logLevel == TUM.logLevel.ERROR then
trigger.action.outText("ERROR: "..message, 3600)
env.warning("TUM - ERROR: "..message, false)
elseif logLevel == TUM.logLevel.WARNING then
trigger.action.outText("WARNING: "..message, 10)
env.warning("TUM - WARNING: "..message, false)
else
if TUM.DEBUG_MODE then -- Info messages are only printed out if debug mode is enabled
trigger.action.outText(message, 3)
end
env.info("TUM: "..message, false)
end
logLevel = logLevel or TUM.logger.logLevel.INFO
TUM.logger.print(logLevel, message)
end
--------------------------------------
--- Radio menu for the mission commands
--------------------------------------
TUM.rootMenu = nil
function TUM.getOrCreateRootMenu(reset) -- Get or create the root menu for the mission commands; if reset is true, the menu will be cleared and recreated
if reset then
missionCommands.removeItem(TUM.rootMenu) -- Clear the menu
TUM.rootMenu = nil
TUM.getOrCreateRootMenu() -- Recreate the root menu
end
if not TUM.rootMenu then
if TUM.administrativeSettings.getValue(TUM.administrativeSettings.USE_SPECIFIC_RADIOMENU) then
local rootMenuTitle = "✈ TUM"
TUM.rootMenu = missionCommands.addSubMenu(rootMenuTitle)
end
end
return TUM.rootMenu
end
--------------------------------------
--[[DCS EXTENSIONS]]--
--[[LIBRARY]]--
@@ -48,118 +51,128 @@ end
-- Module startup --
--------------------
do
local function startUpMission()
TUM.hasStarted = false
function TUM.initialize()
do
TUM.administrativeSettings.onStartUp() -- load the administrative settings
local coreSettings = {
multiplayer = false
}
local function startUpMission()
TUM.hasStarted = false
if not net or not net.dostring_in then
TUM.log("Mission failed to execute. Please copy the provided \"autoexec.cfg\" file to the [Saved Games]\\DCS\\Config directory.\nThe file can be downloaded from github.com/akaAgar/the-universal-mission-for-dcs-world", TUM.logLevel.ERROR)
return nil
end
local coreSettings = {
multiplayer = false
}
if #DCSEx.envMission.getPlayerGroups() == 0 then
TUM.log("No \"Player\" or \"Client\" aircraft slots have been found. Please fix this problem in the mission editor.", TUM.logLevel.ERROR)
return nil
end
if world:getPlayer() then
coreSettings.multiplayer = false
if #DCSEx.envMission.getPlayerGroups() > 1 then
TUM.log("Multiple players slots have been found in addition to the single-player \"Player\" aircraft. Please fix this problem in the mission editor.", TUM.logLevel.ERROR)
if not net or not net.dostring_in then
TUM.log("Mission failed to execute. Please copy the provided \"autoexec.cfg\" file to the [Saved Games]\\DCS\\Config directory.\nThe file can be downloaded from github.com/akaAgar/the-universal-mission-for-dcs-world", TUM.logger.logLevel.ERROR)
return nil
end
else
coreSettings.multiplayer = true
if #DCSEx.envMission.getPlayerGroups() == 0 then
TUM.log("No \"Client\" aircraft slots have been found. Please fix this problem in the mission editor.", TUM.logger.logLevel.ERROR)
return nil
end
if world:getPlayer() then
TUM.log("A \"Player\" aircraft slot has been found. The Universal Mission only uses \"Client\" slots, even for single-player missions. Please fix this problem in the mission editor.", TUM.logger.logLevel.ERROR)
return nil
end
coreSettings.multiplayer = (#DCSEx.envMission.getPlayerGroups() > 1)
if #DCSEx.envMission.getPlayerGroups(coalition.side.BLUE) == 0 and #DCSEx.envMission.getPlayerGroups(coalition.side.RED) == 0 then
TUM.log("Neither BLUE nor RED coalitions have player slots. Please make sure one coalition has player slots in the mission editor.", TUM.logLevel.ERROR)
TUM.log("Neither BLUE nor RED coalitions have player slots. Please make sure one coalition has player slots in the mission editor.", TUM.logger.logLevel.ERROR)
return nil
end
if #DCSEx.envMission.getPlayerGroups(coalition.side.BLUE) > 0 and #DCSEx.envMission.getPlayerGroups(coalition.side.RED) > 0 then
TUM.log("Both coalitions have player slots. The Universal Mission is a purely singleplayer/PvE experience and does not support PvP. Please make sure only one coalition has player slots in the mission editor.", TUM.logLevel.ERROR)
TUM.log("Both coalitions have player slots. The Universal Mission is a purely singleplayer/PvE experience and does not support PvP. Please make sure only one coalition has player slots in the mission editor.", TUM.logger.logLevel.ERROR)
return nil
end
if not TUM.territories.onStartUp() then return nil end
if not TUM.settings.onStartUp(coreSettings) then return nil end -- Must be called after TUM.territories.onStartUp()
if not TUM.playerCareer.onStartUp() then return nil end
if not TUM.intermission.onStartUp() then return nil end
if not TUM.airForce.onStartUp() then return nil end
if not TUM.mizCleaner.onStartUp() then return nil end -- Must be called after TUM.settings.onStartUp()
TUM.hasStarted = true
return coreSettings
end
if not TUM.territories.onStartUp() then return nil end
if not TUM.settings.onStartUp(coreSettings) then return nil end -- Must be called after TUM.territories.onStartUp()
if not TUM.playerCareer.onStartUp() then return nil end
if not TUM.intermission.onStartUp() then return nil end
if not TUM.airForce.onStartUp() then return nil end
if not TUM.mizCleaner.onStartUp() then return nil end -- Must be called after TUM.settings.onStartUp()
TUM.hasStarted = true
return coreSettings
if not startUpMission() then
trigger.action.outText("A critical error has happened, cannot start the mission.", 3600)
end
end
if not startUpMission() then
trigger.action.outText("A critical error has happened, cannot start the mission.", 3600)
end
end
-------------------
-- Event handler --
-------------------
do
local eventHandler = {}
-------------------
-- Event handler --
-------------------
do
local eventHandler = {}
function eventHandler:onEvent(event)
if not event then return end -- No event
function eventHandler:onEvent(event)
if not event then return end -- No event
TUM.ambientRadio.onEvent(event) -- Must be first so other (more important) radio messages will interrupt the "ambient" ones
TUM.ambientWorld.onEvent(event)
TUM.objectives.onEvent(event)
TUM.playerScore.onEvent(event)
TUM.mission.onEvent(event)
TUM.wingmen.onEvent(event)
TUM.mizCleaner.onEvent(event) -- Must be last, can remove units which could cause bugs in other onEvent methods
end
function TUM.onEvent(event)
eventHandler:onEvent(event)
end
if TUM.hasStarted then
world.addEventHandler(eventHandler)
end
end
--------------------------------------------
-- Game clock, called every 10-20 seconds --
--------------------------------------------
do
local clockTick = -1
function TUM.onClockTick(arg, time)
local nextTickTime = time + math.random(10, 20)
clockTick = clockTick + 1
TUM.wingmenTasking.onClockTick() -- No need to check the function return, it's just here to check if wingmen target is still alive
if clockTick % 4 == 0 then
if TUM.playerScore.onClockTick() then return nextTickTime end
if TUM.mission.onClockTick() then return nextTickTime end
elseif clockTick % 4 == 1 then
if TUM.airForce.onClockTick(TUM.settings.getPlayerCoalition()) then return nextTickTime end
elseif clockTick % 4 == 2 then
if TUM.supportAWACS.onClockTick() then return nextTickTime end
else
if TUM.airForce.onClockTick(TUM.settings.getEnemyCoalition()) then return nextTickTime end
TUM.ambientRadio.onEvent(event) -- Must be first so other (more important) radio messages will interrupt the "ambient" ones
TUM.airForce.onEvent(event)
TUM.ambientWorld.onEvent(event)
TUM.ambientWorld.onEvent(event)
TUM.objectives.onEvent(event)
TUM.playerScore.onEvent(event)
TUM.mission.onEvent(event)
TUM.wingmen.onEvent(event)
TUM.mizCleaner.onEvent(event) -- Must be last, can remove units which could cause bugs in other onEvent methods
end
if TUM.wingmenContacts.onClockTick() then return nextTickTime end -- Called every tick if no other action has taken place
function TUM.onEvent(event)
eventHandler:onEvent(event)
end
return nextTickTime
if TUM.hasStarted then
world.addEventHandler(eventHandler)
end
end
if TUM.hasStarted then
timer.scheduleFunction(TUM.onClockTick, nil, timer.getTime() + math.random(10, 15))
--------------------------------------------
-- Game clock, called every 10-20 seconds --
--------------------------------------------
do
local clockTick = -1
function TUM.onClockTick(arg, time)
local nextTickTime = time + math.random(10, 20)
clockTick = clockTick + 1
TUM.wingmenTasking.onClockTick() -- No need to check the function return, it's just here to check if wingmen target is still alive
if clockTick % 4 == 0 then
if TUM.playerScore.onClockTick() then return nextTickTime end
if TUM.mission.onClockTick() then return nextTickTime end
elseif clockTick % 4 == 1 then
if TUM.airForce.onClockTick(TUM.settings.getPlayerCoalition()) then return nextTickTime end
elseif clockTick % 4 == 2 then
if TUM.supportAWACS.onClockTick() then return nextTickTime end
else
if TUM.airForce.onClockTick(TUM.settings.getEnemyCoalition()) then return nextTickTime end
end
if TUM.wingmenContacts.onClockTick() then return nextTickTime end -- Called every tick if no other action has taken place
return nextTickTime
end
if TUM.hasStarted then
timer.scheduleFunction(TUM.onClockTick, nil, timer.getTime() + math.random(10, 15))
end
end
TUM.supportAWACS.create()
end
if TUM.administrativeSettings.getValue(TUM.administrativeSettings.INITIALIZE_AUTOMATICALLY) then
TUM.initialize()
else
TUM.log("TUM has been loaded, but not initialized. Call TUM.initialize() to start the mission.", TUM.logger.logLevel.INFO)
end
+89 -27
View File
@@ -1,6 +1,32 @@
TUM.atc = {}
do
local function getFlyTime(playerUnit, point3)
local point2 = DCSEx.math.vec3ToVec2(point3)
local velocity = playerUnit:getVelocity()
local speed = math.max(1, math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z))
local distance = DCSEx.math.getDistance2D(point2, DCSEx.math.vec3ToVec2(playerUnit:getPoint()))
local timeInMinutes = math.max(1, math.floor(distance / (speed * 60)))
local eta = DCSEx.string.getTimeString(timer.getAbsTime() + timeInMinutes * 60)
if timeInMinutes > 600 then
return "More than ten hours of flight time at current airspeed\n"
elseif timeInMinutes > 120 then
return tostring(math.floor(timeInMinutes / 60)).." hours of flight time at current airspeed, ETA "..eta.."\n"
elseif timeInMinutes < 2 then
return "Less than 2 minutes of flight time at current airspeed, ETA "..eta.."\n"
else
return tostring(timeInMinutes).." minutes of flight time at current airspeed, ETA "..eta.."\n"
end
end
local function getTemperatureCelsiusAndFarenheit(temperature, inKelvins)
inKelvins = inKelvins or false
if inKelvins then temperature = temperature - 273.15 end
return tostring(math.floor(temperature)).."°C/"..tostring(math.floor(DCSEx.converter.celsiusToFahrenheit(temperature))).."°F"
end
function TUM.atc.requestNavAssistanceToObjective(index, delayRadioAnswer)
local obj = TUM.objectives.getObjective(index)
if not obj then return end
@@ -12,20 +38,7 @@ do
for _,p in ipairs(players) do
-- Give BRA to objective
local navInfo = "- Fly "..DCSEx.dcs.getBRAA(obj.waypoint3, p:getPoint(), false).."\n"
-- Give flight time and ETA
local velocity = p:getVelocity()
local speed = math.max(1, math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z))
local distance = DCSEx.math.getDistance2D(obj.waypoint2, DCSEx.math.vec3ToVec2(p:getPoint()))
local timeInMinutes = math.max(1, math.floor(distance / (speed * 60)))
local eta = DCSEx.string.getTimeString(timer.getAbsTime() + timeInMinutes * 60)
if timeInMinutes > 600 then
navInfo = navInfo.."- More than ten hours of flight time at current airspeed\n"
elseif timeInMinutes > 120 then
navInfo = navInfo.."- "..tostring(math.floor(timeInMinutes / 60)).." hours of flight time at current airspeed, ETA "..eta.."\n"
else
navInfo = navInfo.."- "..tostring(timeInMinutes).." minute(s) of flight time at current airspeed, ETA "..eta.."\n"
end
navInfo = navInfo.."- "..getFlyTime(p, obj.waypoint3) -- Give flight time and ETA
-- Give objective coordinates
if obj.preciseCoordinates then
@@ -35,20 +48,20 @@ do
end
navInfo = navInfo..DCSEx.world.getCoordinatesAsString(obj.waypoint3, false)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "commandObjectiveCoordinates"..msgIDSuffix, { obj.name, navInfo }, "Command", delayRadioAnswer)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "commandObjectiveCoordinates"..msgIDSuffix, { obj.name, navInfo }, "Command", delayRadioAnswer, nil, nil, 2.5)
end
end
function TUM.atc.requireNearestAirbase(delayRadioAnswer)
function TUM.atc.requestNavAssistanceToAirbase(delayRadioAnswer)
local players = coalition.getPlayers(TUM.settings.getPlayerCoalition())
for _,p in ipairs(players) do
local airbaseInfo = "- No airbase available near you at the moment." -- TODO: proper "no airbase" message
local airbaseInfo = nil -- Mark as nil, in case no valid airbase is found
local validAirbaseTypes = { Airbase.Category.AIRDROME }
if p:hasAttribute("Helicopters") then table.insert(validAirbaseTypes, Airbase.Category.HELIPAD) end
local pDesc = p:getDesc()
if pDesc.LandRWCategories and #pDesc.LandRWCategories > 0 then
-- TODO: check player unit description to filter compatible carrier types
-- TODO: check player unit description to filter compatible carrier types (Harrier/helos can land anywhere, naval fighters can land on carriers, etc)
table.insert(validAirbaseTypes, Airbase.Category.SHIP)
end
@@ -57,32 +70,81 @@ do
allAirbases = DCSEx.dcs.getNearestObjects(DCSEx.math.vec3ToVec2(p:getPoint()), allAirbases)
for i=1,#allAirbases do
local abDesc = airbaseInfo[i]:getDesc()
local abDesc = allAirbases[i]:getDesc()
if DCSEx.table.contains(validAirbaseTypes, abDesc.category) then
airbaseInfo = abDesc.displayName
break
local abPoint = allAirbases[i]:getPoint()
if abDesc.category == Airbase.Category.AIRDROME then
airbaseInfo = abDesc.displayName:upper().." AIRBASE:\n"
else -- Helipad or ship
airbaseInfo = abDesc.displayName:upper()..":\n"
end
airbaseInfo = airbaseInfo.."- Fly "..DCSEx.dcs.getBRAA(abPoint, p:getPoint(), false).."\n"
airbaseInfo = airbaseInfo.."- "..getFlyTime(p, abPoint).."\n"
airbaseInfo = airbaseInfo..DCSEx.world.getCoordinatesAsString(abPoint, false)
local runways = allAirbases[i]:getRunways()
if #runways > 0 then
airbaseInfo = airbaseInfo.."\n\nRunways: "
for j=1,#runways do
-- Compute the runway course (in degrees, divided by 10)
local courseDeg = math.floor(DCSEx.converter.radiansToDegrees(runways[j].course * -1))
if courseDeg < 0 then courseDeg = courseDeg + 360 end
if courseDeg >= 360 then courseDeg = courseDeg - 360 end
courseDeg = math.floor(courseDeg / 10)
-- Compute the opposite runway coursecourseDegNeg
local courseDegNeg = courseDeg + 18
if courseDegNeg >= 36 then courseDegNeg = courseDegNeg - 36 end
-- Make sure the lowest runway heading is displayed first
if courseDeg > courseDegNeg then
local tmp = courseDegNeg
courseDegNeg = courseDeg
courseDeg = tmp
end
airbaseInfo = airbaseInfo..tostring(courseDeg).."/"..tostring(courseDegNeg).." ("..tostring(math.floor(runways[j].length)).." m)"
if j < #runways then airbaseInfo = airbaseInfo..", " end
end
end
-- TODO: radio tower frequency?
break -- Stop after finding one
end
end
end
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcRequireNearestAirbase", { airbaseInfo }, "Control", delayRadioAnswer)
if airbaseInfo then
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcRequireNearestAirbase", { airbaseInfo }, "Control", delayRadioAnswer, nil, false, 2.5)
else
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcRequireNearestAirbaseNone", nil, "Control", delayRadioAnswer)
end
end
end
function TUM.atc.requestWeatherUpdate(delayRadioAnswer)
local weatherInfo = "- It is currenly "..DCSEx.string.getTimeString()
local commonWeatherInfo = "- It is currenly "..DCSEx.string.getTimeString()
if Library.environment.isItNightTime() then
weatherInfo = weatherInfo.." (night, sunrise at "..DCSEx.string.getTimeString(Library.environment.getDayTime(nil, false))..")\n"
commonWeatherInfo = commonWeatherInfo.." (night, sunrise at "..DCSEx.string.getTimeString(Library.environment.getDayTime(nil, false))..")\n"
else
weatherInfo = weatherInfo.." (day, sunset at "..DCSEx.string.getTimeString(Library.environment.getDayTime(nil, true))..")\n"
commonWeatherInfo = commonWeatherInfo.." (day, sunset at "..DCSEx.string.getTimeString(Library.environment.getDayTime(nil, true))..")\n"
end
weatherInfo = weatherInfo.."- Average windspeed is "..tostring(DCSEx.floor(Library.environment.getWindAverage())).."m/s\n"
commonWeatherInfo = commonWeatherInfo.."- Cloud cover: "..TUM.weather.getWeatherName(nil, true).."\n"
commonWeatherInfo = commonWeatherInfo.."- Wind: "..TUM.weather.getWindName()..", with avg. speed of "..tostring(math.floor(Library.environment.getWindAverage())).."m/s\n"
commonWeatherInfo = commonWeatherInfo.."- Average ground-level temperature is "..getTemperatureCelsiusAndFarenheit(env.mission.weather.season.temperature).."\n"
local players = coalition.getPlayers(TUM.settings.getPlayerCoalition())
for _,p in ipairs(players) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcWeatherUpdate", { weatherInfo }, "Control", delayRadioAnswer)
local lTemperature, _ = atmosphere.getTemperatureAndPressure(p:getPoint())
local localWeatherInfo = ""
localWeatherInfo = localWeatherInfo.."- Wind speed at your location is "..tostring(math.floor(DCSEx.math.getLength3D(atmosphere.getWind(p:getPoint())))).."m/s\n"
localWeatherInfo = localWeatherInfo.."- Temperature at your location is "..getTemperatureCelsiusAndFarenheit(lTemperature, true).."\n"
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcWeatherUpdate", { commonWeatherInfo..localWeatherInfo }, "Control", delayRadioAnswer)
end
end
end
@@ -0,0 +1,87 @@
-- ====================================================================================
-- TUM.ADMINISTRATIVESETTINGS - HANDLE ADMINISTRATIVE SETTINGS
-- ====================================================================================
-- (enum) TUM.administrativeSettings
-- TUM.administrativeSettingsDefaultValues
-- TUM.administrativeSettingsValues
-- TUM.administrativeSettings.getValue(key)
-- TUM.administrativeSettings.onStartUp()
-- TUM.administrativeSettings.setValue(key, value)
-- ====================================================================================
TUM.administrativeSettings = {
-- This table defines the administrative settings for the script.
-- These settings can modify the behavior of the script, and can be set by the mission maker via a specific trigger zone's parameters, or via script (defining the TUM.administrativeSettingsValues below)
USE_SPECIFIC_RADIOMENU = 1, -- Use a specific radio menu for the mission commands, or use the main one?
INITIALIZE_AUTOMATICALLY = 2, -- Automatically initialize the mission when the script is loaded. If false, you must call TUM.initialize() manually.
IGNORE_ZONES_STARTINGWITH = 3, -- If set, ignore all zones starting with this string. This is useful to avoid conflicts with other scripts that use the same zone names.
ONLY_ZONES_STARTINGWITH = 4, -- If set, only adds zones starting with this string. This is useful to avoid conflicts with other scripts that use the same zone names.
}
TUM.administrativeSettingsDefaultValues = {
-- This table defines the default values for the administrative settings.
-- The keys must match the keys in TUM.administrativeSettings
[TUM.administrativeSettings.USE_SPECIFIC_RADIOMENU] = false, -- Use a specific radio menu for the mission commands, or use the main one?
[TUM.administrativeSettings.INITIALIZE_AUTOMATICALLY] = true, -- Automatically initialize the mission when the script is loaded. If false, you must call TUM.initialize() manually.
[TUM.administrativeSettings.IGNORE_ZONES_STARTINGWITH] = nil, -- If set, ignore all zones starting with this string. This is useful to avoid conflicts with other scripts that use the same zone names.
[TUM.administrativeSettings.ONLY_ZONES_STARTINGWITH] = nil, -- If set, only adds zones starting with this string. This is useful to avoid conflicts with other scripts that use the same zone names.
}
TUM.administrativeSettingsValues = {
-- This table defines the administrative settings values for the script.
-- The keys must match the keys in TUM.administrativeSettings
-- If set, these values will prevail over both the default values in TUM.administrativeSettings and the values set by the mission maker via a specific trigger zone's parameters.
}
--- Returns the value of the administrative setting with the given key
function TUM.administrativeSettings.getValue(key)
if TUM.administrativeSettingsValues[key] ~= nil then
return TUM.administrativeSettingsValues[key]
else
return TUM.administrativeSettingsDefaultValues[key]
end
end
--- Takes all the values from (in order of priority):
--- 1. The TUM.administrativeSettingsValues table (optionnaly set by script)
--- 2. The trigger zone parameters (set by the mission maker)
--- 3. The TUM.administrativeSettingsDefaultValues table (default values)
function TUM.administrativeSettings.onStartUp()
local ADMIN_ZONE_NAME = "TUM_Administrative_Settings" -- The name of the administrative settings trigger zone
local adminZone = DCSEx.zones.getByName(ADMIN_ZONE_NAME)
for key, _ in pairs(TUM.administrativeSettings) do
local value = nil
if TUM.administrativeSettingsValues[TUM.administrativeSettings[key]] then -- Check if the value is set by script
value = TUM.administrativeSettingsValues[TUM.administrativeSettings[key]]
end
if value == nil and adminZone then -- If the value is not set by script, check the trigger zone parameters
local zoneValue = DCSEx.zones.getProperty(adminZone, key)
if zoneValue ~= nil then
value = zoneValue
end
end
if value == nil then -- If the value is not set by script or trigger zone, use the default value
value = TUM.administrativeSettingsDefaultValues[TUM.administrativeSettings[key]]
end
TUM.administrativeSettingsValues[TUM.administrativeSettings[key]] = value
end
end
--- Sets the value of the administrative setting with the given key
function TUM.administrativeSettings.setValue(key, value)
-- check if the key is in the administrative settings table
local foundKey = false
for _, v in pairs(TUM.administrativeSettings) do
if v == key then
foundKey = true
break
end
end
if not foundKey then
TUM.log("Tried to set an unknown administrative setting: "..tostring(key), TUM.logger.logLevel.ERROR)
return nil
end
TUM.administrativeSettingsValues[key] = value
end
+37 -2
View File
@@ -6,9 +6,13 @@
TUM.airForce = {}
do
local SUPPRESSION_INCREASE_ON_KILL = 0.35 -- Value added to enemyCAPSuppression each time an enemy aircraft is shot down
local desiredUnitCount = { 4, 4 } -- Desired max number of aircraft in the air at any single time
local fighterGroups = { {}, {} }
local enemyCAPSuppression = 1
local enemyCAPSuppressionTimer = 1
local playerCenter = nil
local function getSkillLevel(side)
@@ -65,7 +69,7 @@ do
local function launchNewAircraftGroup(side, airbases)
local groupSize = DCSEx.table.getRandom({ 1, 2, 2, 2, 2, 3, 3, 4 })
groupSize = math.min(groupSize, desiredUnitCount[side] - getAirborneUnitCount(side))
if groupSize <= 0 then return false end
if groupSize <= 0 then return false end -- No aircraft slots left
local faction = TUM.settings.getEnemyFaction()
if side == TUM.settings.getPlayerCoalition() then faction = TUM.settings.getPlayerFaction() end
@@ -73,6 +77,17 @@ do
local units = Library.factions.getUnits(faction, DCSEx.enums.unitFamily.PLANE_FIGHTER, groupSize, true)
if not units or #units == 0 then return false end -- No aircraft found
-- If enemy CAP suppression timer > 0, decrement it by 1 but don't spawn any aircraft
if side == TUM.settings.getEnemyCoalition() then
enemyCAPSuppressionTimer = enemyCAPSuppressionTimer - 1
if enemyCAPSuppressionTimer > 0 then
TUM.log("Enemy CAP is still suppressed (suppression="..tostring(enemyCAPSuppressionTimer).."), no enemy CAP spawned.")
return false
end
enemyCAPSuppressionTimer = enemyCAPSuppression
end
local launchAirbase = airbases[DCSEx.math.clamp(math.random(1, math.ceil(math.sqrt(#airbases))), 1, #airbases)]
local originPt = DCSEx.math.vec3ToVec2(launchAirbase:getPoint())
@@ -150,7 +165,6 @@ do
randomizeDesiredAircraftCount(side)
end
-- return launchNewAircraftGroup(side, airbases)
return launchNewAircraftGroup(side, validAirbases)
end
end
@@ -170,6 +184,23 @@ do
return updateAirForce(side)
end
-------------------------------------
-- Called when an event is raised
-- @param event The DCS World event
-------------------------------------
function TUM.airForce.onEvent(event)
if not event.initiator then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if event.id ~= world.event.S_EVENT_UNIT_LOST then return end
local groupID = DCSEx.dcs.getGroupIDAsNumber(event.initiator:getGroup())
if DCSEx.table.contains(fighterGroups[TUM.settings.getEnemyCoalition()], groupID) then
enemyCAPSuppression = enemyCAPSuppression + SUPPRESSION_INCREASE_ON_KILL
TUM.log("Enemy CAP suppression increased to "..tostring(enemyCAPSuppression))
end
end
function TUM.airForce.create()
TUM.airForce.removeAll()
TUM.log("Creating friendly and enemy air forces...")
@@ -190,6 +221,10 @@ do
end
end
-- Reset enemy CAP suppression
enemyCAPSuppression = 1
enemyCAPSuppressionTimer = 1
fighterGroups = { {}, {} }
end
@@ -225,11 +225,9 @@ do
if not event.initiator then return end -- No event initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly
if not event.place then return end -- Not landed at an airbase (e.g. helicopter landing on the ground)
local baseName = "AIRBASE"
if event.place then
baseName = event.place:getName():upper()
end
local baseName = event.place:getName():upper()
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) or not event.initiator:getPlayerName() then
doAmbientChatter("atcSafeLanding", {event.initiator:getCallsign(), baseName}, baseName.." ATC", 1)
+14 -1
View File
@@ -32,7 +32,7 @@ do
end
-- Called when a unit is destroyed
-- Called when an unit is destroyed
local function onEventDead(event)
if not event.initiator then return end -- Nothing was hit
@@ -53,6 +53,17 @@ do
)
end
-- Called when an unit takes damage
local function onEventHit(event)
if not event.initiator then return end -- Nothing was hit
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Target wasn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getEnemyCoalition() then return end -- Unit is not an enemy
if event.initiator:getDesc().category ~= Unit.Category.AIRPLANE and event.initiator:getDesc().category ~= Unit.Category.HELICOPTER then return end -- Wasn't an aircraft
if event.initiator:inAir() then return end -- Unit is currently in air
trigger.action.explosion(event.initiator:getPoint(), 100) -- Detonate the parked aircraft, to make it easier to kill
end
function TUM.ambientWorld.removeAll()
for _,id in ipairs(groupIDs) do
DCSEx.world.destroyGroupByID(id)
@@ -70,6 +81,8 @@ do
if event.id == world.event.S_EVENT_DEAD then
onEventDead(event)
elseif event.id == world.event.S_EVENT_HIT then
onEventHit(event)
end
end
end
+5 -2
View File
@@ -68,7 +68,10 @@ do
local runwayTouchEvent = { id = world.event.S_EVENT_RUNWAY_TOUCH, initiator = playerUnit }
TUM.onEvent(runwayTouchEvent)
local landingEvent = { id = world.event.S_EVENT_LAND, initiator = playerUnit }
local friendlyAirbases = coalition.getAirbases(TUM.settings.getPlayerCoalition())
if not friendlyAirbases or #friendlyAirbases == 0 then return end
local landingEvent = { id = world.event.S_EVENT_LAND, place = friendlyAirbases[1], initiator = playerUnit }
timer.scheduleFunction(TUM.onEvent, landingEvent, timer.getTime() + 1)
end
@@ -94,7 +97,7 @@ do
function TUM.debugMenu.createMenu()
if not TUM.DEBUG_MODE then return end
local rootMenu = missionCommands.addSubMenu("[DEBUG]")
local rootMenu = missionCommands.addSubMenu("[DEBUG]", TUM.getOrCreateRootMenu())
missionCommands.addCommand("Detonate - BOOM map markers", rootMenu, doMarkersBoom, nil)
missionCommands.addCommand("Detonate - AIRBOOM map markers", rootMenu, doMarkersAirBoom, nil)
missionCommands.addCommand("Wingman - kill", rootMenu, doKillWingman, nil)
@@ -109,7 +109,7 @@ do
if addAirDefenseGroup(side, faction, unitFamily, point) then
realCount = realCount + 1
else
TUM.log("Failed to add point air defense group near objective "..TUM.objectives.getObjective(i).name..".", TUM.logLevel.WARNING)
TUM.log("Failed to add point air defense group near objective "..TUM.objectives.getObjective(i).name..".", TUM.logger.logLevel.WARNING)
end
end
end
@@ -134,7 +134,7 @@ do
if addAirDefenseGroup(side, faction, unitFamily, point) then
realCount = realCount + 1
else
TUM.log("Failed to add local air defense group.", TUM.logLevel.WARNING)
TUM.log("Failed to add local air defense group.", TUM.logger.logLevel.WARNING)
end
end
@@ -154,7 +154,7 @@ do
if addAirDefenseGroup(side, faction, DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS, point) then
realCount = realCount + 1
else
TUM.log("Failed to add local MANPADS group.", TUM.logLevel.WARNING)
TUM.log("Failed to add local MANPADS group.", TUM.logger.logLevel.WARNING)
end
end
+39 -29
View File
@@ -10,31 +10,6 @@ TUM.intermission = {}
do
local missionZonesMarkers = {}
local function doCommandStartMission()
local players = DCSEx.world.getAllPlayers()
if #players == 0 then
trigger.action.outText("No player slots occupied. At least one client slot must be occupied by a player to start the mission.", 5)
trigger.action.outSound("UI-Error.ogg")
return
end
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then
for _,p in ipairs(players) do
if p:inAir() then
trigger.action.outText("Cannot start a single player mission while the player is in the air. Please land before starting the mission.", 5)
trigger.action.outSound("UI-Error.ogg")
return
end
end
end
trigger.action.outText("Generating mission and loading assets, this can take some time...", 5)
-- Add a little delay for the "Generating mission..." message be printed out. Once generation begins, the main DCS thread will be to busy to output anything.
timer.scheduleFunction(TUM.mission.beginMission, false, timer.getTime() + 1)
end
local function setSetting(args)
if not args.id or not args.value then return end
@@ -74,6 +49,41 @@ do
end
end
function TUM.intermission.doCommandStartMission(allowEvenInAir)
allowEvenInAir = allowEvenInAir or false
local players = DCSEx.world.getAllPlayers()
if #players == 0 then
trigger.action.outText("No player slots occupied. At least one client slot must be occupied by a player to start the mission.", 5)
trigger.action.outSound("UI-Error.ogg")
return
end
if not allowEvenInAir and not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then
for _,p in ipairs(players) do
if p:inAir() then
trigger.action.outText("Cannot start a single player mission while the player is in the air. Please land before starting the mission.", 5)
trigger.action.outSound("UI-Error.ogg")
return
end
end
end
-- If an OCA mission has been selected and the selected target zone doesn't contain any enemy airfield, default to ground attack
if TUM.settings.getValue(TUM.settings.id.TASKING) == DCSEx.enums.taskFamily.OCA then
local zone = DCSEx.zones.getByName(TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true))
if #DCSEx.zones.getAirbases(zone, TUM.settings.getEnemyCoalition()) == 0 then
trigger.action.outText("OCA tasking selected in a zone without any enemy airfields, defaulting to GROUND ATTACK tasking.", 3)
TUM.settings.setValue(TUM.settings.id.TASKING, DCSEx.enums.taskFamily.GROUND_ATTACK, true)
end
end
trigger.action.outText("Generating mission and loading assets, this can take some time...", 3)
-- Add a little delay for the "Generating mission..." message be printed out. Once generation begins, the main DCS thread will be to busy to output anything.
timer.scheduleFunction(TUM.mission.beginMission, false, timer.getTime() + 1)
end
function TUM.intermission.removeMissionZonesMarkers()
for _,id in ipairs(missionZonesMarkers) do
trigger.action.removeMark(id)
@@ -86,7 +96,7 @@ do
-- Creates the mission briefing menu
-------------------------------------
function TUM.intermission.createMenu()
missionCommands.removeItem() -- Clear the menu
local rootMenu = TUM.getOrCreateRootMenu(true) -- Clear the menu
local briefingText = "Welcome to The Universal Mission for DCS World, a highly customizable mission available for single-player and PvE.\n\nOpen the communication menu and select the ''F10. Other'' option to access mission settings."
DCSEx.envMission.setBriefing(coalition.side.RED, briefingText)
@@ -94,9 +104,9 @@ do
TUM.intermission.createMissionZonesMarkers() -- Show the available mission zones on the F10 map
missionCommands.addCommand(" Display mission settings", nil, TUM.settings.printSettingsSummary, false)
missionCommands.addCommand(" Display mission settings", rootMenu, TUM.settings.printSettingsSummary, false)
local settingsMenu = missionCommands.addSubMenu("✎ Change mission settings")
local settingsMenu = missionCommands.addSubMenu("✎ Change mission settings", rootMenu)
createSubMenu(TUM.settings.id.COALITION_BLUE, settingsMenu)
createSubMenu(TUM.settings.id.COALITION_RED, settingsMenu)
createSubMenu(TUM.settings.id.TASKING, settingsMenu)
@@ -107,7 +117,7 @@ do
createSubMenu(TUM.settings.id.WINGMEN, settingsMenu)
createSubMenu(TUM.settings.id.AI_CAP, settingsMenu)
TUM.playerCareer.createMenu()
missionCommands.addCommand("➤ Begin mission", nil, doCommandStartMission, nil)
missionCommands.addCommand("➤ Begin mission", rootMenu, TUM.intermission.doCommandStartMission, false)
TUM.debugMenu.createMenu() -- Append debug menu to other menus (if debug mode enabled)
end
+140
View File
@@ -0,0 +1,140 @@
-- ====================================================================================
-- TUM.LOGGER - LOGS WARNINGS, ERRORS AND DEBUG INFO
-- ====================================================================================
-- (enum) TUM.logger.
-- TUM.logger.debug(text, ...)
-- TUM.logger.error(text, ...)
-- TUM.logger.formatText(text, ...)
-- TUM.logger.info(text, ...)
-- TUM.logger.print(level, text)
-- TUM.logger.splitText(text)
-- TUM.logger.trace(text, ...)
-- TUM.logger.warn(text, ...)
-- ====================================================================================
TUM.logger = {}
TUM.logger.logLevel = {
TRACE = -2,
DEBUG = -1,
INFO = 0,
WARNING = 1,
ERROR = 2
}
function TUM.logger.debug(text, ...)
if TUM.DEBUG_MODE then
text = TUM.logger.formatText(text, arg)
TUM.logger.print(TUM.logger.logLevel.DEBUG, text)
end
end
function TUM.logger.error(text, ...)
text = TUM.logger.formatText(text, arg)
local mText = text
if debug and debug.traceback then
mText = mText .. "\n" .. debug.traceback()
end
TUM.logger.print(TUM.logger.logLevel.ERROR, mText)
end
function TUM.logger.formatText(text, ...)
if not text then
return ""
end
if type(text) ~= 'string' then
text = TUM.p(text)
else
local args = ...
if args and args.n and args.n > 0 then
local pArgs = {}
for i=1,args.n do
pArgs[i] = TUM.p(args[i])
end
text = text:format(unpack(pArgs))
end
end
local fName = nil
local cLine = nil
if debug and debug.getinfo then
local dInfo = debug.getinfo(3)
fName = dInfo.name
cLine = dInfo.currentline
-- local fsrc = dinfo.short_src
--local fLine = dInfo.linedefined
end
if fName and cLine then
return fName .. '|' .. cLine .. ': ' .. text
elseif cLine then
return cLine .. ': ' .. text
else
return ' ' .. text
end
end
function TUM.logger.info(text, ...)
text = TUM.logger.formatText(text, arg)
TUM.logger.print(TUM.logger.logLevel.INFO, text)
end
function TUM.logger.print(level, text)
local texts = TUM.logger.splitText(text)
local levelChar = 'E'
local logFunction = function(messageForLogfile, messageForUser)
trigger.action.outText("ERROR: "..messageForUser, 3600)
env.error(messageForLogfile)
end
if level == TUM.logger.logLevel.WARNING then
levelChar = 'W'
logFunction = function(messageForLogfile, messageForUser)
trigger.action.outText("WARNING: "..messageForUser, 10)
env.warning(messageForLogfile)
end
elseif level == TUM.logger.logLevel.INFO then
levelChar = 'I'
logFunction = function(messageForLogfile, messageForUser)
if TUM.DEBUG_MODE then -- Info messages are only printed out if debug mode is enabled
trigger.action.outText(messageForUser, 3)
end
env.info(messageForLogfile)
end
elseif level == TUM.logger.logLevel.DEBUG then
levelChar = 'D'
logFunction = env.info
elseif level == TUM.logger.logLevel.TRACE then
levelChar = 'T'
logFunction = env.info
end
for i = 1, #texts do
if i == 1 then
local theText = 'TUM|' .. levelChar .. '|' .. texts[i]
logFunction(theText, texts[i])
else
local theText = texts[i]
logFunction(theText, theText)
end
end
end
function TUM.logger.splitText(text)
local tbl = {}
while text:len() > 4000 do
local sub = text:sub(1, 4000)
text = text:sub(4001)
table.insert(tbl, sub)
end
table.insert(tbl, text)
return tbl
end
function TUM.logger.trace(text, ...)
if TUM.DEBUG_MODE then
text = TUM.logger.formatText(text, arg)
TUM.logger.print(TUM.logger.logLevel.TRACE, text)
end
end
function TUM.logger.warn(text, ...)
text = TUM.logger.formatText(text, arg)
TUM.logger.print(TUM.logger.logLevel.WARNING, text)
end
+25 -11
View File
@@ -62,17 +62,18 @@ do
closeMission(true)
TUM.intermission.removeMissionZonesMarkers()
TUM.objectivesMaker.clear()
for _=1,TUM.settings.getValue(TUM.settings.id.TARGET_COUNT) do
TUM.objectives.add()
end
if TUM.objectives.getCount() == 0 then
TUM.log("Couldn't create any objective, mission creation failed.", TUM.logLevel.WARNING)
TUM.log("Couldn't create any objective, mission creation failed.", TUM.logger.logLevel.WARNING)
closeMission(true)
return
end
TUM.supportAWACS.create() -- Create the AWACS aircraft if it wasn't airborne already
-- TUM.supportAWACS.create() -- Create the AWACS aircraft if it wasn't airborne already
TUM.enemyAirDefense.create() -- Must be called once objectives have been created
TUM.airForce.create() -- Must be called once objectives have been created
TUM.missionMenu.create() -- Must be called once objectives have been created
@@ -107,7 +108,7 @@ do
end
function TUM.mission.getPlayerCallsign()
local player = world.getPlayer()
local player = DCSEx.world.getFirstPlayer(TUM.settings.getPlayerCoalition())
if player then return player:getCallsign() end
return "Flight"
end
@@ -193,10 +194,20 @@ do
-- @param event The DCS World event
-------------------------------------
function TUM.mission.onEvent(event)
if missionStatus == TUM.mission.status.NONE then return end
if not event.initiator then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if not event.initiator:getPlayerName() then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator is not an unit
if not event.initiator:getPlayerName() then return end -- Initiator is not a player
-- Start mission (it wasn't started yet) when all players have taken off
if event.id == world.event.S_EVENT_TAKEOFF then
if missionStatus == TUM.mission.status.NONE and #DCSEx.world.getPlayersOnGround(TUM.settings.getPlayerCoalition()) == 0 then
TUM.intermission.doCommandStartMission(true)
-- Force wingman spawning because the "on player takeoff spawn wingman" function won't be called as mission wasn't started yet when the "take off" even was raised
timer.scheduleFunction(TUM.wingmen.create, nil, timer.getTime() + 3)
end
end
if missionStatus == TUM.mission.status.NONE then return end
-- All objectives complete and all players on the ground? Mission is complete
if event.id == world.event.S_EVENT_RUNWAY_TOUCH or event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT then
@@ -205,11 +216,14 @@ do
end
end
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end
-- When player dies in single-player, fail the mission
if event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_EJECTION or event.id == world.event.S_EVENT_PILOT_DEAD then
TUM.mission.endMission(TUM.mission.endCause.FAILED)
-- When the player dies in single-player, remind them that they can respawn
-- (because no one knows the "respawn" shortcut key in DCS and it's not possible to respawn by
-- changing slots when there's only one)
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then
if event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_EJECTION or event.id == world.event.S_EVENT_PILOT_DEAD then
-- TUM.mission.endMission(TUM.mission.endCause.FAILED)
trigger.action.outText("Your aircraft has been downed.\nPress Right CTRL+Right Shift+Tab (default) to respawn.", 10)
end
end
end
+10 -10
View File
@@ -17,7 +17,7 @@ do
local function doCommandNearestAirbase()
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerATCRequireNearestAirbase", nil, TUM.mission.getPlayerCallsign(), false)
TUM.atc.requestNavAssistanceToAirbase(false)
TUM.atc.requestNavAssistanceToAirbase(true)
end
local function doCommandObjectiveLocation(index)
@@ -30,17 +30,17 @@ do
local function doCommandWeatherUpdate()
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerATCWeatherUpdate", nil, TUM.mission.getPlayerCallsign(), false)
TUM.atc.requestWeatherUpdate(false)
TUM.atc.requestWeatherUpdate(true)
end
function TUM.missionMenu.create()
missionCommands.removeItem() -- Clear the menu
missionCommands.addCommand("☱ Mission status", nil, doCommandMissionStatus, nil)
local rootMenu = TUM.getOrCreateRootMenu(true) -- Clear the menu
missionCommands.addCommand("☱ Mission status", rootMenu, doCommandMissionStatus, nil)
local objectivesMenuRoot = missionCommands.addSubMenu("❖ Objectives")
local navigationMenuRoot = missionCommands.addSubMenu("➽ Navigation")
-- missionCommands.addCommand("Nav to nearest airbase", navigationMenuRoot, doCommandNearestAirbase, nil)
local objectivesMenuRoot = missionCommands.addSubMenu("❖ Objectives", rootMenu)
local navigationMenuRoot = missionCommands.addSubMenu("➽ Navigation", rootMenu)
missionCommands.addCommand("Nav to nearest airbase", navigationMenuRoot, doCommandNearestAirbase, nil)
for i=1,TUM.objectives.getCount() do
local obj = TUM.objectives.getObjective(i)
if obj then
@@ -51,16 +51,16 @@ do
missionCommands.addCommand("Nav to objective "..objNameAndDescription, navigationMenuRoot, doCommandObjectiveLocation, i)
end
end
-- missionCommands.addCommand("Weather update", navigationMenuRoot, doCommandWeatherUpdate, nil)
missionCommands.addCommand("Weather update", navigationMenuRoot, doCommandWeatherUpdate, nil)
TUM.wingmenMenu.create()
TUM.supportAWACS.createMenu()
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then -- If not multiplayer, add "show mission score" command
missionCommands.addCommand("★ Display mission score", nil, TUM.playerScore.showScore, nil)
missionCommands.addCommand("★ Display mission score", rootMenu, TUM.playerScore.showScore, nil)
end
local abortRoot = missionCommands.addSubMenu("⬣ Abort mission")
local abortRoot = missionCommands.addSubMenu("⬣ Abort mission", rootMenu)
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) and DCSEx.io.canReadAndWrite() then
missionCommands.addCommand("✓ Confirm (all xp since last landing will be lost!)", abortRoot, doCommandAbortMission, nil)
else
+4 -3
View File
@@ -15,7 +15,6 @@ do
-- @param event A DCS World event, possibly a S_EVENT_LAND event
-------------------------------------
local function removeAIAircraftOnLandEvent(event)
if event.id ~= world.event.S_EVENT_LAND then return end
if not event.initiator then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Not an unit
if event.initiator:getPlayerName() then return end -- Don't remove player aircraft, that would cause horrendous bugs
@@ -55,7 +54,7 @@ do
local u = DCSEx.world.getUnitByID(id)
if u then u:destroy() end
end
TUM.log("Removed "..tostring(#aiWingMenToRemove).." AI wingmen from the mission.\nPlease do not add AI wingmen to the mission, The Universal Mission uses its own wingman system.", TUM.logLevel.WARNING)
TUM.log("Removed "..tostring(#aiWingMenToRemove).." AI wingmen from the mission.\nPlease do not add AI wingmen to the mission, The Universal Mission relies on its own wingman system.", TUM.logger.logLevel.WARNING)
end
end
@@ -73,6 +72,8 @@ do
-- @param event The DCS World event
-------------------------------------
function TUM.mizCleaner.onEvent(event)
removeAIAircraftOnLandEvent(event)
if event.id == world.event.S_EVENT_LAND and event.place then
removeAIAircraftOnLandEvent(event)
end
end
end
+35 -4
View File
@@ -25,7 +25,7 @@ do
local objective = TUM.objectivesMaker.create()
if not objective then
TUM.log("Failed to spawn a group for objective #"..tostring(#objectives + 1)..".", TUM.logLevel.WARNING)
TUM.log("Failed to spawn a group for objective #"..tostring(#objectives + 1)..".", TUM.logger.logLevel.WARNING)
return false
end
@@ -168,13 +168,44 @@ do
end
local function onObjectiveEvent(index, event)
if not event.initiator then return end
if index < 1 or index > #objectives then return end -- Out of bounds
if objectives[index].completed then return end -- Objective already completed
if event.id ~= world.event.S_EVENT_DEAD and event.id ~= world.event.S_EVENT_UNIT_LOST then return end
if not event.initiator then return end
local completionEvent = Library.tasks[objectives[index].taskID].completionEvent
if objectives[index].isSceneryTarget then
if completionEvent == DCSEx.enums.taskEvent.DAMAGE then
if event.id ~= world.event.S_EVENT_DEAD and event.id ~= world.event.S_EVENT_HIT and event.id ~= world.event.S_EVENT_UNIT_LOST then return end
elseif completionEvent == DCSEx.enums.taskEvent.DESTROY then
if event.id ~= world.event.S_EVENT_DEAD and event.id ~= world.event.S_EVENT_UNIT_LOST then return end
elseif completionEvent == DCSEx.enums.taskEvent.LAND then
if event.id ~= world.event.S_EVENT_LAND then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if event.initiator:getDesc().category ~= Unit.Category.HELICOPTER then return end
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end
if DCSEx.math.getDistance2D(DCSEx.math.vec3ToVec2(event.initiator:getPoint()), objectives[index].point2) > 500 then return end -- Too far from objective
-- Remove target group if it exists (to simulate it was picked up/captured)
if objectives[index].groupID then
local targetGroup = DCSEx.world.getGroupByID(objectives[index].groupID)
if targetGroup then
targetGroup:destroy()
end
end
timer.scheduleFunction(markObjectiveAsComplete, index, timer.getTime() + 3)
updateObjectiveText(index)
return
end
if objectives[index].isAirbaseTarget then
if Object.getCategory(event.initiator) == Object.Category.BASE then
if DCSEx.math.isSamePoint(event.initiator:getPoint(), objectives[index].point3) then
timer.scheduleFunction(markObjectiveAsComplete, index, timer.getTime() + 3)
end
end
elseif objectives[index].isSceneryTarget then
if Object.getCategory(event.initiator) == Object.Category.SCENERY then
if DCSEx.math.isSamePoint(event.initiator:getPoint(), objectives[index].point3) then
timer.scheduleFunction(markObjectiveAsComplete, index, timer.getTime() + 3)
@@ -6,6 +6,8 @@
TUM.objectivesMaker = {}
do
local usedParkingSpots = {}
local function pickRandomTask()
local taskFamily = TUM.settings.getValue(TUM.settings.id.TASKING)
@@ -41,24 +43,30 @@ do
return possiblePoints[1]
end
function TUM.objectivesMaker.clear()
usedParkingSpots = {}
end
function TUM.objectivesMaker.create()
local zone = DCSEx.zones.getByName(TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true))
local taskID = pickRandomTask()
if not taskID then
TUM.log("Failed to find a valid task.", TUM.logLevel.WARNING)
TUM.log("Failed to find a valid task.", TUM.logger.logLevel.WARNING)
return nil
end
local objectiveDB = Library.tasks[taskID]
local parkingInfo = nil
local spawnPoint2 = nil
local spawnPoint3 = nil
local isAirbaseTarget = false
local isSceneryTarget = false
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.SCENERY_TARGET) then
local validSceneries = DCSEx.world.getSceneriesInZone(zone, DCSEx.zones.getRadius(zone), 250)
if not validSceneries or #validSceneries == 0 then
TUM.log("Failed to find a valid scenery object to use as target.", TUM.logLevel.WARNING)
TUM.log("Failed to find a valid scenery object to use as target.", TUM.logger.logLevel.WARNING)
return nil
end
@@ -66,6 +74,40 @@ do
spawnPoint3 = DCSEx.table.deepCopy(pickedScenery:getPoint())
spawnPoint2 = DCSEx.math.vec3ToVec2(spawnPoint3)
isSceneryTarget = true
elseif DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.AIRBASE_TARGET) then
local validAirbases = DCSEx.zones.getAirbases(zone, TUM.settings.getEnemyCoalition())
if #validAirbases == 0 then
TUM.log("Failed to find a valid airbase to use as target.", TUM.logger.logLevel.WARNING)
return nil
end
local pickedAirbase = DCSEx.table.getRandom(validAirbases)
spawnPoint3 = DCSEx.table.deepCopy(pickedAirbase:getPoint())
spawnPoint2 = DCSEx.math.vec3ToVec2(spawnPoint3)
isAirbaseTarget = true
elseif DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET) then
local validAirbases = DCSEx.zones.getAirbases(zone, TUM.settings.getEnemyCoalition())
if #validAirbases == 0 then
TUM.log("Failed to find a valid airbase to use as target.", TUM.logger.logLevel.WARNING)
return nil
end
local pickedAirbase = DCSEx.table.getRandom(validAirbases)
local parkings = pickedAirbase:getParking()
local validParkings = {}
for _,p in pairs(parkings) do
local parkingUniqueID = pickedAirbase:getID() * 10000 + p.Term_Index
if p.Term_Type == 104 and not DCSEx.table.contains(usedParkingSpots, parkingUniqueID) then
table.insert(validParkings, p)
end
end
if #validParkings == 0 then
TUM.log("Failed to find a valid airbase parking to spawn a target.", TUM.logger.logLevel.WARNING)
return nil
end
local pickedParking = DCSEx.table.getRandom(validParkings)
table.insert(usedParkingSpots, pickedAirbase:getID() * 10000 + pickedParking.Term_Index) -- Mark parking spot as used so it won't be taken by another objective
parkingInfo = { airbaseID = pickedAirbase:getID(), parkingID = pickedParking.Term_Index }
spawnPoint3 = pickedParking.vTerminalPos
spawnPoint2 = DCSEx.math.vec3ToVec2(spawnPoint3)
elseif objectiveDB.surfaceType == land.SurfaceType.WATER then
spawnPoint2 = pickWaterPoint(zone)
if not spawnPoint2 then
@@ -76,7 +118,7 @@ do
end
if not spawnPoint2 then
TUM.log("Failed to find a spawn point for objective.", TUM.logLevel.WARNING)
TUM.log("Failed to find a spawn point for objective.", TUM.logger.logLevel.WARNING)
return nil
end
@@ -89,10 +131,12 @@ do
local objective = {
completed = false,
completedUnitsID = {},
isAirbaseTarget = isAirbaseTarget,
isSceneryTarget = isSceneryTarget,
markerID = DCSEx.world.getNextMarkerID(),
markerTextID = DCSEx.world.getNextMarkerID(),
name = Library.objectiveNames.get():upper(),
parkingInfo = parkingInfo,
point2 = DCSEx.table.deepCopy(spawnPoint2),
point3 = DCSEx.table.deepCopy(spawnPoint3),
preciseCoordinates = objectiveDB.waypointInaccuracy <= 0,
@@ -108,7 +152,7 @@ do
objective.waypoint3 = DCSEx.math.vec2ToVec3(objective.waypoint2, "land")
end
if not DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.SCENERY_TARGET) then
if not isAirbaseTarget and not isSceneryTarget then
-- Check group options
local groupOptions = {}
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.MOVING) then
@@ -123,7 +167,26 @@ do
end
end
local units = Library.factions.getUnits(TUM.settings.getEnemyFaction(), objectiveDB.targetFamilies, math.random(objectiveDB.targetCount[1], objectiveDB.targetCount[2]))
-- Parked aircraft only
if parkingInfo then
groupOptions.airbaseID = parkingInfo.airbaseID
groupOptions.invisible = true -- Not ideal because wingmen can't be tasked with attacking targets, but only way I've found to prevent friendly CAP from attacking parked aircraft
groupOptions.parkingID = parkingInfo.parkingID
end
-- Target group belongs to the enemy coalition, unless DCSEx.enums.taskFlag.FRIENDLY_TARGET is set
local groupCoalition = TUM.settings.getEnemyCoalition()
local groupFaction = TUM.settings.getEnemyFaction()
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.FRIENDLY_TARGET) then
groupCoalition = TUM.settings.getPlayerCoalition()
groupFaction = TUM.settings.getPlayerFaction()
-- Friendly target groups are immortal and invisible, so AI won't kill them before the player got a chance to interact with them
groupOptions.immortal = true
groupOptions.invisible = true
end
local units = Library.factions.getUnits(groupFaction, objectiveDB.targetFamilies, math.random(objectiveDB.targetCount[1], objectiveDB.targetCount[2]))
local groupInfo = nil
if objectiveDB.targetFamilies[1] == DCSEx.enums.unitFamily.STATIC_STRUCTURE then
@@ -132,11 +195,11 @@ do
groupInfo.unitsID = { DCSEx.unitGroupMaker.createStatic(TUM.settings.getEnemyCoalition(), objective.point2, units[1], "") }
end
else
groupInfo = DCSEx.unitGroupMaker.create(TUM.settings.getEnemyCoalition(), DCSEx.dcs.getUnitCategoryFromFamily(objectiveDB.targetFamilies[1]), objective.point2, units, groupOptions)
groupInfo = DCSEx.unitGroupMaker.create(groupCoalition, DCSEx.dcs.getUnitCategoryFromFamily(objectiveDB.targetFamilies[1]), objective.point2, units, groupOptions)
end
if not groupInfo then
TUM.log("Failed to spawn a group for objective.", TUM.logLevel.WARNING)
TUM.log("Failed to spawn a group for objective.", TUM.logger.logLevel.WARNING)
return nil
end
objective.groupID = groupInfo.groupID
@@ -146,10 +146,11 @@ do
-- Appends the career menu to the F10 menu. Only works in single-player missions
-------------------------------------
function TUM.playerCareer.createMenu()
local rootMenu = TUM.getOrCreateRootMenu()
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No career in multiplayer
missionCommands.addCommand("✪ View pilot career stats", nil, TUM.playerCareer.displayMedalBox, true)
missionCommands.addCommand("✪ View pilot career stats", rootMenu, TUM.playerCareer.displayMedalBox, true)
end
-------------------------------------
@@ -273,7 +274,7 @@ do
msg = msg.."To enable the IO module, comment or remove the \"sanitizeModule('io')\" line in \n"
msg = msg.."[DCSWorld installation directory]\\Scripts\\MissionScripting.lua and restart the game."
TUM.log(msg, TUM.logLevel.WARNING)
TUM.log(msg, TUM.logger.logLevel.WARNING)
end
return true
+16 -4
View File
@@ -55,7 +55,12 @@ do
if not objectDesc or not objectDesc.attributes then return 10 end -- No description, assume a default value of 10 points
local groundMultiplier = 1
if not killedObject:inAir() then groundMultiplier = 0.5 end -- Aircraft killed on the ground are worth less points
if not killedObject:inAir() then
-- Aircraft killed on the ground are worth less points, except AWACS, bombers and transports
if not objectDesc.attributes["AWACS"] and not objectDesc.attributes["Transports"] and not objectDesc.attributes["Strategic bombers"] then
groundMultiplier = 0.5
end
end
-- Misc
if objectDesc.attributes["Missiles"] then return 10 end
@@ -68,8 +73,8 @@ do
if objectDesc.attributes["Planes"] then return math.floor(25 * groundMultiplier) end
-- Rotary wing
if objectDesc.attributes["Attack helicopters"] then return math.floor(30 * groundMultiplier) end
if objectDesc.attributes["Helicopters"] then return math.floor(25 * groundMultiplier) end
if objectDesc.attributes["Attack helicopters"] then return math.floor(25 * groundMultiplier) end
if objectDesc.attributes["Helicopters"] then return math.floor(15 * groundMultiplier) end
-- Default air
if objectDesc.attributes["Air"] then return math.floor(20 * groundMultiplier) end
@@ -295,11 +300,18 @@ do
if not DCSEx.io.canReadAndWrite() then return 1.0 end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 1.0 end -- No scoring in multiplayer
-- Base XP multiplier is 1.0 (100%)
local scoreMultiplier = 1.0
-- Add XP multipliers for game settings
for _,v in pairs(TUM.settings.id) do
scoreMultiplier = scoreMultiplier + (TUM.playerScore.getScoreMultiplier(v) or 0.0)
end
-- Add XP multipliers for weather
scoreMultiplier = scoreMultiplier + TUM.weather.getWeatherXPModifier()
scoreMultiplier = scoreMultiplier + TUM.weather.getWindXPModifier()
return math.max(0.0, scoreMultiplier)
end
@@ -335,7 +347,7 @@ do
return
end
if event.id == world.event.S_EVENT_LAND then
if event.id == world.event.S_EVENT_LAND and event.place then
onLandEvent(event)
return
end
+13 -8
View File
@@ -50,7 +50,7 @@ do
end
message = DCSEx.string.firstToUpper(message)
local duration = DCSEx.string.getReadingTime(message)
local duration = DCSEx.string.getReadingTime(message) * args.displayTimeMultiplier
-- Print message
trigger.action.outTextForUnit(args.unitID, callsign:upper()..": "..message, duration, false)
@@ -73,12 +73,13 @@ do
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-- @param displayTimeMultiplier Multiplier for how long the message should be displayed (2.0=twice as long, 0.5=half as long). Default is 1.0
-------------------------------------
function TUM.radio.playForAll(messageID, replacements, callsign, delayed, functionToRun, functionParameters)
function TUM.radio.playForAll(messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
local players = DCSEx.world.getAllPlayers()
for _, unit in pairs(players) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(unit), messageID, replacements, callsign, delayed, functionToRun, functionParameters)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(unit), messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
end
end
@@ -91,12 +92,13 @@ do
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-- @param displayTimeMultiplier Multiplier for how long the message should be displayed (2.0=twice as long, 0.5=half as long). Default is 1.0
-------------------------------------
function TUM.radio.playForCoalition(coalitionID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
function TUM.radio.playForCoalition(coalitionID, messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
local players = coalition.getPlayers(coalitionID)
for _,u in pairs(players) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(u), messageID, replacements, callsign, delayed, functionToRun, functionParameters)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(u), messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
end
end
@@ -109,13 +111,14 @@ do
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-- @param displayTimeMultiplier Multiplier for how long the message should be displayed (2.0=twice as long, 0.5=half as long). Default is 1.0
-------------------------------------
function TUM.radio.playForGroup(groupID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
function TUM.radio.playForGroup(groupID, messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
local group = DCSEx.world.getGroupByID(groupID)
if not group then return end -- group does not exist
for _,u in pairs(group:getUnits()) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(u), messageID, replacements, callsign, delayed, functionToRun, functionParameters)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(u), messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
end
end
@@ -129,8 +132,9 @@ do
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-- @param displayTimeMultiplier Multiplier for how long the message should be displayed (2.0=twice as long, 0.5=half as long). Default is 1.0
-------------------------------------
function TUM.radio.playForUnit(unitID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
function TUM.radio.playForUnit(unitID, messageID, replacements, callsign, delayed, functionToRun, functionParameters, displayTimeMultiplier)
if not messageID then return end
if not Library.radioMessages[messageID] then return end
delayed = delayed or false
@@ -141,6 +145,7 @@ do
local radioArgs = {
callsign = callsign,
displayTimeMultiplier = math.max(0.1, displayTimeMultiplier or 1.0),
functionToRun = functionToRun,
functionParameters = functionParameters,
messageID = messageID,
+19 -14
View File
@@ -52,11 +52,14 @@ do
[TUM.settings.id.PLAYER_COALITION] = { "Red", "Blue" }, -- Must match values in the coalition.side enum
[TUM.settings.id.TARGET_COUNT] = { "1", "2", "3", "4" },
[TUM.settings.id.TARGET_LOCATION] = { },
[TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Interception", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum
[TUM.settings.id.TIME_PERIOD] = { "World War 2", "Korea War", "Vietnam War", "Late Cold War", "Gulf war", "Modern" }, -- Must match values in the DCSEx.enums.timePeriod enum
-- [TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Helicopter-specific tasks", "Helo hunt", "Interception", "Offensive counter-air", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum
[TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Helo hunt", "Interception", "Offensive counter-air", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum
[TUM.settings.id.TIME_PERIOD] = { "World War 2", "Korea War", "Vietnam War", "Late Cold War", "Modern" }, -- Must match values in the DCSEx.enums.timePeriod enum
[TUM.settings.id.WINGMEN] = { "None", "1", "2", "3" }
}
local targetLocation
local function getFaction(side)
if side == coalition.side.BLUE then
return TUM.settings.getValue(TUM.settings.id.COALITION_BLUE, true)
@@ -81,20 +84,18 @@ do
[TUM.settings.id.WINGMEN] = 2
}
-- TODO: enable WW2
-- TODO: set default time period according to mission year
-- if env.mission.date.Year <= 1945 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.WORLD_WAR_2
if env.mission.date.Year < 1960 then
settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.KOREA_WAR
elseif env.mission.date.Year < 1975 then
settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.VIETNAM_WAR
elseif env.mission.date.Year < 1990 then
settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.COLD_WAR
elseif env.mission.date.Year < 2000 then
settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.GULF_WAR
else
settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.MODERN
end
-- elseif env.mission.date.Year < 1960 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.KOREA_WAR
-- elseif env.mission.date.Year < 1975 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.VIETNAM_WAR
-- elseif env.mission.date.Year < 1990 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.COLD_WAR
-- else
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.MODERN
-- end
for i,id in pairs(SETTING_VALUES[TUM.settings.id.COALITION_BLUE]) do
if id == Library.factions.defaults[coalition.side.BLUE] then settings[TUM.settings.id.COALITION_BLUE] = i end
@@ -193,6 +194,10 @@ do
end
end
-- Weather
summary = summary.."\n\nWEATHER: "..TUM.weather.getWeatherName().." (+"..tostring(math.ceil(TUM.weather.getWeatherXPModifier() * 100)).."% xp)"
summary = summary.."\nWIND: "..DCSEx.string.firstToUpper(TUM.weather.getWindName()).." (+"..tostring(math.ceil(TUM.weather.getWindXPModifier() * 100)).."% xp)"
if showScoreMultiplier then
summary = summary.."\n\nTotal XP modifier: "..tostring(math.ceil(TUM.playerScore.getTotalScoreMultiplier() * 100)).."%"
end
+12 -2
View File
@@ -43,6 +43,15 @@ do
end
end
end
detectedGroups = coalition.getGroups(TUM.settings.getEnemyCoalition(), Group.Category.HELICOPTER)
for _,g in pairs(detectedGroups) do
local units = g:getUnits()
for _,u in pairs(units) do
if u:inAir() then
table.insert(detectedAircraft, u)
end
end
end
-- No aircraft on picture
if #detectedAircraft == 0 then
@@ -91,8 +100,9 @@ do
function TUM.supportAWACS.createMenu()
if not awacsGroupID then return end -- No AWACS
local rootMenu = TUM.getOrCreateRootMenu()
local rootPath = missionCommands.addSubMenu("⌾ Awacs")
local rootPath = missionCommands.addSubMenu("⌾ Awacs", rootMenu)
missionCommands.addCommand("Bogey dope", rootPath, doCommandBogeyDope, nil)
missionCommands.addCommand("Picture", rootPath, doCommandPicture, nil)
end
@@ -131,7 +141,7 @@ do
end
TUM.log("Spawned AWACS aircraft")
else
TUM.log("Failed to create AWACS aircraft", TUM.logLevel.WARNING)
TUM.log("Failed to create AWACS aircraft", TUM.logger.logLevel.WARNING)
end
else
TUM.log("No AWACS aircraft available")
+1 -1
View File
@@ -60,7 +60,7 @@ do
return
end
if obj.isSceneryTarget then
if obj.isAirbaseTarget or obj.isSceneryTarget then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeOK", { jtacName[index], smokeColorName }, jtacName[index], true, spawnSmoke, { point3 = obj.point3, smokeColor = smokeColor })
else
for _,id in ipairs(obj.unitsID) do
+26 -6
View File
@@ -125,7 +125,27 @@ do
elseif DCSEx.string.startsWith(z.name:lower(), "water") then
table.insert(waterZones, z)
else
table.insert(missionZones, z)
local onlyZonesStartingWith = TUM.administrativeSettings.getValue(TUM.administrativeSettings.ONLY_ZONES_STARTINGWITH)
if onlyZonesStartingWith and #onlyZonesStartingWith > 0 then
if type(onlyZonesStartingWith) ~= "table" then
onlyZonesStartingWith = { onlyZonesStartingWith }
end
for _, zonePrefix in ipairs(onlyZonesStartingWith) do
if DCSEx.string.startsWith(z.name:lower(), zonePrefix:lower()) then
table.insert(missionZones, z)
break
end
end
else
local ignoreZonesStartingWith = TUM.administrativeSettings.getValue(TUM.administrativeSettings.IGNORE_ZONES_STARTINGWITH)
if ignoreZonesStartingWith then
if not DCSEx.string.startsWith(z.name:lower(), ignoreZonesStartingWith:lower()) then
table.insert(missionZones, z)
end
else
table.insert(missionZones, z)
end
end
end
end
@@ -135,18 +155,18 @@ do
local zoneName = "BLUFOR"
if side == 1 then zoneName = "REDFOR" end
TUM.log("Coalition "..name.." has no territory zones and/or controls no airfields. Please add zone with a name starting with "..zoneName.." in the mission editor and make sure at least one contains an airbase.", TUM.logLevel.ERROR)
TUM.log("Coalition "..name.." has no territory zones and/or controls no airfields. Please add zone with a name starting with "..zoneName.." in the mission editor and make sure at least one contains an airbase.", TUM.logger.logLevel.ERROR)
return false
end
end
if #missionZones == 0 then
TUM.log("No mission zones found. Create at least one mission zone in the mission editor.", TUM.logLevel.ERROR)
TUM.log("No mission zones found. Create at least one mission zone in the mission editor.", TUM.logger.logLevel.ERROR)
return false
end
if #missionZones > 10 then
TUM.log("Too many mission zones, extra zones removed.", TUM.logLevel.WARNING)
TUM.log("Too many mission zones, extra zones removed.", TUM.logger.logLevel.WARNING)
while #missionZones > 10 do
table.remove(missionZones, 11)
end
@@ -157,10 +177,10 @@ do
-- zones[coalition.side.RED] = DCSEx.zones.getByName("REDFOR")
-- if not zones[coalition.side.BLUE] then
-- TUM.log("BLUFOR zone not found.", TUM.logLevel.ERROR)
-- TUM.log("BLUFOR zone not found.", TUM.logger.logLevel.ERROR)
-- return false
-- elseif not zones[coalition.side.RED] then
-- TUM.log("REDFOR zone not found.", TUM.logLevel.ERROR)
-- TUM.log("REDFOR zone not found.", TUM.logger.logLevel.ERROR)
-- return false
-- end
+322
View File
@@ -0,0 +1,322 @@
-- ====================================================================================
-- TUM.WEATHER - HANDLES THE MISSION'S WEATHER SETTINGS
-- ====================================================================================
-- ====================================================================================
TUM.weather = {}
do
local cloudPresets = {
-- "Light Scattered 1",
Preset1 = {
readableName = "Few scattered clouds (METAR: FEW/SCT 7/8)",
readableNameShort = "Light scattered",
xpBonus = 0,
},
-- "Light Scattered 2",
Preset2 = {
readableName = "Two layers few and scattered (METAR: FEW/SCT 8/10 SCT 23/24)",
readableNameShort = "Light scattered",
xpBonus = 0,
},
-- "High Scattered 1",
Preset3 = {
readableName = "Two layers scattered (METAR: SCT 8/9 FEW 21)",
readableNameShort = "High Scattered",
xpBonus = 0,
},
-- "High Scattered 2",
Preset4 = {
readableName = "Two layers scattered (METAR: SCT 8/10 FEW/SCT 24/26)",
readableNameShort = "High Scattered",
xpBonus = 0,
},
-- "Scattered 1",
Preset5 = {
readableName = "Three layers high altitude scattered (METAR: SCT 14/17 FEW 27/29 BKN 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 2",
Preset6 = {
readableName = "One layer scattered/broken (METAR: SCT/BKN 8/10 FEW 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 3",
Preset7 = {
readableName = "Two layers scattered/broken (METAR: BKN 7.5/12 SCT/BKN 21/23 SCT 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "High Scattered 3",
Preset8 = {
readableName = "Two layers scattered/broken high altitude (METAR: SCT/BKN 18/20 FEW 36/38 FEW 40)",
readableNameShort = "High Scattered",
xpBonus = 0,
},
-- "Scattered 4",
Preset9 = {
readableName = "Two layers broken/scattered (METAR: BKN 7.5/10 SCT 20/22 FEW41)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 5",
Preset10 = {
readableName = "Two layers scattered large thick clouds (METAR: SCT/BKN 18/20 FEW36/38 FEW 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 6",
Preset11 = {
readableName = "Two layers scattered large clouds high ceiling (METAR: BKN 18/20 BKN 32/33 FEW 41)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 7",
Preset12 = {
readableName = "Two layers scattered large clouds high ceiling (METAR: BKN 12/14 SCT 22/23 FEW 41)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Broken 1",
Preset13 = {
readableName = "Two layers broken clouds (METAR: BKN 12/14 BKN 26/28 FEW 41)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 2",
Preset14 = {
readableName = "Broken thick low layer with few high layers\nMETAR: BKN LYR 7/16 FEW 41)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 3",
Preset15 = {
readableName = "Two layers broken large clouds (METAR: SCT/BKN 14/18 BKN 24/27 FEW 40)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 4",
Preset16 = {
readableName = "Two layers broken large clouds (METAR: BKN 14/18 BKN 28/30 FEW 40)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 5",
Preset17 = {
readableName = "Three layers broken/overcast (METAR: BKN/OVC LYR 7/13 20/22 32/34)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 6",
Preset18 = {
readableName = "Three layers broken/overcast (METAR: BKN/OVC LYR 13/15 25/29 38/41)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 7",
Preset19 = {
readableName = "Three layers overcast at low level (METAR: OVC 9/16 BKN/OVC LYR 23/24 31/33)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 8",
Preset20 = {
readableName = "Three layers overcast low level (METAR: BKN/OVC 13/18 BKN 28/30 SCT FEW 38)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Overcast 1",
Preset21 = {
readableName = "Overcast low level (METAR: BKN/OVC LYR 7/8 17/19)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 2",
Preset22 = {
readableName = "Overcast low level (METAR: BKN LYR 7/10 17/20)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 3",
Preset23 = {
readableName = "Three layers broken low level scattered high (METAR: BKN LYR 11/14 18/25 SCT 32/35)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 4",
Preset24 = {
readableName = "Three layers overcast (METAR: BKN/OVC 3/7 17/22 BKN 34)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 5",
Preset25 = {
readableName = "Three layers overcast (METAR: OVC LYR 12/14 22/25 40/42)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 6",
Preset26 = {
readableName = "Three layers overcast (METAR: OVC 9/15 BKN 23/25 SCT 32)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 7",
Preset27 = {
readableName = "Three layer overcast (METAR: OVC 8/15 SCT/BKN 25/26 34/36)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast And Rain 1",
RainyPreset1 = {
readableName = "Overcast with rain (METAR: VIS 3-5KM RA OVC 3/15 28/30 FEW 40)",
readableNameShort = "Overcast and rain",
xpBonus = 0.25,
},
-- "Overcast And Rain 2",
RainyPreset2 = {
readableName = "Overcast with rain (METAR: VIS 1-5KM RA BKN/OVC 3/11 SCT 18/29 FEW 40)",
readableNameShort = "Overcast and rain",
xpBonus = 0.25,
},
-- "Overcast And Rain 3",
RainyPreset3 = {
readableName = "Overcast with rain (METAR: VIS 3-5KM RA OVC LYR 6/18 19/21 SCT 34)",
readableNameShort = "Overcast and rain",
xpBonus = 0.25,
},
-- "Light Rain 1",
RainyPreset4 = {
readableName = "Two layers scattered large thick clouds (METAR: SCT/BKN 18/20 FEW36/38 FEW 40)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
-- "Light Rain 2",
RainyPreset5 = {
readableName = "Three layers broken/overcast (METAR: BKN/OVC LYR 7/13 20/22 32/34)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
-- "Light Rain 3",
RainyPreset6 = {
readableName = "Three layers overcast at low level (METAR: OVC 9/16 BKN/OVC LYR 23/24 31/33)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
-- "Light Rain 4",
NEWRAINPRESET4 = {
readableName = "Two layers overcast at low level (METAR: OVC 9/16 BKN/OVC LYR 23/24 31/33)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
}
local function getWindBeaufortScale(speedInMS)
local speedInKMH = DCSEx.converter.mpsToKmph(speedInMS or Library.environment.getWindAverage())
if speedInKMH < 1 then return 0
elseif speedInKMH <= 5 then return 1
elseif speedInKMH <= 11 then return 2
elseif speedInKMH <= 19 then return 3
elseif speedInKMH <= 28 then return 4
elseif speedInKMH <= 38 then return 5
elseif speedInKMH <= 49 then return 6
elseif speedInKMH <= 61 then return 7
elseif speedInKMH <= 74 then return 8
elseif speedInKMH <= 88 then return 9
elseif speedInKMH <= 102 then return 10
elseif speedInKMH <= 117 then return 11
else return 12
end
end
function TUM.weather.getWeatherName(presetID, longForm)
presetID = presetID or env.mission.weather.clouds.preset
longForm = longForm or false
if cloudPresets[presetID] == nil then return "Unknown" end
if longForm then return cloudPresets[presetID].readableName end
return cloudPresets[presetID].readableNameShort
end
function TUM.weather.getWeatherXPModifier(presetID)
presetID = presetID or env.mission.weather.clouds.preset
if cloudPresets[presetID] == nil then return 0 end
return cloudPresets[presetID].xpBonus
end
function TUM.weather.getWindName(speedInMS)
local windBeaufort = getWindBeaufortScale(speedInMS)
if windBeaufort == 0 then return "calm"
elseif windBeaufort == 1 then return "light air"
elseif windBeaufort == 2 then return "light breeze"
elseif windBeaufort == 3 then return "gentle breeze"
elseif windBeaufort == 4 then return "moderate breeze"
elseif windBeaufort == 5 then return "fresh breeze"
elseif windBeaufort == 6 then return "strong breeze"
elseif windBeaufort == 7 then return "moderate gale"
elseif windBeaufort == 8 then return "fresh gale"
elseif windBeaufort == 9 then return "strong gale"
elseif windBeaufort == 10 then return "storm"
elseif windBeaufort == 11 then return "violent storm"
elseif windBeaufort == 12 then return "hurricane"
end
end
function TUM.weather.getWindXPModifier(speedInMS)
local windBeaufort = getWindBeaufortScale(speedInMS)
if windBeaufort == 0 then return 0.00
elseif windBeaufort == 1 then return 0.02
elseif windBeaufort == 2 then return 0.04
elseif windBeaufort == 3 then return 0.08
elseif windBeaufort == 4 then return 0.10
elseif windBeaufort == 5 then return 0.12
elseif windBeaufort == 6 then return 0.15
elseif windBeaufort == 7 then return 0.18
elseif windBeaufort == 8 then return 0.21
elseif windBeaufort == 9 then return 0.24
elseif windBeaufort == 10 then return 0.27
elseif windBeaufort == 11 then return 0.30
elseif windBeaufort == 12 then return 0.33
end
end
end
+7 -5
View File
@@ -23,11 +23,11 @@ do
elseif taskingID == DCSEx.enums.taskFamily.GROUND_ATTACK then
return "attack"
-- elseif taskingID == DCSEx.enums.taskFamily.HELICOPTER then
-- elseif taskingID == DCSEx.enums.taskFamily.HELO_HUN then
elseif taskingID == DCSEx.enums.taskFamily.HELO_HUNT then
elseif taskingID == DCSEx.enums.taskFamily.INTERCEPTION then
return "cap"
-- elseif taskingID == DCSEx.enums.taskFamily.OCA then
elseif taskingID == DCSEx.enums.taskFamily.SEAD then
-- elseif taskingID == DCSEx.enums.taskFamily.OCA then
return "sead"
-- elseif taskingID == DCSEx.enums.taskFamily.STRIKE then
-- return "strike"
@@ -48,7 +48,7 @@ do
-- Retrive player unit type
local playerTypeName = player:getTypeName()
if not Library.aircraft[playerTypeName] then
TUM.log("Cannot spawn AI wingmen, aircraft \""..playerTypeName.."\" not found in the database.", TUM.logLevel.WARNING)
TUM.log("Cannot spawn AI wingmen, aircraft \""..playerTypeName.."\" not found in the database.", TUM.logger.logLevel.WARNING)
return
end
local playerCategory = Group.Category.AIRPLANE
@@ -88,7 +88,7 @@ do
)
if not groupInfo then
TUM.log("Failed to spawn AI wingmen", TUM.logLevel.WARNING)
TUM.log("Failed to spawn AI wingmen", TUM.logger.logLevel.WARNING)
return
end
wingmenGroupID = groupInfo.groupID
@@ -177,9 +177,11 @@ do
if not event.initiator:getPlayerName() then return end
if TUM.mission.getStatus() == TUM.mission.status.NONE then return end -- Mission not in progress, no wingman needed
TUM.wingmen.create()
elseif event.id == world.event.S_EVENT_LAND then -- Remove wingmen on player landing
elseif event.id == world.event.S_EVENT_LAND and event.place then -- Remove wingmen on player landing
if not event.initiator:getPlayerName() then return end
TUM.wingmen.removeAll()
elseif event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then -- Remove wingmen when player takes control of a new unit
TUM.wingmen.removeAll()
end
end
+5 -2
View File
@@ -75,11 +75,12 @@ do
end
function TUM.wingmenMenu.create()
local rootMenu = TUM.getOrCreateRootMenu()
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No wingmen in multiplayer
if TUM.settings.getValue(TUM.settings.id.WINGMEN) <= 1 then return end -- No wingmen
local isWW2 = (TUM.settings.getValue(TUM.settings) == DCSEx.enums.timePeriod.WORLD_WAR_2) -- Some options are different when time period is WW2
local rootPath = missionCommands.addSubMenu("✈ Flight")
local rootPath = missionCommands.addSubMenu("✈ Flight", rootMenu)
missionCommands.addCommand("Cover me!", rootPath, radioCommandCoverMe, nil)
------------------------------------------------------
@@ -137,7 +138,9 @@ do
-- "Change altitude" submenu
------------------------------------------------------
local altitudePath = missionCommands.addSubMenu("Change altitude", rootPath)
local baseAltitude = DCSEx.converter.metersToFeet(Library.aircraft[world.getPlayer():getTypeName()].altitude)
local player = DCSEx.world.getFirstPlayer(TUM.settings.getPlayerCoalition())
local baseAltitude = DCSEx.converter.metersToFeet(10000)
if player then baseAltitude = DCSEx.converter.metersToFeet(Library.aircraft[player:getTypeName()].altitude) end
local altitudeFactions = { 0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5 }
for _,f in ipairs(altitudeFactions) do
local altText = DCSEx.string.toStringThousandsSeparator(math.floor((baseAltitude * f) / 100) * 100).."ft"
@@ -29,7 +29,7 @@ do
end
local function getAltitude()
local player = world.getPlayer()
local player = DCSEx.world.getFirstPlayer(TUM.settings.getPlayerCoalition())
if not player then return 600 end -- Don't care about altitude if player's dead anyway
local altitude = Library.aircraft[player:getTypeName()].altitude * cruiseAltitudeFraction
@@ -133,10 +133,16 @@ do
local function getRejoinTaskTable(formationDistance)
formationDistance = formationDistance or 800
local player = DCSEx.world.getFirstPlayer(TUM.settings.getPlayerCoalition())
local groupID = 1
if player then
groupID = DCSEx.dcs.getObjectIDAsNumber(player:getGroup()) or 1
end
return {
id = "Follow",
params = {
groupId = DCSEx.dcs.getObjectIDAsNumber(world.getPlayer():getGroup()),
groupId = groupID,
lastWptIndexFlag = false,
lastWptIndex = -1,
pos = { x = -formationDistance, y = 0, z = -formationDistance }
+8 -4
View File
@@ -5,7 +5,7 @@
User's manual
</p>
<p class="heavy">
The Universal Mission v0.2.250729<br />
The Universal Mission v0.3.250914<br />
Created and maintained by Ambroise Garel (<a href='mailto:akaagarmail@gmail.com'>akaagarmail@gmail.com</a>)
</p>
<p class="heavy">
@@ -46,7 +46,7 @@ I hope you'll like it.
- Can generate any kind of mission: ground attack, interception, strike, airbase attack, CAS, CAP, and more
- Completely dynamic, no two missions are ever the same
- Entirely self-contained inside a .miz file, no need for any external program
- More than 300 voiced radio messages for immersive and realistic coms
- More than 325 voiced radio messages for immersive and realistic coms
- Supports both single-player and small-scale PvE on closed servers
- Persistent single player career mode, with awards and promotions. Dying won't reset your progress, but you have to come back to base alive for your kills and completed objectives to be saved to your profile, so watch out for SAMs on your way home
- All new AI wingman system, smarter and more immersive than DCS's original wingmen
@@ -73,7 +73,6 @@ I hope you'll like it.
- Download the latest release from this GitHub page.
- Copy the provided autoexec.cfg file to your **[Saved Games]\DCS\Config directory**
- Please note: as of DCS 2.9.18.12899, it seems the autoexec.cfg file [is no longer needed](https://www.digitalcombatsimulator.com/en/news/changelog/release/2.9.18.12899/) but I advise you to copy it anyway, ED might change its mind again.
- Copy the .miz files for your theater(s) of choice to your **[Saved Games]\DCS\Missions directory**
- _**(Optional but strongly recommended)**_ Unsanitize the Lua IO module. You don't have to do this, but the persistent career system won't work if you don't. To do it, open the file **[DCS World installation directory]\Scripts\MissionScripting.lua** with a text editor and comment or remove the line "sanitizeModule('io')". Make sure you restart DCS World once you've modified the file.
- Please note: should you want to backup, delete or transfer it, career progress is saved in **[DCS World installation directory]\TheUniversalMission.sav**
@@ -113,7 +112,10 @@ The exact content of the menu will depend on the current phase of the mission.
- **Mission type**: What will your mission be?
- **Antiship strike**: Sink enemy warships and cargo ships.
- **Ground attack**: Interdiction missions against armor, artillery and convoys.
- **Helicopter-specific tasks**: Tasks specifically designed for helicopters (lift/pick up friendly units, suppress infantry...)
- **Helicopter hunt**: Shoot down enemy transport and attack helicopters.
- **Interception**: Shoot down strategic airplanes (bombers, transports...) and enemy attack planes on interdiction missions.
- **Offensive counter-air**: Bomb enemy airbases and destroy parked aircraft. **(Requires a target location with at least one enemy land airbase)**
- **SEAD**: Destroy enemy SAM sites.
- **Strike**: Destroy enemy structures and civilian buildings occupied by enemy forces.
- **Target location**: Where on the map will the targets be spawned? Approximate distance to possible regions is displayed in the menu.
@@ -145,7 +147,9 @@ The exact content of the menu will depend on the current phase of the mission.
- **Objectives**: Displays a list of special commands related to each of the mission's objectives. Be aware that some objectives may have no special commands associated with them.
- **Smoke marker on target**: Asks for a friendly JTAC to pop a smoke marker on the target. Makes finding the target easier, but will cost you a small XP penalty. Only available for missions where a JTAC is available (it's pretty hard to throw a smoke grenade at an airplane or a ship in the middle of the sea).
- **Navigation**: Displays a list of commands related to navigational assistance.
- **Navigation to nearest airbase**: Displays the coordinates of the nearest friendly airbase, its BRA ("fly X for Y") relative to the player's position and an estimated flight time and ETA.
- **Navigation to objective [OBJECTIVE NAME]**: Displays the coordinates of the objective, its BRA ("fly X for Y") relative to the player's position and an estimated flight time and ETA. Some objectives types (e.g. strike missions) are provided with exact coordinates, but most will only have approximate coordiantes, so you'll have to search for targets yourself once in the objective area.
- **Weather update**: Displays information about the weather (wind speed, temperature...) at the player location.
- **Flight**: Displays a list of commands for your wingmen. Only shown in single-player missions and if wingmen are available for this mission.
- **Cover me!**: Tasks your wingmen to immediately engage any nearby air threats.
- **Engage**: Tasks your wingmen to engage a certain type of targets. Targets must be detected by your wingmen (see "Report contacts" below), or they won't be able to engage them.
@@ -174,7 +178,7 @@ The Universal Mission is designed to be easily editable to suit your preferences
- Move it to another airbase, change its coalition (make sure blue players are spawned on an airbase located in a BLUFOR zone are red players are spawned on an airbase located in a REDFOR zone)
- You may also add an aircraft carrier or a FARP for the player to take off from
- Change its default loadout if you plan to play a specific kind of mission and don't want to lose time asking the ground crew to rearm your aircraft (e.g. if you know you want to play SEAD missions, you may as well stock up on AGM-88s)
- Change the skill level from "Player" to "Client" and add other aircraft to create a multiplayer mission to play with your friends. Keep in mind that the persistent career/player stats system will be disabled in multiplayer missions and that all player aircraft must belong to the same coalition (TUM does not support PvP)
- Add other aircraft to create a multiplayer mission to play with your friends. Keep in mind that the persistent career/player stats system will be disabled in multiplayer missions and that all player aircraft must belong to the same coalition (TUM does not support PvP)
<h3>Zones</h3>
+159
View File
@@ -0,0 +1,159 @@
{
"displayName": "Germany Cold War",
"dcsID": "GermanyCW",
"mapCenter": [-185764.56195212, -700000],
"mapZoom": 825958.7020649,
"dateTime": {
"day": 1,
"month": 6,
"year": 2010,
"hour": 9,
"minute": 0
},
"temperature": 20,
"airbasesIDs": [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94,
95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167,
168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185,
186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203,
204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221,
222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255
],
"bullseye": {
"blue": [-175892.0672249, -516165.19174041],
"red": [-263207.70144319, -729262.53687316]
},
"player": {
"airdromeID": 107,
"coordinates": [-213558.34375, -675577]
},
"territories": {
"blue": [
[
[-486980.39467552, -1075457.2271386],
[-118130.83715339, -637699.11504425],
[-34827.002345132, -609852.50737463],
[-55593.963997049, -1038879.0560472]
],
[
[-236596.91384956, -663657.81710914],
[-141493.6690118, -590737.46312684],
[-105387.47432153, -632507.37463127],
[-409812.25308259, -1001828.9085546]
],
[
[-570737.46312684, -715575.22123894],
[-343735.55691741, -749557.52212389],
[-227374.63126844, -748141.59292035],
[-493805.30973451, -1099056.0471976]
],
[
[-448844.43584661, -767964.60176991],
[-314331.16151033, -711799.4100295],
[-266109.53472566, -672678.25972862],
[-248049.94676106, -785324.93965782]
]
],
"red": [
[
[-103952.80235988, -627315.63421829],
[-269616.51917404, -402418.87905605],
[23716.814159291, -412566.37168142],
[-27020.648967553, -604660.76696165]
],
[
[-251209.43952802, -680176.99115044],
[-395870.20648968, -508849.55752212],
[-237758.1120944, -401946.90265487],
[-133687.31563422, -582241.8879056]
],
[
[-409835.3584123, -745309.73451327],
[-428908.55457227, -607020.64896755],
[-295103.24483776, -493746.31268437],
[-260691.65833628, -668389.10758702]
]
]
},
"water": [
[
[-28849.557522124, -1063185.840708],
[-1474.9262536873, -772684.36578171],
[86312.684365782, -786371.68141593],
[31563.421828909, -1088436.5781711]
],
[
[-18938.053097345, -545899.70501475],
[41002.949852507, -482182.89085546],
[24719.764011799, -537404.12979351],
[-17286.135693215, -612920.3539823]
],
[
[5840.7079646018, -553923.30383481],
[39351.032448378, -397463.12684366],
[68377.581120944, -395811.20943953],
[39587.020648968, -537404.12979351]
]
],
"targetZones": {
"Berlin": [
[-229395.01555967, -531485.76918785],
[-259421.10835878, -447931.26582763],
[-200456.21860007, -435636.45902688],
[-188385.2142356, -489175.57499588]
],
"Fassberg": [
[-247191.58418879, -813059.75504425],
[-221545.92330383, -637203.79469027],
[-123542.8620649, -678649.01451327],
[-219485.11126844, -867327.80530973]
],
"Hamburg": [
[-126211.54521994, -771032.44837758],
[-80429.834305488, -609852.50737463],
[-3969.657314338, -644070.79646018],
[-104972.60716685, -806902.65486726]
],
"Leipzig": [
[-348235.75989842, -658154.69330028],
[-364451.04063155, -542876.3175586],
[-284030.66324113, -539864.77631412],
[-264392.49441197, -624589.44754851]
],
"Neubrandenburg": [
[-85867.341455633, -586901.83936957],
[-151457.6531, -426123.7738649],
[-33365.345967059, -405301.45270796],
[-198.36298135891, -516552.14003219]
],
"Ramstein": [
[-541345.29186423, -1007397.2292776],
[-536064.48212853, -833130.50799962],
[-371982.17962653, -887258.80779051],
[-404232.83908382, -1012112.2379702]
],
"Wiitstock": [
[-202970.05962823, -596596.27265076],
[-178043.23620461, -508945.97506878],
[-122770.71470005, -507184.84080515],
[-96489.172612102, -584810.22027111]
]
}
}