dcs-retribution/resources/plugins/splashdamage3/Splash_Damage_3.4.2_Standard_Retribution.lua
Drexyl 1ad27e27ea
Update Splash Damage Plugin to 3.4.2 (#601)
Co-authored-by: Druss99 <42724070+Druss99@users.noreply.github.com>
2025-11-08 14:35:17 +00:00

7750 lines
453 KiB
Lua
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=
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 #####
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-]]