--[[ 04 April 2025 (Stevey666) - 3.1 - Set default cluster munitions option to false, set this to true in the options if you want it - Added missing radio commands for Cascade Scaling - Adjust default cascading to 2 (from 1) - Adjusted Ural-4320 to be a tanker and ammo carrier for cargo cookoff - Prevent weapons not in the list from being tracked - Moved some logging behind the debug mode flag - Ordnance Protection, added a max height ordnance protection will snap explosion to ground - Ordnance Protection, fixed enable/disable option - Added Giant Explosion feature - Adjusted some hydra70 values on recom. from ETBSmorgan 09 March 2025 (Stevey666) - 3.0 - Added ordinance protection gives a few options - stop the additional larger_explosion that tends to blow up your own bombs if theyre dropped at the same place if its within x m - Additional ordnance protection option that will cause a snap to ground larger_explosion if its within x meters of a recent larger explosion and within x seconds (can set in options) - Added vehicle scanning around a weapon to allow for.. - Cook offs - you can set vehicles that will cook off i.e ammo trucks, number of explosions, debris explosions, power adjustable - Fuel/Tanker explosion and flames - when a fuel tanker blows it will through up a big flame - adjustable in the scripts - Added section for vehicles for the above - Added radio commands for everything - Added in cluster munitions changes (note: barely tested, its not particularly accurate or that useful at this point so leaving disabled) - Potential bug - testing, stacking too many units together may cause a MIST error if you're using mist - Setting this as 3.0 as I'd like to be responsive to requests, updates etc - creating a new fork to track this 10 Feb 2025 (Stevey666) - 2.0.7 - Fixed AGM 154/Adjusted weapons - Added overall damage scaling - Added modifier for shaped charges (i.e. Mavericks), adjusted weapon list accordingly - Adjusted blast radius and damage calculations, created option for dynamic blast radius - Adjusted cascading explosions, added additional "cascade_scaling" modifier and cascade explode threshold modifier. Units wont explode on initial impact unless health drops under threshold - Added always_cascade_explode option so you can set it to the old ways of making everything in the blast wave go kaboom - Added in game radio commands to change the new options ingame without having to reload everything in mission editor to test it out 12 November 2024 (by JGi | Quéton 1-1) - Tweak down radius 100>90 (Thanks Arhibeau) - Tweak down some values 20 January 2024 (by JGi | Quéton 1-1) - Added missing weapons to explTable - Sort weapons in explTable by type - Added aircraft type in log when missing 03 May 2023 (KERV) Correction AGM 154 (https://forum.dcs.world/topic/289290-splash-damage-20-script-make-explosions-better/page/5/#comment-5207760) 06 March 2023 (Kerv) - Add some data for new ammunition 16 April 2022 spencershepard (GRIMM): - Added new/missing weapons to explTable - Added new option rocket_multiplier 31 December 2021 spencershepard (GRIMM): - Added many new weapons - Added filter for weapons.shells events - Fixed mission weapon message option - Changed default for damage_model option 21 December 2021 spencershepard (GRIMM): SPLASH DAMAGE 2.0: - Added blast wave effect to add timed and scaled secondary explosions on top of game objects - Object geometry within blast wave changes damage intensity - Damage boost for structures since they are hard to kill, even if very close to large explosions - Increased some rocket values in explTable - Missing weapons from explTable will display message to user and log to DCS.log so that we can add what's missing - Damage model for ground units that will disable their weapons and ability to move with partial damage before they are killed - Added options table to allow easy adjustments before release - General refactoring and restructure 28 October 2020 FrozenDroid: - Uncommented error logging, actually made it an error log which shows a message box on error. - Fixed the too restrictive weapon filter (took out the HE warhead requirement) 2 October 2020 FrozenDroid: - Added error handling to all event handler and scheduled functions. Lua script errors can no longer bring the server down. - Added some extra checks to which weapons to handle, make sure they actually have a warhead (how come S-8KOM's don't have a warhead field...?) --]] ----[[ ##### SCRIPT CONFIGURATION ##### ]]---- splash_damage_options = { --debug options ["game_messages"] = false, --enable some messages on screen ["debug"] = false, --enable debugging messages ["weapon_missing_message"] = false, --false disables messages alerting you to weapons missing from the explTable ["track_pre_explosion_debug"] = false, --Toggle to enable/disable pre-explosion tracking debugging ["enable_radio_menu"] = true, --enables the in-game radio menu for modifying settings ["static_damage_boost"] = 2000, --apply extra damage to Unit.Category.STRUCTUREs with wave explosions ["wave_explosions"] = true, --secondary explosions on top of game objects, radiating outward from the impact point and scaled based on size of object and distance from weapon impact point ["larger_explosions"] = true, --secondary explosions on top of weapon impact points, dictated by the values in the explTable ["damage_model"] = true, --allow blast wave to affect ground unit movement and weapons ["blast_search_radius"] = 90, --this is the max size of any blast wave radius, since we will only find objects within this zone ["cascade_damage_threshold"] = 0.1, --if the calculated blast damage doesn't exceed this value, there will be no secondary explosion damage on the unit. If this value is too small, the appearance of explosions far outside of an expected radius looks incorrect. ["blast_stun"] = false, --not implemented ["unit_disabled_health"] = 30, --if health is below this value after our explosions, disable its movement ["unit_cant_fire_health"] = 40, --if health is below this value after our explosions, set ROE to HOLD to simulate damage weapon systems ["infantry_cant_fire_health"] = 60, --if health is below this value after our explosions, set ROE to HOLD to simulate severe injury ["rocket_multiplier"] = 1.3, --multiplied by the explTable value for rockets ["overall_scaling"] = 1, --overall scaling for explosive power ["apply_shaped_charge_effects"] = true, --apply reduction in blastwave etc for shaped charge munitions ["shaped_charge_multiplier"] = 0.2, --multiplier that reduces blast radius and explosion power for shaped charge munitions. ["use_dynamic_blast_radius"] = true, --if true, blast radius is calculated from explosion power; if false, blast_search_radius (90) is used ["dynamic_blast_radius_modifier"] = 2, --multiplier for the blast radius ["cascade_scaling"] = 2, --multiplier for secondary (cascade) blast damage, 1 damage fades out too soon, 2 or 3 damage seems a good balance ["cascade_explode_threshold"] = 60, --only trigger cascade explosion if the unit's current health is <= this percent of its maximum, setting can help blow nearby jeeps but not tanks ["always_cascade_explode"] = false, --switch if you want everything to explode like with the original script --track_pre_explosion/enable_cargo_effects should both be the same value ["track_pre_explosion"] = true, --Toggle to enable/disable pre-explosion tracking ["enable_cargo_effects"] = true, --Toggle for enabling/disabling cargo explosions and cook-offs ["cargo_damage_threshold"] = 60, --Health % below which cargo explodes (0 = destroyed only) ["debris_effects"] = true, --Enable debris from cargo cook-offs ["debris_power"] = 1, --Power of each debris explosion ["debris_count_min"] = 6, --Minimum debris pieces per cook-off ["debris_count_max"] = 12, --Maximum debris pieces per cook-off ["debris_max_distance"] = 10, --Max distance debris can travel (meters), the min distance from the vehicle will be 10% of this ["ordnance_protection"] = true, --Toggle ordinance protection features ["ordnance_protection_radius"] = 10, --Distance in meters to protect nearby bombs ["detect_ordnance_destruction"] = true, --Toggle detection of ordnance destroyed by large explosions ["snap_to_ground_if_destroyed_by_large_explosion"] = true, --If the ordnance protection fails or is disabled we can snap larger_explosions to the ground (if enabled - power as set in weapon list) - so an explosion still does hit the ground ["max_snapped_height"] = 80, --max height it will snap to ground from ["recent_large_explosion_snap"] = true, --enable looking for a recent large_explosion generated by the script ["recent_large_explosion_range"] = 100, --range its looking for in meters for a recent large_explosion generated by the script ["recent_large_explosion_time"] = 4, --in seconds how long ago there was a recent large_explosion generated by the script --Cluster bomb settings ["cluster_enabled"] = false, ["cluster_base_length"] = 150, --Base forward spread (meters) ["cluster_base_width"] = 200, --Base lateral spread (meters) ["cluster_max_length"] = 300, --Max forward spread (meters) ["cluster_max_width"] = 400, --Max lateral spread (meters) ["cluster_min_length"] = 100, --Min forward spread ["cluster_min_width"] = 150, --Min lateral spread ["cluster_bomblet_reductionmodifier"] = true, --Use equation to reduce number of bomblets (to make it look better) ["cluster_bomblet_damage_modifier"] = 1, --Adjustable global modifier for bomblet explosive power --Giant Explosion Options - Remember, any target you want to blow up needs to be named "GiantExplosionTarget(X)" (X) being any value/name etc ["giant_explosion_enabled"] = true, --Toggle to enable/disable Giant Explosion ["giant_explosion_power"] = 6000, --Power in kg of TNT (default 8 tons) ["giant_explosion_scale"] = 1, --Size scale factor (default 1) ["giant_explosion_duration"] = 3.0, --Total duration in seconds (default 3s) ["giant_explosion_count"] = 250, --Number of explosions (default 300) ["giant_explosion_target_static"] = true, --Toggle to true for static targets (store position once), false for dynamic (update every second) ["giant_explosion_poll_rate"] = 1, --Polling rate in seconds for flag checks (default 1s) } local script_enable = 1 refreshRate = 0.1 ----[[ ##### End of SCRIPT CONFIGURATION ##### ]]---- --Helper function: Trim whitespace. local function trim(s) return s:match("^%s*(.-)%s*$") end cargoUnits = { --[[ flamesize: 1 = small smoke and fire 2 = medium smoke and fire 3 = large smoke and fire 4 = huge smoke and fire 5 = small smoke 6 = medium smoke 7 = large smoke 8 = huge smoke ]]-- --1) M92 R11 Volvo driveable (Fuel Truck Tanker) ["r11_volvo_drivable"] = { cargoExplosion = true, cargoExplosionMult = 2.0, cargoExplosionPower = 200, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --2) Refueler ATMZ-5 ["ATMZ-5"] = { cargoExplosion = true, cargoExplosionMult = 2.0, cargoExplosionPower = 200, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --3) Refueler ATZ-10 ["ATZ-10"] = { cargoExplosion = true, cargoExplosionMult = 2, cargoExplosionPower = 200, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --4) Refueler ATZ-5 ["ATZ-5"] = { cargoExplosion = true, cargoExplosionMult = 1.8, cargoExplosionPower = 200, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --5) Refueler M978 HEMTT (Fuel truck tanker) ["M978 HEMTT Tanker"] = { cargoExplosion = true, cargoExplosionMult = 2.0, cargoExplosionPower = 200, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --##### AMMO CARRIERS ##### ["GAZ-66"] = { cargoExplosion = true, cargoExplosionMult = 1, cargoExplosionPower = 200, cargoCookOff = true, cookOffCount = 4, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = false, flameSize = 1, flameDuration = 30, }, --#Technically this is both ammo and fuel looking at the model ["Ural-4320"] = { cargoExplosion = true, cargoExplosionMult = 1, cargoExplosionPower = 200, cargoCookOff = true, cookOffCount = 4, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = true, flameSize = 1, flameDuration = 30, }, ["ZIL-135"] = { cargoExplosion = true, cargoExplosionMult = 1, cargoExplosionPower = 200, cargoCookOff = true, cookOffCount = 5, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = false, flameSize = 1, flameDuration = 30, }, } --Weapon Explosive Table explTable = { --*** WWII BOMBS *** ["British_GP_250LB_Bomb_Mk1"] = { explosive = 100, shaped_charge = false }, ["British_GP_250LB_Bomb_Mk4"] = { explosive = 100, shaped_charge = false }, ["British_GP_250LB_Bomb_Mk5"] = { explosive = 100, shaped_charge = false }, ["British_GP_500LB_Bomb_Mk1"] = { explosive = 213, shaped_charge = false }, ["British_GP_500LB_Bomb_Mk4"] = { explosive = 213, shaped_charge = false }, ["British_GP_500LB_Bomb_Mk4_Short"] = { explosive = 213, shaped_charge = false }, ["British_GP_500LB_Bomb_Mk5"] = { explosive = 213, shaped_charge = false }, ["British_MC_250LB_Bomb_Mk1"] = { explosive = 100, shaped_charge = false }, ["British_MC_250LB_Bomb_Mk2"] = { explosive = 100, shaped_charge = false }, ["British_MC_500LB_Bomb_Mk1_Short"] = { explosive = 213, shaped_charge = false }, ["British_MC_500LB_Bomb_Mk2"] = { explosive = 213, shaped_charge = false }, ["British_SAP_250LB_Bomb_Mk5"] = { explosive = 100, shaped_charge = false }, ["British_SAP_500LB_Bomb_Mk5"] = { explosive = 213, shaped_charge = false }, ["British_AP_25LBNo1_3INCHNo1"] = { explosive = 4, shaped_charge = false }, ["British_HE_60LBSAPNo2_3INCHNo1"] = { explosive = 4, shaped_charge = false }, ["British_HE_60LBFNo1_3INCHNo1"] = { explosive = 4, shaped_charge = false }, ["SC_50"] = { explosive = 20, shaped_charge = false }, ["ER_4_SC50"] = { explosive = 20, shaped_charge = false }, ["SC_250_T1_L2"] = { explosive = 100, shaped_charge = false }, ["SC_501_SC250"] = { explosive = 100, shaped_charge = false }, ["Schloss500XIIC1_SC_250_T3_J"] = { explosive = 100, shaped_charge = false }, ["SC_501_SC500"] = { explosive = 213, shaped_charge = false }, ["SC_500_L2"] = { explosive = 213, shaped_charge = false }, ["SD_250_Stg"] = { explosive = 100, shaped_charge = false }, ["SD_500_A"] = { explosive = 213, shaped_charge = false }, --*** WWII CBU *** ["AB_250_2_SD_2"] = { explosive = 100, shaped_charge = false }, ["AB_250_2_SD_10A"] = { explosive = 100, shaped_charge = false }, ["AB_500_1_SD_10A"] = { explosive = 213, shaped_charge = false }, --*** WWII ROCKETS *** ["3xM8_ROCKETS_IN_TUBES"] = { explosive = 4, shaped_charge = false }, ["WGr21"] = { explosive = 4, shaped_charge = false }, --*** UNGUIDED BOMBS (UGB) *** ["M_117"] = { explosive = 201, shaped_charge = false }, ["AN_M30A1"] = { explosive = 45, shaped_charge = false }, ["AN_M57"] = { explosive = 100, shaped_charge = false }, ["AN_M64"] = { explosive = 121, shaped_charge = false }, ["AN_M65"] = { explosive = 400, shaped_charge = false }, ["AN_M66"] = { explosive = 800, shaped_charge = false }, ["AN-M66A2"] = { explosive = 536, shaped_charge = false }, ["AN-M81"] = { explosive = 100, shaped_charge = false }, ["AN-M88"] = { explosive = 100, shaped_charge = false }, ["Mk_81"] = { explosive = 60, shaped_charge = false }, ["MK-81SE"] = { explosive = 60, shaped_charge = false }, ["Mk_82"] = { explosive = 100, shaped_charge = false }, ["MK_82AIR"] = { explosive = 100, shaped_charge = false }, ["MK_82SNAKEYE"] = { explosive = 100, shaped_charge = false }, ["Mk_83"] = { explosive = 274, shaped_charge = false }, ["Mk_84"] = { explosive = 582, shaped_charge = false }, ["HEBOMB"] = { explosive = 40, shaped_charge = false }, ["HEBOMBD"] = { explosive = 40, shaped_charge = false }, ["SAMP125LD"] = { explosive = 60, shaped_charge = false }, ["SAMP250LD"] = { explosive = 118, shaped_charge = false }, ["SAMP250HD"] = { explosive = 118, shaped_charge = false }, ["SAMP400LD"] = { explosive = 274, shaped_charge = false }, ["SAMP400HD"] = { explosive = 274, shaped_charge = false }, ["BR_250"] = { explosive = 100, shaped_charge = false }, ["BR_500"] = { explosive = 100, shaped_charge = false }, ["FAB_100"] = { explosive = 45, shaped_charge = false }, ["FAB_250"] = { explosive = 118, shaped_charge = false }, ["FAB_250M54TU"] = { explosive = 118, shaped_charge = false }, ["FAB-250-M62"] = { explosive = 118, shaped_charge = false }, ["FAB_500"] = { explosive = 213, shaped_charge = false }, ["FAB_1500"] = { explosive = 675, shaped_charge = false }, --*** UNGUIDED BOMBS WITH PENETRATOR / ANTI-RUNWAY *** ["Durandal"] = { explosive = 64, shaped_charge = false }, ["BLU107B_DURANDAL"] = { explosive = 64, shaped_charge = false }, ["BAP_100"] = { explosive = 32, shaped_charge = false }, ["BAP-100"] = { explosive = 32, shaped_charge = false }, ["BAT-120"] = { explosive = 32, shaped_charge = false }, ["TYPE-200A"] = { explosive = 107, shaped_charge = false }, ["BetAB_500"] = { explosive = 98, shaped_charge = false }, ["BetAB_500ShP"] = { explosive = 107, shaped_charge = false }, --*** GUIDED BOMBS (GBU) *** ["GBU_10"] = { explosive = 582, shaped_charge = false }, ["GBU_12"] = { explosive = 100, shaped_charge = false }, ["GBU_16"] = { explosive = 274, shaped_charge = false }, ["GBU_24"] = { explosive = 582, shaped_charge = false }, ["KAB_1500Kr"] = { explosive = 675, shaped_charge = false }, ["KAB_500Kr"] = { explosive = 213, shaped_charge = false }, ["KAB_500"] = { explosive = 213, shaped_charge = false }, --*** CLUSTER BOMBS (CBU) *** --I don't have most of these so can't test them with debug on ["CBU_99"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 247, submunition_explosive = 2, submunition_name = "Mk 118" }, --Mk 20 Rockeye variant, confirmed 247 Mk 118 bomblets ["ROCKEYE"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 247, submunition_explosive = 2, submunition_name = "Mk 118" }, --Mk 20 Rockeye, confirmed 247 Mk 118 bomblets ["BLU_3B_GROUP"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 19, submunition_explosive = 0.2, submunition_name = "BLU_3B" }, --Not in datamine, possibly custom or outdated; submunition name guessed ["MK77mod0-WPN"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 132, submunition_explosive = 0.1, submunition_name = "BLU_1B" }, --Not in datamine, possibly custom; submunition name guessed ["MK77mod1-WPN"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 132, submunition_explosive = 0.1, submunition_name = "BLU_1B" }, --Not in datamine, possibly custom; submunition name guessed ["CBU_87"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 202, submunition_explosive = 0.5, submunition_name = "BLU_97B" }, --Confirmed 202 BLU-97/B bomblets ["CBU_103"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 202, submunition_explosive = 0.5, submunition_name = "BLU_97B" }, --WCMD variant of CBU-87, confirmed 202 BLU-97/B bomblets ["CBU_97"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 10, submunition_explosive = 15, submunition_name = "BLU_108" }, --Confirmed 10 BLU-108 submunitions, each with 4 skeets ["CBU_105"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 10, submunition_explosive = 15, submunition_name = "BLU_108" }, --WCMD variant of CBU-97, confirmed 10 BLU-108 submunitions ["BELOUGA"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 151, submunition_explosive = 0.3, submunition_name = "grenade_AC" }, --Confirmed 151 grenade_AC bomblets (French BLG-66) ["BLG66_BELOUGA"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 151, submunition_explosive = 0.3, submunition_name = "grenade_AC" }, --Alias for BELOUGA, confirmed 151 grenade_AC bomblets ["BL_755"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 147, submunition_explosive = 0.4, submunition_name = "BL_755_bomblet" }, --Confirmed 147 bomblets, submunition name from your table ["RBK_250"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 60, submunition_explosive = 0.5, submunition_name = "PTAB_25M" }, --Confirmed 60 PTAB-2.5M anti-tank bomblets ["RBK_250_275_AO_1SCH"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 150, submunition_explosive = 0.2, submunition_name = "AO_1SCh" }, --Confirmed 150 AO-1SCh fragmentation bomblets ["RBK_500"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 108, submunition_explosive = 0.5, submunition_name = "PTAB_10_5" }, --Confirmed 108 PTAB-10-5 anti-tank bomblets ["RBK_500U"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 352, submunition_explosive = 0.2, submunition_name = "OAB_25RT" }, --Confirmed 352 OAB-2.5RT fragmentation bomblets ["RBK_500AO"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 108, submunition_explosive = 0.5, submunition_name = "AO_25RT" }, --Confirmed 108 AO-2.5RT fragmentation bomblets ["RBK_500U_OAB_2_5RT"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 352, submunition_explosive = 0.2, submunition_name = "OAB_25RT" }, --Confirmed 352 OAB-2.5RT fragmentation bomblets ["RBK_500_255_PTO_1M"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 126, submunition_explosive = 0.5, submunition_name = "PTO_1M" }, ["RBK_500_255_ShO"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 565, submunition_explosive = 0.1, submunition_name = "ShO" }, --*** INS/GPS BOMBS (JDAM) *** ["GBU_31"] = { explosive = 582, shaped_charge = false }, ["GBU_31_V_3B"] = { explosive = 582, shaped_charge = false }, ["GBU_31_V_2B"] = { explosive = 582, shaped_charge = false }, ["GBU_31_V_4B"] = { explosive = 582, shaped_charge = false }, ["GBU_32_V_2B"] = { explosive = 202, shaped_charge = false }, ["GBU_38"] = { explosive = 100, shaped_charge = false }, ["GBU_54_V_1B"] = { explosive = 100, shaped_charge = false }, --*** GLIDE BOMBS (JSOW) *** ["AGM_154A"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 145, submunition_explosive = 2, submunition_name = "BLU-97/B" }, --JSOW-A, confirmed 145 BLU-97 bomblets from datamine ["AGM_154C"] = { explosive = 305, shaped_charge = false }, ["AGM_154"] = { explosive = 305, shaped_charge = false }, ["BK90_MJ1"] = { explosive = 0, shaped_charge = false }, ["BK90_MJ1_MJ2"] = { explosive = 0, shaped_charge = false }, ["BK90_MJ2"] = { explosive = 0, shaped_charge = false }, ["LS-6-100"] = { explosive = 45, shaped_charge = false }, ["LS-6-250"] = { explosive = 100, shaped_charge = false }, ["LS-6-500"] = { explosive = 274, shaped_charge = false }, ["GB-6"] = { explosive = 0, shaped_charge = false }, ["GB-6-HE"] = { explosive = 0, shaped_charge = false }, ["GB-6-SFW"] = { explosive = 0, shaped_charge = false }, --*** AIR GROUND MISSILE (AGM) *** ["AGM_62"] = { explosive = 400, shaped_charge = false }, ["AGM_65D"] = { explosive = 38, shaped_charge = true }, ["AGM_65E"] = { explosive = 80, shaped_charge = true }, ["AGM_65F"] = { explosive = 80, shaped_charge = true }, ["AGM_65G"] = { explosive = 80, shaped_charge = true }, ["AGM_65H"] = { explosive = 38, shaped_charge = true }, ["AGM_65K"] = { explosive = 80, shaped_charge = true }, ["AGM_65L"] = { explosive = 80, shaped_charge = true }, ["AGM_123"] = { explosive = 274, shaped_charge = false }, ["AGM_130"] = { explosive = 582, shaped_charge = false }, ["AGM_119"] = { explosive = 176, shaped_charge = false }, ["AGM_114"] = { explosive = 10, shaped_charge = true }, ["AGM_114K"] = { explosive = 10, shaped_charge = true }, ["Rb 05A"] = { explosive = 217, shaped_charge = false }, ["RB75"] = { explosive = 38, shaped_charge = false }, ["RB75A"] = { explosive = 38, shaped_charge = false }, ["RB75B"] = { explosive = 38, shaped_charge = false }, ["RB75T"] = { explosive = 80, shaped_charge = false }, ["HOT3_MBDA"] = { explosive = 15, shaped_charge = false }, ["C-701T"] = { explosive = 38, shaped_charge = false }, ["C-701IR"] = { explosive = 38, shaped_charge = false }, ["Vikhr_M"] = { explosive = 11, shaped_charge = false }, ["Vikhr_9M127_1"] = { explosive = 11, shaped_charge = false }, ["AT_6"] = { explosive = 11, shaped_charge = false }, ["Ataka_9M120"] = { explosive = 11, shaped_charge = false }, ["Ataka_9M120F"] = { explosive = 11, shaped_charge = false }, ["P_9M117"] = { explosive = 0, shaped_charge = false }, ["KH-66_Grom"] = { explosive = 108, shaped_charge = false }, ["X_23"] = { explosive = 111, shaped_charge = false }, ["X_23L"] = { explosive = 111, shaped_charge = false }, ["X_28"] = { explosive = 160, shaped_charge = false }, ["X_25ML"] = { explosive = 89, shaped_charge = false }, ["X_25MR"] = { explosive = 140, shaped_charge = false }, ["X_29L"] = { explosive = 320, shaped_charge = false }, ["X_29T"] = { explosive = 320, shaped_charge = false }, ["X_29TE"] = { explosive = 320, shaped_charge = false }, --*** ANTI-RADAR MISSILE (ARM) *** ["AGM_88C"] = { explosive = 89, shaped_charge = false }, ["AGM_88"] = { explosive = 89, shaped_charge = false }, ["AGM_122"] = { explosive = 15, shaped_charge = false }, ["LD-10"] = { explosive = 89, shaped_charge = false }, ["AGM_45A"] = { explosive = 38, shaped_charge = false }, ["X_58"] = { explosive = 140, shaped_charge = false }, ["X_25MP"] = { explosive = 89, shaped_charge = false }, --*** ANTI-SHIP MISSILE (ASh) *** ["AGM_84D"] = { explosive = 488, shaped_charge = false }, ["Rb 15F"] = { explosive = 500, shaped_charge = false }, ["C-802AK"] = { explosive = 500, shaped_charge = false }, --*** CRUISE MISSILE *** ["CM-802AKG"] = { explosive = 488, shaped_charge = false }, ["AGM_84E"] = { explosive = 488, shaped_charge = false }, ["AGM_84H"] = { explosive = 488, shaped_charge = false }, ["X_59M"] = { explosive = 488, shaped_charge = false }, --*** ROCKETS *** ["HYDRA_70M15"] = { explosive = 5, shaped_charge = false }, ["HYDRA_70_MK1"] = { explosive = 5, shaped_charge = false }, ["HYDRA_70_MK5"] = { explosive = 8, shaped_charge = false }, ["HYDRA_70_M151"] = { explosive = 5, shaped_charge = false }, ["HYDRA_70_M151_M433"] = { explosive = 5, shaped_charge = false }, ["HYDRA_70_M229"] = { explosive = 10, shaped_charge = false }, ["FFAR Mk1 HE"] = { explosive = 5, shaped_charge = false }, ["FFAR Mk5 HEAT"] = { explosive = 8, shaped_charge = false }, ["HVAR"] = { explosive = 5, shaped_charge = false }, ["Zuni_127"] = { explosive = 8, shaped_charge = false }, ["ARAKM70BHE"] = { explosive = 5, shaped_charge = false }, ["ARAKM70BAP"] = { explosive = 8, shaped_charge = false }, ["SNEB_TYPE251_F1B"] = { explosive = 4, shaped_charge = false }, ["SNEB_TYPE252_F1B"] = { explosive = 4, shaped_charge = false }, ["SNEB_TYPE253_F1B"] = { explosive = 5, shaped_charge = false }, ["SNEB_TYPE256_F1B"] = { explosive = 6, shaped_charge = false }, ["SNEB_TYPE257_F1B"] = { explosive = 8, shaped_charge = false }, ["SNEB_TYPE251_F4B"] = { explosive = 4, shaped_charge = false }, ["SNEB_TYPE252_F4B"] = { explosive = 4, shaped_charge = false }, ["SNEB_TYPE253_F4B"] = { explosive = 5, shaped_charge = false }, ["SNEB_TYPE256_F4B"] = { explosive = 6, shaped_charge = false }, ["SNEB_TYPE257_F4B"] = { explosive = 8, shaped_charge = false }, ["SNEB_TYPE251_H1"] = { explosive = 4, shaped_charge = false }, ["SNEB_TYPE252_H1"] = { explosive = 4, shaped_charge = false }, ["SNEB_TYPE253_H1"] = { explosive = 5, shaped_charge = false }, ["SNEB_TYPE256_H1"] = { explosive = 6, shaped_charge = false }, ["SNEB_TYPE257_H1"] = { explosive = 8, shaped_charge = false }, ["MATRA_F4_SNEBT251"] = { explosive = 8, shaped_charge = false }, ["MATRA_F4_SNEBT253"] = { explosive = 8, shaped_charge = false }, ["MATRA_F4_SNEBT256"] = { explosive = 8, shaped_charge = false }, ["MATRA_F1_SNEBT253"] = { explosive = 8, shaped_charge = false }, ["MATRA_F1_SNEBT256"] = { explosive = 8, shaped_charge = false }, ["TELSON8_SNEBT251"] = { explosive = 4, shaped_charge = false }, ["TELSON8_SNEBT253"] = { explosive = 8, shaped_charge = false }, ["TELSON8_SNEBT256"] = { explosive = 4, shaped_charge = false }, ["TELSON8_SNEBT257"] = { explosive = 6, shaped_charge = false }, ["ARF8M3API"] = { explosive = 8, shaped_charge = false }, ["UG_90MM"] = { explosive = 8, shaped_charge = false }, ["S-24A"] = { explosive = 24, shaped_charge = false }, ["S-25OF"] = { explosive = 194, shaped_charge = false }, ["S-25OFM"] = { explosive = 150, shaped_charge = false }, ["S-25O"] = { explosive = 150, shaped_charge = false }, ["S-25-O"] = { explosive = 150, shaped_charge = false }, ["S_25L"] = { explosive = 190, shaped_charge = false }, ["S-5M"] = { explosive = 1, shaped_charge = false }, ["C_5"] = { explosive = 8, shaped_charge = false }, ["C5"] = { explosive = 5, shaped_charge = false }, ["C_8"] = { explosive = 4, shaped_charge = false }, ["C_8OFP2"] = { explosive = 3, shaped_charge = false }, ["C_13"] = { explosive = 21, shaped_charge = false }, ["C_24"] = { explosive = 123, shaped_charge = false }, ["C_25"] = { explosive = 151, shaped_charge = false }, --*** LASER ROCKETS *** ["AGR_20"] = { explosive = 8, shaped_charge = false }, ["AGR_20A"] = { explosive = 8, shaped_charge = false }, ["AGR_20_M282"] = { explosive = 8, shaped_charge = false }, ["Hydra_70_M282_MPP"] = { explosive = 5, shaped_charge = true }, ["BRM-1_90MM"] = { explosive = 8, shaped_charge = false }, } local effectSmokeId = 1 ----[[ ##### HELPER/UTILITY FUNCTIONS ##### ]]---- local function tableHasKey(table, key) return table[key] ~= nil end local function debugMsg(str) if splash_damage_options.debug == true then debugCounter = (debugCounter or 0) + 1 local uniqueStr = str .. " [" .. timer.getTime() .. " - " .. debugCounter .. "]" trigger.action.outText(uniqueStr, 5) env.info("DEBUG: " .. uniqueStr) end end local function gameMsg(str) if splash_damage_options.game_messages == true then trigger.action.outText(str, 5) end end local function getDistance(point1, point2) local x1 = point1.x local y1 = point1.y local z1 = point1.z local x2 = point2.x local y2 = point2.y local z2 = point2.z local dX = math.abs(x1 - x2) local dZ = math.abs(z1 - z2) local distance = math.sqrt(dX * dX + dZ * dZ) return distance end local function getDistance3D(point1, point2) local x1 = point1.x local y1 = point1.y local z1 = point1.z local x2 = point2.x local y2 = point2.y local z2 = point2.z local dX = math.abs(x1 - x2) local dY = math.abs(y1 - y2) local dZ = math.abs(z1 - z2) local distance = math.sqrt(dX * dX + dZ * dZ + dY * dY) return distance end local function vec3Mag(speedVec) return math.sqrt(speedVec.x^2 + speedVec.y^2 + speedVec.z^2) end local function lookahead(speedVec) local speed = vec3Mag(speedVec) local dist = speed * refreshRate * 1.5 return dist end --Cluster-specific helper functions from Rockeye script local function normalizeVector(vec) local mag = math.sqrt(vec.x^2 + vec.z^2) if mag > 0 then return { x = vec.x / mag, z = vec.z / mag } else return { x = 1, z = 0 } end end local function calculate_drop_angle(velocity) local horizontal_speed = math.sqrt((velocity.x or 0)^2 + (velocity.z or 0)^2) local vertical_speed = math.abs(velocity.y or 0) if horizontal_speed == 0 then return 90 end local angle_rad = math.atan(vertical_speed / horizontal_speed) return math.deg(angle_rad) end local function calculate_dispersion(velocity, burst_altitude) local velocity_magnitude = math.sqrt((velocity.x or 0)^2 + (velocity.z or 0)^2) local drop_angle = calculate_drop_angle(velocity) local length = splash_damage_options.cluster_base_length * (1 + velocity_magnitude / 200) local width = splash_damage_options.cluster_base_width * (1 + burst_altitude / 6000) local length_jitter = length * (0.85 + math.random() * 0.3) local width_jitter = width * (0.85 + math.random() * 0.3) return math.max(splash_damage_options.cluster_min_length, math.min(splash_damage_options.cluster_max_length, length_jitter)), math.max(splash_damage_options.cluster_min_width, math.min(splash_damage_options.cluster_max_width, width_jitter)) end ----[[ ##### End of HELPER/UTILITY FUNCTIONS ##### ]]---- giantExplosionTargets = {} cargoEffectsQueue = {} WpnHandler = {} tracked_target_position = nil --Store the last known position of TargetUnit for giant explosion tracked_weapons = {} local processedUnitsGlobal = {} function scanGiantExplosionTargets() giantExplosionTargets = {} local function findTargets(obj) if obj:isExist() then local name = obj:getName() if string.find(name, "GiantExplosionTarget") then local flagName = string.gsub(name, "Target", "") table.insert(giantExplosionTargets, { name = name, flag = flagName, obj = obj, pos = obj:getPoint(), static = splash_damage_options.giant_explosion_target_static }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, {id = world.VolumeType.ALL}, findTargets) if not splash_damage_options.giant_explosion_target_static then timer.scheduleFunction(updateGiantExplosionPositions, {}, timer.getTime() + 1.0) end end function updateTargetPosition() for name, target in pairs(giantExplosionTargets) do if target.obj:isExist() then target.pos = target.obj:getPosition().p end end return timer.getTime() + 1.0 end --Giant Explosion Function function triggerGiantExplosion(params) if not splash_damage_options.giant_explosion_enabled then debugMsg("Giant Explosion is disabled in options.") return end local initialPos = params.pos or {x = 0, y = 0, z = 0} local explosionPower = params.power or splash_damage_options.giant_explosion_power local sizeScale = params.scale or splash_damage_options.giant_explosion_scale local totalDuration = params.duration or splash_damage_options.giant_explosion_duration local explosionCount = params.count or splash_damage_options.giant_explosion_count if not initialPos.x or not initialPos.y or not initialPos.z then gameMsg("Error: Invalid position for giant explosion!") debugMsg("No valid initial position set for giant explosion!") return end debugMsg("Triggering giant fireball at X: " .. initialPos.x .. ", Y: " .. initialPos.y .. ", Z: " .. initialPos.z) local function scheduleExplosion(pos, delay) if not pos or not pos.x or not pos.y or not pos.z then debugMsg("Error: Invalid position for explosion - pos: " .. tostring(pos)) return end timer.scheduleFunction(function(p) if p and p.x and p.y and p.z then trigger.action.explosion(p, explosionPower) end end, pos, timer.getTime() + delay) end -- Pre-explosion scan for cargo units local scanRadius = 1500 * sizeScale -- 1500m base radius, scaled by sizeScale local preExplosionTargets = {} if splash_damage_options.enable_cargo_effects then local volS = { id = world.VolumeType.SPHERE, params = { point = initialPos, radius = scanRadius } } local ifFound = function(foundObject) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and foundObject:getDesc().category == Unit.Category.GROUND_UNIT) or category == Object.Category.STATIC then table.insert(preExplosionTargets, { name = foundObject:getTypeName(), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0, unit = foundObject }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound) debugMsg("Pre-explosion scan for Giant Explosion: " .. #preExplosionTargets .. " targets found within " .. scanRadius .. "m") end -- Trigger the explosion local maxRadius = 200 * sizeScale local maxHeight = 500 * sizeScale local adjustedExplosionCount = math.floor(explosionCount * (sizeScale ^ 2.5)) local stepTime = totalDuration / adjustedExplosionCount local variance = 0.25 --Fixed at 25% for i = 1, adjustedExplosionCount do local progress = i / adjustedExplosionCount local currentRadius = maxRadius * progress local r = currentRadius * (0.9 + math.random() * 0.1) local theta = math.random() * 2 * math.pi local phi = math.acos(math.random()) local offsetX = r * math.sin(phi) * math.cos(theta) local offsetZ = r * math.sin(phi) * math.sin(theta) local offsetY = r * math.cos(phi) offsetX = offsetX * (1 + (math.random() - 0.5) * variance) offsetZ = offsetZ * (1 + (math.random() - 0.5) * variance) offsetY = offsetY * (1 + (math.random() - 0.5) * variance * 0.5) local blastPos = { x = initialPos.x + offsetX, y = land.getHeight({x = initialPos.x, y = initialPos.z}) + offsetY, z = initialPos.z + offsetZ } if blastPos.y < land.getHeight({x = blastPos.x, y = blastPos.z}) then blastPos.y = land.getHeight({x = blastPos.x, y = blastPos.z}) end local delay = (i - 1) * stepTime + (math.random() - 0.5) * stepTime * variance scheduleExplosion(blastPos, delay) end gameMsg("Expanding giant fireball over " .. totalDuration .. "s (scale " .. sizeScale .. ")!") -- Post-explosion scan and cargo cook-off queuing if splash_damage_options.enable_cargo_effects then timer.scheduleFunction(function(args) local centerPos = args[1] local radius = args[2] local preTargets = args[3] local postExplosionTargets = {} local volS = { id = world.VolumeType.SPHERE, params = { point = centerPos, radius = radius } } local ifFound = function(foundObject) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and foundObject:getDesc().category == Unit.Category.GROUND_UNIT) or category == Object.Category.STATIC then table.insert(postExplosionTargets, { name = foundObject:getTypeName(), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0 }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound) debugMsg("Post-explosion scan for Giant Explosion: " .. #postExplosionTargets .. " targets found within " .. radius .. "m") -- Compare pre- and post-explosion targets for _, preTarget in ipairs(preTargets) do local found = false local postHealth = 0 for _, postTarget in ipairs(postExplosionTargets) do if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then found = true postHealth = postTarget.health break end end local cargoData = cargoUnits[preTarget.name] if cargoData and (not found or postHealth <= 0) then local distance = getDistance(initialPos, preTarget.position) if distance <= radius then local cargoPower = cargoData.cargoExplosionPower or explosionPower table.insert(cargoEffectsQueue, { name = preTarget.name, distance = distance, coords = preTarget.position, power = cargoPower, explosion = cargoData.cargoExplosion, cookOff = cargoData.cargoCookOff, cookOffCount = cargoData.cookOffCount, cookOffPower = cargoData.cookOffPower, cookOffDuration = cargoData.cookOffDuration, cookOffRandomTiming = cargoData.cookOffRandomTiming, cookOffPowerRandom = cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration }) debugMsg("Queued cargo effect for " .. preTarget.name .. " destroyed by Giant Explosion at " .. string.format("%.1f", distance) .. "m") end end end -- Process queued cargo effects with prioritized flames if #cargoEffectsQueue > 0 then local flameIndex = 0 -- Separate index for flames local otherIndex = 0 -- Index for explosions, cook-offs, debris local processedCargoUnits = {} local flamePositions = {} for _, effect in ipairs(cargoEffectsQueue) do local unitKey = effect.name .. "_" .. effect.coords.x .. "_" .. effect.coords.z if not processedUnitsGlobal[unitKey] and not processedCargoUnits[unitKey] then -- Handle tanker flames first with minimal delay if effect.isTanker and effect.explosion then debugMsg("Triggering cargo explosion for tanker " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m with power " .. effect.power .. " scheduled at " .. flameIndex .. "s") timer.scheduleFunction(function(params) debugMsg("Executing cargo explosion at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, effect.power}, timer.getTime() + flameIndex + 0.1) local flameSize = effect.flameSize or 3 local flameDuration = effect.flameDuration local flameDensity = 1.0 local effectId = effectSmokeId effectSmokeId = effectSmokeId + 1 local isDuplicate = false for _, pos in pairs(flamePositions) do if getDistance3D(effect.coords, pos) < 3 then isDuplicate = true debugMsg("Skipping duplicate flame for " .. effect.name .. " near X: " .. string.format("%.0f", pos.x) .. ", Y: " .. string.format("%.0f", pos.y) .. ", Z: " .. string.format("%.0f", pos.z)) break end end if not isDuplicate then debugMsg("Adding flame effect for tanker " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m (Size: " .. flameSize .. ", Duration: " .. flameDuration .. "s, ID: " .. effectId .. ") scheduled at " .. flameIndex .. "s") timer.scheduleFunction(function(params) local terrainHeight = land.getHeight({x = params[1].x, y = params[1].z}) local adjustedCoords = {x = params[1].x, y = terrainHeight + 2, z = params[1].z} debugMsg("Spawning flame effect at X: " .. string.format("%.0f", adjustedCoords.x) .. ", Y: " .. string.format("%.0f", adjustedCoords.y) .. ", Z: " .. string.format("%.0f", adjustedCoords.z)) trigger.action.explosion(adjustedCoords, 10) -- Small trigger explosion trigger.action.effectSmokeBig(adjustedCoords, params[2], params[3], params[4]) end, {effect.coords, flameSize, flameDensity, effectId}, timer.getTime() + flameIndex + 0.2) timer.scheduleFunction(function(id) debugMsg("Stopping flame effect for " .. effect.name .. " (ID: " .. id .. ")") trigger.action.effectSmokeStop(id) end, effectId, timer.getTime() + flameIndex + flameDuration + 0.2) table.insert(flamePositions, effect.coords) end flameIndex = flameIndex + 0.5 -- Fast spacing for flames (0.5s) end -- Handle non-tanker explosions, cook-offs, and debris if not effect.isTanker or (effect.explosion and not effect.isTanker) then if effect.explosion then debugMsg("Triggering cargo explosion for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m with power " .. effect.power .. " scheduled at " .. otherIndex .. "s") timer.scheduleFunction(function(params) debugMsg("Executing cargo explosion at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, effect.power}, timer.getTime() + otherIndex + 0.1) end if effect.cookOff and effect.cookOffCount > 0 then debugMsg("Scheduling " .. effect.cookOffCount .. " cook-off explosions for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m over " .. effect.cookOffDuration .. "s starting at " .. otherIndex .. "s") for i = 1, effect.cookOffCount do local delay = effect.cookOffRandomTiming and math.random() * effect.cookOffDuration or (i - 1) * (effect.cookOffDuration / effect.cookOffCount) local basePower = effect.cookOffPower local powerVariation = effect.cookOffPowerRandom / 100 local cookOffPower = effect.cookOffPowerRandom == 0 and basePower or basePower * (1 + powerVariation * (math.random() * 2 - 1)) debugMsg("Cook-off #" .. i .. " for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m scheduled at " .. string.format("%.3f", delay) .. "s with power " .. string.format("%.2f", cookOffPower)) timer.scheduleFunction(function(params) debugMsg("Executing cook-off at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, cookOffPower}, timer.getTime() + otherIndex + delay) end if splash_damage_options.debris_effects then local debrisCount = math.random(splash_damage_options.debris_count_min, splash_damage_options.debris_count_max) for j = 1, debrisCount do local theta = math.random() * 2 * math.pi local phi = math.acos(math.random() * 2 - 1) local minDist = splash_damage_options.debris_max_distance * 0.1 local maxDist = splash_damage_options.debris_max_distance local r = math.random() * (maxDist - minDist) + minDist local debrisX = effect.coords.x + r * math.sin(phi) * math.cos(theta) local debrisZ = effect.coords.z + r * math.sin(phi) * math.sin(theta) local terrainY = land.getHeight({x = debrisX, y = debrisZ}) local debrisY = terrainY + math.random() * maxDist local debrisPos = {x = debrisX, y = debrisY, z = debrisZ} local debrisPower = splash_damage_options.debris_power local debrisDelay = (j - 1) * (effect.cookOffDuration / debrisCount) timer.scheduleFunction(function(debrisArgs) debugMsg("Debris explosion at X: " .. string.format("%.0f", debrisArgs[1].x) .. ", Y: " .. string.format("%.0f", debrisArgs[1].y) .. ", Z: " .. string.format("%.0f", debrisArgs[1].z) .. " with power " .. debrisArgs[2]) trigger.action.explosion(debrisArgs[1], debrisArgs[2]) end, {debrisPos, debrisPower}, timer.getTime() + otherIndex + debrisDelay) end end end otherIndex = otherIndex + 1 -- Slower spacing for non-flame effects (1s) end processedCargoUnits[unitKey] = true processedUnitsGlobal[unitKey] = true end end cargoEffectsQueue = {} -- Clear the queue after processing end end, {initialPos, scanRadius, preExplosionTargets}, timer.getTime() + totalDuration + 1.0) end end --Flag Checker for mission editor function checkGiantExplosionFlag() for name, target in pairs(giantExplosionTargets) do local flagName = name:gsub("GiantExplosionTarget", "GiantExplosionTarget") local flagValue = trigger.misc.getUserFlag(flagName) --commenting out as it spams every second debugMsg("Checking flag " .. flagName .. ": " .. flagValue) if flagValue == 1 then debugMsg("Triggering explosion for " .. name .. " at X:" .. target.pos.x .. " Y:" .. target.pos.y .. " Z:" .. target.pos.z) triggerGiantExplosion({ pos = target.pos, power = splash_damage_options.giant_explosion_power, scale = splash_damage_options.giant_explosion_scale, duration = splash_damage_options.giant_explosion_duration, count = splash_damage_options.giant_explosion_count }) trigger.action.setUserFlag(flagName, 2) end end return timer.getTime() + splash_damage_options.giant_explosion_poll_rate end function getWeaponExplosive(name) local weaponData = explTable[name] if weaponData then return weaponData.explosive, weaponData.shaped_charge else return 0, false end end function track_wpns_cluster_scan(args) local parentPos = args[1] local parentDir = args[2] local parentName = args[3] local subName = args[4] local subCount = args[5] local subPower = args[6] local parentVel = args[7] local attempt = args[8] or 1 local maxAttempts = 3 local scanVol = { id = world.VolumeType.SPHERE, params = { point = parentPos, radius = 400 } } local bombletsFound = {} local allWeaponsFound = {} --General scan for all weapons world.searchObjects(Object.Category.WEAPON, scanVol, function(wpn) if wpn:isExist() then local wpnId = wpn.id_ local wpnType = wpn:getTypeName() local wpnPos = wpn:getPosition().p table.insert(allWeaponsFound, { id = wpnId, type = wpnType, x = wpnPos.x, y = wpnPos.y, z = wpnPos.z }) if wpnType == subName and not tracked_weapons[wpnId] then tracked_weapons[wpnId] = { wpn = wpn, pos = wpnPos, speed = wpn:getVelocity(), name = wpnType, parent = parentName, parentVelocity = parentVel } table.insert(bombletsFound, wpnId) debugMsg("Detected expected submunition '" .. wpnType .. "' from '" .. parentName .. "' at X: " .. string.format("%.0f", wpnPos.x) .. ", Y: " .. string.format("%.0f", wpnPos.y) .. ", Z: " .. string.format("%.0f", wpnPos.z) .. " (Attempt " .. attempt .. ")") end end return true end) --Log results debugMsg("Scanned for submunition '" .. subName .. "' bomblets from '" .. parentName .. "': " .. #bombletsFound .. " found (Attempt " .. attempt .. ")") if #allWeaponsFound > 0 then local msg = "General scan for '" .. parentName .. "': " .. #allWeaponsFound .. " bomblets released, expected " .. subCount .. " '" .. subName .. "'" local typeMismatch = false for _, wpn in ipairs(allWeaponsFound) do if wpn.type ~= subName then typeMismatch = true break end end if typeMismatch then msg = msg .. " - Mismatch detected! Actual bomblets: " for _, wpn in ipairs(allWeaponsFound) do msg = msg .. "'" .. wpn.type .. "' (X: " .. string.format("%.0f", wpn.x) .. ", Y: " .. string.format("%.0f", wpn.y) .. ", Z: " .. string.format("%.0f", wpn.z) .. ") " end msg = msg .. "Script may need changing." end debugMsg(msg) elseif #bombletsFound == 0 and #allWeaponsFound == 0 then debugMsg("No bomblets of any type detected for '" .. parentName .. "' (Attempt " .. attempt .. ")") end --Retry if no expected submunitions found if #bombletsFound == 0 and attempt < maxAttempts then debugMsg("No expected submunition '" .. subName .. "' found on attempt " .. attempt .. ", retrying in 0.5s") timer.scheduleFunction(track_wpns_cluster_scan, {parentPos, parentDir, parentName, subName, subCount, subPower, parentVel, attempt + 1}, timer.getTime() + 0.5) elseif #bombletsFound == 0 and attempt == maxAttempts then debugMsg("No submunition '" .. subName .. "' spawned by DCS for '" .. parentName .. "' after " .. maxAttempts .. " attempts - skipping additional explosions") end end ----[[ ##### Updated track_wpns() Function ##### ]]---- local recentExplosions = {} function track_wpns() local weaponsToRemove = {} --Delay removal to ensure all weapons are checked for wpn_id_, wpnData in pairs(tracked_weapons) do local status, err = pcall(function() if wpnData.wpn:isExist() then --Update position, direction, speed wpnData.pos = wpnData.wpn:getPosition().p wpnData.dir = wpnData.wpn:getPosition().x wpnData.speed = wpnData.wpn:getVelocity() --[[ --Tick-by-tick tracking from weapon's actual position local tickVol = { id = world.VolumeType.SPHERE, params = { point = wpnData.pos, --Real weapon position radius = 150 --150m radius } } local tickTargets = {} world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, tickVol, function(obj) if obj:isExist() then table.insert(tickTargets, { name = obj:getTypeName(), distance = getDistance3D(wpnData.pos, obj:getPoint()), --3D distance position = obj:getPoint(), health = obj:getLife() or 0 }) end return true end) debugMsg("Tick Track for " .. wpnData.name .. " at X: " .. string.format("%.0f", wpnData.pos.x) .. ", Y: " .. string.format("%.0f", wpnData.pos.y) .. ", Z: " .. string.format("%.0f", wpnData.pos.z) .. " - " .. #tickTargets .. " targets") for i, target in ipairs(tickTargets) do debugMsg("Tick Target #" .. i .. ": " .. target.name .. " at X: " .. string.format("%.0f", target.position.x) .. ", Y: " .. string.format("%.0f", target.position.y) .. ", Z: " .. string.format("%.0f", target.position.z) .. ", Dist: " .. string.format("%.1f", target.distance) .. "m, Health: " .. target.health) end ]]-- --Scan potential blast zone in the last frame before impact if splash_damage_options.track_pre_explosion then local ip = land.getIP(wpnData.pos, wpnData.dir, lookahead(wpnData.speed)) local predictedImpact = ip or wpnData.pos local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name) base_explosive = base_explosive * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then base_explosive = base_explosive * splash_damage_options.rocket_multiplier end local explosionPower = base_explosive if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier end local blastRadius = splash_damage_options.blast_search_radius * 2 --Wider post-scan (180m default) if splash_damage_options.use_dynamic_blast_radius then blastRadius = math.pow(explosionPower, 1/3) * 10 * splash_damage_options.dynamic_blast_radius_modifier end --Tight scan while weapon exists --local tightRadius = 50 --if splash_damage_options.use_dynamic_blast_radius then --tightRadius = math.pow(explosionPower, 1/3) * 5 * splash_damage_options.dynamic_blast_radius_modifier --end local tightRadius = blastRadius --Use already calculated blastRadius local volS = { id = world.VolumeType.SPHERE, params = { point = wpnData.pos, --Use current pos radius = tightRadius } } local tightTargets = {} local ifFound = function(foundObject, targets, center) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and (foundObject:getDesc().category == Unit.Category.GROUND_UNIT or foundObject:getDesc().category == Unit.Category.AIRPLANE)) or category == Object.Category.STATIC then table.insert(targets, { name = foundObject:getTypeName(), distance = getDistance(center, foundObject:getPoint()), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0, unit = foundObject }) end end return true end if splash_damage_options.track_pre_explosion_debug then debugMsg("Scanning tight radius " .. tightRadius .. "m at current pos while weapon exists") end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, function(obj) ifFound(obj, tightTargets, wpnData.pos) end) wpnData.tightTargets = tightTargets --Store for impact --Wider scan for lastKnownTargets volS.params.point = predictedImpact volS.params.radius = blastRadius local foundTargets = {} world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, function(obj) ifFound(obj, foundTargets, predictedImpact) end) wpnData.lastKnownTargets = foundTargets end --Submunition impact handling local weaponData = explTable[wpnData.parent or wpnData.name] or { submunition_name = "unknown" } if wpnData.name == weaponData.submunition_name then local groundHeight = land.getHeight({x = wpnData.pos.x, y = wpnData.pos.z}) if wpnData.pos.y - groundHeight < 50 then --Impact threshold like old script debugMsg("Submunition '" .. wpnData.name .. "' from '" .. (wpnData.parent or "unknown") .. "' impacted at X: " .. string.format("%.0f", wpnData.pos.x) .. ", Z: " .. string.format("%.0f", wpnData.pos.z)) local parentWeaponData = explTable[wpnData.parent] or { submunition_count = 30, submunition_explosive = 1 } local submunitionCount = parentWeaponData.submunition_count or 30 local submunitionPower = (parentWeaponData.submunition_explosive or 1) * splash_damage_options.cluster_bomblet_damage_modifier * splash_damage_options.overall_scaling if splash_damage_options.cluster_bomblet_reductionmodifier then if submunitionCount > 35 then local reductionFactor = (60 - 35) / (247 - 35) submunitionCount = 35 + math.floor((submunitionCount - 35) * reductionFactor) if submunitionCount > 60 then submunitionCount = 60 end end end --Use parent velocity if available, else submunition speed local parentDir = wpnData.parentVelocity or wpnData.speed local dispersionLength, dispersionWidth = calculate_dispersion(parentDir, 2000) --Match original 2000m local dirMag = math.sqrt(parentDir.x^2 + parentDir.z^2) local dir = dirMag > 0 and {x = parentDir.x / dirMag, z = parentDir.z / dirMag} or {x = 1, z = 0} debugMsg("Simulating " .. submunitionCount .. " bomblets for submunition '" .. wpnData.name .. "' from '" .. (wpnData.parent or "unknown") .. "' over " .. string.format("%.0f", dispersionLength) .. "m x " .. string.format("%.0f", dispersionWidth) .. "m") for i = 1, submunitionCount do local theta = math.random() * 2 * math.pi local r = math.sqrt(math.random()) local xOffset = r * dispersionLength * 0.5 * math.cos(theta) local zOffset = r * dispersionWidth * 0.5 * math.sin(theta) local subPos = { x = wpnData.pos.x + (xOffset * dir.x - zOffset * dir.z), z = wpnData.pos.z + (xOffset * dir.z + zOffset * dir.x) } subPos.y = land.getHeight({x = subPos.x, y = subPos.z}) debugMsg("Triggering bomblet #" .. i .. " for submunition '" .. wpnData.name .. "' at X: " .. string.format("%.0f", subPos.x) .. ", Z: " .. string.format("%.0f", subPos.z) .. " with power " .. submunitionPower) trigger.action.explosion(subPos, submunitionPower) end table.insert(weaponsToRemove, wpn_id_) end end else --Weapon has impacted debugMsg("Weapon " .. wpnData.name .. " no longer exists at " .. timer.getTime() .. "s") local ip = land.getIP(wpnData.pos, wpnData.dir, lookahead(wpnData.speed)) --terrain intersection point with weapon's nose. Only search out 20 meters though. local explosionPoint if not ip then --use last calculated IP explosionPoint = wpnData.pos else --use intersection point explosionPoint = ip end local chosenTargets = wpnData.tightTargets or {} local safeToBlast = true if splash_damage_options.ordnance_protection then local checkVol = { id = world.VolumeType.SPHERE, params = { point = explosionPoint, radius = splash_damage_options.ordnance_protection_radius } } debugMsg("Checking ordnance protection for '" .. wpnData.name .. "' at X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. " with radius " .. splash_damage_options.ordnance_protection_radius .. "m") world.searchObjects(Object.Category.WEAPON, checkVol, function(obj) if obj:isExist() and tracked_weapons[obj.id_] then safeToBlast = false debugMsg("Skipping explosion for '" .. wpnData.name .. "' - nearby bomb '" .. tracked_weapons[obj.id_].name .. "' within " .. splash_damage_options.ordnance_protection_radius .. "m") return false end return true end) end if safeToBlast then debugMsg("FinalPos Check for '" .. wpnData.name .. "': X: " .. string.format("%.0f", explosionPoint.x) .. ", Y: " .. string.format("%.0f", explosionPoint.y) .. ", Z: " .. string.format("%.0f", explosionPoint.z) .. ")") local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name) base_explosive = base_explosive * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then base_explosive = base_explosive * splash_damage_options.rocket_multiplier end local explosionPower = base_explosive if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier end local blastRadius = splash_damage_options.blast_search_radius * 2 --Wider post-scan (180m default) if splash_damage_options.use_dynamic_blast_radius then blastRadius = math.pow(explosionPower, 1/3) * 10 * splash_damage_options.dynamic_blast_radius_modifier end --Store pre-explosion state of all tracked weapons for detection local preExplosionWeapons = {} if splash_damage_options.ordnance_protection and splash_damage_options.detect_ordnance_destruction and splash_damage_options.larger_explosions then for id, data in pairs(tracked_weapons) do if data.wpn:isExist() then preExplosionWeapons[id] = { name = data.name, pos = data.wpn:getPosition().p, distance = getDistance3D(explosionPoint, data.wpn:getPosition().p), explosive = getWeaponExplosive(data.name) --Store the explosive power } end end end --Cluster Bomb Handling local weaponData = explTable[wpnData.name] or { explosive = 0, shaped_charge = false } local isCluster = weaponData.cluster or false if splash_damage_options.cluster_enabled and isCluster then local submunitionCount = weaponData.submunition_count or 30 local submunitionPower = (weaponData.submunition_explosive or 1) * splash_damage_options.cluster_bomblet_damage_modifier * splash_damage_options.overall_scaling local submunitionName = weaponData.submunition_name or "unknown" --Apply bomblet reduction logic if enabled if splash_damage_options.cluster_bomblet_reductionmodifier then if submunitionCount > 35 then local reductionFactor = (60 - 35) / (247 - 35) submunitionCount = 35 + math.floor((submunitionCount - 35) * reductionFactor) if submunitionCount > 60 then submunitionCount = 60 end --Cap at 60 end end --Extended scan with general bomblet detection timer.scheduleFunction(track_wpns_cluster_scan, {explosionPoint, wpnData.dir, wpnData.name, submunitionName, submunitionCount, submunitionPower, wpnData.speed}, timer.getTime() + 0.3) else --Standard explosion handling if splash_damage_options.larger_explosions then debugMsg("Triggering initial explosion for '" .. wpnData.name .. "' at power " .. explosionPower) trigger.action.explosion(explosionPoint, explosionPower) table.insert(recentExplosions, { pos = explosionPoint, time = timer.getTime(), radius = blastRadius }) debugMsg("Added to recentExplosions for '" .. wpnData.name .. "': X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. ", Time: " .. timer.getTime()) end blastWave(explosionPoint, splash_damage_options.blast_search_radius, wpnData.ordnance, explosionPower, isShapedCharge) end --detect_ordnance_destruction comes before recent_large_explosion_snap in original if splash_damage_options.ordnance_protection and splash_damage_options.detect_ordnance_destruction and splash_damage_options.larger_explosions then timer.scheduleFunction(function(args) local explosionPoint = args[1] local blastRadius = args[2] local triggeringWeapon = args[3] local preExplosionWeapons = args[4] for id, preData in pairs(preExplosionWeapons) do if tracked_weapons[id] and not tracked_weapons[id].wpn:isExist() then if preData.distance <= blastRadius then local msg = "WARNING: " .. preData.name .. " destroyed by large explosion from " .. triggeringWeapon .. " at " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", explosionPoint.x, explosionPoint.y, explosionPoint.z) gameMsg(msg) debugMsg(msg) env.info(msg) if splash_damage_options.snap_to_ground_if_destroyed_by_large_explosion then local groundPos = { x = preData.pos.x, y = land.getHeight({x = preData.pos.x, y = preData.pos.z}), z = preData.pos.z } local destroyedWeaponPower, isShapedCharge = preData.explosive destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and tracked_weapons[id].cat == Weapon.Category.ROCKET then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.rocket_multiplier end if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.shaped_charge_multiplier end debugMsg("Triggering ground explosion for destroyed " .. preData.name .. " (detect_ordnance_destruction) at X: " .. string.format("%.0f", groundPos.x) .. ", Y: " .. string.format("%.0f", groundPos.y) .. ", Z: " .. string.format("%.0f", groundPos.z) .. " with power " .. destroyedWeaponPower) trigger.action.explosion(groundPos, destroyedWeaponPower) end end end end end, {explosionPoint, blastRadius, wpnData.name, preExplosionWeapons}, timer.getTime() + 0.2) end --recent_large_explosion_snap comes after main explosion and detect_ordnance_destruction if splash_damage_options.ordnance_protection and splash_damage_options.larger_explosions and splash_damage_options.recent_large_explosion_snap and splash_damage_options.snap_to_ground_if_destroyed_by_large_explosion then local currentTime = timer.getTime() for id, data in pairs(tracked_weapons) do if id ~= wpn_id_ and not data.wpn:isExist() then local terrainHeight = land.getHeight({x = data.pos.x, y = data.pos.z}) local weaponHeight = data.pos.y - terrainHeight --Calculate height above ground local isMidAir = weaponHeight > 5 --Still checks if above ground local snapTriggered = false for _, explosion in ipairs(recentExplosions) do local timeDiff = currentTime - explosion.time local distance = getDistance3D(data.pos, explosion.pos) debugMsg("Checking " .. data.name .. " at X: " .. data.pos.x .. ", Y: " .. data.pos.y .. ", Z: " .. data.pos.z .. " against explosion at X: " .. explosion.pos.x .. ", Y: " .. explosion.pos.y .. ", Z: " .. explosion.pos.z .. " - Distance: " .. distance .. "m, TimeDiff: " .. timeDiff .. "s") if timeDiff <= splash_damage_options.recent_large_explosion_time and distance <= splash_damage_options.recent_large_explosion_range then if isMidAir and weaponHeight <= splash_damage_options.max_snapped_height then --New height check local groundPos = { x = data.pos.x, y = terrainHeight, z = data.pos.z } local destroyedWeaponPower, isShapedCharge = getWeaponExplosive(data.name) destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and data.cat == Weapon.Category.ROCKET then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.rocket_multiplier end if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.shaped_charge_multiplier end debugMsg("Weapon " .. data.name .. " detected recent large explosion within " .. splash_damage_options.recent_large_explosion_range .. "m and " .. splash_damage_options.recent_large_explosion_time .. "s, snapping to ground at X: " .. string.format("%.0f", groundPos.x) .. ", Y: " .. string.format("%.0f", groundPos.y) .. ", Z: " .. string.format("%.0f", groundPos.z) .. " with power " .. destroyedWeaponPower .. " (Height: " .. string.format("%.0f", weaponHeight) .. "m)") trigger.action.explosion(groundPos, destroyedWeaponPower) snapTriggered = true table.insert(weaponsToRemove, id) break elseif isMidAir then debugMsg("Weapon " .. data.name .. " destroyed above max_snapped_height (" .. splash_damage_options.max_snapped_height .. "m) at " .. string.format("%.0f", weaponHeight) .. "m, skipping snap") else debugMsg("Weapon " .. data.name .. " impacted ground within recent_large_explosion_range (" .. splash_damage_options.recent_large_explosion_range .. "m) and time (" .. splash_damage_options.recent_large_explosion_time .. "s), no snap needed") snapTriggered = true break end end end if not snapTriggered then if isMidAir then debugMsg("Weapon " .. data.name .. " destroyed in air, but no recent large explosion within " .. splash_damage_options.recent_large_explosion_range .. "m or " .. splash_damage_options.recent_large_explosion_time .. "s") else debugMsg("Weapon " .. data.name .. " impacted ground, not processed by recent large explosion settings") end end end end local newExplosions = {} for _, explosion in ipairs(recentExplosions) do if currentTime - explosion.time <= splash_damage_options.recent_large_explosion_time then table.insert(newExplosions, explosion) end end recentExplosions = newExplosions end --Mark units as destroyed to avoid MiST accessing them local destroyedUnits = {} for _, target in ipairs(chosenTargets) do if target.unit:isExist() and target.health > 0 and target.unit:getLife() <= 0 then destroyedUnits[target.name] = true debugMsg("Marked " .. target.name .. " as destroyed pre-impact") end end --Schedule explosion handling with original 0.1-second delay, enhanced error handling timer.scheduleFunction(function(args) local finalPos = args[1] local explosionPoint = args[2] local explosionPower = args[3] local isShapedCharge = args[4] local blastRadius = args[5] local chosenTargets = args[6] local weaponName = args[7] local wpnData = args[8] if splash_damage_options.debug then debugMsg("Starting impact handling for " .. weaponName .. " at " .. timer.getTime() .. "s") end local status, err = pcall(function() --Log pre-explosion targets if splash_damage_options.track_pre_explosion then if #chosenTargets > 0 then local msg = "Targets in blast zone for " .. weaponName .. " BEFORE explosion (last frame, using finalPos):\n" for i, target in ipairs(chosenTargets) do msg = msg .. "- " .. target.name .. " (Dist: " .. string.format("%.1f", target.distance) .. "m, Health: " .. target.health .. ")\n" end debugMsg(msg) env.info("SplashDamage Pre-Explosion (Last Frame): " .. msg) else debugMsg("No targets in blast zone for " .. weaponName .. " BEFORE explosion (last frame)") env.info("SplashDamage Pre-Explosion (Last Frame): No targets in blast zone for " .. weaponName) end end blastWave(explosionPoint, splash_damage_options.blast_search_radius, wpnData.ordnance, explosionPower, isShapedCharge) --Post-explosion analysis and queue cargo effects if splash_damage_options.track_pre_explosion then timer.scheduleFunction(function(innerArgs) local impactPoint = innerArgs[1] local blastRadius = innerArgs[2] local preExplosionTargets = innerArgs[3] or {} local weaponName = innerArgs[4] local weaponPower = innerArgs[5] if splash_damage_options.debug == true then debugMsg("Starting post-explosion analysis for " .. weaponName .. " at " .. timer.getTime() .. "s") end --Scan all units in wider radius local postExplosionTargets = {} local volS = { id = world.VolumeType.SPHERE, params = { point = impactPoint, radius = blastRadius } } local ifFound = function(foundObject) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and (foundObject:getDesc().category == Unit.Category.GROUND_UNIT or foundObject:getDesc().category == Unit.Category.AIRPLANE)) or category == Object.Category.STATIC then table.insert(postExplosionTargets, { name = foundObject:getTypeName(), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0 }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound) local msg = "Post-explosion analysis for " .. weaponName .. ":\n" --Match pre-detected units for _, preTarget in ipairs(preExplosionTargets) do local found = false local postHealth = 0 local postPosition = nil for _, postTarget in ipairs(postExplosionTargets) do if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then found = true postHealth = postTarget.health postPosition = postTarget.position break end end local healthPercent = preTarget.maxHealth > 0 and (postHealth / preTarget.maxHealth * 100) or 0 local status = "" if not found or postHealth <= 0 then status = "WAS FULLY DESTROYED" elseif healthPercent < splash_damage_options.cargo_damage_threshold then status = "WAS DAMAGED BELOW THRESHOLD" else status = "SURVIVED (Health: " .. postHealth .. ")" end --Always include coords in status message local coords = found and postPosition or preTarget.position local statusMsg = status .. " AT " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", coords.x, coords.y, coords.z) .. " (Pre: " .. preTarget.health .. ", Post: " .. postHealth .. ")" --Check if target is in cargoUnits and within blast radius local cargoData = cargoUnits[preTarget.name] if cargoData and preTarget.distance <= blastRadius and (not found or postHealth <= 0 or healthPercent < splash_damage_options.cargo_damage_threshold) then if splash_damage_options.enable_cargo_effects then local cargoPower = cargoData.cargoExplosionPower or weaponPower --Use fixed power or fallback table.insert(cargoEffectsQueue, { name = preTarget.name, distance = preTarget.distance, coords = coords, power = cargoPower, explosion = cargoData.cargoExplosion, cookOff = cargoData.cargoCookOff, cookOffCount = cargoData.cookOffCount, cookOffPower = cargoData.cookOffPower, cookOffDuration = cargoData.cookOffDuration, cookOffRandomTiming = cargoData.cookOffRandomTiming, cookOffPowerRandom = cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration }) statusMsg = statusMsg .. " WITH CARGO EXPLOSION (Power: " .. cargoPower .. ")" if cargoData.cargoCookOff and cargoData.cookOffCount > 0 then statusMsg = statusMsg .. " WITH COOK-OFF (" .. cargoData.cookOffCount .. " blasts over " .. cargoData.cookOffDuration .. "s)" end end end msg = msg .. "- " .. preTarget.name .. " " .. statusMsg .. "\n" end --Check for additional units for _, postTarget in ipairs(postExplosionTargets) do local isPreDetected = false for _, preTarget in ipairs(preExplosionTargets) do if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then isPreDetected = true break end end if not isPreDetected then local coords = postTarget.position local healthPercent = postTarget.maxHealth > 0 and (postTarget.health / postTarget.maxHealth * 100) or 0 local status = postTarget.health <= 0 and "WAS FULLY DESTROYED" or (healthPercent < splash_damage_options.cargo_damage_threshold and "WAS DAMAGED BELOW THRESHOLD" or "SURVIVED (Health: " .. postTarget.health .. ")") local statusMsg = status .. " AT " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", coords.x, coords.y, coords.z) .. " (Pre: Unknown, Post: " .. postTarget.health .. ")" local cargoData = cargoUnits[postTarget.name] if cargoData and (postTarget.health <= 0 or healthPercent < splash_damage_options.cargo_damage_threshold) then if splash_damage_options.enable_cargo_effects then local cargoPower = cargoData.cargoExplosionPower or weaponPower --Use fixed power or fallback local distance = getDistance(impactPoint, coords) table.insert(cargoEffectsQueue, { name = postTarget.name, distance = distance, coords = coords, power = cargoPower, explosion = cargoData.cargoExplosion, cookOff = cargoData.cargoCookOff, cookOffCount = cargoData.cookOffCount, cookOffPower = cargoData.cookOffPower, cookOffDuration = cargoData.cookOffDuration, cookOffRandomTiming = cargoData.cookOffRandomTiming, cookOffPowerRandom = cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration }) statusMsg = statusMsg .. " WITH CARGO EXPLOSION (Power: " .. cargoPower .. ")" if cargoData.cargoCookOff and cargoData.cookOffCount > 0 then statusMsg = statusMsg .. " WITH COOK-OFF (" .. cargoData.cookOffCount .. " blasts over " .. cargoData.cookOffDuration .. "s)" end end end msg = msg .. "- " .. postTarget.name .. " " .. statusMsg .. "\n" end end --Schedule all queued cargo effects if #cargoEffectsQueue > 0 then local effectIndex = 0 local processedCargoUnits = {} --Track processed units local flamePositions = {} --Track flame coords with 3m radius for _, effect in ipairs(cargoEffectsQueue) do local unitKey = effect.name .. "_" .. effect.coords.x .. "_" .. effect.coords.z if not processedUnitsGlobal[unitKey] and not processedCargoUnits[unitKey] then if effect.explosion then debugMsg("Triggering cargo explosion for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m with power " .. effect.power .. " scheduled at " .. effectIndex .. "s") timer.scheduleFunction(function(params) debugMsg("Executing cargo explosion at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, effect.power}, timer.getTime() + effectIndex + 0.1) --Slight delay for visibility if effect.isTanker then local flameSize = effect.flameSize or 3 local flameDuration = effect.flameDuration --Use cargoUnits value directly, no default local flameDensity = 1.0 --Max density for visibility local effectId = effectSmokeId effectSmokeId = effectSmokeId + 1 --Check for nearby flames within 3m local isDuplicate = false for _, pos in pairs(flamePositions) do if getDistance3D(effect.coords, pos) < 3 then isDuplicate = true debugMsg("Skipping duplicate flame for " .. effect.name .. " near X: " .. string.format("%.0f", pos.x) .. ", Y: " .. string.format("%.0f", pos.y) .. ", Z: " .. string.format("%.0f", pos.z)) break end end if not isDuplicate then debugMsg("Adding flame effect for tanker " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m (Size: " .. flameSize .. ", Duration: " .. flameDuration .. "s, ID: " .. effectId .. ") scheduled at " .. effectIndex .. "s") timer.scheduleFunction(function(params) --Adjust Y-coordinate to terrain height + offset local terrainHeight = land.getHeight({x = params[1].x, y = params[1].z}) local adjustedCoords = {x = params[1].x, y = terrainHeight + 2, z = params[1].z} debugMsg("Spawning flame effect at X: " .. string.format("%.0f", adjustedCoords.x) .. ", Y: " .. string.format("%.0f", adjustedCoords.y) .. ", Z: " .. string.format("%.0f", adjustedCoords.z)) trigger.action.explosion(adjustedCoords, 10) --Small explosion to force visibility trigger.action.effectSmokeBig(adjustedCoords, params[2], params[3], params[4]) end, {effect.coords, flameSize, flameDensity, effectId}, timer.getTime() + effectIndex + 0.2) --Slight delay timer.scheduleFunction(function(id) debugMsg("Stopping flame effect for " .. effect.name .. " (ID: " .. id .. ")") trigger.action.effectSmokeStop(id) end, effectId, timer.getTime() + effectIndex + flameDuration + 0.2) table.insert(flamePositions, effect.coords) end end end debugMsg("Checking cook-off for " .. effect.name .. ": cookOff=" .. tostring(effect.cookOff) .. ", count=" .. tostring(effect.cookOffCount)) if effect.cookOff and effect.cookOffCount > 0 then debugMsg("Scheduling " .. effect.cookOffCount .. " cook-off explosions for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m over " .. effect.cookOffDuration .. "s starting at " .. effectIndex .. "s") for i = 1, effect.cookOffCount do local delay = effect.cookOffRandomTiming and math.random() * effect.cookOffDuration or (i - 1) * (effect.cookOffDuration / effect.cookOffCount) local basePower = effect.cookOffPower local powerVariation = effect.cookOffPowerRandom / 100 local cookOffPower = effect.cookOffPowerRandom == 0 and basePower or basePower * (1 + powerVariation * (math.random() * 2 - 1)) debugMsg("Cook-off #" .. i .. " for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m scheduled at " .. string.format("%.3f", delay) .. "s with power " .. string.format("%.2f", cookOffPower)) timer.scheduleFunction(function(params) local pos = params[1] local power = params[2] debugMsg("Executing cook-off at " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", pos.x, pos.y, pos.z) .. " with power " .. power) trigger.action.explosion(pos, power) end, {effect.coords, cookOffPower}, timer.getTime() + effectIndex + delay) end --Debris burst only if cook-off is true and enabled if splash_damage_options.debris_effects then local debrisCount = math.random(splash_damage_options.debris_count_min, splash_damage_options.debris_count_max) for j = 1, debrisCount do --Random spherical offset local theta = math.random() * 2 * math.pi --Horizontal angle local phi = math.acos(math.random() * 2 - 1) --Vertical angle for sphere local minDist = splash_damage_options.debris_max_distance * 0.1 --10% of max local maxDist = splash_damage_options.debris_max_distance local r = math.random() * (maxDist - minDist) + minDist --10% to full max distance local debrisX = effect.coords.x + r * math.sin(phi) * math.cos(theta) local debrisZ = effect.coords.z + r * math.sin(phi) * math.sin(theta) local terrainY = land.getHeight({x = debrisX, y = debrisZ}) local debrisY = terrainY + math.random() * maxDist --0 to max_distance above ground local debrisPos = {x = debrisX, y = debrisY, z = debrisZ} local debrisPower = splash_damage_options.debris_power local debrisDelay = (j - 1) * (effect.cookOffDuration / debrisCount) --Spread over cook-off duration timer.scheduleFunction(function(debrisArgs) local dPos = debrisArgs[1] local dPower = debrisArgs[2] debugMsg("Debris explosion at X: " .. string.format("%.0f", dPos.x) .. ", Y: " .. string.format("%.0f", dPos.y) .. ", Z: " .. string.format("%.0f", dPos.z) .. " with power " .. dPower) trigger.action.explosion(dPos, dPower) end, {debrisPos, debrisPower}, timer.getTime() + effectIndex + debrisDelay) end end end processedCargoUnits[unitKey] = true processedUnitsGlobal[unitKey] = true effectIndex = effectIndex + 3 --3 secs spacing if not random end end --Clear the queue after scheduling cargoEffectsQueue = {} end debugMsg(msg) env.info("SplashDamage Post-Explosion: " .. msg) end, {finalPos, blastRadius, chosenTargets, weaponName, explosionPower}, timer.getTime() + 1) end end) if not status then debugMsg("Impact handling error for '" .. weaponName .. "': " .. err) end end, {explosionPoint, explosionPoint, explosionPower, isShapedCharge, blastRadius, chosenTargets, wpnData.name, wpnData}, timer.getTime() + 0.1) else debugMsg("Explosion skipped due to ordnance protection for '" .. wpnData.name .. "'") if splash_damage_options.larger_explosions then table.insert(recentExplosions, { pos = explosionPoint, time = timer.getTime(), radius = blastRadius }) debugMsg("Skipped explosion logged for snap check for '" .. wpnData.name .. "': X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. ", Time: " .. timer.getTime()) end end table.insert(weaponsToRemove, wpn_id_) end end) if not status then debugMsg("Error in track_wpns for '" .. (wpnData.name or "unknown weapon") .. "': " .. err) end end --Perform all removals after iteration for _, id in ipairs(weaponsToRemove) do tracked_weapons[id] = nil end return timer.getTime() + refreshRate end function onWpnEvent(event) if event.id == world.event.S_EVENT_SHOT then if event.weapon then local ordnance = event.weapon local typeName = trim(ordnance:getTypeName()) if splash_damage_options.debug then env.info("Weapon fired: [" .. typeName .. "]") debugMsg("Weapon fired: [" .. typeName .. "]") end if string.find(typeName, "weapons.shells") then if splash_damage_options.debug then debugMsg("Event shot, but not tracking: " .. typeName) env.info("SplashDamage: event shot, but not tracking: " .. typeName .. " (" .. event.initiator:getTypeName() .. ")") end return end --Check if weapon is in explTable before tracking if not explTable[typeName] then env.info("SplashDamage: " .. typeName .. " missing from script (" .. event.initiator:getTypeName() .. ")") if splash_damage_options.weapon_missing_message == true then trigger.action.outText("SplashDamage: " .. typeName .. " missing from script (" .. (event.initiator and event.initiator:isExist() and event.initiator:getTypeName() or "no initiator") .. ")", 3) -- if mist and mist.utils and mist.utils.tableShow then --Only if MiST is present -- local success, desc = pcall(mist.utils.tableShow, ordnance:getDesc()) -- if success then -- debugMsg("desc for [" .. typeName .. "]: " .. desc) -- else -- debugMsg("Could not retrieve description for [" .. typeName .. "]. Object may no longer exist.") -- end -- end env.info("Current keys in explTable:") for k, v in pairs(explTable) do env.info("Key: [" .. k .. "]") end end return --Skip tracking this weapon since its not in the table end if (ordnance:getDesc().category ~= 0) and event.initiator then if ordnance:getDesc().category == 1 then if (ordnance:getDesc().MissileCategory ~= 1 and ordnance:getDesc().MissileCategory ~= 2) then tracked_weapons[event.weapon.id_] = { wpn = ordnance, init = event.initiator:getName(), pos = ordnance:getPoint(), dir = ordnance:getPosition().x, name = typeName, speed = ordnance:getVelocity(), cat = ordnance:getCategory() } end else tracked_weapons[event.weapon.id_] = { wpn = ordnance, init = event.initiator:getName(), pos = ordnance:getPoint(), dir = ordnance:getPosition().x, name = typeName, speed = ordnance:getVelocity(), cat = ordnance:getCategory() } end end end end end local function protectedCall(...) local status, retval = pcall(...) if not status then env.warning("Splash damage script error... gracefully caught! " .. retval, true) end end function WpnHandler:onEvent(event) protectedCall(onWpnEvent, event) end function explodeObject(args) local point = args[1] local distance = args[2] local power = args[3] trigger.action.explosion(point, power) end function blastWave(_point, _radius, weapon, power, isShapedCharge) if isShapedCharge then _radius = _radius * splash_damage_options.shaped_charge_multiplier end if splash_damage_options.use_dynamic_blast_radius then local dynamicRadius = math.pow(power, 1/3) * 5 * splash_damage_options.dynamic_blast_radius_modifier if isShapedCharge then _radius = dynamicRadius * splash_damage_options.shaped_charge_multiplier else _radius = dynamicRadius end end local foundUnits = {} local volS = { id = world.VolumeType.SPHERE, params = { point = _point, radius = _radius } } local ifFound = function(foundObject, val) if foundObject:getDesc().category == Unit.Category.GROUND_UNIT and foundObject:getCategory() == Object.Category.UNIT then foundUnits[#foundUnits + 1] = foundObject end if foundObject:getDesc().category == Unit.Category.GROUND_UNIT then if splash_damage_options.blast_stun == true then --suppressUnit(foundObject, 2, weapon) end end if splash_damage_options.wave_explosions then local obj = foundObject local obj_location = obj:getPoint() local dist = getDistance(_point, obj_location) local timing = dist / 500 if obj:isExist() and tableHasKey(obj:getDesc(), "box") then local length = (obj:getDesc().box.max.x + math.abs(obj:getDesc().box.min.x)) local height = (obj:getDesc().box.max.y + math.abs(obj:getDesc().box.min.y)) local depth = (obj:getDesc().box.max.z + math.abs(obj:getDesc().box.min.z)) local _length = length local _depth = depth if depth > length then _length = depth _depth = length end local surface_distance = dist - _depth / 2 local scaled_power_factor = 0.006 * power + 1 local intensity = (power * scaled_power_factor) / (4 * math.pi * surface_distance^2) local surface_area = _length * height local damage_for_surface = intensity * surface_area if damage_for_surface > splash_damage_options.cascade_damage_threshold then local explosion_size = damage_for_surface if obj:getDesc().category == Unit.Category.STRUCTURE then explosion_size = intensity * splash_damage_options.static_damage_boost end if explosion_size > power then explosion_size = power end local triggerExplosion = false if splash_damage_options.always_cascade_explode then triggerExplosion = true else if obj:getDesc().life then local healthPercent = (obj:getLife() / obj:getDesc().life) * 100 if healthPercent <= splash_damage_options.cascade_explode_threshold then triggerExplosion = true end --Queue cargo effects for units below local cargoData = cargoUnits[obj:getTypeName()] if cargoData and healthPercent <= splash_damage_options.cargo_damage_threshold and splash_damage_options.enable_cargo_effects then local cargoPower = power * cargoData.cargoExplosionMult table.insert(cargoEffectsQueue, { name = obj:getTypeName(), distance = dist, coords = obj_location, power = cargoPower, explosion = cargoData.cargoExplosion, cookOff = cargoData.cargoCookOff, cookOffCount = cargoData.cookOffCount, cookOffPower = cargoData.cookOffPower, cookOffDuration = cargoData.cookOffDuration, cookOffRandomTiming = cargoData.cookOffRandomTiming, cookOffPowerRandom = cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration }) end else triggerExplosion = true end if not triggerExplosion and obj:getDesc().category == Unit.Category.GROUND_UNIT then local health = obj:getLife() or 0 if health <= 0 then triggerExplosion = true end end end if triggerExplosion then timer.scheduleFunction(explodeObject, {obj_location, dist, explosion_size * splash_damage_options.cascade_scaling}, timer.getTime() + timing) end end end end return true end world.searchObjects(Object.Category.UNIT, volS, ifFound) world.searchObjects(Object.Category.STATIC, volS, ifFound) world.searchObjects(Object.Category.SCENERY, volS, ifFound) world.searchObjects(Object.Category.CARGO, volS, ifFound) if splash_damage_options.damage_model then timer.scheduleFunction(modelUnitDamage, foundUnits, timer.getTime() + 1.5) end end function modelUnitDamage(units) for i, unit in ipairs(units) do if unit:isExist() then local health = (unit:getLife() / unit:getDesc().life) * 100 if unit:hasAttribute("Infantry") and health > 0 then if health <= splash_damage_options.infantry_cant_fire_health then unit:getController():setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) end end if unit:getDesc().category == Unit.Category.GROUND_UNIT and (not unit:hasAttribute("Infantry")) and health > 0 then if health <= splash_damage_options.unit_cant_fire_health then unit:getController():setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) gameMsg(unit:getTypeName() .. " weapons disabled") end if health <= splash_damage_options.unit_disabled_health and health > 0 then unit:getController():setTask({id = 'Hold', params = {}}) unit:getController():setOnOff(false) gameMsg(unit:getTypeName() .. " disabled") end end end end end function updateSplashDamageSetting(setting, increment) if not splash_damage_options[setting] then env.info("Error: Setting " .. setting .. " does not exist.") return end local newValue = math.max(0, splash_damage_options[setting] + increment) env.info("Updating " .. setting .. " from " .. tostring(splash_damage_options[setting]) .. " to " .. tostring(newValue)) splash_damage_options[setting] = newValue trigger.action.outText("Updated " .. setting .. " to: " .. tostring(splash_damage_options[setting]), 5) end function toggleSplashDamageSetting(setting) splash_damage_options[setting] = not splash_damage_options[setting] trigger.action.outText("Toggled " .. setting .. " to: " .. tostring(splash_damage_options[setting]), 5) if setting == "enable_radio_menu" then if splash_damage_options.enable_radio_menu then addSplashDamageMenu() else missionCommands.removeItem(splash_damage_menu) splash_damage_menu = nil end end end function addValueAdjustmentCommands(menu, setting) missionCommands.addCommand("+0.1", menu, updateSplashDamageSetting, setting, 0.1) missionCommands.addCommand("+1", menu, updateSplashDamageSetting, setting, 1) missionCommands.addCommand("+10", menu, updateSplashDamageSetting, setting, 10) missionCommands.addCommand("+100", menu, updateSplashDamageSetting, setting, 100) missionCommands.addCommand("-0.1", menu, updateSplashDamageSetting, setting, -0.1) missionCommands.addCommand("-1", menu, updateSplashDamageSetting, setting, -1) missionCommands.addCommand("-10", menu, updateSplashDamageSetting, setting, -10) missionCommands.addCommand("-100", menu, updateSplashDamageSetting, setting, -100) end function exitSplashDamageMenu() if splash_damage_menu then missionCommands.removeItem(splash_damage_menu) splash_damage_menu = nil end end function addSplashDamageMenu() if not splash_damage_options.enable_radio_menu then return end if splash_damage_menu then missionCommands.removeItem(splash_damage_menu) end splash_damage_menu = missionCommands.addSubMenu("Splash Damage Settings") --Page 1: Debug & General Settings local debugGeneralMenu = missionCommands.addSubMenu("Debug & General Settings", splash_damage_menu) missionCommands.addCommand("Toggle Game Messages", debugGeneralMenu, toggleSplashDamageSetting, "game_messages") missionCommands.addCommand("Toggle Debug Messages", debugGeneralMenu, toggleSplashDamageSetting, "debug") missionCommands.addCommand("Toggle Weapon Missing Messages", debugGeneralMenu, toggleSplashDamageSetting, "weapon_missing_message") missionCommands.addCommand("Toggle Pre-Explosion Debug", debugGeneralMenu, toggleSplashDamageSetting, "track_pre_explosion_debug") missionCommands.addCommand("Toggle Damage Model", debugGeneralMenu, toggleSplashDamageSetting, "damage_model") missionCommands.addCommand("Toggle Blast Stun", debugGeneralMenu, toggleSplashDamageSetting, "blast_stun") local unitDisabledMenu = missionCommands.addSubMenu("Unit Disabled Health", debugGeneralMenu) addValueAdjustmentCommands(unitDisabledMenu, "unit_disabled_health") local unitCantFireMenu = missionCommands.addSubMenu("Unit Cant Fire Health", debugGeneralMenu) addValueAdjustmentCommands(unitCantFireMenu, "unit_cant_fire_health") local infantryCantFireMenu = missionCommands.addSubMenu("Infantry Cant Fire Health", debugGeneralMenu) addValueAdjustmentCommands(infantryCantFireMenu, "infantry_cant_fire_health") local rocketMultiplierMenu = missionCommands.addSubMenu("Rocket Multiplier", debugGeneralMenu) addValueAdjustmentCommands(rocketMultiplierMenu, "rocket_multiplier") --Page 2/3: Explosions local explosionCargoMenu = missionCommands.addSubMenu("Explosion Settings", splash_damage_menu) local staticDamageMenu = missionCommands.addSubMenu("Static Damage Boost", explosionCargoMenu) addValueAdjustmentCommands(staticDamageMenu, "static_damage_boost") missionCommands.addCommand("Toggle Wave Explosions", explosionCargoMenu, toggleSplashDamageSetting, "wave_explosions") missionCommands.addCommand("Toggle Larger Explosions", explosionCargoMenu, toggleSplashDamageSetting, "larger_explosions") local blastRadiusMenu = missionCommands.addSubMenu("Blast Search Radius", explosionCargoMenu) addValueAdjustmentCommands(blastRadiusMenu, "blast_search_radius") local overallScalingMenu = missionCommands.addSubMenu("Overall Scaling", explosionCargoMenu) addValueAdjustmentCommands(overallScalingMenu, "overall_scaling") missionCommands.addCommand("Toggle Shaped Charge Effects", explosionCargoMenu, toggleSplashDamageSetting, "apply_shaped_charge_effects") local shapedChargeMenu = missionCommands.addSubMenu("Shaped Charge Multiplier", explosionCargoMenu) addValueAdjustmentCommands(shapedChargeMenu, "shaped_charge_multiplier") missionCommands.addCommand("Toggle Dynamic Blast Radius", explosionCargoMenu, toggleSplashDamageSetting, "use_dynamic_blast_radius") local dynamicBlastMenu = missionCommands.addSubMenu("Dynamic Blast Radius Modifier", explosionCargoMenu) addValueAdjustmentCommands(dynamicBlastMenu, "dynamic_blast_radius_modifier") local explosionCargoMenu = missionCommands.addSubMenu("Cascade Settings", splash_damage_menu) local cascadeScalingMenu = missionCommands.addSubMenu("Cascade Scaling", explosionCargoMenu) addValueAdjustmentCommands(cascadeScalingMenu, "cascade_scaling") local cascadeExplodeThresholdMenu = missionCommands.addSubMenu("Cascade Explode Threshold", explosionCargoMenu) addValueAdjustmentCommands(cascadeExplodeThresholdMenu, "cascade_explode_threshold") local cascadeThresholdMenu = missionCommands.addSubMenu("Cascade Damage Threshold", explosionCargoMenu) addValueAdjustmentCommands(cascadeThresholdMenu, "cascade_damage_threshold") --Page 4: Cargo and Ordnance Protection local explosionCargoMenu = missionCommands.addSubMenu("Cargo and Ordnance", splash_damage_menu) missionCommands.addCommand("Toggle Always Cascade Explode", explosionCargoMenu, toggleSplashDamageSetting, "always_cascade_explode") missionCommands.addCommand("Toggle Tracking & Cargo Effects", explosionCargoMenu, toggleSplashDamageSetting, "track_pre_explosion") local cargoThresholdMenu = missionCommands.addSubMenu("Cargo Damage Threshold", explosionCargoMenu) addValueAdjustmentCommands(cargoThresholdMenu, "cargo_damage_threshold") missionCommands.addCommand("Toggle Ordnance Protection", explosionCargoMenu, toggleSplashDamageSetting, "ordnance_protection") local ordnanceRadiusMenu = missionCommands.addSubMenu("Ordnance Protection Radius", explosionCargoMenu) addValueAdjustmentCommands(ordnanceRadiusMenu, "ordnance_protection_radius") missionCommands.addCommand("Toggle Snap To Ground If Destroyed By LE", explosionCargoMenu, toggleSplashDamageSetting, "snap_to_ground_if_destroyed_by_large_explosion") local ordnanceRadiusMenu = missionCommands.addSubMenu("Ordnance Protection Radius", explosionCargoMenu) local cargoThresholdMenu = missionCommands.addSubMenu("Max Snap Height", explosionCargoMenu) addValueAdjustmentCommands(cargoThresholdMenu, "max_snapped_height") missionCommands.addCommand("Toggle Recent Expl Track Snap", explosionCargoMenu, toggleSplashDamageSetting, "recent_large_explosion_snap") --Page 5: Debris Settings local debrisMenu = missionCommands.addSubMenu("Debris Settings", splash_damage_menu) missionCommands.addCommand("Toggle Debris Effects", debrisMenu, toggleSplashDamageSetting, "debris_effects") local debrisCountMinMenu = missionCommands.addSubMenu("Min Debris Count", debrisMenu) addValueAdjustmentCommands(debrisCountMinMenu, "debris_count_min") local debrisCountMaxMenu = missionCommands.addSubMenu("Max Debris Count", debrisMenu) addValueAdjustmentCommands(debrisCountMaxMenu, "debris_count_max") local debrisDistanceMenu = missionCommands.addSubMenu("Max Debris Distance", debrisMenu) addValueAdjustmentCommands(debrisDistanceMenu, "debris_max_distance") local debrisPowerMenu = missionCommands.addSubMenu("Debris Power", debrisMenu) addValueAdjustmentCommands(debrisPowerMenu, "debris_power") --Page 6: Cluster Settings local clusterMenu = missionCommands.addSubMenu("Cluster Settings", splash_damage_menu) missionCommands.addCommand("Toggle Cluster Enabled", clusterMenu, toggleSplashDamageSetting, "cluster_enabled") local clusterBaseLengthMenu = missionCommands.addSubMenu("Cluster Base Length", clusterMenu) addValueAdjustmentCommands(clusterBaseLengthMenu, "cluster_base_length") local clusterBaseWidthMenu = missionCommands.addSubMenu("Cluster Base Width", clusterMenu) addValueAdjustmentCommands(clusterBaseWidthMenu, "cluster_base_width") local clusterMaxLengthMenu = missionCommands.addSubMenu("Cluster Max Length", clusterMenu) addValueAdjustmentCommands(clusterMaxLengthMenu, "cluster_max_length") local clusterMaxWidthMenu = missionCommands.addSubMenu("Cluster Max Width", clusterMenu) addValueAdjustmentCommands(clusterMaxWidthMenu, "cluster_max_width") local clusterMinLengthMenu = missionCommands.addSubMenu("Cluster Min Length", clusterMenu) addValueAdjustmentCommands(clusterMinLengthMenu, "cluster_min_length") local clusterMinWidthMenu = missionCommands.addSubMenu("Cluster Min Width", clusterMenu) addValueAdjustmentCommands(clusterMinWidthMenu, "cluster_min_width") missionCommands.addCommand("Toggle Bomblet Reduction Modifier", clusterMenu, toggleSplashDamageSetting, "cluster_bomblet_reductionmodifier") local clusterBombletDamageMenu = missionCommands.addSubMenu("Bomblet Damage Modifier", clusterMenu) addValueAdjustmentCommands(clusterBombletDamageMenu, "cluster_bomblet_damage_modifier") --Page 7: Giant Explosion Settings local giantExplosionMenu = missionCommands.addSubMenu("Giant Explosion Settings", splash_damage_menu) missionCommands.addCommand("Toggle Giant Explosion", giantExplosionMenu, toggleSplashDamageSetting, "giant_explosion_enabled") missionCommands.addCommand("Toggle Static Target", giantExplosionMenu, toggleSplashDamageSetting, "giant_explosion_target_static") for name, target in pairs(giantExplosionTargets) do local displayName = name:gsub("GiantExplosionTarget", "GiantExplosionTarget") missionCommands.addCommand("Detonate " .. displayName, giantExplosionMenu, function() trigger.action.setUserFlag(displayName, 1) end) end missionCommands.addCommand("Detonate All Giant Targets", giantExplosionMenu, function() for name, target in pairs(giantExplosionTargets) do local flagName = name:gsub("GiantExplosionTarget", "GiantExplosionTarget") trigger.action.setUserFlag(flagName, 1) end end) local powerMenu = missionCommands.addSubMenu("Explosion Power", giantExplosionMenu) addValueAdjustmentCommands(powerMenu, "giant_explosion_power") local scaleMenu = missionCommands.addSubMenu("Size Scale", giantExplosionMenu) missionCommands.addCommand("+0.1", scaleMenu, updateSplashDamageSetting, "giant_explosion_scale", 0.1) missionCommands.addCommand("+0.5", scaleMenu, updateSplashDamageSetting, "giant_explosion_scale", 0.5) missionCommands.addCommand("-0.1", scaleMenu, updateSplashDamageSetting, "giant_explosion_scale", -0.1) missionCommands.addCommand("-0.5", scaleMenu, updateSplashDamageSetting, "giant_explosion_scale", -0.5) local durationMenu = missionCommands.addSubMenu("Duration", giantExplosionMenu) missionCommands.addCommand("+0.25s", durationMenu, updateSplashDamageSetting, "giant_explosion_duration", 0.25) missionCommands.addCommand("-0.25s", durationMenu, updateSplashDamageSetting, "giant_explosion_duration", -0.25) local countMenu = missionCommands.addSubMenu("Explosion Count", giantExplosionMenu) addValueAdjustmentCommands(countMenu, "giant_explosion_count") end if (script_enable == 1) then gameMsg("SPLASH DAMAGE 3.1 SCRIPT RUNNING") env.info("SPLASH DAMAGE 3.1 SCRIPT RUNNING") timer.scheduleFunction(function() protectedCall(track_wpns) return timer.getTime() + refreshRate end, {}, timer.getTime() + refreshRate) if splash_damage_options.giant_explosion_enabled then giantExplosionTargets = {} -- Ensure it’s fresh local targetCount = 0 for coa = 0, 2 do local groups = coalition.getGroups(coa) if groups then for _, group in pairs(groups) do local units = group:getUnits() if units then for _, unit in pairs(units) do local name = unit:getName() if name:find("GiantExplosionTarget") then local pos = unit:getPosition().p giantExplosionTargets[name] = {obj = unit, pos = pos} if splash_damage_options.giant_explosion_target_static then giantExplosionTargets[name].pos = pos end debugMsg("Found GiantExplosionTarget: " .. name .. " at X:" .. pos.x .. " Y:" .. pos.y .. " Z:" .. pos.z) targetCount = targetCount + 1 end end end end end end debugMsg("Total GiantExplosionTargets found: " .. targetCount) timer.scheduleFunction(checkGiantExplosionFlag, {}, timer.getTime() + splash_damage_options.giant_explosion_poll_rate) if not splash_damage_options.giant_explosion_target_static then timer.scheduleFunction(updateTargetPosition, {}, timer.getTime() + 1.0) end end world.addEventHandler(WpnHandler) addSplashDamageMenu() end