mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
584 lines
18 KiB
Lua
584 lines
18 KiB
Lua
guardianAngel = {}
|
|
guardianAngel.version = "2.0.3"
|
|
guardianAngel.ups = 10
|
|
guardianAngel.launchWarning = true -- detect launches and warn pilot
|
|
guardianAngel.intervention = true -- remove missiles just before hitting
|
|
guardianAngel.explosion = -1 -- small poof when missile explodes. -1 = off.
|
|
guardianAngel.verbose = false -- debug info
|
|
guardianAngel.announcer = true -- angel talks to you
|
|
guardianAngel.private = false -- angel only talks to group
|
|
guardianAngel.autoAddPlayers = true
|
|
|
|
guardianAngel.requiredLibs = {
|
|
"dcsCommon", -- always
|
|
"cfxZones", -- Zones, of course
|
|
}
|
|
|
|
--[[--
|
|
Version History
|
|
1.0.0 - Initial version
|
|
2.0.0 - autoAddPlayer
|
|
- verbose
|
|
- lib check
|
|
- config zone
|
|
- sneaky detection logic
|
|
- god intervention 100m
|
|
- detect re-acquisition
|
|
- addUnitToWatch supports string names
|
|
- detect non-miss
|
|
- announcer
|
|
- intervene optional
|
|
- launch warning optional
|
|
2.0.1 - warnings go to group
|
|
- invokeCallbacks added to module
|
|
- reworked CB structure
|
|
- private option
|
|
2.0.2 - poof! explosion option to show explosion on intervention
|
|
- can be dangerous
|
|
2.0.3 - fxDistance
|
|
- mea cupa capability
|
|
|
|
|
|
This script detects missiles launched against protected aircraft an
|
|
removes them when they are about to hit
|
|
|
|
--]]--
|
|
|
|
guardianAngel.minMissileDist = 50 -- m. below this distance the missile is killed by god, not the angel :)
|
|
guardianAngel.myEvents = {1, 15, 20, 21, 23, 2} -- 1 - shot, 15 - birth, 20 - enter unit, 21 - player leave unit, 23 - start shooting
|
|
-- added 2 (hit) event to see if angel was defeated
|
|
guardianAngel.safetyFactor = 1.8 -- for calculating dealloc range
|
|
guardianAngel.unitsToWatchOver = {} -- I'll watch over these
|
|
|
|
guardianAngel.missilesInTheAir = {} -- missiles in the air
|
|
|
|
guardianAngel.callBacks = {} -- callbacks
|
|
-- callback signature: callBack(reason, targetUnitName, weaponName)
|
|
-- reasons (string): "launch", "miss", "reacquire", "trackloss", "disappear", "intervention"
|
|
|
|
function guardianAngel.addCallback(theCallback)
|
|
if theCallback then
|
|
table.insert(guardianAngel.callBacks, theCallback)
|
|
end
|
|
end
|
|
|
|
function guardianAngel.invokeCallbacks(reason, targetName, weaponName)
|
|
for idx, theCB in pairs(guardianAngel.callBacks) do
|
|
theCB(reason, targetName, weaponName)
|
|
end
|
|
end
|
|
|
|
--
|
|
-- units to watch
|
|
--
|
|
function guardianAngel.addUnitToWatch(aUnit)
|
|
if type(aUnit) == "string" then
|
|
aUnit = Unit.getByName(aUnit)
|
|
end
|
|
if not aUnit then return end
|
|
local unitName = aUnit:getName()
|
|
guardianAngel.unitsToWatchOver[unitName] = aUnit
|
|
if guardianAngel.verbose then
|
|
trigger.action.outText("+++gA: now watching unit " .. aUnit:getName(), 30)
|
|
end
|
|
end
|
|
|
|
function guardianAngel.removeUnitToWatch(aUnit)
|
|
if type(aUnit) == "string" then
|
|
aUnit = Unit.getByName(aUnit)
|
|
end
|
|
if not aUnit then return end
|
|
local unitName = aUnit:getName()
|
|
if not unitName then return end
|
|
guardianAngel.unitsToWatchOver[unitName] = nil
|
|
if guardianAngel.verbose then
|
|
trigger.action.outText("+++gA: no longer watching " .. aUnit:getName(), 30)
|
|
end
|
|
end
|
|
|
|
function guardianAngel.getWatchedUnitByName(aName)
|
|
if not aName then return nil end
|
|
return guardianAngel.unitsToWatchOver[aName]
|
|
end
|
|
|
|
--
|
|
-- watch q items
|
|
--
|
|
function guardianAngel.createQItem(theWeapon, theTarget, detectProbability)
|
|
if not theWeapon then return nil end
|
|
if not theTarget then return nil end
|
|
if not theTarget:isExist() then return nil end
|
|
if not detectProbability then detectProbability = 1.0 end
|
|
local theItem = {}
|
|
theItem.theWeapon = theWeapon
|
|
theItem.wP = theWeapon:getPoint() -- save location
|
|
theItem.weaponName = theWeapon:getName()
|
|
theItem.theTarget = theTarget
|
|
theItem.tGroup = theTarget:getGroup()
|
|
theItem.tID = theItem.tGroup:getID()
|
|
|
|
theItem.targetName = theTarget:getName()
|
|
theItem.launchTimeStamp = timer.getTime()
|
|
theItem.lastCheckTimeStamp = -1000
|
|
theItem.lastDistance = math.huge
|
|
theItem.detected = false
|
|
theItem.lostTrack = false -- so we can detect sneakies!
|
|
theItem.missed = false -- just keep watching for re-ack
|
|
return theItem
|
|
end
|
|
|
|
|
|
-- calculate a point in direction from plane (pln) to weapon (wpn), dist meters
|
|
function guardianAngel.calcSafeExplosionPoint(wpn, pln, dist)
|
|
local dirToWpn = dcsCommon.vSub(wpn, pln) -- vector to weapon.
|
|
local v = dcsCommon.vNorm(dirToWpn) -- |v| = 1
|
|
local v = dcsCommon.vMultScalar(v, dist) -- |v| = dist
|
|
local newPoint = dcsCommon.vAdd(pln, v)
|
|
--trigger.action.outText("+++ gA: safe dist is ".. dist, 30)
|
|
return newPoint
|
|
end
|
|
|
|
function guardianAngel.bubbleCheck(wPos, w)
|
|
if true then return false end
|
|
for idx, aProtectee in pairs (guardianAngel.unitsToWatchOver) do
|
|
local uP = aProtectee:getPoint()
|
|
local d = math.floor(dcsCommon.dist(wPos, uP))
|
|
if d < guardianAngel.minMissileDist * 2 then
|
|
trigger.action.outText("+++gA: gazing at w=" .. w:getName() .. " APR:" .. aProtectee:getName() .. ", d=" .. d .. ", cutoff=" .. guardianAngel.minMissileDist, 30)
|
|
if w:getTarget() then
|
|
trigger.action.outText("+++gA: w is targeting " .. w:getTarget():getName(), 30)
|
|
else
|
|
trigger.action.outText("+++gA: w is NOT targeting anything")
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function guardianAngel.monitorItem(theItem)
|
|
local w = theItem.theWeapon
|
|
local ID = theItem.tID
|
|
if not w then return false end
|
|
if not w:isExist() then
|
|
if (not theItem.missed) and (not theItem.lostTrack) then
|
|
local desc = theItem.weaponName .. ": DISAPPEARED"
|
|
if guardianAngel.announcer then
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(ID, desc, 30)
|
|
else
|
|
trigger.action.outText(desc, 30)
|
|
end
|
|
end
|
|
guardianAngel.invokeCallbacks("disappear", theItem.targetName, theItem.weaponName)
|
|
end
|
|
return false
|
|
end
|
|
|
|
local t = theItem.theTarget
|
|
local currentTarget = w:getTarget()
|
|
local oldWPos = theItem.wP
|
|
local A = w:getPoint() -- A is new point of weapon
|
|
theItem.wp = A -- update new position, old is in oldWPos
|
|
|
|
-- new code: safety check with ALL protected wings
|
|
local bubbleThreat = guardianAngel.bubbleCheck(A, w)
|
|
|
|
local B
|
|
if currentTarget then B = currentTarget:getPoint() else B = A end
|
|
|
|
local d = math.floor(dcsCommon.dist(A, B))
|
|
local desc = theItem.weaponName .. ": "
|
|
if t == currentTarget then
|
|
desc = desc .. "tracking " .. theItem.targetName .. ", d = " .. d .. "m"
|
|
local vcc = dcsCommon.getClosingVelocity(t, w)
|
|
desc = desc .. ", Vcc = " .. math.floor(vcc) .. "m/s"
|
|
|
|
-- now calculate lethal distance: vcc is in meters per second
|
|
-- and we sample ups times per second
|
|
-- making the missile cover vcc / ups meters in the next
|
|
-- timer interval. If it now is closer than that, we have to
|
|
-- destroy the missile
|
|
local lethalRange = math.abs(vcc / guardianAngel.ups) * guardianAngel.safetyFactor
|
|
desc = desc .. ", LR= " .. math.floor(lethalRange) .. "m"
|
|
if guardianAngel.intervention and
|
|
d <= lethalRange + 10
|
|
then
|
|
desc = desc .. " ANGEL INTERVENTION"
|
|
if theItem.lostTrack then desc = desc .. " (little sneak!)" end
|
|
if theItem.missed then desc = desc .. " (missed you!)" end
|
|
|
|
|
|
if guardianAngel.announcer then
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(ID, desc, 30)
|
|
else
|
|
trigger.action.outText(desc, 30)
|
|
end
|
|
end
|
|
guardianAngel.invokeCallbacks("intervention", theItem.targetName, theItem.weaponName)
|
|
w:destroy()
|
|
|
|
-- now add some showy explosion so the missile
|
|
-- doesn't just disappear
|
|
if guardianAngel.explosion > 0 then
|
|
local xP = guardianAngel.calcSafeExplosionPoint(A,B, guardianAngel.fxDistance)
|
|
trigger.action.explosion(xP, guardianAngel.explosion)
|
|
end
|
|
|
|
return false -- remove from list
|
|
end
|
|
|
|
if guardianAngel.intervention and
|
|
d <= guardianAngel.minMissileDist -- god's override
|
|
then
|
|
desc = desc .. " GOD INTERVENTION"
|
|
if theItem.lostTrack then desc = desc .. " (little sneak!)" end
|
|
if theItem.missed then desc = desc .. " (missed you!)" end
|
|
|
|
if guardianAngel.announcer then
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(ID, desc, 30)
|
|
else
|
|
trigger.action.outText(desc, 30)
|
|
end
|
|
end
|
|
guardianAngel.invokeCallbacks("intervention", theItem.targetName, theItem.weaponName)
|
|
w:destroy()
|
|
if guardianAngel.explosion > 0 then
|
|
local xP = guardianAngel.calcSafeExplosionPoint(A,B, guardianAngel.fxDistance)
|
|
trigger.action.explosion(xP, guardianAngel.explosion)
|
|
end
|
|
return false -- remove from list
|
|
end
|
|
else
|
|
if not theItem.lostTrack then
|
|
desc = desc .. "Missile LOST TRACK"
|
|
|
|
if guardianAngel.announcer then
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(ID, desc, 30)
|
|
else
|
|
trigger.action.outText(desc, 30)
|
|
end
|
|
end
|
|
guardianAngel.invokeCallbacks("trackloss", theItem.targetName, theItem.weaponName)
|
|
theItem.lostTrack = true
|
|
end
|
|
theItem.lastDistance = d
|
|
return true -- true because they can re-acquire!
|
|
end
|
|
|
|
if d > theItem.lastDistance then
|
|
-- this can be wrong because if a missile is launched
|
|
-- at an angle, it can initially look as if it missed
|
|
if not theItem.missed then
|
|
desc = desc .. " Missile MISSED!"
|
|
|
|
if guardianAngel.announcer then
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(ID, desc, 30)
|
|
else
|
|
trigger.action.outText(desc, 30)
|
|
end
|
|
end
|
|
guardianAngel.invokeCallbacks("miss", theItem.targetName, theItem.weaponName)
|
|
theItem.missed = true
|
|
end
|
|
theItem.lastDistance = d
|
|
return true -- better not disregard - they can re-acquire!
|
|
end
|
|
|
|
if theItem.missed and d < theItem.lastDistance then
|
|
desc = desc .. " Missile RE-ACQUIRED!"
|
|
|
|
if guardianAngel.announcer then
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(ID, desc, 30)
|
|
else
|
|
trigger.action.outText(desc, 30)
|
|
end
|
|
end
|
|
theItem.missed = false
|
|
guardianAngel.invokeCallbacks("reacquire", theItem.targetName, theItem.weaponName)
|
|
end
|
|
|
|
theItem.lastDistance = d
|
|
|
|
return true
|
|
end
|
|
|
|
function guardianAngel.monitorMissiles()
|
|
local newArray = {} -- we collect all still existing missiles here
|
|
-- and replace missilesInTheAir with that for next round
|
|
for idx, anItem in pairs (guardianAngel.missilesInTheAir) do
|
|
-- we now have an item
|
|
-- see about detection
|
|
-- guardianAngel.detectItem(anItem)
|
|
|
|
-- see if the weapon is still in existence
|
|
stillAlive = guardianAngel.monitorItem(anItem)
|
|
if stillAlive then
|
|
table.insert(newArray, anItem)
|
|
end
|
|
end
|
|
guardianAngel.missilesInTheAir = newArray
|
|
end
|
|
|
|
--
|
|
-- E V E N T P R O C E S S I N G
|
|
--
|
|
function guardianAngel.isInteresting(eventID)
|
|
-- return true if we are interested in this event, false else
|
|
for key, evType in pairs(guardianAngel.myEvents) do
|
|
if evType == eventID then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- event pre-proc: only return true if we need to process this event
|
|
function guardianAngel.preProcessor(event)
|
|
-- all events must have initiator set
|
|
if not event.initiator then return false end
|
|
-- see if the event ID is interesting for us
|
|
local interesting = guardianAngel.isInteresting(event.id)
|
|
return interesting
|
|
end
|
|
|
|
function guardianAngel.postProcessor(event)
|
|
-- don't do anything for now
|
|
end
|
|
|
|
-- event callback from dcsCommon event handler. preProcessor has returned true
|
|
function guardianAngel.somethingHappened(event)
|
|
-- when this is invoked, the preprocessor guarantees that
|
|
-- it's an interesting event and has initiator
|
|
local ID = event.id
|
|
local theUnit = event.initiator
|
|
local playerName = theUnit:getPlayerName() -- nil if not a player
|
|
|
|
if ID == 15 and playerName then
|
|
-- this is a player created unit
|
|
if guardianAngel.verbose then
|
|
trigger.action.outText("+++gA: unit born " .. theUnit:getName(), 30)
|
|
end
|
|
if guardianAngel.autoAddPlayers then
|
|
guardianAngel.addUnitToWatch(theUnit)
|
|
end
|
|
return
|
|
end
|
|
|
|
if ID == 20 and playerName then
|
|
-- this is a player entered unit
|
|
if guardianAngel.verbose then
|
|
trigger.action.outText("+++gA: player seated in unit " .. theUnit:getName(), 30)
|
|
end
|
|
if guardianAngel.autoAddPlayers then
|
|
guardianAngel.addUnitToWatch(theUnit)
|
|
end
|
|
return
|
|
end
|
|
|
|
if ID == 21 and playerName then
|
|
guardianAngel.removeUnitToWatch(theUnit)
|
|
return
|
|
end
|
|
|
|
if ID == 15 or ID == 20 or ID == 21 then
|
|
-- non-player events of same type, disregard
|
|
return
|
|
end
|
|
|
|
|
|
if ID == 1 then
|
|
-- someone shot something. see if it is fire directed at me
|
|
local theWeapon = event.weapon
|
|
local theTarget
|
|
if theWeapon then
|
|
theTarget = theWeapon:getTarget()
|
|
else
|
|
return
|
|
end
|
|
if not theTarget then
|
|
return
|
|
end
|
|
if not theTarget:isExist() then return end
|
|
|
|
-- if we get here, we have weapon aimed at a target
|
|
local targetName = theTarget:getName()
|
|
local watchedUnit = guardianAngel.getWatchedUnitByName(targetName)
|
|
if not watchedUnit then return end -- fired at some other poor sucker, we don't care
|
|
|
|
-- if we get here, someone fired a guided weapon at my watched units
|
|
-- create a new item for my queue
|
|
local theQItem = guardianAngel.createQItem(theWeapon, theTarget) -- prob 100
|
|
table.insert(guardianAngel.missilesInTheAir, theQItem)
|
|
guardianAngel.invokeCallbacks("launch", theQItem.targetName, theQItem.weaponName)
|
|
|
|
local unitHeading = dcsCommon.getUnitHeadingDegrees(theTarget)
|
|
local A = theWeapon:getPoint()
|
|
local B = theTarget:getPoint()
|
|
local oclock = dcsCommon.clockPositionOfARelativeToB(A, B, unitHeading)
|
|
|
|
local grpID = theTarget:getGroup():getID()
|
|
if guardianAngel.launchWarning then
|
|
-- currently, we always detect immediately
|
|
-- can be moved to update()
|
|
if guardianAngel.private then
|
|
trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock", 30)
|
|
else
|
|
trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock", 30)
|
|
end
|
|
|
|
theQItem.detected = true -- remember: we detected and warned already
|
|
end
|
|
return
|
|
end
|
|
|
|
if ID == 2 then
|
|
if not guardianAngel.intervention then return end -- we don't intervene
|
|
if not event.weapon then return end -- no weapon, no interest
|
|
local theWeapon = event.weapon
|
|
local wName = theWeapon:getName()
|
|
local theTarget = event.target
|
|
if not theTarget then return end -- should not happen, better safe then dead
|
|
local tName = theTarget:getName()
|
|
|
|
local theProtegee = nil
|
|
for idx, aProt in pairs(guardianAngel.unitsToWatchOver) do
|
|
if tName == aProt:getName() then
|
|
theProtegee = aProt
|
|
end
|
|
end
|
|
|
|
if not theProtegee then return end
|
|
|
|
-- one of our protegees was hit
|
|
--trigger.action.outText("+++gA: Protegee " .. tName .. " was hit", 30)
|
|
trigger.action.outText("+++gA: I:" .. theUnit:getName() .. " hit " .. tName .. " with " .. wName, 30)
|
|
-- let's see if the victim was in our list of protected
|
|
-- units
|
|
local thePerp = nil
|
|
for idx, anItem in pairs(guardianAngel.missilesInTheAir) do
|
|
if anItem.weaponName == wName then
|
|
thePerp = anItem
|
|
end
|
|
end
|
|
|
|
if not thePerp then return end
|
|
--trigger.action.outText("+++gA: offender was known to gA: " .. wName, 30)
|
|
|
|
-- stats only: do target and intended target match?
|
|
local theWTarget = theWeapon:getTarget()
|
|
if not theWTarget then return end -- no target no interest
|
|
local wtName = theWTarget:getName()
|
|
if wtName == tName then
|
|
trigger.action.outText("+++gA: perp's ill intent confirmed", 30)
|
|
else
|
|
trigger.action.outText("+++gA: UNINTENDED CONSEQUENCES", 30)
|
|
end
|
|
|
|
-- if we should have protected: mea maxima culpa
|
|
trigger.action.outText("[+++gA: Angel hangs her head in shame. Mea Culpa, " .. tName.."]", 30)
|
|
return
|
|
end
|
|
|
|
local myType = theUnit:getTypeName()
|
|
if guardianAngel.verbose then
|
|
trigger.action.outText("+++gA: event " .. ID .. " for unit " .. theUnit:getName() .. " of type " .. myType, 30)
|
|
end
|
|
end
|
|
|
|
--
|
|
-- U P D A T E L O O P
|
|
--
|
|
|
|
|
|
function guardianAngel.update()
|
|
timer.scheduleFunction(guardianAngel.update, {}, timer.getTime() + 1/guardianAngel.ups)
|
|
|
|
guardianAngel.monitorMissiles()
|
|
end
|
|
|
|
function guardianAngel.collectPlayerUnits()
|
|
-- make sure we have all existing player units
|
|
-- at start of game
|
|
if not guardianAngel.autoAddPlayer then return end
|
|
|
|
for i=1, 2 do
|
|
-- currently only two factions in dcs
|
|
factionUnits = coalition.getPlayers(i)
|
|
for idx, aPlayerUnit in pairs(factionUnits) do
|
|
-- add all existing faction units
|
|
guardianAngel.addUnitToWatch(aPlayerUnit)
|
|
end
|
|
end
|
|
end
|
|
|
|
--
|
|
-- config reading
|
|
--
|
|
function guardianAngel.readConfigZone()
|
|
-- note: must match exactly!!!!
|
|
local theZone = cfxZones.getZoneByName("guardianAngelConfig")
|
|
if not theZone then
|
|
trigger.action.outText("+++gA: no config zone!", 30)
|
|
return
|
|
end
|
|
if guardianAngel.verbose then
|
|
trigger.action.outText("+++gA: found config zone!", 30)
|
|
end
|
|
|
|
guardianAngel.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
|
|
|
|
guardianAngel.autoAddPlayer = cfxZones.getBoolFromZoneProperty(theZone, "autoAddPlayer", true)
|
|
guardianAngel.launchWarning = cfxZones.getBoolFromZoneProperty(theZone, "launchWarning", true)
|
|
guardianAngel.intervention = cfxZones.getBoolFromZoneProperty(theZone, "intervention", true)
|
|
guardianAngel.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
|
|
guardianAngel.private = cfxZones.getBoolFromZoneProperty(theZone, "private", false)
|
|
guardianAngel.explosion = cfxZones.getNumberFromZoneProperty(theZone, "explosion", -1)
|
|
guardianAngel.fxDistance = cfxZones.getNumberFromZoneProperty(theZone, "fxDistance", 500)
|
|
end
|
|
|
|
|
|
--
|
|
-- start
|
|
--
|
|
function guardianAngel.start()
|
|
-- lib check
|
|
if not dcsCommon.libCheck("cfx Guardian Angel",
|
|
guardianAngel.requiredLibs) then
|
|
return false
|
|
end
|
|
|
|
-- read config
|
|
guardianAngel.readConfigZone()
|
|
|
|
-- install event monitor
|
|
dcsCommon.addEventHandler(guardianAngel.somethingHappened,
|
|
guardianAngel.preProcessor,
|
|
guardianAngel.postProcessor)
|
|
|
|
-- collect all units that are already in the game at this point
|
|
guardianAngel.collectPlayerUnits()
|
|
|
|
-- start update
|
|
guardianAngel.update()
|
|
|
|
trigger.action.outText("Guardian Angel v" .. guardianAngel.version .. " running", 30)
|
|
return true
|
|
end
|
|
|
|
function guardianAngel.testCB(reason, targetName, weaponName)
|
|
trigger.action.outText("gA - CB for ".. reason .. ": " .. targetName .. " w: " .. weaponName, 30)
|
|
end
|
|
|
|
-- go go go
|
|
if not guardianAngel.start() then
|
|
trigger.action.outText("Loading Guardian Angel failed.", 30)
|
|
guardianAngel = nil
|
|
end
|
|
|
|
-- test callback
|
|
--guardianAngel.addCallback(guardianAngel.testCB)
|
|
--guardianAngel.invokeCallbacks("A", "B", "C")
|