Version: v3.3.1
A DCS World Lua mission script that sends coalition-specific attack alerts when ground or ship units inside defined trigger zones are hit.
ZoneAttackAlert monitors the DCS World event stream for S_EVENT_HIT events. When a ground unit or ship inside a configured trigger zone is hit, the script broadcasts a location-aware alert message to the coalition that owns the actual unit being hit (read dynamically via target:getCoalition()). Blue coalition receives distances in nautical miles (NM) and English alert text; Red coalition receives distances in kilometers (KM) and German alert text. A per-zone cooldown timer prevents message spam.
Unit.Category.GROUND_UNIT and Unit.Category.SHIPALERT: Friendly forces under attack!); German for Red (ALARM: Freundliche Kräfte unter Beschuss!)config.debug to emit [ZAA] prefixed diagnostics to DCS.log| Change | Description |
|---|---|
| Remove prefix-based routing | Zone names (RT* / BT*) no longer determine alert recipient; target:getCoalition() is used instead |
| Dynamic location strings | build_zone_loc called at alert-time instead of pre-cached at startup, so units/NM vs KM match the actual recipient |
| German alert text for Red | Blue receives English "Friendly forces under attack!"; Red receives German "Freundliche Kräfte unter Beschuss!" |
| Multi-coalition zone support | Units from either coalition inside any zone now alert their actual owner |
ZAA.config = {
zone_names = {
"RT1", "RT2", "RT3", "RT4", "RT5",
"BT1", "BT2", "BT3", "BT4", "BT5"
},
message_cooldown = 180, -- seconds per zone
message_duration = 18, -- seconds on screen
debug = false, -- enable DCS.log diagnostics
}
Zone prefixes:
RT* — Red Team trigger zonesBT* — Blue Team trigger zonesNote: Prefixes are for organisational convenience only. Alert routing is determined by
target:getCoalition()at runtime, so a Blue unit inside anRT*zone will alert Blue, and a Red unit inside aBT*zone will alert Red.
Set debug = true in the config table to emit detailed diagnostics to the DCS log file. When enabled the script logs every step of event processing, including zone cache initialization, hit-event receipt, target category validation, distance checks against each zone, cooldown state, the detected target coalition, and the final alert dispatch. This produces zero runtime overhead when disabled.
ZoneAttackAlert.lua
├── RELOAD GUARD (skip if ZAA.loaded already set)
├── ZAA CONFIGURATION (table, user-editable)
├── GLOBAL LOCALIZATION (captured locals for hot path)
├── STATIC DATA (cardinal_directions)
├── ZONE CACHE (built at load time; geometry only, no coalition)
├── AIRBASE CACHE (built at load time; nearest-airbase lookup)
├── build_zone_loc() (runtime location formatter, NM for Blue / KM for Red)
│ └── Compass bearing: atan2(East, North) using DCS axes (+x=N, +z=E)
├── STATE (last_alert_time table)
└── EVENT HANDLER (onEvent -> S_EVENT_HIT only, stored in ZAA.event_handler)
ZoneAttackAlert uses the DCS world coordinate system for all bearing and distance calculations:
The compass bearing from North is computed with the standard formula:
local north_offset = px - airbase.x -- positive when target is North
local east_offset = pz - airbase.z -- positive when target is East
local bearing_deg = math.deg(math.atan2(east_offset, north_offset)) % 360
This follows the DCS scripting engine FAQ definition (atan2(East, North)) and requires no additional rotational offsets. The result is a clockwise bearing from North in the range [0, 360).
ZoneAttackAlert.lua to your DCS mission's trigger DO SCRIPT action or load it via require in your mission scriptingRT1–RT5 and BT1–BT5 exist in your missionworld.addEventHandler)| Version | Changes |
|---|---|
| v1.x | Original: camelCase, runtime API calls, all unit types, no pcall, distance printed "0 NM/KM" |
| v2.0 | Optimized caches, aircraft/ground-only filtering, snake_case, pcall guards, < 1 distance fix, coalition-split zones |
| v2.1 | Removed redundant isExist() and getCategory() checks; namespaced config/state into ZAA table |
| v2.1.1 | Heartbeat option added (heartbeatEnabled, heartbeatInterval) |
| v2.3 | Per-coalition cooldown; pre-built zone callouts; removed dead get_location_string function |
| v2.4 | Per-zone cooldown (replaces per-coalition); cooldown check moved into zone scan loop |
| v2.5 | Overlapping zone scan fix; heartbeat timer drift fix; deduplicated zone cache build |
| v2.6 | Reload guard; cardinal direction fix; getDesc() nil guard; removed dedup registry |
| v2.6.1 | Simplified event chain; overlapping zone scan fix; slimmed handler |
| v2.7 | S_EVENT_DEAD fallback; generic "enemy forces" alerts; unified find_zone/send_alert path |
| v3.0 | Radical simplification: S_EVENT_HIT only, no friendly-fire filter, ground+ship targets, single zone cache, single callout per zone, no heartbeat, 50% fewer lines |
| v3.1 | Defensive coding fix: getDesc() guarded against nil (event.target can be destroyed before getDesc completes); ensures S_EVENT_HIT handler never crashes on deleted/damaged units |
| v3.2 | Debug diagnostics: togglable config.debug boolean; structured [ZAA] prefix logging to DCS.log for zone cache, hit events, target validation, zone distance checks, cooldown state, and alert dispatch; zero-cost when disabled |
| v3.3 | Robust coalition detection: alert routing now uses target:getCoalition() at runtime; removed prefix-based .coal field from zone cache; German alert text + KM units for Red coalition; English + NM units for Blue; dynamic location string built per-alert |
| v3.3.1 | Localized cardinal directions: translated cardinal directions into German for the Red coalition (e.g., "nordöstlich") to prevent mixed-language alert messages. |