--[[-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-= Latest Changes -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=- Forum Thread: https://forum.dcs.world/topic/370261-splash-damage-3x-script-now-with-more-explosions-version-33-napalm-edition/ User Files: https://www.digitalcombatsimulator.com/en/files/3344761/ Any issues/suggestions etc feel free to post on the forum or DM me in Discord - stevey9062 --noting this from gashpl - for easy script release/config testing, add this as the do script trigger: assert(loadfile("C:\\Users\\[USER]\\Saved Games\\DCS\\Missions\\Splash_Damage_3.4.lua"))() 4th July 2025 - 3.4 (Stevey666) - Added in optional kill feed feature, this will try to display kills from DCS engine and kills from the additional explosions by checking pre/post scans of the explosion area --SPLASH KILL FEED WORKS IN MP ONLY (you can host your local SP mission as MP if you want to see it) - Added in Lekas Foothold Integration to allow splash kills to count towards the points, killfeed is required to be enabled for this - Added AGM_45B to expl table - Added instant phosphor/signal flares option to cook off events - Added in missing JF17/JAS39 weapons as per Kurdes - Added killfeed to napalm and cluster features. Note, it may not support all features in this script i.e ied explosions but should work with splashdamage by dropping bombs, the new CBU cluster feature and napalm. - New Feature: A-10 Murder Mode, Named Unit Murder Mode (disabled by default) - adds a configurable sized explosion to every hit event with the a10 or the named unit with the name MurderMode in it as an initiator - New Feature: Trophy APS System (disabled by default) -The script tracks weapons heading towards a TrophyAPS vehicle, triggers a small explosion by the unit to mimic the Trophy system and triggers a larger explosion at the co-ords of the incoming weapon. The script mimics there being a Trophy system on the front right and back left of the vehicle, with each launcher having 4 rounds. -It contains 2 methods of enabling, either the vehicle has TrophyAPS in its name or you put the unit type into the AllUnitType table. By default, only the name method is enabled, both can be enabled at the same time as below: - New Feature: Vehicle IEDs. (disabled by default) If a unit is contains VehicleIEDTarget (or other names as set in the config) it will trigger a large configurable explosion - New Feature: Tactical Explosion, similar to the IED effect but a little bigger and has the ability to be assigned to a weapon in a table or as an override - New Feature: Critical Component. % chance on a hit event of triggering an explosion at unit level - New Feature: Ground Unit Explosion On Death. - If a vehicle is flaming it takes time to pop, this will trigger an explosion with a %chance when its begins to flame (when it does not "exist" but has not triggerd a killed/dead event) - There's a % chance settable - You can also trigger this to happen if the unit has "GUED" in its name even if chance is set to 0 - New Feature: CBU Bomblet Hit Spread - On a Hit event from a cluster bomb, it will scan the local area for nearby vehicles and trigger an additional explosion - This features aims to help wipe out areas, but it works by scanning 20 meters radius (adjustable) for any vehicles nearby the hit vehicle and then 20m (adjustable) from those vehicles - Max of 1 additional explosion will spawn on the vehicles. Not enabled for CBU_97/CBU_105 due to them already being effective. - The spread mechanic could miss vehicles in the area still if one doesnt get hit, or theyre at opposite sides of the visible area and not within 20m (adjustable) - There is % chance to hit per unit found in the area, % chance for that hit to be indirect, and armour damage modifiers - New Feature: Strobe Marker - generates a tiny explosion above a unit, no smoke but sound + light appears - can be used as a marker for planes - Generates on an active and living unit with "Strobe" in the name - Good: Visible to eye/FLIR(TV mode) - Not good: Not visible in IR, audible explosions if you're close to the unit - New Feature: All Unit Cook/off smoke chances and advanced sequences - It's possible to assign a % chance to allunits having smoke/cookoffs - Advanced sequences allow for having multiple smoke/fire sizes of multiple lengths of time - and have smoke for example indefinitely burn - New trigger for cookoff - Cookoff with the allunits settings can be enabled for specific units by the having "CargoCookoffTarget" in the name - Reworked how cookoff works, cookoffs will now follow a moving vehicle as it travels instead of just going off where it was. Flames/smoke will trigger when the vehicle stops. - You can have a chance of cookoff, smoke with a cookoff and also a chance of smoke only - Added chance options to the flares for cookoffs also - Effects (i.e cookoff) no longer only bound by damage from tracked weapons. Gun cannon kills will now count! May time until the unit pops before it triggers a cookoff - Giant explosion effects now tracked on events instead of checking the unit every second - Jogaredi's suggestion added - ["only_players_weapons"] = true, --track only weapons launch by players, this will be defaulted to false - Due to ED boosting damage values for MK82s and a few others, added the ability to skip larger_explosion and damage_model by having a specific entry in the explosive table - Example below, you would need to add this to each weapon that you need this for (or I can do it in the base script if multiple people think its a good idea) - ["Mk_82"] = { explosive = 100, Skip_larger_explosions = true, Skip_damage_model = true }, --3.4.2 - Adjusted Lekas Foothold Integration - Added flak units to ground ord tracking with 0 extra damage for night time light bursts -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=- Full Changelog at the bottom of the script -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=- -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=- ##### SCRIPT CONFIGURATION ##### -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-]] splash_damage_options = { ---------------------------------------------------------------------- Debug and Messages ---------------------------------------------------------------- ["game_messages"] = false, --enable some messages on screen ["debug"] = false, --enable debugging messages ["weapon_missing_message"] = false, --false disables messages alerting you to weapons missing from the explTable ["track_pre_explosion_debug"] = false, --Toggle to enable/disable pre-explosion tracking debugging ["track_groundunitordnance_debug"] = false, --Enable detailed debug messages for ground unit ordnance tracking ["napalm_unitdamage_debug"] = false, --Enable detailed debug messages for napalm unit damage tracking ["damage_model_game_messages"] = false, --ground unit movement and weapons disabled notification ["killfeed_debug"] = false, --Enable detailed debug messages for killfeed ["events_debug"] = false, --enable debugging for event handling (logevent function) - you will get a lot of logs if you set this to true ["vehicleied_debug"] = false, --enable debugging for event handling ["MurderMode_debug"] = false, ["trophy_debug"] = false, --Debug mode for trophy aps ["cargocookoff_debug"] = false, --Debug mode for cargo cookoff ["CriticalComponent_debug"] = false, --Toggle debug logging ["GU_Explode_debug"] = false, --Toggle debug logging ["CBU_Bomblet_Hit_debug"] = false, --Toggle debug logging ["StrobeMarker_debug"] = false, --Logging for StrobeMarker ["shipRadarDamageEnable"] = false, -- Ship Raders turn off from HARM Radiation ["oca_aircraft_damage_boost"] = 3000, --apply extra damage to parked Unit.Category.AIRPLANEs and Unit.Category.HELICOPTERs with wave explosions ---------------------------------------------------------------------- Radio ----------------------------------------------------------------------------- ["enable_radio_menu"] = false, --enables the in-game radio menu for modifying settings ---------------------------------------------------------------------- Basic Splash Settings ------------------------------------------------------------- ["static_damage_boost"] = 2000, --apply extra damage to Unit.Category.STRUCTUREs with wave explosions ["wave_explosions"] = true, --secondary explosions on top of game objects, radiating outward from the impact point and scaled based on size of object and distance from weapon impact point ["larger_explosions"] = true, --secondary explosions on top of weapon impact points, dictated by the values in the explTable ["damage_model"] = true, --allow blast wave to affect ground unit movement and weapons ["blast_search_radius"] = 90, --this is the max size of any blast wave radius, since we will only find objects within this zone. Only used if dynamic is not enabled ["use_dynamic_blast_radius"] = true, --if true, blast radius is calculated from explosion power; if false, blast_search_radius (90) is used ["dynamic_blast_radius_modifier"] = 2, --multiplier for the blast radius ["blast_stun"] = false, --not implemented ["overall_scaling"] = 1, --overall scaling for explosive power ["only_players_weapons"] = false, --track only weapons launched by players ---------------------------------------------------------------------- Units ----------------------------------------------------------------------------- ["unit_disabled_health"] = 30, --if health is below this value after our explosions, disable its movement ["unit_cant_fire_health"] = 40, --if health is below this value after our explosions, set ROE to HOLD to simulate damage weapon systems ["infantry_cant_fire_health"] = 60, --if health is below this value after our explosions, set ROE to HOLD to simulate severe injury ---------------------------------------------------------------------- Rockets --------------------------------------------------------------------------- ["rocket_multiplier"] = 1.3, --multiplied by the explTable value for rockets ---------------------------------------------------------------------- Shaped Charge --------------------------------------------------------------------- ["apply_shaped_charge_effects"] = true, --apply reduction in blastwave etc for shaped charge munitions ["shaped_charge_multiplier"] = 0.2, --multiplier that reduces blast radius and explosion power for shaped charge munitions. ---------------------------------------------------------------------- Cascading ------------------------------------------------------------------------- ["cascade_scaling"] = 2, --multiplier for secondary (cascade) blast damage, 1 damage fades out too soon, 2 or 3 damage seems a good balance ["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. ["cascade_explode_threshold"] = 60, --only trigger cascade explosion if the unit's current health is <= this percent of its maximum, setting can help blow nearby jeeps but not tanks ["always_cascade_explode"] = false, --switch if you want everything to explode like with the original script ---------------------------------------------------------------------- Cargo Cook Off/Fuel Explosion ---------------------------------------------------- --track_pre_explosion/enable_cargo_effects should both be the same value-- ["track_pre_explosion"] = true, --Toggle to enable/disable pre-explosion tracking ["enable_cargo_effects"] = true, --Toggle for enabling/disabling cargo explosions and cook-offs ["cargo_effects_chance"] = 1, -- Chance of cargo effects occurring. 0.1 = 10%, 1 = 100% ["cargo_damage_threshold"] = 25, --Health % below which cargo explodes (0 = destroyed only) ["debris_effects"] = true, --Enable debris from cargo cook-offs ["debris_power"] = 1, --Power of each debris explosion ["debris_count_min"] = 6, --Minimum debris pieces per cook-off ["debris_count_max"] = 12, --Maximum debris pieces per cook-off ["debris_max_distance"] = 8, --Max distance debris can travel (meters), the min distance from the vehicle will be 10% of this ["cookoff_flares_enabled"] = true, --Enable/disable flare effects for cook-offs, this applies to allvehicles too. ["cookoff_flare_color"] = 2, ["cookoff_flare_instant"] = true, --If true, spawns flares instantly using napalm phosphor style; if false, spawns over time ["cookoff_flare_instant_min"] = 2, --Minimum number of instant flares when cookoff_flare_instant is true ["cookoff_flare_instant_max"] = 5, --Maximum number of instant flares when cookoff_flare_instant is true ["cookoff_flare_count_modifier"] = 1, --Multiplier for non instant flare count (e.g., 1x, 2x cookOffCount from the vehicle table) ["cookoff_flare_offset"] = 0.5, --Max offset distance for flares in meters (horizontal) ["cookoff_flare_chance"] = 0.5, --Chance - where 1 = 100% 0.4 = 40% chance of the flares firing out --All Vehicles Section --If a Unit is called CookOffTarget it will trigger a cookoff with the below effects ["smokeandcookoffeffectallvehicles"] = true, --Enable effects for all ground vehicles not in cargoUnits vehicle table ["allunits_enable_smoke"] = true, -- Enable /disable smoke effects if smokeandcookoffeffectallvehicles is true ["allunits_enable_cookoff"] = true, -- Enable /disable cookoffs if smokeandcookoffeffectallvehicles is true ["allunits_damage_threshold"] = 25, --Health % below which cargo/smoke attempts to trigger ["allunits_explode_power"] = 40, --Initial power of vehicle exploding ["allunits_default_flame_size"] = 6, --Default smoke size (called flame here in the code, but it'll be smoke) 5 = small smoke, 6 = medium smoke, 7 = large smoke, 8 = huge smoke ["allunits_default_flame_duration"] = 240, --Default smoke (called flame here in the code, but it's smoke) duration in seconds for non-cargoUnits vehicles ["allunits_cookoff_count"] = 4, --number of cookoff explosions to schedule ["allunits_cookoff_duration"] = 30, --max time window of cookoffs (will be scheduled randomly between 0 seconds and this figure) ["allunits_cookoff_power"] = 10, --power of the cookoff explosions ["allunits_cookoff_powerrandom"] = 50, --percentage higher or lower of the cookoff power figure ["allunits_cookoff_chance"] = 0.4, --Chance of cookoff effects occurring for all vehicles. 0.6 = 60%, 1 = 100% ["allunits_smokewithcookoff"] = true, --Automatically smoke along with cookoff, or leave it to chance ["allunits_smoke_chance"] = 0.7, --Chance of smoke effect, 1 = 100%, 0.5 = 50% ["allunits_explode_on_smoke_only"] = true, --If its a smoke only effect, add an explosion to finish the vehicle off (allunits_explode_power) ["allunits_advanced_effect_sequence"] = true, --When set to true, its possible for units to be trigger an advanced effect sequence. This will take precedence over the standard allunits cookoff. it will ignore the previous settings for smoke/flame size and duration and instead it will let you program a specific sequence of smoke/flame effects ["allunits_advanced_effect_sequence_chance"] = 0.2, --Chance of the script picking the advanced effect instead of the standard all unit effect. 1 = 100%, 0.5 = 50% ["allunits_advanced_effect_force_on_name"] = true, --Regardless of chance, if the unit has "AdvSeq" in its name it will trigger the advanced sequence ["allunits_advanced_effect_order"] = {"2", "7", "6", "5"}, --List of flame and smoke : sizes, 1 = small smoke and fire, 2 = med, 3 = large, 4 = huge. 5 = small smoke only, 6 = medium, 7 = large, 8 = huge ["allunits_advanced_effect_timing"] = {"30", "90", "120", "600"}, --How many seconds per effect in the order config key above ["allunits_advanced_effect_cookoff_chance"] = 1, --Chance of cookoff effects occurring for the advanced effect sequence ["allunits_advanced_effect_cookoff_count"] = 4, --number of cookoff explosions to schedule ["allunits_advanced_effect_cookoff_duration"] = 30, --max time window of cookoffs (will be scheduled randomly between 0 seconds and this figure) ["allunits_advanced_effect_cookoff_power"] = 10, --power of the cookoff explosions ["allunits_advanced_effect_cookoff_powerrandom"] = 50, --percentage higher or lower of the cookoff power figure ["allunits_advanced_effect_cookoff_flares_enabled"] = true, --Enable or disable phospor like signal flares, number etc taken from cookoff_flare_instant_count ["allunits_advanced_effect_explode_power"] = 40, --Initial power of vehicle exploding ---------------------------------------------------------------------- Ordnance Protection -------------------------------------------------------------- ["ordnance_protection"] = true, --Toggle ordinance protection features ["ordnance_protection_radius"] = 20, --Distance in meters to protect nearby bombs ["detect_ordnance_destruction"] = true, --Toggle detection of ordnance destroyed by large explosions ["snap_to_ground_if_destroyed_by_large_explosion"] = true, --If the ordnance protection fails or is disabled we can snap larger_explosions to the ground (if enabled - power as set in weapon list) - so an explosion still does hit the ground ["max_snapped_height"] = 80, --max height it will snap to ground from ["recent_large_explosion_snap"] = true, --enable looking for a recent large_explosion generated by the script ["recent_large_explosion_range"] = 100, --range its looking for in meters for a recent large_explosion generated by the script ["recent_large_explosion_time"] = 4, --in seconds how long ago there was a recent large_explosion generated by the script ---------------------------------------------------------------------- Cluster Bombs Spread Mimic ------------------------------------------------------- ["cluster_enabled"] = false, ["cluster_base_length"] = 150, --Base forward spread (meters) ["cluster_base_width"] = 200, --Base lateral spread (meters) ["cluster_max_length"] = 300, --Max forward spread (meters) ["cluster_max_width"] = 400, --Max lateral spread (meters) ["cluster_min_length"] = 100, --Min forward spread ["cluster_min_width"] = 150, --Min lateral spread ["cluster_bomblet_reductionmodifier"] = true, --Use equation to reduce number of bomblets (to make it look better) ["cluster_bomblet_damage_modifier"] = 1, --Adjustable global modifier for bomblet explosive power ---------------------------------------------------------------------- Giant Explosions ------------------------------------------------------------------ --Remember, any target you want to blow up needs to be named "GiantExplosionTarget(X)" (X) being any value/name etc ["giant_explosion_enabled"] = false, --Toggle to enable/disable Giant Explosion ["giant_explosion_power"] = 6000, --Power in kg of TNT (default 8 tons) ["giant_explosion_scale"] = 1, --Size scale factor (default 1) ["giant_explosion_duration"] = 3.0, --Total duration in seconds (default 3s) ["giant_explosion_count"] = 250, --Number of explosions (default 250) ["giantexplosion_ondamage"] = true, --Trigger explosion when unit is damaged ["giantexplosion_ondeath"] = true, --Trigger explosion when unit is destroyed ["giantexplosion_testmode"] = true, --Enable test mode with separate array for radio commands ---------------------------------------------------------------------- Ground/Ship Ordnance ------------------------------------------------------------- ["track_groundunitordnance"] = true, --Enable tracking of ground unit ordnance for larger explosion function and blastwave cookoffs(shells) ["groundunitordnance_damage_modifier"] = 1.0, --Multiplier for ground unit ordnance explosive power ["groundunitordnance_blastwave_modifier"] = 4.0, --Additional multiplier for blast wave intensity of ground unit ordnance ["groundunitordnance_maxtrackedcount"] = 100, --Maximum number of ground ordnance shells tracked at once to prevent overload ["scan_50m_for_groundordnance"] = true, --If true, uses a 50m scan radius for ground ordnance instead of dynamic blast radius ---------------------------------------------------------------------- Napalm --------------------------------------------------------------------------- ["napalm_mk77_enabled"] = true, --Enable napalm effects for MK77mod0-WPN and MK77mod1-WPN ["napalmoverride_enabled"] = false, --If true, enables napalm effects for weapons in napalm_override_weapons ["napalm_override_weapons"] = "Mk_82,SAMP125LD", --Comma-separated list of weapons to override as napalm when overrides enabled, i.e Mk_82,SAMP125LD. Do not pick CBUs ["napalm_spread_points"] = 4, --Number of points of explosion per each bomb (aka spawns of dummy fuel tank), so 1 bomb can have 4 fireballs as such. The MK77 0 is bigger and will do a % more by default (i.e 5 instead of 4) ["napalm_spread_spacing"] = 25, --Distance m between the points ["napalm_phosphor_enabled"] = true, --If true, enables phosphor flare effects for napalm weapons ["napalm_phosphor_multiplier"] = 0.5, --Multiplier for number of phosphor flares that shoot out, there is a level of randomisation in the code already ["napalm_addflame"] = true, --Enable flame effects at napalm spawn points ["napalm_addflame_size"] = 3, --Flame size (1-8, 4 = huge smoke and fire) ["napalm_addflame_duration"] = 180, --Flame duration in seconds napalm_destroy_delay ["napalm_flame_delay"] = 0.01, --Delay in seconds before flame effect ["napalm_explode_delay"] = 0.01, --Delay in seconds before putting an exlode on the ground to blow up the spawned fuel tank, original script had this as 0.1 ["napalm_destroy_delay"] = 0.02, --Delay in seconds before it destroys the fuel tank object, original script had this as 0.12 ["napalm_doublewide_enabled"] = false, --Toggle for double-wide napalm (two points per spread point, ~28m width) ["napalm_doublewide_spread"] = 15, --Meters either side of bomb vector either side to spawn a fuel tank ["napalm_unitdamage_enable"] = true, --Enable/disable napalm unit damage ["napalm_unitdamage_scandistance"] = 70, --Scan radius in meters ["napalm_unitdamage_startdelay"] = 0.1, --Seconds between Napalm exploding and explosion occurring (can be 0 for no delay) ["napalm_unitdamage_spreaddelay"] = 0, --If startdelay is greater than 0, explosions are ordered by distance with this gap between each unit ---------------------------------------------------------------------- Kill Feed ------------------------------------------------------------------------ ["killfeed_enable"] = false, --Enable killfeed, required for lekas foothold ["killfeed_game_messages"] = false, --Show killfeed SPLASH KILL FEED WORKS IN MP ONLY (you can host your local SP mission as MP for now) ["killfeed_game_message_duration"] = 15, --Duration in seconds for game messages (killfeed and SplashKillFeed) - note the message will be delayed to let DCS catch up as per next option ["killfeed_splashdelay"] = 8, --Duration in seconds delay to allow dcs to see that units are dead before saying the splash damage got them instead of the the players weapon ["killfeed_lekas_foothold_integration"] = false, --Enable Lekas Foothold integration ["killfeed_lekas_contribution_delay"] = 240, -- Delay in seconds before processing splash kills into Lekas contributions (default 240 seconds/4mins) ---------------------------------------------------------------------- Vehicle IEDs --------------------------------------------------------------------- ["vehicleied_enabled"] = false, --If a unit is called VehicleIEDTarget(*) (or anything set in the config key below) it will trigger a vehicleied explosion ["vehicleied_targetname"] = "VehicleIEDTarget,VBID", ["vehicleied_scaling"] = 1, --For easy changing - scaling of explosion powers, counts, radius ["vehicleied_central_power"] = 600, --Power of central explosion ["vehicleied_explosion_power"] = 400, --Base power for secondary explosions ["vehicleied_explosion_count_min"] = 10, --Min number of secondary explosions ["vehicleied_explosion_count_max"] = 14, --Max number of secondary explosions ["vehicleied_power_variance"] = 0.3, --Power variation for secondary explosions (±30%) ["vehicleied_radius"] = 35, -- Max radius for secondary explosions (meters) ["vehicleied_explosion_delay_max"] = 0.4, -- Max delay multiplier for secondary explosions multiplier ["vehicleied_fueltankspawn"] = true, -- Spawn a fuel tank at the location of the explosion for explosion effect and fire/smoke ["vehicleied_destroy_vehicle"] = false, -- Option to attempt to instantly destroy the vehicle (can sometimes leave a ghost smoke vortex or fire) ["vehicleied_explode_on_hit"] = true, --Will it explode instantly on hit event or only on death/kill/when vehice stops moving and no longer "alive" ---------------------------------------------------------------------- Murder Mode ---------------------------------------------------------------------- ["A10MurderMode"] = false, --This tracks hit events, if the initiator is an A10 it will spawn and explosion on the target ["A10MurderMode_Power"] = 5, --Power of the explosion ["A10MurderMode__Chance"] = 1, -- Percent chance a vehicle explodes on hit (0.05 = 5%, 0.5 = 50%) ["NamedUnitMurderMode"] = false, --This tracks hit events, if the initiator has "MurderMode" in the pilot name in the mission editor, every hit event from them will put an explosion of the below power at the target's coords ["NamedUnitMurderMode_Power"] = 5, --Power of the explosion from the named unit ["NamedUnitMurderMode_Chance"] = 1, -- Percent chance a vehicle explodes on hit (0.05 = 5%, 0.5 = 50%) ---------------------------------------------------------------------- Trophy APS ----------------------------------------------------------------------- ["trophy_enabled"] = false, --Enable/disable Trophy APS (true/false) ["trophy_selfExplosionSize"] = 1, --Explosion size near vehicle, mimicking trophy location (default: 1) ["trophy_explosionOffsetDistance"] = 2, --Launcher offset from vehicle center (default: 2 meters) ["trophy_weaponExplosionSize"] = 20, --Explosion size to destroy weapon (default: 20) ["trophy_detectRange"] = 200, --Detection range in meters (default: 200) when in detection range speed up the location checks of the weapon ["trophy_interceptRange"] = 30, --Interception range in meters (default: 30) you can reduce this to 20 to make it more realistic but the script may struggle hitting fast missiles ["trophy_frontRightRounds"] = 4, --Initial front-right launcher rounds (default: 4) ["trophy_backLeftRounds"] = 4, --Initial back-left launcher rounds (default: 4) ["trophy_failureChance"] = 0.00, --Failure chance for interception (0.0 to 1.0 0% to 100%, i.e 0.05 for 5%) ["trophy_markShooterOrigin"] = true, --Enable/disable marking shooter origin with a point marker ["trophy_drawOriginLine"] = true, --Enable/disable drawing line from tank to shooter origin ["trophy_maxMapMarkerDistance"] = 1000, --Max distance for shooter map marker and line length (meters ["trophy_markerDuration"] = 120, --Duration of point and line markers (seconds) ["trophy_showInterceptionMessage"] = true, --Enable/disable interception message (true/false ["trophy_messageDuration"] = 10, --Duration of interception message display (seconds) ---------------------------------------------------------------------- Critical Component ---------------------------------------------------------------- ["CriticalComponent"] = false, -- Toggle to enable CriticalComponent Feature - % Chance a vehicle is destroyed from a single hit ["CriticalComponent_Chance"] = 0.01, -- Percent chance a vehicle explodes on hit (0.01 = 1%, 0.5 = 50%) ["CriticalComponent_Explosion_Power"] = 50, --Explosion power for CriticalComponent ["CriticalComponent_Specific_Weapons_Only"] = {"GAU8_30_HE", "GAU8_30_AP", "GAU8_30_TP"}, -- {} means all weapons. List of specific weapons to trigger CriticalComponent, i.e {"GAU8_30_HE", "GAU8_30_AP", "GAU8_30_TP"} ---------------------------------------------------------------------- Ground Unit Explosion On Death ---------------------------------------------------- --You can also trigger this to happen if the unit has "GUED" in its name - so you can set the chance to 0 and still have them go off for specific units ["GU_Explode_on_Death"] = true, --If a vehicle is dead and has had no other effects on it, trigger an explosion - This is at the start of its on fire for a bit before popping stage if you've hit it or on pop if its a dead event ["GU_Explode_on_Death_Chance"] = 0.5, --Percent chance a vehicle explodes on death (0.05 = 5%, 0.5 = 50%) ["GU_Explode_on_Death_Explosion_Power"] = 30, --Explosion power for explode on death ["GU_Explode_on_Death_Height"] = 1, --Height above coords of the vehicle. Close to ground throws up more dirt, higher up more of a puff of smoke ["GU_Explode_Exclude_Infantry"] = true, --Set to false to make infantry blow up too ---------------------------------------------------------------------- CBU Bomblet Hit Explosion --------------------------------------------------------- ["CBU_Bomblet_Hit_Explosion"] = false, --ONLY TESTED WITH JSOW-A - Enable/Disable - on a hit even by a bomblet it can do extra damage AND/OR scan around the unit to deal damage with additional explosions of the power set in the cluster table ["CBU_Bomblet_Hit_Explosion_Scaling"] = 35, --Overall Multiplier for the final bomblet damage result. Default 35 to get the effects we want when the ground level is less than 1.6 - WHEN TESTED WITH JSOW-A ["CBU_Bomblet_Hit_Mimic_Spread"] = true, --Enable/Disable - Mimic spread of clusterbomb warheads by scanning an area around the target that was hit and triggering an explosion against any unit or structure (unitIds can only be hit once by this weaponid) ["CBU_Bomblet_Hit_Spread"] = 50, --Scan radius m to look for units to hit ["CBU_Bomblet_Hit_Spread_SecondaryScan"] = 50, --Scan radius m to look for units to hit ["CBU_Bomblet_Hit_Spread_Duration"] = 2, --Schedule additional unit explosions over this many seconds ["CBU_Bomblet_NonArmored_Dmg_Modifier"] = 1.0, --Multiplier damage for NonArmored units (e.g., Infantry, trucks), vulnerable to bomblets ["CBU_Bomblet_LightlyArmored_Dmg_Modifier"] = 0.8, --Multiplier damage for LightlyArmored units. 0.3 = 30% of damage (e.g., BTR-80, ZSU-23-4, moderately vulnerable (e.g., BLU-97B, PTAB-10-5) ["CBU_Bomblet_Armored_Dmg_Modifier"] = 0.6, --Multiplier for damage for Armored units. 0.3 = 30% of damage (e.g., T-90, BMP-3), highly resistant (e.g., Mk 118, HEAT) ["CBU_Bomblet_Hit_Chance"] = 0.8, --Chance that a unit gets hit. 0.8 = 80%. ["CBU_Bomblet_Indirect_Hit_Chance"] = 0.2, --Chance that the direct hit was actually indirect or less critical, 0.5% = 20% chance ["CBU_Bomblet_Indirect_Dmg_Modifier"] = 0.4, --Multiplier for if its an indirect or less critical hit 0.4 = 40% of damage/60% reduction ["CBU_Bomblet_Explosion_Height"] = 0.1, -- Explosions at ground height do less damage and typically kick up more dirt, increase height by this much ---------------------------------------------------------------------- Strobe Marker / Beacon ------------------------------------------------------------ --Only enable one of the strobe methods at a time ["StrobeMarker_allstrobeunits"] = false, --Constantly fire off strobe for all living, active units and not invisible units with Strobe in the name ["StrobeMarker_individuals"] = false, --Ability to enable or disable the strobing via radio commands for individual "Strobe" units ["StrobeMarker_interval"] = 2, --Default interval in seconds for strobing explosions ---------------------------------------------------------------------- Tactical Explosion ---------------------------------------------------- ["tactical_explosion"] = false, --Enable tactical explosion effects ["tactical_explosion_override_enabled"] = false, --Set this to true to enable override weapons in the key below ["tactical_explosion_override_weapons"] = "BGM_109B,Mk_82", --Comma-separated list of weapons to override as tactical explosion, can be changed as needed. Needs to be enabled in the key above. Current has tomahawks and mk82 bombs there as examples ["tactical_explosion_max_height"] = 40, --Max height above ground for tactical explosion to trigger (meters) ["tactical_explosion_scaling"] = 1, --Scaling of explosion powers, counts, radius ["tactical_explosion_central_power"] = 4000, --Power of central explosion ["tactical_explosion_explosion_power"] = 1000, --Base power for secondary explosions ["tactical_explosion_explosion_count_min"] = 35, --Min number of secondary explosions ["tactical_explosion_explosion_count_max"] = 35, --Max number of secondary explosions ["tactical_explosion_radius"] = 100, --Max radius for secondary explosions (meters) ["tactical_explosion_explosion_delay_max"] = 0.4, --Max delay multiplier for secondary explosions ["tactical_explosion_fueltankspawn"] = false, --Spawn a fuel tank at the explosion location for effect/smoke } local script_enable = 1 refreshRate = 0.1 ----[[ ##### End of SCRIPT CONFIGURATION ##### ]]---- --Helper function: Trim whitespace. local function trim(s) return s:match("^%s*(.-)%s*$") end cargoUnits = { --[[ flamesize: 1 = small smoke and fire 2 = medium smoke and fire 3 = large smoke and fire 4 = huge smoke and fire 5 = small smoke 6 = medium smoke 7 = large smoke 8 = huge smoke ]]-- --1) M92 R11 Volvo driveable (Fuel Truck Tanker) ["r11_volvo_drivable"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --2) Refueler ATMZ-5 ["ATMZ-5"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --3) Refueler ATZ-10 ["ATZ-10"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --4) Refueler ATZ-5 ["ATZ-5"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --5) Refueler M978 HEMTT (Fuel truck tanker) ["M978 HEMTT Tanker"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 0, cookOffPower = 0, cookOffDuration = 0, cookOffRandomTiming = false, cookOffPowerRandom = 50, isTanker = true, flameSize = 3, flameDuration = 5, }, --##### AMMO CARRIERS ##### ["GAZ-66"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = true, cookOffCount = 4, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = false, flameSize = 1, flameDuration = 30, }, --#Technically this is both ammo and fuel looking at the model --#Called Ural-4320 in game, but in code its Ural-375 ["Ural-375"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = true, cookOffCount = 4, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = true, flameSize = 1, flameDuration = 30, }, ["ZIL-135"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = true, cookOffCount = 6, cookOffPower = 1, cookOffDuration = 24, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = false, flameSize = 1, flameDuration = 30, }, --#Ammo Boxes etc --#Long ammo box ["Cargo06"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = true, cookOffCount = 5, cookOffPower = 1, cookOffDuration = 10, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = false, flameSize = 1, flameDuration = 30, }, --#ammo boxes ["Cargo03"] = { cargoExplosion = true, cargoExplosionPower = 10, cargoCookOff = true, cookOffCount = 10, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 0, isTanker = false, flameSize = 1, flameDuration = 30, }, --FuelBarrels ["Cargo05"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 2, cookOffPower = 1, cookOffDuration = 10, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = true, flameSize = 2, flameDuration = 30, }, --APFC fuel ["APFC fuel"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = false, cookOffCount = 2, cookOffPower = 1, cookOffDuration = 10, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = true, flameSize = 2, flameDuration = 30, }, --Oil Barrel ["Oil Barrel"] = { cargoExplosion = true, cargoExplosionPower = 20, cargoCookOff = false, cookOffCount = 0, cookOffPower = 1, cookOffDuration = 10, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = true, flameSize = 1, flameDuration = 20, }, --FARP Ammo Dump Coating ["FARP Ammo Dump Coating"] = { cargoExplosion = true, cargoExplosionPower = 50, cargoCookOff = true, cookOffCount = 5, cookOffPower = 1, cookOffDuration = 20, cookOffRandomTiming = true, cookOffPowerRandom = 50, isTanker = false, flameSize = 1, flameDuration = 20, }, } --[[ -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=- Weapon Explosive Table -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-]] --If you want to the specific weapon to skip the damage_model (blastwave damage) or the larger_explosion you can modify the entry like so: --["Mk_82"] = { explosive = 100, Skip_larger_explosions = true, Skip_damage_model = true }, explTable = { --*** WWII BOMBS *** ["British_GP_250LB_Bomb_Mk1"] = { explosive = 100 }, ["British_GP_250LB_Bomb_Mk4"] = { explosive = 100 }, ["British_GP_250LB_Bomb_Mk5"] = { explosive = 100 }, ["British_GP_500LB_Bomb_Mk1"] = { explosive = 213 }, ["British_GP_500LB_Bomb_Mk4"] = { explosive = 213 }, ["British_GP_500LB_Bomb_Mk4_Short"] = { explosive = 213 }, ["British_GP_500LB_Bomb_Mk5"] = { explosive = 213 }, ["British_MC_250LB_Bomb_Mk1"] = { explosive = 100 }, ["British_MC_250LB_Bomb_Mk2"] = { explosive = 100 }, ["British_MC_500LB_Bomb_Mk1_Short"] = { explosive = 213 }, ["British_MC_500LB_Bomb_Mk2"] = { explosive = 213 }, ["British_SAP_250LB_Bomb_Mk5"] = { explosive = 100 }, ["British_SAP_500LB_Bomb_Mk5"] = { explosive = 213 }, ["British_AP_25LBNo1_3INCHNo1"] = { explosive = 4 }, ["British_HE_60LBSAPNo2_3INCHNo1"] = { explosive = 4 }, ["British_HE_60LBFNo1_3INCHNo1"] = { explosive = 4 }, ["SC_50"] = { explosive = 20 }, ["ER_4_SC50"] = { explosive = 20 }, ["SC_250_T1_L2"] = { explosive = 100 }, ["SC_501_SC250"] = { explosive = 100 }, ["Schloss500XIIC1_SC_250_T3_J"] = { explosive = 100 }, ["SC_501_SC500"] = { explosive = 213 }, ["SC_500_L2"] = { explosive = 213 }, ["SD_250_Stg"] = { explosive = 100 }, ["SD_500_A"] = { explosive = 213 }, --*** WWII CBU *** ["AB_250_2_SD_2"] = { explosive = 100 }, ["AB_250_2_SD_10A"] = { explosive = 100 }, ["AB_500_1_SD_10A"] = { explosive = 213 }, --*** WWII ROCKETS *** ["3xM8_ROCKETS_IN_TUBES"] = { explosive = 4 }, ["WGr21"] = { explosive = 4 }, --*** UNGUIDED BOMBS (UGB) *** ["M_117"] = { explosive = 201 }, ["AN_M30A1"] = { explosive = 45 }, ["AN_M57"] = { explosive = 100 }, ["AN_M64"] = { explosive = 121 }, ["AN_M65"] = { explosive = 400 }, ["AN_M66"] = { explosive = 800 }, ["AN-M66A2"] = { explosive = 536 }, ["AN-M81"] = { explosive = 100 }, ["AN-M88"] = { explosive = 100 }, ["Mk_81"] = { explosive = 60 }, ["MK-81SE"] = { explosive = 60 }, ["Mk_82"] = { explosive = 100}, ["MK_82AIR"] = { explosive = 100 }, ["MK_82SNAKEYE"] = { explosive = 100 }, ["Mk_83"] = { explosive = 274 }, ["Mk_84"] = { explosive = 582 }, ["HEBOMB"] = { explosive = 40 }, ["HEBOMBD"] = { explosive = 40 }, ["SAMP125LD"] = { explosive = 60 }, ["SAMP250LD"] = { explosive = 118 }, ["SAMP250HD"] = { explosive = 118 }, ["SAMP400LD"] = { explosive = 274 }, ["SAMP400HD"] = { explosive = 274 }, ["BR_250"] = { explosive = 100 }, ["BR_500"] = { explosive = 100 }, ["FAB_100"] = { explosive = 45 }, ["FAB_250"] = { explosive = 118 }, ["FAB_250M54TU"] = { explosive = 118 }, ["FAB-250-M62"] = { explosive = 118 }, ["FAB_500"] = { explosive = 213 }, ["FAB_1500"] = { explosive = 675 }, --*** UNGUIDED BOMBS WITH PENETRATOR / ANTI-RUNWAY *** ["Durandal"] = { explosive = 64 }, ["BLU107B_DURANDAL"] = { explosive = 64 }, ["BAP_100"] = { explosive = 32 }, ["BAP-100"] = { explosive = 32 }, ["BAT-120"] = { explosive = 32 }, ["TYPE-200A"] = { explosive = 107 }, ["BetAB_500"] = { explosive = 98 }, ["BetAB_500ShP"] = { explosive = 107 }, --*** GUIDED BOMBS (GBU) *** ["GBU_10"] = { explosive = 582 }, ["GBU_12"] = { explosive = 100 }, ["GBU_16"] = { explosive = 274 }, ["GBU_24"] = { explosive = 582 }, ["KAB_1500Kr"] = { explosive = 675 }, ["KAB_500Kr"] = { explosive = 213 }, ["KAB_500"] = { explosive = 213 }, --*** CLUSTER BOMBS (CBU) *** --I don't have most of these so can't test them with debug on --For the CBU_Bomblet_Hit_Explosion feature, see a different table called "clusterSubMunTable" ["MK77mod0-WPN"] = { explosive = 0, cluster = false, submunition_count = 132, submunition_explosive = 0.1, submunition_name = "BLU_1B" }, --napalm skyhawk, have set to cluster (false) for napalm purposes ["MK77mod1-WPN"] = { explosive = 0, cluster = false, submunition_count = 132, submunition_explosive = 0.1, submunition_name = "BLU_1B" }, --napalm skyhawk, have set to cluster (false) for napalm purposes ["CBU_99"] = { explosive = 0, cluster = true, submunition_count = 247, submunition_explosive = 2, submunition_name = "Mk 118" }, --Mk 20 Rockeye variant, confirmed 247 Mk 118 bomblets ["ROCKEYE"] = { explosive = 0, cluster = true, submunition_count = 247, submunition_explosive = 2, submunition_name = "Mk 118" }, --Mk 20 Rockeye, confirmed 247 Mk 118 bomblets ["BLU_3B_GROUP"] = { explosive = 0, cluster = true, submunition_count = 19, submunition_explosive = 0.2, submunition_name = "BLU_3B" }, --Not in datamine, possibly custom or outdated; submunition name guessed ["CBU_87"] = { explosive = 0, cluster = true, submunition_count = 202, submunition_explosive = 0.5, submunition_name = "BLU_97B" }, --Confirmed 202 BLU-97/B bomblets ["CBU_103"] = { explosive = 0, cluster = true, submunition_count = 202, submunition_explosive = 0.5, submunition_name = "BLU_97B" }, --WCMD variant of CBU-87, confirmed 202 BLU-97/B bomblets ["CBU_97"] = { explosive = 0, cluster = true, submunition_count = 10, submunition_explosive = 15, submunition_name = "BLU_108" }, --Confirmed 10 BLU-108 submunitions, each with 4 skeets ["CBU_105"] = { explosive = 0, cluster = true, submunition_count = 10, submunition_explosive = 15, submunition_name = "BLU_108" }, --WCMD variant of CBU-97, confirmed 10 BLU-108 submunitions ["BELOUGA"] = { explosive = 0, cluster = true, submunition_count = 151, submunition_explosive = 0.3, submunition_name = "grenade_AC" }, --Confirmed 151 grenade_AC bomblets (French BLG-66) ["BLG66_BELOUGA"] = { explosive = 0, cluster = true, submunition_count = 151, submunition_explosive = 0.3, submunition_name = "grenade_AC" }, --Alias for BELOUGA, confirmed 151 grenade_AC bomblets ["BL_755"] = { explosive = 0, cluster = true, submunition_count = 147, submunition_explosive = 0.4, submunition_name = "BL_755_bomblet" }, --Confirmed 147 bomblets, submunition name from your table ["RBK_250"] = { explosive = 0, cluster = true, submunition_count = 60, submunition_explosive = 0.5, submunition_name = "PTAB_25M" }, --Confirmed 60 PTAB-2.5M anti-tank bomblets ["RBK_250_275_AO_1SCH"] = { explosive = 0, cluster = true, submunition_count = 150, submunition_explosive = 0.2, submunition_name = "AO_1SCh" }, --Confirmed 150 AO-1SCh fragmentation bomblets ["RBK_500"] = { explosive = 0, cluster = true, submunition_count = 108, submunition_explosive = 0.5, submunition_name = "PTAB_10_5" }, --Confirmed 108 PTAB-10-5 anti-tank bomblets ["RBK_500U"] = { explosive = 0, cluster = true, submunition_count = 352, submunition_explosive = 0.2, submunition_name = "OAB_25RT" }, --Confirmed 352 OAB-2.5RT fragmentation bomblets ["RBK_500AO"] = { explosive = 0, cluster = true, submunition_count = 108, submunition_explosive = 0.5, submunition_name = "AO_25RT" }, --Confirmed 108 AO-2.5RT fragmentation bomblets ["RBK_500U_OAB_2_5RT"] = { explosive = 0, cluster = true, submunition_count = 352, submunition_explosive = 0.2, submunition_name = "OAB_25RT" }, --Confirmed 352 OAB-2.5RT fragmentation bomblets ["RBK_500_255_PTO_1M"] = { explosive = 0, cluster = true, submunition_count = 126, submunition_explosive = 0.5, submunition_name = "PTO_1M" }, ["RBK_500_255_ShO"] = { explosive = 0, cluster = true, submunition_count = 565, submunition_explosive = 0.1, submunition_name = "ShO" }, --*** INS/GPS BOMBS (JDAM) *** ["GBU_31"] = { explosive = 582 }, ["GBU_31_V_3B"] = { explosive = 582 }, ["GBU_31_V_2B"] = { explosive = 582 }, ["GBU_31_V_4B"] = { explosive = 582 }, ["GBU_32_V_2B"] = { explosive = 202 }, ["GBU_38"] = { explosive = 100 }, ["GBU_54_V_1B"] = { explosive = 100 }, --*** GLIDE BOMBS (JSOW) *** ["AGM_154A"] = { explosive = 0, cluster = true, submunition_count = 145, submunition_explosive = 2, submunition_name = "BLU-97/B" }, --JSOW-A, confirmed 145 BLU-97 bomblets from datamine ["AGM_154C"] = { explosive = 305 }, ["AGM_154"] = { explosive = 305 }, ["BK90_MJ1"] = { explosive = 0 }, ["BK90_MJ1_MJ2"] = { explosive = 0 }, ["BK90_MJ2"] = { explosive = 0 }, ["LS-6-100"] = { explosive = 45 }, ["LS-6-250"] = { explosive = 100 }, ["LS-6-500"] = { explosive = 274 }, ["GB-6"] = { explosive = 0 }, ["GB-6-HE"] = { explosive = 0 }, ["GB-6-SFW"] = { explosive = 0 }, --*** AIR GROUND MISSILE (AGM) *** ["AGM_62"] = { explosive = 400 }, ["AGM_65D"] = { explosive = 38, shaped_charge = true }, ["AGM_65E"] = { explosive = 80, shaped_charge = true }, ["AGM_65F"] = { explosive = 80, shaped_charge = true }, ["AGM_65G"] = { explosive = 80, shaped_charge = true }, ["AGM_65H"] = { explosive = 38, shaped_charge = true }, ["AGM_65K"] = { explosive = 80, shaped_charge = true }, ["AGM_65L"] = { explosive = 80, shaped_charge = true }, ["AGM_123"] = { explosive = 274 }, ["AGM_130"] = { explosive = 582 }, ["AGM_119"] = { explosive = 176 }, ["AGM_114"] = { explosive = 10, shaped_charge = true }, ["AGM_114K"] = { explosive = 10, shaped_charge = true }, ["Rb 05A"] = { explosive = 217 }, ["RB75"] = { explosive = 38 }, ["RB75A"] = { explosive = 38 }, ["RB75B"] = { explosive = 38 }, ["RB75T"] = { explosive = 80 }, ["HOT3_MBDA"] = { explosive = 15 }, ["C-701T"] = { explosive = 38 }, ["C-701IR"] = { explosive = 38 }, ["Vikhr_M"] = { explosive = 11 }, ["Vikhr_9M127_1"] = { explosive = 11 }, ["AT_6"] = { explosive = 11 }, ["Ataka_9M120"] = { explosive = 11 }, ["Ataka_9M120F"] = { explosive = 11 }, ["P_9M117"] = { explosive = 0 }, ["KH-66_Grom"] = { explosive = 108 }, ["X_23"] = { explosive = 111 }, ["X_23L"] = { explosive = 111 }, ["X_28"] = { explosive = 160 }, ["X_25ML"] = { explosive = 89 }, ["X_25MR"] = { explosive = 140 }, ["X_29L"] = { explosive = 320 }, ["X_29T"] = { explosive = 320 }, ["X_29TE"] = { explosive = 320 }, ["AKD-10"] = { explosive = 10 }, --drone --*** ANTI-RADAR MISSILE (ARM) *** ["AGM_88C"] = { explosive = 69 }, ["AGM_88"] = { explosive = 69 }, ["AGM_122"] = { explosive = 12 }, ["LD-10"] = { explosive = 75 }, ["AGM_45A"] = { explosive = 66 }, ["AGM_45B"] = { explosive = 66 }, ["X_58"] = { explosive = 149 }, ["X_25MP"] = { explosive = 90 }, ["X_31P"] = { explosive = 90, shaped_charge = false }, --*** ANTI-SHIP MISSILE (ASh) *** ["AGM_84D"] = { explosive = 488 }, ["Rb 15F"] = { explosive = 500 }, ["C-802AK"] = { explosive = 500 }, ["X_31A"] = { explosive = 89, shaped_charge = false }, --KH-31A ASh ["X_22"] = { explosive = 1200, shaped_charge = false }, --Ash 1ton RDX = 1600KG TNT ["X_35"] = { explosive = 145, shaped_charge = true }, --ASh 145KG --*** CRUISE MISSILE *** ["CM-802AKG"] = { explosive = 240 }, ["AGM_84E"] = { explosive = 360 }, ["AGM_84H"] = { explosive = 380 }, ["X_59M"] = { explosive = 340 }, ["X_65"] = { explosive = 545 }, --*** ROCKETS *** ["HYDRA_70M15"] = { explosive = 5 }, ["HYDRA_70_MK1"] = { explosive = 5 }, ["HYDRA_70_MK5"] = { explosive = 8 }, ["HYDRA_70_M151"] = { explosive = 5 }, ["HYDRA_70_M151_M433"] = { explosive = 5 }, ["HYDRA_70_M229"] = { explosive = 10 }, ["FFAR Mk1 HE"] = { explosive = 5 }, ["FFAR Mk5 HEAT"] = { explosive = 8 }, ["HVAR"] = { explosive = 5 }, ["Zuni_127"] = { explosive = 8 }, ["ARAKM70BHE"] = { explosive = 5 }, ["ARAKM70BAP"] = { explosive = 8 }, ["SNEB_TYPE251_F1B"] = { explosive = 4 }, ["SNEB_TYPE252_F1B"] = { explosive = 4 }, ["SNEB_TYPE253_F1B"] = { explosive = 5 }, ["SNEB_TYPE256_F1B"] = { explosive = 6 }, ["SNEB_TYPE257_F1B"] = { explosive = 8 }, ["SNEB_TYPE251_F4B"] = { explosive = 4 }, ["SNEB_TYPE252_F4B"] = { explosive = 4 }, ["SNEB_TYPE253_F4B"] = { explosive = 5 }, ["SNEB_TYPE256_F4B"] = { explosive = 6 }, ["SNEB_TYPE257_F4B"] = { explosive = 8 }, ["SNEB_TYPE251_H1"] = { explosive = 4 }, ["SNEB_TYPE252_H1"] = { explosive = 4 }, ["SNEB_TYPE253_H1"] = { explosive = 5 }, ["SNEB_TYPE256_H1"] = { explosive = 6 }, ["SNEB_TYPE257_H1"] = { explosive = 8 }, ["MATRA_F4_SNEBT251"] = { explosive = 8 }, ["MATRA_F4_SNEBT253"] = { explosive = 8 }, ["MATRA_F4_SNEBT256"] = { explosive = 8 }, ["MATRA_F1_SNEBT253"] = { explosive = 8 }, ["MATRA_F1_SNEBT256"] = { explosive = 8 }, ["TELSON8_SNEBT251"] = { explosive = 4 }, ["TELSON8_SNEBT253"] = { explosive = 8 }, ["TELSON8_SNEBT256"] = { explosive = 4 }, ["TELSON8_SNEBT257"] = { explosive = 6 }, ["ARF8M3API"] = { explosive = 8 }, ["UG_90MM"] = { explosive = 8 }, ["S-24A"] = { explosive = 24 }, ["S-25OF"] = { explosive = 194 }, ["S-25OFM"] = { explosive = 150 }, ["S-25O"] = { explosive = 150 }, ["S-25-O"] = { explosive = 150 }, ["S_25L"] = { explosive = 190 }, ["S-5M"] = { explosive = 3 }, ["C_5"] = { explosive = 8 }, ["C5"] = { explosive = 5 }, ["C_8"] = { explosive = 5 }, ["C_8OFP2"] = { explosive = 5 }, ["C_13"] = { explosive = 21 }, ["C_24"] = { explosive = 123 }, ["C_25"] = { explosive = 151 }, --*** LASER ROCKETS *** ["AGR_20"] = { explosive = 8 }, ["AGR_20A"] = { explosive = 8 }, ["AGR_20_M282"] = { explosive = 8 }, ["Hydra_70_M282_MPP"] = { explosive = 5, shaped_charge = true }, ["BRM-1_90MM"] = { explosive = 8 }, --*** JF17 weapons changes as per Kurdes *** ["C_701T"] = { explosive = 38 }, ["C_701IR"] = { explosive = 38 }, ["LS_6_100"] = { explosive = 45 }, ["LS_6"] = { explosive = 100 }, ["LS_6_500"] = { explosive = 274 }, ["Type_200A"] = { explosive = 107 }, ["C_802AK"] = { explosive = 500 }, ["CM_802AKG"] = { explosive = 240 }, --*** JF39 Mod by Whisky.Actual as per Kurdes *** ["Brimstone Laser Guided Missile x3"] = { explosive = 38, shaped_charge = true }, ["MAR-1 High Speed Anti-Radiation Missile"] = { explosive = 75 }, ["GBU-39 SDB 285lb Guided Glide-Bomb"] = { explosive = 45 }, ["SPEAR-3 Air-to-Ground Glide Missile"] = { explosive = 38 }, ["Spear EW"] = { explosive = 0 }, --==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==-- --*** Vehicle/Ship based ***-- --==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==-- --*** Rocketry *** ["9M22U"] = { explosive = 25, groundordnance = true }, --122mm HE rocket, BM-21 Grad (~20-30 kg TNT equiv) ["GRAD_9M22U"] = { explosive = 25, groundordnance = true }, --122mm HE rocket, BM-21 Grad (~20-30 kg TNT equiv) -- ["M26"] = { explosive = 0, groundordnance = true}, --227mm cluster rocket, M270 MLRS (adjusted for cluster) ["M26"] = { explosive = 0, cluster = true, submunition_count = 644, submunition_explosive = 0.1, submunition_name = "M77", groundordnance = true }, --227mm cluster rocket, M270 MLRS (adjusted for cluster) ["SCUD_RAKETA"] = { explosive = 985, groundordnance = true }, ["SMERCH_9M55F"] = { explosive = 46, groundordnance = true }, --220mm HE rocket, (~25-45 kg TNT equiv) ["TOW2"] = { explosive = 6.5, shaped_charge = true, groundordnance = true }, --ATGM --*** Shells *** ---***AAA set to 0.0000001 so there is no extra damage but there is light and sound - added for night time use*** ["weapons.shells.Bofors_40mm_HE"] = { explosive = 0.0000001, groundordnance = true }, --WWII Bofors 40mm AAA ["weapons.shells.Flak18_Sprgr_39"] = { explosive = 0.0000001, groundordnance = true }, --WWII German 88mm Flak 18 ["weapons.shells.Flak41_Sprgr_39"] = { explosive = 0.0000001, groundordnance = true }, --WWII German 88mm Flak 41 ["weapons.shells.KS19_100HE"] = { explosive = 0.0000001, groundordnance = true }, --Modern Soviet 100mm AAA ["weapons.shells.QF94_AA_HE"] = { explosive = 0.0000001, groundordnance = true }, --WWII British 94mm AAA ["weapons.shells.ship_Bofors_40mm_HE"] = { explosive = 0.0000001, groundordnance = true }, --WWII Naval Bofors 40mm AAA ["weapons.shells.Sprgr_34_L70"] = { explosive = 0.0000001, groundordnance = true }, --WWII German 88mm Flak 36/37 ["weapons.shells.Sprgr_38"] = { explosive = 0.0000001, groundordnance = true }, --WWII German 88mm Flak 38 ["weapons.shells.Sprgr_39"] = { explosive = 0.0000001, groundordnance = true }, --WWII German 88mm Flak 18/36/37 ["weapons.shells.Sprgr_43_L71"] = { explosive = 0.0000001, groundordnance = true }, --WWII German 88mm Flak 43 --***Tank etc*** ["weapons.shells.M_105mm_HE"] = { explosive = 12, groundordnance = true }, --105mm HE shell, M119/M102 (~10-15 kg TNT equiv) ["weapons.shells.M_155mm_HE"] = { explosive = 60, groundordnance = true }, --155mm HE shell, M777/M109 (~50-70 kg TNT equiv) ["weapons.shells.2A60_120"] = { explosive = 18, groundordnance = true }, --120mm HE shell, 2B11 mortar (~15-20 kg TNT equiv) ["weapons.shells.2A18_122"] = { explosive = 22, groundordnance = true }, --122mm HE shell, D-30 (~20-25 kg TNT equiv) ["weapons.shells.2A33_152"] = { explosive = 50, groundordnance = true }, --152mm HE shell, SAU Akatsia (~40-60 kg TNT equiv) ["weapons.shells.PLZ_155_HE"] = { explosive = 60, groundordnance = true }, --155mm HE shell, PLZ05 (~50-70 kg TNT equiv) ["weapons.shells.M185_155"] = { explosive = 60, groundordnance = true }, --155mm HE shell, M109 (~50-70 kg TNT equiv) ["weapons.shells.2A64_152"] = { explosive = 50, groundordnance = true }, --152mm HE shell, SAU Msta (~40-60 kg TNT equiv) ["weapons.shells.2A46M_125_HE"] = { explosive = 5, groundordnance = true }, --125mm HE shell, T-90 (~5-6 kg TNT equiv) ["weapons.shells.HESH_105"] = { explosive = 6, groundordnance = true }, --105mm HESH shell, M1128 Stryker (~4-6 kg TNT equiv) ---*** Naval *** ["BGM_109B"] = { explosive = 450, groundordnance = true }, -- Tomahawk ["AGM_84S"] = { explosive = 225, groundordnance = true }, --Harpoon missile, Ticonderoga (~200-250 kg TNT equiv) ["P_500"] = { explosive = 500, groundordnance = true }, --P-500 Bazalt missile, Moscow (~450-550 kg TNT equiv) ["weapons.shells.AK176_76"] = { explosive = 1, groundordnance = true }, --76mm HE shell, AK-176 (~0.7-1 kg TNT equiv) ["weapons.shells.A222_130"] = { explosive = 5, groundordnance = true }, --130mm HE shell, A-222 Bereg (~4-5 kg TNT equiv) ["weapons.shells.53-UBR-281U"] = { explosive = 5, groundordnance = true }, --130mm HE shell, SM-2-1 (~4-5 kg TNT equiv) ["weapons.shells.PJ87_100_PFHE"] = { explosive = 3, groundordnance = true }, --100mm HE-PF shell, Type 052B (~2.4-3.4 kg TNT equiv) ["weapons.shells.AK100_100"] = { explosive = 3, groundordnance = true }, --100mm HE shell, AK-100 (~2.5-3.5 kg TNT equiv) AK-100 100mm (e.g., on Project 1135 Krivak-class) ["weapons.shells.AK130_130"] = { explosive = 5, groundordnance = true }, --130mm HE shell, AK-130 (~4-5 kg TNT equiv) AK-130 130mm (e.g., on Project 956 Sovremenny-class) ["weapons.shells.2A70_100"] = { explosive = 3, groundordnance = true }, --100mm HE shell, 2A70 (~3-3.5 kg TNT equiv) 2A70 100mm (e.g., on Project 775 Ropucha-class) ["weapons.shells.OTO_76"] = { explosive = 1, groundordnance = true }, --76mm HE shell, OTO Melara (~0.8-1.1 kg TNT equiv) OTO Melara 76mm (e.g., on NATO frigates like Oliver Hazard Perry-class) ["weapons.shells.MK45_127"] = { explosive = 5, groundordnance = true }, --127mm HE shell, Mark 45 (~4.8-5.6 kg TNT equiv) Mark 45 127mm (e.g., on Arleigh Burke-class destroyers) ["weapons.shells.PJ26_76_PFHE"] = { explosive = 1, groundordnance = true }, --76mm HE-PF shell, PJ-26 (~0.8-1.1 kg TNT equiv) ["weapons.shells.53-UOR-281U"] = { explosive = 5, groundordnance = true }, --130mm HE shell, SM-2-1 (~4-5 kg TNT equiv) ["weapons.shells.MK75_76"] = { explosive = 1, groundordnance = true }, --76mm HE shell, Mk 75 (~0.8-1.1 kg TNT equiv) --*** Bismark Mod Weapon *** ["weapons.shells.Breda_37_HE"] = { explosive = 70, groundordnance = true }, --380mm HE shell, 38 cm SK C/34 (~60-75 kg TNT equiv) --*** Bismark Mod Weapons *** ["weapons.shells.380mm_HE"] = { explosive = 70, groundordnance = true }, --380mm HE shell, 38 cm SK C/34 (~60-75 kg TNT equiv) ["weapons.shells.SK_C_33_105_HE"] = { explosive = 15, groundordnance = true }, --105mm HE shell, SK C/33 (~12-16 kg TNT equiv) } napalm_unitcat_tabl = { ["Infantry"] = { maxDamageDistance = 50, explosionPower = 0.5 }, ["Tank"] = { maxDamageDistance = 30, explosionPower = 5 }, ["Artillery"] = { maxDamageDistance = 40, explosionPower = 5 }, ["Armored Vehicle"] = { maxDamageDistance = 35, explosionPower = 5 }, ["Anti-Air"] = { maxDamageDistance = 35, explosionPower = 5 }, ["Helicopter"] = { maxDamageDistance = 45, explosionPower = 5 }, ["Airplane"] = { maxDamageDistance = 40, explosionPower = 5 }, ["Structure"] = { maxDamageDistance = 60, explosionPower = 60 } } --Table for cluster submunitions clusterSubMunTable = { ["Mk 118"] = { explosive = 2 }, --Rockeye/CBU99, 247 bomblets, 0.18 kg TNT, expected to damage: infantry, light vehicles, light armor (up to ~190 mm penetration) ["BLU-97B"] = { explosive = 3 }, --CBU_87/CBU_103, 202 bomblets, 0.45 kg TNT, expected to damage: infantry, light vehicles, light to medium armor, soft structures ["BLU-97/B"] = { explosive = 3 }, --AGM 154s, variable bomblets, 0.45 kg TNT, expected to damage: infantry, light vehicles, light to medium armor, soft structures --["BLU-108"] = { explosive = 9.0 }, --CBU_97/CBU_105, 40 bomblets, 3.4 kg TNT, expected to damage: medium to heavy armor, vehicles, fortifications **DISABLED DUE TO BEING AFFECTIVE ALREADY** ["AO-2-5"] = { explosive = 2.5 }, --RBK_500AO, 96 bomblets, 0.37 kg TNT, expected to damage: infantry, light vehicles, light armor ["BLU-3"] = { explosive = 1 }, --Heatblur F4 BLU-3_GROUP, 426 bomblets, 0.08 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["BLU-3B"] = { explosive = 1 }, --Heatblur F4, 426 bomblets, 0.08 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["BLU-4B"] = { explosive = 1 }, --Heatblur F4, 96 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["HEAT"] = { explosive = 1.5 }, --BL_755, 147 bomblets, 0.6 kg TNT, expected to damage: infantry, light vehicles, light to medium armor ["MJ2"] = { explosive = 1 }, --Mjolnir, 72 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["MJ1"] = { explosive = 1 }, --Gripen/DWS Mjolnir, 72 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["GR_66_AC"] = { explosive = 1 }, --BL66_BELOUGA, 49 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["9N235"] = { explosive = 1.85 }, --sMERCH 9m55K, 30 bomblets, 0.18 kg TNT, expected to damage: infantry, light vehicles, light armor ["GB-06"] = { explosive = 2 }, --GB6 glide bomb, variable bomblets, 0.3 kg TNT, expected to damage: infantry, light vehicles, light armor ["SD-10A"] = { explosive = 1 }, --WW2 German cluster/AB_500_1_SD_10A, 78 bomblets, 0.07 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["PTAB-2.5KO"] = { explosive = 1 }, --PBKF - 12 x PTAB-2.5KO, 12 bomblets, 0.25 kg TNT, expected to damage: infantry, light vehicles, light armor ["PTAB-10-5"] = { explosive = 2.5 }, --RBK_500AO, 96 bomblets, 0.5 kg TNT, expected to damage: infantry, light vehicles, light to medium armor ["OAB-2-5RT"] = { explosive = 1 }, --RBK_500U_OAB_2_5RT, 126 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["AO-1SCh"] = { explosive = 1 }, --RBK_250_275_AO_1SCH, 275 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["MM-06"] = { explosive = 1.5 }, --GB-6-SFW, variable bomblets, 0.3 kg TNT, expected to damage: infantry, light vehicles, light armor ["BETAB-M"] = { explosive = 5.0 }, --RBK_500U_BETAB_M, 25 bomblets, 0.76 kg TNT, expected to damage: medium armor, fortifications, concrete structures ["MJ1-MJ2"] = { explosive = 1 }, --BK90_MJ1_MJ2, 72 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["BLU-61"] = { explosive = 1 }, --CBU_52B, 72 bomblets, 0.12 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["BLG-66 AC"] = { explosive = 1 }, --BLG66, 49 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["PTAB-2-5"] = { explosive = 1 }, --KMGU_2_PTAB_2_5KO, 96 bomblets, 0.25 kg TNT, expected to damage: infantry, light vehicles, light armor ["AO-2.5RT"] = { explosive = 1 }, --BKF_AO2_5RT, 126 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["M77"] = { explosive = 1 }, --M26, 600 bomblets, 0.09 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["SD-2"] = { explosive = 1 }, --AB_250_2_SD_2, 140 bomblets, 0.06 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets ["BLG-66 EG"] = { explosive = 1 }, --BLG66_EG, 49 bomblets, 0.1 kg TNT, expected to damage: infantry, unarmored vehicles, soft targets } --currently unused unitTypeTable = { ["Infantry"] = { damageModifier = 1.0 }, -- Unarmored, highly vulnerable to explosives and napalm ["Tank"] = { damageModifier = 0.3 }, -- Heavy armor, resistant to most bomblets and napalm ["Artillery"] = { damageModifier = 0.5 }, -- Moderate armor, vulnerable to precise hits ["Armored Vehicle"] = { damageModifier = 0.4 }, -- Light to medium armor, moderately resistant ["Anti-Air"] = { damageModifier = 0.5 }, -- Light armor, exposed systems vulnerable ["Helicopter"] = { damageModifier = 0.6 }, -- Lightly armored, susceptible to fire and shrapnel ["Airplane"] = { damageModifier = 0.5 }, -- Grounded, moderate resilience but vulnerable to fire ["Structure"] = { damageModifier = 0.8 }, -- Varies, but often vulnerable to sustained damage (e.g., napalm, BETAB-M) ["Unarmored Vehicle"] = { damageModifier = 0.9 }, -- Soft-skinned, highly vulnerable to explosives } --Unit types eligible for Trophy APS local TrophyAllUnitType = { --["M-1 Abrams"] = true, --Example unit, uncomment to enable Trophy APS for all M1A2 Abrams units as opposed to only name searching. You can add units too. } --Weapons to be tracked by script and max range to be tracked from local trophyWeapons = { --For weapon types: typeName:gsub("^weapons%.missiles%.", ""):gsub("^weapons%.nurs%.", ""), other types not supported in code currently. shells were too fast. ["AGM_114K"] = { range = 8000, name = "Hellfire" }, --Hellfire missile ["AGM_114"] = { range = 8000, name = "Hellfire" }, --Hellfire ["vikhr_m"] = { range = 10000, name = "Vikhr" }, --Vikhr ATGM ["Vikhr_9M127_1"] = { range = 10000, name = "Vikhr" }, --Vikhr ATGM ["AT_6"] = { range = 5000, name = "Shturm" }, --Shturm ATGM ["Ataka_9M120"] = { range = 6000, name = "Ataka" }, --Ataka ATGM ["Ataka_9M120F"] = { range = 6000, name = "Ataka" }, --Ataka ATGM ["P_9M117"] = { range = 5000, name = "AT-10 Stabber" }, --AT-10 Stabber ["9M133"] = { range = 5500, name = "Kornet" }, --Kornet ATGM ["9M120"] = { range = 6000, name = "Ataka" }, --Ataka ATGM ["HOT3"] = { range = 4300, name = "HOT-3" }, --HOT-3 ATGM ["PG_16V"] = { range = 800, name = "RPG-16 HEAT" }, --RPG-16 HEAT ["HYDRA_70_M151"] = { range = 8000, name = "Hydra 70 M151" }, --Hydra 70 M151 HE ["HYDRA_70_M282"] = { range = 8000, name = "Hydra 70 M282" }, --Hydra 70 M282 Multi-Purpose Penetrator ["HYDRA_70_MK5"] = { range = 8000, name = "Hydra 70 Mk5" }, --Hydra 70 Mk5 HEAT ["S_8KOM"] = { range = 4000, name = "S-8KOM" }, --S-8KOM HEAT rocket ["S_5M"] = { range = 3000, name = "S-5M" }, --S-5M HE rocket ["S_24B"] = { range = 4000, name = "S-24B" }, --S-24B HE rocket ["3BK18M"] = { range = 4000, name = "125mm HEAT" }, --125mm HEAT round ["M456"] = { range = 3000, name = "105mm HEAT" }, --105mm HEAT round ["HYDRA_70M15"] = { range = 4000, name = "Hydra 70 M15" }, ["HYDRA_70_MK1"] = { range = 4000, name = "Hydra 70 Mk1" }, ["HYDRA_70_M151_M433"] = { range = 4000, name = "Hydra 70 M151 M433" }, ["HYDRA_70_M229"] = { range = 8000, name = "Hydra 70 M229" }, --Hydra 70 M229 ["FFAR Mk1 HE"] = { range = 8000, name = "FFAR Mk1 HE" }, --FFAR Mk1 HE ["FFAR Mk5 HEAT"] = { range = 8000, name = "FFAR Mk5 HEAT" }, --FFAR Mk5 HEAT ["HVAR"] = { range = 8000, name = "HVAR" }, --HVAR rocket ["Zuni_127"] = { range = 8000, name = "Zuni 127mm" }, --Zuni 127mm rocket ["ARAKM70BHE"] = { range = 8000, name = "ARAK M70B HE" }, --ARAK M70B HE ["ARAKM70BAP"] = { range = 8000, name = "ARAK M70B AP" }, --ARAK M70B AP ["SNEB_TYPE251_F1B"] = { range = 4000, name = "SNEB Type 251" }, --SNEB Type 251 ["SNEB_TYPE252_F1B"] = { range = 4000, name = "SNEB Type 252" }, --SNEB Type 252 ["SNEB_TYPE253_F1B"] = { range = 4000, name = "SNEB Type 253" }, --SNEB Type 253 ["SNEB_TYPE256_F1B"] = { range = 4000, name = "SNEB Type 256" }, --SNEB Type 256 ["SNEB_TYPE257_F1B"] = { range = 4000, name = "SNEB Type 257" }, --SNEB Type 257 ["SNEB_TYPE251_F4B"] = { range = 4000, name = "SNEB Type 251 F4B" }, --SNEB Type 251 F4B ["SNEB_TYPE252_F4B"] = { range = 4000, name = "SNEB Type 252 F4B" }, --SNEB Type 252 F4B ["SNEB_TYPE253_F4B"] = { range = 4000, name = "SNEB Type 253 F4B" }, --SNEB Type 253 F4B ["SNEB_TYPE256_F4B"] = { range = 4000, name = "SNEB Type 256 F4B" }, --SNEB Type 256 F4B ["SNEB_TYPE257_F4B"] = { range = 4000, name = "SNEB Type 257 F4B" }, --SNEB Type 257 F4B ["SNEB_TYPE251_H1"] = { range = 4000, name = "SNEB Type 251 H1" }, --SNEB Type 251 H1 ["SNEB_TYPE252_H1"] = { range = 4000, name = "SNEB Type 252 H1" }, --SNEB Type 252 H1 ["SNEB_TYPE253_H1"] = { range = 4000, name = "SNEB Type 253 H1" }, --SNEB Type 253 H1 ["SNEB_TYPE256_H1"] = { range = 4000, name = "SNEB Type 256 H1" }, --SNEB Type 256 H1 ["SNEB_TYPE257_H1"] = { range = 4000, name = "SNEB Type 257 H1" }, --SNEB Type 257 H1 ["MATRA_F4_SNEBT251"] = { range = 4000, name = "Matra SNEB Type 251" }, --Matra SNEB Type 251 ["MATRA_F4_SNEBT253"] = { range = 4000, name = "Matra SNEB Type 253" }, --Matra SNEB Type 253 ["MATRA_F4_SNEBT256"] = { range = 4000, name = "Matra SNEB Type 256" }, --Matra SNEB Type 256 ["MATRA_F1_SNEBT253"] = { range = 4000, name = "Matra SNEB Type 253 F1" }, --Matra SNEB Type 253 F1 ["MATRA_F1_SNEBT256"] = { range = 4000, name = "Matra SNEB Type 256 F1" }, --Matra SNEB Type 256 F1 ["TELSON8_SNEBT251"] = { range = 4000, name = "Telson 8 SNEB Type 251" }, --Telson 8 SNEB Type 251 ["TELSON8_SNEBT253"] = { range = 4000, name = "Telson 8 SNEB Type 253" }, --Telson 8 SNEB Type 253 ["TELSON8_SNEBT256"] = { range = 4000, name = "Telson 8 SNEB Type 256" }, --Telson 8 SNEB Type 256 ["TELSON8_SNEBT257"] = { range = 4000, name = "Telson 8 SNEB Type 257" }, --Telson 8 SNEB Type 257 ["ARF8M3API"] = { range = 4000, name = "ARF-8/M3 API" }, --ARF-8/M3 API rocket ["UG_90MM"] = { range = 4000, name = "UG 90mm" }, --UG 90mm rocket ["S-24A"] = { range = 4000, name = "S-24A" }, ["S-25OF"] = { range = 4000, name = "S-25OF" }, ["S-25OFM"] = { range = 4000, name = "S-25OFM" }, ["S-25O"] = { range = 4000, name = "S-25O" }, ["S-25-O"] = { range = 4000, name = "S-25-O" }, ["S_25L"] = { range = 4000, name = "S-25L" }, ["S-5M"] = { range = 4000, name = "S-5M" }, ["C_5"] = { range = 4000, name = "S-5" }, ["C5"] = { range = 4000, name = "S-5" }, ["C_8"] = { range = 4000, name = "S-8" }, ["C_8OFP2"] = { range = 4000, name = "S-8OFP2" }, ["C_13"] = { range = 4000, name = "S-13" }, ["C_24"] = { range = 4000, name = "S-24" }, ["C_25"] = { range = 4000, name = "S-25" }, ["TOW"] = { range = 3750, name = "TOW" }, --TOW missile } --Weapons tracked for tactical uses tacticalwpn_tabl = { -- F22 AGM Nuke ["AGM_88G_N_ARM"] = { nuke = 50000 }, -- Anti-radiation nuke (nuke value unused currently) } --Ammo tracking table: { unitId = { FR = count, BL = count } } local trophyAmmo = {} local effectSmokeId = 1 ----[[ ##### HELPER/UTILITY FUNCTIONS/TABLES ##### ]]---- --Global tables and value setting local processedUnitIds = {} local killfeedTable = {} local splashKillfeedTable = {} local splashKillfeedTemp = {} local HitEventTempTable = {} local VehicleIEDPendingTable = {} local CargoCookOffPendingTable = {} local processedCookoffs = {} local tacticalFuelTankSpawnQueue = {} local fuelTankSpawnQueue = {} local lastSpawnTime = 0 local SPAWN_INTERVAL = 0.1 --0.1s gap between spawns local trophyAmmo = {} --Trophy Ammo tracking table: { unitId = { FR = count, BL = count } } local trophyHandler = {} local trophyWeaponsLookup = {} local recentExplosions = {} local strobeUnits = {} local individualStrobeUnits = {} local processedSmoke = {} giantExplosionTargets = {} giantExplosionTestTargets = {} cargoEffectsQueue = {} WpnHandler = {} tracked_target_position = nil --Store the last known position of TargetUnit for giant explosion tracked_weapons = {} local processedUnitsGlobal = {} napalmCounter = 1 local recentExplosions = {} local cbuProcessed = {} --Table to track processed unitID-weaponID pairs for cbus local cbuParentUnits = {} -- Helper function to dump table contents (for undocumented event fields) local function dumpTable(t, indent) indent = indent or "" local result = "" for k, v in pairs(t) do if type(v) == "table" then result = result .. indent .. k .. ": {\n" .. dumpTable(v, indent .. " ") .. indent .. "}\n" else result = result .. indent .. k .. ": " .. tostring(v) .. "\n" end end return result end --Helper function to approximate Gaussian random (since math.randomGaussian isn't available) local function gaussRandom(mean, stdDev) local u1, u2 = math.random(), math.random() local z = math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2) return mean + stdDev * z end --Function to safely get data with pcall local function safeGet(func, default) local success, result = pcall(func) return success and result or default end --Function to clear processed unit IDs after a delay function clearProcessedUnitIds(unitId) if processedUnitIds[unitId] then processedUnitIds[unitId] = nil if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Cleared unit ID " .. unitId .. " from processedUnitIds") end end end --Debug functions local function debugMsg(str) if splash_damage_options.debug == true then debugCounter = (debugCounter or 0) + 1 local uniqueStr = str .. " [" .. timer.getTime() .. " - " .. debugCounter .. "]" trigger.action.outText(uniqueStr, 5) env.info("DEBUG: " .. uniqueStr) end end local function debugTrophy(str) if splash_damage_options.trophy_debug then trophydebugCounter = (trophydebugCounter or 0) + 1 local uniqueStr = str .. " [" .. timer.getTime() .. " - " .. trophydebugCounter .. "]" --trigger.action.outText(uniqueStr, 5) --uncomment to show messages ingame too env.info("[Trophy Debug:] " .. uniqueStr) end end local function debugCargoCookOff(str) if splash_damage_options.cargocookoff_debug then cargocookoffdebugCounter = (cargocookoffdebugCounter or 0) + 1 local uniqueStr = str .. " [" .. timer.getTime() .. " - " .. cargocookoffdebugCounter .. "]" --trigger.action.outText(uniqueStr, 5) --uncomment to show messages ingame too env.info("[CargoCookoff Debug:] " .. uniqueStr) end end local function debugCBUBombletHit(str) if splash_damage_options.CBU_Bomblet_Hit_debug then cbubombletdebugCounter = (cbubombletdebugCounter or 0) + 1 local uniqueStr = str .. "[" .. timer.getTime() .. " - " .. cbubombletdebugCounter .. "]" env.info("[CBU Bomblet Hit Debug:] " .. uniqueStr) end end local function debugStrobeMarker(str) if splash_damage_options.StrobeMarker_debug then strobeMarkerDebugCounter = (strobeMarkerDebugCounter or 0) + 1 local uniqueStr = str .. " [" .. timer.getTime() .. " - " .. strobeMarkerDebugCounter .. "]" env.info("[StrobeMarker Debug:] " .. uniqueStr) end end function napalm_phosphor(vec3) local baseFlareCount = math.random(0, 8) --Wider range for variation local randomFactor = math.random(0.1, 1) --Random scaling per call local scaledFlareCount = math.max(1, math.floor(baseFlareCount * splash_damage_options.napalm_phosphor_multiplier * randomFactor)) for i = 1, scaledFlareCount do local randomAzimuth = math.random(0, 359) --Random angle for scatter local offsetX = math.random(-15, 15) --Position offset (meters) local offsetZ = math.random(-15, 15) local flarePos = { x = vec3.x + offsetX, y = vec3.y, z = vec3.z + offsetZ } trigger.action.signalFlare(flarePos, 2, randomAzimuth) end if splash_damage_options.debug then debugMsg("Triggered " .. scaledFlareCount .. " napalm phosphor flares at X: " .. string.format("%.0f", vec3.x) .. ", Z: " .. string.format("%.0f", vec3.z)) end end --getSpreadPoints function local function getSpreadPoints(impactPoint, velocity, numPoints, spacing) local points = {} local mag = math.sqrt(velocity.x^2 + velocity.z^2) if mag == 0 then table.insert(points, {x = impactPoint.x, y = land.getHeight({x = impactPoint.x, y = impactPoint.z}), z = impactPoint.z}) return points end local dir = {x = velocity.x / mag, z = velocity.z / mag} local perpDir = {x = -dir.z, z = dir.x} --Perpendicular to velocity direction local prevHeight = land.getHeight({x = impactPoint.x, y = impactPoint.z}) for i = 1, numPoints do local offset = (i - 1) * spacing if splash_damage_options.napalm_doublewide_enabled then --Double-wide: two points with ±15m lateral offset local point1 = { x = impactPoint.x + dir.x * offset + perpDir.x * splash_damage_options.napalm_doublewide_spread, z = impactPoint.z + dir.z * offset + perpDir.z * splash_damage_options.napalm_doublewide_spread } local terrainHeight1 = land.getHeight({x = point1.x, y = point1.z}) local heightDiff1 = terrainHeight1 - prevHeight point1.y = prevHeight + math.max(math.min(heightDiff1, 30), -30) table.insert(points, point1) local point2 = { x = impactPoint.x + dir.x * offset - perpDir.x * splash_damage_options.napalm_doublewide_spread, z = impactPoint.z + dir.z * offset - perpDir.z * splash_damage_options.napalm_doublewide_spread } local terrainHeight2 = land.getHeight({x = point2.x, y = point2.z}) local heightDiff2 = terrainHeight2 - prevHeight point2.y = prevHeight + math.max(math.min(heightDiff2, 30), -30) table.insert(points, point2) prevHeight = (terrainHeight1 + terrainHeight2) / 2 else --Single point, linear spread local point = { x = impactPoint.x + dir.x * offset, z = impactPoint.z + dir.z * offset } local terrainHeight = land.getHeight({x = point.x, y = point.z}) local heightDiff = terrainHeight - prevHeight point.y = prevHeight + math.max(math.min(heightDiff, 30), -30) table.insert(points, point) prevHeight = terrainHeight end end return points end function explodeNapalm(vec3) local explosionPos = { x = vec3.x, y = vec3.y + 1.6, --Add 1.6m to the ground height z = vec3.z } trigger.action.explosion(explosionPos, 10) end --Helper function to calculate 2D distance local function getDistance(point1, point2) local dX = math.abs(point1.x - point2.x) local dZ = math.abs(point1.z - point2.z) return math.sqrt(dX * dX + dZ * dZ) end --Scan for units around the napalm explosions and apply damage if required function scanUnitsForNapalm(posX, posY, posZ, playerName) if not splash_damage_options.napalm_unitdamage_enable then if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Napalm unit damage disabled, skipping scan") end return end if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Starting scan at (X: " .. posX .. ", Y: " .. posY .. ", Z: " .. posZ .. ") with radius " .. splash_damage_options.napalm_unitdamage_scandistance) end local volS = { id = world.VolumeType.SPHERE, params = { point = {x = posX, y = posY, z = posZ}, radius = splash_damage_options.napalm_unitdamage_scandistance } } local foundUnits = {} local status, err = pcall(function() --Scan for units world.searchObjects(Object.Category.UNIT, volS, function(foundObject) local success, result = pcall(function() if foundObject:isExist() and foundObject:getCategory() == Object.Category.UNIT then local unitType = foundObject:getTypeName() or "Unknown" --Exclude Fuel tank if unitType ~= "Fuel tank" then local unitPos = foundObject:getPoint() local distance = getDistance({x = posX, y = posY, z = posZ}, unitPos) if distance <= splash_damage_options.napalm_unitdamage_scandistance then local category = "Unknown" local desc = foundObject:getDesc() if desc and foundObject:hasAttribute("Infantry") then category = "Infantry" elseif desc and foundObject:hasAttribute("Tanks") then category = "Tank" elseif desc and foundObject:hasAttribute("Artillery") then category = "Artillery" elseif desc and foundObject:hasAttribute("Armored vehicles") then category = "Armored Vehicle" elseif desc and foundObject:hasAttribute("AA") then category = "Anti-Air" elseif desc and foundObject:hasAttribute("Helicopters") then category = "Helicopter" elseif desc and foundObject:hasAttribute("Planes") then category = "Airplane" end table.insert(foundUnits, { unit = foundObject, id = foundObject:getID(), type = unitType, distance = distance, category = category, position = unitPos }) end end end end) if not success and splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Error processing unit ID " .. (foundObject:getID() or "unknown") .. ": " .. tostring(result)) end return true end) --Scan for static objects world.searchObjects(Object.Category.STATIC, volS, function(foundObject) local success, result = pcall(function() if foundObject:isExist() and foundObject:getCategory() == Object.Category.STATIC then local unitType = foundObject:getTypeName() or "Unknown" --Exclude Fuel tank if unitType ~= "Fuel tank" then local unitPos = foundObject:getPoint() local distance = getDistance({x = posX, y = posY, z = posZ}, unitPos) if distance <= splash_damage_options.napalm_unitdamage_scandistance then table.insert(foundUnits, { unit = foundObject, id = foundObject:getID(), type = unitType, distance = distance, category = "Structure", position = unitPos }) end end end end) if not success and splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Error processing static object ID " .. (foundObject:getID() or "unknown") .. ": " .. tostring(result)) end return true end) end) if not status and splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Error during scan: " .. tostring(err)) return end table.sort(foundUnits, function(a, b) return a.distance < b.distance end) if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Scan completed, found " .. #foundUnits .. " objects within " .. splash_damage_options.napalm_unitdamage_scandistance .. " meters at position (X: " .. posX .. ", Y: " .. posY .. ", Z: " .. posZ .. ")") --Log all found objects for _, unitData in ipairs(foundUnits) do env.info("scanUnitsForNapalm: Found object ID " .. tostring(unitData.id) .. " of type: " .. unitData.type .. ", Category: " .. unitData.category .. ", Distance: " .. string.format("%.2f", unitData.distance) .. " meters, Position: (X: " .. string.format("%.2f", unitData.position.x) .. ", Y: " .. string.format("%.2f", unitData.position.y) .. ", Z: " .. string.format("%.2f", unitData.position.z) .. ")") end end if #foundUnits > 0 then local processedPositions = {} --Track processed coordinates for this scan local explosionIndex = 0 for _, unitData in ipairs(foundUnits) do if napalm_unitcat_tabl[unitData.category] and unitData.distance <= napalm_unitcat_tabl[unitData.category].maxDamageDistance then --Check if unit ID has already been processed if not processedUnitIds[unitData.id] then --Check for duplicate position (within 1 meter) local posKey = string.format("%.0f_%.0f_%.0f", unitData.position.x, unitData.position.y, unitData.position.z) if not processedPositions[posKey] then --Check if unit is still alive (for units) or exists (for statics) local isAlive = unitData.unit:isExist() and (unitData.category == "Structure" or unitData.unit:getLife() > 0) if isAlive then processedPositions[posKey] = true processedUnitIds[unitData.id] = true local power = napalm_unitcat_tabl[unitData.category].explosionPower --Calculate delay local delay = splash_damage_options.napalm_unitdamage_startdelay if splash_damage_options.napalm_unitdamage_startdelay > 0 then delay = delay + (explosionIndex * splash_damage_options.napalm_unitdamage_spreaddelay) explosionIndex = explosionIndex + 1 end --Adjust position for infantry to reduce ground interaction local explosionPos = unitData.position if unitData.category == "Infantry" then explosionPos = { x = unitData.position.x, y = land.getHeight({x = unitData.position.x, y = unitData.position.z}) + 1.6, z = unitData.position.z } end if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Scheduling explosion on unit ID " .. tostring(unitData.id) .. " (" .. unitData.type .. ") at (X: " .. string.format("%.2f", explosionPos.x) .. ", Z: " .. string.format("%.2f", explosionPos.z) .. ") with power " .. power .. " after " .. string.format("%.2f", delay) .. "s") end --Record potential kills for kill feed if splash_damage_options.killfeed_enable then timer.scheduleFunction(function(params) local unit = params.unit local playerName = params.playerName or "unknown" local weaponName = params.weaponName or "Napalm" if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Checking killfeed for unit ID " .. tostring(params.unitId) .. " (" .. params.unitType .. "), player: " .. playerName .. ", exists: " .. tostring(unit:isExist()) .. ", life: " .. (unit:isExist() and unit:getLife() or "N/A")) end if not unit:isExist() or (unit:isExist() and unit:getLife() <= 0) then local alreadyInKillfeed = false for _, entry in ipairs(splashKillfeedTable) do if entry.unitId == params.unitId then alreadyInKillfeed = true break end end if alreadyInKillfeed and splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Unit ID " .. tostring(params.unitId) .. " already in splashKillfeedTable, skipping") end if not alreadyInKillfeed then local status, isPlayer = pcall(function() local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.name == playerName then return true end end return false end) if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Player validation for " .. playerName .. ": status=" .. tostring(status) .. ", isPlayer=" .. tostring(isPlayer)) end if status and isPlayer then table.insert(splashKillfeedTemp, { playerName = playerName, weaponName = weaponName, unitName = params.unitName, unitType = params.unitType, unitId = params.unitId, time = timer.getTime(), position = params.position }) if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Added to splashKillfeedTemp: unit ID " .. tostring(params.unitId) .. " (" .. params.unitType .. ") destroyed by " .. playerName .. ", temp table size: " .. #splashKillfeedTemp) end if splash_damage_options.killfeed_game_messages then local msg = string.format("%s destroyed by %s's %s", params.unitType, playerName, weaponName) local status, err = pcall(function() trigger.action.outTextForCoalition(2, msg, splash_damage_options.killfeed_game_message_duration) end) if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Attempted to display killfeed message for unit ID " .. tostring(params.unitId) .. ": status=" .. tostring(status) .. ", error=" .. tostring(err or "none")) end end end end else if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Skipped killfeed for unit ID " .. tostring(params.unitId) .. " (" .. params.unitType .. "), exists: " .. tostring(unit:isExist()) .. ", life: " .. (unit:isExist() and unit:getLife() or "N/A")) end end if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Scheduling splashKillFeed for unit ID " .. tostring(params.unitId) .. " at time " .. timer.getTime()) end timer.scheduleFunction(splashKillFeed, {}, timer.getTime() + splash_damage_options.killfeed_splashdelay) end, { unit = unitData.unit, playerName = playerName, weaponName = "Napalm", unitName = unitData.unit:getName() or "unknown", unitType = unitData.type, unitId = unitData.id, position = explosionPos }, timer.getTime() + delay + 1) -- Increased to 2 seconds end timer.scheduleFunction(function(params) trigger.action.explosion(params.position, params.power) end, {position = explosionPos, power = power}, timer.getTime() + delay) --Schedule cleanup for this unit ID 20 seconds after its explosion timer.scheduleFunction(clearProcessedUnitIds, unitData.id, timer.getTime() + delay + 20) elseif splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Skipped explosion for unit ID " .. tostring(unitData.id) .. " (" .. unitData.type .. ") at (X: " .. string.format("%.2f", unitData.position.x) .. ", Z: " .. string.format("%.2f", unitData.position.z) .. ") because unit is not alive (isExist: " .. tostring(unitData.unit:isExist()) .. ", life: " .. (unitData.category == "Structure" and "N/A" or tostring(unitData.unit:getLife())) .. ")") end elseif splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Skipped explosion for unit ID " .. tostring(unitData.id) .. " (" .. unitData.type .. ") at (X: " .. string.format("%.2f", unitData.position.x) .. ", Z: " .. string.format("%.2f", unitData.position.z) .. ") due to duplicate position") end elseif splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: Skipped explosion for unit ID " .. tostring(unitData.id) .. " (" .. unitData.type .. ") at (X: " .. string.format("%.2f", unitData.position.x) .. ", Z: " .. string.format("%.2f", unitData.position.z) .. ") due to already processed unit ID") end end end else if splash_damage_options.napalm_unitdamage_debug then env.info("scanUnitsForNapalm: No objects found in scan area") end end end function removeNapalm(staticName) StaticObject.getByName(staticName):destroy() end local function tableHasKey(table, key) return table[key] ~= nil end local function gameMsg(str) if splash_damage_options.game_messages == true then trigger.action.outText(str, 5) end end local function getDistance(point1, point2) local x1 = point1.x local y1 = point1.y local z1 = point1.z local x2 = point2.x local y2 = point2.y local z2 = point2.z local dX = math.abs(x1 - x2) local dZ = math.abs(z1 - z2) local distance = math.sqrt(dX * dX + dZ * dZ) return distance end local function getDistance3D(point1, point2) local x1 = point1.x local y1 = point1.y local z1 = point1.z local x2 = point2.x local y2 = point2.y local z2 = point2.z local dX = math.abs(x1 - x2) local dY = math.abs(y1 - y2) local dZ = math.abs(z1 - z2) local distance = math.sqrt(dX * dX + dZ * dZ + dY * dY) return distance end local function vec3Mag(speedVec) return math.sqrt(speedVec.x^2 + speedVec.y^2 + speedVec.z^2) end local function lookahead(speedVec) local speed = vec3Mag(speedVec) local dist = speed * refreshRate * 1.5 return dist end function napalmOnImpact(impactPoint, velocity, weaponName, playerName) if not (splash_damage_options.napalmoverride_enabled or (splash_damage_options.napalm_mk77_enabled and (weaponName == "MK77mod0-WPN" or weaponName == "MK77mod1-WPN"))) then return end --For MK77 cluster munitions, snap impact point to ground local finalImpactPoint = impactPoint if splash_damage_options.napalm_mk77_enabled and (weaponName == "MK77mod0-WPN" or weaponName == "MK77mod1-WPN") then local groundHeight = land.getHeight({x = impactPoint.x, y = impactPoint.z}) finalImpactPoint = { x = impactPoint.x, y = groundHeight, z = impactPoint.z } if splash_damage_options.debug then debugMsg("Snapped MK77 " .. weaponName .. " impact to ground at X: " .. string.format("%.0f", finalImpactPoint.x) .. ", Z: " .. string.format("%.0f", finalImpactPoint.z)) end else --For non-MK77, skip if more than 50m above ground local groundHeight = land.getHeight({x = impactPoint.x, y = impactPoint.z}) if impactPoint.y - groundHeight > 50 then return end --Skip if more than 50m above ground end --Adjust spread points for MK77mod0-WPN (30% more) local spreadPointsCount = splash_damage_options.napalm_spread_points if weaponName == "MK77mod0-WPN" then spreadPointsCount = math.floor(spreadPointsCount * 1.3 + 0.5) --30% more, rounded end --Use horizontal velocity for MK77, full velocity for others local spreadVelocity = velocity if weaponName == "MK77mod0-WPN" or weaponName == "MK77mod1-WPN" then spreadVelocity = {x = velocity.x, z = velocity.z} end local spreadPoints = getSpreadPoints(finalImpactPoint, spreadVelocity, spreadPointsCount, splash_damage_options.napalm_spread_spacing) if splash_damage_options.debug then debugMsg("Generated " .. #spreadPoints .. " spread points for " .. weaponName .. " (expected " .. (splash_damage_options.napalm_doublewide_enabled and spreadPointsCount * 2 or spreadPointsCount) .. ")") for i, point in ipairs(spreadPoints) do debugMsg("Point " .. i .. ": X: " .. string.format("%.0f", point.x) .. ", Y: " .. string.format("%.0f", point.y) .. ", Z: " .. string.format("%.0f", point.z)) end end local flamePositions = {} --Track flame coordinates to avoid duplicates if splash_damage_options.debug then debugMsg("napalmOnImpact: Using playerName: " .. tostring(playerName) .. " for weapon: " .. weaponName) end local function spawnAndExplode(pairIndex) if pairIndex > spreadPointsCount then return end local pointsToProcess = {} if splash_damage_options.napalm_doublewide_enabled then --Process two points (pair) at indices 2*pairIndex-1 and 2*pairIndex local idx1 = 2 * pairIndex - 1 local idx2 = 2 * pairIndex if idx1 <= #spreadPoints then table.insert(pointsToProcess, spreadPoints[idx1]) end if idx2 <= #spreadPoints then table.insert(pointsToProcess, spreadPoints[idx2]) end else --Process single point at pairIndex if pairIndex <= #spreadPoints then table.insert(pointsToProcess, spreadPoints[pairIndex]) end end for _, point in ipairs(pointsToProcess) do local napalmName = "napalmImpact" .. napalmCounter local currentCounter = napalmCounter napalmCounter = napalmCounter + 1 local owngroupID = math.random(9999, 99999) local cvnunitID = math.random(9999, 99999) local _dataFuel = { ["groupId"] = owngroupID, ["category"] = "Fortifications", ["shape_name"] = "toplivo-bak", ["type"] = "Fuel tank", ["unitId"] = cvnunitID, ["rate"] = 100, ["y"] = point.z, ["x"] = point.x, ["name"] = napalmName, ["heading"] = 0, ["dead"] = false, ["hidden"] = true, } if splash_damage_options.debug then local staticCount = 0 for _, coalitionId in pairs(coalition.side) do local statics = coalition.getStaticObjects(coalitionId) staticCount = staticCount + #statics end debugMsg("Spawning napalm object '" .. napalmName .. "' (Counter: " .. currentCounter .. ") at X: " .. string.format("%.0f", point.x) .. ", Y: " .. string.format("%.0f", point.y) .. ", Z: " .. string.format("%.0f", point.z) .. " (Active static objects: " .. staticCount .. ")") end local status, result = pcall(function() return coalition.addStaticObject(coalition.side.BLUE, _dataFuel) end) local spawnSuccess = status and result and StaticObject.getByName(napalmName) and StaticObject.getByName(napalmName):isExist() if not spawnSuccess then if splash_damage_options.debug then debugMsg("Failed to spawn napalm object '" .. napalmName .. "' at X: " .. string.format("%.0f", point.x) .. ", Y: " .. string.format("%.0f", point.y) .. ", Z: " .. string.format("%.0f", point.z) .. ": " .. (status and "Object not found or does not exist" or tostring(result))) end else timer.scheduleFunction(explodeNapalm, point, timer.getTime() + splash_damage_options.napalm_explode_delay) timer.scheduleFunction(function(name) if splash_damage_options.debug then debugMsg("Destroying napalm object '" .. name .. "' at X: " .. string.format("%.0f", point.x) .. ", Z: " .. string.format("%.0f", point.z)) end removeNapalm(name) end, napalmName, timer.getTime() + splash_damage_options.napalm_destroy_delay) end if splash_damage_options.napalm_phosphor_enabled then timer.scheduleFunction(napalm_phosphor, point, timer.getTime() + splash_damage_options.napalm_explode_delay) local status, err = pcall(function() scanUnitsForNapalm(point.x, point.y, point.z, playerName) -- New: Pass playerName end) if not status then --env.info("napalmOnImpact: Error during unit scan for point (X: " .. point.x .. ", Y: " .. point.y .. ", Z: " .. point.z .. "): " .. tostring(err)) end end --Add flame effect if enabled if splash_damage_options.napalm_addflame then local flameSize = splash_damage_options.napalm_addflame_size local flameDuration = splash_damage_options.napalm_addflame_duration local flameDensity = 1.0 local effectId = effectSmokeId effectSmokeId = effectSmokeId + 1 local isDuplicate = false for _, pos in pairs(flamePositions) do if getDistance3D(point, pos) < 3 then isDuplicate = true if splash_damage_options.debug then debugMsg("Skipping duplicate flame for napalm object '" .. napalmName .. "' near X: " .. string.format("%.0f", pos.x) .. ", Z: " .. string.format("%.0f", pos.z)) end break end end if not isDuplicate then if splash_damage_options.debug then debugMsg("Adding flame effect for napalm object '" .. napalmName .. "' at X: " .. string.format("%.0f", point.x) .. ", Z: " .. string.format("%.0f", point.z) .. " (Size: " .. flameSize .. ", Duration: " .. flameDuration .. "s, ID: " .. effectId .. ")") end timer.scheduleFunction(function(params) local terrainHeight = land.getHeight({x = params[1].x, y = params[1].z}) local adjustedCoords = {x = params[1].x, y = terrainHeight + 2, z = params[1].z} trigger.action.effectSmokeBig(adjustedCoords, params[2], params[3], params[4]) end, {point, flameSize, flameDensity, effectId}, timer.getTime() + splash_damage_options.napalm_flame_delay) timer.scheduleFunction(function(id) if splash_damage_options.debug then debugMsg("Stopping flame effect for napalm object (ID: " .. id .. ")") end trigger.action.effectSmokeStop(id) end, effectId, timer.getTime() + splash_damage_options.napalm_flame_delay + flameDuration) table.insert(flamePositions, point) end end end timer.scheduleFunction(spawnAndExplode, pairIndex + 1, timer.getTime() + 0.2) end spawnAndExplode(1) end local function normalizeVector(vec) local mag = math.sqrt(vec.x^2 + vec.z^2) if mag > 0 then return { x = vec.x / mag, z = vec.z / mag } else return { x = 1, z = 0 } end end local function calculate_drop_angle(velocity) local horizontal_speed = math.sqrt((velocity.x or 0)^2 + (velocity.z or 0)^2) local vertical_speed = math.abs(velocity.y or 0) if horizontal_speed == 0 then return 90 end local angle_rad = math.atan(vertical_speed / horizontal_speed) return math.deg(angle_rad) end local function calculate_dispersion(velocity, burst_altitude) local velocity_magnitude = math.sqrt((velocity.x or 0)^2 + (velocity.z or 0)^2) local drop_angle = calculate_drop_angle(velocity) local length = splash_damage_options.cluster_base_length * (1 + velocity_magnitude / 200) local width = splash_damage_options.cluster_base_width * (1 + burst_altitude / 6000) local length_jitter = length * (0.85 + math.random() * 0.3) local width_jitter = width * (0.85 + math.random() * 0.3) return math.max(splash_damage_options.cluster_min_length, math.min(splash_damage_options.cluster_max_length, length_jitter)), math.max(splash_damage_options.cluster_min_width, math.min(splash_damage_options.cluster_max_width, width_jitter)) end local function protectedCall(...) local status, retval = pcall(...) if not status then env.warning("Splash damage script error... gracefully caught! " .. retval, true) end end --[[ -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=- ##### End of HELPER/UTILITY FUNCTIONS ##### ##### End of HELPER/UTILITY FUNCTIONS ##### ##### End of HELPER/UTILITY FUNCTIONS ##### -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-]] --Function to trigger tactical explosion (like VehicleIEDTrigger) function TacticalExplosionTrigger(coords) if not splash_damage_options.tactical_explosion then if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Disabled, skipping explosion at X: " .. (coords.x or "nil") .. ", Y: " .. (coords.y or "nil") .. ", Z: " .. (coords.z or "nil")) end return end if not coords or not coords.x or not coords.y or not coords.z then if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Invalid coordinates, skipping explosion") end return end --Check height above ground local groundHeight = land.getHeight({x = coords.x, y = coords.z}) local heightAboveGround = coords.y - groundHeight if heightAboveGround > splash_damage_options.tactical_explosion_max_height then if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Explosion at height " .. heightAboveGround .. "m exceeds max height " .. splash_damage_options.tactical_explosion_max_height .. "m, skipping") end return end local scaling = splash_damage_options.tactical_explosion_scaling or 1 if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Processing at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. " with " .. splash_damage_options.tactical_explosion_explosion_count_max .. " max explosions, central power: " .. (splash_damage_options.tactical_explosion_central_power * scaling) .. ", fuel tank spawn: " .. tostring(splash_damage_options.tactical_explosion_fueltankspawn) .. ", scaling: " .. scaling) end --Prepare fuel tank data if spawning is enabled local tacName = "TAC_FuelTank_" .. tostring(timer.getTime()) if splash_damage_options.tactical_explosion_fueltankspawn then table.insert(tacticalFuelTankSpawnQueue, {coords = coords, tacName = tacName}) if #tacticalFuelTankSpawnQueue == 1 then tacticalExplosionProcessSpawnQueue() end end --Generate explosion points local explosionPoints = {} local baseMinCount = splash_damage_options.tactical_explosion_explosion_count_min local baseMaxCount = splash_damage_options.tactical_explosion_explosion_count_max local explosionCount = math.random(math.floor(baseMinCount * scaling), math.floor(baseMaxCount * scaling)) if explosionCount > 0 then --Central explosion local centralPoint = { x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}) + 2, z = coords.z } table.insert(explosionPoints, {point = centralPoint, power = splash_damage_options.tactical_explosion_central_power * scaling, delay = 0.01}) --Secondary explosions with Gaussian distribution for i = 1, explosionCount do local offsetX = gaussRandom(0, (splash_damage_options.tactical_explosion_radius * scaling) / 3) * (1 + (math.random() - 0.5) * 0.1) local offsetZ = gaussRandom(0, (splash_damage_options.tactical_explosion_radius * scaling) / 3) * (1 + (math.random() - 0.5) * 0.1) local point = { x = coords.x + offsetX, y = land.getHeight({x = coords.x + offsetX, y = coords.z + offsetZ}) + 1.3, z = coords.z + offsetZ } local basePower = splash_damage_options.tactical_explosion_explosion_power local power = (basePower * scaling) local delay = math.random() * splash_damage_options.tactical_explosion_explosion_delay_max table.insert(explosionPoints, {point = point, power = power, delay = delay}) end end --Trigger explosions if #explosionPoints > 0 then if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Scheduling " .. #explosionPoints .. " explosions") end for i, entry in ipairs(explosionPoints) do if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Scheduling explosion #" .. i .. " at X: " .. entry.point.x .. ", Y: " .. entry.point.y .. ", Z: " .. entry.point.z .. " with power " .. entry.power .. " and delay " .. entry.delay) end timer.scheduleFunction(function(params) if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Triggering explosion #" .. params[3] .. " at X: " .. params[1].x .. ", Y: " .. params[1].y .. ", Z: " .. params[1].z .. " with power " .. params[2]) end trigger.action.explosion(params[1], params[2]) end, {entry.point, entry.power, i}, timer.getTime() + entry.delay) end end --Trigger blastWave for central explosion local centralPoint = { x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}) + 0.5, z = coords.z } local dynamicRadius = math.pow(splash_damage_options.tactical_explosion_central_power * scaling, 1/3) * 5 * splash_damage_options.dynamic_blast_radius_modifier if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Triggering blastWave at X: " .. centralPoint.x .. ", Y: " .. centralPoint.y .. ", Z: " .. centralPoint.z .. " with power " .. (splash_damage_options.tactical_explosion_central_power * scaling) .. " and dynamic radius " .. dynamicRadius) end timer.scheduleFunction(function(params) blastWave(params[1], params[2], params[3], params[4], params[5]) end, {centralPoint, dynamicRadius, "TacticalExplosion", splash_damage_options.tactical_explosion_central_power * scaling, false}, timer.getTime() + 0.4) end --Function to process the spawn queue for tactical explosion fuel tanks (like vehicleIEDprocessSpawnQueue) function tacticalExplosionProcessSpawnQueue() if #tacticalFuelTankSpawnQueue == 0 then return end local currentTime = timer.getTime() if currentTime < lastSpawnTime + SPAWN_INTERVAL then timer.scheduleFunction(tacticalExplosionProcessSpawnQueue, {}, currentTime + SPAWN_INTERVAL / 2) return end local task = table.remove(tacticalFuelTankSpawnQueue, 1) lastSpawnTime = currentTime local coords, tacName = task.coords, task.tacName if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Spawning fuel tank at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) end local owngroupID = math.random(9999, 99999) local cvnunitID = math.random(9999, 99999) local _dataFuel = { ["groupId"] = owngroupID, ["category"] = "Fortifications", ["shape_name"] = "toplivo-bak", ["type"] = "Fuel tank", ["unitId"] = cvnunitID, ["rate"] = 100, ["y"] = coords.z, ["x"] = coords.x, ["name"] = tacName, ["heading"] = 0, ["dead"] = false, ["hidden"] = true, } --Attempt to spawn at original coordinates _dataFuel.y = coords.z _dataFuel.x = coords.x local spawnY = land.getHeight({x = coords.x, y = coords.z}) + 0.5 _dataFuel.position = {x = coords.x, y = spawnY, z = coords.z} local status, result = pcall(function() return coalition.addStaticObject(coalition.side.BLUE, _dataFuel) end) local spawnSuccess = status and result and StaticObject.getByName(tacName) and StaticObject.getByName(tacName):isExist() if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Fuel tank spawn attempt at original coords - " .. (spawnSuccess and "succeeded" or "failed")) end --Try offset positions if spawn fails if not spawnSuccess then if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Failed to spawn fuel tank at original coords, attempting 1m offsets") end local offsets = { {x = coords.x + 1, z = coords.z}, {x = coords.x - 1, z = coords.z}, {x = coords.x, z = coords.z + 1}, {x = coords.x, z = coords.z - 1} } for i, offset in ipairs(offsets) do _dataFuel.x = offset.x _dataFuel.y = offset.z _dataFuel.position = {x = offset.x, y = land.getHeight({x = offset.x, y = offset.z}) + 0.5, z = offset.z} _dataFuel.name = tacName .. "_offset" .. i status, result = pcall(function() return coalition.addStaticObject(coalition.side.BLUE, _dataFuel) end) spawnSuccess = status and result and StaticObject.getByName(_dataFuel.name) and StaticObject.getByName(_dataFuel.name):isExist() if spawnSuccess then coords.x = offset.x coords.z = offset.z tacName = _dataFuel.name if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Successfully spawned fuel tank at offset #" .. i .. " (X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. ")") end break end end end if not spawnSuccess and splash_damage_options.debug then env.info("TacticalExplosionTrigger: Failed to spawn fuel tank after all attempts") end --Schedule destruction if spawnSuccess then timer.scheduleFunction(function(name) if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Attempting to destroy fuel tank " .. name) end local staticObj = StaticObject.getByName(name) if staticObj then local status, err = pcall(function() staticObj:destroy() end) if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Fuel tank " .. name .. " destruction - " .. (status and "succeeded" or "failed: " .. tostring(err))) end else if splash_damage_options.debug then env.info("TacticalExplosionTrigger: Fuel tank " .. name .. " not found for destruction") end end end, tacName, timer.getTime() + 0.5) end --Schedule next spawn if queue is not empty if #tacticalFuelTankSpawnQueue > 0 then timer.scheduleFunction(tacticalExplosionProcessSpawnQueue, {}, timer.getTime() + SPAWN_INTERVAL) end end --Function to trigger smoke effect with specified size and duration local function triggerSmokeEffect(coords, flameSize, duration, effectId) local terrainHeight = land.getHeight({x = coords.x, y = coords.z}) local adjustedCoords = {x = coords.x, y = terrainHeight + 2, z = coords.z} debugCargoCookOff("Spawning smoke effect at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z .. " with size " .. flameSize .. " (ID: " .. effectId .. ")") trigger.action.effectSmokeBig(adjustedCoords, flameSize, 1, effectId) timer.scheduleFunction(function(id) debugCargoCookOff("Stopping smoke effect (ID: " .. id .. ")") trigger.action.effectSmokeStop(id) end, effectId, timer.getTime() + duration) end --Schedule advanced sequence cargo effects local function scheduleAdvancedEffectSequence(unitID, coords, effectData, fromDeadEvent) local function triggerEffects(pos) processedSmoke[unitID] = true --Ensure unit is marked as processed local effectOrder = splash_damage_options.allunits_advanced_effect_order local effectTiming = splash_damage_options.allunits_advanced_effect_timing local cumulativeTime = 0 effectSmokeId = effectSmokeId or 1 --Use global effectSmokeId, initialize if nil --Helper function to get current unit position local function getUnitPosition() local entry = CargoCookoffPendingTable[unitID] if entry and entry.unit then local success, newPos = pcall(function() return entry.unit:getPosition().p end) if success and newPos then entry.coords = newPos return newPos end end return pos end --Trigger initial explosion at 1.6m off the ground timer.scheduleFunction(function(params) local currentPos = getUnitPosition() local explosionCoords = {x = currentPos.x, y = land.getHeight({x = currentPos.x, y = currentPos.z}) + 1.6, z = currentPos.z} debugCargoCookOff("Executing initial explosion for unit ID " .. tostring(params.unitID or "nil") .. " at X: " .. explosionCoords.x .. ", Y: " .. explosionCoords.y .. ", Z: " .. explosionCoords.z) trigger.action.explosion(explosionCoords, params.power) end, {power = splash_damage_options.allunits_advanced_effect_explode_power, unitID = unitID}, timer.getTime() + 0.1) --Spawn first smoke effect immediately if #effectOrder > 0 then local flameSize = tonumber(effectOrder[1]) local duration = tonumber(effectTiming[1]) or 99999999 local effectId = effectSmokeId effectSmokeId = effectSmokeId + 1 local smokeCoords = {x = pos.x, y = land.getHeight({x = pos.x, y = pos.z}) + 2, z = pos.z} debugCargoCookOff("Spawning immediate smoke effect for unit ID " .. tostring(unitID) .. " at X: " .. smokeCoords.x .. ", Y: " .. smokeCoords.y .. ", Z: " .. smokeCoords.z .. " with size " .. flameSize .. " (ID: " .. effectId .. ")") trigger.action.effectSmokeBig(smokeCoords, flameSize, 0.9, effectId) timer.scheduleFunction(function(id) debugCargoCookOff("Stopping smoke effect (ID: " .. id .. ")") trigger.action.effectSmokeStop(id) end, effectId, timer.getTime() + duration) cumulativeTime = cumulativeTime + duration end --Schedule remaining smoke effects for i = 2, #effectOrder do local duration = tonumber(effectTiming[i]) or 99999999 local effectId = effectSmokeId effectSmokeId = effectSmokeId + 1 timer.scheduleFunction(function(params) local currentPos = getUnitPosition() triggerSmokeEffect({x = currentPos.x, y = land.getHeight({x = currentPos.x, y = currentPos.z}) + 2, z = currentPos.z}, params.flameSize, params.duration, params.effectId) end, {flameSize = tonumber(effectOrder[i]), duration = duration, effectId = effectId}, timer.getTime() + cumulativeTime) cumulativeTime = cumulativeTime + duration end if effectData.cookOff and effectData.cookOffCount > 0 then if splash_damage_options.allunits_advanced_effect_cookoff_flares_enabled then timer.scheduleFunction(function(params) local currentPos = getUnitPosition() local flareCoords = {x = currentPos.x, y = land.getHeight({x = currentPos.x, y = currentPos.z}) + 1, z = currentPos.z} debugCargoCookOff("Executing flares for unit ID " .. tostring(params[1].unitID or "nil") .. " at X: " .. flareCoords.x .. ", Y: " .. flareCoords.y .. ", Z: " .. flareCoords.z) scheduleCookOffFlares(flareCoords, params[1].cookOffCount, params[1].cookOffDuration, params[2]) end, {effectData, splash_damage_options.cookoff_flare_color}, timer.getTime() + 0.2) end for i = 1, effectData.cookOffCount do local delay = effectData.cookOffRandomTiming and math.random() * effectData.cookOffDuration or (i - 1) * (effectData.cookOffDuration / effectData.cookOffCount) local basePower = effectData.cookOffPower local powerVariation = effectData.cookOffPowerRandom / 100 local cookOffPower = effectData.cookOffPowerRandom == 0 and basePower or basePower * (1 + powerVariation * (math.random() * 2 - 1)) timer.scheduleFunction(function(params) local currentPos = getUnitPosition() local cookOffCoords = {x = currentPos.x, y = land.getHeight({x = currentPos.x, y = currentPos.z}) + 1, z = currentPos.z} debugCargoCookOff("Executing cookoff explosion #" .. params[3] .. " for unit ID " .. tostring(params[1].unitID or "nil") .. " at X: " .. cookOffCoords.x .. ", Y: " .. cookOffCoords.y .. ", Z: " .. cookOffCoords.z) trigger.action.explosion(cookOffCoords, params[2]) end, {effectData, cookOffPower, i}, timer.getTime() + delay) end if splash_damage_options.debris_effects then local debrisCount = math.random(splash_damage_options.debris_count_min, splash_damage_options.debris_count_max) for j = 1, debrisCount do local theta = math.random() * 2 * math.pi local phi = math.acos(math.random() * 2 - 1) local minDist = splash_damage_options.debris_max_distance * 0.1 local maxDist = splash_damage_options.debris_max_distance local r = math.random() * (maxDist - minDist) + minDist local debrisDelay = (j - 1) * (effectData.cookOffDuration / debrisCount) timer.scheduleFunction(function(debrisArgs) local currentPos = getUnitPosition() local debrisBaseCoords = {x = currentPos.x, y = land.getHeight({x = currentPos.x, y = currentPos.z}) + 1, z = currentPos.z} debugCargoCookOff("Executing debris explosion #" .. debrisArgs[3] .. " for unit ID " .. tostring(debrisArgs[1].unitID or "nil") .. " at X: " .. debrisBaseCoords.x .. ", Y: " .. debrisBaseCoords.y .. ", Z: " .. debrisBaseCoords.z) local debrisX = debrisBaseCoords.x + r * math.sin(phi) * math.cos(theta) local debrisZ = debrisBaseCoords.z + r * math.sin(phi) * math.sin(theta) local terrainY = land.getHeight({x = debrisX, y = debrisZ}) local debrisY = terrainY + math.random() * maxDist local debrisPos = {x = debrisX, y = debrisY, z = debrisZ} trigger.action.explosion(debrisPos, debrisArgs[2]) end, {effectData, splash_damage_options.debris_power, j}, timer.getTime() + debrisDelay) end end end end local function checkMovement(params) local entry = CargoCookoffPendingTable[params.unitID] if not entry then debugCargoCookOff("Stopped tracking movement for unit ID " .. tostring(params.unitID) .. ": no entry in CargoCookoffPendingTable") --Create fallback entry and trigger effects CargoCookoffPendingTable[params.unitID] = { coords = coords, prevCoords = coords, unit = nil } triggerEffects(coords) return end if processedSmoke[params.unitID] then debugCargoCookOff("Stopped tracking movement for unit ID " .. tostring(params.unitID) .. ": smoke already processed") return end local newPos = entry.coords if entry.unit then local success, pos = pcall(function() return entry.unit:getPosition().p end) if success and pos then newPos = pos debugCargoCookOff("Updated position for unit ID " .. tostring(params.unitID) .. " to X: " .. pos.x .. ", Z: " .. pos.z) else debugCargoCookOff("Failed to get position for unit ID " .. tostring(params.unitID) .. ", using last known coords X: " .. newPos.x .. ", Z: " .. newPos.z) end else debugCargoCookOff("Unit ID " .. tostring(params.unitID) .. " is gone, using last known coords X: " .. newPos.x .. ", Z: " .. newPos.z) end local hasStopped = math.abs(newPos.x - entry.prevCoords.x) < 0.1 and math.abs(newPos.y - entry.prevCoords.y) < 0.1 and math.abs(newPos.z - entry.prevCoords.z) < 0.1 debugCargoCookOff("Checking movement for unit ID " .. tostring(params.unitID) .. ": stopped=" .. tostring(hasStopped) .. ", newPos X=" .. newPos.x .. ", Z=" .. newPos.z) if hasStopped or fromDeadEvent then entry.coords = newPos triggerEffects(newPos) return end entry.prevCoords = newPos entry.coords = newPos timer.scheduleFunction(checkMovement, params, timer.getTime() + 0.1) end --Ensure table entry exists before scheduling movement check local entry = CargoCookoffPendingTable[unitID] if not entry then CargoCookoffPendingTable[unitID] = { coords = coords, prevCoords = coords, unit = Unit.getByName(effectData.name) -- Attempt to get unit, may be nil for DEAD events } entry = CargoCookoffPendingTable[unitID] end entry.prevCoords = coords timer.scheduleFunction(checkMovement, {unitID = unitID}, timer.getTime() + 0.1) end --Schedule cargo effects local function scheduleCargoEffects(unitType, unitName, unitID, effectIndex, fromDeadEvent) if not unitID then debugCargoCookOff("scheduleCargoEffects: Skipping call with nil unitID") return end debugCargoCookOff("scheduleCargoEffects called for unit ID " .. tostring(unitID) .. ", unitType: " .. unitType .. ", fromDeadEvent: " .. tostring(fromDeadEvent)) local cargoData = cargoUnits[unitType] or { cargoExplosionPower = splash_damage_options.allunits_explode_power, cargoExplosion = true, cookOff = splash_damage_options.allunits_enable_cookoff, cookOffCount = splash_damage_options.allunits_cookoff_count, cookOffPower = splash_damage_options.allunits_cookoff_power, cookOffDuration = splash_damage_options.allunits_cookoff_duration, cookOffRandomTiming = true, cookOffPowerRandom = splash_damage_options.allunits_cookoff_powerrandom, isTanker = splash_damage_options.allunits_enable_smoke, flameSize = splash_damage_options.allunits_default_flame_size, flameDuration = splash_damage_options.allunits_default_flame_duration } cargoData.cookOff = cargoUnits[unitType] and cargoUnits[unitType].cargoCookOff ~= nil and cargoUnits[unitType].cargoCookOff or cargoData.cookOff debugCargoCookOff("Using cargoData for unitType " .. unitType .. ": cookOff=" .. tostring(cargoData.cookOff)) local isAllUnitsVehicle = not cargoUnits[unitType] and splash_damage_options.smokeandcookoffeffectallvehicles local useAdvancedSequence = false if isAllUnitsVehicle and splash_damage_options.allunits_advanced_effect_sequence then if splash_damage_options.allunits_advanced_effect_force_on_name and unitName:find("AdvSeq") then useAdvancedSequence = true debugCargoCookOff("Forcing advanced effect sequence for unit ID " .. tostring(unitID) .. " due to AdvSeq in name") elseif math.random() <= splash_damage_options.allunits_advanced_effect_sequence_chance then useAdvancedSequence = true debugCargoCookOff("Selected advanced effect sequence for unit ID " .. tostring(unitID) .. " based on chance") else debugCargoCookOff("Using standard sequence for unit ID " .. tostring(unitID) .. ", name: " .. tostring(unitName) .. ", chance: " .. tostring(splash_damage_options.allunits_advanced_effect_sequence_chance)) end end local effect = { name = unitType, distance = 0, coords = {x = 0, y = 0, z = 0}, power = cargoData.cargoExplosionPower or splash_damage_options.allunits_explode_power, explosion = cargoData.cargoExplosion, cookOff = cargoData.cookOff, -- Initially set, may be modified cookOffCount = useAdvancedSequence and splash_damage_options.allunits_advanced_effect_cookoff_count or cargoData.cookOffCount, cookOffPower = useAdvancedSequence and splash_damage_options.allunits_advanced_effect_cookoff_power or cargoData.cookOffPower, cookOffDuration = useAdvancedSequence and splash_damage_options.allunits_advanced_effect_cookoff_duration or cargoData.cookOffDuration, cookOffRandomTiming = true, cookOffPowerRandom = useAdvancedSequence and splash_damage_options.allunits_advanced_effect_cookoff_powerrandom or cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, --Initially set, may be modified flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration, unitID = unitID } local entry = CargoCookoffPendingTable[unitID] if entry then effect.coords = entry.coords debugCargoCookOff("Using coords from CargoCookoffPendingTable for unit ID " .. tostring(unitID) .. ": X=" .. effect.coords.x .. ", Z=" .. effect.coords.z) end if useAdvancedSequence then --Apply advanced sequence cook-off chance effect.cookOff = math.random() <= splash_damage_options.allunits_advanced_effect_cookoff_chance debugCargoCookOff("Scheduling advanced effect sequence for unit ID " .. tostring(unitID)) scheduleAdvancedEffectSequence(unitID, effect.coords, effect, fromDeadEvent) return end --Standard sequence: Apply cook-off and smoke chances if isAllUnitsVehicle then local cookoffChance = splash_damage_options.allunits_cookoff_chance or 1 if splash_damage_options.allunits_enable_cookoff and math.random() <= cookoffChance then debugCargoCookOff("scheduleCargoEffects: Triggering cook-off effects for all-units unit ID " .. tostring(unitID) .. " with allunits_cookoff_chance (" .. cookoffChance .. ")") effect.cookOff = true effect.isTanker = splash_damage_options.allunits_enable_smoke and splash_damage_options.allunits_smokewithcookoff else debugCargoCookOff("scheduleCargoEffects: Skipped cook-off effects for all-units unit ID " .. tostring(unitID) .. " due to allunits_cookoff_chance (" .. cookoffChance .. ")") effect.cookOff = false effect.isTanker = splash_damage_options.allunits_enable_smoke and math.random() <= splash_damage_options.allunits_smoke_chance if not effect.isTanker and not effect.cookOff then debugCargoCookOff("scheduleCargoEffects: Skipped smoke effects for unit ID " .. tostring(unitID) .. " due to allunits_smoke_chance (" .. splash_damage_options.allunits_smoke_chance .. ")") return end end end table.insert(cargoEffectsQueue, effect) debugCargoCookOff("Queued effects for unit ID " .. tostring(unitID) .. " at X: " .. effect.coords.x .. ", Z: " .. effect.coords.z) local processedCargoUnits = {} local flamePositions = {} for _, eff in ipairs(cargoEffectsQueue) do local unitKey = eff.name .. "_" .. eff.coords.x .. "_" .. eff.coords.z if not processedCargoUnits[unitKey] then local function getUnitPosition(params) local id = params.unitID or (params[1] and params[1].unitID) local pos = params.coords or (params[1] and params[1].coords) if not id then debugCargoCookOff("Error: No unitID provided in getUnitPosition") return pos or {x = 0, y = land.getHeight({x = 0, y = 0}) + 1, z = 0} end local entry = CargoCookoffPendingTable[id] if entry then if entry.unit then local success, newPos = pcall(function() return entry.unit:getPosition().p end) if success and newPos then entry.coords = newPos pos = newPos debugCargoCookOff("Updated position for unit ID " .. id .. " to X: " .. pos.x .. ", Z: " .. pos.z) else debugCargoCookOff("Failed to get position for unit ID " .. id .. ", using last known coords X: " .. pos.x .. ", Z: " .. pos.z) end else debugCargoCookOff("No unit for unit ID " .. id .. ", using last known coords X: " .. pos.x .. ", Z: " .. pos.z) end else debugCargoCookOff("No entry for unit ID " .. id .. ", using coords X: " .. pos.x .. ", Z: " .. pos.z) end --Always adjust y-coordinate to ground level pos.y = land.getHeight({x = pos.x, y = pos.z}) + 1.6 debugCargoCookOff("Adjusted position for unit ID " .. id .. " to Y: " .. pos.y) return pos end if eff.explosion then timer.scheduleFunction(function(params) local coords = getUnitPosition(params) debugCargoCookOff("Executing explosion for unit ID " .. tostring(params.unitID or "nil") .. " at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) trigger.action.explosion(coords, params.power) end, eff, timer.getTime() + effectIndex + 0.1) end if eff.cookOff and eff.cookOffCount > 0 then timer.scheduleFunction(function(params) local coords = getUnitPosition(params[1]) debugCargoCookOff("Executing flares for unit ID " .. tostring(params[1].unitID or "nil") .. " at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) scheduleCookOffFlares(coords, params[1].cookOffCount, params[1].cookOffDuration, params[2]) end, {eff, splash_damage_options.cookoff_flare_color}, timer.getTime() + 0.2) for i = 1, eff.cookOffCount do local delay = eff.cookOffRandomTiming and math.random() * eff.cookOffDuration or (i - 1) * (eff.cookOffDuration / eff.cookOffCount) local basePower = eff.cookOffPower local powerVariation = eff.cookOffPowerRandom / 100 local cookOffPower = eff.cookOffPowerRandom == 0 and basePower or basePower * (1 + powerVariation * (math.random() * 2 - 1)) timer.scheduleFunction(function(params) local coords = getUnitPosition(params[1]) debugCargoCookOff("Executing cookoff explosion #" .. params[3] .. " for unit ID " .. tostring(params[1].unitID or "nil") .. " at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) trigger.action.explosion(coords, params[2]) end, {eff, cookOffPower, i}, timer.getTime() + effectIndex + delay) end if splash_damage_options.debris_effects then local debrisCount = math.random(splash_damage_options.debris_count_min, splash_damage_options.debris_count_max) for j = 1, debrisCount do local theta = math.random() * 2 * math.pi local phi = math.acos(math.random() * 2 - 1) local minDist = splash_damage_options.debris_max_distance * 0.1 local maxDist = splash_damage_options.debris_max_distance local r = math.random() * (maxDist - minDist) + minDist local debrisDelay = (j - 1) * (eff.cookOffDuration / debrisCount) timer.scheduleFunction(function(debrisArgs) local coords = getUnitPosition(debrisArgs[1]) debugCargoCookOff("Executing debris explosion #" .. debrisArgs[3] .. " for unit ID " .. tostring(debrisArgs[1].unitID or "nil") .. " at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) local debrisX = coords.x + r * math.sin(phi) * math.cos(theta) local debrisZ = coords.z + r * math.sin(phi) * math.sin(theta) local terrainY = land.getHeight({x = debrisX, y = debrisZ}) local debrisY = terrainY + math.random() * maxDist local debrisPos = {x = debrisX, y = debrisY, z = debrisZ} trigger.action.explosion(debrisPos, debrisArgs[2]) end, {eff, splash_damage_options.debris_power, j}, timer.getTime() + effectIndex + debrisDelay) end end end processedCargoUnits[unitKey] = true effectIndex = effectIndex + 3 end end cargoEffectsQueue = {} --Handle smoke spawning for non-advanced sequence if effect.isTanker and entry and not processedSmoke[unitID] then local terrainHeight = land.getHeight({x = effect.coords.x, y = effect.coords.z}) local adjustedCoords = {x = effect.coords.x, y = terrainHeight + 2, z = effect.coords.z} if fromDeadEvent then --For DEAD events, spawn smoke immediately without movement tracking processedSmoke[unitID] = true debugCargoCookOff("Spawning immediate smoke for unit ID " .. tostring(unitID) .. " from DEAD event at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z) if not effect.cookOff and splash_damage_options.allunits_explode_on_smoke_only then debugCargoCookOff("Triggering explosion for smoke-only unit ID " .. tostring(unitID) .. " at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z) trigger.action.explosion(adjustedCoords, splash_damage_options.allunits_explode_power) end debugCargoCookOff("Triggered additional explosion for unit ID " .. tostring(unitID) .. " at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z) trigger.action.effectSmokeBig(adjustedCoords, effect.flameSize or 3, 1.0, effectSmokeId) effectSmokeId = effectSmokeId + 1 timer.scheduleFunction(function(id) trigger.action.effectSmokeStop(id) end, effectSmokeId - 1, timer.getTime() + (effect.flameDuration or splash_damage_options.allunits_default_flame_duration)) else --For HIT/KILL events, track movement until stationary local function checkMovement(params) local entry = CargoCookoffPendingTable[params.unitID] if not entry then debugCargoCookOff("Stopped tracking movement for unit ID " .. tostring(params.unitID) .. ": no entry in CargoCookoffPendingTable") return end if processedSmoke[params.unitID] then debugCargoCookOff("Stopped tracking movement for unit ID " .. tostring(params.unitID) .. ": smoke already processed") return end local newPos = entry.coords if entry.unit then local success, pos = pcall(function() return entry.unit:getPosition().p end) if success and pos then newPos = pos debugCargoCookOff("Updated position for unit ID " .. tostring(params.unitID) .. " to X: " .. pos.x .. ", Z: " .. pos.z) else debugCargoCookOff("Failed to get position for unit ID " .. tostring(params.unitID) .. ", using last known coords X: " .. newPos.x .. ", Z: " .. newPos.z) end else debugCargoCookOff("Unit ID " .. tostring(params.unitID) .. " is gone, using last known coords X: " .. newPos.x .. ", Z: " .. newPos.z) end local hasStopped = math.abs(newPos.x - entry.prevCoords.x) < 0.1 and math.abs(newPos.y - entry.prevCoords.y) < 0.1 and math.abs(newPos.z - entry.prevCoords.z) < 0.1 debugCargoCookOff("Checking movement for unit ID " .. tostring(params.unitID) .. ": stopped=" .. tostring(hasStopped) .. ", newPos X=" .. newPos.x .. ", Z=" .. newPos.z) if hasStopped then processedSmoke[params.unitID] = true entry.coords = newPos local terrainHeight = land.getHeight({x = newPos.x, y = newPos.z}) local adjustedCoords = {x = newPos.x, y = terrainHeight + 2, z = newPos.z} debugCargoCookOff("Spawning smoke for unit ID " .. tostring(params.unitID) .. " at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z) if not params.cookOff and splash_damage_options.allunits_explode_on_smoke_only then debugCargoCookOff("Triggering explosion for smoke-only unit ID " .. tostring(params.unitID) .. " at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z) trigger.action.explosion(adjustedCoords, splash_damage_options.allunits_explode_power) end debugCargoCookOff("Triggered additional explosion for unit ID " .. tostring(params.unitID) .. " at X: " .. adjustedCoords.x .. ", Z: " .. adjustedCoords.z) trigger.action.effectSmokeBig(adjustedCoords, params.flameSize or 3, 1.0, effectSmokeId) effectSmokeId = effectSmokeId + 1 timer.scheduleFunction(function(id) trigger.action.effectSmokeStop(id) end, effectSmokeId - 1, timer.getTime() + (params.flameDuration or splash_damage_options.allunits_default_flame_duration)) return end entry.prevCoords = newPos entry.coords = newPos timer.scheduleFunction(checkMovement, params, timer.getTime() + 0.3) end timer.scheduleFunction(checkMovement, effect, timer.getTime() + 0.3) end end end --Function to check if a weapon is in the Trophy APS target list local function isTrophyWeapon(weaponName) debugTrophy("Checking weapon: " .. tostring(weaponName)) if not weaponName then debugTrophy("Weapon name is nil") return false end local weaponNameLower = string.lower(weaponName) if trophyWeaponsLookup[weaponNameLower] then debugTrophy("Weapon " .. weaponNameLower .. " is a Trophy target") return true end debugTrophy("Weapon " .. weaponNameLower .. " is not a Trophy target") return false end --Function to find TrophyAPS vehicles within weapon's max range local function findTrophyVehicles(weaponPos, weaponName) debugTrophy("Finding TrophyAPS vehicles for " .. tostring(weaponName)) if not weaponPos then debugTrophy("Weapon position is nil") return {} end local trophyUnits = {} local unitIds = {} --Track unique unit IDs local weaponNameLower = string.lower(weaponName) local searchRadius = trophyWeaponsLookup[weaponNameLower] and trophyWeaponsLookup[weaponNameLower].range or 16093 --Default to 10 miles if no range debugTrophy("Search radius: " .. searchRadius .. " meters") local function searchUnit(unit) if unit then local success, errorMsg = pcall(function() if unit:isExist() and unit:getLife() > 1 then local unitType = unit:getTypeName() local unitName = unit:getName() if TrophyAllUnitType[unitType] or string.find(unitName, "TrophyAPS") then local unitId = unit:getID() --Check ammo status if not trophyAmmo[unitId] then trophyAmmo[unitId] = { FR = splash_damage_options.trophy_frontRightRounds, BL = splash_damage_options.trophy_backLeftRounds } end if trophyAmmo[unitId].FR + trophyAmmo[unitId].BL > 0 then if not unitIds[unitId] then local unitPos = unit:getPosition().p if unitPos then local distance = math.sqrt((unitPos.x - weaponPos.x)^2 + (unitPos.z - weaponPos.z)^2) if distance <= searchRadius then table.insert(trophyUnits, unit) unitIds[unitId] = true debugTrophy("Found unit " .. unitName .. " (ID: " .. unitId .. ", Type: " .. unitType .. ") with FR: " .. trophyAmmo[unitId].FR .. ", BL: " .. trophyAmmo[unitId].BL) end else debugTrophy("Failed to get position for unit " .. unitName) end end else debugTrophy("Unit " .. unitName .. " (ID: " .. unitId .. ") has no remaining Trophy rounds") end end end end) if not success then debugTrophy("Error processing unit: " .. tostring(errorMsg)) end end end local volume = { id = world.VolumeType.SPHERE, params = { point = { x = weaponPos.x, y = weaponPos.y, z = weaponPos.z }, radius = searchRadius } } local success, errorMsg = pcall(function() world.searchObjects(Object.Category.UNIT, volume, searchUnit) end) if not success then debugTrophy("Error in world.searchObjects: " .. tostring(errorMsg)) end debugTrophy("Found " .. #trophyUnits .. " TrophyAPS vehicles within " .. searchRadius .. " meters") return trophyUnits end --Function to get compass direction from bearing local function getCompassDirection(bearing) local directions = {"NORTH", "NORTHEAST", "EAST", "SOUTHEAST", "SOUTH", "SOUTHWEST", "WEST", "NORTHWEST"} local index = math.floor((bearing + 22.5) / 45) % 8 + 1 return directions[index] end --Function to report shooter position with map marker, line, text, and message local function reportShooterPosition(shooterUnit, targetUnit, weaponName) if shooterUnit and shooterUnit:isExist() then local shooterPos, targetPos local success, errorMsg = pcall(function() shooterPos = shooterUnit:getPosition().p targetPos = targetUnit:getPosition().p end) if success and shooterPos and targetPos then local shooterName = shooterUnit:getName() or "Unknown" local targetName = targetUnit:getName() or "Unknown" local targetCoalition = targetUnit:getCoalition() local weaponData = trophyWeaponsLookup[string.lower(weaponName)] local weaponDisplayName = weaponData and weaponData.name or weaponName --Calculate bearing for compass direction local bearing = math.atan2(shooterPos.z - targetPos.z, shooterPos.x - targetPos.x) * 180 / math.pi if bearing < 0 then bearing = bearing + 360 end local compassDir = getCompassDirection(bearing) --Calculate distance local distance = math.sqrt((targetPos.x - shooterPos.x)^2 + (targetPos.z - shooterPos.z)^2) --Format message local originStatus = distance <= splash_damage_options.trophy_maxMapMarkerDistance and "ORIGIN MARKED." or "ORIGIN NOT DETECTED." local msg = string.format("%s: THREAT INTERCEPTION: %s %s BEARING %.0f. %s", targetName, weaponDisplayName, compassDir, bearing, originStatus) if splash_damage_options.trophy_showInterceptionMessage then trigger.action.outTextForCoalition(targetCoalition, msg, splash_damage_options.trophy_messageDuration) debugTrophy(msg) else debugTrophy("Interception message disabled: " .. msg) end --Add map marker if within configured distance local markerId = timer.getTime() .. math.random(1000, 9999) if distance <= splash_damage_options.trophy_maxMapMarkerDistance and splash_damage_options.trophy_markShooterOrigin then debugTrophy("Attempting to create map marker for shooter: " .. shooterName .. " at x=" .. shooterPos.x .. ", z=" .. shooterPos.z .. ", coalition=" .. tostring(targetCoalition)) if not shooterPos.x or not shooterPos.z then debugTrophy("Invalid shooter position for marker: x=" .. tostring(shooterPos.x) .. ", z=" .. tostring(shooterPos.z)) elseif not targetCoalition then debugTrophy("Invalid target coalition for marker: " .. tostring(targetCoalition)) else local markerSuccess, markerError = pcall(function() trigger.action.markToCoalition(markerId, "Enemy shooter detected: " .. shooterName, shooterPos, targetCoalition, true) end) if markerSuccess then debugTrophy("Map marker created with ID: " .. markerId) --Schedule marker removal timer.scheduleFunction(function(id) trigger.action.removeMark(id) end, markerId, timer.getTime() + splash_damage_options.trophy_markerDuration) else debugTrophy("Failed to create map marker: " .. tostring(markerError)) end end else debugTrophy("Map marker not created: distance=" .. distance .. " (max=" .. splash_damage_options.trophy_maxMapMarkerDistance .. "), markShooterOrigin=" .. tostring(splash_damage_options.trophy_markShooterOrigin)) end --Draw line if enabled if splash_damage_options.trophy_drawOriginLine then local lineId = timer.getTime() .. math.random(1000, 9999) debugTrophy("Calculating line from target x=" .. tostring(targetPos.x) .. ", z=" .. tostring(targetPos.z) .. " to shooter x=" .. tostring(shooterPos.x) .. ", z=" .. tostring(shooterPos.z)) if targetPos.x and targetPos.z and shooterPos.x and shooterPos.z then local startPos = {x = targetPos.x, y = 0, z = targetPos.z} local endPos --Calculate direction vector to shooter local dirX = shooterPos.x - targetPos.x local dirZ = shooterPos.z - targetPos.z local mag = math.sqrt(dirX^2 + dirZ^2) if mag > 0 then dirX, dirZ = dirX / mag, dirZ / mag --Limit line length to maxMapMarkerDistance local lineLength = math.min(distance, splash_damage_options.trophy_maxMapMarkerDistance) endPos = {x = targetPos.x + dirX * lineLength, y = 0, z = targetPos.z + dirZ * lineLength} local lineStyle = distance <= splash_damage_options.trophy_maxMapMarkerDistance and 1 or 2 --Solid if within range, dotted if beyond debugTrophy("Drawing line to x=" .. endPos.x .. ", z=" .. endPos.z .. ", length=" .. lineLength .. ", style=" .. lineStyle) local lineSuccess, lineError = pcall(function() trigger.action.lineToAll(-1, lineId + 1, startPos, endPos, {1, 0, 0, 0.5}, lineStyle, true, "TROPHY THREAT LINE") end) if lineSuccess then debugTrophy("Line drawn with ID: " .. (lineId + 1)) --Schedule line removal timer.scheduleFunction(function(id) trigger.action.removeMark(id) end, lineId + 1, timer.getTime() + splash_damage_options.trophy_markerDuration) else debugTrophy("Failed to draw line: " .. tostring(lineError)) end else debugTrophy("Invalid direction vector magnitude for line") end else debugTrophy("Invalid coordinates for line draw: target x=" .. tostring(targetPos.x) .. ", z=" .. tostring(targetPos.z) .. ", shooter x=" .. tostring(shooterPos.x) .. ", z=" .. tostring(shooterPos.z)) end end else debugTrophy("Failed to get shooter or target position: " .. tostring(errorMsg)) end else debugTrophy("Shooter unit no longer exists or is invalid") end end --Function to check if weapon is heading toward a unit local function isWeaponHeadingToward(weapon, unit, callback) local sampleCount = 3 local sampleInterval = 0.05 local initialDelay = 0.05 local samples = {} local function collectSample(count) if count > sampleCount then --Process samples local success, result = pcall(function() if #samples < 2 then debugTrophy("Insufficient samples collected: " .. #samples) callback(false) return end --Calculate displacement between first and last sample local firstPos = samples[1].pos local lastPos = samples[#samples].pos local dispX = lastPos.x - firstPos.x local dispZ = lastPos.z - firstPos.z local magDisp = math.sqrt(dispX^2 + dispZ^2) debugTrophy("Displacement: dx=" .. dispX .. ", dz=" .. dispZ .. ", mag=" .. magDisp) --Try velocity if displacement is too small local avgVelX, avgVelZ = 0, 0 local validVel = false if magDisp < 0.1 then for _, sample in ipairs(samples) do local magVel = math.sqrt(sample.vel.x^2 + sample.vel.z^2) if magVel >= 1 then avgVelX = avgVelX + sample.vel.x / magVel avgVelZ = avgVelZ + sample.vel.z / magVel validVel = true end end magDisp = math.sqrt(avgVelX^2 + avgVelZ^2) if validVel and magDisp >= 0.0001 then dispX = avgVelX / magDisp dispZ = avgVelZ / magDisp debugTrophy("Using average velocity: x=" .. dispX .. ", z=" .. dispZ) else debugTrophy("No valid displacement or velocity") callback(false) return end else dispX = dispX / magDisp dispZ = dispZ / magDisp debugTrophy("Normalized displacement: x=" .. dispX .. ", z=" .. dispZ) end --Vector from last weapon position to unit local unitPos = unit:getPosition().p local toUnitX = unitPos.x - lastPos.x local toUnitZ = unitPos.z - lastPos.z local magToUnit = math.sqrt(toUnitX^2 + toUnitZ^2) if magToUnit < 0.0001 then debugTrophy("Weapon too close to unit, magToUnit is zero") callback(false) return end toUnitX = toUnitX / magToUnit toUnitZ = toUnitZ / magToUnit debugTrophy("To-unit vector: x=" .. toUnitX .. ", z=" .. toUnitZ) --Dot product to check alignment local dot = toUnitX * dispX + toUnitZ * dispZ local angle = math.acos(math.max(-1, math.min(1, dot))) * 180 / math.pi debugTrophy("Trajectory dot product: " .. dot .. ", angle: " .. angle .. " degrees") --Consider heading toward if within 45 degrees local isHeading = dot > 0.707 --cos(45 degrees) debugTrophy("Heading toward: " .. tostring(isHeading)) callback(isHeading) end) if not success then debugTrophy("Error processing samples: " .. tostring(result)) callback(false) end return end local success, errorMsg = pcall(function() if weapon:isExist() then local pos = weapon:getPosition().p local vel = weapon:getVelocity() table.insert(samples, { pos = pos, vel = vel }) debugTrophy("Sample " .. count .. ": pos x=" .. math.floor(pos.x) .. ", z=" .. math.floor(pos.z) .. ", vel x=" .. vel.x .. ", z=" .. vel.z) timer.scheduleFunction(function() collectSample(count + 1) end, {}, timer.getTime() + sampleInterval) else debugTrophy("Weapon no longer exists during sampling") callback(false) end end) if not success then debugTrophy("Error collecting sample " .. count .. ": " .. tostring(errorMsg)) callback(false) end end debugTrophy("Scheduling trajectory sampling for weapon near " .. unit:getName() .. " with 0.1-second delay") timer.scheduleFunction(function() if weapon:isExist() then collectSample(1) else debugTrophy("Weapon no longer exists before sampling") callback(false) end end, {}, timer.getTime() + initialDelay) end --Function to track weapon and check for nearby TrophyAPS vehicles local function trackWeapon(weapon, weaponName, initTime, targetUnit, shooterUnit) if not splash_damage_options.trophy_enabled then debugTrophy("Trophy APS disabled, skipping tracking for " .. tostring(weaponName)) return end if not weapon then debugTrophy("Weapon " .. tostring(weaponName) .. " is nil, stopping tracking") return end local success, errorMsg = pcall(function() if not weapon:isExist() then debugTrophy("Weapon " .. tostring(weaponName) .. " no longer exists, stopping tracking") return end local weaponPos local posSuccess, posError = pcall(function() weaponPos = weapon:getPosition().p end) if not posSuccess or not weaponPos then debugTrophy("Failed to get position for weapon " .. tostring(weaponName) .. ": " .. tostring(posError)) return end debugTrophy("Tracking weapon: " .. tostring(weaponName) .. " at x=" .. math.floor(weaponPos.x) .. ", z=" .. math.floor(weaponPos.z)) if targetUnit:isExist() and targetUnit:getLife() > 1 then local unitPos local unitSuccess, unitErrorMsg = pcall(function() unitPos = targetUnit:getPosition().p end) if not unitSuccess or not unitPos then debugTrophy("Failed to get position for unit " .. targetUnit:getName() .. ": " .. tostring(unitErrorMsg)) return end local distance if unitPos and weaponPos then distance = math.sqrt((unitPos.x - weaponPos.x)^2 + (unitPos.y - weaponPos.y)^2 + (unitPos.z - weaponPos.z)^2) debugTrophy("Weapon " .. tostring(weaponName) .. " distance to TrophyAPS vehicle " .. targetUnit:getName() .. ": " .. math.floor(distance) .. " meters") else debugTrophy("Failed to calculate distance for weapon " .. tostring(weaponName) .. " to unit " .. targetUnit:getName()) return end if distance <= splash_damage_options.trophy_detectRange then --Within detection range if distance <= splash_damage_options.trophy_interceptRange then --Within interception range local unitId = targetUnit:getID() if not trophyAmmo[unitId] then trophyAmmo[unitId] = { FR = splash_damage_options.trophy_frontRightRounds, BL = splash_damage_options.trophy_backLeftRounds } end debugTrophy("Interception triggered for " .. tostring(weaponName) .. " near " .. targetUnit:getName()) --Report shooter position reportShooterPosition(shooterUnit, targetUnit, weaponName) --Get vehicle orientation (heading) at interception time local unitOrientationSuccess, unitOrientation = pcall(function() return targetUnit:getPosition().x end) if not unitOrientationSuccess then debugTrophy("Error getting unit orientation: " .. tostring(unitOrientation)) return end local headingX, headingZ = unitOrientation.x, unitOrientation.z local headingMag = math.sqrt(headingX^2 + headingZ^2) if headingMag == 0 then debugTrophy("Invalid unit heading for " .. targetUnit:getName()) return end headingX, headingZ = headingX / headingMag, headingZ / headingMag debugTrophy("Tank heading: x=" .. headingX .. ", z=" .. headingZ) --Calculate threat direction (weapon to vehicle) local threatX = weaponPos.x - unitPos.x local threatZ = weaponPos.z - unitPos.z local magThreat = math.sqrt(threatX^2 + threatZ^2) if magThreat == 0 then debugTrophy("Invalid threat vector magnitude") return end threatX, threatZ = threatX / magThreat, threatZ / magThreat debugTrophy("Threat direction: x=" .. threatX .. ", z=" .. threatZ) --Compute relative angle using atan2 for correct quadrant local angle = math.atan2(threatZ, threatX) - math.atan2(headingZ, headingX) angle = angle * 180 / math.pi if angle < 0 then angle = angle + 360 end debugTrophy("Threat angle: " .. angle .. " degrees (relative to vehicle heading)") local offsetDistance = splash_damage_options.trophy_explosionOffsetDistance local explosionX, explosionZ local launcher --Rotate offsets based on tank heading local rightX, rightZ = -headingZ, headingX --Perpendicular to heading (right vector) if (angle >= 315 or angle <= 135) then --Front-right launcher (forward + right) explosionX = unitPos.x + headingX * offsetDistance + rightX * offsetDistance explosionZ = unitPos.z + headingZ * offsetDistance + rightZ * offsetDistance launcher = "FR" debugTrophy("Selected front-right launcher for angle " .. angle) if trophyAmmo[unitId].FR > 0 then trophyAmmo[unitId].FR = trophyAmmo[unitId].FR - 1 debugTrophy("Using front-right launcher for " .. tostring(weaponName) .. ", unit " .. unitId .. " FR rounds left: " .. trophyAmmo[unitId].FR) else debugTrophy("No front-right rounds left for unit " .. targetUnit:getName()) return end else --Back-left launcher (backward + left) explosionX = unitPos.x - headingX * offsetDistance - rightX * offsetDistance explosionZ = unitPos.z - headingZ * offsetDistance - rightZ * offsetDistance launcher = "BL" debugTrophy("Selected back-left launcher for angle " .. angle) if trophyAmmo[unitId].BL > 0 then trophyAmmo[unitId].BL = trophyAmmo[unitId].BL - 1 debugTrophy("Using back-left launcher for " .. tostring(weaponName) .. ", unit " .. unitId .. " BL rounds left: " .. trophyAmmo[unitId].BL) else debugTrophy("No back-left rounds left for unit " .. targetUnit:getName()) return end end debugTrophy("Explosion position: x=" .. explosionX .. ", z=" .. explosionZ) if math.random() >= splash_damage_options.trophy_failureChance then --Explosion 1.6 meters above ground local groundHeight = land.getHeight({x = explosionX, y = explosionZ}) local explosionY = groundHeight + (groundHeight + 1.6 < 1.6 and 1.6 or 1.6) local explosionSuccess, explosionError = pcall(function() trigger.action.explosion({ x = explosionX, y = explosionY, z = explosionZ }, splash_damage_options.trophy_selfExplosionSize) end) if not explosionSuccess then debugTrophy("Error triggering interception explosion: " .. tostring(explosionError)) else --Check unit health after Trophy explosion local healthSuccess, unitHealth = pcall(function() return targetUnit:getLife() end) if healthSuccess and unitHealth then debugTrophy("Unit " .. targetUnit:getName() .. " health after " .. launcher .. " firing: " .. unitHealth) else debugTrophy("Failed to get health for unit " .. targetUnit:getName() .. ": " .. tostring(unitHealth)) end end --Immediate weapon destruction local destroySuccess, destroyError = pcall(function() if weapon and weapon:isExist() then local wpnPos = weapon:getPosition().p debugTrophy("Destroying weapon: " .. tostring(weaponName)) local groundHeight = land.getHeight({x = wpnPos.x, y = wpnPos.z}) local explosionY = wpnPos.y < groundHeight + 1.6 and groundHeight + 1.6 or wpnPos.y trigger.action.explosion({ x = wpnPos.x, y = explosionY, z = wpnPos.z }, splash_damage_options.trophy_weaponExplosionSize) else debugTrophy("Weapon " .. tostring(weaponName) .. " no longer exists for destruction") end end) if not destroySuccess then debugTrophy("Error destroying weapon: " .. tostring(destroyError)) end else debugTrophy("Interception missed for " .. tostring(weaponName)) return --Skip destruction, allow threat to continue end --Continue tracking for other units end end --Continue tracking with fast (0.1s) at under 1000m or slow at more than 1000m (1s) interval based on distance. Even faster at 200m/100m or less local trackInterval = distance and (distance <= 100 and 0.02 or (distance <= 200 and 0.05 or (distance <= 1000 and 0.1 or 1))) or 1 debugTrophy("Scheduling next track for " .. tostring(weaponName) .. " in " .. trackInterval .. " seconds") timer.scheduleFunction(function(args) local wpn, wpnName, unit, shooter = args[1], args[2], args[3], args[4] if not wpn then debugTrophy("Scheduled weapon " .. tostring(wpnName) .. " is nil, stopping tracking") return end local success, errorMsg = pcall(function() trackWeapon(wpn, wpnName, initTime, unit, shooter) end) if not success then debugTrophy("Error in scheduled tracking for " .. tostring(wpnName) .. ": " .. tostring(errorMsg)) end end, {weapon, weaponName, targetUnit, shooterUnit}, timer.getTime() + trackInterval) else debugTrophy("Target unit " .. targetUnit:getName() .. " no longer exists or is dead, stopping tracking") return end end) if not success then debugTrophy("Error tracking weapon " .. tostring(weaponName) .. ": " .. tostring(errorMsg)) end end --Event handler for weapon firing function trophyHandler:onEvent(event) debugTrophy("Event received: " .. tostring(event.id)) local success, errorMsg = pcall(function() if event.id == world.event.S_EVENT_SHOT then local weapon = event.weapon if weapon and weapon:isExist() then local weaponDesc = weapon:getDesc() local displayName = weaponDesc.displayName or "None" local typeName = weaponDesc.typeName or "None" --Capture shooter unit local shooterUnit = event.initiator --Check if typeName starts with weapons.missiles. or weapons.nurs. if typeName:match("^weapons%.missiles%.") or typeName:match("^weapons%.nurs%.") then local weaponName = typeName:gsub("^weapons%.missiles%.", ""):gsub("^weapons%.nurs%.", "") local isMatch = isTrophyWeapon(weaponName) debugTrophy("Weapon fired: " .. tostring(weaponName) .. " (Matches Trophy list: " .. (isMatch and "Yes" or "No") .. ") | DisplayName: " .. tostring(displayName) .. " | TypeName: " .. tostring(typeName)) if isMatch and splash_damage_options.trophy_enabled then debugTrophy("Trophy weapon detected: " .. tostring(weaponName)) local weaponPos local success, errorMsg = pcall(function() weaponPos = weapon:getPosition().p end) if not success or not weaponPos then debugTrophy("Failed to get initial position for weapon " .. tostring(weaponName) .. ": " .. tostring(errorMsg)) return end local trophyUnits = findTrophyVehicles(weaponPos, weaponName) if #trophyUnits > 0 then local trackedUnits = 0 local processedUnits = 0 for _, unit in pairs(trophyUnits) do isWeaponHeadingToward(weapon, unit, function(isHeading) processedUnits = processedUnits + 1 if isHeading then debugTrophy("Weapon " .. tostring(weaponName) .. " heading toward " .. unit:getName() .. ", starting tracking") trackWeapon(weapon, weaponName, timer.getTime(), unit, shooterUnit) trackedUnits = trackedUnits + 1 else debugTrophy("Weapon " .. tostring(weaponName) .. " not heading toward " .. unit:getName() .. ", skipping tracking") end --Log tracking summary after all units are checked if processedUnits == #trophyUnits then debugTrophy("Tracking " .. tostring(weaponName) .. " against " .. trackedUnits .. " of " .. #trophyUnits .. " TrophyAPS vehicles in range") end end) end else debugTrophy("No TrophyAPS vehicles within range for " .. tostring(weaponName)) end end else debugTrophy("Weapon typeName " .. tostring(typeName) .. " does not match missiles or nurs, skipping") end else debugTrophy("Weapon is nil or does not exist") end end end) if not success then debugTrophy("Error in event handler: " .. tostring(errorMsg)) end end --Giant Explosion Function function triggerGiantExplosion(params) if not splash_damage_options.giant_explosion_enabled then debugMsg("Giant Explosion is disabled in options.") return end local initialPos = params.pos or {x = 0, y = 0, z = 0} local explosionPower = params.power or splash_damage_options.giant_explosion_power local sizeScale = params.scale or splash_damage_options.giant_explosion_scale local totalDuration = params.duration or splash_damage_options.giant_explosion_duration local explosionCount = params.count or splash_damage_options.giant_explosion_count if not initialPos.x or not initialPos.y or not initialPos.z then gameMsg("Error: Invalid position for giant explosion!") debugMsg("No valid initial position set for giant explosion!") return end debugMsg("Triggering giant fireball at X: " .. initialPos.x .. ", Y: " .. initialPos.y .. ", Z: " .. initialPos.z) local function scheduleExplosion(pos, delay) if not pos or not pos.x or not pos.y or not pos.z then debugMsg("Error: Invalid position for explosion - pos: " .. tostring(pos)) return end timer.scheduleFunction(function(p) if p and p.x and p.y and p.z then trigger.action.explosion(p, explosionPower) end end, pos, timer.getTime() + delay) end --Pre-explosion scan for cargo units local scanRadius = 1500 * sizeScale --1500m base radius, scaled by sizeScale local preExplosionTargets = {} if splash_damage_options.enable_cargo_effects then local volS = { id = world.VolumeType.SPHERE, params = { point = initialPos, radius = scanRadius } } local ifFound = function(foundObject) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and foundObject:getDesc().category == Unit.Category.GROUND_UNIT) or category == Object.Category.STATIC then table.insert(preExplosionTargets, { name = foundObject:getTypeName(), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0, unit = foundObject }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound) debugMsg("Pre-explosion scan for Giant Explosion: " .. #preExplosionTargets .. " targets found within " .. scanRadius .. "m") end --Trigger the explosion local maxRadius = 200 * sizeScale local maxHeight = 500 * sizeScale local adjustedExplosionCount = math.floor(explosionCount * (sizeScale ^ 2.5)) local stepTime = totalDuration / adjustedExplosionCount local variance = 0.25 --Fixed at 25% for i = 1, adjustedExplosionCount do local progress = i / adjustedExplosionCount local currentRadius = maxRadius * progress local r = currentRadius * (0.9 + math.random() * 0.1) local theta = math.random() * 2 * math.pi local phi = math.acos(math.random()) local offsetX = r * math.sin(phi) * math.cos(theta) local offsetZ = r * math.sin(phi) * math.sin(theta) local offsetY = r * math.cos(phi) offsetX = offsetX * (1 + (math.random() - 0.5) * variance) offsetZ = offsetZ * (1 + (math.random() - 0.5) * variance) offsetY = offsetY * (1 + (math.random() - 0.5) * variance * 0.5) local blastPos = { x = initialPos.x + offsetX, y = land.getHeight({x = initialPos.x, y = initialPos.z}) + offsetY, z = initialPos.z + offsetZ } if blastPos.y < land.getHeight({x = blastPos.x, y = blastPos.z}) then blastPos.y = land.getHeight({x = blastPos.x, y = blastPos.z}) end local delay = (i - 1) * stepTime + (math.random() - 0.5) * stepTime * variance scheduleExplosion(blastPos, delay) end gameMsg("Expanding giant fireball over " .. totalDuration .. "s (scale " .. sizeScale .. ")!") --Post-explosion scan and cargo cook-off queuing if splash_damage_options.enable_cargo_effects then timer.scheduleFunction(function(args) local centerPos = args[1] local radius = args[2] local preTargets = args[3] local postExplosionTargets = {} local volS = { id = world.VolumeType.SPHERE, params = { point = centerPos, radius = radius } } local ifFound = function(foundObject) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and foundObject:getDesc().category == Unit.Category.GROUND_UNIT) or category == Object.Category.STATIC then table.insert(postExplosionTargets, { name = foundObject:getTypeName(), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0 }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound) debugMsg("Post-explosion scan for Giant Explosion: " .. #postExplosionTargets .. " targets found within " .. radius .. "m") --Compare pre- and post-explosion targets for _, preTarget in ipairs(preTargets) do local found = false local postHealth = 0 for _, postTarget in ipairs(postExplosionTargets) do if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then found = true postHealth = postTarget.health break end end local cargoData = cargoUnits[preTarget.name] if cargoData and (not found or postHealth <= 0) then local distance = getDistance(initialPos, preTarget.position) if distance <= radius then local cargoPower = cargoData.cargoExplosionPower or explosionPower table.insert(cargoEffectsQueue, { name = preTarget.name, distance = distance, coords = preTarget.position, power = cargoPower, explosion = cargoData.cargoExplosion, cookOff = cargoData.cargoCookOff, cookOffCount = cargoData.cookOffCount, cookOffPower = cargoData.cookOffPower, cookOffDuration = cargoData.cookOffDuration, cookOffRandomTiming = cargoData.cookOffRandomTiming, cookOffPowerRandom = cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration }) debugMsg("Queued cargo effect for " .. preTarget.name .. " destroyed by Giant Explosion at " .. string.format("%.1f", distance) .. "m") end end end --Process queued cargo effects with prioritized flames if #cargoEffectsQueue > 0 then local flameIndex = 0 --Separate index for flames local otherIndex = 0 --Index for explosions, cook-offs, debris local processedCargoUnits = {} local flamePositions = {} for _, effect in ipairs(cargoEffectsQueue) do local unitKey = effect.name .. "_" .. effect.coords.x .. "_" .. effect.coords.z if not processedUnitsGlobal[unitKey] and not processedCargoUnits[unitKey] then --Handle tanker flames first with minimal delay if effect.isTanker and effect.explosion then debugMsg("Triggering cargo explosion for tanker " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m with power " .. effect.power .. " scheduled at " .. flameIndex .. "s") timer.scheduleFunction(function(params) debugMsg("Executing cargo explosion at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, effect.power}, timer.getTime() + flameIndex + 0.1) local flameSize = effect.flameSize or 3 local flameDuration = effect.flameDuration local flameDensity = 1.0 local effectId = effectSmokeId effectSmokeId = effectSmokeId + 1 local isDuplicate = false for _, pos in pairs(flamePositions) do if getDistance3D(effect.coords, pos) < 3 then isDuplicate = true debugMsg("Skipping duplicate flame for " .. effect.name .. " near X: " .. string.format("%.0f", pos.x) .. ", Y: " .. string.format("%.0f", pos.y) .. ", Z: " .. string.format("%.0f", pos.z)) break end end if not isDuplicate then debugMsg("Adding flame effect for tanker " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m (Size: " .. flameSize .. ", Duration: " .. flameDuration .. "s, ID: " .. effectId .. ") scheduled at " .. flameIndex .. "s") timer.scheduleFunction(function(params) local terrainHeight = land.getHeight({x = params[1].x, y = params[1].z}) local adjustedCoords = {x = params[1].x, y = terrainHeight + 2, z = params[1].z} debugMsg("Spawning flame effect at X: " .. string.format("%.0f", adjustedCoords.x) .. ", Y: " .. string.format("%.0f", adjustedCoords.y) .. ", Z: " .. string.format("%.0f", adjustedCoords.z)) trigger.action.explosion(adjustedCoords, 10) --Small trigger explosion trigger.action.effectSmokeBig(adjustedCoords, params[2], params[3], params[4]) end, {effect.coords, flameSize, flameDensity, effectId}, timer.getTime() + flameIndex + 0.2) timer.scheduleFunction(function(id) debugMsg("Stopping flame effect for " .. effect.name .. " (ID: " .. id .. ")") trigger.action.effectSmokeStop(id) end, effectId, timer.getTime() + flameIndex + flameDuration + 0.2) table.insert(flamePositions, effect.coords) end flameIndex = flameIndex + 0.5 --Fast spacing for flames (0.5s) end --Handle non-tanker explosions, cook-offs, and debris if not effect.isTanker or (effect.explosion and not effect.isTanker) then if effect.explosion then debugMsg("Triggering cargo explosion for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m with power " .. effect.power .. " scheduled at " .. otherIndex .. "s") timer.scheduleFunction(function(params) debugMsg("Executing cargo explosion at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, effect.power}, timer.getTime() + otherIndex + 0.1) end if effect.cookOff and effect.cookOffCount > 0 then debugMsg("Scheduling " .. effect.cookOffCount .. " cook-off explosions for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m over " .. effect.cookOffDuration .. "s starting at " .. otherIndex .. "s") for i = 1, effect.cookOffCount do local delay = effect.cookOffRandomTiming and math.random() * effect.cookOffDuration or (i - 1) * (effect.cookOffDuration / effect.cookOffCount) local basePower = effect.cookOffPower local powerVariation = effect.cookOffPowerRandom / 100 local cookOffPower = effect.cookOffPowerRandom == 0 and basePower or basePower * (1 + powerVariation * (math.random() * 2 - 1)) debugMsg("Cook-off #" .. i .. " for " .. effect.name .. " at " .. string.format("%.1f", effect.distance) .. "m scheduled at " .. string.format("%.3f", delay) .. "s with power " .. string.format("%.2f", cookOffPower)) timer.scheduleFunction(function(params) debugMsg("Executing cook-off at X: " .. string.format("%.0f", params[1].x) .. ", Y: " .. string.format("%.0f", params[1].y) .. ", Z: " .. string.format("%.0f", params[1].z) .. " with power " .. params[2]) trigger.action.explosion(params[1], params[2]) end, {effect.coords, cookOffPower}, timer.getTime() + otherIndex + delay) end if splash_damage_options.debris_effects then local debrisCount = math.random(splash_damage_options.debris_count_min, splash_damage_options.debris_count_max) for j = 1, debrisCount do local theta = math.random() * 2 * math.pi local phi = math.acos(math.random() * 2 - 1) local minDist = splash_damage_options.debris_max_distance * 0.1 local maxDist = splash_damage_options.debris_max_distance local r = math.random() * (maxDist - minDist) + minDist local debrisX = effect.coords.x + r * math.sin(phi) * math.cos(theta) local debrisZ = effect.coords.z + r * math.sin(phi) * math.sin(theta) local terrainY = land.getHeight({x = debrisX, y = debrisZ}) local debrisY = terrainY + math.random() * maxDist local debrisPos = {x = debrisX, y = debrisY, z = debrisZ} local debrisPower = splash_damage_options.debris_power local debrisDelay = (j - 1) * (effect.cookOffDuration / debrisCount) timer.scheduleFunction(function(debrisArgs) debugMsg("Debris explosion at X: " .. string.format("%.0f", debrisArgs[1].x) .. ", Y: " .. string.format("%.0f", debrisArgs[1].y) .. ", Z: " .. string.format("%.0f", debrisArgs[1].z) .. " with power " .. debrisArgs[2]) trigger.action.explosion(debrisArgs[1], debrisArgs[2]) end, {debrisPos, debrisPower}, timer.getTime() + otherIndex + debrisDelay) end end end otherIndex = otherIndex + 1 --Slower spacing for non-flame effects (1s) end processedCargoUnits[unitKey] = true processedUnitsGlobal[unitKey] = true end end cargoEffectsQueue = {} --Clear the queue after processing end end, {initialPos, scanRadius, preExplosionTargets}, timer.getTime() + totalDuration + 1.0) end end function getWeaponExplosive(name) local weaponData = explTable[name] if weaponData then return weaponData.explosive, weaponData.shaped_charge else return 0, false end end function track_wpns_cluster_scan(args) local parentPos = args[1] local parentDir = args[2] local parentName = args[3] local subName = args[4] local subCount = args[5] local subPower = args[6] local parentVel = args[7] local attempt = args[8] or 1 local maxAttempts = 3 local scanVol = { id = world.VolumeType.SPHERE, params = { point = parentPos, radius = 400 } } local bombletsFound = {} local allWeaponsFound = {} --General scan for all weapons world.searchObjects(Object.Category.WEAPON, scanVol, function(wpn) if wpn:isExist() then local wpnId = wpn.id_ local wpnType = wpn:getTypeName() local wpnPos = wpn:getPosition().p table.insert(allWeaponsFound, { id = wpnId, type = wpnType, x = wpnPos.x, y = wpnPos.y, z = wpnPos.z }) if wpnType == subName and not tracked_weapons[wpnId] then tracked_weapons[wpnId] = { wpn = wpn, pos = wpnPos, speed = wpn:getVelocity(), name = wpnType, parent = parentName, parentVelocity = parentVel } table.insert(bombletsFound, wpnId) debugMsg("Detected expected submunition '" .. wpnType .. "' from '" .. parentName .. "' at X: " .. string.format("%.0f", wpnPos.x) .. ", Y: " .. string.format("%.0f", wpnPos.y) .. ", Z: " .. string.format("%.0f", wpnPos.z) .. " (Attempt " .. attempt .. ")") end end return true end) --Log results debugMsg("Scanned for submunition '" .. subName .. "' bomblets from '" .. parentName .. "': " .. #bombletsFound .. " found (Attempt " .. attempt .. ")") if #allWeaponsFound > 0 then local msg = "General scan for '" .. parentName .. "': " .. #allWeaponsFound .. " bomblets released, expected " .. subCount .. " '" .. subName .. "'" local typeMismatch = false for _, wpn in ipairs(allWeaponsFound) do if wpn.type ~= subName then typeMismatch = true break end end if typeMismatch then msg = msg .. " - Mismatch detected! Actual bomblets: " for _, wpn in ipairs(allWeaponsFound) do msg = msg .. "'" .. wpn.type .. "' (X: " .. string.format("%.0f", wpn.x) .. ", Y: " .. string.format("%.0f", wpn.y) .. ", Z: " .. string.format("%.0f", wpn.z) .. ") " end msg = msg .. "Script may need changing." end debugMsg(msg) elseif #bombletsFound == 0 and #allWeaponsFound == 0 then debugMsg("No bomblets of any type detected for '" .. parentName .. "' (Attempt " .. attempt .. ")") end --Retry if no expected submunitions found if #bombletsFound == 0 and attempt < maxAttempts then debugMsg("No expected submunition '" .. subName .. "' found on attempt " .. attempt .. ", retrying in 0.5s") timer.scheduleFunction(track_wpns_cluster_scan, {parentPos, parentDir, parentName, subName, subCount, subPower, parentVel, attempt + 1}, timer.getTime() + 0.5) elseif #bombletsFound == 0 and attempt == maxAttempts then debugMsg("No submunition '" .. subName .. "' spawned by DCS for '" .. parentName .. "' after " .. maxAttempts .. " attempts - skipping additional explosions") end end --function to schedule flares for cook-offs function scheduleCookOffFlares(coords, cookOffCount, cookOffDuration, flareColor) if not splash_damage_options.cookoff_flares_enabled then return end --Skip if flares disabled if math.random() > splash_damage_options.cookoff_flare_chance then return end --Skip if chance fails local flareCount = math.floor(cookOffCount * splash_damage_options.cookoff_flare_count_modifier) if flareCount < 1 then return end --Skip if no flares debugCargoCookOff("Scheduling flares for cook-off at X: " .. string.format("%.0f", coords.x) .. ", Z: " .. string.format("%.0f", coords.z)) if splash_damage_options.cookoff_flare_instant then --Use evenly distributed azimuths for instant flares local scaledFlareCount = math.random(splash_damage_options.cookoff_flare_instant_min, splash_damage_options.cookoff_flare_instant_max) debugCargoCookOff("Spawning " .. scaledFlareCount .. " instant flares") local angleStep = 360 / scaledFlareCount --Divide circle into equal segments for i = 1, scaledFlareCount do --Base azimuth for this flare, with a small random offset within ±20 degrees local baseAzimuth = (i - 1) * angleStep local randomAzimuth = baseAzimuth + math.random(-33, 40) randomAzimuth = randomAzimuth % 360 --Normalize to [0, 359] local offsetX = math.random(-splash_damage_options.cookoff_flare_offset, splash_damage_options.cookoff_flare_offset) local offsetZ = math.random(-splash_damage_options.cookoff_flare_offset, splash_damage_options.cookoff_flare_offset) local flarePos = { x = coords.x + offsetX, y = coords.y, z = coords.z + offsetZ } debugCargoCookOff("Spawning instant flare #" .. i .. " at X: " .. string.format("%.0f", flarePos.x) .. ", Z: " .. string.format("%.0f", flarePos.z) .. " with color " .. flareColor .. " and azimuth " .. randomAzimuth) trigger.action.signalFlare(flarePos, flareColor, randomAzimuth) end else --Original time-based flare spawning debugCargoCookOff("Spawning " .. flareCount .. " flares over " .. cookOffDuration .. " seconds") for i = 1, flareCount do local delay = math.random() * cookOffDuration --Random time within cook-off duration local terrainHeight = land.getHeight({x = coords.x, y = coords.z}) local offset = { x = coords.x + math.random(-splash_damage_options.cookoff_flare_offset, splash_damage_options.cookoff_flare_offset), y = terrainHeight, --Start at ground level z = coords.z + math.random(-splash_damage_options.cookoff_flare_offset, splash_damage_options.cookoff_flare_offset) } local azimuth = math.random(1, 360) --Random direction timer.scheduleFunction(function(params) debugCargoCookOff("Spawning flare #" .. params[1] .. " at X: " .. string.format("%.0f", params[2].x) .. ", Y: " .. string.format("%.0f", params[2].y) .. ", Z: " .. string.format("%.0f", params[2].z) .. " with color " .. params[3]) trigger.action.signalFlare(params[2], params[3], params[4]) end, {i, offset, flareColor, azimuth}, timer.getTime() + delay) end end end function track_wpns() local weaponsToRemove = {} --Delay removal to ensure all weapons are checked for wpn_id_, wpnData in pairs(tracked_weapons) do local status, err = pcall(function() --Skip ground ordnance if track_groundunitordnance is disabled if wpnData.isGroundUnitOrdnance and not splash_damage_options.track_groundunitordnance then debugMsg("Ground unit ordnance weapon, track_groundunitordnance set to false, skipping tracking + effects") table.insert(weaponsToRemove, wpn_id_) return --Exit this weapon's processing end if wpnData.wpn:isExist() then --Update position, direction, speed wpnData.pos = wpnData.wpn:getPosition().p wpnData.dir = wpnData.wpn:getPosition().x wpnData.speed = wpnData.wpn:getVelocity() --Scan potential blast zone in the last frame before impact if splash_damage_options.track_pre_explosion then local ip = land.getIP(wpnData.pos, wpnData.dir, lookahead(wpnData.speed)) local predictedImpact = ip or wpnData.pos local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name) base_explosive = base_explosive * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then base_explosive = base_explosive * splash_damage_options.rocket_multiplier end if wpnData.isGroundUnitOrdnance and splash_damage_options.track_groundunitordnance then base_explosive = base_explosive * splash_damage_options.groundunitordnance_damage_modifier end local explosionPower = base_explosive if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier end local blastRadius = splash_damage_options.blast_search_radius * 2 --Wider post-scan (180m default) if splash_damage_options.use_dynamic_blast_radius then blastRadius = math.pow(explosionPower, 1/3) * 10 * splash_damage_options.dynamic_blast_radius_modifier end --Set tightRadius, use 50m for ground ordnance if enabled local tightRadius = blastRadius if wpnData.isGroundUnitOrdnance and splash_damage_options.scan_50m_for_groundordnance then tightRadius = 50 --Fixed 50m radius for ground ordnance if splash_damage_options.track_groundunitordnance_debug then debugMsg("Using 50m scan radius for ground ordnance " .. wpnData.name) end end local volS = { id = world.VolumeType.SPHERE, params = { point = wpnData.pos, --Use current pos radius = tightRadius } } local tightTargets = {} local ifFound = function(foundObject, targets, center) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and (foundObject:getDesc().category == Unit.Category.GROUND_UNIT or foundObject:getDesc().category == Unit.Category.AIRPLANE)) or category == Object.Category.STATIC then table.insert(targets, { name = foundObject:getTypeName(), distance = getDistance(center, foundObject:getPoint()), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0, unit = foundObject, id = foundObject:getID(), unitName = foundObject:getName() or "Unknown" }) end end return true end if splash_damage_options.track_pre_explosion_debug then debugMsg("Scanning tight radius " .. tightRadius .. "m at current pos while weapon exists") end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, function(obj) ifFound(obj, tightTargets, wpnData.pos) end) wpnData.tightTargets = tightTargets --Store for impact --Wider scan for lastKnownTargets volS.params.point = predictedImpact volS.params.radius = blastRadius local foundTargets = {} world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, function(obj) ifFound(obj, foundTargets, predictedImpact) end) wpnData.lastKnownTargets = foundTargets end --Submunition impact handling local weaponData = explTable[wpnData.parent or wpnData.name] or { submunition_name = "unknown" } if wpnData.name == weaponData.submunition_name then local groundHeight = land.getHeight({x = wpnData.pos.x, y = wpnData.pos.z}) if wpnData.pos.y - groundHeight < 50 then --Impact threshold like old script if splash_damage_options.debug then 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)) end local parentWeaponData = explTable[wpnData.parent] or { submunition_count = 30, submunition_explosive = 1 } local submunitionCount = parentWeaponData.submunition_count or 30 local submunitionPower = (parentWeaponData.submunition_explosive or 1) * splash_damage_options.cluster_bomblet_damage_modifier * (splash_damage_options.overall_scaling /100) if splash_damage_options.cluster_bomblet_reductionmodifier then if submunitionCount > 35 then local reductionFactor = (60 - 35) / (247 - 35) submunitionCount = 35 + math.floor((submunitionCount - 35) * reductionFactor) if submunitionCount > 60 then submunitionCount = 60 end end end --Use parent velocity if available, else submunition speed local parentDir = wpnData.parentVelocity or wpnData.speed local dispersionLength, dispersionWidth = calculate_dispersion(parentDir, 2000) --Match original 2000m local dirMag = math.sqrt(parentDir.x^2 + parentDir.z^2) local dir = dirMag > 0 and {x = parentDir.x / dirMag, z = parentDir.z / dirMag} or {x = 1, z = 0} if splash_damage_options.debug then 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") end for i = 1, submunitionCount do local theta = math.random() * 2 * math.pi local r = math.sqrt(math.random()) local xOffset = r * dispersionLength * 0.5 * math.cos(theta) local zOffset = r * dispersionWidth * 0.5 * math.sin(theta) local subPos = { x = wpnData.pos.x + (xOffset * dir.x - zOffset * dir.z), z = wpnData.pos.z + (xOffset * dir.z + zOffset * dir.x) } subPos.y = land.getHeight({x = subPos.x, y = subPos.z}) if splash_damage_options.debug then debugMsg("Triggering bomblet #" .. i .. " for submunition '" .. wpnData.name .. "' at X: " .. string.format("%.0f", subPos.x) .. ", Z: " .. string.format("%.0f", subPos.z) .. " with power " .. submunitionPower) end trigger.action.explosion(subPos, submunitionPower) end table.insert(weaponsToRemove, wpn_id_) end end else --Weapon has impacted if splash_damage_options.debug then debugMsg("Weapon " .. wpnData.name .. " no longer exists at " .. timer.getTime() .. "s") end local ip = land.getIP(wpnData.pos, wpnData.dir, lookahead(wpnData.speed)) --terrain intersection point with weapon's nose local explosionPoint if not ip then --use last calculated IP explosionPoint = wpnData.pos else --use intersection point explosionPoint = ip end if wpnData.isGroundUnitOrdnance and splash_damage_options.track_groundunitordnance_debug then local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name) base_explosive = base_explosive * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then base_explosive = base_explosive * splash_damage_options.rocket_multiplier end if wpnData.isGroundUnitOrdnance and splash_damage_options.track_groundunitordnance then base_explosive = base_explosive * splash_damage_options.groundunitordnance_damage_modifier end local explosionPower = base_explosive if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier end debugMsg("Ground unit ordnance " .. wpnData.name .. " impacted at X: " .. string.format("%.0f", explosionPoint.x) .. ", Y: " .. string.format("%.0f", explosionPoint.y) .. ", Z: " .. string.format("%.0f", explosionPoint.z) .. " with power " .. explosionPower) end local chosenTargets = wpnData.tightTargets or {} local safeToBlast = true --Check for tactical explosion conditions local isTactical = false --Check tactical weapons table if tacticalwpn_tabl[wpnData.name] then isTactical = true if splash_damage_options.debug then debugMsg("Tactical explosion triggered for weapon: " .. wpnData.name .. " at X: " .. explosionPoint.x .. ", Z: " .. explosionPoint.z) end TacticalExplosionTrigger(explosionPoint) table.insert(weaponsToRemove, wpn_id_) end --Check tactical override weapons if splash_damage_options.tactical_explosion and splash_damage_options.tactical_explosion_override_enabled then local tacticalWeapons = {} for weapon in splash_damage_options.tactical_explosion_override_weapons:gmatch("[^,]+") do tacticalWeapons[trim(weapon:upper())] = true -- Normalize to uppercase end if tacticalWeapons[wpnData.name:upper()] then -- Compare in uppercase isTactical = true if splash_damage_options.debug then debugMsg("Tactical explosion override triggered for " .. wpnData.name .. " at X: " .. explosionPoint.x .. ", Z: " .. explosionPoint.z) end TacticalExplosionTrigger(explosionPoint) table.insert(weaponsToRemove, wpn_id_) end end --Check if weapon is napalm local isNapalm = false --Check for napalm override weapons if splash_damage_options.napalmoverride_enabled then local napalmWeapons = {} for weapon in splash_damage_options.napalm_override_weapons:gmatch("[^,]+") do napalmWeapons[trim(weapon)] = true end if napalmWeapons[wpnData.name] then isNapalm = true if splash_damage_options.debug then debugMsg("Napalm override triggered for " .. wpnData.name .. " at X: " .. string.format("%.0f", explosionPoint.x) .. ", Z: " .. string.format("%.0f", explosionPoint.z) .. ", playerName: " .. tostring(wpnData.initiatorPilotName or wpnData.init or "unknown")) end napalmOnImpact(explosionPoint, wpnData.speed, wpnData.name, wpnData.initiatorPilotName or wpnData.init or "unknown") -- Pass initiatorPilotName table.insert(weaponsToRemove, wpn_id_) end end --Check for MK77 weapons independently if splash_damage_options.napalm_mk77_enabled and (wpnData.name == "MK77mod0-WPN" or wpnData.name == "MK77mod1-WPN") then isNapalm = true if splash_damage_options.debug then debugMsg("MK77 napalm triggered for " .. wpnData.name .. " at X: " .. string.format("%.0f", explosionPoint.x) .. ", Z: " .. string.format("%.0f", explosionPoint.z) .. ", playerName: " .. tostring(wpnData.initiatorPilotName or wpnData.init or "unknown")) end napalmOnImpact(explosionPoint, wpnData.speed, wpnData.name, wpnData.initiatorPilotName or wpnData.init or "unknown") -- Pass initiatorPilotName table.insert(weaponsToRemove, wpn_id_) end if not isNapalm and not isTactical then if splash_damage_options.ordnance_protection then local checkVol = { id = world.VolumeType.SPHERE, params = { point = explosionPoint, radius = splash_damage_options.ordnance_protection_radius } } if splash_damage_options.debug then 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") end world.searchObjects(Object.Category.WEAPON, checkVol, function(obj) if obj:isExist() and tracked_weapons[obj.id_] then safeToBlast = false if splash_damage_options.debug then debugMsg("Skipping explosion for '" .. wpnData.name .. "' - nearby bomb '" .. tracked_weapons[obj.id_].name .. "' within " .. splash_damage_options.ordnance_protection_radius .. "m") end return false end return true end) end if safeToBlast then if splash_damage_options.debug then debugMsg("FinalPos Check for '" .. wpnData.name .. "': X: " .. string.format("%.0f", explosionPoint.x) .. ", Y: " .. string.format("%.0f", explosionPoint.y) .. ", Z: " .. string.format("%.0f", explosionPoint.z) .. ")") end local base_explosive, isShapedCharge = getWeaponExplosive(wpnData.name) base_explosive = base_explosive * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and wpnData.cat == Weapon.Category.ROCKET then base_explosive = base_explosive * splash_damage_options.rocket_multiplier end if wpnData.isGroundUnitOrdnance and splash_damage_options.track_groundunitordnance then base_explosive = base_explosive * splash_damage_options.groundunitordnance_damage_modifier if splash_damage_options.track_groundunitordnance_debug then debugMsg("Applying ground unit ordnance damage modifier " .. splash_damage_options.groundunitordnance_damage_modifier .. " to " .. wpnData.name .. ", base explosive power: " .. base_explosive) end end local explosionPower = base_explosive if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then explosionPower = explosionPower * splash_damage_options.shaped_charge_multiplier end local blastRadius = splash_damage_options.blast_search_radius * 2 --Wider post-scan (180m default) if splash_damage_options.use_dynamic_blast_radius then blastRadius = math.pow(explosionPower, 1/3) * 10 * splash_damage_options.dynamic_blast_radius_modifier end --Store pre-explosion state of all tracked weapons for detection local preExplosionWeapons = {} if splash_damage_options.ordnance_protection and splash_damage_options.detect_ordnance_destruction and splash_damage_options.larger_explosions then for id, data in pairs(tracked_weapons) do if data.wpn:isExist() then preExplosionWeapons[id] = { name = data.name, pos = data.wpn:getPosition().p, distance = getDistance3D(explosionPoint, data.wpn:getPosition().p), explosive = getWeaponExplosive(data.name) --Store the explosive power } end end end --Cluster Bomb Handling local weaponData = explTable[wpnData.name] or { explosive = 0, shaped_charge = false, Skip_larger_explosions = false, Skip_damage_model = false } local isCluster = weaponData.cluster or false if splash_damage_options.cluster_enabled and isCluster then local submunitionCount = weaponData.submunition_count or 30 local submunitionPower = (weaponData.submunition_explosive or 1) * splash_damage_options.cluster_bomblet_damage_modifier * splash_damage_options.overall_scaling local submunitionName = weaponData.submunition_name or "unknown" --Apply bomblet reduction logic if enabled if splash_damage_options.cluster_bomblet_reductionmodifier then if submunitionCount > 35 then local reductionFactor = (60 - 35) / (247 - 35) submunitionCount = 35 + math.floor((submunitionCount - 35) * reductionFactor) if submunitionCount > 60 then submunitionCount = 60 end --Cap at 60 end end --Extended scan with general bomblet detection timer.scheduleFunction(track_wpns_cluster_scan, {explosionPoint, wpnData.dir, wpnData.name, submunitionName, submunitionCount, submunitionPower, wpnData.speed}, timer.getTime() + 0.3) else --Standard explosion handling if splash_damage_options.larger_explosions and not (weaponData.Skip_larger_explosions or false) then if splash_damage_options.debug then debugMsg("Triggering initial explosion for '" .. wpnData.name .. "' at power " .. explosionPower) end trigger.action.explosion(explosionPoint, explosionPower) table.insert(recentExplosions, { pos = explosionPoint, time = timer.getTime(), radius = blastRadius }) if splash_damage_options.debug then debugMsg("Added to recentExplosions for '" .. wpnData.name .. "': X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. ", Time: " .. timer.getTime()) end --Check for units destroyed by initial explosion local playerName = wpnData.init or "unknown" for _, target in ipairs(chosenTargets) do if target.unit:isExist() and target.health > 0 and target.unit:getLife() <= 0 then if splash_damage_options.debug then debugMsg("Unit " .. target.name .. " destroyed by initial explosion, credited to player: " .. playerName) end end end elseif splash_damage_options.debug and (weaponData.Skip_larger_explosions or false) then debugMsg("Skipped larger explosion for '" .. wpnData.name .. "' due to Skip_larger_explosions = true") end if not (weaponData.Skip_damage_model or false) then blastWave(explosionPoint, splash_damage_options.blast_search_radius, wpnData.name, explosionPower, isShapedCharge) elseif splash_damage_options.debug then debugMsg("Skipped damage model for '" .. wpnData.name .. "' due to Skip_damage_model = true") end end --detect_ordnance_destruction comes before recent_large_explosion_snap in original if splash_damage_options.ordnance_protection and splash_damage_options.detect_ordnance_destruction and splash_damage_options.larger_explosions and not (weaponData.Skip_larger_explosions or false) then timer.scheduleFunction(function(args) local explosionPoint = args[1] local blastRadius = args[2] local triggeringWeapon = args[3] local preExplosionWeapons = args[4] for id, preData in pairs(preExplosionWeapons) do if tracked_weapons[id] and not tracked_weapons[id].wpn:isExist() then if preData.distance <= blastRadius then local msg = "WARNING: " .. preData.name .. " destroyed by large explosion from " .. triggeringWeapon .. " at " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", explosionPoint.x, explosionPoint.y, explosionPoint.z) gameMsg(msg) if splash_damage_options.debug then debugMsg(msg) end env.info(msg) if splash_damage_options.snap_to_ground_if_destroyed_by_large_explosion then local groundPos = { x = preData.pos.x, y = land.getHeight({x = preData.pos.x, y = preData.pos.z}), z = preData.pos.z } local destroyedWeaponPower, isShapedCharge = preData.explosive destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and tracked_weapons[id].cat == Weapon.Category.ROCKET then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.rocket_multiplier end if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.shaped_charge_multiplier end if splash_damage_options.debug then 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) end trigger.action.explosion(groundPos, destroyedWeaponPower) end end end end end, {explosionPoint, blastRadius, wpnData.name, preExplosionWeapons}, timer.getTime() + 0.2) end --recent_large_explosion_snap comes after main explosion and detect_ordnance_destruction if splash_damage_options.ordnance_protection and splash_damage_options.larger_explosions and splash_damage_options.recent_large_explosion_snap and splash_damage_options.snap_to_ground_if_destroyed_by_large_explosion and not (weaponData.Skip_larger_explosions or false) then local currentTime = timer.getTime() for id, data in pairs(tracked_weapons) do if id ~= wpn_id_ and not data.wpn:isExist() then local terrainHeight = land.getHeight({x = data.pos.x, y = data.pos.z}) local weaponHeight = data.pos.y - terrainHeight --Calculate height above ground local isMidAir = weaponHeight > 5 --Still checks if above ground local snapTriggered = false for _, explosion in ipairs(recentExplosions) do local timeDiff = currentTime - explosion.time local distance = getDistance3D(data.pos, explosion.pos) if splash_damage_options.debug then 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") end if timeDiff <= splash_damage_options.recent_large_explosion_time and distance <= splash_damage_options.recent_large_explosion_range then if isMidAir and weaponHeight <= splash_damage_options.max_snapped_height then --New height check local groundPos = { x = data.pos.x, y = terrainHeight, z = data.pos.z } local destroyedWeaponPower, isShapedCharge = getWeaponExplosive(data.name) destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.overall_scaling if splash_damage_options.rocket_multiplier and data.cat == Weapon.Category.ROCKET then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.rocket_multiplier end if splash_damage_options.apply_shaped_charge_effects and isShapedCharge then destroyedWeaponPower = destroyedWeaponPower * splash_damage_options.shaped_charge_multiplier end if splash_damage_options.debug then debugMsg("Weapon " .. data.name .. " detected recent large explosion within " .. splash_damage_options.recent_large_explosion_range .. "m and " .. splash_damage_options.recent_large_explosion_time .. "s, snapping to ground at X: " .. string.format("%.0f", groundPos.x) .. ", Y: " .. string.format("%.0f", groundPos.y) .. ", Z: " .. string.format("%.0f", groundPos.z) .. " with power " .. destroyedWeaponPower .. " (Height: " .. string.format("%.0f", weaponHeight) .. "m)") end trigger.action.explosion(groundPos, destroyedWeaponPower) snapTriggered = true table.insert(weaponsToRemove, id) break elseif isMidAir then if splash_damage_options.debug then debugMsg("Weapon " .. data.name .. " destroyed above max_snapped_height (" .. splash_damage_options.max_snapped_height .. "m) at " .. string.format("%.0f", weaponHeight) .. "m, skipping snap") end else if splash_damage_options.debug then 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") end snapTriggered = true break end end end if not snapTriggered then if isMidAir then if splash_damage_options.debug then debugMsg("Weapon " .. data.name .. " destroyed in air, but no recent large explosion within " .. splash_damage_options.recent_large_explosion_range .. "m or " .. splash_damage_options.recent_large_explosion_time .. "s") end else if splash_damage_options.debug then debugMsg("Weapon " .. data.name .. " impacted ground, not processed by recent large explosion settings") end end end end end local newExplosions = {} for _, explosion in ipairs(recentExplosions) do if currentTime - explosion.time <= splash_damage_options.recent_large_explosion_time then table.insert(newExplosions, explosion) end end recentExplosions = newExplosions end --Mark units as destroyed to avoid MiST accessing them local destroyedUnits = {} for _, target in ipairs(chosenTargets) do if target.unit:isExist() and target.health > 0 and target.unit:getLife() <= 0 then destroyedUnits[target.name] = true if splash_damage_options.debug then debugMsg("Marked " .. target.name .. " as destroyed pre-impact") end end end --Schedule explosion handling with original 0.1-second delay, enhanced error handling timer.scheduleFunction(function(args) local finalPos = args[1] local explosionPoint = args[2] local explosionPower = args[3] local isShapedCharge = args[4] local blastRadius = args[5] local chosenTargets = args[6] local weaponName = args[7] local wpnData = args[8] if splash_damage_options.debug then debugMsg("Starting impact handling for " .. weaponName .. " at " .. timer.getTime() .. "s") end local status, err = pcall(function() --Log pre-explosion targets --Sort pre-explosion targets by distance table.sort(chosenTargets, function(a, b) return a.distance < b.distance end) if splash_damage_options.track_pre_explosion then if #chosenTargets > 0 then local msg = "Targets in blast zone for " .. weaponName .. " BEFORE explosion (last frame, using finalPos):\n" for i, target in ipairs(chosenTargets) do msg = msg .. "- " .. target.name .. " (ID: " .. target.id .. ", Dist: " .. string.format("%.1f", target.distance) .. "m, Health: " .. target.health .. ")\n" end if splash_damage_options.debug then debugMsg(msg) env.info("SplashDamage Pre-Explosion (Last Frame): " .. msg) end else if splash_damage_options.debug then debugMsg("No targets in blast zone for " .. weaponName .. " BEFORE explosion (last frame)") env.info("SplashDamage Pre-Explosion (Last Frame): No targets in blast zone for " .. weaponName) end end end if not (weaponData.Skip_damage_model or false) then blastWave(explosionPoint, splash_damage_options.blast_search_radius, wpnData.name, explosionPower, isShapedCharge) elseif splash_damage_options.debug then debugMsg("Skipped damage model for '" .. wpnData.name .. "' due to Skip_damage_model = true in scheduled explosion handling") end --Post-explosion analysis if splash_damage_options.track_pre_explosion then timer.scheduleFunction(function(innerArgs) local impactPoint = innerArgs[1] local blastRadius = innerArgs[2] local preExplosionTargets = innerArgs[3] or {} local weaponName = innerArgs[4] local weaponPower = innerArgs[5] local playerName = innerArgs[6] if splash_damage_options.debug then debugMsg("Starting post-explosion analysis for " .. weaponName .. " at " .. timer.getTime() .. "s") end --Scan all units in wider radius local postExplosionTargets = {} local volS = { id = world.VolumeType.SPHERE, params = { point = impactPoint, radius = blastRadius } } local ifFound = function(foundObject) if foundObject:isExist() then local category = foundObject:getCategory() if (category == Object.Category.UNIT and (foundObject:getDesc().category == Unit.Category.GROUND_UNIT or foundObject:getDesc().category == Unit.Category.AIRPLANE)) or category == Object.Category.STATIC then local distance = getDistance(impactPoint, foundObject:getPoint()) table.insert(postExplosionTargets, { name = foundObject:getTypeName(), health = foundObject:getLife() or 0, position = foundObject:getPoint(), maxHealth = (category == Object.Category.UNIT and foundObject:getDesc().life) or foundObject:getLife() or 0, distance = distance, id = foundObject:getID(), unitName = foundObject:getName() or "Unknown", unit = foundObject }) end end return true end world.searchObjects({Object.Category.UNIT, Object.Category.STATIC}, volS, ifFound) --Sort post-explosion targets by distance table.sort(postExplosionTargets, function(a, b) return a.distance < b.distance end) local msg = "Post-explosion analysis for " .. weaponName .. ":\n" --Check for VehicleIEDTarget units if splash_damage_options.vehicleied_enabled then local targetNames = {} for name in splash_damage_options.vehicleied_targetname:gmatch("[^,]+") do targetNames[#targetNames + 1] = name:gsub("^%s*(.-)%s*$", "%1") --Trim whitespace end for _, preTarget in ipairs(preExplosionTargets) do for _, targetName in ipairs(targetNames) do if preTarget.unitName:find(targetName) then local found = false local postHealth = 0 for _, postTarget in ipairs(postExplosionTargets) do if preTarget.id == postTarget.id and getDistance(preTarget.position, postTarget.position) < 1 then found = true postHealth = postTarget.health break end end local isDamaged = postHealth < preTarget.maxHealth and postHealth > 0 local isDead = not found or (found and postHealth <= 0) local unitExists = preTarget.unit:isExist() if (splash_damage_options.vehicleied_explode_on_hit and isDamaged and unitExists) or (isDead and not unitExists) then if not processedUnitsGlobal then processedUnitsGlobal = {} end if not processedUnitsGlobal[preTarget.id] then processedUnitsGlobal[preTarget.id] = { id = preTarget.id, name = preTarget.unitName, type = preTarget.name, position = string.format("x=%.0f, y=%.0f, z=%.0f", preTarget.position.x, preTarget.position.y, preTarget.position.z), life = postHealth, event = "POST_EXPLOSION", time = timer.getTime() } local coords = { x = preTarget.position.x, y = preTarget.position.y, z = preTarget.position.z } if splash_damage_options.vehicleied_destroy_vehicle and unitExists then local status, err = pcall(function() preTarget.unit:destroy() end) if not status and splash_damage_options.vehicleied_debug then debugMsg("VehicleIEDTrigger: Failed to destroy unit " .. preTarget.unitName .. " (ID: " .. preTarget.id .. "): " .. tostring(err)) end end if splash_damage_options.vehicleied_debug then debugMsg("VehicleIEDTrigger: Unit " .. preTarget.unitName .. " (ID: " .. preTarget.id .. ") triggered in post-explosion, damaged: " .. tostring(isDamaged) .. ", exists: " .. tostring(unitExists) .. ", dead: " .. tostring(isDead) .. ", triggering VehicleIED") end VehicleIEDTrigger(coords, nil) end end end end end end --Match pre-detected units for _, preTarget in ipairs(preExplosionTargets) do local found = false local postHealth = 0 local postPosition = nil local postDistance = 0 local postUnit = nil local postId = nil local postUnitName = nil for _, postTarget in ipairs(postExplosionTargets) do if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then found = true postHealth = postTarget.health postPosition = postTarget.position postDistance = postTarget.distance postUnit = postTarget.unit postId = postTarget.id postUnitName = postTarget.unitName break end end local healthPercent = preTarget.maxHealth > 0 and (postHealth / preTarget.maxHealth * 100) or 0 local status = "" if not found or postHealth <= 0 then status = "WAS FULLY DESTROYED" elseif healthPercent < splash_damage_options.cargo_damage_threshold then status = "WAS DAMAGED BELOW THRESHOLD" -- Trigger effects for units below threshold if in cargoUnits if splash_damage_options.enable_cargo_effects and not processedCookoffs[preTarget.id] and cargoUnits[preTarget.name] then processedCookoffs[preTarget.id] = true debugCargoCookOff("Track_WPNs: Added unit ID " .. preTarget.id .. " to processedCookoffs, triggering effects") if not CargoCookoffPendingTable[preTarget.id] then CargoCookoffPendingTable[preTarget.id] = { id = preTarget.id, name = preTarget.unitName, type = preTarget.name, coords = preTarget.position, prevCoords = preTarget.position, unit = preTarget.unit, startTime = timer.getTime(), isCargoCookoff = true, isDead = postHealth <= 0 } debugCargoCookOff("Track_WPNs: Added unit ID " .. preTarget.id .. " to CargoCookoffPendingTable") end scheduleCargoEffects(preTarget.name, preTarget.unitName, preTarget.id, 0) end else status = "SURVIVED (Health: " .. postHealth .. ")" end --Killfeed logic if splash_damage_options.killfeed_enable and explTable[weaponName] and playerName ~= "unknown" then local status, isPlayer = pcall(function() local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.name == playerName then return true end end return false end) if status and isPlayer then table.insert(splashKillfeedTemp, { playerName = playerName, weaponName = weaponName, unitName = preTarget.unitName, unitType = preTarget.name, unitId = preTarget.id, time = timer.getTime(), position = postPosition or preTarget.position }) end end local coords = found and postPosition or preTarget.position msg = msg .. "- " .. preTarget.name .. " (ID: " .. preTarget.id .. ") " .. status .. " AT " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", coords.x, coords.y, coords.z) .. " (Dist: " .. string.format("%.1f", postDistance) .. "m, Pre: " .. preTarget.health .. ", Post: " .. postHealth .. ")\n" end --Check for additional units for _, postTarget in ipairs(postExplosionTargets) do local isPreDetected = false for _, preTarget in ipairs(preExplosionTargets) do if preTarget.name == postTarget.name and getDistance(preTarget.position, postTarget.position) < 1 then isPreDetected = true break end end if not isPreDetected then local coords = postTarget.position local healthPercent = postTarget.maxHealth > 0 and (postTarget.health / postTarget.maxHealth * 100) or 0 local status = postTarget.health <= 0 and "WAS FULLY DESTROYED" or (healthPercent < splash_damage_options.cargo_damage_threshold and "WAS DAMAGED BELOW THRESHOLD" or "SURVIVED (Health: " .. postTarget.health .. ")") -- Trigger effects for new units below threshold if in cargoUnits if splash_damage_options.enable_cargo_effects and status == "WAS DAMAGED BELOW THRESHOLD" and not processedCookoffs[postTarget.id] and cargoUnits[postTarget.name] then processedCookoffs[postTarget.id] = true debugCargoCookOff("Track_WPNs: Added unit ID " .. postTarget.id .. " to processedCookoffs, triggering effects") if not CargoCookoffPendingTable[postTarget.id] then CargoCookoffPendingTable[postTarget.id] = { id = postTarget.id, name = postTarget.unitName, type = postTarget.name, coords = postTarget.position, prevCoords = postTarget.position, unit = postTarget.unit, startTime = timer.getTime(), isCargoCookoff = true, isDead = postTarget.health <= 0 } debugCargoCookOff("Added unit ID " .. postTarget.id .. " to CargoCookoffPendingTable") end scheduleCargoEffects(postTarget.name, postTarget.unitName, postTarget.id, 0) end msg = msg .. "- " .. postTarget.name .. " " .. status .. " AT " .. string.format("X: %.0f, Y: %.0f, Z: %.0f", coords.x, coords.y, coords.z) .. " (Dist: " .. string.format("%.1f", postTarget.distance) .. "m, Pre: Unknown, Post: " .. postTarget.health .. ")\n" end end if splash_damage_options.debug then debugMsg(msg) env.info("SplashDamage Post-Explosion: " .. msg) end --Schedule splashKillFeed if there are entries if #splashKillfeedTemp > 0 and splash_damage_options.killfeed_enable then timer.scheduleFunction(splashKillFeed, {}, timer.getTime() + splash_damage_options.killfeed_splashdelay) end end, {finalPos, blastRadius, chosenTargets, weaponName, explosionPower, wpnData.init}, timer.getTime() + 1) end end) if not status then if splash_damage_options.debug then debugMsg("Impact handling error for '" .. weaponName .. "': " .. err) end end end, {explosionPoint, explosionPoint, explosionPower, isShapedCharge, blastRadius, chosenTargets, wpnData.name, wpnData}, timer.getTime() + 0.1) else if splash_damage_options.debug then debugMsg("Explosion skipped due to ordnance protection for '" .. wpnData.name .. "'") end if splash_damage_options.larger_explosions and not (weaponData.Skip_larger_explosions or false) then table.insert(recentExplosions, { pos = explosionPoint, time = timer.getTime(), radius = blastRadius }) if splash_damage_options.debug then debugMsg("Skipped explosion logged for snap check for '" .. wpnData.name .. "': X: " .. explosionPoint.x .. ", Y: " .. explosionPoint.y .. ", Z: " .. explosionPoint.z .. ", Time: " .. timer.getTime()) end elseif splash_damage_options.debug then debugMsg("Skipped recentExplosions logging for '" .. wpnData.name .. "' due to Skip_larger_explosions = true") end end table.insert(weaponsToRemove, wpn_id_) -- Ensure removal even if safeToBlast is false end table.insert(weaponsToRemove, wpn_id_) -- Ensure removal after processing impact end end) if not status then if splash_damage_options.debug then debugMsg("Error in track_wpns for '" .. (wpnData.name or "unknown weapon") .. "': " .. err) end table.insert(weaponsToRemove, wpn_id_) -- Remove weapon on error to prevent looping end end --Perform all removals after iteration for _, id in ipairs(weaponsToRemove) do tracked_weapons[id] = nil end return timer.getTime() + refreshRate end function onWpnEvent(event) if event.id == world.event.S_EVENT_SHOT then if event.weapon then local ordnance = event.weapon --verify isExist and getDesc local isValid = false local status, desc = pcall(function() return ordnance:isExist() and ordnance:getDesc() end) if status and desc then isValid = true end if not isValid then if splash_damage_options.debug then env.info("SplashDamage: Invalid weapon object in S_EVENT_SHOT") debugMsg("Invalid weapon object in S_EVENT_SHOT") end return end --Safely get typeName with pcall local status, typeName = pcall(function() return trim(ordnance:getTypeName()) end) if not status or not typeName then if splash_damage_options.debug then env.info("SplashDamage: Failed to get weapon typeName: " .. tostring(typeName)) debugMsg("Failed to get weapon typeName: " .. tostring(typeName)) end return end local playerName = "Unknown" if event.initiator then local status, playerNameResult = pcall(function() return event.initiator:getPlayerName() end) if status and playerNameResult then playerName = playerNameResult else local status, unitId = pcall(function() return event.initiator:getID() end) if status and unitId then local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.ucid and (tonumber(pinfo.slot) == unitId or pinfo.slot == event.initiator:getName()) then playerName = pinfo.name or "Unknown" break end end end end end if splash_damage_options.debug then env.info("Weapon [" .. typeName .. "] fired by player " .. playerName) debugMsg("Weapon [" .. typeName .. "] fired by player " .. playerName) end --Skip non-player weapons if only_players_weapons is enabled if splash_damage_options.only_players_weapons and playerName == "Unknown" then if splash_damage_options.debug then env.info("SplashDamage: Skipping non-player weapon [" .. typeName .. "]") debugMsg("Skipping non-player weapon [" .. typeName .. "]") end return end if splash_damage_options.napalmoverride_enabled then local napalmWeapons = {} for weapon in splash_damage_options.napalm_override_weapons:gmatch("[^,]+") do napalmWeapons[trim(weapon)] = true end if napalmWeapons[typeName] then isNapalm = true if splash_damage_options.debug then debugMsg("Tracking napalm override weapon: [" .. typeName .. "]") end end end if splash_damage_options.napalm_mk77_enabled and (typeName == "MK77mod0-WPN" or typeName == "MK77mod1-WPN") then isNapalm = true if splash_damage_options.debug then debugMsg("Tracking MK77 napalm weapon: [" .. typeName .. "]") end end if isNapalm then tracked_weapons[event.weapon.id_] = { wpn = ordnance, init = playerName, pos = ordnance:getPoint(), dir = ordnance:getPosition().x, name = typeName, speed = ordnance:getVelocity(), cat = ordnance:getCategory() } return end --Debug the exact typeName and explTable lookup if splash_damage_options.debug then debugMsg("Checking explTable for typeName: [" .. typeName .. "]") end local weaponData = explTable[typeName] if splash_damage_options.debug then if weaponData then debugMsg("Found in explTable: explosive=" .. weaponData.explosive .. ", groundordnance=" .. tostring(weaponData.groundordnance)) else debugMsg("Not found in explTable: [" .. typeName .. "]") end end --Handle ground ordnance explicitly if weaponData and weaponData.groundordnance then if splash_damage_options.track_groundunitordnance then --Count tracked ground ordnance local groundOrdnanceCount = 0 for _, wpnData in pairs(tracked_weapons) do if wpnData.isGroundUnitOrdnance then groundOrdnanceCount = groundOrdnanceCount + 1 end end if groundOrdnanceCount >= splash_damage_options.groundunitordnance_maxtrackedcount then if splash_damage_options.debug then debugMsg("Skipping tracking for " .. typeName .. ": ground ordnance limit reached (" .. groundOrdnanceCount .. "/" .. splash_damage_options.groundunitordnance_maxtrackedcount .. ")") env.info("SplashDamage: Skipping tracking for " .. typeName .. ": ground ordnance limit reached (" .. groundOrdnanceCount .. "/" .. splash_damage_options.groundunitordnance_maxtrackedcount .. ")") end return end if splash_damage_options.track_groundunitordnance_debug then debugMsg("Tracking ground unit ordnance: " .. typeName .. " fired by " .. (event.initiator and event.initiator:getTypeName() or "unknown")) env.info("SplashDamage: Tracking ground unit ordnance: " .. typeName .. " (" .. (event.initiator and event.initiator:getTypeName() or "no initiator") .. ")") end tracked_weapons[event.weapon.id_] = { wpn = ordnance, init = playerName, pos = ordnance:getPoint(), dir = ordnance:getPosition().x, name = typeName, speed = ordnance:getVelocity(), cat = ordnance:getCategory(), isGroundUnitOrdnance = true --Flag for ground ordnance } elseif splash_damage_options.track_groundunitordnance_debug then debugMsg("Event shot, but not tracking ground unit ordnance: " .. typeName) env.info("SplashDamage: event shot, but not tracking ground unit ordnance: " .. typeName .. " (" .. (event.initiator and event.initiator:getTypeName() or "no initiator") .. ")") end return end --Handle other tracked weapons in explTable if weaponData then if (ordnance:getDesc().category ~= 0) and event.initiator then if ordnance:getDesc().category == 1 then --Missiles if (ordnance:getDesc().MissileCategory ~= 1 and ordnance:getDesc().MissileCategory ~= 2) then --Exclude AAM and SAM tracked_weapons[event.weapon.id_] = { wpn = ordnance, init = playerName, pos = ordnance:getPoint(), dir = ordnance:getPosition().x, name = typeName, speed = ordnance:getVelocity(), cat = ordnance:getCategory() } end else --Rockets, bombs, etc. tracked_weapons[event.weapon.id_] = { wpn = ordnance, init = playerName, pos = ordnance:getPoint(), dir = ordnance:getPosition().x, name = typeName, speed = ordnance:getVelocity(), cat = ordnance:getCategory() } end end return --Exit after handling known weapons end --Handle unknown weapons or non-tracked shells if string.find(typeName, "weapons.shells") then if splash_damage_options.debug then debugMsg("Event shot, but not tracking: " .. typeName) env.info("SplashDamage: event shot, but not tracking: " .. typeName .. " (" .. (event.initiator and event.initiator:getTypeName() or "no initiator") .. ")") end return end --Log missing weapons env.info("SplashDamage: " .. typeName .. " missing from script (" .. (event.initiator and event.initiator:getTypeName() or "no initiator") .. ")") if splash_damage_options.weapon_missing_message then trigger.action.outText("SplashDamage: " .. typeName .. " missing from script (" .. (event.initiator and event.initiator:isExist() and event.initiator:getTypeName() or "no initiator") .. ")", 3) env.info("Current keys in explTable:") for k, v in pairs(explTable) do env.info("Key: [" .. k .. "]") end end end end end function splashKillFeed() if not splash_damage_options.killfeed_enable then return end local status, err = pcall(function() local tempTable = splashKillfeedTemp splashKillfeedTemp = {} local processedUnitIds = {} --Track unit IDs processed in this batch for _, entry in ipairs(tempTable) do local unitId = entry.unitId local unitName = entry.unitName local unitType = entry.unitType local playerName = entry.playerName local weaponName = entry.weaponName local position = entry.position --Skip if unitType is "Unknown" if unitType == "Unknown" then if splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: Skipped unit ID %s with unknown type at %.2f", unitId, timer.getTime())) end return end --Check if unit ID was already processed in this batch if processedUnitIds[unitId] then if splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: Skipped duplicate splash kill in batch for unit ID %s (%s) by %s with %s at %.2f", unitId, unitType, playerName, weaponName, timer.getTime())) end return --skip to next iteration end local unitExists = false local status, exists = pcall(function() local obj = Unit.getByName(unitName) or StaticObject.getByName(unitName) return obj and obj:isExist() end) if status and not exists then unitExists = false elseif status then unitExists = true else if splash_damage_options.killfeed_debug then env.info("SplashKillFeed: Error checking existence of unit ID " .. tostring(unitId) .. ": " .. tostring(exists)) end end if not unitExists then --Check if unit is in killfeedTable with "Unknown" killer local killfeedIndex = nil for i, killEntry in ipairs(killfeedTable) do if killEntry.unitID == unitId and killEntry.killer == "Unknown" then killfeedIndex = i break end end --Check if unit is in splashKillfeedTable local splashIndex = nil for i, splashEntry in ipairs(splashKillfeedTable) do if splashEntry.unitId == unitId then splashIndex = i break end end if killfeedIndex and playerName ~= "Unknown" then --Replace "Unknown" killfeed entry with splash kill table.remove(killfeedTable, killfeedIndex) if splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: Replaced Unknown killfeed entry for unit ID %s (%s) with splash kill by %s at %.2f", unitId, unitType, playerName, timer.getTime())) end elseif splashIndex then --Skip if already in splashKillfeedTable if splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: Skipped duplicate splash kill for unit ID %s (%s) by %s with %s at %.2f", unitId, unitType, playerName, weaponName, timer.getTime())) end return end if not splashIndex then local msg = string.format("%s destroyed by %s's %s Splash Damage", unitType, playerName, weaponName) if splash_damage_options.killfeed_game_messages then local status, err = pcall(function() trigger.action.outTextForCoalition(2, msg, splash_damage_options.killfeed_game_message_duration) end) if not status then trigger.action.outText(msg, splash_damage_options.killfeed_game_message_duration) if splash_damage_options.killfeed_debug then env.info("SplashKillFeed: Failed coalition message: " .. tostring(err)) end end end table.insert(splashKillfeedTable, { unitName = unitName, unitType = unitType, unitId = unitId, playerName = playerName, weaponName = weaponName, time = timer.getTime(), position = position }) if splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: %s destroyed by %s's %s Splash Damage [ID: %s] at %.2f", unitType, playerName, weaponName, unitId, timer.getTime())) end processedUnitIds[unitId] = true --Mark unit ID as processed end elseif splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: Unit ID %s (%s) still exists, skipping splash kill at %.2f", unitId, unitType, timer.getTime())) end end end) if not status and splash_damage_options.killfeed_debug then env.info("SplashKillFeed: Error: " .. tostring(err)) end end local function processSplashKillfeed() if not splash_damage_options.killfeed_enable or not splash_damage_options.killfeed_lekas_foothold_integration then if splash_damage_options.killfeed_debug then env.info("SplashDamage: processSplashKillfeed skipped") end return timer.getTime() + 60 end if not bc or type(bc) ~= "table" or not bc.addTempStat then if splash_damage_options.killfeed_debug then env.info("SplashDamage: bc is not accessible or missing addTempStat") end return timer.getTime() + 60 end local currentTime = timer.getTime() local entriesToRemove = {} local processedCount = 0 --Log bc table state before processing if splash_damage_options.killfeed_debug then env.info("SplashDamage: processSplashKillfeed started at " .. string.format("%.2f", currentTime)) env.info("SplashDamage: bc table state: " .. (bc and "exists" or "nil")) env.info("SplashDamage: bc.addTempStat: " .. (bc.addTempStat and "exists" or "nil")) env.info("SplashDamage: bc.context: " .. (bc.context and "exists" or "nil")) if bc.context then env.info("SplashDamage: bc.context.playerContributions: " .. (bc.context.playerContributions and "exists" or "nil")) if bc.context.playerContributions then env.info("SplashDamage: bc.context.playerContributions[2]: " .. (bc.context.playerContributions[2] and "exists" or "nil")) end end end for i, entry in ipairs(splashKillfeedTable) do if currentTime - entry.time >= splash_damage_options.killfeed_lekas_contribution_delay then local playerName = entry.playerName local unitType = entry.unitType local unitId = entry.unitId --Check if unitId exists in killfeedTable local inKillfeed = false for _, killEntry in ipairs(killfeedTable) do if killEntry.unitID == unitId then inKillfeed = true end end if inKillfeed then table.insert(entriesToRemove, i) if splash_damage_options.killfeed_debug then env.info(string.format("SplashDamage: Skipped processing and removed duplicate splash kill entry for unitId=%s, unitType=%s at %.2f", unitId, unitType, currentTime)) end else --Log entry details if splash_damage_options.killfeed_debug then env.info(string.format("SplashDamage: Processing splash kill entry %d: unitId=%s, unitType=%s, player=%s, time=%.2f", i, unitId, unitType, playerName, entry.time)) end local status, result = pcall(function() local statName = "Ground Units" local points = 10 if unitType:find("Plane") then statName = "Air" points = 30 elseif unitType:find("Helicopter") then statName = "Helo" points = 30 elseif unitType:find("SAM") then statName = "SAM" points = 30 elseif unitType:find("Infantry") then statName = "Infantry" points = 10 elseif unitType:find("Ship") then statName = "Ship" points = 250 elseif unitType:find("Building") then statName = "Structure" points = 30 end bc:addTempStat(playerName, statName, 1) if splash_damage_options.killfeed_debug then env.info(string.format("SplashDamage: Added temp stat for %s: stat=%s, count=1", playerName, statName)) end if bc.context and type(bc.context) == "table" and bc.context.playerContributions and type(bc.context.playerContributions) == "table" then bc.context.playerContributions[2] = bc.context.playerContributions[2] or {} local oldPoints = bc.context.playerContributions[2][playerName] or 0 bc.context.playerContributions[2][playerName] = oldPoints + points if splash_damage_options.killfeed_debug then env.info(string.format("SplashDamage: Updated contributions for %s: old=%d, new=%d, added=%d", playerName, oldPoints, bc.context.playerContributions[2][playerName], points)) end else if splash_damage_options.killfeed_debug then env.info("SplashDamage: Skipped contribution update for " .. playerName .. ": bc.context or bc.context.playerContributions is nil") end end processedCount = processedCount + 1 if splash_damage_options.killfeed_debug then env.info(string.format("SplashDamage: Processed splash kill for %s by %s: stat=%s, points=%d, unitId=%s", unitType, playerName, statName, points, unitId)) end end) if not status and splash_damage_options.killfeed_debug then env.info("SplashDamage: Error processing splash kill for unitId=" .. tostring(unitId) .. ": " .. tostring(result)) end table.insert(entriesToRemove, i) end end end for i = #entriesToRemove, 1, -1 do table.remove(splashKillfeedTable, entriesToRemove[i]) end if splash_damage_options.killfeed_debug then if bc.tempStats and type(bc.tempStats) == "table" then env.info("SplashDamage: tempStats contents:") for playerName, stats in pairs(bc.tempStats) do local statStr = "" for statKey, value in pairs(stats) do statStr = statStr .. statKey .. "=" .. tostring(value) .. ", " end env.info("SplashDamage: " .. playerName .. ": " .. (statStr ~= "" and statStr or "empty")) end if not next(bc.tempStats) then env.info("SplashDamage: tempStats is empty") end else env.info("SplashDamage: bc.tempStats is nil or not a table") end end if splash_damage_options.killfeed_debug and processedCount > 0 then env.info("SplashDamage: Processed " .. processedCount .. " splash kills, remaining: " .. #splashKillfeedTable) end return timer.getTime() + 60 end --Scan for Strobe units local function scanStrobeUnits() strobeUnits = {} for coa = 0, 2 do local groups = coalition.getGroups(coa) if groups then for _, group in pairs(groups) do local units = group:getUnits() if units then for _, unit in ipairs(units) do if unit:isExist() then local unitName = unit:getName() if unitName:find("Strobe") then table.insert(strobeUnits, { unit = unit, name = unitName, id = unit:getID() }) debugStrobeMarker("Found Strobe unit: " .. unitName .. " (ID: " .. unit:getID() .. ")") end end end end end end end debugStrobeMarker("Total Strobe units found: " .. #strobeUnits) end --StrobeMarker function local function triggerStrobeMarker() if not splash_damage_options.StrobeMarker_allstrobeunits then debugStrobeMarker("StrobeMarker disabled, skipping execution") return timer.getTime() + splash_damage_options.StrobeMarker_interval end local explosionPower = 0.000001 --Minimal explosion strength local heightOffset = 3 --Height above unit in meters for _, strobeData in ipairs(strobeUnits) do local unit = strobeData.unit local status, err = pcall(function() if unit:isExist() and unit:isActive() and not unit:getDesc().isInvisible then local life = unit:getLife() or 0 if life > 0 then local pos = unit:getPosition().p pos.y = pos.y + heightOffset --[[pos.z = pos.z + heightOffset pos.x = pos.x + heightOffset]]-- debugStrobeMarker("Triggering explosion for unit " .. strobeData.name .. " (ID: " .. strobeData.id .. ") at X: " .. pos.x .. ", Y: " .. pos.y .. ", Z: " .. pos.z .. " with power " .. explosionPower) trigger.action.explosion(pos, explosionPower) --[[ local pos = unit:getPosition().p pos.y = pos.y + heightOffset pos.z = pos.z - heightOffset pos.x = pos.x - heightOffset trigger.action.explosion(pos, explosionPower) local pos = unit:getPosition().p pos.y = pos.y + heightOffset pos.z = pos.z - heightOffset pos.x = pos.x + heightOffset trigger.action.explosion(pos, explosionPower) local pos = unit:getPosition().p pos.y = pos.y + heightOffset pos.z = pos.z + heightOffset pos.x = pos.x - heightOffset trigger.action.explosion(pos, explosionPower)]]-- else debugStrobeMarker("Skipping unit " .. strobeData.name .. " (ID: " .. strobeData.id .. "): Unit is dead (life: " .. life .. ")") end else debugStrobeMarker("Skipping unit " .. strobeData.name .. " (ID: " .. strobeData.id .. "): Not exist, inactive, or invisible") end end) if not status then debugStrobeMarker("Error triggering explosion for unit " .. strobeData.name .. " (ID: " .. strobeData.id .. "): " .. tostring(err)) end end return timer.getTime() + splash_damage_options.StrobeMarker_interval end --Function for individual unit strobing local function triggerIndividualStrobe(unitId) local strobeData = individualStrobeUnits[unitId] if not strobeData or not strobeData.enabled then return timer.getTime() + (strobeData and strobeData.interval or splash_damage_options.StrobeMarker_interval) end local unit = strobeData.unit local status, err = pcall(function() if unit:isExist() and unit:isActive() and not unit:getDesc().isInvisible then local life = unit:getLife() or 0 if life > 0 then local pos = unit:getPosition().p local explosionPower = 0.000001 local heightOffset = 3 pos.y = pos.y + heightOffset pos.z = pos.z + heightOffset pos.x = pos.x + heightOffset trigger.action.explosion(pos, explosionPower) pos = unit:getPosition().p pos.y = pos.y + heightOffset pos.z = pos.z - heightOffset pos.x = pos.x - heightOffset trigger.action.explosion(pos, explosionPower) pos = unit:getPosition().p pos.y = pos.y + heightOffset pos.z = pos.z - heightOffset pos.x = pos.x + heightOffset trigger.action.explosion(pos, explosionPower) pos = unit:getPosition().p pos.y = pos.y + heightOffset pos.z = pos.z + heightOffset pos.x = pos.x - heightOffset trigger.action.explosion(pos, explosionPower) end end end) if not status then debugStrobeMarker("Error triggering individual strobe for unit ID " .. unitId .. ": " .. tostring(err)) end return timer.getTime() + strobeData.interval end --toggle individual strobe local function toggleIndividualStrobe(args) local unitId, enable = args.unitId, args.enable if individualStrobeUnits[unitId] then individualStrobeUnits[unitId].enabled = enable if enable then timer.scheduleFunction(triggerIndividualStrobe, unitId, timer.getTime() + individualStrobeUnits[unitId].interval) end debugStrobeMarker("Strobe for unit ID " .. unitId .. " " .. (enable and "enabled" or "disabled")) end end --set individual strobe interval local function setIndividualStrobeInterval(args) local unitId, interval = args.unitId, args.interval if individualStrobeUnits[unitId] then individualStrobeUnits[unitId].interval = interval if individualStrobeUnits[unitId].enabled then timer.scheduleFunction(triggerIndividualStrobe, unitId, timer.getTime() + interval) end debugStrobeMarker("Strobe interval for unit ID " .. unitId .. " set to " .. interval .. " seconds") end end --Create strobe radio menu local function createStrobeRadioMenu() missionCommands.removeItem({"Strobe Control"}) local mainMenu = missionCommands.addSubMenu("Strobe Control") --Scan for Strobe/Beacon units local strobeTargets = {} local function processObject(obj) if obj:isExist() then local name = obj:getName() if string.find(name:lower(), "strobe") or string.find(name:lower(), "beacon") then table.insert(strobeTargets, {name = name, id = obj:getID(), unit = obj}) debugStrobeMarker("Found Strobe/Beacon unit: " .. name) end end end for coa = 0, 2 do local groups = coalition.getGroups(coa) if groups then for _, group in pairs(groups) do local units = group:getUnits() if units then for _, unit in pairs(units) do processObject(unit) end end end end local statics = coalition.getStaticObjects(coa) if statics then for _, static in pairs(statics) do processObject(static) end end end if #strobeTargets == 0 then --("No Strobe or Beacon units found!") return end --Create menu for each found unit for _, target in ipairs(strobeTargets) do local unitId = target.id --Initialize individual strobe unit if not already if not individualStrobeUnits[unitId] then individualStrobeUnits[unitId] = { unit = target.unit, enabled = false, interval = splash_damage_options.StrobeMarker_interval, } end local unitMenu = missionCommands.addSubMenu(target.name, mainMenu) missionCommands.addCommand("Enable Strobe", unitMenu, toggleIndividualStrobe, {unitId = unitId, enable = true}) missionCommands.addCommand("Disable Strobe", unitMenu, toggleIndividualStrobe, {unitId = unitId, enable = false}) local intervalMenu = missionCommands.addSubMenu("Set Interval", unitMenu) for _, interval in ipairs({1, 2, 3, 5, 10}) do missionCommands.addCommand(interval .. " seconds", intervalMenu, setIndividualStrobeInterval, {unitId = unitId, interval = interval}) end end end --Initialize individual strobe units local function initIndividualStrobeUnits() individualStrobeUnits = {} for _, strobeData in ipairs(strobeUnits) do individualStrobeUnits[strobeData.id] = { unit = strobeData.unit, enabled = false, interval = splash_damage_options.StrobeMarker_interval, } end end --Function to trigger CriticalComponent explosion function CriticalComponent(coords, weaponName, initiator, unitName, unitID, unitType) if not splash_damage_options.CriticalComponent then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Disabled, skipping explosion at X: " .. (coords.x or "nil") .. ", Z: " .. (coords.z or "nil")) end return end if not coords or not coords.x or not coords.y or not coords.z then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Invalid coordinates, skipping explosion") end return end if splash_damage_options.CriticalComponent_Specific_Weapons_Only and #splash_damage_options.CriticalComponent_Specific_Weapons_Only > 0 then local validWeapon = false for _, wpn in ipairs(splash_damage_options.CriticalComponent_Specific_Weapons_Only) do if weaponName == wpn then validWeapon = true break end end if not validWeapon then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Weapon " .. (weaponName or "nil") .. " not in CriticalComponent_Specific_Weapons_Only, skipping") end return end end local chance = splash_damage_options.CriticalComponent_Chance or 0.1 if math.random() >= chance then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Chance check failed") end return end local explosionPower = splash_damage_options.CriticalComponent_Explosion_Power or 100 if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Triggering explosion at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. " with power " .. explosionPower .. " for unit " .. unitName .. " (ID: " .. unitID .. ")") end trigger.action.explosion(coords, explosionPower) --Killfeed check for the hit unit, commenting out as it looks like it works already --[[if splash_damage_options.killfeed_enable then local playerName = "Unknown" if initiator then local status, playerNameResult = pcall(function() return initiator:getPlayerName() end) if status and playerNameResult then playerName = playerNameResult else local status, initiatorId = pcall(function() return initiator:getID() end) if status and initiatorId then local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.ucid and (tonumber(pinfo.slot) == initiatorId or pinfo.slot == initiator:getName()) then playerName = pinfo.name or "Unknown" break end end end end end if playerName ~= "Unknown" then local status, isPlayer = pcall(function() local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.name == playerName then return true end end return false end) if status and isPlayer then timer.scheduleFunction(function() local unit = Unit.getByName(unitName) local isAlive = unit and unit:isExist() and safeGet(function() return unit:getLife() end, 0) > 0 if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Killfeed check for unit " .. unitName .. " (ID: " .. unitID .. "), alive: " .. tostring(isAlive)) end if not isAlive then local alreadyInKillfeed = false for _, entry in ipairs(splashKillfeedTable) do if entry.unitId == unitID then alreadyInKillfeed = true break end end if not alreadyInKillfeed then table.insert(splashKillfeedTemp, { unitName = unitName, unitType = unitType or "unknown", unitId = unitID, playerName = playerName, weaponName = weaponName or "Critical Component", time = timer.getTime(), position = coords }) if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Added to splashKillfeed: " .. unitName .. " destroyed by " .. playerName) end if splash_damage_options.killfeed_game_messages then local msg = string.format("%s destroyed by %s's %s Splash Damage", unitType or "unknown", playerName, weaponName or "Critical Component") local status, err = pcall(function() trigger.action.outTextForCoalition(2, msg, splash_damage_options.killfeed_game_message_duration) end) if not status then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Error displaying killfeed message: " .. tostring(err)) end end end end end if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: splashKillfeedTable size: " .. #splashKillfeedTable) end end, {}, timer.getTime() + 1) -- 1-second delay for killfeed check end end end--]] end --Function to trigger A-10 Murder Mode explosion function A10MurderMode(coords) if not splash_damage_options.A10MurderMode then if splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Disabled, skipping explosion at X: " .. (coords.x or "nil") .. ", Z: " .. (coords.z or "nil")) end return end if not coords or not coords.x or not coords.y or not coords.z then if splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Invalid coordinates, skipping explosion") end return end if splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Triggering explosion at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. " with power " .. splash_damage_options.A10MurderMode_Power) end trigger.action.explosion(coords, splash_damage_options.A10MurderMode_Power) end --Function to trigger Named Unit Murder Mode explosion function NamedUnitMurderMode(coords) if not splash_damage_options.NamedUnitMurderMode then if splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Disabled, skipping explosion at X: " .. (coords.x or "nil") .. ", Z: " .. (coords.z or "nil")) end return end if not coords or not coords.x or not coords.y or not coords.z then if splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Invalid coordinates, skipping explosion") end return end if splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Triggering explosion at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. " with power " .. splash_damage_options.NamedUnitMurderMode_Power) end trigger.action.explosion(coords, splash_damage_options.NamedUnitMurderMode_Power) end --Function to process the spawn queue for vehicleIED local function vehicleIEDprocessSpawnQueue() if #fuelTankSpawnQueue == 0 then return end local currentTime = timer.getTime() if currentTime < lastSpawnTime + SPAWN_INTERVAL then --Schedule next check timer.scheduleFunction(vehicleIEDprocessSpawnQueue, {}, currentTime + SPAWN_INTERVAL / 2) return end --Pop the next spawn task local task = table.remove(fuelTankSpawnQueue, 1) lastSpawnTime = currentTime --Execute the spawn local coords, iedName = task.coords, task.iedName if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Spawning fuel tank at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) end local owngroupID = math.random(9999, 99999) local cvnunitID = math.random(9999, 99999) local _dataFuel = { ["groupId"] = owngroupID, ["category"] = "Fortifications", ["shape_name"] = "toplivo-bak", ["type"] = "Fuel tank", ["unitId"] = cvnunitID, ["rate"] = 100, ["y"] = coords.z, ["x"] = coords.x, ["name"] = iedName, ["heading"] = 0, ["dead"] = false, ["hidden"] = true, } --Attempt to spawn at original coordinates with vertical offset _dataFuel.y = coords.z _dataFuel.x = coords.x local spawnY = land.getHeight({x = coords.x, y = coords.z}) + 0.5 _dataFuel.position = {x = coords.x, y = spawnY, z = coords.z} local status, result = pcall(function() return coalition.addStaticObject(coalition.side.BLUE, _dataFuel) end) local spawnSuccess = status and result and StaticObject.getByName(iedName) and StaticObject.getByName(iedName):isExist() --Log spawn result and surface type if splash_damage_options.vehicleied_debug then local surfaceType = land.getSurfaceType({x = coords.x, y = coords.z}) env.info("VehicleIEDTrigger: Spawn surface type: " .. tostring(surfaceType)) env.info("VehicleIEDTrigger: Fuel tank spawn attempt at original coords - " .. (spawnSuccess and "succeeded" or "failed")) end --If spawn fails, try shifting 1 meter in four directions if not spawnSuccess then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Failed to spawn fuel tank at original coords, attempting 1m offsets") end local offsets = { {x = coords.x + 1, z = coords.z}, {x = coords.x - 1, z = coords.z}, {x = coords.x, z = coords.z + 1}, {x = coords.x, z = coords.z - 1} } for i, offset in ipairs(offsets) do _dataFuel.x = offset.x _dataFuel.y = offset.z _dataFuel.position = {x = offset.x, y = land.getHeight({x = offset.x, y = offset.z}) + 0.5, z = offset.z} _dataFuel.name = iedName .. "_offset" .. i status, result = pcall(function() return coalition.addStaticObject(coalition.side.BLUE, _dataFuel) end) spawnSuccess = status and result and StaticObject.getByName(_dataFuel.name) and StaticObject.getByName(_dataFuel.name):isExist() if spawnSuccess then coords.x = offset.x coords.z = offset.z iedName = _dataFuel.name if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Successfully spawned fuel tank at offset #" .. i .. " (X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. ")") end break end end end if not spawnSuccess and splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Failed to spawn fuel tank after all attempts") end --Schedule destruction with logging if spawnSuccess then timer.scheduleFunction(function(name) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Attempting to destroy fuel tank " .. name) end local staticObj = StaticObject.getByName(name) if staticObj then local status, err = pcall(function() staticObj:destroy() end) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Fuel tank " .. name .. " destruction - " .. (status and "succeeded" or "failed: " .. tostring(err))) end else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Fuel tank " .. name .. " not found for destruction") end end end, iedName, timer.getTime() + 0.5) end --Schedule next spawn if queue is not empty if #fuelTankSpawnQueue > 0 then timer.scheduleFunction(vehicleIEDprocessSpawnQueue, {}, timer.getTime() + SPAWN_INTERVAL) end end function VehicleIEDTrigger(coords, unit) if not splash_damage_options.vehicleied_enabled then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Disabled, skipping explosion at X: " .. (coords.x or "nil") .. ", Y: " .. (coords.y or "nil") .. ", Z: " .. (coords.z or "nil")) end return end if not coords or not coords.x or not coords.y or not coords.z then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Invalid coordinates, skipping explosion") end return end local scaling = splash_damage_options.vehicleied_scaling or 1 --Get unit name and ID for logging and tracking local unitName = unit and unit:isExist() and safeGet(function() return unit:getName() end, "unknown") or "unknown" local unitId = unit and unit:isExist() and safeGet(function() return unit:getID() end, "unknown") or "unknown" local initialHealth = unit and unit:isExist() and safeGet(function() return unit:getLife() end, 0) or 0 if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Processing at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. " with " .. splash_damage_options.vehicleied_explosion_count_max .. " max explosions, central power: " .. (splash_damage_options.vehicleied_central_power * scaling) .. ", fuel tank spawn: " .. tostring(splash_damage_options.vehicleied_fueltankspawn) .. ", scaling: " .. scaling .. ", unit: " .. unitName .. " (ID: " .. unitId .. "), initial health: " .. initialHealth) end --Prepare fuel tank data if spawning is enabled local iedName = "IED_FuelTank_" .. tostring(timer.getTime()) if splash_damage_options.vehicleied_fueltankspawn then table.insert(fuelTankSpawnQueue, {coords = coords, iedName = iedName, unitName = unitName}) if #fuelTankSpawnQueue == 1 then vehicleIEDprocessSpawnQueue() end end --Handle special case: explosioncount = 0, fueltankspawn = true if splash_damage_options.vehicleied_explosion_count_min == 0 and splash_damage_options.vehicleied_fueltankspawn then local spawnSuccess = false --Note: This is a limitation; spawnSuccess isn't set yet due to async queue if spawnSuccess then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Fuel tank spawned for unit " .. unitName .. ", triggering single explosion with power 10 at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z) end local explosionPoint = {x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}) + 1.6, z = coords.z} timer.scheduleFunction(function(pos) trigger.action.explosion(pos, 10) --Fixed power of 10 end, explosionPoint, timer.getTime() + 0.01) timer.scheduleFunction(function(name) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Destroying fuel tank " .. name .. " for unit " .. unitName) end local staticObj = StaticObject.getByName(name) if staticObj then staticObj:destroy() else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Fuel tank " .. name .. " not found for destruction for unit " .. unitName) end end end, iedName, timer.getTime() + 0.02) else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Failed to spawn fuel tank for unit " .. unitName .. ", triggering direct explosion with power 10") end local explosionPoint = {x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}) + 1.6, z = coords.z} trigger.action.explosion(explosionPoint, 10) --Direct explosion with power 10 end return end --Generate explosion points local explosionPoints = {} local baseMinCount = splash_damage_options.vehicleied_explosion_count_min local baseMaxCount = splash_damage_options.vehicleied_explosion_count_max local explosionCount = math.random(math.floor(baseMinCount * scaling), math.floor(baseMaxCount * scaling)) if explosionCount > 0 then --Central explosion local centralPoint = { x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}) + 0.1, z = coords.z } local centralPointGroundLevel = { x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}), z = coords.z } table.insert(explosionPoints, {point = centralPoint, power = splash_damage_options.vehicleied_central_power * scaling, delay = 0.011}) table.insert(explosionPoints, {point = centralPointGroundLevel, power = splash_damage_options.vehicleied_central_power * scaling, delay = 0.01}) --Secondary explosions with Gaussian distribution for i = 1, explosionCount do local offsetX = gaussRandom(0, (splash_damage_options.vehicleied_radius * scaling) / 2) * (1 + (math.random() - 0.5) * 0.1) local offsetZ = gaussRandom(0, (splash_damage_options.vehicleied_radius * scaling) / 2) * (1 + (math.random() - 0.5) * 0.1) local point = { x = coords.x + offsetX, y = land.getHeight({x = coords.x + offsetX, y = coords.z + offsetZ}) + 0.3, z = coords.z + offsetZ } local basePower = splash_damage_options.vehicleied_explosion_power local power = (basePower * scaling) * (1 + (math.random() - 0.5) * splash_damage_options.vehicleied_power_variance) local delay = math.random() * splash_damage_options.vehicleied_explosion_delay_max table.insert(explosionPoints, {point = point, power = power, delay = delay}) end end --Trigger explosions if #explosionPoints > 0 then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Scheduling " .. #explosionPoints .. " explosions for unit " .. unitName .. " (ID: " .. unitId .. ")") end for i, entry in ipairs(explosionPoints) do if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Scheduling explosion #" .. i .. " at X: " .. entry.point.x .. ", Y: " .. entry.point.y .. ", Z: " .. entry.point.z .. " with power " .. entry.power .. " and delay " .. entry.delay) end timer.scheduleFunction(function(params) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Callback started for explosion #" .. tostring(params[3]) .. " at time " .. timer.getTime()) end local status, err = pcall(function() if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Validating params for explosion #" .. tostring(params[3]) .. ": point=" .. (params[1] and "table" or "nil") .. ", power=" .. tostring(params[2]) .. ", index=" .. tostring(params[3])) end if not params[1] or not params[1].x or not params[1].y or not params[1].z then error("Invalid point parameter: " .. tostring(params[1])) end if not params[2] or type(params[2]) ~= "number" then error("Invalid power parameter: " .. tostring(params[2])) end if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Triggering explosion #" .. params[3] .. " at X: " .. params[1].x .. ", Y: " .. params[1].y .. ", Z: " .. params[1].z .. " with power " .. params[2]) end trigger.action.explosion(params[1], params[2]) end) if not status and splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Error in explosion callback #" .. tostring(params[3]) .. ": " .. tostring(err)) end end, {entry.point, entry.power, i}, timer.getTime() + entry.delay) end else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: No explosion points generated, triggering single fallback explosion for unit " .. unitName .. " (ID: " .. unitId .. ")") end local point = {x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}), z = coords.z} trigger.action.explosion(point, splash_damage_options.vehicleied_central_power * scaling) --Apply scaling end --Check if unit still exists and schedule another explosion if it does if unit and unitId ~= "unknown" then local function checkUnitExistence(params) local unit = params.unit local coords = params.coords local unitName = params.unitName local unitId = params.unitId local attempt = params.attempt local maxAttempts = 5 --Limit to prevent infinite loops local unitExists = unit and unit:isExist() local currentHealth = unitExists and safeGet(function() return unit:getLife() end, 0) or 0 if unitExists then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " (ID: " .. unitId .. ") still exists after attempt " .. attempt .. ", health: " .. currentHealth .. ", scheduling additional explosion") end if attempt >= maxAttempts then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Max attempts (" .. maxAttempts .. ") reached for unit " .. unitName .. " (ID: " .. unitId .. "), stopping further explosions") end return end if not processedUnitsGlobal then processedUnitsGlobal = {} end if not processedUnitsGlobal[unitId] then processedUnitsGlobal[unitId] = { id = unitId, name = unitName, type = unit and safeGet(function() return unit:getTypeName() end, "unknown") or "unknown", position = string.format("x=%.0f, y=%.0f, z=%.0f", coords.x, coords.y, coords.z), life = currentHealth, event = "REPEAT_EXPLOSION", time = timer.getTime() } end --Attempt to destroy the unit if splash_damage_options.vehicleied_destroy_vehicle then local status, err = pcall(function() unit:destroy() end) if not status and splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Failed to destroy unit " .. unitName .. " (ID: " .. unitId .. "): " .. tostring(err)) end end --Trigger a single high-power explosion at the unit's location local enhancedPower = splash_damage_options.vehicleied_central_power * scaling * (2 + attempt * 0.5) --Increase power significantly (2x + 50% per attempt) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Triggering additional explosion for unit " .. unitName .. " (ID: " .. unitId .. ") at X: " .. coords.x .. ", Y: " .. coords.y .. ", Z: " .. coords.z .. " with enhanced power " .. enhancedPower) end local point = {x = coords.x, y = land.getHeight({x = coords.x, y = coords.z}) + 0.1, z = coords.z} trigger.action.explosion(point, enhancedPower) --Schedule another check timer.scheduleFunction(checkUnitExistence, { unit = unit, coords = coords, unitName = unitName, unitId = unitId, attempt = attempt + 1 }, timer.getTime() + 0.5) else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " (ID: " .. unitId .. ") no longer exists after attempt " .. attempt .. ", health: " .. currentHealth) end end end timer.scheduleFunction(checkUnitExistence, { unit = unit, coords = coords, unitName = unitName, unitId = unitId, attempt = 1 }, timer.getTime() + 0.5) end end --Function for CBU Bomblet Additional Explosion function CBUBombletHitExplosion(coords, unitName, unitID, weaponName, weaponID, submunitionPower, initiator) if not splash_damage_options.CBU_Bomblet_Hit_Explosion then debugCBUBombletHit("CBUBomblet: Disabled, skipping explosion at X: " .. (coords.x or "nil") .. ", Z: " .. (coords.z or "nil") .. " for unit " .. unitName .. " (ID: " .. unitID .. ")") return end if not coords or not coords.x or not coords.y or not coords.z then debugCBUBombletHit("Invalid coordinates, skipping explosion for unit " .. unitName .. " (ID: " .. unitID .. ")") return end local explosionPower = (submunitionPower or 1) * splash_damage_options.CBU_Bomblet_Hit_Explosion_Scaling * splash_damage_options.overall_scaling local key = unitID .. "-" .. weaponID local explosionHeight = splash_damage_options.CBU_Bomblet_Explosion_Height or 1.6 --Default to 1.6m local adjustedCoords = { x = coords.x, y = land.getHeight({x = coords.x, z = coords.z}) + explosionHeight, z = coords.z } --Mimic spread if enabled if splash_damage_options.CBU_Bomblet_Hit_Mimic_Spread then local scanRadius = splash_damage_options.CBU_Bomblet_Hit_Spread local secondaryScanRadius = splash_damage_options.CBU_Bomblet_Hit_Spread_SecondaryScan or scanRadius local spreadDuration = splash_damage_options.CBU_Bomblet_Hit_Spread_Duration or 2 local volS = { id = world.VolumeType.SPHERE, params = { point = adjustedCoords, radius = scanRadius } } local foundUnits = {} local seenUnitIDs = {} --Track unique unit IDs to avoid duplicates local ifFound = function(obj) local targetUnitID = safeGet(function() return obj:getID() end, "unavailable") local targetUnitName = safeGet(function() return obj:getName() end, "unknown") local targetUnitType = safeGet(function() return obj:getTypeName() end, "unknown") local targetCoords = safeGet(function() return obj:getPosition().p end, nil) local targetHealth = safeGet(function() return obj:getLife() end, 0) local targetAttrs = safeGet(function() return obj:getDesc().attributes end, {}) if targetUnitID ~= "unavailable" and targetCoords and not seenUnitIDs[targetUnitID] then seenUnitIDs[targetUnitID] = true local distance = math.sqrt((coords.x - targetCoords.x)^2 + (coords.z - targetCoords.z)^2) table.insert(foundUnits, {id = targetUnitID, name = targetUnitName, type = targetUnitType, coords = targetCoords, health = targetHealth, distance = distance, attributes = targetAttrs}) end end debugCBUBombletHit("Primary scan for objects within " .. scanRadius .. "m radius") world.searchObjects(Object.Category.UNIT, volS, ifFound) world.searchObjects(Object.Category.STATIC, volS, ifFound) world.searchObjects(Object.Category.SCENERY, volS, ifFound) world.searchObjects(Object.Category.CARGO, volS, ifFound) --Secondary scans for each found unit for _, unit in ipairs(foundUnits) do local secondaryVolS = { id = world.VolumeType.SPHERE, params = { point = unit.coords, radius = secondaryScanRadius } } debugCBUBombletHit("Secondary widening scan within " .. scanRadius .. "m radius of found units") world.searchObjects(Object.Category.UNIT, secondaryVolS, ifFound) world.searchObjects(Object.Category.STATIC, secondaryVolS, ifFound) world.searchObjects(Object.Category.SCENERY, secondaryVolS, ifFound) world.searchObjects(Object.Category.CARGO, secondaryVolS, ifFound) end --Sort by distance and display scan results table.sort(foundUnits, function(a, b) return a.distance < b.distance end) local unitDetails = {} for _, unit in ipairs(foundUnits) do table.insert(unitDetails, string.format("%s (ID: %s, Type: %s, Health: %.1f, Distance: %.1fm)", unit.name, unit.id, unit.type, unit.health, unit.distance)) end debugCBUBombletHit("CBUBomblet: Found " .. #foundUnits .. " unique objects: " .. (#unitDetails > 0 and table.concat(unitDetails, ", ") or "none")) --Schedule explosions for found units for i, unit in ipairs(foundUnits) do local key = unit.id .. "-" .. weaponID if not cbuProcessed[key] then if math.random() <= splash_damage_options.CBU_Bomblet_Hit_Chance then debugCBUBombletHit("CBUBomblet: hit chance passed for unit " .. unit.name .. " (ID: " .. unit.id .. ")") local isIndirect = math.random() < splash_damage_options.CBU_Bomblet_Indirect_Hit_Chance local indirectMod = isIndirect and splash_damage_options.CBU_Bomblet_Indirect_Dmg_Modifier or 1.0 debugCBUBombletHit("CBUBomblet: Indirect hit check: " .. (isIndirect and "Indirect (by def 30% chance, 50% damage)" or "Direct (100% damage)") .. " for spread unit " .. unit.name) local armorMod = 1.0 local armorType = "Unknown" if unit.attributes["NonArmoredUnits"] then armorMod = splash_damage_options.CBU_Bomblet_NonArmored_Dmg_Modifier armorType = "NonArmored" elseif unit.attributes["LightArmoredUnits"] or unit.attributes["NonAndLightArmoredUnits"] then armorMod = splash_damage_options.CBU_Bomblet_LightlyArmored_Dmg_Modifier armorType = "LightlyArmored" elseif unit.attributes["ArmoredUnits"] or unit.attributes["Tanks"] then armorMod = splash_damage_options.CBU_Bomblet_Armored_Dmg_Modifier armorType = "Armored" end debugCBUBombletHit("CBUBomblet: unit " .. unit.name .. " identified as " .. armorType .. " with armor modifier " .. armorMod) local finalPower = explosionPower * indirectMod * armorMod if unit.attributes["Infantry"] then finalPower = 1 --Set explosion power to 1 for infantry explosionHeight = 2 end cbuProcessed[key] = true local adjustedUnitCoords = { x = unit.coords.x, y = land.getHeight({x = unit.coords.x, z = unit.coords.z}) + explosionHeight, z = unit.coords.z } local delay = (i - 1) * (spreadDuration / math.max(1, #foundUnits)) --Evenly spread over duration debugCBUBombletHit("CBUBomblet: Scheduling explosion for unit " .. unit.name .. " (ID: " .. unit.id .. ") at X: " .. adjustedUnitCoords.x .. ", Y: " .. adjustedUnitCoords.y .. ", Z: " .. adjustedUnitCoords.z .. " with final power " .. finalPower .. " (indirectMod: " .. indirectMod .. ", armorMod: " .. armorMod .. ") in " .. string.format("%.2f", delay) .. "s") timer.scheduleFunction(function() debugCBUBombletHit("CBUBomblet: Explosion triggered for unit " .. unit.name .. " (ID: " .. unit.id .. ") at X: " .. adjustedUnitCoords.x .. ", Y: " .. adjustedUnitCoords.y .. ", Z: " .. adjustedUnitCoords.z .. " with power " .. finalPower .. " due to weapon " .. weaponName) trigger.action.explosion(adjustedUnitCoords, finalPower) end, {name = unit.name, id = unit.id, coords = unit.coords}, timer.getTime() + delay) else cbuProcessed[key] = true debugCBUBombletHit("CBUBomblet: Hit chance failed for unit " .. unit.name .. " (ID: " .. unit.id .. "), skipping explosion") end end end --Post-scan for killfeed with 200m radius around first unit if splash_damage_options.killfeed_enable and #foundUnits > 0 then local playerName = "Unknown" if initiator then local status, playerNameResult = pcall(function() return initiator:getPlayerName() end) debugCBUBombletHit("CBUBomblet: Attempting to get player name from initiator, status: " .. tostring(status) .. ", result: " .. tostring(playerNameResult)) if status and playerNameResult and playerNameResult ~= "" then playerName = playerNameResult else local status, unitId = pcall(function() return initiator:getID() end) debugCBUBombletHit("CBUBomblet: Initiator ID check, status: " .. tostring(status) .. ", unitId: " .. tostring(unitId)) if status and unitId then local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.ucid and (tonumber(pinfo.slot) == unitId or (initiator.getName and pinfo.slot == initiator:getName())) then playerName = pinfo.name or "Unknown" debugCBUBombletHit("CBUBomblet: Player name found via slot match: " .. playerName) break end end end --Fallback for submunition initiator if playerName == "Unknown" and initiator.getTypeName and initiator:getTypeName() == "BLU-97/B" then local status, launcher = pcall(function() return Weapon.getLauncher(initiator) end) debugCBUBombletHit("CBUBomblet: Weapon launcher check, status: " .. tostring(status) .. ", launcher: " .. tostring(launcher)) if status and launcher then local status, launcherName = pcall(function() return launcher:getPlayerName() end) if status and launcherName and launcherName ~= "" then playerName = launcherName debugCBUBombletHit("CBUBomblet: Player name from launcher: " .. playerName) end end end end end debugCBUBombletHit("CBUBomblet: Final playerName: " .. playerName) if playerName ~= "Unknown" then local status, isPlayer = pcall(function() local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.name == playerName then return true end end return false end) debugCBUBombletHit("CBUBomblet: Player validation, status: " .. tostring(status) .. ", isPlayer: " .. tostring(isPlayer)) if status and isPlayer then local function performKillfeedScan(delay, attempt) local killfeedScanRadius = 200 local firstUnit = foundUnits[1] debugCBUBombletHit("CBUBomblet: Starting killfeed scan (attempt " .. attempt .. "), first unit: " .. tostring(firstUnit.name) .. ", coords: X=" .. firstUnit.coords.x .. ", Z=" .. firstUnit.coords.z) local killfeedVolS = { id = world.VolumeType.SPHERE, params = { point = firstUnit.coords, radius = killfeedScanRadius } } local secondScanUnits = {} local secondSeenUnitIDs = {} local ifKillfeedFound = function(obj) local targetUnitID = safeGet(function() return obj:getID() end, "unavailable") local targetHealth = safeGet(function() return obj:getLife() end, 0) local isExist = safeGet(function() return obj:isExist() end, false) if targetUnitID ~= "unavailable" then if isExist and targetHealth > 0 then secondSeenUnitIDs[targetUnitID] = true debugCBUBombletHit("CBUBomblet: Killfeed scan (attempt " .. attempt .. ") found unit ID: " .. targetUnitID .. " (Health: " .. targetHealth .. ", isExist: " .. tostring(isExist) .. ")") else debugCBUBombletHit("CBUBomblet: Killfeed scan (attempt " .. attempt .. ") found unit ID: " .. targetUnitID .. " marked as dead (Health: " .. targetHealth .. ", isExist: " .. tostring(isExist) .. ")") end end end debugCBUBombletHit("CBUBomblet: Killfeed scan for objects within " .. killfeedScanRadius .. "m radius around first unit (attempt " .. attempt .. ")") world.searchObjects(Object.Category.UNIT, killfeedVolS, ifKillfeedFound) world.searchObjects(Object.Category.STATIC, killfeedVolS, ifKillfeedFound) world.searchObjects(Object.Category.SCENERY, killfeedVolS, ifKillfeedFound) world.searchObjects(Object.Category.CARGO, killfeedVolS, ifKillfeedFound) --Custom function to get table keys local keys = {} for k in pairs(secondSeenUnitIDs) do table.insert(keys, k) end debugCBUBombletHit("CBUBomblet: Post-scan unit IDs (attempt " .. attempt .. "): " .. table.concat(keys, ", ")) for _, unit in ipairs(foundUnits) do debugCBUBombletHit("CBUBomblet: Checking unit " .. unit.name .. " (ID: " .. unit.id .. ") for splash killfeed (attempt " .. attempt .. ")") if not secondSeenUnitIDs[unit.id] then local alreadyInKillfeed = false for _, entry in ipairs(splashKillfeedTable) do if entry.unitId == unit.id then alreadyInKillfeed = true debugCBUBombletHit("CBUBomblet: Unit " .. unit.name .. " (ID: " .. unit.id .. ") already in splashKillfeedTable, skipping (attempt " .. attempt .. ")") break end end if not alreadyInKillfeed then table.insert(splashKillfeedTemp, { unitName = unit.name, unitType = unit.type, unitId = unit.id, playerName = playerName, weaponName = weaponName, time = timer.getTime(), position = unit.coords }) debugCBUBombletHit("CBUBomblet: Added to splashKillfeed: " .. unit.name .. " destroyed by " .. playerName .. " (attempt " .. attempt .. ")") if splash_damage_options.killfeed_game_messages then local msg = string.format("%s destroyed by %s's %s Splash Damage", unit.type, playerName, weaponName) local status, err = pcall(function() trigger.action.outTextForCoalition(2, msg, splash_damage_options.killfeed_game_message_duration) end) if not status then debugCBUBombletHit("CBUBomblet Error displaying killfeed message: " .. tostring(err) .. " (attempt " .. attempt .. ")") end end end else debugCBUBombletHit("CBUBomblet: Unit " .. unit.name .. " (ID: " .. unit.id .. ") still alive, not added to splashKillfeed (attempt " .. attempt .. ")") end end debugCBUBombletHit("CBUBomblet: splashKillfeedTemp size: " .. #splashKillfeedTemp .. " (attempt " .. attempt .. ")") --Schedule retry if first attempt and no kills added if attempt == 1 and #splashKillfeedTemp == 0 then debugCBUBombletHit("CBUBomblet: No kills added on first scan, scheduling retry in 5 seconds") timer.scheduleFunction(performKillfeedScan, 35, timer.getTime() + 5, 2) end end timer.scheduleFunction(performKillfeedScan, 30, timer.getTime() + 30, 1) else debugCBUBombletHit("CBUBomblet: Killfeed scan skipped, player validation failed") end else debugCBUBombletHit("CBUBomblet: Killfeed scan skipped, playerName is Unknown") end else debugCBUBombletHit("CBUBomblet: Killfeed scan skipped, no units found or killfeed disabled") end end end --A10 MurderMode action block --VehicleIED action block function logEvent(eventName, eventData) local logStr = "\n---EVENT: " .. eventName .. " ---\n" --Debug logging if enabled if splash_damage_options.events_debug then --Core event details logStr = logStr .. " Event Name: " .. eventName .. "\n" logStr = logStr .. " Event ID: " .. tostring(eventData.id or "unknown") .. "\n" logStr = logStr .. " Time: " .. tostring(eventData.time or "unknown") .. "\n" --Initiator details local initiatorID = "unavailable" local initiatorName = "unknown" local initiatorType = "unknown" local initiatorCoalition = "unknown" local initiatorPosition = "unavailable" if eventData.initiator then initiatorID = safeGet(function() return eventData.initiator:getID() end, "unavailable") initiatorName = safeGet(function() return eventData.initiator:getName() end, "unknown") initiatorType = safeGet(function() return eventData.initiator:getTypeName() end, "unknown") initiatorCoalition = safeGet(function() return eventData.initiator:getCoalition() end, "unknown") initiatorPosition = safeGet(function() initiatorGroup = safeGet(function() return eventData.initiator:getGroup():getName() end, "unknown") local pos = eventData.initiator:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") end logStr = logStr .. " Initiator:\n" logStr = logStr .. " ID: " .. initiatorID .. "\n" logStr = logStr .. " Name: " .. initiatorName .. "\n" logStr = logStr .. " Type: " .. initiatorType .. "\n" logStr = logStr .. " Coalition: " .. initiatorCoalition .. "\n" logStr = logStr .. " Position: " .. initiatorPosition .. "\n" logStr = logStr .. " Group: " .. initiatorGroup .. "\n" --Target details local targetID = "unavailable" local targetName = "unknown" local targetType = "unknown" local targetCategory = "unknown" local targetCoalition = "unknown" local targetPosition = "unavailable" local targetCountry = "unknown" local targetLife = 0 local targetGroup = "unknown" if eventData.target then targetID = safeGet(function() return eventData.target:getID() end, "unavailable") targetName = safeGet(function() return eventData.target:getName() end, "unknown") targetType = safeGet(function() return eventData.target:getTypeName() end, "unknown") targetCategory = safeGet(function() return eventData.target:getDesc().category end, "unknown") targetCoalition = safeGet(function() return eventData.target:getCoalition() end, 3) targetPosition = safeGet(function() local pos = eventData.target:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") targetCountry = safeGet(function() return eventData.target:getCountry() end, "unknown") targetLife = safeGet(function() return eventData.target:getLife() end, 0) targetGroup = safeGet(function() return eventData.target:getGroup():getName() end, "unknown") end logStr = logStr .. " Target:\n" logStr = logStr .. " ID: " .. targetID .. "\n" logStr = logStr .. " Name: " .. targetName .. "\n" logStr = logStr .. " Type: " .. targetType .. "\n" logStr = logStr .. " Category: " .. targetCategory .. "\n" logStr = logStr .. " Coalition: " .. targetCoalition .. "\n" logStr = logStr .. " Position: " .. targetPosition .. "\n" logStr = logStr .. " Country: " .. targetCountry .. "\n" logStr = logStr .. " Life: " .. targetLife .. "\n" logStr = logStr .. " Group: " .. targetGroup .. "\n" --Full event data dump logStr = logStr .. " Full eventData:\n" .. dumpTable(eventData, " ") env.info(logStr) end --Skip all DEAD events with invalid initiator if eventName == "DEAD" then if not eventData.initiator then if splash_damage_options.vehicleied_debug then env.info("Eventlog: Skipping DEAD event with no initiator") end else local initiatorID = safeGet(function() return eventData.initiator:getID() end, "unavailable") local initiatorName = safeGet(function() return eventData.initiator:getName() end, "unknown") local initiatorType = safeGet(function() return eventData.initiator:getTypeName() end, "unknown") if initiatorID == "unavailable" or type(initiatorName) ~= "string" then if splash_damage_options.event_debug then env.info("Eventlog: Skipping DEAD event for invalid initiator (ID: " .. tostring(initiatorID) .. ", Name: " .. tostring(initiatorName) .. ", Type: " .. tostring(initiatorType) .. ")") end end end end --Process GiantExplosionTarget units if enabled if splash_damage_options.giant_explosion_enabled then local checkName = (eventName == "HIT" or eventName == "KILL") and eventData.target and safeGet(function() return eventData.target:getName() end, "unknown") or (eventName == "DEAD") and eventData.initiator and safeGet(function() return eventData.initiator:getName() end, "unknown") or "unknown" if type(checkName) == "string" and checkName:find("GiantExplosionTarget") then local unitID, unitName, unitType, unitPosition, unitLife, rawCoords local status, err = pcall(function() if eventName == "HIT" or eventName == "KILL" then local tgt = eventData.target or eventData.object unitID = safeGet(function() return tgt:getID() end, "unavailable") unitName = safeGet(function() return tgt:getName() end, "unknown") unitType = safeGet(function() return tgt:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = tgt:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = tgt:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return tgt:getLife() end, "Alive") elseif eventName == "DEAD" then unitID = safeGet(function() return eventData.initiator:getID() end, "unavailable") unitName = safeGet(function() return eventData.initiator:getName() end, "unknown") unitType = safeGet(function() return eventData.initiator:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = eventData.initiator:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = eventData.initiator:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return eventData.initiator:getLife() end, 0) end end) if not status then debugMsg("GiantExplosionTrigger: Error extracting unit data for event " .. eventName .. ": " .. tostring(err)) elseif unitID == "unavailable" then debugMsg("GiantExplosionTrigger: Skipping event " .. eventName .. " for invalid unit ID: " .. tostring(unitID)) else if not processedUnitsGlobal then processedUnitsGlobal = {} end if processedUnitsGlobal[unitID] then debugMsg("GiantExplosionTrigger: Unit ID " .. unitID .. " (" .. unitName .. ") already processed, skipping") else local shouldTrigger = false if eventName == "HIT" and splash_damage_options.giantexplosion_ondamage then shouldTrigger = true debugMsg("GiantExplosionTrigger: Unit " .. unitName .. " hit, triggering explosion") elseif (eventName == "KILL" or eventName == "DEAD") and splash_damage_options.giantexplosion_ondeath then shouldTrigger = true debugMsg("GiantExplosionTrigger: Unit " .. unitName .. " killed/dead, triggering explosion") end if shouldTrigger then processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType, position = unitPosition, life = unitLife, event = eventName, time = timer.getTime() } triggerGiantExplosion({ pos = rawCoords, power = splash_damage_options.giant_explosion_power, scale = splash_damage_options.giant_explosion_scale, duration = splash_damage_options.giant_explosion_duration, count = splash_damage_options.giant_explosion_count }) return --Skip further processing end end end end end --Process VehicleIED units if enabled if splash_damage_options.vehicleied_enabled then --Early check for target names local checkName = (eventName == "HIT" or eventName == "KILL") and eventData.target and safeGet(function() return eventData.target:getName() end, "unknown") or (eventName == "DEAD") and eventData.initiator and safeGet(function() return eventData.initiator:getName() end, "unknown") or "unknown" local targetNames = {} for name in splash_damage_options.vehicleied_targetname:gmatch("[^,]+") do targetNames[#targetNames + 1] = name:gsub("^%s*(.-)%s*$", "%1") --Trim whitespace end local isTarget = false if type(checkName) == "string" then for _, targetName in ipairs(targetNames) do if checkName:find(targetName) then isTarget = true break end end end if isTarget then --Extract unit data local unitID, unitName, unitType, unitPosition, unitLife, rawCoords local status, err = pcall(function() if eventName == "HIT" or eventName == "KILL" then local tgt = eventData.target or eventData.object unitID = safeGet(function() return tgt:getID() end, "unavailable") unitName = safeGet(function() return tgt:getName() end, "unknown") unitType = safeGet(function() return tgt:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = tgt:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = tgt:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return tgt:getLife() end, "Alive") elseif eventName == "DEAD" then unitID = safeGet(function() return eventData.initiator:getID() end, "unavailable") unitName = safeGet(function() return eventData.initiator:getName() end, "unknown") unitType = safeGet(function() return eventData.initiator:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = eventData.initiator:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = eventData.initiator:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return eventData.initiator:getLife() end, 0) end end) if not status then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Error extracting unit data for event " .. eventName .. ": " .. tostring(err)) end else --Process target units if type(unitName) == "string" and unitID ~= "unavailable" then --Initialize processed table if not processedUnitsGlobal then processedUnitsGlobal = {} end --Skip if already processed if processedUnitsGlobal[unitID] then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit ID " .. unitID .. " (" .. unitName .. ") already processed in " .. eventName .. " event, skipping") end else --Log initial HIT event details for diagnostics if eventName == "HIT" and splash_damage_options.vehicleied_debug then logStr = logStr .. "Stored Unit Data: ID=" .. unitID .. ", Name=" .. unitName .. ", Type=" .. unitType .. ", Position=" .. unitPosition .. ", Life=" .. unitLife .. "\n" logStr = logStr .. "Processing initial HIT event for unit " .. unitName .. " (ID: " .. unitID .. ")\n" env.info(logStr) end if splash_damage_options.vehicleied_explode_on_hit then --Initialize hit tracking for retries if not HitEventTempTable then HitEventTempTable = {} end if not HitEventTempTable[unitID] then HitEventTempTable[unitID] = { retryCount = 0 } end local unit = eventData.target --Attempt to destroy the unit if enabled if splash_damage_options.vehicleied_destroy_vehicle then if unit then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Attempt to destroy unit " .. unitName .. " (ID: " .. unitID .. ") - failed: " .. tostring(err)) end local status, err = pcall(function() unit:destroy() end) else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: No unit, attempt to destroy unit " .. unitName .. " (ID: " .. unitID .. ") - failed: " .. tostring(err)) end end end --Trigger IED immediately if splash_damage_options.vehicleied_debug then local freshHealth = unit and unit:isExist() and safeGet(function() return unit:getLife() end, 0) or "unknown" env.info("VehicleIEDTrigger: Unit " .. unitName .. " hit, triggering explosion") end local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } --Add to processed table only when exploding processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType, position = unitPosition, life = unitLife, event = eventName, time = timer.getTime() } VehicleIEDTrigger(coords, nil) --Pass nil unit since destruction is handled here else --Initialize pending table if not VehicleIEDPendingTable then VehicleIEDPendingTable = {} end --Handle HIT event if eventName == "HIT" then --Check if unit is already in pending table if VehicleIEDPendingTable[unitID] then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit ID " .. unitID .. " (" .. unitName .. ") already in VehicleIEDPendingTable, ignoring HIT event") end else --Add to pending table and schedule checks local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } VehicleIEDPendingTable[unitID] = { id = unitID, name = unitName, coords = coords, prevCoords = coords, --Store initial coords as previous startTime = timer.getTime(), checksRemaining = 20, --10 seconds / 0.5 seconds = 20 checks deadChecks = 0 --Track additional checks after death } if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Added unit " .. unitName .. " (ID: " .. unitID .. ") to VehicleIEDPendingTable for movement monitoring") end --Schedule periodic checks local function checkUnitStatus(params) local unitID = params.id local unitName = params.name local entry = VehicleIEDPendingTable[unitID] if not entry then return end entry.checksRemaining = entry.checksRemaining - 1 local unit = Unit.getByName(unitName) local isAlive = unit and unit:isExist() and safeGet(function() return unit:getLife() end, 0) > 0 --Update coordinates regardless of alive status to track rolling local newPosition = safeGet(function() local pos = unit and unit:isExist() and unit:getPosition().p or entry.coords return { x = pos.x, y = pos.y, z = pos.z } end, entry.coords) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Updated coords for unit " .. unitName .. " (ID: " .. unitID .. ") to X: " .. newPosition.x .. ", Y: " .. newPosition.y .. ", Z: " .. newPosition.z) end --Check if unit has stopped moving (coords unchanged) local hasStopped = math.abs(newPosition.x - entry.prevCoords.x) < 0.1 and math.abs(newPosition.y - entry.prevCoords.y) < 0.1 and math.abs(newPosition.z - entry.prevCoords.z) < 0.1 if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Checking unit " .. unitName .. " (ID: " .. unitID .. "), alive: " .. tostring(isAlive) .. ", stopped: " .. tostring(hasStopped) .. ", checks remaining: " .. entry.checksRemaining .. ", dead checks: " .. entry.deadChecks) end --If unit is dead, perform additional checks to confirm stopped if not isAlive then entry.deadChecks = entry.deadChecks + 1 if entry.deadChecks < 4 then --Check for 2 seconds (4 * 0.5s) if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " (ID: " .. unitID .. ") is dead, performing additional check #" .. entry.deadChecks .. " for movement") end entry.prevCoords = newPosition entry.coords = newPosition timer.scheduleFunction(checkUnitStatus, params, timer.getTime() + 0.1) return end end --Trigger explosion only if dead and stopped if not isAlive and hasStopped and entry.deadChecks >= 4 then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " (ID: " .. unitID .. ") is dead and stopped, previous position: " .. entry.prevCoords.x .. ", Y: " .. entry.prevCoords.y .. ", Z: " .. entry.prevCoords.z .. ", current position: " .. newPosition.x .. ", Y: " .. newPosition.y .. ", Z: " .. newPosition.z .. ", no movement detected, attempting final coords update before explosion") end --Attempt one final coordinate update with pcall local finalCoords = entry.coords local status, result = pcall(function() local u = Unit.getByName(unitName) if u and u:isExist() then local pos = u:getPosition().p return { x = pos.x, y = pos.y, z = pos.z } end return newPosition end) if status and result then finalCoords = result if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Final coords update for unit " .. unitName .. " (ID: " .. unitID .. ") to X: " .. finalCoords.x .. ", Y: " .. finalCoords.y .. ", Z: " .. finalCoords.z) end elseif splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Final coords update failed for unit " .. unitName .. " (ID: " .. unitID .. "), using last coords: X: " .. finalCoords.x .. ", Y: " .. finalCoords.y .. ", Z: " .. finalCoords.z .. ", error: " .. tostring(result)) end --Ensure not in processed table if not processedUnitsGlobal[unitID] then processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType or "unknown", position = string.format("x=%.0f, y=%.0f, z=%.0f", finalCoords.x, finalCoords.y, finalCoords.z), life = 0, event = "HIT", time = timer.getTime() } if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " (ID: " .. unitID .. ") added to processed table, scheduling explosion at X: " .. finalCoords.x .. ", Y: " .. finalCoords.y .. ", Z: " .. finalCoords.z) end local unittodestroy = eventData.target --Attempt to destroy the unit if enabled if splash_damage_options.vehicleied_destroy_vehicle then if unittodestroy then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Attempt to destroy unit " .. unitName .. " (ID: " .. unitID .. ")") end local status, err = pcall(function() unittodestroy:destroy() end) else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: No unit, attempt to destroy unit " .. unitName .. " (ID: " .. unitID .. ") - failed: " .. tostring(err)) end end end VehicleIEDTrigger(finalCoords, nil) end VehicleIEDPendingTable[unitID] = nil elseif entry.checksRemaining <= 0 then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " (ID: " .. unitID .. ") still moving or alive after 10 seconds, removing from pending table") end VehicleIEDPendingTable[unitID] = nil else --Update previous coords and schedule next check entry.prevCoords = newPosition entry.coords = newPosition timer.scheduleFunction(checkUnitStatus, params, timer.getTime() + 0.2) end end timer.scheduleFunction(checkUnitStatus, {id = unitID, name = unitName}, timer.getTime() + 0.2) end end --Handle KILL or DEAD event if eventName == "KILL" or eventName == "DEAD" then if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Unit " .. unitName .. " triggered " .. eventName .. ", processing event") end local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } --Add to processed table only when exploding processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType, position = unitPosition, life = unitLife, event = eventName, time = timer.getTime() } VehicleIEDTrigger(coords, nil) --Trigger IED VehicleIEDPendingTable[unitID] = nil --Remove from pending table if present end end end end end else if splash_damage_options.vehicleied_debug then env.info("VehicleIEDTrigger: Skipping non-target for vehicleied unit: " .. tostring(checkName)) end end end --Handle A10MurderMode if splash_damage_options.A10MurderMode and eventName == "HIT" and eventData.initiator then local initiatorType = safeGet(function() return eventData.initiator:getTypeName() end, "unknown") --Extract unit data for A10MurderMode local unitID, unitName, unitType, unitPosition, unitLife, rawCoords local status, err = pcall(function() local tgt = eventData.target unitID = safeGet(function() return tgt:getID() end, "unavailable") unitName = safeGet(function() return tgt:getName() end, "unknown") unitType = safeGet(function() return tgt:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = tgt:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = tgt:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return tgt:getLife() end, "Alive") end) if not status and splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Error extracting unit data for HIT event: " .. tostring(err)) end if splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Checking initiator type: " .. initiatorType .. " for target: " .. unitName) end if initiatorType:match("A%-10") then local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } if coords.x and coords.y and coords.z then if splash_damage_options.MurderMode_debug then env.info("A10MurderMode: A-10 initiator detected, triggering explosion for target: " .. unitName) end A10MurderMode(coords) elseif splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Invalid coordinates for target: " .. unitName .. ", skipping explosion") end elseif splash_damage_options.MurderMode_debug then env.info("A10MurderMode: Initiator not an A-10, skipping for target: " .. unitName) end end --Handle NamedUnitMurderMode if splash_damage_options.NamedUnitMurderMode and eventName == "HIT" and eventData.initiator then local initiatorName = tostring(safeGet(function() return eventData.initiator:getName() end, "unknown")) --Extract unit data for NamedUnitMurderMode local unitID, unitName, unitType, unitPosition, unitLife, rawCoords local status, err = pcall(function() local tgt = eventData.target unitID = safeGet(function() return tgt:getID() end, "unavailable") unitName = safeGet(function() return tgt:getName() end, "unknown") unitType = safeGet(function() return tgt:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = tgt:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = tgt:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return tgt:getLife() end, "Alive") end) if not status and splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Error extracting unit data for HIT event: " .. tostring(err)) end if splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Checking initiator name: " .. initiatorName .. " for target: " .. unitName) end if initiatorName:find("MurderMode") then local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } if coords.x and coords.y and coords.z then if splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Initiator with 'MurderMode' detected, triggering explosion for target: " .. unitName) end NamedUnitMurderMode(coords) elseif splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Invalid coordinates for target: " .. unitName .. ", skipping explosion") end elseif splash_damage_options.MurderMode_debug then env.info("NamedUnitMurderMode: Initiator name does not contain 'MurderMode', skipping for target: " .. unitName) end end --Handle CriticalComponent if splash_damage_options.CriticalComponent and eventName == "HIT" and eventData.initiator then local initiatorType = safeGet(function() return eventData.initiator:getTypeName() end, "unknown") --Extract unit data for CriticalComponent local unitID, unitName, unitType, unitPosition, rawCoords, weaponName local status, err = pcall(function() local tgt = eventData.target unitID = safeGet(function() return tgt:getID() end, "unavailable") unitName = safeGet(function() return tgt:getName() end, "unknown") unitType = safeGet(function() return tgt:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = tgt:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = tgt:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) weaponName = safeGet(function() local fullName = eventData.weapon:getTypeName() return fullName:match(".*%.(.*)") or fullName end, "unknown") end) if not status and splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Error extracting unit data for HIT event: " .. tostring(err)) end if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Checking initiator type: " .. initiatorType .. " for target: " .. unitName) end if eventData.weapon then if unitID == "unavailable" then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Skipping event HIT for invalid unit ID: " .. tostring(unitID)) end else if splash_damage_options.CriticalComponent_Specific_Weapons_Only and #splash_damage_options.CriticalComponent_Specific_Weapons_Only > 0 then local validWeapon = false for _, wpn in ipairs(splash_damage_options.CriticalComponent_Specific_Weapons_Only) do if weaponName == wpn then validWeapon = true break end end if not validWeapon then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Weapon " .. (weaponName or "nil") .. " not in CriticalComponent_Specific_Weapons_Only, skipping") end else local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } if coords.x and coords.y and coords.z then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Valid weapon detected, triggering explosion for target: " .. unitName .. " with weapon: " .. (weaponName or "nil")) end CriticalComponent(coords, weaponName, eventData.initiator, unitName, unitID, unitType) elseif splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Invalid coordinates for target: " .. unitName .. ", skipping explosion") end end else local coords = { x = tonumber(unitPosition:match("x=(.-),")), y = tonumber(unitPosition:match("y=(.-),")), z = tonumber(unitPosition:match("z=(.-)$")) } if coords.x and coords.y and coords.z then if splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Valid weapon detected, triggering explosion for target: " .. unitName .. " with weapon: " .. (weaponName or "nil")) end CriticalComponent(coords, weaponName, eventData.initiator, unitName, unitID, unitType) elseif splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: Invalid coordinates for target: " .. unitName .. ", skipping explosion") end end end elseif splash_damage_options.CriticalComponent_debug then env.info("CriticalComponent: No valid weapon, skipping for target: " .. unitName) end end --Process GU_Explode_on_Death for ground units if splash_damage_options.GU_Explode_on_Death then local unit, unitID, unitName, unitType, unitPosition, rawCoords, unitCategory local status, err = pcall(function() unit = eventName == "DEAD" and eventData.initiator or eventData.target unitID = safeGet(function() return unit:getID() end, "unavailable") unitName = safeGet(function() return unit:getName() end, "unknown") unitType = safeGet(function() return unit:getTypeName() end, "unknown") unitCategory = safeGet(function() return unit:getDesc().category end, "unknown") unitPosition = safeGet(function() local pos = unit:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = unit:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) --Fallback to CargoCookoffPendingTable coords if available for DEAD event if eventName == "DEAD" and (rawCoords.x == 0 and rawCoords.y == 0 and rawCoords.z == 0) then local pendingEntry = CargoCookoffPendingTable[unitID] if pendingEntry and pendingEntry.coords then rawCoords = pendingEntry.coords unitPosition = string.format("x=%.0f, y=%.0f, z=%.0f", rawCoords.x, rawCoords.y, rawCoords.z) if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Using coords from CargoCookoffPendingTable for unit ID " .. unitID .. ": X=" .. rawCoords.x .. ", Y=" .. rawCoords.y .. ", Z=" .. rawCoords.z) end end end end) if not status and splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Error extracting unit data for " .. eventName .. " event: " .. tostring(err)) end local isInfantry = safeGet(function() return unit:hasAttribute("Infantry") end, false) if unitID ~= "unavailable" and type(unitName) == "string" and unitCategory == Unit.Category.GROUND_UNIT and (not splash_damage_options.GU_Explode_Exclude_Infantry or not isInfantry) then if not GUProcessedUnits then GUProcessedUnits = {} end if GUProcessedUnits[unitID] then if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Unit ID " .. unitID .. " (" .. unitName .. ") already processed, skipping event " .. eventName) end else local function checkUnitStatus(params) if GUProcessedUnits[params.id] then if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Unit ID " .. params.id .. " (" .. params.name .. ") already processed after 0.1s check, skipping") end return end local u = Unit.getByName(params.name) local isAlive = u and u:isExist() and safeGet(function() return u:getLife() end, 0) > 0 local terrainHeight = land.getHeight({x = params.coords.x, y = params.coords.z}) local heightAboveGround = params.coords.y - terrainHeight if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Checking unit " .. params.name .. " (ID: " .. params.id .. ") after 0.1s, alive: " .. tostring(isAlive) .. ", raw Y: " .. string.format("%.1f", params.coords.y) .. ", terrain height: " .. string.format("%.1f", terrainHeight) .. ", height above ground: " .. string.format("%.1f", heightAboveGround) .. "m, event: " .. params.event) end if not isAlive then if params.coords.x == 0 and params.coords.z == 0 then if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Invalid coordinates for unit " .. params.name .. " (ID: " .. params.id .. "), skipping explosion") end return end if heightAboveGround > 5 then if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Unit " .. params.name .. " (ID: " .. params.id .. ") is " .. string.format("%.1f", heightAboveGround) .. "m above ground, forcing to terrain height") end end if params.name:find("GUED") or math.random() <= splash_damage_options.GU_Explode_on_Death_Chance then local explosionCoords = { x = params.coords.x, y = terrainHeight + (splash_damage_options.GU_Explode_on_Death_Height or 0), z = params.coords.z } GUProcessedUnits[params.id] = { id = params.id, name = params.name, type = params.type, position = params.position, life = 0, event = params.event, time = timer.getTime() } trigger.action.explosion(explosionCoords, splash_damage_options.GU_Explode_on_Death_Explosion_Power) if splash_damage_options.GU_Explode_debug then local reason = params.name:find("GUED") and "GUED in name" or "chance check passed" env.info("GU_Explode_on_Death: Triggered explosion for unit " .. params.name .. " (ID: " .. params.id .. ") at X: " .. string.format("%.1f", explosionCoords.x) .. ", Y: " .. string.format("%.1f", explosionCoords.y) .. ", Z: " .. string.format("%.1f", explosionCoords.z) .. " with power " .. splash_damage_options.GU_Explode_on_Death_Explosion_Power .. " (" .. reason .. ")") end else if splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Chance check failed for unit " .. params.name .. " (ID: " .. params.id .. ")") end end end end if eventName == "HIT" then timer.scheduleFunction(checkUnitStatus, { id = unitID, name = unitName, type = unitType, position = unitPosition, coords = rawCoords, event = "HIT" }, timer.getTime() + 0.1) elseif eventName == "DEAD" then checkUnitStatus({ id = unitID, name = unitName, type = unitType, position = unitPosition, coords = rawCoords, event = "DEAD" }) end end elseif splash_damage_options.GU_Explode_debug then env.info("GU_Explode_on_Death: Skipping non-ground unit, invalid unit, or excluded infantry (ID: " .. tostring(unitID) .. ", Name: " .. tostring(unitName) .. ")") end end --CBU Bomblet Hit Explosion handling if eventName == "SHOT" and eventData.initiator and eventData.weapon then local weaponName = safeGet(function() return eventData.weapon:getTypeName():match(".*%.(.*)") or eventData.weapon:getTypeName() end, "unknown") local weaponID = safeGet(function() return eventData.weapon:getID() end, "unknown") --Check if the weapon is in clusterSubMunTable if clusterSubMunTable[weaponName] then cbuParentUnits[weaponID] = eventData.initiator debugCBUBombletHit("CBUBomblet: Stored parent unit for weapon " .. weaponName .. " (ID: " .. weaponID .. ")") end end if splash_damage_options.CBU_Bomblet_Hit_Explosion and eventName == "HIT" and eventData.initiator and eventData.target and eventData.weapon then local status, err = pcall(function() local unitID = safeGet(function() return eventData.target:getID() end, "unavailable") local unitName = safeGet(function() return eventData.target:getName() end, "unknown") local weaponName = safeGet(function() return eventData.weapon:getTypeName():match(".*%.(.*)") or eventData.weapon:getTypeName() end, "unknown") local weaponID = safeGet(function() return eventData.weapon:getID() end, "unknown") local rawCoords = safeGet(function() return eventData.target:getPosition().p end, {x = 0, y = 0, z = 0}) local initiator = eventData.initiator --If initiator is a submunition, try to find the parent unit if clusterSubMunTable[weaponName] then for storedWeaponID, parentInitiator in pairs(cbuParentUnits) do if parentInitiator and parentInitiator:isExist() then initiator = parentInitiator debugCBUBombletHit("CBUBomblet: Using parent unit for submunition " .. weaponName .. " (ID: " .. weaponID .. ")") break end end end if unitID == "unavailable" or type(unitName) ~= "string" then debugCBUBombletHit("CBUBomblet: Skipping HIT event for invalid unit (ID: " .. tostring(unitID) .. ", Name: " .. tostring(unitName) .. ")") else --Check if weapon is a submunition in clusterSubMunTable local submunitionData = clusterSubMunTable[weaponName] if submunitionData then local key = unitID .. "-" .. weaponID if not cbuProcessed[key] then if not splash_damage_options.CBU_Bomblet_Hit_OriginUnit_Twice then cbuProcessed[key] = true end debugCBUBombletHit("CBUBomblet: Submunition " .. weaponName .. " detected for unit " .. unitName .. " (ID: " .. unitID .. ")") CBUBombletHitExplosion(rawCoords, unitName, unitID, weaponName, weaponID, submunitionData.explosive, initiator) else debugCBUBombletHit("CBUBomblet: Unit " .. unitName .. " (ID: " .. unitID .. ") already processed for weapon " .. weaponName) end else debugCBUBombletHit("CBUBomblet: Weapon " .. weaponName .. " is not a submunition in clusterSubMunTable, skipping for unit " .. unitName .. " (ID: " .. unitID .. ")") end end end) if not status then debugCBUBombletHit("CBUBomblet: Error processing HIT event: " .. tostring(err)) end end --Process Cargo Cookoff units if splash_damage_options.enable_cargo_effects then if not processedCookoffs then processedCookoffs = {} end local unit, unitID, unitName, unitType, unitPosition, unitLife, maxHealth, rawCoords local isCargoCandidate = false local isCargoUnit = false local status, err = pcall(function() if eventName == "HIT" or eventName == "KILL" then unit = eventData.target unitID = safeGet(function() return unit:getID() end, "unavailable") unitName = safeGet(function() return unit:getName() end, "unknown") unitType = safeGet(function() return unit:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = unit:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = unit:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) unitLife = safeGet(function() return unit:getLife() end, 0) maxHealth = safeGet(function() return unit:getDesc().life end, 1) elseif eventName == "DEAD" then unit = eventData.initiator unitID = safeGet(function() return unit:getID() end, "unavailable") unitName = safeGet(function() return unit:getName() end, "unknown") unitType = safeGet(function() return unit:getTypeName() end, "unknown") unitPosition = safeGet(function() local pos = unit:getPosition().p return string.format("x=%.0f, y=%.0f, z=%.0f", pos.x, pos.y, pos.z) end, "unavailable") rawCoords = safeGet(function() local pos = unit:getPosition().p return {x = pos.x, y = pos.y, z = pos.z} end, {x = 0, y = 0, z = 0}) --Fallback to CargoCookoffPendingTable coords if available if rawCoords.x == 0 and rawCoords.y == 0 and rawCoords.z == 0 then local pendingEntry = CargoCookoffPendingTable[unitID] if pendingEntry and pendingEntry.coords then rawCoords = pendingEntry.coords unitPosition = string.format("x=%.0f, y=%.0f, z=%.0f", rawCoords.x, rawCoords.y, rawCoords.z) debugCargoCookOff("Using coords from CargoCookoffPendingTable for unit ID " .. unitID .. ": X=" .. rawCoords.x .. ", Z=" .. rawCoords.z) end end unitLife = safeGet(function() return unit:getLife() end, 0) maxHealth = safeGet(function() return unit:getDesc().life end, 1) end unitName = tostring(unitName) --Exclude static objects and fortifications unless explicitly in cargoUnits or named CargoCookoffTarget local objectCategory = safeGet(function() return Object.getCategory(unit) end, "unknown") if (objectCategory == Object.Category.STATIC or objectCategory == Object.Category.FORTIFICATION) and not (cargoUnits[unitType] or unitName:find("CargoCookoffTarget")) then debugCargoCookOff("Unit ID " .. unitID .. " is a static object or fortification (" .. unitType .. "), skipping unless in cargoUnits or CargoCookoffTarget") return false end local targetNames = {} for name in splash_damage_options.vehicleied_targetname:gmatch("[^,]+") do targetNames[#targetNames + 1] = name:gsub("^%s*(.-)%s*$", "%1") --Trim whitespace end for _, targetName in ipairs(targetNames) do if unitName:find(targetName) then debugCargoCookOff("Unit ID " .. unitID .. " contains target name (" .. unitName .. "), skipping cargo cookoff") return false end end if cargoUnits[unitType] then isCargoCandidate = true isCargoUnit = true debugCargoCookOff("Unit ID " .. unitID .. " identified as cargo candidate via cargoUnits table") elseif unitName:find("CargoCookoffTarget") then isCargoCandidate = true debugCargoCookOff("Unit ID " .. unitID .. " identified as cargo candidate via CargoCookoffTarget name") elseif splash_damage_options.smokeandcookoffeffectallvehicles then local category = safeGet(function() return unit:getDesc().category end, "unknown") local isInfantry = safeGet(function() return unit:hasAttribute("Infantry") end, false) if (category == Unit.Category.GROUND_UNIT or category == Unit.Category.SHIP) and not isInfantry then isCargoCandidate = true debugCargoCookOff("Unit ID " .. unitID .. " identified as cargo candidate via smokeandcookoffeffectallvehicles") end end return true end) if not status or err == false then debugCargoCookOff("Error extracting unit data for event " .. eventName .. ": " .. tostring(err)) elseif unitID == "unavailable" then debugCargoCookOff("Skipping event " .. eventName .. " for invalid unit ID: " .. tostring(unitID)) elseif processedCookoffs[unitID] then debugCargoCookOff("Unit ID " .. unitID .. " already triggered cookoff for event " .. eventName .. ", skipping") elseif not isCargoCandidate then debugCargoCookOff("Unit ID " .. unitID .. " not a cargo candidate, skipping") else if not CargoCookoffPendingTable then CargoCookoffPendingTable = {} end if eventName == "HIT" then local healthPercent = maxHealth > 0 and (unitLife / maxHealth * 100) or 0 debugCargoCookOff("Unit ID " .. unitID .. " hit, health: " .. unitLife .. "/" .. maxHealth .. " (" .. string.format("%.2f", healthPercent) .. "%)") local damageThreshold = isCargoUnit and splash_damage_options.cargo_damage_threshold or splash_damage_options.allunits_damage_threshold if healthPercent <= damageThreshold or unitLife <= 0 then CargoCookoffPendingTable[unitID] = { id = unitID, name = unitName, type = unitType, coords = rawCoords, prevCoords = rawCoords, unit = unit, startTime = timer.getTime(), isCargoCookoff = true, isDead = unitLife <= 0 } processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType, position = unitPosition, life = unitLife, event = eventName, time = timer.getTime() } processedCookoffs[unitID] = true debugCargoCookOff("Added unit ID " .. unitID .. " to CargoCookoffPendingTable") debugCargoCookOff("Marked unit ID " .. unitID .. " as processed in processedUnitsGlobal and processedCookoffs") debugCargoCookOff("Triggering cookoff for unit ID " .. unitID .. ", isCargoUnit: " .. tostring(isCargoUnit)) scheduleCargoEffects(unitType, unitName, unitID, 0, false) --Pass fromDeadEvent = false end elseif eventName == "KILL" then debugCargoCookOff("Unit ID " .. unitID .. " triggered KILL, processing") CargoCookoffPendingTable[unitID] = { id = unitID, name = unitName, type = unitType, coords = rawCoords, prevCoords = rawCoords, unit = unit, startTime = timer.getTime(), isCargoCookoff = true, isDead = true } processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType, position = unitPosition, life = unitLife, event = eventName, time = timer.getTime() } processedCookoffs[unitID] = true debugCargoCookOff("Added unit ID " .. unitID .. " to CargoCookoffPendingTable") debugCargoCookOff("Marked unit ID " .. unitID .. " as processed in processedUnitsGlobal and processedCookoffs") debugCargoCookOff("Triggering cookoff for unit ID " .. unitID .. ", isCargoUnit: " .. tostring(isCargoUnit)) scheduleCargoEffects(unitType, unitName, unitID, 0, false) --Pass fromDeadEvent = false elseif eventName == "DEAD" then debugCargoCookOff("Unit ID " .. unitID .. " triggered DEAD, processing") local coords = CargoCookoffPendingTable[unitID] and CargoCookoffPendingTable[unitID].coords or rawCoords if unitPosition ~= "unavailable" and coords.x == 0 and coords.y == 0 and coords.z == 0 then local newCoords = { x = tonumber(unitPosition:match("x=(.-),")) or 0, y = tonumber(unitPosition:match("y=(.-),")) or 0, z = tonumber(unitPosition:match("z=(.-)$")) or 0 } if newCoords.x ~= 0 or newCoords.y ~= 0 or newCoords.z ~= 0 then coords = newCoords debugCargoCookOff("Updated coords from unitPosition for unit ID " .. unitID .. ": X=" .. coords.x .. ", Z=" .. coords.z) end end if not coords or (coords.x == 0 and coords.y == 0 and coords.z == 0) then debugCargoCookOff("Skipping cookoff for unit ID " .. unitID .. " due to invalid coordinates (X: 0, Z: 0)") else CargoCookoffPendingTable[unitID] = { id = unitID, name = unitName, type = unitType, coords = coords, prevCoords = coords, unit = nil, --Unit is dead startTime = timer.getTime(), isCargoCookoff = true, isDead = true } processedUnitsGlobal[unitID] = { id = unitID, name = unitName, type = unitType, position = unitPosition, life = unitLife, event = eventName, time = timer.getTime() } processedCookoffs[unitID] = true debugCargoCookOff("Marked unit ID " .. unitID .. " as processed in processedUnitsGlobal and processedCookoffs") debugCargoCookOff("Triggering cookoff for unit ID " .. unitID .. " at X: " .. coords.x .. ", Z: " .. coords.z .. ", isCargoUnit: " .. tostring(isCargoUnit)) scheduleCargoEffects(unitType, unitName, unitID, 0, true) --Pass fromDeadEvent = true CargoCookoffPendingTable[unitID] = nil end end end end end function WpnHandler:onEvent(event) protectedCall(onWpnEvent, event) if event.id == world.event.S_EVENT_HIT then logEvent("HIT", event) elseif event.id == world.event.S_EVENT_KILL then logEvent("KILL", event) protectedCall(onKillEvent, event) elseif event.id == world.event.S_EVENT_DEAD then logEvent("DEAD", event) end end --kill feed event function function onKillEvent(event) if not splash_damage_options.killfeed_enable or event.id ~= world.event.S_EVENT_KILL then return end local status, err = pcall(function() local killedUnit = event.target local killer = event.initiator if not killedUnit then if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Skipped, no target at %.2f", timer.getTime())) end return end local unitName = safeGet(function() return killedUnit:getName() end, "unknown") local unitType = safeGet(function() return killedUnit:getTypeName() end, "unknown") local unitID = safeGet(function() return killedUnit:getID() end, "unavailable") local position = safeGet(function() local pos = killedUnit:getPoint() return {x = pos.x, y = pos.y, z = pos.z} end, {x=0, y=0, z=0}) if unitName == "unknown" or unitType == "unknown" or unitID == "unavailable" or unitID == 0 then if splash_damage_options.killfeed_debug then --env.info(string.format("KillFeed: Skipped unit ID %s with name %s and type %s at %.2f", tostring(unitID), unitName, unitType, timer.getTime())) end return end --Check if unitID is already in killfeedTable for _, entry in ipairs(killfeedTable) do if entry.unitID == unitID then if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Skipped unit ID %s (%s) already in killfeedTable at %.2f", unitID, unitType, timer.getTime())) end return end end local killerName = "Unknown" local killerUnitName = "Unknown" if killer then local status, unitNameResult = pcall(function() return killer:getName() end) if status and unitNameResult then killerUnitName = unitNameResult end local status, playerNameResult = pcall(function() return killer:getPlayerName() end) if status and playerNameResult then killerName = playerNameResult else local status, unitId = pcall(function() return killer:getID() end) if status and unitId then local playerList = net.get_player_list() or {} for _, pid in ipairs(playerList) do local pinfo = net.get_player_info(pid) if pinfo and pinfo.ucid then local slotUnitId = tonumber(pinfo.slot) or pinfo.slot if slotUnitId == unitId or pinfo.slot == killerUnitName then killerName = pinfo.name or killerUnitName break end end end end end if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Killer UnitName: %s, PlayerName: %s, UnitID: %s, Type: %s, Slot: %s", killerUnitName, killerName, unitID, unitType, killer.getID and killer:getID() or "unknown")) end elseif splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Unit ID %s (%s) killed with no initiator at %.2f", unitID, unitType, timer.getTime())) end --Log bc table state for direct kill only if Lekas integration is enabled if splash_damage_options.killfeed_debug and splash_damage_options.killfeed_lekas_foothold_integration then env.info("KillFeed: bc table state for direct kill: " .. (bc and "exists" or "nil")) env.info("KillFeed: bc.addTempStat: " .. (bc and bc.addTempStat and "exists" or "nil")) env.info("KillFeed: bc.context: " .. (bc and bc.context and "exists" or "nil")) if bc and bc.context then env.info("KillFeed: bc.context.playerContributions: " .. (bc.context.playerContributions and "exists" or "nil")) if bc.context.playerContributions then env.info("KillFeed: bc.context.playerContributions[2]: " .. (bc.context.playerContributions[2] and "exists" or "nil")) end end end --Check if unitID is in splashKillfeedTable local splashIndex = nil for i, entry in ipairs(splashKillfeedTable) do if entry.unitId == unitID then splashIndex = i break end end if splashIndex then local dupeMsg = string.format("Duplicate kill: %s (%s) [ID: %s]", unitName, unitType, unitID) if splash_damage_options.killfeed_game_messages then local status, err = pcall(function() --trigger.action.outTextForCoalition(2, dupeMsg, splash_damage_options.killfeed_game_message_duration) --ignore for now end) if not status then trigger.action.outText(dupeMsg, splash_damage_options.killfeed_game_message_duration) if splash_damage_options.killfeed_debug then env.info("KillFeed: Failed coalition message for duplicate: " .. tostring(err)) end end end if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: %s at %.2f", dupeMsg, timer.getTime())) end table.remove(splashKillfeedTable, splashIndex) if splash_damage_options.killfeed_debug then env.info(string.format("SplashKillFeed: Removed duplicate entry for unit ID %s (%s) from splashKillfeedTable at %.2f", unitID, unitType, timer.getTime())) end else --[[ --Process direct kill contribution if killerName ~= "Unknown" and splash_damage_options.killfeed_lekas_foothold_integration then local status, result = pcall(function() local statName = "Ground Units" local points = 10 if unitType:find("Plane") then statName = "Air" points = 30 elseif unitType:find("Helicopter") then statName = "Helo" points = 30 elseif unitType:find("SAM") then statName = "SAM" points = 30 elseif unitType:find("Infantry") then statName = "Infantry" points = 10 elseif unitType:find("Ship") then statName = "Ship" points = 250 elseif unitType:find("Building") then statName = "Structure" points = 30 end bc:addTempStat(killerName, statName, 1) if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Added temp stat for %s: stat=%s, count=1", killerName, statName)) end if bc.context and type(bc.context) == "table" and bc.context.playerContributions and type(bc.context.playerContributions) == "table" then bc.context.playerContributions[2] = bc.context.playerContributions[2] or {} local oldPoints = bc.context.playerContributions[2][killerName] or 0 bc.context.playerContributions[2][killerName] = oldPoints + points if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Updated contributions for %s: old=%d, new=%d, added=%d", killerName, oldPoints, bc.context.playerContributions[2][killerName], points)) end else if splash_damage_options.killfeed_debug then env.info("KillFeed: Skipped contribution update for " .. killerName .. ": bc.context or bc.context.playerContributions is nil") end end end) if not status and splash_damage_options.killfeed_debug then env.info("KillFeed: Error processing direct kill for unitId=" .. tostring(unitID) .. ": " .. tostring(result)) end ]]-- end end if unitType ~= "Unknown" then table.insert(killfeedTable, { unitName = unitName, unitType = unitType, unitID = unitID, killer = killerName, time = timer.getTime(), position = position }) if splash_damage_options.killfeed_game_messages and not splashIndex then local msg = string.format("%s destroyed by %s", unitType, killerName) local status, err = pcall(function() --trigger.action.outTextForCoalition(2, msg, splash_damage_options.killfeed_game_message_duration) --disabled due to lots of unknowns appearing end) if not status then trigger.action.outText(msg, splash_damage_options.killfeed_game_message_duration) if splash_damage_options.killfeed_debug then env.info("KillFeed: Failed coalition message: " .. tostring(err)) end end end if splash_damage_options.killfeed_debug then env.info(string.format("KillFeed: Recorded %s destroyed by %s [ID: %s] at %.2f", unitType, killerName, unitID, timer.getTime())) end end end) if not status and splash_damage_options.killfeed_debug then env.info("KillFeed: Error: " .. tostring(err)) end end --kill feed event function function onDeadEvent(event) if not splash_damage_options.killfeed_enable or event.id ~= world.event.S_EVENT_DEAD then return end local status, err = pcall(function() local deadUnit = event.initiator if not deadUnit then if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Skipped, no initiator at %.2f", timer.getTime())) end return end --Extract unit data using safeGet, matching logEvent defaults local unitID = safeGet(function() return deadUnit:getID() end, "unavailable") local unitName = safeGet(function() return deadUnit:getName() end, "unknown") local unitType = safeGet(function() return deadUnit:getTypeName() end, "unknown") local position = safeGet(function() local pos = deadUnit:getPoint() return {x = pos.x, y = pos.y, z = pos.z} end, {x=0, y=0, z=0}) --Skip invalid units (unknown type, unavailable ID, or scenery with ID 0) if unitName == "unknown" or unitType == "unknown" or unitID == "unavailable" or unitID == 0 then if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Skipped unit ID %s with name %s and type %s at %.2f", tostring(unitID), unitName, unitType, timer.getTime())) end return end --Check if unitID is already in killfeedTable before scheduling for _, entry in ipairs(killfeedTable) do if entry.unitID == unitID then if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Skipped unit ID %s (%s) already in killfeedTable at %.2f", unitID, unitType, timer.getTime())) end return end end --Delay processing by 2 seconds to allow S_EVENT_KILL to take precedence timer.scheduleFunction(function(params) local unitID = params.unitID local unitName = params.unitName local unitType = params.unitType local position = params.position local currentTime = timer.getTime() --Re-check killfeedTable after delay to ensure no race condition for _, entry in ipairs(killfeedTable) do if entry.unitID == unitID then if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Skipped unit ID %s (%s) already in killfeedTable at %.2f", unitID, unitType, currentTime)) end return end end --Remove from splashKillfeedTable if present local splashIndex = nil for i, entry in ipairs(splashKillfeedTable) do if entry.unitId == unitID then splashIndex = i break end end if splashIndex then table.remove(splashKillfeedTable, splashIndex) if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Removed unit ID %s (%s) from splashKillfeedTable at %.2f", unitID, unitType, currentTime)) end end --Remove from splashKillfeedTemp if present local tempIndex = nil for i, entry in ipairs(splashKillfeedTemp) do if entry.unitId == unitID then tempIndex = i break end end if tempIndex then table.remove(splashKillfeedTemp, tempIndex) if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Removed unit ID %s (%s) from splashKillfeedTemp at %.2f", unitID, unitType, currentTime)) end end --Add to killfeedTable table.insert(killfeedTable, { unitName = unitName, unitType = unitType, unitID = unitID, killer = "unknown", time = currentTime, position = position }) --Display in-game message if splash_damage_options.killfeed_game_messages then local msg = string.format("%s destroyed", unitType) local status, err = pcall(function() trigger.action.outTextForCoalition(2, msg, splash_damage_options.killfeed_game_message_duration) end) if not status then trigger.action.outText(msg, splash_damage_options.killfeed_game_message_duration) if splash_damage_options.killfeed_debug then env.info("DeadFeed: Failed coalition message: " .. tostring(err)) end end end if splash_damage_options.killfeed_debug then env.info(string.format("DeadFeed: Recorded %s destroyed [ID: %s] at %.2f", unitType, unitID, currentTime)) end end, { unitID = unitID, unitName = unitName, unitType = unitType, position = position }, timer.getTime() + 2) end) if not status and splash_damage_options.killfeed_debug then env.info("DeadFeed: Error: " .. tostring(err)) end end function explodeObject(args) local point = args[1] local distance = args[2] local power = args[3] trigger.action.explosion(point, power) end function blastWave(_point, _radius, weapon, power, isShapedCharge) local weaponData = explTable[weapon] or { Skip_damage_model = false, Skip_larger_explosions = false } if isShapedCharge then _radius = _radius * splash_damage_options.shaped_charge_multiplier end if splash_damage_options.use_dynamic_blast_radius then local dynamicRadius = math.pow(power, 1/3) * 5 * splash_damage_options.dynamic_blast_radius_modifier _radius = isShapedCharge and dynamicRadius * splash_damage_options.shaped_charge_multiplier or dynamicRadius end if splash_damage_options.debug then debugMsg("blastWave called for weapon '" .. weapon .. "' at X: " .. _point.x .. ", Y: " .. _point.y .. ", Z: " .. _point.z .. " with power " .. power .. " and radius " .. _radius .. "m") end local foundUnits = {} local volS = { id = world.VolumeType.SPHERE, params = { point = _point, radius = _radius } } local ifFound = function(foundObject, val) if foundObject:getDesc().category == Unit.Category.GROUND_UNIT and foundObject:getCategory() == Object.Category.UNIT then foundUnits[#foundUnits + 1] = foundObject end if foundObject:getDesc().category == Unit.Category.GROUND_UNIT and splash_damage_options.blast_stun then --suppressUnit(foundObject, 2, weapon) --Not implemented, commented out end if splash_damage_options.wave_explosions then local obj = foundObject local obj_location = obj:getPoint() local dist = getDistance(_point, obj_location) if dist > 1 then --Avoid re-exploding at exact impact point local timing = dist / 500 if obj:isExist() and tableHasKey(obj:getDesc(), "box") then local length = (obj:getDesc().box.max.x + math.abs(obj:getDesc().box.min.x)) local height = (obj:getDesc().box.max.y + math.abs(obj:getDesc().box.min.y)) local depth = (obj:getDesc().box.max.z + math.abs(obj:getDesc().box.min.z)) local _length = length local _depth = depth if depth > length then _length = depth _depth = length end local surface_distance = dist - _depth / 2 local scaled_power_factor = 0.006 * power + 1 local intensity = (power * scaled_power_factor) / (4 * math.pi * surface_distance^2) --Apply ground ordnance blastwave modifier local weaponData = explTable[weapon] or {} if splash_damage_options.track_groundunitordnance and weaponData.groundordnance then intensity = intensity * splash_damage_options.groundunitordnance_blastwave_modifier if splash_damage_options.track_groundunitordnance_debug then debugMsg("Applied groundunitordnance_blastwave_modifier " .. splash_damage_options.groundunitordnance_blastwave_modifier .. " to " .. weapon .. ", intensity now: " .. intensity) end end local surface_area = _length * height local damage_for_surface = intensity * surface_area if splash_damage_options.debug then debugMsg("Processing unit '" .. obj:getTypeName() .. "' at dist=" .. string.format("%.1f", dist) .. "m: intensity=" .. string.format("%.4f", intensity) .. ", surface_area=" .. string.format("%.2f", surface_area) .. ", damage_for_surface=" .. string.format("%.4f", damage_for_surface)) end if damage_for_surface > splash_damage_options.cascade_damage_threshold then local explosion_size = damage_for_surface if obj:getDesc().category == Unit.Category.STRUCTURE then explosion_size = intensity * splash_damage_options.static_damage_boost end local obj_altitude_ground = getAGL(obj) -- Deal extra damage to parked airplanes and helicopters to make OCA/Aircraft missions more viable if (obj:getDesc().category == Unit.Category.AIRPLANE or obj:getDesc().category == Unit.Category.HELICOPTER) and (obj:inAir() == false or obj_altitude_ground < 50) then explosion_size = intensity * splash_damage_options.oca_aircraft_damage_boost --apply an extra damage boost for aircraft to increase kill probability on OCA/Aircraft missions. --debugMsg("static obj :"..obj:getTypeName()) end -- According to toutenglisse on DCS World forums (2022-06-11), ships do not have sensors attributes and therefore obj:hasSensors(Unit.SensorType.RADAR) cannot be used -- "I don't know why, but no Ship in DCS has ["sensors"] in its attributes (while obviously they have and can use them in game...). No way to use Ship with getDetectedTargets function (except for visual detection)." if splash_damage_options.shipRadarDamageEnable and obj:getDesc().category == Unit.Category.SHIP and antiRadiationMissile[weapon:getTypeName()] ~= nil then obj:enableEmission(false) env.info("BDA: "..event.target:getTypeName().." radar destroyed") if player ~= nil then gameMsg("BDA: "..obj:getTypeName().." radar destroyed") end end if explosion_size > power then explosion_size = power end local triggerExplosion = false if splash_damage_options.always_cascade_explode then triggerExplosion = true if splash_damage_options.debug then debugMsg("Triggering secondary explosion for '" .. obj:getTypeName() .. "' due to always_cascade_explode") end else if obj:getDesc().life then local health = obj:getLife() or 0 local maxHealth = obj:getDesc().life or 1 local healthPercent = (health / maxHealth) * 100 if splash_damage_options.debug then debugMsg("Health check for '" .. obj:getTypeName() .. "': " .. health .. "/" .. maxHealth .. " (" .. string.format("%.2f", healthPercent) .. "%) vs threshold " .. splash_damage_options.cascade_explode_threshold) end if healthPercent <= splash_damage_options.cascade_explode_threshold then triggerExplosion = true end else triggerExplosion = true if splash_damage_options.debug then debugMsg("Triggering secondary explosion for '" .. obj:getTypeName() .. "' (no life data)") end end if not triggerExplosion and obj:getDesc().category == Unit.Category.GROUND_UNIT then local health = obj:getLife() or 0 if health <= 0 then triggerExplosion = true if splash_damage_options.debug then debugMsg("Triggering secondary explosion for '" .. obj:getTypeName() .. "' (health <= 0)") end end end end --Queue cargo effects for units below if obj:getDesc().life then local healthPercent = (obj:getLife() / obj:getDesc().life) * 100 local cargoData = cargoUnits[obj:getTypeName()] if cargoData and healthPercent <= splash_damage_options.cargo_damage_threshold and splash_damage_options.enable_cargo_effects then local cargoPower = power table.insert(cargoEffectsQueue, { name = obj:getTypeName(), distance = dist, coords = obj_location, power = cargoPower, explosion = cargoData.cargoExplosion, cookOff = cargoData.cargoCookOff, cookOffCount = cargoData.cookOffCount, cookOffPower = cargoData.cookOffPower, cookOffDuration = cargoData.cookOffDuration, cookOffRandomTiming = cargoData.cookOffRandomTiming, cookOffPowerRandom = cargoData.cookOffPowerRandom, isTanker = cargoData.isTanker, flameSize = cargoData.flameSize, flameDuration = cargoData.flameDuration }) if splash_damage_options.debug then debugMsg("Queued cargo effect for '" .. obj:getTypeName() .. "' with power " .. cargoPower) end end end if triggerExplosion then local final_power = explosion_size * splash_damage_options.cascade_scaling if splash_damage_options.debug then debugMsg("Scheduling secondary explosion for '" .. obj:getTypeName() .. "' at X: " .. obj_location.x .. ", Y: " .. obj_location.y .. ", Z: " .. obj_location.z .. ", dist=" .. string.format("%.1f", dist) .. "m, power=" .. string.format("%.2f", final_power)) end if splash_damage_options.track_groundunitordnance_debug and weaponData.groundordnance then debugMsg("Calculated power for '" .. obj:getTypeName() .. "' at X: " .. obj_location.x .. ", Y: " .. obj_location.y .. ", Z: " .. obj_location.z .. ", distance " .. dist .. "m: " .. final_power) end local playerName = tracked_weapons[weapon] and tracked_weapons[weapon].init or "unknown" timer.scheduleFunction(function(args) local obj = args[1] local playerName = args[2] if obj:isExist() and obj:getLife() <= 0 then debugMsg("Unit '" .. obj:getTypeName() .. "' destroyed by secondary explosion, credited to player: " .. playerName) end end, {obj, playerName}, timer.getTime() + timing + 0.1) timer.scheduleFunction(explodeObject, {obj_location, dist, final_power}, timer.getTime() + timing) else if splash_damage_options.debug then debugMsg("No secondary explosion for '" .. obj:getTypeName() .. "': health above threshold (" .. string.format("%.2f", (obj:getLife() / obj:getDesc().life) * 100) .. "% > " .. splash_damage_options.cascade_explode_threshold .. "%)") end end else if splash_damage_options.debug then debugMsg("No secondary explosion for '" .. obj:getTypeName() .. "': damage_for_surface=" .. string.format("%.4f", damage_for_surface) .. " below threshold " .. splash_damage_options.cascade_damage_threshold) end end end end end return true end --Search all relevant object categories if splash_damage_options.debug then debugMsg("Scanning for objects within " .. _radius .. "m radius") end world.searchObjects(Object.Category.UNIT, volS, ifFound) world.searchObjects(Object.Category.STATIC, volS, ifFound) world.searchObjects(Object.Category.SCENERY, volS, ifFound) world.searchObjects(Object.Category.CARGO, volS, ifFound) if splash_damage_options.debug then debugMsg("Found " .. #foundUnits .. " ground units for damage modeling") end --Apply damage model if enabled and not skipped if splash_damage_options.damage_model and not (weaponData.Skip_damage_model or false) then timer.scheduleFunction(modelUnitDamage, foundUnits, timer.getTime() + 1.5) elseif splash_damage_options.debug and (weaponData.Skip_damage_model or false) then debugMsg("Skipped damage model application for '" .. weapon .. "' due to Skip_damage_model = true") end end function modelUnitDamage(units) for i, unit in ipairs(units) do if unit:isExist() then local health = (unit:getLife() / unit:getDesc().life) * 100 if unit:hasAttribute("Infantry") and health > 0 then if health <= splash_damage_options.infantry_cant_fire_health then unit:getController():setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) end end if unit:getDesc().category == Unit.Category.GROUND_UNIT and (not unit:hasAttribute("Infantry")) and health > 0 then if health <= splash_damage_options.unit_cant_fire_health then unit:getController():setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) --gameMsg(unit:getTypeName() .. " weapons disabled") end if health <= splash_damage_options.unit_disabled_health and health > 0 then unit:getController():setTask({id = 'Hold', params = {}}) unit:getController():setOnOff(false) --gameMsg(unit:getTypeName() .. " disabled") end end end end end function updateSplashDamageSetting(setting, increment) if not splash_damage_options[setting] then env.info("Error: Setting " .. setting .. " does not exist.") return end local newValue = math.max(0, splash_damage_options[setting] + increment) env.info("Updating " .. setting .. " from " .. tostring(splash_damage_options[setting]) .. " to " .. tostring(newValue)) splash_damage_options[setting] = newValue trigger.action.outText("Updated " .. setting .. " to: " .. tostring(splash_damage_options[setting]), 5) end function toggleSplashDamageSetting(setting) splash_damage_options[setting] = not splash_damage_options[setting] trigger.action.outText("Toggled " .. setting .. " to: " .. tostring(splash_damage_options[setting]), 5) if setting == "enable_radio_menu" then if splash_damage_options.enable_radio_menu then addSplashDamageMenu() else missionCommands.removeItem(splash_damage_menu) splash_damage_menu = nil end end end function addValueAdjustmentCommands(menu, setting, increments) for _, inc in ipairs(increments) do missionCommands.addCommand("+" .. inc, menu, updateSplashDamageSetting, setting, inc) missionCommands.addCommand("-" .. inc, menu, updateSplashDamageSetting, setting, -inc) end end function exitSplashDamageMenu() if splash_damage_menu then missionCommands.removeItem(splash_damage_menu) splash_damage_menu = nil end end function addSplashDamageMenu() if not splash_damage_options.enable_radio_menu then return end if splash_damage_menu then missionCommands.removeItem(splash_damage_menu) end splash_damage_menu = missionCommands.addSubMenu("Splash Damage Settings") --1. Debug and Messages local debugMenu = missionCommands.addSubMenu("Debug and Messages", splash_damage_menu) local debugSettings = { "game_messages", "debug", "weapon_missing_message", "track_pre_explosion_debug", "track_groundunitordnance_debug", "napalm_unitdamage_debug" } for _, setting in ipairs(debugSettings) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), debugMenu, toggleSplashDamageSetting, setting) end --2. Basic Splash Settings local splashMenu = missionCommands.addSubMenu("Basic Splash Settings", splash_damage_menu) local splashToggles = { "wave_explosions", "larger_explosions", "damage_model", "blast_stun" } for _, setting in ipairs(splashToggles) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), splashMenu, toggleSplashDamageSetting, setting) end local staticDamageMenu = missionCommands.addSubMenu("Static Damage Boost", splashMenu) addValueAdjustmentCommands(staticDamageMenu, "static_damage_boost", {100, 500, 1000}) --Submenu: Scaling and Cascading local scalingMenu = missionCommands.addSubMenu("Scaling and Cascading", splashMenu) local scalingSettings = { {name = "Overall Scaling", setting = "overall_scaling", increments = {0.1, 0.5, 1}}, {name = "Rocket Multiplier", setting = "rocket_multiplier", increments = {0.1, 0.5, 1}}, {name = "Cascade Scaling", setting = "cascade_scaling", increments = {0.1, 0.5, 1}}, {name = "Cascade Damage Threshold", setting = "cascade_damage_threshold", increments = {0.01, 0.05, 0.1}}, {name = "Cascade Explode Threshold", setting = "cascade_explode_threshold", increments = {5, 10, 25}} } for _, s in ipairs(scalingSettings) do local subMenu = missionCommands.addSubMenu(s.name, scalingMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end missionCommands.addCommand("Toggle Always Cascade Explode", scalingMenu, toggleSplashDamageSetting, "always_cascade_explode") --Submenu: Blast Radius & Shaped Charge local blastMenu = missionCommands.addSubMenu("Blast Radius & Shaped Charge", splashMenu) local blastRadiusMenu = missionCommands.addSubMenu("Blast Search Radius", blastMenu) addValueAdjustmentCommands(blastRadiusMenu, "blast_search_radius", {5, 10, 25}) missionCommands.addCommand("Toggle Dynamic Blast Radius", blastMenu, toggleSplashDamageSetting, "use_dynamic_blast_radius") local dynamicBlastMenu = missionCommands.addSubMenu("Dynamic Blast Radius Modifier", blastMenu) addValueAdjustmentCommands(dynamicBlastMenu, "dynamic_blast_radius_modifier", {0.1, 0.5, 1}) missionCommands.addCommand("Toggle Shaped Charge Effects", blastMenu, toggleSplashDamageSetting, "apply_shaped_charge_effects") local shapedChargeMenu = missionCommands.addSubMenu("Shaped Charge Multiplier", blastMenu) addValueAdjustmentCommands(shapedChargeMenu, "shaped_charge_multiplier", {0.1, 0.5, 1}) --Submenu: Units local unitsMenu = missionCommands.addSubMenu("Units", splashMenu) local unitSettings = { {name = "Unit Disabled Health", setting = "unit_disabled_health", increments = {5, 10, 25}}, {name = "Unit Can't Fire Health", setting = "unit_cant_fire_health", increments = {5, 10, 25}}, {name = "Infantry Can't Fire Health", setting = "infantry_cant_fire_health", increments = {5, 10, 25}} } for _, s in ipairs(unitSettings) do local subMenu = missionCommands.addSubMenu(s.name, unitsMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --Submenu: Ground Ordnance Tracking local groundOrdnanceMenu = missionCommands.addSubMenu("Ground Ordnance Tracking", splashMenu) missionCommands.addCommand("Toggle Ground Ordnance Tracking", groundOrdnanceMenu, toggleSplashDamageSetting, "track_groundunitordnance") local groundSettings = { {name = "Damage Modifier", setting = "groundunitordnance_damage_modifier", increments = {0.1, 0.5, 1}}, {name = "Blastwave Modifier", setting = "groundunitordnance_blastwave_modifier", increments = {0.1, 0.5, 1}}, {name = "Max Tracked Count", setting = "groundunitordnance_maxtrackedcount", increments = {5, 10, 25}} } for _, s in ipairs(groundSettings) do local subMenu = missionCommands.addSubMenu(s.name, groundOrdnanceMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end missionCommands.addCommand("Toggle 50m Scan", groundOrdnanceMenu, toggleSplashDamageSetting, "scan_50m_for_groundordnance") --3. Cargo Cook-off & Fuel Explosion local cargoMenu = missionCommands.addSubMenu("Cargo Cook-off & Fuel Explosion", splash_damage_menu) missionCommands.addCommand("Toggle Track Pre-Explosion", cargoMenu, toggleSplashDamageSetting, "track_pre_explosion") missionCommands.addCommand("Toggle Cargo Effects", cargoMenu, toggleSplashDamageSetting, "enable_cargo_effects") local cargoThresholdMenu = missionCommands.addSubMenu("Cargo Damage Threshold", cargoMenu) addValueAdjustmentCommands(cargoThresholdMenu, "cargo_damage_threshold", {5, 10, 25}) missionCommands.addCommand("Toggle Debris Effects", cargoMenu, toggleSplashDamageSetting, "debris_effects") local debrisSettings = { {name = "Debris Power", setting = "debris_power", increments = {1, 5, 10}}, {name = "Min Debris Count", setting = "debris_count_min", increments = {1, 5, 10}}, {name = "Max Debris Count", setting = "debris_count_max", increments = {1, 5, 10}}, {name = "Max Debris Distance", setting = "debris_max_distance", increments = {1, 5, 10}} } for _, s in ipairs(debrisSettings) do local subMenu = missionCommands.addSubMenu(s.name, cargoMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --Submenu: Cook-off Flares local flareMenu = missionCommands.addSubMenu("Cook-off Flares", cargoMenu) missionCommands.addCommand("Toggle Cook-off Flares", flareMenu, toggleSplashDamageSetting, "cookoff_flares_enabled") local flareColorMenu = missionCommands.addSubMenu("Flare Color", flareMenu) local flareColors = { {name = "Green", value = 0}, {name = "White", value = 1}, {name = "Red", value = 2}, {name = "Yellow", value = 3} } for _, color in ipairs(flareColors) do missionCommands.addCommand(color.name, flareColorMenu, function() splash_damage_options.cookoff_flare_color = color.value trigger.action.outText("Cook-off flare color set to " .. color.name, 5) end) end local flareCountMenu = missionCommands.addSubMenu("Flare Count Modifier", flareMenu) addValueAdjustmentCommands(flareCountMenu, "cookoff_flare_count_modifier", {0.1, 0.5, 1}) local flareOffsetMenu = missionCommands.addSubMenu("Flare Offset", flareMenu) addValueAdjustmentCommands(flareOffsetMenu, "cookoff_flare_offset", {1, 5, 10}) --Submenu: All Vehicles Options local allVehiclesMenu = missionCommands.addSubMenu("All Vehicles Options", cargoMenu) local vehicleToggles = { "smokeandcookoffeffectallvehicles", "allunits_enable_smoke", "allunits_enable_cookoff" } for _, setting in ipairs(vehicleToggles) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), allVehiclesMenu, toggleSplashDamageSetting, setting) end local vehicleSettings = { {name = "Explosion Power", setting = "allunits_explode_power", increments = {5, 10, 25}}, {name = "Default Flame Size", setting = "allunits_default_flame_size", increments = {1, 5, 10}}, {name = "Default Flame Duration", setting = "allunits_default_flame_duration", increments = {5, 10, 25}}, {name = "Cook-off Count", setting = "allunits_cookoff_count", increments = {1, 5, 10}}, {name = "Cook-off Duration", setting = "allunits_cookoff_duration", increments = {5, 10, 25}}, {name = "Cook-off Power", setting = "allunits_cookoff_power", increments = {5, 10, 25}}, {name = "Cook-off Power Random", setting = "allunits_cookoff_powerrandom", increments = {5, 10, 25}} } for _, s in ipairs(vehicleSettings) do local subMenu = missionCommands.addSubMenu(s.name, allVehiclesMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --4. Ordnance Protection & Cluster local ordnanceMenu = missionCommands.addSubMenu("Ordnance Protection & Cluster", splash_damage_menu) local ordnanceToggles = { "ordnance_protection", "detect_ordnance_destruction", "snap_to_ground_if_destroyed_by_large_explosion", "recent_large_explosion_snap" } for _, setting in ipairs(ordnanceToggles) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), ordnanceMenu, toggleSplashDamageSetting, setting) end local ordnanceSettings = { {name = "Ordnance Protection Radius", setting = "ordnance_protection_radius", increments = {5, 10, 25}}, {name = "Max Snapped Height", setting = "max_snapped_height", increments = {5, 10, 25}}, {name = "Recent Explosion Range", setting = "recent_large_explosion_range", increments = {5, 10, 25}}, {name = "Recent Explosion Time", setting = "recent_large_explosion_time", increments = {1, 5, 10}} } for _, s in ipairs(ordnanceSettings) do local subMenu = missionCommands.addSubMenu(s.name, ordnanceMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --Submenu: Cluster Bombs local clusterMenu = missionCommands.addSubMenu("Cluster Bombs", ordnanceMenu) missionCommands.addCommand("Toggle Cluster Enabled", clusterMenu, toggleSplashDamageSetting, "cluster_enabled") local clusterSettings = { {name = "Cluster Base Length", setting = "cluster_base_length", increments = {25, 50, 100}}, {name = "Cluster Base Width", setting = "cluster_base_width", increments = {25, 50, 100}}, {name = "Cluster Max Length", setting = "cluster_max_length", increments = {25, 50, 100}}, {name = "Cluster Max Width", setting = "cluster_max_width", increments = {25, 50, 100}}, {name = "Cluster Min Length", setting = "cluster_min_length", increments = {25, 50, 100}}, {name = "Cluster Min Width", setting = "cluster_min_width", increments = {25, 50, 100}}, {name = "Bomblet Damage Modifier", setting = "cluster_bomblet_damage_modifier", increments = {1, 5, 10}} } for _, s in ipairs(clusterSettings) do local subMenu = missionCommands.addSubMenu(s.name, clusterMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end missionCommands.addCommand("Toggle Bomblet Reduction", clusterMenu, toggleSplashDamageSetting, "cluster_bomblet_reductionmodifier") --5. Giant Explosions local giantExplosionMenu = missionCommands.addSubMenu("Giant Explosions", splash_damage_menu) local giantToggles = { "giant_explosion_enabled", "giantexplosion_ondamage", "giantexplosion_ondeath", } for _, setting in ipairs(giantToggles) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), giantExplosionMenu, toggleSplashDamageSetting, setting) end local giantSettings = { {name = "Explosion Power", setting = "giant_explosion_power", increments = {500, 1000, 2000}}, {name = "Size Scale", setting = "giant_explosion_scale", increments = {0.1, 0.5, 1, 2}}, {name = "Duration", setting = "giant_explosion_duration", increments = {0.1, 0.5, 1, 2}}, {name = "Explosion Count", setting = "giant_explosion_count", increments = {25, 50, 100}}, } for _, s in ipairs(giantSettings) do local subMenu = missionCommands.addSubMenu(s.name, giantExplosionMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --Test Explosion Menu (Dynamic Scan) local function scanForTestTargets() local testTargets = {} local function processObject(obj) if obj:isExist() then local name = obj:getName() if string.find(name, "GiantExplosionTarget") then table.insert(testTargets, {name = name, pos = obj:getPoint()}) debugMsg("Found GiantExplosionTarget for test: " .. name) end end end for coa = 0, 2 do local groups = coalition.getGroups(coa) if groups then for _, group in pairs(groups) do local units = group:getUnits() if units then for _, unit in pairs(units) do processObject(unit) end end end end local statics = coalition.getStaticObjects(coa) if statics then for _, static in pairs(statics) do processObject(static) end end end return testTargets end local testExplosionMenu = missionCommands.addSubMenu("Test Explosions", giantExplosionMenu) missionCommands.addCommand("Scan and Detonate Targets", testExplosionMenu, function() local testTargets = scanForTestTargets() if #testTargets == 0 then gameMsg("No GiantExplosionTarget units found!") return end for _, target in ipairs(testTargets) do missionCommands.addCommand("Detonate " .. target.name, testExplosionMenu, function() triggerGiantExplosion({ pos = target.pos, power = splash_damage_options.giant_explosion_power, scale = splash_damage_options.giant_explosion_scale, duration = splash_damage_options.giant_explosion_duration, count = splash_damage_options.giant_explosion_count }) end) end missionCommands.addCommand("Detonate All Giant Targets", testExplosionMenu, function() for _, target in ipairs(testTargets) do triggerGiantExplosion({ pos = target.pos, power = splash_damage_options.giant_explosion_power, scale = splash_damage_options.giant_explosion_scale, duration = splash_damage_options.giant_explosion_duration, count = splash_damage_options.giant_explosion_count }) end end) end) --6. Napalm local napalmMenu = missionCommands.addSubMenu("Napalm", splash_damage_menu) local napalmToggles = { "napalm_mk77_enabled", "napalmoverride_enabled", "napalm_phosphor_enabled", "napalm_addflame" } for _, setting in ipairs(napalmToggles) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), napalmMenu, toggleSplashDamageSetting, setting) end --Submenu: Spread/Phosphor/Flame local spreadPhosphorFlameMenu = missionCommands.addSubMenu("Spread/Phosphor/Flame", napalmMenu) local napalmSettings = { {name = "Spread Points", setting = "napalm_spread_points", increments = {1, 2, 3}}, {name = "Spread Spacing", setting = "napalm_spread_spacing", increments = {1, 5, 10}}, {name = "Phosphor Multiplier", setting = "napalm_phosphor_multiplier", increments = {0.1, 0.5, 1}}, {name = "Flame Duration", setting = "napalm_addflame_duration", increments = {10, 30, 60}} } for _, s in ipairs(napalmSettings) do local subMenu = missionCommands.addSubMenu(s.name, spreadPhosphorFlameMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end local napalmFlameSizeMenu = missionCommands.addSubMenu("Flame Size", spreadPhosphorFlameMenu) for i = 1, 8 do missionCommands.addCommand("Set " .. i, napalmFlameSizeMenu, function() splash_damage_options.napalm_addflame_size = i trigger.action.outText("Napalm flame size set to " .. i, 5) end) end --Submenu: Delay Settings local napalmDelayMenu = missionCommands.addSubMenu("Delay Settings", napalmMenu) local napalmDelaySettings = { {name = "Explode Delay", setting = "napalm_explode_delay", increments = {0.01, 0.05, 0.1}}, {name = "Destroy Delay", setting = "napalm_destroy_delay", increments = {0.01, 0.05, 0.1}}, {name = "Flame Delay", setting = "napalm_flame_delay", increments = {0.01, 0.05, 0.1}} } for _, s in ipairs(napalmDelaySettings) do local subMenu = missionCommands.addSubMenu(s.name, napalmDelayMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --Submenu: DoubleWide local doubleWideMenu = missionCommands.addSubMenu("DoubleWide", napalmMenu) missionCommands.addCommand("Toggle DoubleWide Enabled", doubleWideMenu, toggleSplashDamageSetting, "napalm_doublewide_enabled") local doubleWideSpreadMenu = missionCommands.addSubMenu("DoubleWide Spread", doubleWideMenu) addValueAdjustmentCommands(doubleWideSpreadMenu, "napalm_doublewide_spread", {1, 5, 10}) --Submenu: Unit Damage local unitDamageMenu = missionCommands.addSubMenu("Unit Damage", napalmMenu) missionCommands.addCommand("Toggle Unit Damage Enabled", unitDamageMenu, toggleSplashDamageSetting, "napalm_unitdamage_enable") missionCommands.addCommand("Toggle Infantry Fire", unitDamageMenu, toggleSplashDamageSetting, "napalm_unitdamage_infantryfire") local scanDistanceMenu = missionCommands.addSubMenu("Scan Distance", unitDamageMenu) addValueAdjustmentCommands(scanDistanceMenu, "napalm_unitdamage_scandistance", {20, 25, 50}) local startDelayMenu = missionCommands.addSubMenu("Start Delay", unitDamageMenu) addValueAdjustmentCommands(startDelayMenu, "napalm_unitdamage_startdelay", {0.1, 0.2, 0.5}) local spreadDelayMenu = missionCommands.addSubMenu("Spread Delay", unitDamageMenu) addValueAdjustmentCommands(spreadDelayMenu, "napalm_unitdamage_spreaddelay", {0.1, 0.2, 0.5}) --7. Vehicle IEDs local vehicleIEDMenu = missionCommands.addSubMenu("Vehicle IEDs", splash_damage_menu) local vehicleIEDToggles = { "vehicleied_enabled", "vehicleied_fueltankspawn", "vehicleied_destroy_vehicle", "vehicleied_explode_on_hit" } for _, setting in ipairs(vehicleIEDToggles) do missionCommands.addCommand("Toggle " .. setting:gsub("_", " "), vehicleIEDMenu, toggleSplashDamageSetting, setting) end local explosionPowerCountMenu = missionCommands.addSubMenu("Explosion Power and Count", vehicleIEDMenu) local explosionPowerCountSettings = { {name = "Central Power", setting = "vehicleied_central_power", increments = {50, 100, 200}}, {name = "Explosion Power", setting = "vehicleied_explosion_power", increments = {50, 100, 200}}, {name = "Explosion Count Min", setting = "vehicleied_explosion_count_min", increments = {1, 2, 5}}, {name = "Explosion Count Max", setting = "vehicleied_explosion_count_max", increments = {1, 2, 5}} } for _, s in ipairs(explosionPowerCountSettings) do local subMenu = missionCommands.addSubMenu(s.name, explosionPowerCountMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end local otherVehicleIEDSettings = { {name = "Radius", setting = "vehicleied_radius", increments = {5, 10, 20}}, {name = "Power Variance", setting = "vehicleied_power_variance", increments = {0.1, 0.2, 0.5}}, {name = "Explosion Delay Max", setting = "vehicleied_explosion_delay_max", increments = {0.1, 0.2, 0.5}}, {name = "Scaling", setting = "vehicleied_scaling", increments = {0.1, 0.5, 1}} } for _, s in ipairs(otherVehicleIEDSettings) do local subMenu = missionCommands.addSubMenu(s.name, vehicleIEDMenu) addValueAdjustmentCommands(subMenu, s.setting, s.increments) end --8. Exit Menu missionCommands.addCommand("Exit Splash Damage Menu", splash_damage_menu, exitSplashDamageMenu) end if (script_enable == 1) then gameMsg("SPLASH DAMAGE 3.4 SCRIPT RUNNING") env.info("SPLASH DAMAGE 3.4 SCRIPT RUNNING") timer.scheduleFunction(function() protectedCall(track_wpns) return timer.getTime() + refreshRate end, {}, timer.getTime() + refreshRate) if splash_damage_options.killfeed_enable then world.addEventHandler({ onEvent = function(self, event) protectedCall(onKillEvent, event) end }) --Add kill event handler end if splash_damage_options.trophy_enabled then for wpnName, data in pairs(trophyWeapons) do --preload lowercase trophy weapons table trophyWeaponsLookup[string.lower(wpnName)] = data end world.addEventHandler(trophyHandler) --initiate trophyhandler end world.addEventHandler(WpnHandler) addSplashDamageMenu() --Lekas integration if splash_damage_options.killfeed_enable and splash_damage_options.killfeed_lekas_foothold_integration then timer.scheduleFunction(processSplashKillfeed, {}, timer.getTime() + 60) if splash_damage_options.killfeed_debug then env.info("SplashDamage: Scheduled process SplashKillfeed for Lekas Foothold integration") end end --Strobe if splash_damage_options.StrobeMarker_allstrobeunits then scanStrobeUnits() timer.scheduleFunction(triggerStrobeMarker, {}, timer.getTime() + splash_damage_options.StrobeMarker_interval) --env.info("SPLASH DAMAGE: StrobeMarker initialized with interval " .. splash_damage_options.StrobeMarker_interval .. " seconds") end if splash_damage_options.StrobeMarker_individuals then scanStrobeUnits() initIndividualStrobeUnits() createStrobeRadioMenu() --env.info("SPLASH DAMAGE: Individual StrobeMarker initialized") end end --[[-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-= ##### Changelog ##### -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- =--=-=-=-=-=-=-= 24th May 2025 - 3.3 (Stevey666) - Added some naval weapons into weapon/expl table - Added some ground unit ordnance to explosive table and allowing a wider area to be tracked - Game_mesages and enable_radio_menu options defaulted to false. -Please be advised that the non debug script has these two defaulted to false, so that users don't see that the script is in use nor can they access the test/config radio options.  -Set either to true as required.   The notice that the Splash Damage 3.x is running uses game_messsages. - Overhauled the radio options - Added optional cook-off effect - signal flares firing at random throughout the cook-off (see cookoff_flares_enabled). Not sure if I like this one so leaving in as optional - Reduced cargo cook off initial explosion values as they were a little too high - New feature: Napalm. MK77 A4 Skyhawk Napalm and Optional Napalm weapon override - Allows napalm effects, overriding specific weapons set in options is possible too. - This feature has been adapated from titi69's Napalm script https://www.digitalcombatsimulator.com/en/files/3340469/ , credit to him and the Olympus mod team for the Napalm method (Sniex) - Added weapon types in the weapon/expl - Adjusted some rocket explosive power numbers (+1 or 2) - Adjusted explosive power for anti radar, ship missile, cruise missile and some others - Increased script readability (Kurdes) - Added changed/missing JF17 ordnance to weapons table - Added JF29 mod ordnance to the weapons table 10 May 2025 (Stevey666) - 3.2 - New feature (user request): ground ordnance tracking, this tracks ground artillery etc if in the explosives table, set to false by default. - New feature (user request): option to create additional smoke and cargo cookoff effect for all ground vehicles initially destroyed by your ordnance or the script, set to false by default. - Adjusted blastwave explosion - Changes to debug output, ordering by vehicle distance - Thanks to tae. for the report, adjusted Ural-4320 in vehicle table, had incorrect name so wasn't triggering cook off. - Fixed error popup when using Mig 21's SPRD-99 - Added Cargo Cook off / fireball to some static objects i.e crates/barrels - Reworked Giant Explosion tracking - no mission editor trigger needed, just name static unit or unit "GiantExplosionTarget[X]" - Allow for Giant Explosion trigger on damage or on death 04 April 2025 (Stevey666) - 3.1 - Set default cluster munitions option to false, set this to true in the options if you want it - Added missing radio commands for Cascade Scaling - Adjust default cascading to 2 (from 1) - Adjusted Ural-4320 to be a tanker and ammo carrier for cargo cookoff - Prevent weapons not in the list from being tracked - Moved some logging behind the debug mode flag - Ordnance Protection, added a max height ordnance protection will snap explosion to ground - Ordnance Protection, fixed enable/disable option - Added Giant Explosion feature - Adjusted some hydra70 values on recom. from ETBSmorgan 09 March 2025 (Stevey666) - 3.0 - Added ordinance protection gives a few options - stop the additional larger_explosion that tends to blow up your own bombs if theyre dropped at the same place if its within x m - Additional ordnance protection option that will cause a snap to ground larger_explosion if its within x meters of a recent larger explosion and within x seconds (can set in options) - Added vehicle scanning around a weapon to allow for.. - Cook offs - you can set vehicles that will cook off i.e ammo trucks, number of explosions, debris explosions, power adjustable - Fuel/Tanker explosion and flames - when a fuel tanker blows it will through up a big flame - adjustable in the scripts - Added section for vehicles for the above - Added radio commands for everything - Added in cluster munitions changes (note: barely tested, its not particularly accurate or that useful at this point so leaving disabled) - Potential bug - testing, stacking too many units together may cause a MIST error if you're using mist - Setting this as 3.0 as I'd like to be responsive to requests, updates etc - creating a new fork to track this 10 Feb 2025 (Stevey666) - 2.0.7 - Fixed AGM 154/Adjusted weapons - Added overall damage scaling - Added modifier for shaped charges (i.e. Mavericks), adjusted weapon list accordingly - Adjusted blast radius and damage calculations, created option for dynamic blast radius - Adjusted cascading explosions, added additional "cascade_scaling" modifier and cascade explode threshold modifier. Units wont explode on initial impact unless health drops under threshold - Added always_cascade_explode option so you can set it to the old ways of making everything in the blast wave go kaboom - Added in game radio commands to change the new options ingame without having to reload everything in mission editor to test it out 12 November 2024 (by JGi | Quéton 1-1) - Tweak down radius 100>90 (Thanks Arhibeau) - Tweak down some values 20 January 2024 (by JGi | Quéton 1-1) - Added missing weapons to explTable - Sort weapons in explTable by type - Added aircraft type in log when missing 03 May 2023 (KERV) Correction AGM 154 (https://forum.dcs.world/topic/289290-splash-damage-20-script-make-explosions-better/page/5/#comment-5207760) 06 March 2023 (Kerv) - Add some data for new ammunition 16 April 2022 spencershepard (GRIMM): - Added new/missing weapons to explTable - Added new option rocket_multiplier 31 December 2021 spencershepard (GRIMM): - Added many new weapons - Added filter for weapons.shells events - Fixed mission weapon message option - Changed default for damage_model option 21 December 2021 spencershepard (GRIMM): SPLASH DAMAGE 2.0: - Added blast wave effect to add timed and scaled secondary explosions on top of game objects - Object geometry within blast wave changes damage intensity - Damage boost for structures since they are hard to kill, even if very close to large explosions - Increased some rocket values in explTable - Missing weapons from explTable will display message to user and log to DCS.log so that we can add what's missing - Damage model for ground units that will disable their weapons and ability to move with partial damage before they are killed - Added options table to allow easy adjustments before release - General refactoring and restructure 28 October 2020 FrozenDroid: - Uncommented error logging, actually made it an error log which shows a message box on error. - Fixed the too restrictive weapon filter (took out the HE warhead requirement) 2 October 2020 FrozenDroid: - Added error handling to all event handler and scheduled functions. Lua script errors can no longer bring the server down. - Added some extra checks to which weapons to handle, make sure they actually have a warhead (how come S-8KOM's don't have a warhead field...?) -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-= ##### END of Changelog ##### -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-]]