ZoneAttackAlert

Version: v2.1.1

A DCS World Lua mission script that sends coalition-specific attack alerts when enemy aircraft destroy friendly ground units inside defined trigger zones.

Overview

ZoneAttackAlert monitors the DCS World event stream for S_EVENT_KILL events, filters them according to strict criteria (aircraft attacker, ground victim, opposite coalitions, zone containment), and broadcasts location-aware alert messages to the affected coalition. Blue coalition receives distances in nautical miles (NM); Red coalition receives distances in kilometers (KM).

Features

Logic Flow Diagrams

Initialization Flow

flowchart TD A[Script loaded] --> B[Localize all globals] B --> C[Parse config.zone_names] C --> D{For each zone name} D -->|Trigger found| E[Cache zone point x,z and radius_sq] E --> F[Split into zone_cache_blue or zone_cache_red by prefix] F --> D D -->|No more zones| G[Fetch world.getAirbases] G --> H{For each airbase} H --> I[Cache obj, x, z, name] I --> H H -->|No more airbases| J[Register world event handler] J --> K[Monitor S_EVENT_KILL]

Event Handler Flow

flowchart TD A["S_EVENT_KILL fired"] --> B["pcall wrapper catches errors"] B --> C{"event.id is S_EVENT_KILL?"} C -->|No| D["Return silently"] C -->|Yes| E{"initiator and target exist?"} E -->|No| D E -->|Yes| F{"initiator is AIRPLANE or HELICOPTER?"} F -->|No| D F -->|Yes| G{"target is GROUND_UNIT?"} G -->|No| D G -->|Yes| H{"target coalition differs from initiator?"} H -->|No - friendly fire| D H -->|Yes - enemy action| I{"target position exists?"} I -->|No| D I -->|Yes| J["Select zone cache by victim coalition"] J --> K{"For each cached zone"} K -->|Outside| K K -->|Inside| L["active_zone_name = zone.name"] L --> M["Cooldown check timer.getTime"] M -->|On cooldown| D M -->|Ready| N["Build and send message"] N --> O["trigger.action.outTextForCoalition"]

Airbase Distance Lookup

flowchart TD A["Receive victim px, pz and coalition"] --> B["closest_ab = nil, min_dist_sq = math.huge"] B --> C{"For each cached airbase"} C --> D["dx = px - ab.x, dz = pz - ab.z"] D --> E["d_sq = dx^2 + dz^2"] E --> F{"d_sq less than min_dist_sq?"} F -->|Yes| G["Update closest_ab and min_dist_sq"] G --> C F -->|No| C C -->|End loop| H{"closest_ab found?"} H -->|No| I["Return LOCATION UNKNOWN"] H -->|Yes| J["dist_m = sqrt(min_dist_sq)"] J --> K["atan2 for angle_deg"] K --> L["get_cardinal(angle_deg)"] L --> M{"Coalition equals BLUE?"} M -->|Yes| N["dist_nm = dist_m * 0.000539957"] M -->|No| O["dist_km = dist_m / 1000"] N --> P{"dist less than 1?"} O --> Q{"dist less than 1?"} P -->|Yes| R["Return < 1 NM cardinal from name"] P -->|No| S["Return %.0f NM cardinal from name"] Q -->|Yes| T["Return < 1 KM cardinal from name"] Q -->|No| U["Return %.0f KM cardinal from name"]

Cooldown Logic

flowchart TD A["zone_name identified"] --> B{"last_alert_time for zone exists?"} B -->|No| C["Allow alert"] B -->|Yes| D{"now - last < cooldown?"} D -->|Yes 180s| E["Return silently / ignore"] D -->|No| C C --> F["last_alert_time[zone] = now"] F --> G["Continue to message construction"]

Message Construction Flow

flowchart TD A["get_country_noun for initiator"] --> B["attacker_noun"] B --> C["Format: Friendly troops under %s air attack!"] C --> D["get_location_string for victim_point and victim_coal"] D --> E["msg_top + newline + msg_bot"] E --> F["trigger.action.outTextForCoalition(victim_coal, final_msg)"] F --> G["Message displayed on screen"]

Fixes in v2.1

Performance Optimizations

FixDescriptionImpact
Pre-cached zonestrigger.misc.getZone() resolved once at startup into Lua tablesEliminates 10 C++ API calls per zone check
Coalition-split zonesBT* zones cached separately from RT* zones; handler only scans relevant half~50% fewer zone iterations per event
Pre-cached airbasesworld.getAirbases() + :getPoint() resolved once at startupEliminates 20-40 C++ API calls per location lookup
Localized globalsmath.floor, string.format, timer.getTime, ipairs, env.info cached to locals at file scopeFaster table lookups, no repeated global resolution
Inlined helpersSmall single-call functions kept inline where hotReduced function call overhead per alert
Pure-Lua distance checkCompares squared distance from cached zone pointsNo math.sqrt() inside loop
Single format callMessage built in one string.format instead of concatenating twoFewer string allocations

Logic Improvements

FixDescription
Aircraft-only filterUnit.Category.AIRPLANE or Unit.Category.HELICOPTER required; missiles, artillery, ships ignored
Ground-unit victimUnit.Category.GROUND_UNIT required; air-to-air kills ignored
No friendly fireSame-coalition initiator/victim silently skipped
Distance rounding fix< 1 NM / < 1 KM instead of "0 NM" / "0 KM" for sub-unit distances
pcall wrapperEvent handler body wrapped; errors logged to env.info without breaking mission scripts

Style / Compliance

FixDescription
snake_caseAll variables and functions converted (message_cooldown, get_location_string, etc.)
PascalCase modulesHandler table follows convention
2-space indentEntire file reformatted
120 char line limitLong expressions broken
Local preferenceNo globals leaked; all functions and state local

Configuration

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
    heartbeatEnabled   = false, -- periodic "ZAA script is still running ..." in-game message
    heartbeatInterval  = 10,    -- seconds between heartbeat messages
}

Zone prefixes:

Script Architecture

ZoneAttackAlert.lua
├── ZAA CONFIGURATION (table, user-editable)
├── GLOBAL LOCALIZATION (captured locals for hot path)
├── STATIC DATA (country_to_noun, cardinal_directions)
├── ZAA.ZONE CACHE (built at load time, split by coalition)
├── ZAA.AIRBASE CACHE (built at load time, includes ALL airbases; lookup is geographical proximity)
├── ZAA.STATE (last_alert_time table)
├── HELPERS (get_country_noun, get_cardinal, get_location_string)
└── EVENT HANDLER (onEvent + handle_event)

Installation

  1. Copy ZoneAttackAlert.lua to your DCS mission's trigger DO SCRIPT action or load it via require in your mission scripting
  2. Ensure trigger zones named RT1RT5 and BT1BT5 exist in your mission
  3. Run the mission; alerts appear automatically when criteria are met

Dependencies

Version History

VersionChanges
v1.xOriginal: camelCase, runtime API calls, all unit types, no pcall, distance printed "0 NM/KM"
v2.0Optimized caches, aircraft/ground-only filtering, snake_case, pcall guards, < 1 distance fix, coalition-split zones
v2.1.1Heartbeat option added (heartbeatEnabled, heartbeatInterval). Heartbeat and script-load messages now sent to all players in-game. Script-load message also logged.
v2.1Removed redundant isExist() and getCategory() checks (simplified guard chain); namespaced config/state into ZAA table; airbase cache now documented as geographical proximity