@jtff/miztemplate-lib 3.6.5 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1716 @@
1
+ --[[
2
+
3
+ 09 March 2025 (Stevey666) - 3.0
4
+ - 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
5
+ - 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)
6
+ - Added vehicle scanning around a weapon to allow for..
7
+ - Cook offs - you can set vehicles that will cook off i.e ammo trucks, number of explosions, debris explosions, power adjustable
8
+ - Fuel/Tanker explosion and flames - when a fuel tanker blows it will through up a big flame - adjustable in the scripts
9
+ - Added section for vehicles for the above
10
+ - Added radio commands for everything
11
+ - Added in cluster munitions changes (note: barely tested, its not particularly accurate or that useful at this point so leaving disabled)
12
+ - Potential bug - testing, stacking too many units together may cause a MIST error if you're using mist
13
+
14
+ - Setting this as 3.0 as I'd like to be responsive to requests, updates etc - creating a new fork to track this
15
+
16
+
17
+ 10 Feb 2025 (Stevey666) - 2.0.7
18
+ - Fixed AGM 154/Adjusted weapons
19
+ - Added overall damage scaling
20
+ - Added modifier for shaped charges (i.e. Mavericks), adjusted weapon list accordingly
21
+ - Adjusted blast radius and damage calculations, created option for dynamic blast radius
22
+ - Adjusted cascading explosions, added additional "cascade_scaling" modifier and cascade explode threshold modifier. Units wont explode on initial impact unless health drops under threshold
23
+ - Added always_cascade_explode option so you can set it to the old ways of making everything in the blast wave go kaboom
24
+ - Added in game radio commands to change the new options ingame without having to reload everything in mission editor to test it out
25
+
26
+ 12 November 2024 (by JGi | Quéton 1-1)
27
+ - Tweak down radius 100>90 (Thanks Arhibeau)
28
+ - Tweak down some values
29
+
30
+ 20 January 2024 (by JGi | Quéton 1-1)
31
+ - Added missing weapons to explTable
32
+ - Sort weapons in explTable by type
33
+ - Added aircraft type in log when missing
34
+
35
+ 03 May 2023 (KERV)
36
+ Correction AGM 154 (https://forum.dcs.world/topic/289290-splash-damage-20-script-make-explosions-better/page/5/#comment-5207760)
37
+
38
+ 06 March 2023 (Kerv)
39
+ - Add some data for new ammunition
40
+
41
+ 16 April 2022
42
+ spencershepard (GRIMM):
43
+ - Added new/missing weapons to explTable
44
+ - Added new option rocket_multiplier
45
+
46
+ 31 December 2021
47
+ spencershepard (GRIMM):
48
+ - Added many new weapons
49
+ - Added filter for weapons.shells events
50
+ - Fixed mission weapon message option
51
+ - Changed default for damage_model option
52
+
53
+ 21 December 2021
54
+ spencershepard (GRIMM):
55
+ SPLASH DAMAGE 2.0:
56
+ - Added blast wave effect to add timed and scaled secondary explosions on top of game objects
57
+ - Object geometry within blast wave changes damage intensity
58
+ - Damage boost for structures since they are hard to kill, even if very close to large explosions
59
+ - Increased some rocket values in explTable
60
+ - Missing weapons from explTable will display message to user and log to DCS.log so that we can add what's missing
61
+ - Damage model for ground units that will disable their weapons and ability to move with partial damage before they are killed
62
+ - Added options table to allow easy adjustments before release
63
+ - General refactoring and restructure
64
+
65
+ 28 October 2020
66
+ FrozenDroid:
67
+ - Uncommented error logging, actually made it an error log which shows a message box on error.
68
+ - Fixed the too restrictive weapon filter (took out the HE warhead requirement)
69
+
70
+ 2 October 2020
71
+ FrozenDroid:
72
+ - Added error handling to all event handler and scheduled functions. Lua script errors can no longer bring the server down.
73
+ - 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...?)
74
+ --]]
75
+
76
+ ----[[ ##### SCRIPT CONFIGURATION ##### ]]----
77
+ splash_damage_options = {
78
+ --debug options
79
+ ["game_messages"] = false, --enable some messages on screen
80
+ ["debug"] = false, --enable debugging messages
81
+ ["weapon_missing_message"] = false, --false disables messages alerting you to weapons missing from the explTable
82
+
83
+ ["track_pre_explosion_debug"] = false, --Toggle to enable/disable pre-explosion tracking debugging
84
+
85
+ ["enable_radio_menu"] = true, --enables the in-game radio menu for modifying settings
86
+
87
+ ["static_damage_boost"] = 2000, --apply extra damage to Unit.Category.STRUCTUREs with wave explosions
88
+ ["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
89
+ ["larger_explosions"] = true, --secondary explosions on top of weapon impact points, dictated by the values in the explTable
90
+ ["damage_model"] = true, --allow blast wave to affect ground unit movement and weapons
91
+ ["blast_search_radius"] = 90, --this is the max size of any blast wave radius, since we will only find objects within this zone
92
+ ["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.
93
+ ["blast_stun"] = false, --not implemented
94
+ ["unit_disabled_health"] = 30, --if health is below this value after our explosions, disable its movement
95
+ ["unit_cant_fire_health"] = 40, --if health is below this value after our explosions, set ROE to HOLD to simulate damage weapon systems
96
+ ["infantry_cant_fire_health"] = 60, --if health is below this value after our explosions, set ROE to HOLD to simulate severe injury
97
+
98
+ ["rocket_multiplier"] = 1.3, --multiplied by the explTable value for rockets
99
+ ["overall_scaling"] = 1, --overall scaling for explosive power
100
+
101
+ ["apply_shaped_charge_effects"] = true, --apply reduction in blastwave etc for shaped charge munitions
102
+ ["shaped_charge_multiplier"] = 0.2, --multiplier that reduces blast radius and explosion power for shaped charge munitions.
103
+
104
+ ["use_dynamic_blast_radius"] = true, --if true, blast radius is calculated from explosion power; if false, blast_search_radius (90) is used
105
+ ["dynamic_blast_radius_modifier"] = 2, --multiplier for the blast radius
106
+
107
+ ["cascade_scaling"] = 1, --multiplier for secondary (cascade) blast damage, 1 damage fades out too soon, 3 damage seems a good balance
108
+ ["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
109
+ ["always_cascade_explode"] = false, --switch if you want everything to explode like with the original script
110
+
111
+
112
+ --track_pre_explosion/enable_cargo_effects should both be the same value
113
+ ["track_pre_explosion"] = true, --Toggle to enable/disable pre-explosion tracking
114
+ ["enable_cargo_effects"] = true, --Toggle for enabling/disabling cargo explosions and cook-offs
115
+ ["cargo_damage_threshold"] = 60, --Health % below which cargo explodes (0 = destroyed only)
116
+ ["debris_effects"] = true, --Enable debris from cargo cook-offs
117
+ ["debris_power"] = 1, --Power of each debris explosion
118
+ ["debris_count_min"] = 6, --Minimum debris pieces per cook-off
119
+ ["debris_count_max"] = 12, --Maximum debris pieces per cook-off
120
+ ["debris_max_distance"] = 10, --Max distance debris can travel (meters), the min distance from the vehicle will be 10% of this
121
+
122
+ ["ordnance_protection"] = true, --Toggle ordinance protection features
123
+ ["ordnance_protection_radius"] = 10, --Distance in meters to protect nearby bombs
124
+ ["detect_ordnance_destruction"] = true, --Toggle detection of ordnance destroyed by large explosions
125
+ ["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
126
+ ["recent_large_explosion_snap"] = true, --enable looking for a recent large_explosion generated by the script
127
+ ["recent_large_explosion_range"] = 200, --range its looking for in meters for a recent large_explosion generated by the script
128
+ ["recent_large_explosion_time"] = 4, --in seconds how long ago there was a recent large_explosion generated by the script
129
+
130
+ -- Cluster bomb settings
131
+ ["cluster_enabled"] = false,
132
+ ["cluster_base_length"] = 150, -- Base forward spread (meters)
133
+ ["cluster_base_width"] = 200, -- Base lateral spread (meters)
134
+ ["cluster_max_length"] = 300, -- Max forward spread (meters)
135
+ ["cluster_max_width"] = 400, -- Max lateral spread (meters)
136
+ ["cluster_min_length"] = 100, -- Min forward spread
137
+ ["cluster_min_width"] = 150, -- Min lateral spread
138
+ ["cluster_bomblet_reductionmodifier"] = true, -- Use equation to reduce number of bomblets (to make it look better)
139
+ ["cluster_bomblet_damage_modifier"] = 1, -- Adjustable global modifier for bomblet explosive power
140
+ }
141
+
142
+ local script_enable = 1
143
+ refreshRate = 0.1
144
+ ----[[ ##### End of SCRIPT CONFIGURATION ##### ]]----
145
+
146
+ --Helper function: Trim whitespace.
147
+ local function trim(s)
148
+ return s:match("^%s*(.-)%s*$")
149
+ end
150
+
151
+ cargoUnits = {
152
+
153
+ --[[
154
+ flamesize:
155
+
156
+ 1 = small smoke and fire
157
+ 2 = medium smoke and fire
158
+ 3 = large smoke and fire
159
+ 4 = huge smoke and fire
160
+ 5 = small smoke
161
+ 6 = medium smoke
162
+ 7 = large smoke
163
+ 8 = huge smoke
164
+ ]]--
165
+
166
+ --1) M92 R11 Volvo driveable (Fuel Truck Tanker)
167
+ ["r11_volvo_drivable"] = {
168
+ cargoExplosion = true,
169
+ cargoExplosionMult = 2.0,
170
+ cargoExplosionPower = 200,
171
+ cargoCookOff = false,
172
+ cookOffCount = 0,
173
+ cookOffPower = 0,
174
+ cookOffDuration = 0,
175
+ cookOffRandomTiming = false,
176
+ cookOffPowerRandom = 50,
177
+ isTanker = true,
178
+ flameSize = 3,
179
+ flameDuration = 5,
180
+ },
181
+
182
+ --2) Refueler ATMZ-5
183
+ ["ATMZ-5"] = {
184
+ cargoExplosion = true,
185
+ cargoExplosionMult = 2.0,
186
+ cargoExplosionPower = 200,
187
+ cargoCookOff = false,
188
+ cookOffCount = 0,
189
+ cookOffPower = 0,
190
+ cookOffDuration = 0,
191
+ cookOffRandomTiming = false,
192
+ cookOffPowerRandom = 50,
193
+ isTanker = true,
194
+ flameSize = 3,
195
+ flameDuration = 5,
196
+ },
197
+
198
+ --3) Refueler ATZ-10
199
+ ["ATZ-10"] = {
200
+ cargoExplosion = true,
201
+ cargoExplosionMult = 1.8,
202
+ cargoExplosionPower = 200,
203
+ cargoCookOff = false,
204
+ cookOffCount = 0,
205
+ cookOffPower = 0,
206
+ cookOffDuration = 0,
207
+ cookOffRandomTiming = false,
208
+ cookOffPowerRandom = 50,
209
+ isTanker = true,
210
+ flameSize = 3,
211
+ flameDuration = 5,
212
+ },
213
+
214
+ --4) Refueler ATZ-5
215
+ ["ATZ-5"] = {
216
+ cargoExplosion = true,
217
+ cargoExplosionMult = 1.8,
218
+ cargoExplosionPower = 200,
219
+ cargoCookOff = false,
220
+ cookOffCount = 0,
221
+ cookOffPower = 0,
222
+ cookOffDuration = 0,
223
+ cookOffRandomTiming = false,
224
+ cookOffPowerRandom = 50,
225
+ isTanker = true,
226
+ flameSize = 3,
227
+ flameDuration = 5,
228
+ },
229
+
230
+ --5) Refueler M978 HEMTT (Fuel truck tanker)
231
+ ["M978 HEMTT Tanker"] = {
232
+ cargoExplosion = true,
233
+ cargoExplosionMult = 2.0,
234
+ cargoExplosionPower = 200,
235
+ cargoCookOff = false,
236
+ cookOffCount = 0,
237
+ cookOffPower = 0,
238
+ cookOffDuration = 0,
239
+ cookOffRandomTiming = false,
240
+ cookOffPowerRandom = 50,
241
+ isTanker = true,
242
+ flameSize = 3,
243
+ flameDuration = 5,
244
+ },
245
+
246
+ --##### AMMO CARRIERS #####
247
+ ["GAZ-66"] = {
248
+ cargoExplosion = true,
249
+ cargoExplosionMult = 1,
250
+ cargoExplosionPower = 200,
251
+ cargoCookOff = true,
252
+ cookOffCount = 4,
253
+ cookOffPower = 1,
254
+ cookOffDuration = 20,
255
+ cookOffRandomTiming = true,
256
+ cookOffPowerRandom = 50,
257
+ isTanker = false,
258
+ flameSize = 1,
259
+ flameDuration = 30,
260
+ },
261
+
262
+ ["Ural-4320"] = {
263
+ cargoExplosion = true,
264
+ cargoExplosionMult = 1,
265
+ cargoExplosionPower = 200,
266
+ cargoCookOff = true,
267
+ cookOffCount = 4,
268
+ cookOffPower = 1,
269
+ cookOffDuration = 20,
270
+ cookOffRandomTiming = true,
271
+ cookOffPowerRandom = 50,
272
+ isTanker = false,
273
+ flameSize = 1,
274
+ flameDuration = 30,
275
+ },
276
+
277
+ ["ZIL-135"] = {
278
+ cargoExplosion = true,
279
+ cargoExplosionMult = 1,
280
+ cargoExplosionPower = 200,
281
+ cargoCookOff = true,
282
+ cookOffCount = 5,
283
+ cookOffPower = 1,
284
+ cookOffDuration = 20,
285
+ cookOffRandomTiming = true,
286
+ cookOffPowerRandom = 50,
287
+ isTanker = false,
288
+ flameSize = 1,
289
+ flameDuration = 30,
290
+ },
291
+ }
292
+
293
+ --Weapon Explosive Table
294
+ explTable = {
295
+ --*** WWII BOMBS ***
296
+ ["British_GP_250LB_Bomb_Mk1"] = { explosive = 100, shaped_charge = false },
297
+ ["British_GP_250LB_Bomb_Mk4"] = { explosive = 100, shaped_charge = false },
298
+ ["British_GP_250LB_Bomb_Mk5"] = { explosive = 100, shaped_charge = false },
299
+ ["British_GP_500LB_Bomb_Mk1"] = { explosive = 213, shaped_charge = false },
300
+ ["British_GP_500LB_Bomb_Mk4"] = { explosive = 213, shaped_charge = false },
301
+ ["British_GP_500LB_Bomb_Mk4_Short"] = { explosive = 213, shaped_charge = false },
302
+ ["British_GP_500LB_Bomb_Mk5"] = { explosive = 213, shaped_charge = false },
303
+ ["British_MC_250LB_Bomb_Mk1"] = { explosive = 100, shaped_charge = false },
304
+ ["British_MC_250LB_Bomb_Mk2"] = { explosive = 100, shaped_charge = false },
305
+ ["British_MC_500LB_Bomb_Mk1_Short"] = { explosive = 213, shaped_charge = false },
306
+ ["British_MC_500LB_Bomb_Mk2"] = { explosive = 213, shaped_charge = false },
307
+ ["British_SAP_250LB_Bomb_Mk5"] = { explosive = 100, shaped_charge = false },
308
+ ["British_SAP_500LB_Bomb_Mk5"] = { explosive = 213, shaped_charge = false },
309
+ ["British_AP_25LBNo1_3INCHNo1"] = { explosive = 4, shaped_charge = false },
310
+ ["British_HE_60LBSAPNo2_3INCHNo1"] = { explosive = 4, shaped_charge = false },
311
+ ["British_HE_60LBFNo1_3INCHNo1"] = { explosive = 4, shaped_charge = false },
312
+
313
+ ["SC_50"] = { explosive = 20, shaped_charge = false },
314
+ ["ER_4_SC50"] = { explosive = 20, shaped_charge = false },
315
+ ["SC_250_T1_L2"] = { explosive = 100, shaped_charge = false },
316
+ ["SC_501_SC250"] = { explosive = 100, shaped_charge = false },
317
+ ["Schloss500XIIC1_SC_250_T3_J"] = { explosive = 100, shaped_charge = false },
318
+ ["SC_501_SC500"] = { explosive = 213, shaped_charge = false },
319
+ ["SC_500_L2"] = { explosive = 213, shaped_charge = false },
320
+ ["SD_250_Stg"] = { explosive = 100, shaped_charge = false },
321
+ ["SD_500_A"] = { explosive = 213, shaped_charge = false },
322
+
323
+ --*** WWII CBU ***
324
+ ["AB_250_2_SD_2"] = { explosive = 100, shaped_charge = false },
325
+ ["AB_250_2_SD_10A"] = { explosive = 100, shaped_charge = false },
326
+ ["AB_500_1_SD_10A"] = { explosive = 213, shaped_charge = false },
327
+
328
+ --*** WWII ROCKETS ***
329
+ ["3xM8_ROCKETS_IN_TUBES"] = { explosive = 4, shaped_charge = false },
330
+ ["WGr21"] = { explosive = 4, shaped_charge = false },
331
+
332
+ --*** UNGUIDED BOMBS (UGB) ***
333
+ ["M_117"] = { explosive = 201, shaped_charge = false },
334
+ ["AN_M30A1"] = { explosive = 45, shaped_charge = false },
335
+ ["AN_M57"] = { explosive = 100, shaped_charge = false },
336
+ ["AN_M64"] = { explosive = 121, shaped_charge = false },
337
+ ["AN_M65"] = { explosive = 400, shaped_charge = false },
338
+ ["AN_M66"] = { explosive = 800, shaped_charge = false },
339
+ ["AN-M66A2"] = { explosive = 536, shaped_charge = false },
340
+ ["AN-M81"] = { explosive = 100, shaped_charge = false },
341
+ ["AN-M88"] = { explosive = 100, shaped_charge = false },
342
+
343
+ ["Mk_81"] = { explosive = 60, shaped_charge = false },
344
+ ["MK-81SE"] = { explosive = 60, shaped_charge = false },
345
+ ["Mk_82"] = { explosive = 100, shaped_charge = false },
346
+ ["MK_82AIR"] = { explosive = 100, shaped_charge = false },
347
+ ["MK_82SNAKEYE"] = { explosive = 100, shaped_charge = false },
348
+ ["Mk_83"] = { explosive = 274, shaped_charge = false },
349
+ ["Mk_84"] = { explosive = 582, shaped_charge = false },
350
+
351
+ ["HEBOMB"] = { explosive = 40, shaped_charge = false },
352
+ ["HEBOMBD"] = { explosive = 40, shaped_charge = false },
353
+
354
+ ["SAMP125LD"] = { explosive = 60, shaped_charge = false },
355
+ ["SAMP250LD"] = { explosive = 118, shaped_charge = false },
356
+ ["SAMP250HD"] = { explosive = 118, shaped_charge = false },
357
+ ["SAMP400LD"] = { explosive = 274, shaped_charge = false },
358
+ ["SAMP400HD"] = { explosive = 274, shaped_charge = false },
359
+
360
+ ["BR_250"] = { explosive = 100, shaped_charge = false },
361
+ ["BR_500"] = { explosive = 100, shaped_charge = false },
362
+
363
+ ["FAB_100"] = { explosive = 45, shaped_charge = false },
364
+ ["FAB_250"] = { explosive = 118, shaped_charge = false },
365
+ ["FAB_250M54TU"] = { explosive = 118, shaped_charge = false },
366
+ ["FAB-250-M62"] = { explosive = 118, shaped_charge = false },
367
+ ["FAB_500"] = { explosive = 213, shaped_charge = false },
368
+ ["FAB_1500"] = { explosive = 675, shaped_charge = false },
369
+
370
+ --*** UNGUIDED BOMBS WITH PENETRATOR / ANTI-RUNWAY ***
371
+ ["Durandal"] = { explosive = 64, shaped_charge = false },
372
+ ["BLU107B_DURANDAL"] = { explosive = 64, shaped_charge = false },
373
+ ["BAP_100"] = { explosive = 32, shaped_charge = false },
374
+ ["BAP-100"] = { explosive = 32, shaped_charge = false },
375
+ ["BAT-120"] = { explosive = 32, shaped_charge = false },
376
+ ["TYPE-200A"] = { explosive = 107, shaped_charge = false },
377
+ ["BetAB_500"] = { explosive = 98, shaped_charge = false },
378
+ ["BetAB_500ShP"] = { explosive = 107, shaped_charge = false },
379
+
380
+ --*** GUIDED BOMBS (GBU) ***
381
+ ["GBU_10"] = { explosive = 582, shaped_charge = false },
382
+ ["GBU_12"] = { explosive = 100, shaped_charge = false },
383
+ ["GBU_16"] = { explosive = 274, shaped_charge = false },
384
+ ["GBU_24"] = { explosive = 582, shaped_charge = false },
385
+ ["KAB_1500Kr"] = { explosive = 675, shaped_charge = false },
386
+ ["KAB_500Kr"] = { explosive = 213, shaped_charge = false },
387
+ ["KAB_500"] = { explosive = 213, shaped_charge = false },
388
+
389
+ --*** CLUSTER BOMBS (CBU) ***
390
+ --I don't have most of these so can't test them with debug on
391
+ ["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
392
+ ["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
393
+ ["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
394
+ ["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
395
+ ["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
396
+ ["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
397
+ ["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
398
+ ["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
399
+ ["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
400
+ ["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)
401
+ ["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
402
+ ["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
403
+ ["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
404
+ ["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
405
+ ["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
406
+ ["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
407
+ ["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
408
+ ["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
409
+ ["RBK_500_255_PTO_1M"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 126, submunition_explosive = 0.5, submunition_name = "PTO_1M" },
410
+ ["RBK_500_255_ShO"] = { explosive = 0, shaped_charge = false, cluster = true, submunition_count = 565, submunition_explosive = 0.1, submunition_name = "ShO" },
411
+ --*** INS/GPS BOMBS (JDAM) ***
412
+ ["GBU_31"] = { explosive = 582, shaped_charge = false },
413
+ ["GBU_31_V_3B"] = { explosive = 582, shaped_charge = false },
414
+ ["GBU_31_V_2B"] = { explosive = 582, shaped_charge = false },
415
+ ["GBU_31_V_4B"] = { explosive = 582, shaped_charge = false },
416
+ ["GBU_32_V_2B"] = { explosive = 202, shaped_charge = false },
417
+ ["GBU_38"] = { explosive = 100, shaped_charge = false },
418
+ ["GBU_54_V_1B"] = { explosive = 100, shaped_charge = false },
419
+
420
+ --*** GLIDE BOMBS (JSOW) ***
421
+ ["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
422
+ ["AGM_154C"] = { explosive = 305, shaped_charge = false },
423
+ ["AGM_154"] = { explosive = 305, shaped_charge = false },
424
+ ["BK90_MJ1"] = { explosive = 0, shaped_charge = false },
425
+ ["BK90_MJ1_MJ2"] = { explosive = 0, shaped_charge = false },
426
+ ["BK90_MJ2"] = { explosive = 0, shaped_charge = false },
427
+
428
+ ["LS-6-100"] = { explosive = 45, shaped_charge = false },
429
+ ["LS-6-250"] = { explosive = 100, shaped_charge = false },
430
+ ["LS-6-500"] = { explosive = 274, shaped_charge = false },
431
+ ["GB-6"] = { explosive = 0, shaped_charge = false },
432
+ ["GB-6-HE"] = { explosive = 0, shaped_charge = false },
433
+ ["GB-6-SFW"] = { explosive = 0, shaped_charge = false },
434
+
435
+ --*** AIR GROUND MISSILE (AGM) ***
436
+ ["AGM_62"] = { explosive = 400, shaped_charge = false },
437
+ ["AGM_65D"] = { explosive = 38, shaped_charge = true },
438
+ ["AGM_65E"] = { explosive = 80, shaped_charge = true },
439
+ ["AGM_65F"] = { explosive = 80, shaped_charge = true },
440
+ ["AGM_65G"] = { explosive = 80, shaped_charge = true },
441
+ ["AGM_65H"] = { explosive = 38, shaped_charge = true },
442
+ ["AGM_65K"] = { explosive = 80, shaped_charge = true },
443
+ ["AGM_65L"] = { explosive = 80, shaped_charge = true },
444
+ ["AGM_123"] = { explosive = 274, shaped_charge = false },
445
+ ["AGM_130"] = { explosive = 582, shaped_charge = false },
446
+ ["AGM_119"] = { explosive = 176, shaped_charge = false },
447
+ ["AGM_114"] = { explosive = 10, shaped_charge = true },
448
+ ["AGM_114K"] = { explosive = 10, shaped_charge = true },
449
+
450
+ ["Rb 05A"] = { explosive = 217, shaped_charge = false },
451
+ ["RB75"] = { explosive = 38, shaped_charge = false },
452
+ ["RB75A"] = { explosive = 38, shaped_charge = false },
453
+ ["RB75B"] = { explosive = 38, shaped_charge = false },
454
+ ["RB75T"] = { explosive = 80, shaped_charge = false },
455
+ ["HOT3_MBDA"] = { explosive = 15, shaped_charge = false },
456
+ ["C-701T"] = { explosive = 38, shaped_charge = false },
457
+ ["C-701IR"] = { explosive = 38, shaped_charge = false },
458
+
459
+ ["Vikhr_M"] = { explosive = 11, shaped_charge = false },
460
+ ["Vikhr_9M127_1"] = { explosive = 11, shaped_charge = false },
461
+ ["AT_6"] = { explosive = 11, shaped_charge = false },
462
+ ["Ataka_9M120"] = { explosive = 11, shaped_charge = false },
463
+ ["Ataka_9M120F"] = { explosive = 11, shaped_charge = false },
464
+ ["P_9M117"] = { explosive = 0, shaped_charge = false },
465
+
466
+ ["KH-66_Grom"] = { explosive = 108, shaped_charge = false },
467
+ ["X_23"] = { explosive = 111, shaped_charge = false },
468
+ ["X_23L"] = { explosive = 111, shaped_charge = false },
469
+ ["X_28"] = { explosive = 160, shaped_charge = false },
470
+ ["X_25ML"] = { explosive = 89, shaped_charge = false },
471
+ ["X_25MR"] = { explosive = 140, shaped_charge = false },
472
+ ["X_29L"] = { explosive = 320, shaped_charge = false },
473
+ ["X_29T"] = { explosive = 320, shaped_charge = false },
474
+ ["X_29TE"] = { explosive = 320, shaped_charge = false },
475
+
476
+ --*** ANTI-RADAR MISSILE (ARM) ***
477
+ ["AGM_88C"] = { explosive = 89, shaped_charge = false },
478
+ ["AGM_88"] = { explosive = 89, shaped_charge = false },
479
+ ["AGM_122"] = { explosive = 15, shaped_charge = false },
480
+ ["LD-10"] = { explosive = 89, shaped_charge = false },
481
+ ["AGM_45A"] = { explosive = 38, shaped_charge = false },
482
+ ["X_58"] = { explosive = 140, shaped_charge = false },
483
+ ["X_25MP"] = { explosive = 89, shaped_charge = false },
484
+
485
+ --*** ANTI-SHIP MISSILE (ASh) ***
486
+ ["AGM_84D"] = { explosive = 488, shaped_charge = false },
487
+ ["Rb 15F"] = { explosive = 500, shaped_charge = false },
488
+ ["C-802AK"] = { explosive = 500, shaped_charge = false },
489
+
490
+ --*** CRUISE MISSILE ***
491
+ ["CM-802AKG"] = { explosive = 488, shaped_charge = false },
492
+ ["AGM_84E"] = { explosive = 488, shaped_charge = false },
493
+ ["AGM_84H"] = { explosive = 488, shaped_charge = false },
494
+ ["X_59M"] = { explosive = 488, shaped_charge = false },
495
+
496
+ --*** ROCKETS ***
497
+ ["HYDRA_70M15"] = { explosive = 5, shaped_charge = false },
498
+ ["HYDRA_70_MK1"] = { explosive = 5, shaped_charge = false },
499
+ ["HYDRA_70_MK5"] = { explosive = 8, shaped_charge = false },
500
+ ["HYDRA_70_M151"] = { explosive = 5, shaped_charge = false },
501
+ ["HYDRA_70_M151_M433"] = { explosive = 5, shaped_charge = false },
502
+ ["HYDRA_70_M229"] = { explosive = 5, shaped_charge = false },
503
+ ["FFAR Mk1 HE"] = { explosive = 5, shaped_charge = false },
504
+ ["FFAR Mk5 HEAT"] = { explosive = 8, shaped_charge = false },
505
+ ["HVAR"] = { explosive = 5, shaped_charge = false },
506
+ ["Zuni_127"] = { explosive = 8, shaped_charge = false },
507
+ ["ARAKM70BHE"] = { explosive = 5, shaped_charge = false },
508
+ ["ARAKM70BAP"] = { explosive = 8, shaped_charge = false },
509
+ ["SNEB_TYPE251_F1B"] = { explosive = 4, shaped_charge = false },
510
+ ["SNEB_TYPE252_F1B"] = { explosive = 4, shaped_charge = false },
511
+ ["SNEB_TYPE253_F1B"] = { explosive = 5, shaped_charge = false },
512
+ ["SNEB_TYPE256_F1B"] = { explosive = 6, shaped_charge = false },
513
+ ["SNEB_TYPE257_F1B"] = { explosive = 8, shaped_charge = false },
514
+ ["SNEB_TYPE251_F4B"] = { explosive = 4, shaped_charge = false },
515
+ ["SNEB_TYPE252_F4B"] = { explosive = 4, shaped_charge = false },
516
+ ["SNEB_TYPE253_F4B"] = { explosive = 5, shaped_charge = false },
517
+ ["SNEB_TYPE256_F4B"] = { explosive = 6, shaped_charge = false },
518
+ ["SNEB_TYPE257_F4B"] = { explosive = 8, shaped_charge = false },
519
+ ["SNEB_TYPE251_H1"] = { explosive = 4, shaped_charge = false },
520
+ ["SNEB_TYPE252_H1"] = { explosive = 4, shaped_charge = false },
521
+ ["SNEB_TYPE253_H1"] = { explosive = 5, shaped_charge = false },
522
+ ["SNEB_TYPE256_H1"] = { explosive = 6, shaped_charge = false },
523
+ ["SNEB_TYPE257_H1"] = { explosive = 8, shaped_charge = false },
524
+ ["MATRA_F4_SNEBT251"] = { explosive = 8, shaped_charge = false },
525
+ ["MATRA_F4_SNEBT253"] = { explosive = 8, shaped_charge = false },
526
+ ["MATRA_F4_SNEBT256"] = { explosive = 8, shaped_charge = false },
527
+ ["MATRA_F1_SNEBT253"] = { explosive = 8, shaped_charge = false },
528
+ ["MATRA_F1_SNEBT256"] = { explosive = 8, shaped_charge = false },
529
+ ["TELSON8_SNEBT251"] = { explosive = 4, shaped_charge = false },
530
+ ["TELSON8_SNEBT253"] = { explosive = 8, shaped_charge = false },
531
+ ["TELSON8_SNEBT256"] = { explosive = 4, shaped_charge = false },
532
+ ["TELSON8_SNEBT257"] = { explosive = 6, shaped_charge = false },
533
+ ["ARF8M3API"] = { explosive = 8, shaped_charge = false },
534
+ ["UG_90MM"] = { explosive = 8, shaped_charge = false },
535
+ ["S-24A"] = { explosive = 24, shaped_charge = false },
536
+ ["S-25OF"] = { explosive = 194, shaped_charge = false },
537
+ ["S-25OFM"] = { explosive = 150, shaped_charge = false },
538
+ ["S-25O"] = { explosive = 150, shaped_charge = false },
539
+ ["S-25-O"] = { explosive = 150, shaped_charge = false },
540
+ ["S_25L"] = { explosive = 190, shaped_charge = false },
541
+ ["S-5M"] = { explosive = 1, shaped_charge = false },
542
+ ["C_5"] = { explosive = 8, shaped_charge = false },
543
+ ["C5"] = { explosive = 5, shaped_charge = false },
544
+ ["C_8"] = { explosive = 4, shaped_charge = false },
545
+ ["C_8OFP2"] = { explosive = 3, shaped_charge = false },
546
+ ["C_13"] = { explosive = 21, shaped_charge = false },
547
+ ["C_24"] = { explosive = 123, shaped_charge = false },
548
+ ["C_25"] = { explosive = 151, shaped_charge = false },
549
+
550
+ --*** LASER ROCKETS ***
551
+ ["AGR_20"] = { explosive = 8, shaped_charge = false },
552
+ ["AGR_20A"] = { explosive = 8, shaped_charge = false },
553
+ ["AGR_20_M282"] = { explosive = 8, shaped_charge = false },
554
+ ["Hydra_70_M282_MPP"] = { explosive = 8, shaped_charge = false },
555
+ ["BRM-1_90MM"] = { explosive = 8, shaped_charge = false },
556
+ }
557
+
558
+
559
+
560
+
561
+
562
+
563
+ local effectSmokeId = 1
564
+
565
+ ----[[ ##### HELPER/UTILITY FUNCTIONS ##### ]]----
566
+
567
+ local function tableHasKey(table, key)
568
+ return table[key] ~= nil
569
+ end
570
+
571
+ local function debugMsg(str)
572
+ if splash_damage_options.debug == true then
573
+ debugCounter = (debugCounter or 0) + 1
574
+ local uniqueStr = str .. " [" .. timer.getTime() .. " - " .. debugCounter .. "]"
575
+ trigger.action.outText(uniqueStr, 5)
576
+ env.info("DEBUG: " .. uniqueStr)
577
+ end
578
+ end
579
+
580
+ local function gameMsg(str)
581
+ if splash_damage_options.game_messages == true then
582
+ trigger.action.outText(str, 5)
583
+ end
584
+ end
585
+
586
+ local function getDistance(point1, point2)
587
+ local x1 = point1.x
588
+ local y1 = point1.y
589
+ local z1 = point1.z
590
+ local x2 = point2.x
591
+ local y2 = point2.y
592
+ local z2 = point2.z
593
+ local dX = math.abs(x1 - x2)
594
+ local dZ = math.abs(z1 - z2)
595
+ local distance = math.sqrt(dX * dX + dZ * dZ)
596
+ return distance
597
+ end
598
+
599
+ local function getDistance3D(point1, point2)
600
+ local x1 = point1.x
601
+ local y1 = point1.y
602
+ local z1 = point1.z
603
+ local x2 = point2.x
604
+ local y2 = point2.y
605
+ local z2 = point2.z
606
+ local dX = math.abs(x1 - x2)
607
+ local dY = math.abs(y1 - y2)
608
+ local dZ = math.abs(z1 - z2)
609
+ local distance = math.sqrt(dX * dX + dZ * dZ + dY * dY)
610
+ return distance
611
+ end
612
+
613
+ local function vec3Mag(speedVec)
614
+ return math.sqrt(speedVec.x^2 + speedVec.y^2 + speedVec.z^2)
615
+ end
616
+
617
+ local function lookahead(speedVec)
618
+ local speed = vec3Mag(speedVec)
619
+ local dist = speed * refreshRate * 1.5
620
+ return dist
621
+ end
622
+
623
+ --Cluster-specific helper functions from Rockeye script
624
+ local function normalizeVector(vec)
625
+ local mag = math.sqrt(vec.x^2 + vec.z^2)
626
+ if mag > 0 then
627
+ return { x = vec.x / mag, z = vec.z / mag }
628
+ else
629
+ return { x = 1, z = 0 }
630
+ end
631
+ end
632
+ local function calculate_drop_angle(velocity)
633
+ local horizontal_speed = math.sqrt((velocity.x or 0)^2 + (velocity.z or 0)^2)
634
+ local vertical_speed = math.abs(velocity.y or 0)
635
+ if horizontal_speed == 0 then return 90 end
636
+ local angle_rad = math.atan(vertical_speed / horizontal_speed)
637
+ return math.deg(angle_rad)
638
+ end
639
+ local function calculate_dispersion(velocity, burst_altitude)
640
+ local velocity_magnitude = math.sqrt((velocity.x or 0)^2 + (velocity.z or 0)^2)
641
+ local drop_angle = calculate_drop_angle(velocity)
642
+ local length = splash_damage_options.cluster_base_length * (1 + velocity_magnitude / 200)
643
+ local width = splash_damage_options.cluster_base_width * (1 + burst_altitude / 6000)
644
+ local length_jitter = length * (0.85 + math.random() * 0.3)
645
+ local width_jitter = width * (0.85 + math.random() * 0.3)
646
+ return math.max(splash_damage_options.cluster_min_length, math.min(splash_damage_options.cluster_max_length, length_jitter)),
647
+ math.max(splash_damage_options.cluster_min_width, math.min(splash_damage_options.cluster_max_width, width_jitter))
648
+ end
649
+ ----[[ ##### End of HELPER/UTILITY FUNCTIONS ##### ]]----
650
+
651
+ cargoEffectsQueue = {}
652
+ WpnHandler = {}
653
+ tracked_weapons = {}
654
+ local processedUnitsGlobal = {}
655
+
656
+ function getWeaponExplosive(name)
657
+ local weaponData = explTable[name]
658
+ if weaponData then
659
+ return weaponData.explosive, weaponData.shaped_charge
660
+ else
661
+ return 0, false
662
+ end
663
+ end
664
+
665
+ function track_wpns_cluster_scan(args)
666
+ local parentPos = args[1]
667
+ local parentDir = args[2]
668
+ local parentName = args[3]
669
+ local subName = args[4]
670
+ local subCount = args[5]
671
+ local subPower = args[6]
672
+ local parentVel = args[7]
673
+ local attempt = args[8] or 1
674
+ local maxAttempts = 3
675
+ local scanVol = {
676
+ id = world.VolumeType.SPHERE,
677
+ params = { point = parentPos, radius = 400 }
678
+ }
679
+ local bombletsFound = {}
680
+ local allWeaponsFound = {}
681
+ -- General scan for all weapons
682
+ world.searchObjects(Object.Category.WEAPON, scanVol, function(wpn)
683
+ if wpn:isExist() then
684
+ local wpnId = wpn.id_
685
+ local wpnType = wpn:getTypeName()
686
+ local wpnPos = wpn:getPosition().p
687
+ table.insert(allWeaponsFound, { id = wpnId, type = wpnType, x = wpnPos.x, y = wpnPos.y, z = wpnPos.z })
688
+ if wpnType == subName and not tracked_weapons[wpnId] then
689
+ tracked_weapons[wpnId] = {
690
+ wpn = wpn,
691
+ pos = wpnPos,
692
+ speed = wpn:getVelocity(),
693
+ name = wpnType,
694
+ parent = parentName,
695
+ parentVelocity = parentVel
696
+ }
697
+ table.insert(bombletsFound, wpnId)
698
+ 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 .. ")")
699
+ end
700
+ end
701
+ return true
702
+ end)
703
+ -- Log results
704
+ debugMsg("Scanned for submunition '" .. subName .. "' bomblets from '" .. parentName .. "': " .. #bombletsFound .. " found (Attempt " .. attempt .. ")")
705
+ if #allWeaponsFound > 0 then
706
+ local msg = "General scan for '" .. parentName .. "': " .. #allWeaponsFound .. " bomblets released, expected " .. subCount .. " '" .. subName .. "'"
707
+ local typeMismatch = false
708
+ for _, wpn in ipairs(allWeaponsFound) do
709
+ if wpn.type ~= subName then
710
+ typeMismatch = true
711
+ break
712
+ end
713
+ end
714
+ if typeMismatch then
715
+ msg = msg .. " - Mismatch detected! Actual bomblets: "
716
+ for _, wpn in ipairs(allWeaponsFound) do
717
+ msg = msg .. "'" .. wpn.type .. "' (X: " .. string.format("%.0f", wpn.x) .. ", Y: " .. string.format("%.0f", wpn.y) .. ", Z: " .. string.format("%.0f", wpn.z) .. ") "
718
+ end
719
+ msg = msg .. "Script may need changing."
720
+ end
721
+ debugMsg(msg)
722
+ elseif #bombletsFound == 0 and #allWeaponsFound == 0 then
723
+ debugMsg("No bomblets of any type detected for '" .. parentName .. "' (Attempt " .. attempt .. ")")
724
+ end
725
+ -- Retry if no expected submunitions found
726
+ if #bombletsFound == 0 and attempt < maxAttempts then
727
+ debugMsg("No expected submunition '" .. subName .. "' found on attempt " .. attempt .. ", retrying in 0.5s")
728
+ timer.scheduleFunction(track_wpns_cluster_scan, {parentPos, parentDir, parentName, subName, subCount, subPower, parentVel, attempt + 1}, timer.getTime() + 0.5)
729
+ elseif #bombletsFound == 0 and attempt == maxAttempts then
730
+ debugMsg("No submunition '" .. subName .. "' spawned by DCS for '" .. parentName .. "' after " .. maxAttempts .. " attempts - skipping additional explosions")
731
+ end
732
+ end
733
+
734
+ ----[[ ##### Updated track_wpns() Function ##### ]]----
735
+ local recentExplosions = {}
736
+ function track_wpns()
737
+ local weaponsToRemove = {} --Delay removal to ensure all weapons are checked
738
+ for wpn_id_, wpnData in pairs(tracked_weapons) do
739
+ local status, err = pcall(function()
740
+ if wpnData.wpn:isExist() then
741
+ --Update position, direction, speed
742
+ wpnData.pos = wpnData.wpn:getPosition().p
743
+ wpnData.dir = wpnData.wpn:getPosition().x
744
+ wpnData.speed = wpnData.wpn:getVelocity()
745
+ --[[
746
+
747
+
748
+ --Tick-by-tick tracking from weapon's actual position
749
+ local tickVol = {
750
+ id = world.VolumeType.SPHERE,
751
+ params = {
752
+ point = wpnData.pos, --Real weapon position
753
+ radius = 150 --150m radius
754
+ }
755
+ }
756
+ local tickTargets = {}
757
+ world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, tickVol, function(obj)
758
+ if obj:isExist() then
759
+ table.insert(tickTargets, {
760
+ name = obj:getTypeName(),
761
+ distance = getDistance3D(wpnData.pos, obj:getPoint()), --3D distance
762
+ position = obj:getPoint(),
763
+ health = obj:getLife() or 0
764
+ })
765
+ end
766
+ return true
767
+ end)
768
+ 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")
769
+ for i, target in ipairs(tickTargets) do
770
+ 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)
771
+ end
772
+
773
+
774
+ ]]--
775
+
776
+ --Scan potential blast zone in the last frame before impact
777
+ if splash_damage_options.track_pre_explosion then
778
+ local ip = land.getIP(wpnData.pos, wpnData.dir, lookahead(wpnData.speed))
779
+ local predictedImpact = ip or wpnData.pos
780
+
781
+ local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name)
782
+ base_explosive = base_explosive * splash_damage_options.overall_scaling
783
+ if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then
784
+ base_explosive = base_explosive * splash_damage_options.rocket_multiplier
785
+ end
786
+
787
+ local explosionPower = base_explosive
788
+ if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then
789
+ explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier
790
+ end
791
+
792
+ local blastRadius = splash_damage_options.blast_search_radius * 2 --Wider post-scan (180m default)
793
+ if splash_damage_options.use_dynamic_blast_radius then
794
+ blastRadius = math.pow(explosionPower, 1/3) * 10 * splash_damage_options.dynamic_blast_radius_modifier
795
+ end
796
+
797
+ --Tight scan while weapon exists
798
+ --local tightRadius = 50
799
+ --if splash_damage_options.use_dynamic_blast_radius then
800
+ -- tightRadius = math.pow(explosionPower, 1/3) * 5 * splash_damage_options.dynamic_blast_radius_modifier
801
+ --end
802
+ local tightRadius = blastRadius --Use already calculated blastRadius
803
+ local volS = {
804
+ id = world.VolumeType.SPHERE,
805
+ params = {
806
+ point = wpnData.pos, --Use current pos
807
+ radius = tightRadius
808
+ }
809
+ }
810
+ local tightTargets = {}
811
+ local ifFound = function(foundObject, targets, center)
812
+ if foundObject:isExist() then
813
+ local category = foundObject:getCategory()
814
+ if (category == Object.Category.UNIT and (foundObject:getDesc().category == Unit.Category.GROUND_UNIT or foundObject:getDesc().category == Unit.Category.AIRPLANE)) or
815
+ category == Object.Category.STATIC then
816
+ table.insert(targets, {
817
+ name = foundObject:getTypeName(),
818
+ distance = getDistance(center, foundObject:getPoint()),
819
+ health = foundObject:getLife() or 0,
820
+ position = foundObject:getPoint(),
821
+ maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0,
822
+ unit = foundObject
823
+ })
824
+ end
825
+ end
826
+ return true
827
+ end
828
+ if splash_damage_options.track_pre_explosion_debug then
829
+ debugMsg("Scanning tight radius " .. tightRadius .. "m at current pos while weapon exists")
830
+ end
831
+ world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, function(obj) ifFound(obj, tightTargets, wpnData.pos) end)
832
+ wpnData.tightTargets = tightTargets --Store for impact
833
+
834
+ --Wider scan for lastKnownTargets
835
+ volS.params.point = predictedImpact
836
+ volS.params.radius = blastRadius
837
+ local foundTargets = {}
838
+ world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, function(obj) ifFound(obj, foundTargets, predictedImpact) end)
839
+ wpnData.lastKnownTargets = foundTargets
840
+ end
841
+ --Submunition impact handling
842
+ local weaponData = explTable[wpnData.parent or wpnData.name] or { submunition_name = "unknown" }
843
+ if wpnData.name == weaponData.submunition_name then
844
+ local groundHeight = land.getHeight({x = wpnData.pos.x, y = wpnData.pos.z})
845
+ if wpnData.pos.y - groundHeight < 50 then --Impact threshold like old script
846
+ 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))
847
+ local parentWeaponData = explTable[wpnData.parent] or { submunition_count = 30, submunition_explosive = 1 }
848
+ local submunitionCount = parentWeaponData.submunition_count or 30
849
+ local submunitionPower = (parentWeaponData.submunition_explosive or 1) * splash_damage_options.cluster_bomblet_damage_modifier * splash_damage_options.overall_scaling
850
+ if splash_damage_options.cluster_bomblet_reductionmodifier then
851
+ if submunitionCount > 35 then
852
+ local reductionFactor = (60 - 35) / (247 - 35)
853
+ submunitionCount = 35 + math.floor((submunitionCount - 35) * reductionFactor)
854
+ if submunitionCount > 60 then submunitionCount = 60 end
855
+ end
856
+ end
857
+ --Use parent velocity if available, else submunition speed
858
+ local parentDir = wpnData.parentVelocity or wpnData.speed
859
+ local dispersionLength, dispersionWidth = calculate_dispersion(parentDir, 2000) --Match original 2000m
860
+ local dirMag = math.sqrt(parentDir.x^2 + parentDir.z^2)
861
+ local dir = dirMag > 0 and {x = parentDir.x / dirMag, z = parentDir.z / dirMag} or {x = 1, z = 0}
862
+ 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")
863
+ for i = 1, submunitionCount do
864
+ local theta = math.random() * 2 * math.pi
865
+ local r = math.sqrt(math.random())
866
+ local xOffset = r * dispersionLength * 0.5 * math.cos(theta)
867
+ local zOffset = r * dispersionWidth * 0.5 * math.sin(theta)
868
+ local subPos = {
869
+ x = wpnData.pos.x + (xOffset * dir.x - zOffset * dir.z),
870
+ z = wpnData.pos.z + (xOffset * dir.z + zOffset * dir.x)
871
+ }
872
+ subPos.y = land.getHeight({x = subPos.x, y = subPos.z})
873
+ debugMsg("Triggering bomblet #" .. i .. " for submunition '" .. wpnData.name .. "' at X: " .. string.format("%.0f", subPos.x) .. ", Z: " .. string.format("%.0f", subPos.z) .. " with power " .. submunitionPower)
874
+ trigger.action.explosion(subPos, submunitionPower)
875
+ end
876
+ table.insert(weaponsToRemove, wpn_id_)
877
+ end
878
+ end
879
+ else
880
+ --Weapon has impacted
881
+ debugMsg("Weapon " .. wpnData.name .. " no longer exists at " .. timer.getTime() .. "s")
882
+ local ip = land.getIP(wpnData.pos, wpnData.dir, lookahead(wpnData.speed)) --terrain intersection point with weapon's nose. Only search out 20 meters though.
883
+ local explosionPoint
884
+ if not ip then --use last calculated IP
885
+ explosionPoint = wpnData.pos
886
+ else --use intersection point
887
+ explosionPoint = ip
888
+ end
889
+ local chosenTargets = wpnData.tightTargets or {}
890
+ local safeToBlast = true
891
+ if splash_damage_options.ordnance_protection then
892
+ local checkVol = { id = world.VolumeType.SPHERE, params = { point = explosionPoint, radius = splash_damage_options.ordnance_protection_radius } }
893
+ 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")
894
+ world.searchObjects(Object.Category.WEAPON, checkVol, function(obj)
895
+ if obj:isExist() and tracked_weapons[obj.id_] then
896
+ safeToBlast = false
897
+ debugMsg("Skipping explosion for '" .. wpnData.name .. "' - nearby bomb '" .. tracked_weapons[obj.id_].name .. "' within " .. splash_damage_options.ordnance_protection_radius .. "m")
898
+ return false
899
+ end
900
+ return true
901
+ end)
902
+ end
903
+ if safeToBlast then
904
+ debugMsg("FinalPos Check for '" .. wpnData.name .. "': X: " .. string.format("%.0f", explosionPoint.x) .. ", Y: " .. string.format("%.0f", explosionPoint.y) .. ", Z: " .. string.format("%.0f", explosionPoint.z) .. ")")
905
+ local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name)
906
+ base_explosive = base_explosive * splash_damage_options.overall_scaling
907
+ if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then
908
+ base_explosive = base_explosive * splash_damage_options.rocket_multiplier
909
+ end
910
+
911
+ local explosionPower = base_explosive
912
+ if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then
913
+ explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier
914
+ end
915
+
916
+ local blastRadius = splash_damage_options.blast_search_radius * 2 --Wider post-scan (180m default)
917
+ if splash_damage_options.use_dynamic_blast_radius then
918
+ blastRadius = math.pow(explosionPower, 1/3) * 10 * splash_damage_options.dynamic_blast_radius_modifier
919
+ end
920
+
921
+
922
+ --Store pre-explosion state of all tracked weapons for detection
923
+ local preExplosionWeapons = {}
924
+ if splash_damage_options.detect_ordnance_destruction and splash_damage_options.larger_explosions then
925
+ for id, data in pairs(tracked_weapons) do
926
+ if data.wpn:isExist() then
927
+ preExplosionWeapons[id] = {
928
+ name = data.name,
929
+ pos = data.wpn:getPosition().p,
930
+ distance = getDistance3D(explosionPoint, data.wpn:getPosition().p),
931
+ explosive = getWeaponExplosive(data.name) --Store the explosive power
932
+ }
933
+ end
934
+ end
935
+ end
936
+ --Cluster Bomb Handling
937
+ local weaponData = explTable[wpnData.name] or { explosive = 0, shaped_charge = false }
938
+ local isCluster = weaponData.cluster or false
939
+ if splash_damage_options.cluster_enabled and isCluster then
940
+ local submunitionCount = weaponData.submunition_count or 30
941
+ local submunitionPower = (weaponData.submunition_explosive or 1) * splash_damage_options.cluster_bomblet_damage_modifier * splash_damage_options.overall_scaling
942
+ local submunitionName = weaponData.submunition_name or "unknown"
943
+ --Apply bomblet reduction logic if enabled
944
+ if splash_damage_options.cluster_bomblet_reductionmodifier then
945
+ if submunitionCount > 35 then
946
+ local reductionFactor = (60 - 35) / (247 - 35)
947
+ submunitionCount = 35 + math.floor((submunitionCount - 35) * reductionFactor)
948
+ if submunitionCount > 60 then submunitionCount = 60 end --Cap at 60
949
+ end
950
+ end
951
+ -- Extended scan with general bomblet detection
952
+ timer.scheduleFunction(track_wpns_cluster_scan, {explosionPoint, wpnData.dir, wpnData.name, submunitionName, submunitionCount, submunitionPower, wpnData.speed}, timer.getTime() + 0.3)
953
+ else
954
+ --Standard explosion handling
955
+ if splash_damage_options.larger_explosions then
956
+ debugMsg("Triggering initial explosion for '" .. wpnData.name .. "' at power " .. explosionPower)
957
+ trigger.action.explosion(explosionPoint, explosionPower)
958
+ table.insert(recentExplosions, { pos = explosionPoint, time = timer.getTime(), radius = blastRadius })
959
+ debugMsg("Added to recentExplosions for '" .. wpnData.name .. "': X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. ", Time: " .. timer.getTime())
960
+ end
961
+ blastWave(explosionPoint, splash_damage_options.blast_search_radius, wpnData.ordnance, explosionPower, isShapedCharge)
962
+ end
963
+ --detect_ordnance_destruction comes before recent_large_explosion_snap in original
964
+ if splash_damage_options.detect_ordnance_destruction and splash_damage_options.larger_explosions then
965
+ timer.scheduleFunction(function(args)
966
+ local explosionPoint = args[1]
967
+ local blastRadius = args[2]
968
+ local triggeringWeapon = args[3]
969
+ local preExplosionWeapons = args[4]
970
+ for id, preData in pairs(preExplosionWeapons) do
971
+ if tracked_weapons[id] and not tracked_weapons[id].wpn:isExist() then
972
+ if preData.distance <= blastRadius then
973
+ 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)
974
+ gameMsg(msg)
975
+ debugMsg(msg)
976
+ env.info(msg)
977
+ if splash_damage_options.snap_to_ground_if_destroyed_by_large_explosion then
978
+ local groundPos = {
979
+ x = preData.pos.x,
980
+ y = land.getHeight({x = preData.pos.x, y = preData.pos.z}),
981
+ z = preData.pos.z
982
+ }
983
+ local destroyedWeaponPower, isShapedCharge = preData.explosive
984
+ destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.overall_scaling
985
+ if splash_damage_options.rocket_multiplier and tracked_weapons[id].cat == Weapon.Category.ROCKET then
986
+ destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.rocket_multiplier
987
+ end
988
+ if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then
989
+ destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.shaped_charge_multiplier
990
+ end
991
+ 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)
992
+ trigger.action.explosion(groundPos, destroyedWeaponPower)
993
+ end
994
+ end
995
+ end
996
+ end
997
+ end, {explosionPoint, blastRadius, wpnData.name, preExplosionWeapons}, timer.getTime() + 0.2)
998
+ end
999
+ --recent_large_explosion_snap comes after main explosion and detect_ordnance_destruction
1000
+ if splash_damage_options.larger_explosions and splash_damage_options.recent_large_explosion_snap then
1001
+ local currentTime = timer.getTime()
1002
+ for id, data in pairs(tracked_weapons) do
1003
+ if id ~= wpn_id_ and not data.wpn:isExist() then
1004
+ local terrainHeight = land.getHeight({x = data.pos.x, y = data.pos.z})
1005
+ local isMidAir = data.pos.y > terrainHeight + 5
1006
+ local snapTriggered = false
1007
+ for _, explosion in ipairs(recentExplosions) do
1008
+ local timeDiff = currentTime - explosion.time
1009
+ local distance = getDistance3D(data.pos, explosion.pos)
1010
+ 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")
1011
+ if timeDiff <= splash_damage_options.recent_large_explosion_time and distance <= splash_damage_options.recent_large_explosion_range then
1012
+ if isMidAir then
1013
+ local groundPos = { x = data.pos.x, y = terrainHeight, z = data.pos.z }
1014
+ local destroyedWeaponPower, isShapedCharge = getWeaponExplosive(data.name)
1015
+ destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.overall_scaling
1016
+ if splash_damage_options.rocket_multiplier and data.cat == Weapon.Category.ROCKET then
1017
+ destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.rocket_multiplier
1018
+ end
1019
+ if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then
1020
+ destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.shaped_charge_multiplier
1021
+ end
1022
+ 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)
1023
+ trigger.action.explosion(groundPos, destroyedWeaponPower)
1024
+ snapTriggered = true
1025
+ table.insert(weaponsToRemove, id)
1026
+ break
1027
+ else
1028
+ 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")
1029
+ snapTriggered = true
1030
+ break
1031
+ end
1032
+ end
1033
+ end
1034
+ if not snapTriggered then
1035
+ if isMidAir then
1036
+ 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")
1037
+ else
1038
+ debugMsg("Weapon " .. data.name .. " impacted ground, not processed by recent large explosion settings")
1039
+ end
1040
+ end
1041
+ end
1042
+ end
1043
+ local newExplosions = {}
1044
+ for _, explosion in ipairs(recentExplosions) do
1045
+ if currentTime - explosion.time <= splash_damage_options.recent_large_explosion_time then
1046
+ table.insert(newExplosions, explosion)
1047
+ end
1048
+ end
1049
+ recentExplosions = newExplosions
1050
+ end
1051
+ --Mark units as destroyed to avoid MiST accessing them
1052
+ local destroyedUnits = {}
1053
+ for _, target in ipairs(chosenTargets) do
1054
+ if target.unit:isExist() and target.health > 0 and target.unit:getLife() <= 0 then
1055
+ destroyedUnits[target.name] = true
1056
+ debugMsg("Marked " .. target.name .. " as destroyed pre-impact")
1057
+ end
1058
+ end
1059
+ --Schedule explosion handling with original 0.1-second delay, enhanced error handling
1060
+ timer.scheduleFunction(function(args)
1061
+ local finalPos = args[1]
1062
+ local explosionPoint = args[2]
1063
+ local explosionPower = args[3]
1064
+ local isShapedCharge = args[4]
1065
+ local blastRadius = args[5]
1066
+ local chosenTargets = args[6]
1067
+ local weaponName = args[7]
1068
+ local wpnData = args[8]
1069
+ debugMsg("Starting impact handling for " .. weaponName .. " at " .. timer.getTime() .. "s")
1070
+ local status, err = pcall(function()
1071
+ --Log pre-explosion targets
1072
+ if splash_damage_options.track_pre_explosion then
1073
+ if #chosenTargets > 0 then
1074
+ local msg = "Targets in blast zone for " .. weaponName .. " BEFORE explosion (last frame, using finalPos):\n"
1075
+ for i, target in ipairs(chosenTargets) do
1076
+ msg = msg .. "- " .. target.name .. " (Dist: " .. string.format("%.1f", target.distance) .. "m, Health: " .. target.health .. ")\n"
1077
+ end
1078
+ debugMsg(msg)
1079
+ env.info("SplashDamage Pre-Explosion (Last Frame): " .. msg)
1080
+ else
1081
+ debugMsg("No targets in blast zone for " .. weaponName .. " BEFORE explosion (last frame)")
1082
+ env.info("SplashDamage Pre-Explosion (Last Frame): No targets in blast zone for " .. weaponName)
1083
+ end
1084
+ end
1085
+
1086
+ blastWave(explosionPoint, splash_damage_options.blast_search_radius, wpnData.ordnance, explosionPower, isShapedCharge)
1087
+
1088
+ --Post-explosion analysis and queue cargo effects
1089
+ if splash_damage_options.track_pre_explosion then
1090
+ timer.scheduleFunction(function(innerArgs)
1091
+ local impactPoint = innerArgs[1]
1092
+ local blastRadius = innerArgs[2]
1093
+ local preExplosionTargets = innerArgs[3] or {}
1094
+ local weaponName = innerArgs[4]
1095
+ local weaponPower = innerArgs[5]
1096
+ debugMsg("Starting post-explosion analysis for " .. weaponName .. " at " .. timer.getTime() .. "s")
1097
+
1098
+ --Scan all units in wider radius
1099
+ local postExplosionTargets = {}
1100
+ local volS = {
1101
+ id = world.VolumeType.SPHERE,
1102
+ params = {
1103
+ point = impactPoint,
1104
+ radius = blastRadius
1105
+ }
1106
+ }
1107
+
1108
+ local ifFound = function(foundObject)
1109
+ if foundObject:isExist() then
1110
+ local category = foundObject:getCategory()
1111
+ if (category == Object.Category.UNIT and (foundObject:getDesc().category == Unit.Category.GROUND_UNIT or foundObject:getDesc().category == Unit.Category.AIRPLANE)) or
1112
+ category == Object.Category.STATIC then
1113
+ table.insert(postExplosionTargets, {
1114
+ name = foundObject:getTypeName(),
1115
+ health = foundObject:getLife() or 0,
1116
+ position = foundObject:getPoint(),
1117
+ maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0
1118
+ })
1119
+ end
1120
+ end
1121
+ return true
1122
+ end
1123
+
1124
+ world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound)
1125
+
1126
+ local msg = "Post-explosion analysis for " .. weaponName .. ":\n"
1127
+
1128
+ --Match pre-detected units
1129
+ for _, preTarget in ipairs(preExplosionTargets) do
1130
+ local found = false
1131
+ local postHealth = 0
1132
+ local postPosition = nil
1133
+ for _, postTarget in ipairs(postExplosionTargets) do
1134
+ if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then
1135
+ found = true
1136
+ postHealth = postTarget.health
1137
+ postPosition = postTarget.position
1138
+ break
1139
+ end
1140
+ end
1141
+
1142
+ local healthPercent = preTarget.maxHealth > 0 and (postHealth / preTarget.maxHealth * 100) or 0
1143
+ local status = ""
1144
+
1145
+ if not found or postHealth <= 0 then
1146
+ status = "WAS FULLY DESTROYED"
1147
+ elseif healthPercent < splash_damage_options.cargo_damage_threshold then
1148
+ status = "WAS DAMAGED BELOW THRESHOLD"
1149
+ else
1150
+ status = "SURVIVED (Health: " .. postHealth .. ")"
1151
+ end
1152
+
1153
+ --Always include coords in status message
1154
+ local coords = found and postPosition or preTarget.position
1155
+ local statusMsg = status .. " AT " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", coords.x, coords.y, coords.z) .. " (Pre: " .. preTarget.health .. ", Post: " .. postHealth .. ")"
1156
+ --Check if target is in cargoUnits and within blast radius
1157
+ local cargoData = cargoUnits[preTarget.name]
1158
+ if cargoData and preTarget.distance <= blastRadius and
1159
+ (not found or postHealth <= 0 or healthPercent < splash_damage_options.cargo_damage_threshold) then
1160
+
1161
+ if splash_damage_options.enable_cargo_effects then
1162
+ local cargoPower = cargoData.cargoExplosionPower or weaponPower --Use fixed power or fallback
1163
+ table.insert(cargoEffectsQueue, {
1164
+ name = preTarget.name,
1165
+ distance = preTarget.distance,
1166
+ coords = coords,
1167
+ power = cargoPower,
1168
+ explosion = cargoData.cargoExplosion,
1169
+ cookOff = cargoData.cargoCookOff,
1170
+ cookOffCount = cargoData.cookOffCount,
1171
+ cookOffPower = cargoData.cookOffPower,
1172
+ cookOffDuration = cargoData.cookOffDuration,
1173
+ cookOffRandomTiming = cargoData.cookOffRandomTiming,
1174
+ cookOffPowerRandom = cargoData.cookOffPowerRandom,
1175
+ isTanker = cargoData.isTanker,
1176
+ flameSize = cargoData.flameSize,
1177
+ flameDuration = cargoData.flameDuration
1178
+ })
1179
+ statusMsg = statusMsg .. " WITH CARGO EXPLOSION (Power: " .. cargoPower .. ")"
1180
+ if cargoData.cargoCookOff and cargoData.cookOffCount > 0 then
1181
+ statusMsg = statusMsg .. " WITH COOK-OFF (" .. cargoData.cookOffCount .. " blasts over " .. cargoData.cookOffDuration .. "s)"
1182
+ end
1183
+ end
1184
+ end
1185
+
1186
+ msg = msg .. "- " .. preTarget.name .. " " .. statusMsg .. "\n"
1187
+ end
1188
+ --Check for additional units
1189
+ for _, postTarget in ipairs(postExplosionTargets) do
1190
+ local isPreDetected = false
1191
+ for _, preTarget in ipairs(preExplosionTargets) do
1192
+ if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then
1193
+ isPreDetected = true
1194
+ break
1195
+ end
1196
+ end
1197
+ if not isPreDetected then
1198
+ local coords = postTarget.position
1199
+ local healthPercent = postTarget.maxHealth > 0 and (postTarget.health / postTarget.maxHealth * 100) or 0
1200
+ local status = postTarget.health <= 0 and "WAS FULLY DESTROYED" or
1201
+ (healthPercent < splash_damage_options.cargo_damage_threshold and "WAS DAMAGED BELOW THRESHOLD" or
1202
+ "SURVIVED (Health: " .. postTarget.health .. ")")
1203
+ local statusMsg = status .. " AT " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", coords.x, coords.y, coords.z) .. " (Pre: Unknown, Post: " .. postTarget.health .. ")"
1204
+ local cargoData = cargoUnits[postTarget.name]
1205
+ if cargoData and (postTarget.health <= 0 or healthPercent < splash_damage_options.cargo_damage_threshold) then
1206
+ if splash_damage_options.enable_cargo_effects then
1207
+ local cargoPower = cargoData.cargoExplosionPower or weaponPower --Use fixed power or fallback
1208
+ local distance = getDistance(impactPoint, coords)
1209
+ table.insert(cargoEffectsQueue, {
1210
+ name = postTarget.name,
1211
+ distance = distance,
1212
+ coords = coords,
1213
+ power = cargoPower,
1214
+ explosion = cargoData.cargoExplosion,
1215
+ cookOff = cargoData.cargoCookOff,
1216
+ cookOffCount = cargoData.cookOffCount,
1217
+ cookOffPower = cargoData.cookOffPower,
1218
+ cookOffDuration = cargoData.cookOffDuration,
1219
+ cookOffRandomTiming = cargoData.cookOffRandomTiming,
1220
+ cookOffPowerRandom = cargoData.cookOffPowerRandom,
1221
+ isTanker = cargoData.isTanker,
1222
+ flameSize = cargoData.flameSize,
1223
+ flameDuration = cargoData.flameDuration
1224
+ })
1225
+ statusMsg = statusMsg .. " WITH CARGO EXPLOSION (Power: " .. cargoPower .. ")"
1226
+ if cargoData.cargoCookOff and cargoData.cookOffCount > 0 then
1227
+ statusMsg = statusMsg .. " WITH COOK-OFF (" .. cargoData.cookOffCount .. " blasts over " .. cargoData.cookOffDuration .. "s)"
1228
+ end
1229
+ end
1230
+ end
1231
+ msg = msg .. "- " .. postTarget.name .. " " .. statusMsg .. "\n"
1232
+ end
1233
+ end
1234
+
1235
+ --Schedule all queued cargo effects
1236
+ if #cargoEffectsQueue > 0 then
1237
+ local effectIndex = 0
1238
+ local processedCargoUnits = {} --Track processed units
1239
+ local flamePositions = {} --Track flame coords with 3m radius
1240
+ for _, effect in ipairs(cargoEffectsQueue) do
1241
+ local unitKey = effect.name .. "_" .. effect.coords.x .. "_" .. effect.coords.z
1242
+ if not processedUnitsGlobal[unitKey] and not processedCargoUnits[unitKey] then
1243
+ if effect.explosion then
1244
+ debugMsg("Triggering cargo explosion for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m with power " .. effect.power .. " scheduled at " .. effectIndex .. "s")
1245
+ timer.scheduleFunction(function(params)
1246
+ 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])
1247
+ trigger.action.explosion(params[1], params[2])
1248
+ end, {effect.coords, effect.power}, timer.getTime() + effectIndex + 0.1) --Slight delay for visibility
1249
+ if effect.isTanker then
1250
+ local flameSize = effect.flameSize or 3
1251
+ local flameDuration = effect.flameDuration --Use cargoUnits value directly, no default
1252
+ local flameDensity = 1.0 --Max density for visibility
1253
+ local effectId = effectSmokeId
1254
+ effectSmokeId = effectSmokeId + 1
1255
+ --Check for nearby flames within 3m
1256
+ local isDuplicate = false
1257
+ for _, pos in pairs(flamePositions) do
1258
+ if getDistance3D(effect.coords, pos) < 3 then
1259
+ isDuplicate = true
1260
+ 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))
1261
+ break
1262
+ end
1263
+ end
1264
+ if not isDuplicate then
1265
+ 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")
1266
+ timer.scheduleFunction(function(params)
1267
+ --Adjust Y-coordinate to terrain height + offset
1268
+ local terrainHeight = land.getHeight({x = params[1].x, y = params[1].z})
1269
+ local adjustedCoords = {x = params[1].x, y = terrainHeight + 2, z = params[1].z}
1270
+ debugMsg("Spawning flame effect at X: " .. string.format("%.0f", adjustedCoords.x) .. ", Y: " .. string.format("%.0f", adjustedCoords.y) .. ", Z: " .. string.format("%.0f", adjustedCoords.z))
1271
+ trigger.action.explosion(adjustedCoords, 10) --Small explosion to force visibility
1272
+ trigger.action.effectSmokeBig(adjustedCoords, params[2], params[3], params[4])
1273
+ end, {effect.coords, flameSize, flameDensity, effectId}, timer.getTime() + effectIndex + 0.2) --Slight delay
1274
+ timer.scheduleFunction(function(id)
1275
+ debugMsg("Stopping flame effect for " .. effect.name .. " (ID: " .. id .. ")")
1276
+ trigger.action.effectSmokeStop(id)
1277
+ end, effectId, timer.getTime() + effectIndex + flameDuration + 0.2)
1278
+ table.insert(flamePositions, effect.coords)
1279
+ end
1280
+ end
1281
+ end
1282
+ debugMsg("Checking cook-off for " .. effect.name .. ": cookOff=" .. tostring(effect.cookOff) .. ", count=" .. tostring(effect.cookOffCount))
1283
+ if effect.cookOff and effect.cookOffCount > 0 then
1284
+ 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")
1285
+ for i = 1, effect.cookOffCount do
1286
+ local delay = effect.cookOffRandomTiming and math.random() * effect.cookOffDuration or (i - 1) * (effect.cookOffDuration / effect.cookOffCount)
1287
+ local basePower = effect.cookOffPower
1288
+ local powerVariation = effect.cookOffPowerRandom / 100
1289
+ local cookOffPower = effect.cookOffPowerRandom == 0 and basePower or basePower * (1 + powerVariation * (math.random() * 2 - 1))
1290
+ 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))
1291
+ timer.scheduleFunction(function(params)
1292
+ local pos = params[1]
1293
+ local power = params[2]
1294
+ debugMsg("Executing cook-off at " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", pos.x, pos.y, pos.z) .. " with power " .. power)
1295
+ trigger.action.explosion(pos, power)
1296
+ end, {effect.coords, cookOffPower}, timer.getTime() + effectIndex + delay)
1297
+ end
1298
+ --Debris burst only if cook-off is true and enabled
1299
+ if splash_damage_options.debris_effects then
1300
+ local debrisCount = math.random(splash_damage_options.debris_count_min, splash_damage_options.debris_count_max)
1301
+ for j = 1, debrisCount do
1302
+ --Random spherical offset
1303
+ local theta = math.random() * 2 * math.pi --Horizontal angle
1304
+ local phi = math.acos(math.random() * 2 - 1) --Vertical angle for sphere
1305
+ local minDist = splash_damage_options.debris_max_distance * 0.1 --10% of max
1306
+ local maxDist = splash_damage_options.debris_max_distance
1307
+ local r = math.random() * (maxDist - minDist) + minDist --10% to full max distance
1308
+ local debrisX = effect.coords.x + r * math.sin(phi) * math.cos(theta)
1309
+ local debrisZ = effect.coords.z + r * math.sin(phi) * math.sin(theta)
1310
+ local terrainY = land.getHeight({x = debrisX, y = debrisZ})
1311
+ local debrisY = terrainY + math.random() * maxDist --0 to max_distance above ground
1312
+ local debrisPos = {x = debrisX, y = debrisY, z = debrisZ}
1313
+ local debrisPower = splash_damage_options.debris_power
1314
+ local debrisDelay = (j - 1) * (effect.cookOffDuration / debrisCount) --Spread over cook-off duration
1315
+ timer.scheduleFunction(function(debrisArgs)
1316
+ local dPos = debrisArgs[1]
1317
+ local dPower = debrisArgs[2]
1318
+ 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)
1319
+ trigger.action.explosion(dPos, dPower)
1320
+ end, {debrisPos, debrisPower}, timer.getTime() + effectIndex + debrisDelay)
1321
+ end
1322
+ end
1323
+ end
1324
+
1325
+
1326
+ processedCargoUnits[unitKey] = true
1327
+ processedUnitsGlobal[unitKey] = true
1328
+ effectIndex = effectIndex + 3 --3 secs spacing if not random
1329
+ end
1330
+ end
1331
+ --Clear the queue after scheduling
1332
+ cargoEffectsQueue = {}
1333
+ end
1334
+
1335
+ debugMsg(msg)
1336
+ env.info("SplashDamage Post-Explosion: " .. msg)
1337
+ end, {finalPos, blastRadius, chosenTargets, weaponName, explosionPower}, timer.getTime() + 1)
1338
+ end
1339
+ end)
1340
+ if not status then
1341
+ debugMsg("Impact handling error for '" .. weaponName .. "': " .. err)
1342
+ end
1343
+ end, {explosionPoint, explosionPoint, explosionPower, isShapedCharge, blastRadius, chosenTargets, wpnData.name, wpnData}, timer.getTime() + 0.1)
1344
+ else
1345
+ debugMsg("Explosion skipped due to ordnance protection for '" .. wpnData.name .. "'")
1346
+ if splash_damage_options.larger_explosions then
1347
+ table.insert(recentExplosions, { pos = explosionPoint, time = timer.getTime(), radius = blastRadius })
1348
+ debugMsg("Skipped explosion logged for snap check for '" .. wpnData.name .. "': X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. ", Time: " .. timer.getTime())
1349
+ end
1350
+ end
1351
+ table.insert(weaponsToRemove, wpn_id_)
1352
+ end
1353
+ end)
1354
+ if not status then
1355
+ debugMsg("Error in track_wpns for '" .. (wpnData.name or "unknown weapon") .. "': " .. err)
1356
+ end
1357
+ end
1358
+ --Perform all removals after iteration
1359
+ for _, id in ipairs(weaponsToRemove) do
1360
+ tracked_weapons[id] = nil
1361
+ end
1362
+ return timer.getTime() + refreshRate
1363
+ end
1364
+ function onWpnEvent(event)
1365
+ if event.id == world.event.S_EVENT_SHOT then
1366
+ if event.weapon then
1367
+ local ordnance = event.weapon
1368
+ local typeName = trim(ordnance:getTypeName())
1369
+ env.info("Weapon fired: [" .. typeName .. "]")
1370
+ debugMsg("Weapon fired: [" .. typeName .. "]")
1371
+
1372
+ if string.find(typeName, "weapons.shells") then
1373
+ debugMsg("Event shot, but not tracking: " .. typeName)
1374
+ env.info("SplashDamage: event shot, but not tracking: " .. typeName .. " (" .. event.initiator:getTypeName() .. ")")
1375
+ return
1376
+ end
1377
+
1378
+ if not explTable[typeName] then
1379
+ env.info("SplashDamage: " .. typeName .. " missing from script (" .. event.initiator:getTypeName() .. ")")
1380
+ if splash_damage_options.weapon_missing_message == true then
1381
+ trigger.action.outText("SplashDamage: " .. typeName .. " missing from script (" .. (event.initiator and event.initiator:isExist() and event.initiator:getTypeName() or "no initiator") .. ")", 3)
1382
+ -- if mist and mist.utils and mist.utils.tableShow then --Only if MiST is present
1383
+ -- local success, desc = pcall(mist.utils.tableShow, ordnance:getDesc())
1384
+ -- if success then
1385
+ -- debugMsg("desc for [" .. typeName .. "]: " .. desc)
1386
+ -- else
1387
+ -- debugMsg("Could not retrieve description for [" .. typeName .. "]. Object may no longer exist.")
1388
+ -- end
1389
+ -- end
1390
+ env.info("Current keys in explTable:")
1391
+ for k, v in pairs(explTable) do
1392
+ env.info("Key: [" .. k .. "]")
1393
+ end
1394
+ end
1395
+ end
1396
+
1397
+ if (ordnance:getDesc().category ~= 0) and event.initiator then
1398
+ if ordnance:getDesc().category == 1 then
1399
+ if (ordnance:getDesc().MissileCategory ~= 1 and ordnance:getDesc().MissileCategory ~= 2) then
1400
+ 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() }
1401
+ end
1402
+ else
1403
+ 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() }
1404
+ end
1405
+ end
1406
+ end
1407
+ end
1408
+ end
1409
+
1410
+ local function protectedCall(...)
1411
+ local status, retval = pcall(...)
1412
+ if not status then
1413
+ env.warning("Splash damage script error... gracefully caught! " .. retval, true)
1414
+ end
1415
+ end
1416
+
1417
+ function WpnHandler:onEvent(event)
1418
+ protectedCall(onWpnEvent, event)
1419
+ end
1420
+
1421
+ function explodeObject(args)
1422
+ local point = args[1]
1423
+ local distance = args[2]
1424
+ local power = args[3]
1425
+ trigger.action.explosion(point, power)
1426
+ end
1427
+
1428
+ function blastWave(_point, _radius, weapon, power, isShapedCharge)
1429
+ if isShapedCharge then
1430
+ _radius = _radius * splash_damage_options.shaped_charge_multiplier
1431
+ end
1432
+ if splash_damage_options.use_dynamic_blast_radius then
1433
+ local dynamicRadius = math.pow(power, 1/3) * 5 * splash_damage_options.dynamic_blast_radius_modifier
1434
+ if isShapedCharge then
1435
+ _radius = dynamicRadius * splash_damage_options.shaped_charge_multiplier
1436
+ else
1437
+ _radius = dynamicRadius
1438
+ end
1439
+ end
1440
+
1441
+ local foundUnits = {}
1442
+ local volS = {
1443
+ id = world.VolumeType.SPHERE,
1444
+ params = {
1445
+ point = _point,
1446
+ radius = _radius
1447
+ }
1448
+ }
1449
+
1450
+ local ifFound = function(foundObject, val)
1451
+ if foundObject:getDesc().category == Unit.Category.GROUND_UNIT and foundObject:getCategory() == Object.Category.UNIT then
1452
+ foundUnits[#foundUnits + 1] = foundObject
1453
+ end
1454
+ if foundObject:getDesc().category == Unit.Category.GROUND_UNIT then
1455
+ if splash_damage_options.blast_stun == true then
1456
+ --suppressUnit(foundObject, 2, weapon)
1457
+ end
1458
+ end
1459
+ if splash_damage_options.wave_explosions then
1460
+ local obj = foundObject
1461
+ local obj_location = obj:getPoint()
1462
+ local dist = getDistance(_point, obj_location)
1463
+ local timing = dist / 500
1464
+ if obj:isExist() and tableHasKey(obj:getDesc(), "box") then
1465
+ local length = (obj:getDesc().box.max.x + math.abs(obj:getDesc().box.min.x))
1466
+ local height = (obj:getDesc().box.max.y + math.abs(obj:getDesc().box.min.y))
1467
+ local depth = (obj:getDesc().box.max.z + math.abs(obj:getDesc().box.min.z))
1468
+ local _length = length
1469
+ local _depth = depth
1470
+ if depth > length then
1471
+ _length = depth
1472
+ _depth = length
1473
+ end
1474
+ local surface_distance = dist - _depth / 2
1475
+ local scaled_power_factor = 0.006 * power + 1
1476
+ local intensity = (power * scaled_power_factor) / (4 * math.pi * surface_distance^2)
1477
+ local surface_area = _length * height
1478
+ local damage_for_surface = intensity * surface_area
1479
+ if damage_for_surface > splash_damage_options.cascade_damage_threshold then
1480
+ local explosion_size = damage_for_surface
1481
+ if obj:getDesc().category == Unit.Category.STRUCTURE then
1482
+ explosion_size = intensity * splash_damage_options.static_damage_boost
1483
+ end
1484
+ if explosion_size > power then explosion_size = power end
1485
+ local triggerExplosion = false
1486
+ if splash_damage_options.always_cascade_explode then
1487
+ triggerExplosion = true
1488
+ else
1489
+ if obj:getDesc().life then
1490
+ local healthPercent = (obj:getLife() / obj:getDesc().life) * 100
1491
+ if healthPercent <= splash_damage_options.cascade_explode_threshold then
1492
+ triggerExplosion = true
1493
+ end
1494
+ --Queue cargo effects for units below
1495
+ local cargoData = cargoUnits[obj:getTypeName()]
1496
+ if cargoData and healthPercent <= splash_damage_options.cargo_damage_threshold and splash_damage_options.enable_cargo_effects then
1497
+ local cargoPower = power * cargoData.cargoExplosionMult
1498
+ table.insert(cargoEffectsQueue, {
1499
+ name = obj:getTypeName(),
1500
+ distance = dist,
1501
+ coords = obj_location,
1502
+ power = cargoPower,
1503
+ explosion = cargoData.cargoExplosion,
1504
+ cookOff = cargoData.cargoCookOff,
1505
+ cookOffCount = cargoData.cookOffCount,
1506
+ cookOffPower = cargoData.cookOffPower,
1507
+ cookOffDuration = cargoData.cookOffDuration,
1508
+ cookOffRandomTiming = cargoData.cookOffRandomTiming,
1509
+ cookOffPowerRandom = cargoData.cookOffPowerRandom,
1510
+ isTanker = cargoData.isTanker,
1511
+ flameSize = cargoData.flameSize,
1512
+ flameDuration = cargoData.flameDuration
1513
+ })
1514
+ end
1515
+ else
1516
+ triggerExplosion = true
1517
+ end
1518
+ if not triggerExplosion and obj:getDesc().category == Unit.Category.GROUND_UNIT then
1519
+ local health = obj:getLife() or 0
1520
+ if health <= 0 then
1521
+ triggerExplosion = true
1522
+ end
1523
+ end
1524
+ end
1525
+ if triggerExplosion then
1526
+ timer.scheduleFunction(explodeObject, {obj_location, dist, explosion_size * splash_damage_options.cascade_scaling}, timer.getTime() + timing)
1527
+ end
1528
+ end
1529
+ end
1530
+ end
1531
+ return true
1532
+ end
1533
+
1534
+ world.searchObjects(Object.Category.UNIT, volS, ifFound)
1535
+ world.searchObjects(Object.Category.STATIC, volS, ifFound)
1536
+ world.searchObjects(Object.Category.SCENERY, volS, ifFound)
1537
+ world.searchObjects(Object.Category.CARGO, volS, ifFound)
1538
+ if splash_damage_options.damage_model then
1539
+ timer.scheduleFunction(modelUnitDamage, foundUnits, timer.getTime() + 1.5)
1540
+ end
1541
+ end
1542
+
1543
+ function modelUnitDamage(units)
1544
+ for i, unit in ipairs(units) do
1545
+ if unit:isExist() then
1546
+ local health = (unit:getLife() / unit:getDesc().life) * 100
1547
+ if unit:hasAttribute("Infantry") and health > 0 then
1548
+ if health <= splash_damage_options.infantry_cant_fire_health then
1549
+ unit:getController():setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD)
1550
+ end
1551
+ end
1552
+ if unit:getDesc().category == Unit.Category.GROUND_UNIT and (not unit:hasAttribute("Infantry")) and health > 0 then
1553
+ if health <= splash_damage_options.unit_cant_fire_health then
1554
+ unit:getController():setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD)
1555
+ gameMsg(unit:getTypeName() .. " weapons disabled")
1556
+ end
1557
+ if health <= splash_damage_options.unit_disabled_health and health > 0 then
1558
+ unit:getController():setTask({id = 'Hold', params = {}})
1559
+ unit:getController():setOnOff(false)
1560
+ gameMsg(unit:getTypeName() .. " disabled")
1561
+ end
1562
+ end
1563
+ end
1564
+ end
1565
+ end
1566
+
1567
+ function updateSplashDamageSetting(setting, increment)
1568
+ if not splash_damage_options[setting] then
1569
+ env.info("Error: Setting " .. setting .. " does not exist.")
1570
+ return
1571
+ end
1572
+
1573
+ local newValue = math.max(0, splash_damage_options[setting] + increment)
1574
+ env.info("Updating " .. setting .. " from " .. tostring(splash_damage_options[setting]) .. " to " .. tostring(newValue))
1575
+ splash_damage_options[setting] = newValue
1576
+ trigger.action.outText("Updated " .. setting .. " to: " .. tostring(splash_damage_options[setting]), 5)
1577
+ end
1578
+
1579
+ function toggleSplashDamageSetting(setting)
1580
+ splash_damage_options[setting] = not splash_damage_options[setting]
1581
+ trigger.action.outText("Toggled " .. setting .. " to: " .. tostring(splash_damage_options[setting]), 5)
1582
+
1583
+ if setting == "enable_radio_menu" then
1584
+ if splash_damage_options.enable_radio_menu then
1585
+ addSplashDamageMenu()
1586
+ else
1587
+ missionCommands.removeItem(splash_damage_menu)
1588
+ splash_damage_menu = nil
1589
+ end
1590
+ end
1591
+ end
1592
+
1593
+ function addValueAdjustmentCommands(menu, setting)
1594
+ missionCommands.addCommand("+0.1", menu, updateSplashDamageSetting, setting, 0.1)
1595
+ missionCommands.addCommand("+1", menu, updateSplashDamageSetting, setting, 1)
1596
+ missionCommands.addCommand("+10", menu, updateSplashDamageSetting, setting, 10)
1597
+ missionCommands.addCommand("+100", menu, updateSplashDamageSetting, setting, 100)
1598
+
1599
+ missionCommands.addCommand("-0.1", menu, updateSplashDamageSetting, setting, -0.1)
1600
+ missionCommands.addCommand("-1", menu, updateSplashDamageSetting, setting, -1)
1601
+ missionCommands.addCommand("-10", menu, updateSplashDamageSetting, setting, -10)
1602
+ missionCommands.addCommand("-100", menu, updateSplashDamageSetting, setting, -100)
1603
+ end
1604
+
1605
+ function exitSplashDamageMenu()
1606
+ if splash_damage_menu then
1607
+ missionCommands.removeItem(splash_damage_menu)
1608
+ splash_damage_menu = nil
1609
+ end
1610
+ end
1611
+
1612
+ function addSplashDamageMenu()
1613
+ if not splash_damage_options.enable_radio_menu then return end
1614
+
1615
+ if splash_damage_menu then
1616
+ missionCommands.removeItem(splash_damage_menu)
1617
+ end
1618
+
1619
+ splash_damage_menu = missionCommands.addSubMenu("Splash Damage Settings")
1620
+
1621
+ --Page 1: Debug & General Settings
1622
+ local debugGeneralMenu = missionCommands.addSubMenu("Debug & General Settings", splash_damage_menu)
1623
+ missionCommands.addCommand("Toggle Game Messages", debugGeneralMenu, toggleSplashDamageSetting, "game_messages")
1624
+ missionCommands.addCommand("Toggle Debug Messages", debugGeneralMenu, toggleSplashDamageSetting, "debug")
1625
+ missionCommands.addCommand("Toggle Weapon Missing Messages", debugGeneralMenu, toggleSplashDamageSetting, "weapon_missing_message")
1626
+ missionCommands.addCommand("Toggle Pre-Explosion Debug", debugGeneralMenu, toggleSplashDamageSetting, "track_pre_explosion_debug")
1627
+ missionCommands.addCommand("Toggle Damage Model", debugGeneralMenu, toggleSplashDamageSetting, "damage_model")
1628
+ missionCommands.addCommand("Toggle Blast Stun", debugGeneralMenu, toggleSplashDamageSetting, "blast_stun")
1629
+ local unitDisabledMenu = missionCommands.addSubMenu("Unit Disabled Health", debugGeneralMenu)
1630
+ addValueAdjustmentCommands(unitDisabledMenu, "unit_disabled_health")
1631
+ local unitCantFireMenu = missionCommands.addSubMenu("Unit Cant Fire Health", debugGeneralMenu)
1632
+ addValueAdjustmentCommands(unitCantFireMenu, "unit_cant_fire_health")
1633
+ local infantryCantFireMenu = missionCommands.addSubMenu("Infantry Cant Fire Health", debugGeneralMenu)
1634
+ addValueAdjustmentCommands(infantryCantFireMenu, "infantry_cant_fire_health")
1635
+ local rocketMultiplierMenu = missionCommands.addSubMenu("Rocket Multiplier", debugGeneralMenu)
1636
+ addValueAdjustmentCommands(rocketMultiplierMenu, "rocket_multiplier")
1637
+ --Page 2: Explosion
1638
+ local explosionCargoMenu = missionCommands.addSubMenu("Explosion Settings", splash_damage_menu)
1639
+ local staticDamageMenu = missionCommands.addSubMenu("Static Damage Boost", explosionCargoMenu)
1640
+ addValueAdjustmentCommands(staticDamageMenu, "static_damage_boost")
1641
+ missionCommands.addCommand("Toggle Wave Explosions", explosionCargoMenu, toggleSplashDamageSetting, "wave_explosions")
1642
+ missionCommands.addCommand("Toggle Larger Explosions", explosionCargoMenu, toggleSplashDamageSetting, "larger_explosions")
1643
+ local blastRadiusMenu = missionCommands.addSubMenu("Blast Search Radius", explosionCargoMenu)
1644
+ addValueAdjustmentCommands(blastRadiusMenu, "blast_search_radius")
1645
+ local cascadeThresholdMenu = missionCommands.addSubMenu("Cascade Damage Threshold", explosionCargoMenu)
1646
+ addValueAdjustmentCommands(cascadeThresholdMenu, "cascade_damage_threshold")
1647
+ local overallScalingMenu = missionCommands.addSubMenu("Overall Scaling", explosionCargoMenu)
1648
+ addValueAdjustmentCommands(overallScalingMenu, "overall_scaling")
1649
+ missionCommands.addCommand("Toggle Shaped Charge Effects", explosionCargoMenu, toggleSplashDamageSetting, "apply_shaped_charge_effects")
1650
+ local shapedChargeMenu = missionCommands.addSubMenu("Shaped Charge Multiplier", explosionCargoMenu)
1651
+ addValueAdjustmentCommands(shapedChargeMenu, "shaped_charge_multiplier")
1652
+ missionCommands.addCommand("Toggle Dynamic Blast Radius", explosionCargoMenu, toggleSplashDamageSetting, "use_dynamic_blast_radius")
1653
+ local dynamicBlastMenu = missionCommands.addSubMenu("Dynamic Blast Radius Modifier", explosionCargoMenu)
1654
+ addValueAdjustmentCommands(dynamicBlastMenu, "dynamic_blast_radius_modifier")
1655
+ local cascadeScalingMenu = missionCommands.addSubMenu("Cascade Scaling", explosionCargoMenu)
1656
+ addValueAdjustmentCommands(cascadeScalingMenu, "cascade_scaling")
1657
+ local cascadeExplodeThresholdMenu = missionCommands.addSubMenu("Cascade Explode Threshold", explosionCargoMenu)
1658
+ addValueAdjustmentCommands(cascadeExplodeThresholdMenu, "cascade_explode_threshold")
1659
+
1660
+
1661
+ --Page 3: Cargo and Ordnance Protection
1662
+ local explosionCargoMenu = missionCommands.addSubMenu("Cargo and Ordnance", splash_damage_menu)
1663
+ missionCommands.addCommand("Toggle Always Cascade Explode", explosionCargoMenu, toggleSplashDamageSetting, "always_cascade_explode")
1664
+ missionCommands.addCommand("Toggle Tracking & Cargo Effects", explosionCargoMenu, toggleSplashDamageSetting, "track_pre_explosion")
1665
+ local cargoThresholdMenu = missionCommands.addSubMenu("Cargo Damage Threshold", explosionCargoMenu)
1666
+ addValueAdjustmentCommands(cargoThresholdMenu, "cargo_damage_threshold")
1667
+ missionCommands.addCommand("Toggle Ordnance Protection", explosionCargoMenu, toggleSplashDamageSetting, "ordnance_protection")
1668
+ local ordnanceRadiusMenu = missionCommands.addSubMenu("Ordnance Protection Radius", explosionCargoMenu)
1669
+ addValueAdjustmentCommands(ordnanceRadiusMenu, "ordnance_protection_radius")
1670
+ missionCommands.addCommand("Toggle Snap To Ground If Destroyed By LE", explosionCargoMenu, toggleSplashDamageSetting, "snap_to_ground_if_destroyed_by_large_explosion")
1671
+
1672
+ --Page 4: Debris Settings
1673
+ local debrisMenu = missionCommands.addSubMenu("Debris Settings", splash_damage_menu)
1674
+ missionCommands.addCommand("Toggle Debris Effects", debrisMenu, toggleSplashDamageSetting, "debris_effects")
1675
+ local debrisCountMinMenu = missionCommands.addSubMenu("Min Debris Count", debrisMenu)
1676
+ addValueAdjustmentCommands(debrisCountMinMenu, "debris_count_min")
1677
+ local debrisCountMaxMenu = missionCommands.addSubMenu("Max Debris Count", debrisMenu)
1678
+ addValueAdjustmentCommands(debrisCountMaxMenu, "debris_count_max")
1679
+ local debrisDistanceMenu = missionCommands.addSubMenu("Max Debris Distance", debrisMenu)
1680
+ addValueAdjustmentCommands(debrisDistanceMenu, "debris_max_distance")
1681
+ local debrisPowerMenu = missionCommands.addSubMenu("Debris Power", debrisMenu)
1682
+ addValueAdjustmentCommands(debrisPowerMenu, "debris_power")
1683
+
1684
+ --Page 5: Cluster Settings
1685
+ local clusterMenu = missionCommands.addSubMenu("Cluster Settings", splash_damage_menu)
1686
+ missionCommands.addCommand("Toggle Cluster Enabled", clusterMenu, toggleSplashDamageSetting, "cluster_enabled")
1687
+ local clusterBaseLengthMenu = missionCommands.addSubMenu("Cluster Base Length", clusterMenu)
1688
+ addValueAdjustmentCommands(clusterBaseLengthMenu, "cluster_base_length")
1689
+ local clusterBaseWidthMenu = missionCommands.addSubMenu("Cluster Base Width", clusterMenu)
1690
+ addValueAdjustmentCommands(clusterBaseWidthMenu, "cluster_base_width")
1691
+ local clusterMaxLengthMenu = missionCommands.addSubMenu("Cluster Max Length", clusterMenu)
1692
+ addValueAdjustmentCommands(clusterMaxLengthMenu, "cluster_max_length")
1693
+ local clusterMaxWidthMenu = missionCommands.addSubMenu("Cluster Max Width", clusterMenu)
1694
+ addValueAdjustmentCommands(clusterMaxWidthMenu, "cluster_max_width")
1695
+ local clusterMinLengthMenu = missionCommands.addSubMenu("Cluster Min Length", clusterMenu)
1696
+ addValueAdjustmentCommands(clusterMinLengthMenu, "cluster_min_length")
1697
+ local clusterMinWidthMenu = missionCommands.addSubMenu("Cluster Min Width", clusterMenu)
1698
+ addValueAdjustmentCommands(clusterMinWidthMenu, "cluster_min_width")
1699
+ missionCommands.addCommand("Toggle Bomblet Reduction Modifier", clusterMenu, toggleSplashDamageSetting, "cluster_bomblet_reductionmodifier")
1700
+ local clusterBombletDamageMenu = missionCommands.addSubMenu("Bomblet Damage Modifier", clusterMenu)
1701
+ addValueAdjustmentCommands(clusterBombletDamageMenu, "cluster_bomblet_damage_modifier")
1702
+
1703
+ end
1704
+
1705
+ if (script_enable == 1) then
1706
+ gameMsg("SPLASH DAMAGE 3.0 SCRIPT RUNNING")
1707
+ env.info("SPLASH DAMAGE 2. SCRIPT RUNNING")
1708
+
1709
+ timer.scheduleFunction(function()
1710
+ protectedCall(track_wpns)
1711
+ return timer.getTime() + refreshRate
1712
+ end, {}, timer.getTime() + refreshRate)
1713
+
1714
+ world.addEventHandler(WpnHandler)
1715
+ addSplashDamageMenu()
1716
+ end