mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
Version 1.2.3
New ASW modules Lots of maintenance fixes
This commit is contained in:
parent
654b782894
commit
dc81decee6
Binary file not shown.
Binary file not shown.
983
modules/asw.lua
Normal file
983
modules/asw.lua
Normal file
@ -0,0 +1,983 @@
|
||||
asw = {}
|
||||
asw.version = "1.0.0"
|
||||
asw.verbose = false
|
||||
asw.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
"cfxZones", -- Zones, of course
|
||||
}
|
||||
asw.ups = 0.1 -- = once every 10 seconds
|
||||
asw.buoys = {} -- all buoys, by name
|
||||
asw.torpedoes = {} -- all torpedoes in the water.
|
||||
asw.thumpers = {} -- all current sonar amplifiers/booms that are active
|
||||
asw.fixes = {} -- all subs that we have a fix on. indexed by sub name
|
||||
-- fixname encodes the coalition of the fix in "/<coanum>"
|
||||
|
||||
--[[--
|
||||
Version History
|
||||
1.0.0 - initial version
|
||||
|
||||
--]]--
|
||||
|
||||
--
|
||||
-- :::WARNING:::
|
||||
-- CURRENTLY NOT CHECKING FOR COALITIONS
|
||||
--
|
||||
|
||||
function asw.createTorpedo()
|
||||
local t = {}
|
||||
t.lifeTimer = timer.getTime() + asw.torpedoLife
|
||||
t.speed = asw.torpedoSpeed
|
||||
t.state = 0; -- not yet released. FSM
|
||||
t.name = dcsCommon.uuid("asw.t")
|
||||
return t
|
||||
end
|
||||
|
||||
function asw.createTorpedoForUnit(theUnit)
|
||||
local t = asw.createTorpedo()
|
||||
t.coalition = theUnit:getCoalition()
|
||||
t.point = theUnit:getPoint()
|
||||
return t
|
||||
end
|
||||
|
||||
function asw.createTorpedoForZone(theZone)
|
||||
local t = asw.createTorpedo()
|
||||
t.coalition = theZone.coalition
|
||||
t.point = cfxZones.getPoint(theZone)
|
||||
return t
|
||||
end
|
||||
|
||||
function asw.createBuoy()
|
||||
local b = {}
|
||||
b.markID = dcsCommon.numberUUID() -- buoy mark
|
||||
b.coalition = 0
|
||||
b.point = nil
|
||||
b.smokeTimer = timer.getTime() + 5 * 60 -- for refresh
|
||||
b.smokeColor = nil --
|
||||
b.lifeTimer = timer.getTime() + asw.buoyLife
|
||||
b.contacts = {} -- detected contacts in range. by unit name
|
||||
b.timeStamps = {}
|
||||
b.bearing = {} -- bearing to contact
|
||||
b.lines = {} -- line art for contact (wedges)
|
||||
b.lastContactNum = 0
|
||||
b.lastReportedIn = 0 -- time of last report
|
||||
return b
|
||||
end
|
||||
|
||||
function asw.createBuoyForUnit(theUnit)
|
||||
-- theUnit drops buoy, making it belong to the same coalition
|
||||
-- as the dropping unit
|
||||
local b = asw.createBuoy()
|
||||
b.point = theUnit:getPoint()
|
||||
b.point.y = 0
|
||||
b.coalition = theUnit:getCoalition()
|
||||
b.smokeColor = asw.smokeColor -- needs to be done later
|
||||
b.name = dcsCommon.uuid("asw-b." .. theUnit:getName())
|
||||
return b
|
||||
end
|
||||
|
||||
function asw.createBuoyForZone(theZone)
|
||||
-- theZone drops buoy (if zone isn't linked to unit)
|
||||
-- making it belong to the same coalition
|
||||
-- as the dropping unit
|
||||
local theUnit = cfxZones.getLinkedUnit(theZone)
|
||||
if theUnit then
|
||||
b = asw.createBuoyForUnit(theUnit)
|
||||
return b
|
||||
end
|
||||
|
||||
local b = asw.createBuoy()
|
||||
b.point = cfxZones.getPoint(theZone)
|
||||
b.point.y = 0
|
||||
b.coalition = theZone.coalition
|
||||
b.smokeColor = asw.smokeColor -- needs to be done later
|
||||
b.name = dcsCommon.uuid("asw-b." .. theZone.name)
|
||||
return b
|
||||
end
|
||||
|
||||
-- uid generation for this module.
|
||||
asw.ccounter = 0 -- init to preferred value
|
||||
asw.ccinc = 1 -- init to preferred increment
|
||||
function asw.contactCount()
|
||||
asw.ccounter = asw.ccounter + asw.ccinc
|
||||
return asw.ccounter
|
||||
end
|
||||
|
||||
function asw.createFixForSub(theUnit, theCoalition)
|
||||
if not theCoalition then
|
||||
trigger.action.outText("+++ASW: createFix without coalition, assuming BLUE", 30)
|
||||
theCoalition = 2
|
||||
end
|
||||
|
||||
local now = timer.getTime()
|
||||
local f = {}
|
||||
f.coalition = theCoalition
|
||||
f.theUnit = theUnit
|
||||
if theCoalition == theUnit:getCoalition() then
|
||||
trigger.action.outText("+++ASW: createFix - theUnit <" .. theUnit:getName() .. "> has same coalition than detection side (" .. theCoalition .. ")", 30)
|
||||
end
|
||||
|
||||
f.name = theUnit:getName()
|
||||
f.typeName = theUnit:getTypeName()
|
||||
f.desig = "SC-" .. asw.contactCount()
|
||||
f.lifeTimer = now + asw.fixLife -- will be renewed whenever we hit enough signal strength
|
||||
f.lines = 0
|
||||
return f
|
||||
end
|
||||
|
||||
--
|
||||
-- dropping buoys, torpedos and thumpers
|
||||
--
|
||||
|
||||
function asw.dropBuoyFrom(theUnit)
|
||||
if not theUnit or not Unit.isExist(theUnit) then return end
|
||||
-- make sure we do not drop over land
|
||||
local p3 = theUnit:getPoint()
|
||||
local p2 = {x=p3.x, y=p3.z}
|
||||
local lType = land.getSurfaceType(p2)
|
||||
if lType ~= 3 then
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++aswZ: ASW counter-measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theUnit:getName() .. "> failed, counter-measure lost", 30)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local now = timer.getTime()
|
||||
-- create buoy
|
||||
local theBuoy = asw.createBuoyForUnit(theUnit)
|
||||
|
||||
-- mark point
|
||||
dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor)
|
||||
theBuoy.smokeTimer = now + 5 * 60
|
||||
|
||||
-- add buoy to my inventory
|
||||
asw.buoys[theBuoy.name] = theBuoy
|
||||
|
||||
-- mark on map
|
||||
local info = "Buoy dropped by " .. theUnit:getName() .. " at " .. dcsCommon.nowString()
|
||||
trigger.action.markToCoalition(theBuoy.markID, info, theUnit:getPoint(), theBuoy.coalition, true, "")
|
||||
if asw.verbose then
|
||||
trigger.action.outText("Dropping buoy " .. theBuoy.name, 30)
|
||||
end
|
||||
return theBuoy
|
||||
end
|
||||
|
||||
function asw.dropBuoyFromZone(theZone)
|
||||
-- trigger.action.outText("enter asw.dropBuoyFromZone <" .. theZone.name .. ">", 30)
|
||||
local theUnit = cfxZones.getLinkedUnit(theZone)
|
||||
if theUnit and Unit.isExist(theUnit)then
|
||||
return asw.dropBuoyFrom(theUnit)
|
||||
end
|
||||
|
||||
-- try and set the zone's coalition by the unit that
|
||||
-- it is following
|
||||
local coa = cfxZones.getLinkedUnit(theZone)
|
||||
if coa then
|
||||
theZone.coalition = coa
|
||||
end
|
||||
|
||||
if not theZone.coalition or theZone.coalition == 0 then
|
||||
trigger.action.outText("+++aswZ: 0 coalition for aswZone <" .. theZone.name .. ">, aborting buoy drop.", 30)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- make sure we do not drop over land
|
||||
local p3 = cfxZones.getPoint(theZone)
|
||||
local p2 = {x=p3.x, y=p3.z}
|
||||
local lType = land.getSurfaceType(p2)
|
||||
if lType ~= 3 then
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++aswZ: asw measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theZone.name .. ">", 30)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
local now = timer.getTime()
|
||||
-- create buoy
|
||||
local theBuoy = asw.createBuoyForZone(theZone)
|
||||
|
||||
-- mark point
|
||||
dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor)
|
||||
theBuoy.smokeTimer = now + 5 * 60
|
||||
|
||||
-- add buoy to my inventory
|
||||
asw.buoys[theBuoy.name] = theBuoy
|
||||
|
||||
-- mark on map
|
||||
local info = "Buoy dropped by " .. theZone.name .. " at " .. dcsCommon.nowString()
|
||||
local pos = cfxZones.getPoint(theZone)
|
||||
trigger.action.markToCoalition(theBuoy.markID, info, pos, theBuoy.coalition, true, "")
|
||||
if asw.verbose then
|
||||
trigger.action.outText("Dropping buoy " .. theBuoy.name, 30)
|
||||
end
|
||||
return theBuoy
|
||||
end
|
||||
|
||||
function asw.dropTorpedoFrom(theUnit)
|
||||
if not theUnit or not Unit.isExist(theUnit) then
|
||||
return nil
|
||||
end
|
||||
local p3 = theUnit:getPoint()
|
||||
local p2 = {x=p3.x, y=p3.z}
|
||||
local lType = land.getSurfaceType(p2)
|
||||
if lType ~= 3 then
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++aswZ: sub counter-measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theUnit:getName() .. "> failed, counter-measure lost", 30)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local t = asw.createTorpedoForUnit(theUnit)
|
||||
-- add to inventory
|
||||
asw.torpedoes[t.name] = t
|
||||
if asw.verbose then
|
||||
trigger.action.outText("Launching torpedo " .. t.name, 30)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function asw.dropTorpedoFromZone(theZone)
|
||||
local theUnit = cfxZones.getLinkedUnit(theZone)
|
||||
if theUnit then
|
||||
return asw.dropTorpedoFrom(theUnit)
|
||||
end
|
||||
|
||||
-- try and set the zone's coalition by the unit that
|
||||
-- it is following
|
||||
local coa = cfxZones.getLinkedUnit(theZone)
|
||||
if coa then
|
||||
theZone.coalition = coa
|
||||
end
|
||||
|
||||
if not theZone.coalition or theZone.coalition == 0 then
|
||||
trigger.action.outText("+++aswZ: 0 coalition for aswZone <" .. theZone.name .. ">, aborting torpedo drop.", 30)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- make sure we do not drop over land
|
||||
local p3 = cfxZones.getPoint(theZone)
|
||||
local p2 = {x=p3.x, y=p3.z}
|
||||
local lType = land.getSurfaceType(p2)
|
||||
if lType ~= 3 then
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++aswZ: asw measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theZone.name .. ">", 30)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local t = asw.createTorpedoForZone(theZone)
|
||||
-- add to inventory
|
||||
asw.torpedoes[t.name] = t
|
||||
if asw.verbose then
|
||||
trigger.action.outText("Launching torpedo for zone", 30)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
--
|
||||
-- UPDATE
|
||||
--
|
||||
function asw.getClosestFixTo(loc, coalition)
|
||||
local dist = math.huge
|
||||
local closestFix = nil
|
||||
for fixName, theFix in pairs(asw.fixes) do
|
||||
if theFix.coalition == coalition then
|
||||
local theUnit = theFix.theUnit
|
||||
if Unit.isExist(theUnit) then
|
||||
pos = theUnit:getPoint()
|
||||
d = dcsCommon.distFlat(loc, pos)
|
||||
if d < dist then
|
||||
dist = d
|
||||
closestFix = theFix
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return closestFix, dist
|
||||
end
|
||||
|
||||
function asw.getClosestSubToLoc(loc, allSubs)
|
||||
local dist = math.huge
|
||||
local closestSub = nil
|
||||
for cName, contact in pairs(allSubs) do
|
||||
if Unit.isExist(contact.theUnit) then
|
||||
d = dcsCommon.distFlat(loc, contact.theUnit:getPoint())
|
||||
if d < dist then
|
||||
closestSub = contact.theUnit
|
||||
dist = d
|
||||
end
|
||||
end
|
||||
end
|
||||
return closestSub, dist
|
||||
end
|
||||
|
||||
function asw.wedgeForBuoyAndContact(theBuoy, aName, p)
|
||||
--env.info(" >enter wedge for buoy/contact: <" .. theBuoy.name .. ">/< .. aName .. >, p= " .. p)
|
||||
if p > 1 then p = 1 end
|
||||
theBuoy.lines[aName] = dcsCommon.numberUUID()
|
||||
local shape = theBuoy.lines[aName]
|
||||
local p1 = theBuoy.point
|
||||
local deviant = asw.maxDeviation * (1-p) -- get percentage of max dev
|
||||
local minDev = math.floor(5 + (deviant * 0.2)) -- one fifth + 5 is fixed
|
||||
local varDev = math.floor(deviant * 0.8) -- four fifth is variable
|
||||
--env.info(" |will now calculate leftD and rightD")
|
||||
local leftD = math.floor(minDev + varDev * math.random()) -- dcsCommon.smallRandom(varDev) -- varDev * math.random()
|
||||
local rightD = math.floor(minDev + varDev * math.random()) -- dcsCommon.smallRandom(varDev) -- varDev * math.random()
|
||||
--env.info(" |will now calculate p2 and p3")
|
||||
local p2 = dcsCommon.newPointAtDegreesRange(p1, theBuoy.bearing[aName] - leftD, asw.maxDetectionRange)
|
||||
local p3 = dcsCommon.newPointAtDegreesRange(p1, theBuoy.bearing[aName] + rightD, asw.maxDetectionRange)
|
||||
--env.info(" |will now create wedge <" .. shape .. "> ")
|
||||
trigger.action.markupToAll(7, theBuoy.coalition, shape, p1, p2, p3, p1, {1, 0, 0, 0.25}, {1, 0, 0, 0.05}, 4, true, "Contact " .. tonumber(shape))
|
||||
--env.info(" <complete, leaving wedge for buoy/contact: <" .. theBuoy.name .. ">/< .. aName .. >")
|
||||
end
|
||||
|
||||
function asw.updateBuoy(theBuoy, allSubs)
|
||||
--env.info(" >>enter update buoy for " .. theBuoy.name)
|
||||
-- note: buoys never see subs of their own side since it is
|
||||
-- assumed that their location is known and filtered
|
||||
if not theBuoy then return false end
|
||||
|
||||
-- allSubs are all possible contacts
|
||||
local now = timer.getTime()
|
||||
if now > theBuoy.lifeTimer then
|
||||
--env.info(" lifetime ran out")
|
||||
-- buoy timed out: remove mark
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++ASW: removing mark <" .. theBuoy.markID .. "> for buoy <" .. theBuoy.name .. ">", 30)
|
||||
end
|
||||
--env.info(" - will remove mark " .. theBuoy.markID)
|
||||
trigger.action.removeMark(theBuoy.markID)
|
||||
--env.info(" - removed mark")
|
||||
-- now also remove all wedges
|
||||
for name, wedge in pairs(theBuoy.lines) do
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++ASW: removing wedge mark <" .. wedge .. "> for sub <" .. name .. ">", 30)
|
||||
end
|
||||
--env.info(" - will remove wedge " .. wedge)
|
||||
trigger.action.removeMark(wedge)
|
||||
end
|
||||
--env.info(" <<updateBuoy, returning false")
|
||||
return false
|
||||
end
|
||||
|
||||
-- buoy is alive!
|
||||
-- see if we need to resmoke
|
||||
if now > theBuoy.smokeTimer then
|
||||
--env.info(" resmoking buoy, continue")
|
||||
dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor)
|
||||
theBuoy.smokeTimer = now + 5 * 60
|
||||
--env.info(" resmoke done, continue")
|
||||
end
|
||||
|
||||
-- check all contacts, skip own coalition subs
|
||||
-- check signal strength to all subs
|
||||
local newContacts = {} -- as opposed to already in theBuoy.contacts
|
||||
--env.info(" :iterating allSubs for contacts")
|
||||
for contactName, contact in pairs (allSubs) do
|
||||
if contact.coalition ~= theBuoy.coalition then -- not on our side
|
||||
local theSub = contact.theUnit
|
||||
local theSubLoc = theSub:getPoint()
|
||||
local theSubName = contact.name
|
||||
local p = 0 -- detection probability
|
||||
local canDetect = false
|
||||
local sureDetect = false
|
||||
local depth = -dcsCommon.getUnitAGL(theSub) -- NOTE: INVERTED!!
|
||||
if depth > 5 and depth < asw.maxDetectionDepth then
|
||||
-- distance. probability recedes by square of distance
|
||||
local dist = dcsCommon.distFlat(theBuoy.point, theSubLoc)
|
||||
if dist > asw.maxDetectionRange then
|
||||
-- will not detect
|
||||
elseif dist < asw.sureDetectionRange then
|
||||
canDetect = true
|
||||
sureDetect = true
|
||||
p = 1
|
||||
theBuoy.bearing[theSubName] = dcsCommon.bearingInDegreesFromAtoB(theBuoy.point, theSubLoc)
|
||||
else
|
||||
canDetect = true
|
||||
p = 1 - (dist - asw.sureDetectionRange) / asw.maxDetectionRange -- percentage
|
||||
p = p * p * p -- cubed, in 3D
|
||||
theBuoy.bearing[theSubName] = dcsCommon.bearingInDegreesFromAtoB(theBuoy.point, theSubLoc)
|
||||
end
|
||||
end
|
||||
if canDetect then
|
||||
if sureDetect or math.random() < p then
|
||||
-- we have detected sub this round!
|
||||
newContacts[theSubName] = p -- remember for buoy
|
||||
contact.trackedBy[theBuoy.name] = p -- remember for sub
|
||||
else
|
||||
-- didn't detect, do nothing
|
||||
-- contact.trackedBy[theBuoy.name] = nil -- probably not required, contact is new each pass
|
||||
end
|
||||
else
|
||||
-- contact.trackedBy[theBuoy.name] = nil -- probably not required
|
||||
end
|
||||
end -- if not the same coalition
|
||||
end -- for all contacts
|
||||
--env.info(" :iterating allSubs done")
|
||||
-- now compare old contacts with new contacts
|
||||
-- if contact lost, remove wedge
|
||||
--env.info(" >start iterating buoy.contacts to find which contacts we lost")
|
||||
for aName, aP in pairs(theBuoy.contacts) do
|
||||
if newContacts[aName] then
|
||||
-- exists, therefore old contact. Keep it
|
||||
--[[-- code to update wedge removed
|
||||
if theBuoy.timeStamps[aName] + 60 * 2 < now then
|
||||
-- update map: remove wedge
|
||||
local shape = theBuoy.lines[aName]
|
||||
trigger.action.removeMark(shape)
|
||||
-- draw a new one
|
||||
local pc = newContacts[aName] -- new probability
|
||||
asw.wedgeForBuoyAndContact(theBuoy, aName, pc)
|
||||
end
|
||||
--]]--
|
||||
else
|
||||
-- contact lost. remove wedge
|
||||
local shape = theBuoy.lines[aName]
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++ASW: will remove wedge <" .. shape .. ">", 30)
|
||||
end
|
||||
--env.info(" >removing wedge #" .. shape)
|
||||
trigger.action.removeMark(shape)
|
||||
--env.info(" >done removing wedge")
|
||||
-- delete this line entry
|
||||
theBuoy.lines[aName] = nil
|
||||
end
|
||||
end
|
||||
--env.info(" <iterating buoy.contacts for lost contact done")
|
||||
-- check if contact is new and add wedge if so
|
||||
--env.info(" >start iterating newContacts for new contacts")
|
||||
for aName, aP in pairs(newContacts) do
|
||||
if theBuoy.contacts[aName] then
|
||||
-- exists, is old contact, do nothing
|
||||
else
|
||||
-- new contact, draw wedge
|
||||
theBuoy.timeStamps[aName] = now
|
||||
theBuoy.lines[aName] = dcsCommon.numberUUID() -- new shape ID
|
||||
asw.wedgeForBuoyAndContact(theBuoy, aName, aP)
|
||||
-- sound, but suppress ping if we have a fix for that sub
|
||||
-- fixes are indexed by <subname>"/"<coalition>
|
||||
if theBuoy.coalition == 1 then -- and (not asw.fixes[aName .. "/" .. "1"])then
|
||||
asw.newRedBuoyContact = true
|
||||
elseif theBuoy.coalition == 2 then --and (not asw.fixes[aName .. "/" .. "2"]) then
|
||||
asw.newBlueBuoyContact = true
|
||||
end
|
||||
end
|
||||
end
|
||||
--env.info(" >iterating newContacts for new contacts done")
|
||||
-- we may want to suppress beep if the sub is already in a fix
|
||||
|
||||
-- now save the new contacts and overwrite old
|
||||
theBuoy.contacts = newContacts
|
||||
--env.info(" <<done update buoy for " .. theBuoy.name .. ", returning true")
|
||||
return true -- true = keep uoy alive
|
||||
end
|
||||
|
||||
function asw.hasFix(contact)
|
||||
-- determine if this sub can be fixed by the buoys
|
||||
-- run down all buoys that currently see me
|
||||
-- sub is only seen by opposing buoys.
|
||||
|
||||
local bNum = 0
|
||||
local pTotal = 0
|
||||
local deltaB = 0
|
||||
local bearings = {}
|
||||
local subName = contact.name
|
||||
for bName, p in pairs(contact.trackedBy) do
|
||||
local theBuoy = asw.buoys[bName]
|
||||
-- CHECK FOR COALITION
|
||||
-- make bnum to bnumred and bnumblue
|
||||
if theBuoy.coalition == contact.coalition then
|
||||
trigger.action.outText("+++Warning: same coa for buoy <" .. theBuoy.name .. "> and sub contact <" .. contact.name .. "> ", 30)
|
||||
end
|
||||
bNum = bNum + 1 -- count number of tracking buoys
|
||||
pTotal = pTotal + p
|
||||
bearings[bName] = theBuoy.bearing[subName] - 180
|
||||
if bearings[bName] < 0 then bearings[bName] = bearings[bName] + 360 end
|
||||
end
|
||||
|
||||
local best90 = 0
|
||||
local above30 = 0
|
||||
for bName, aBearing in pairs (bearings) do
|
||||
for bbName, bBearing in pairs(bearings) do
|
||||
local a = aBearing
|
||||
if a > 180 then a = a - 180 end
|
||||
local b = bBearing
|
||||
if b > 180 then b = b - 180 end
|
||||
local d = math.abs(a - b) -- 0..180
|
||||
if d > 90 then d = 90 - (d-90) end -- d = 0..90
|
||||
local this90 = d
|
||||
if this90 > 30 then above30 = above30 + 1 end
|
||||
if this90 > best90 then best90 = this90 end
|
||||
end
|
||||
end
|
||||
above30 = above30 / 2 -- number of buoys that have more than 30° angle to contact, by 2 because each counts twice.
|
||||
local solver = above30 * best90/90 * pTotal
|
||||
if solver >= 2.0 then -- we have a fix
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function asw.updateFixes(allSubs)
|
||||
-- in order to create or maintain a fix, we need at least x
|
||||
-- buoys with a confidence level of xx for that sub
|
||||
-- and their azimuth must make at least 45 degrees so we
|
||||
-- can make a fix
|
||||
-- remember that buoys can only see subs of *opposing* side
|
||||
local now = timer.getTime()
|
||||
|
||||
for subName, contact in pairs(allSubs) do
|
||||
-- calculate if we have a fix on this sub
|
||||
local coa = dcsCommon.getEnemyCoalitionFor(contact.coalition)
|
||||
-- if coa is nil, it's a neutral sub, and we skip
|
||||
if coa and asw.hasFix(contact) then
|
||||
-- if new fix? Access existing ones via fix name scheme
|
||||
-- fix naming scheme is to allow (later) detection of
|
||||
-- same-side subs with buoys and not create a fix name
|
||||
-- collision. Currently overkill
|
||||
local theFix = asw.fixes[subName .. "/" .. tonumber(coa)]
|
||||
if theFix then
|
||||
-- exists, nothing to do
|
||||
else
|
||||
-- create a new fix
|
||||
theFix = asw.createFixForSub(contact.theUnit, coa)
|
||||
local theUnit = theFix.theUnit
|
||||
local pos = theUnit:getPoint()
|
||||
local lat, lon, dep = coord.LOtoLL(pos)
|
||||
local lla, llb = dcsCommon.latLon2Text(lat, lon)
|
||||
trigger.action.outTextForCoalition(coa, "NEW FIX " .. theFix.desig .. ": submerged contact, class <" .. theFix.typeName .. ">, location " .. lla .. ", " .. llb .. ", tracking.", 30)
|
||||
if coa == 1 then asw.newRedFix = true
|
||||
elseif coa == 2 then asw.newBlueFix = true
|
||||
end
|
||||
-- add fix to list of fixes
|
||||
asw.fixes[subName .. "/" .. tonumber(coa)] = theFix
|
||||
end
|
||||
-- update life timer for all fixes
|
||||
theFix.lifeTimer = now + asw.fixLife
|
||||
trigger.action.outTextForCoalition(coa, "contact fix " .. theFix.desig .. " confirmed.", 30)
|
||||
if asw.verbose then
|
||||
trigger.action.outText("renewed lease for fix " .. subName .. "/" .. tonumber(coa), 30)
|
||||
end
|
||||
else
|
||||
-- no new fix,
|
||||
end
|
||||
end
|
||||
|
||||
-- now iterate all fixes and update them, or time out
|
||||
local filtered = {}
|
||||
for fixName, theFix in pairs(asw.fixes) do
|
||||
if now < theFix.lifeTimer and Unit.isExist(theFix.theUnit) then
|
||||
-- update the location
|
||||
if theFix.lines and theFix.lines > 0 then
|
||||
-- remove old
|
||||
trigger.action.removeMark(theFix.lines)
|
||||
end
|
||||
-- allocate new fix id. we always need new fix id
|
||||
theFix.lines = dcsCommon.numberUUID()
|
||||
-- mark on map for coalition
|
||||
local theUnit = theFix.theUnit
|
||||
local pos = theUnit:getPoint()
|
||||
-- assemble sub info
|
||||
local vel = math.floor(1.94384 * dcsCommon.getUnitSpeed(theUnit))
|
||||
local heading = math.floor(dcsCommon.getUnitHeadingDegrees(theUnit))
|
||||
local delta = asw.fixLife - (theFix.lifeTimer - now)
|
||||
local timeAgo = dcsCommon.processHMS("<m>:<:s>", delta)
|
||||
local info = "Submerged contact, identified as '" .. theFix.theUnit:getTypeName() .. "' class, moving at " .. vel .. " kts, heading " .. heading .. ", last fix " .. timeAgo .. " minutes ago."
|
||||
-- note: neet to change to markToCoalition!
|
||||
trigger.action.markToCoalition(theFix.lines, info, pos, theFix.coalition, true, "")
|
||||
|
||||
-- add to filtered
|
||||
filtered[fixName] = theFix
|
||||
else
|
||||
-- do not add to filtered, timed out or unit destroyed
|
||||
trigger.action.outTextForCoalition(theFix.coalition, "Lost fix for contact", 30)
|
||||
-- remove mark
|
||||
if theFix.lines and theFix.lines > 0 then
|
||||
trigger.action.removeMark(theFix.lines)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
asw.fixes = filtered
|
||||
end
|
||||
|
||||
function markTorpedo(theTorpedo)
|
||||
theTorpedo.markID = dcsCommon.numberUUID()
|
||||
trigger.action.markToCoalition(theTorpedo.markID, "Torpedo " .. theTorpedo.name, theTorpedo.point, theTorpedo.coalition, true, "")
|
||||
end
|
||||
|
||||
function asw.updateTorpedo(theTorpedo, allSubs)
|
||||
-- homes in on closest torpedo, but only if it can detect it
|
||||
-- else it simply runs in a random direction
|
||||
|
||||
-- remove old mark
|
||||
if theTorpedo.markID then
|
||||
trigger.action.removeMark(theTorpedo.markID)
|
||||
end
|
||||
|
||||
-- outside of lethal range, torp can randomly fail and never
|
||||
-- re-aquire (lostTrack is true) unless it accidentally
|
||||
-- gets into lethal range
|
||||
|
||||
-- see if it timed out
|
||||
local now = timer.getTime()
|
||||
if now > theTorpedo.lifeTimer then
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " ran out", 30)
|
||||
return false
|
||||
end
|
||||
|
||||
-- redraw mark for torpedo. give it a new
|
||||
-- uuid every time
|
||||
-- during update, it gets near and if it can get close
|
||||
-- enough, it will set them up the bomb and create an explosion
|
||||
-- near the sub it detected.
|
||||
-- uses FSM
|
||||
-- state 0 = dropped into water
|
||||
if theTorpedo.state == 0 then
|
||||
-- state 0: dropping in the water
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " in the water!", 30)
|
||||
theTorpedo.state = 1
|
||||
markTorpedo(theTorpedo)
|
||||
return true
|
||||
|
||||
elseif theTorpedo.state == 1 then
|
||||
-- seeking. get closest fix. if we have a fix in range
|
||||
-- we go to stage homing, and it's a race between time and
|
||||
-- and sub
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " is seeking contact...", 30)
|
||||
|
||||
-- select closest fix from same side as torpedo
|
||||
local theFix, dist = asw.getClosestFixTo(theTorpedo.point, theTorpedo.coalition)
|
||||
|
||||
if theFix and dist > asw.maxDetectionRange / 2 then
|
||||
-- too far, forget it existed
|
||||
theFix = nil
|
||||
end
|
||||
|
||||
if not theFix then
|
||||
if asw.verbose then
|
||||
trigger.action.outText("stage1: No fix/distance found for " .. theTorpedo.name, 30)
|
||||
end
|
||||
else
|
||||
if asw.verbose then
|
||||
trigger.action.outText("stage1: found fix <" .. theFix.name .. "> at dist <" .. dist .. "> for " .. theTorpedo.name, 30)
|
||||
end
|
||||
end
|
||||
|
||||
if theFix and dist < 1700 then
|
||||
-- have seeker, go to homing mode
|
||||
theTorpedo.target = theFix.theUnit
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++asw: target found: <" .. theTorpedo.target:getName() .. ">", 30)
|
||||
end
|
||||
theTorpedo.state = 20 -- homing
|
||||
|
||||
elseif theFix then
|
||||
local B = theFix.theUnit:getPoint()
|
||||
theTorpedo.course = dcsCommon.bearingFromAtoB(theTorpedo.point, B)
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++asw: unguided heading for <" .. theFix.theUnit:getName() .. ">", 30)
|
||||
end
|
||||
theTorpedo.state = 10 -- directed run
|
||||
else
|
||||
-- no fix anywhere in range,
|
||||
-- simply pick a course and run
|
||||
-- maybe we get lucky
|
||||
theTorpedo.course = 2 * 3.1415 * math.random()
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++asw: random heading", 30)
|
||||
end
|
||||
theTorpedo.state = 10 -- random run
|
||||
end
|
||||
|
||||
markTorpedo(theTorpedo)
|
||||
return true
|
||||
|
||||
elseif theTorpedo.state == 10 then -- moving, not homing
|
||||
-- move torpedo and see if it's close enough to a sub
|
||||
-- to track or blow up
|
||||
local displacement = asw.torpedoSpeed * 1/asw.ups -- meters travelled
|
||||
if not theTorpedo.course then
|
||||
theTorpedo.course = 0
|
||||
trigger.action.outText("+++ASW: Torpedo <" .. theTorpedo.name .. "> stage (10) with undefined course, setting 0", 30)
|
||||
end
|
||||
|
||||
theTorpedo.point.x = theTorpedo.point.x + displacement * math.cos(theTorpedo.course)
|
||||
theTorpedo.point.z = theTorpedo.point.z + displacement * math.sin(theTorpedo.course)
|
||||
|
||||
-- seeking ANY sub now.
|
||||
-- warning: may go after our own subs as well, torpedo don't care!
|
||||
local theSub, dist = asw.getClosestSubToLoc(theTorpedo.point, allSubs)
|
||||
if dist < 1200 then
|
||||
-- we lock on to this sub
|
||||
theTorpedo.target = theSub
|
||||
theTorpedo.state = 20 -- switch to homing
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " is going active!", 30)
|
||||
end
|
||||
|
||||
if dist < 1.2 * displacement then
|
||||
theTorpedo.state = 99 -- go boom
|
||||
end
|
||||
markTorpedo(theTorpedo)
|
||||
return true
|
||||
|
||||
elseif theTorpedo.state == 20 then -- HOMING!
|
||||
if not Unit.isExist(theTorpedo.target) then
|
||||
-- target was destroyed?
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++asw: target lost", 30)
|
||||
end
|
||||
theTorpedo.course = 2 * 3.1415 * math.random()
|
||||
theTorpedo.state = 10 -- switch to run free
|
||||
theTorpedo.target = nil
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " lost track, searching...", 30)
|
||||
return
|
||||
end
|
||||
|
||||
if not theTorpedo.target then
|
||||
-- sanity check
|
||||
theTorpedo.course = 2 * 3.1415 * math.random()
|
||||
theTorpedo.state = 10 -- switch to run free
|
||||
return
|
||||
end
|
||||
|
||||
-- we know that isExist(target)
|
||||
local B = theTorpedo.target:getPoint()
|
||||
theTorpedo.course = dcsCommon.bearingFromAtoB(theTorpedo.point, B)
|
||||
local displacement = asw.torpedoSpeed * 1/asw.ups -- meters travelled
|
||||
theTorpedo.point.x = theTorpedo.point.x + displacement * math.cos(theTorpedo.course)
|
||||
theTorpedo.point.z = theTorpedo.point.z + displacement * math.sin(theTorpedo.course)
|
||||
local dist = dcsCommon.distFlat(theTorpedo.point, B)
|
||||
if dist < displacement then
|
||||
theTorpedo.state = 99 -- boom, babe!
|
||||
else
|
||||
local hdg = math.floor(57.2958 * theTorpedo.course)
|
||||
if hdg < 0 then hdg = hdg + 360 end
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " is homing, course " .. hdg .. ", " .. math.floor(dist) .. "m to impact", 30)
|
||||
end
|
||||
-- move to this torpedo and blow up
|
||||
-- when close enough
|
||||
markTorpedo(theTorpedo)
|
||||
|
||||
return true
|
||||
elseif theTorpedo.state == 99 then -- go boom
|
||||
if Unit.isExist(theTorpedo.target) then
|
||||
Unit.destroy(theTorpedo.target)
|
||||
end
|
||||
-- impact!
|
||||
trigger.action.outTextForCoalition(theTorpedo.coalition, "Impact for " .. theTorpedo.name .. "! We have confirmed hit on submerged contact!", 30)
|
||||
if theTorpedo.coalition == 1 then
|
||||
if asw.redKill then
|
||||
cfxZones.pollFlag(asw.redKill, asw.method, asw)
|
||||
end
|
||||
elseif theTorpedo.coalition == 2 then
|
||||
if asw.blueKill then
|
||||
cfxZones.pollFlag(asw.blueKill, asw.method, asw)
|
||||
end
|
||||
end
|
||||
|
||||
-- make surface explosion
|
||||
-- choose point 1m under water
|
||||
local loc = theTorpedo.point
|
||||
local alt = land.getHeight({x = loc.x, y = loc.z})
|
||||
loc.y = alt-1
|
||||
trigger.action.explosion(loc, 3000)
|
||||
|
||||
-- we are done
|
||||
return false
|
||||
|
||||
else
|
||||
-- we somehow ran into an unknown state
|
||||
trigger.action.outText("unknown torpedo state <" .. theTorpedo.state .. "> for <" .. theTorpedo.name .. ">", 20)
|
||||
return false
|
||||
end
|
||||
|
||||
-- return true if it should be kept in array
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- MAIN UPDATE
|
||||
--
|
||||
-- does not find subs that have surfaced
|
||||
-- returns a list of 'contacts' - ready made tables
|
||||
-- to track the sub: who sees them (trackedBy) and misc
|
||||
-- info.
|
||||
-- contacts is indexed by unit name
|
||||
function asw.gatherSubs()
|
||||
local allCoas = {0, 1, 2}
|
||||
local subs = {}
|
||||
for idx, coa in pairs(allCoas) do
|
||||
local allGroups = coalition.getGroups(coa, 3) -- ships only
|
||||
for idy, aGroup in pairs(allGroups) do
|
||||
allUnits = aGroup:getUnits()
|
||||
for idz, aUnit in pairs(allUnits) do
|
||||
-- see if this unit is a sub
|
||||
if aUnit and Unit.isExist(aUnit) and
|
||||
(dcsCommon.getUnitAGL(aUnit) < -5) then -- yes, submerged contact.
|
||||
local contact = {}
|
||||
contact.theUnit = aUnit
|
||||
contact.trackedBy = {} -- buoys that have a ping
|
||||
contact.name = aUnit:getName()
|
||||
contact.coalition = aUnit:getCoalition()
|
||||
subs[contact.name] = contact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return subs
|
||||
end
|
||||
|
||||
function asw.update()
|
||||
--env.info("-->Enter asw update")
|
||||
-- first, schedule next invocation
|
||||
timer.scheduleFunction(asw.update, {}, timer.getTime() + 1/asw.ups)
|
||||
|
||||
local subs = asw.gatherSubs() -- ALL contacts/subs
|
||||
|
||||
asw.newRedBuoyContact = false
|
||||
asw.newBlueBuoyContact = false
|
||||
|
||||
-- refresh all buoy detections
|
||||
-- if #asw.buoys > 0 then
|
||||
--env.info("Before buoy proc")
|
||||
local filtered = {}
|
||||
for bName, theBuoy in pairs(asw.buoys) do
|
||||
if asw.updateBuoy(theBuoy, subs) then
|
||||
filtered[bName] = theBuoy
|
||||
end
|
||||
end
|
||||
asw.buoys = filtered
|
||||
--env.info("Complete buoy proc")
|
||||
|
||||
if asw.newRedBuoyContact then
|
||||
trigger.action.outSoundForCoalition(1, asw.sonarSound)
|
||||
end
|
||||
if asw.newBlueBuoyContact then
|
||||
trigger.action.outSoundForCoalition(2, asw.sonarSound)
|
||||
end
|
||||
|
||||
|
||||
-- update fixes: create if they don't exist
|
||||
asw.newBlueFix = false
|
||||
asw.newRedFix = false
|
||||
|
||||
--env.info("Before fixes")
|
||||
asw.updateFixes(subs)
|
||||
--env.info("Complete fixes")
|
||||
|
||||
if asw.newBlueFix then
|
||||
trigger.action.outSoundForCoalition(2, asw.fixSound)
|
||||
end
|
||||
|
||||
if asw.newRedFix then
|
||||
trigger.action.outSoundForCoalition(1, asw.fixSound)
|
||||
end
|
||||
|
||||
-- see if there are any torpedoes in the water
|
||||
--if #asw.torpedoes > 0 then
|
||||
--env.info("Before torpedoes")
|
||||
local filtered = {}
|
||||
for tName, theTorpedo in pairs(asw.torpedoes) do
|
||||
if asw.updateTorpedo(theTorpedo, subs) then
|
||||
filtered[tName] = theTorpedo
|
||||
end
|
||||
end
|
||||
asw.torpedoes = filtered
|
||||
|
||||
--env.info("Complete torpedoes")
|
||||
|
||||
--end
|
||||
--env.info("<--Leave asw update")
|
||||
end
|
||||
|
||||
--
|
||||
-- CONFIG & START
|
||||
--
|
||||
function asw.readConfigZone()
|
||||
local theZone = cfxZones.getZoneByName("aswConfig")
|
||||
if not theZone then
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++asw: no config zone!", 30)
|
||||
end
|
||||
theZone = cfxZones.createSimpleZone("aswConfig")
|
||||
end
|
||||
asw.verbose = theZone.verbose
|
||||
asw.name = "aswConfig" -- make compatible with cfxZones
|
||||
|
||||
-- set defaults, later do the reading
|
||||
asw.buoyLife = 30 * 60 -- 30 minutes life time
|
||||
asw.buoyLife = cfxZones.getNumberFromZoneProperty(theZone, "buoyLife", asw.buoyLife)
|
||||
if asw.buoyLife < 1 then asw.buoyLife = 999999 end -- very, very long time
|
||||
|
||||
asw.maxDetectionRange = 12000 -- 12 km
|
||||
asw.maxDetectionRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionRange", 12000)
|
||||
asw.sureDetectionRange = 1000 -- inside 1 km will always detect sub
|
||||
asw.sureDetectionRange = cfxZones.getNumberFromZoneProperty(theZone, "sureDetect", 1000)
|
||||
asw.torpedoLife = 7 * 60 + 30 -- 7.5 minutes, will reach max range in that time
|
||||
asw.torpedoSpeed = 28.3 -- speed in m/s -- 55 knots
|
||||
asw.maxDetectionDepth = 500 -- in meters. deeper than that, no detection.
|
||||
asw.maxDetectionDepth = cfxZones.getNumberFromZoneProperty(theZone, "detectionDepth", 500)
|
||||
asw.fixLife = 3 * 60 -- a sub "fix" lives 3 minutes past last renew
|
||||
asw.fixLife = cfxZones.getNumberFromZoneProperty(theZone, "fixLife", asw.fixLife)
|
||||
if asw.fixLife < 1 then asw.fixLife = 999999 end -- a long time
|
||||
|
||||
asw.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
|
||||
|
||||
asw.maxDeviation = 40 -- 40 degrees + 5 = 45 degrees left and right max deviation makes a worst-case 90 degree left/right wedge
|
||||
asw.fixSound = "submarine ping.ogg"
|
||||
asw.fixSound = cfxZones.getStringFromZoneProperty(theZone, "fixSound", asw.fixSound)
|
||||
asw.sonarSound = "beacon beep-beep.ogg"
|
||||
asw.sonarSound = cfxZones.getStringFromZoneProperty(theZone, "sonarSound", asw.sonarSound)
|
||||
if cfxZones.hasProperty(theZone, "redKill!") then
|
||||
asw.redKill = cfxZones.getStringFromZoneProperty(theZone, "redKill!", "none")
|
||||
end
|
||||
if cfxZones.hasProperty(theZone, "blueKill!") then
|
||||
asw.blueKill = cfxZones.getStringFromZoneProperty(theZone, "blueKill!", "none")
|
||||
end
|
||||
|
||||
asw.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc")
|
||||
|
||||
asw.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "red")
|
||||
asw.smokeColor = dcsCommon.smokeColor2Num(asw.smokeColor)
|
||||
|
||||
if asw.verbose then
|
||||
trigger.action.outText("+++asw: read config", 30)
|
||||
end
|
||||
end
|
||||
|
||||
function asw.start()
|
||||
if not dcsCommon.libCheck then
|
||||
trigger.action.outText("cfx asw requires dcsCommon", 30)
|
||||
return false
|
||||
end
|
||||
if not dcsCommon.libCheck("cfx asw", asw.requiredLibs) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- read config
|
||||
asw.readConfigZone()
|
||||
|
||||
-- start update
|
||||
asw.update()
|
||||
|
||||
trigger.action.outText("cfx ASW v" .. asw.version .. " started.", 30)
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- start up asw
|
||||
--
|
||||
if not asw.start() then
|
||||
trigger.action.outText("cfx asw aborted: missing libraries", 30)
|
||||
asw = nil
|
||||
end
|
||||
|
||||
--[[--
|
||||
Ideas/to do
|
||||
- false positives for detections
|
||||
- triangle mark for fixes, color red
|
||||
- squares for torps, color yellow
|
||||
- remove torpedoes when they run aground
|
||||
|
||||
--]]--
|
||||
597
modules/aswGUI.lua
Normal file
597
modules/aswGUI.lua
Normal file
@ -0,0 +1,597 @@
|
||||
aswGUI = {}
|
||||
aswGUI.version = "1.0.0"
|
||||
aswGUI.verbose = false
|
||||
aswGUI.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
"cfxZones", -- Zones, of course
|
||||
"asw", -- needs asw module
|
||||
"aswZones", -- also needs the asw zones
|
||||
}
|
||||
|
||||
--[[--
|
||||
Version History
|
||||
1.0.0 - initial version
|
||||
|
||||
--]]--
|
||||
|
||||
aswGUI.ups = 1 -- = once every second
|
||||
aswGUI.aswCraft = {}
|
||||
|
||||
--[[--
|
||||
::::::::::::::::: ASSUMES SINGLE_UNIT GROUPS ::::::::::::::::::
|
||||
--]]--
|
||||
|
||||
|
||||
function aswGUI.resetConf(asc)
|
||||
if asc.rootMenu then
|
||||
missionCommands.removeItemForGroup(asc.groupID, asc.rootMenu)
|
||||
end
|
||||
asc.rootMenu = missionCommands.addSubMenuForGroup(asc.groupID, "ASW")
|
||||
asc.buoyNum = 0
|
||||
asc.torpedoNum = 0
|
||||
asc.coolDown = 0 -- used when waiting, currently not used
|
||||
end
|
||||
|
||||
-- we use lazy init whenever player enters
|
||||
function aswGUI.initUnit(unitName) -- now this unit exists
|
||||
local theUnit = Unit.getByName(unitName)
|
||||
if not theUnit then
|
||||
trigger.action.outText("+++aswGUI: <" .. unitName .. "> not a unit, aborting initUnit", 30)
|
||||
return nil
|
||||
end
|
||||
|
||||
local theGroup = theUnit:getGroup()
|
||||
local asc = {} -- set up player craft config block
|
||||
--local groupData = cfxMX.playerUnit2Group[unitName]
|
||||
asc.groupName = theGroup:getName() -- groupData.name
|
||||
asc.name = unitName
|
||||
asc.groupID = theGroup:getID() -- groupData.groupId
|
||||
aswGUI.resetConf(asc)
|
||||
return asc
|
||||
end
|
||||
|
||||
|
||||
function aswGUI.processWeightFor(conf)
|
||||
-- make total weight and handle all
|
||||
-- cargo for this unit
|
||||
|
||||
-- hand off to DML cargo manager if implemented
|
||||
if cargosuper then
|
||||
trigger.action.outText("CargoSuper handling regquired, using none", 30)
|
||||
return
|
||||
end
|
||||
|
||||
local totalWeight = conf.buoyNum * aswGUI.buoyWeight
|
||||
totalWeight = totalWeight + conf.torpedoNum * aswGUI.torpedoWeight
|
||||
|
||||
-- set cargo weight
|
||||
trigger.action.setUnitInternalCargo(conf.name, totalWeight)
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
trigger.action.outTextForGroup(conf.groupID, "Total asw weight: " .. totalWeight .. "kg (" .. math.floor(totalWeight * 2.20462) .. "lbs)", 30)
|
||||
return totalWeight
|
||||
end
|
||||
|
||||
--
|
||||
-- build unit menu
|
||||
--
|
||||
function aswGUI.getBuoyCapa(conf) -- returns capa per slot
|
||||
-- warning: assumes two "slots" maximum
|
||||
if conf.torpedoNum > aswGUI.torpedoesPerSlot then return 0 end -- both slots are filled with torpedoes
|
||||
if conf.torpedoNum > 0 then -- one slot is taken up by torpedoes
|
||||
return aswGUI.buoysPerSlot - conf.buoyNum
|
||||
end
|
||||
if conf.buoyNum >= aswGUI.buoysPerSlot then
|
||||
return 2 * aswGUI.buoysPerSlot - conf.buoyNum
|
||||
end
|
||||
return aswGUI.buoysPerSlot - conf.buoyNum
|
||||
end
|
||||
|
||||
function aswGUI.getTorpedoCapa(conf)
|
||||
if conf.buoyNum > aswGUI.buoysPerSlot then return 0 end -- both slots are filled with buoys
|
||||
if conf.buoyNum > 0 then -- one slot is taken up by torpedoes
|
||||
return aswGUI.torpedoesPerSlot - conf.torpedoNum
|
||||
end
|
||||
if conf.torpedoNum >= aswGUI.torpedoesPerSlot then
|
||||
return 2 * aswGUI.torpedoesPerSlot - conf.torpedoNum
|
||||
end
|
||||
return aswGUI.torpedoesPerSlot - conf.torpedoNum
|
||||
end
|
||||
|
||||
function aswGUI.setGroundMenu(conf, theUnit)
|
||||
-- build menu for load stores
|
||||
local loc = theUnit:getPoint()
|
||||
local closestAswZone = aswZones.getClosestASWZoneTo(loc)
|
||||
local inZone = cfxZones.pointInZone(loc, closestAswZone)
|
||||
local bStore = 0 -- available buoys
|
||||
local tStore = 0 -- available torpedoes
|
||||
-- ... but only if we are in an asw zone
|
||||
-- calculate how much is available
|
||||
if inZone then
|
||||
bStore = closestAswZone.buoyNum
|
||||
if bStore < 0 then bStore = aswGUI.buoysPerSlot end
|
||||
tStore = closestAswZone.torpedoNum
|
||||
if tStore < 0 then tStore = aswGUI.torpedoesPerSlot end
|
||||
end
|
||||
|
||||
if bStore > 0 then
|
||||
local bCapa = aswGUI.getBuoyCapa(conf)
|
||||
if bCapa > 0 then
|
||||
missionCommands.addCommandForGroup(conf.groupID, "Load <" .. bCapa .."> ASW Buoys", conf.rootMenu, aswGUI.xHandleLoadBuoys, conf)
|
||||
else
|
||||
missionCommands.addCommandForGroup(conf.groupID, "(No free Buoy stores)", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
else
|
||||
missionCommands.addCommandForGroup(conf.groupID, "(Can't load ASW Buoys, no supplies in range)", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
|
||||
if conf.buoyNum > 0 then
|
||||
local toUnload = conf.buoyNum
|
||||
if toUnload > aswGUI.buoysPerSlot then toUnload = aswGUI.buoysPerSlot end
|
||||
missionCommands.addCommandForGroup(conf.groupID, "Unload <" .. toUnload .. "> ASW Buoys (" .. conf.buoyNum .. " on board)", conf.rootMenu, aswGUI.xHandleUnloadBuoys, conf)
|
||||
end
|
||||
|
||||
-- torpedo proccing
|
||||
|
||||
if tStore > 0 then
|
||||
local tCapa = aswGUI.getTorpedoCapa(conf)
|
||||
if tCapa > 0 then
|
||||
tCapa = 1 -- one at a time
|
||||
missionCommands.addCommandForGroup(conf.groupID, "Load <" .. tCapa .."> ASW Torpedoes", conf.rootMenu, aswGUI.xHandleLoadTorpedoes, conf)
|
||||
else
|
||||
missionCommands.addCommandForGroup(conf.groupID, "All stores filled to capacity", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
else
|
||||
missionCommands.addCommandForGroup(conf.groupID, "(Can't load ASW Torpedoes, no supplies in range)", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
|
||||
if conf.torpedoNum > 0 then
|
||||
local toUnload = conf.torpedoNum
|
||||
if toUnload > aswGUI.torpedoesPerSlot then toUnload = aswGUI.buoysPerSlot end
|
||||
missionCommands.addCommandForGroup(conf.groupID, "Unload <" .. toUnload .. "> ASW Torpedoes (" .. conf.torpedoNum .. " on board)", conf.rootMenu, aswGUI.xHandleUnloadTorpedoes, conf)
|
||||
end
|
||||
missionCommands.addCommandForGroup(conf.groupID, "[Stores: <" .. conf.buoyNum .. "> Buoys | <" .. conf.torpedoNum .. "> Torpedoes]", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
|
||||
function aswGUI.setAirMenu(conf, theUnit)
|
||||
-- build menu for load stores
|
||||
local bStore = conf.buoyNum -- available buoys
|
||||
local tStore = conf.torpedoNum -- available torpedoes
|
||||
|
||||
if bStore < 1 and tStore < 1 then
|
||||
missionCommands.addCommandForGroup(conf.groupID, "No ASW munitions on board", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
return
|
||||
end
|
||||
|
||||
if bStore > 0 then
|
||||
missionCommands.addCommandForGroup(conf.groupID, "BUOY - Drop an ASW Buoy", conf.rootMenu, aswGUI.xHandleBuoyDropoff, conf)
|
||||
else
|
||||
missionCommands.addCommandForGroup(conf.groupID, "No ASW Buoys on board", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
|
||||
if tStore > 0 then
|
||||
missionCommands.addCommandForGroup(conf.groupID, "TORP - Drop an ASW Torpedo", conf.rootMenu, aswGUI.xHandleTorpedoDropoff, conf)
|
||||
else
|
||||
missionCommands.addCommandForGroup(conf.groupID, "No ASW Torpedoes on board", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
|
||||
missionCommands.addCommandForGroup(conf.groupID, "[Stores: <" .. conf.buoyNum .. "> Buoys | <" .. conf.torpedoNum .. "> Torpedoes]", conf.rootMenu, aswGUI.xHandleGeneric, conf)
|
||||
end
|
||||
|
||||
function aswGUI.setMenuForUnit(theUnit)
|
||||
if not theUnit then return end
|
||||
if not Unit.isExist(theUnit) then return end
|
||||
local uName = theUnit:getName()
|
||||
|
||||
-- if we get here, the unit exists. fetch unit config
|
||||
local conf = aswGUI.aswCraft[uName]
|
||||
-- delete old, and create new root menu
|
||||
missionCommands.removeItemForGroup(conf.groupID, conf.rootMenu)
|
||||
conf.rootMenu = missionCommands.addSubMenuForGroup(conf.groupID, "ASW")
|
||||
|
||||
-- if we are in the air, we add menus to drop buoys or torpedoes
|
||||
if theUnit:inAir() then
|
||||
aswGUI.setAirMenu(conf, theUnit)
|
||||
else
|
||||
aswGUI.setGroundMenu(conf, theUnit)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- comms callback handling
|
||||
--
|
||||
--
|
||||
-- LOADING / UNLOADING
|
||||
--
|
||||
function aswGUI.xHandleGeneric(args)
|
||||
timer.scheduleFunction(aswGUI.handleGeneric, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function aswGUI.handleGeneric(args)
|
||||
if not args then args = "*EMPTY*" end
|
||||
-- do nothing
|
||||
end
|
||||
|
||||
function aswGUI.xHandleLoadBuoys(args)
|
||||
timer.scheduleFunction(aswGUI.handleLoadBuoys, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
|
||||
function aswGUI.handleLoadBuoys(args)
|
||||
local conf = args
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
if not theUnit then
|
||||
trigger.action.outText("+++aswG: (load buoys) can't find unit <" .. conf.name .. ">", 30)
|
||||
return
|
||||
end
|
||||
local loc = theUnit:getPoint()
|
||||
local theZone = aswZones.getClosestASWZoneTo(loc)
|
||||
local inZone = cfxZones.pointInZone(loc, theZone)
|
||||
local bStore = 0 -- available buoys
|
||||
if inZone then
|
||||
bStore = theZone.buoyNum
|
||||
if bStore < 0 then bStore = aswGUI.buoysPerSlot end
|
||||
else
|
||||
trigger.action.outTextForGroup(conf.groupID, "Nothing loaded. Return to ASW loading zone.", 30)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
return
|
||||
end
|
||||
|
||||
if bStore < 1 then
|
||||
trigger.action.outTextForGroup(conf.groupID, "ASW Buoy stock has run out. Sorry.", 30)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
return
|
||||
end
|
||||
|
||||
local capa = aswGUI.getBuoyCapa(conf)
|
||||
conf.buoyNum=conf.buoyNum + capa
|
||||
|
||||
if theZone.buoyNum >= 0 then
|
||||
theZone.buoyNum = theZone.buoyNum - capa
|
||||
if theZone.buoyNum < 0 then theZone.buoyNum = 0 end
|
||||
-- proc new weight
|
||||
end
|
||||
|
||||
aswGUI.processWeightFor(conf)
|
||||
trigger.action.outTextForGroup(conf.groupID, "Loaded <" .. capa .. "> ASW Buoys.", 30)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
function aswGUI.xHandleUnloadBuoys(args)
|
||||
timer.scheduleFunction(aswGUI.handleUnloadBuoys, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function aswGUI.handleUnloadBuoys(args)
|
||||
local conf = args
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
if not theUnit then
|
||||
trigger.action.outText("+++aswG: (unload buoys) can't find unit <" .. conf.name .. ">", 30)
|
||||
return
|
||||
end
|
||||
local loc = theUnit:getPoint()
|
||||
local theZone = aswZones.getClosestASWZoneTo(loc)
|
||||
local inZone = cfxZones.pointInZone(loc, theZone)
|
||||
|
||||
local amount = conf.buoyNum
|
||||
while amount > aswGUI.buoysPerSlot do -- future proof, any # of slots
|
||||
amount = amount - aswGUI.buoysPerSlot
|
||||
end
|
||||
conf.buoyNum = conf.buoyNum - amount
|
||||
|
||||
if inZone then
|
||||
if theZone.buoyNum >= 0 then theZone.buoyNum = theZone.buoyNum + amount end
|
||||
trigger.action.outTextForGroup(conf.groupID, "Returned <" .. amount .. "> ASW Buoys to storage.", 30)
|
||||
else
|
||||
-- simply drop them, irrecoverable
|
||||
trigger.action.outTextForGroup(conf.groupID, "Discarded <" .. amount .. "> ASW Buoys.", 30)
|
||||
end
|
||||
aswGUI.processWeightFor(conf)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
function aswGUI.xHandleLoadTorpedoes(args)
|
||||
timer.scheduleFunction(aswGUI.handleLoadTorpedoes, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function aswGUI.handleLoadTorpedoes(args)
|
||||
local conf = args
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
if not theUnit then
|
||||
trigger.action.outText("+++aswG: (load torps) can't find unit <" .. conf.name .. ">", 30)
|
||||
return
|
||||
end
|
||||
local loc = theUnit:getPoint()
|
||||
local theZone = aswZones.getClosestASWZoneTo(loc)
|
||||
local inZone = cfxZones.pointInZone(loc, theZone)
|
||||
local tStore = 0 -- available torpedoes
|
||||
if inZone then
|
||||
tStore = theZone.torpedoNum
|
||||
if tStore < 0 then tStore = aswGUI.torpedoesPerSlot end
|
||||
else
|
||||
trigger.action.outTextForGroup(conf.groupID, "Nothing loaded. Return to ASW loading zone.", 30)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
return
|
||||
end
|
||||
|
||||
if tStore < 1 then
|
||||
trigger.action.outTextForGroup(conf.groupID, "ASW Torpedo stock has run out. Sorry.", 30)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
return
|
||||
end
|
||||
|
||||
local capa = aswGUI.getTorpedoCapa(conf)
|
||||
capa = 1 -- load one at a time
|
||||
conf.torpedoNum=conf.torpedoNum + capa
|
||||
if theZone.torpedoNum >= 0 then
|
||||
theZone.torpedoNum = theZone.torpedoNum - capa
|
||||
if theZone.torpedoNum < 0 then theZone.torpedoNum = 0 end
|
||||
end
|
||||
|
||||
aswGUI.processWeightFor(conf)
|
||||
|
||||
trigger.action.outTextForGroup(conf.groupID, "Loaded <" .. capa .. "> asw Torpedoes.", 30)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
function aswGUI.xHandleUnloadTorpedoes(args)
|
||||
timer.scheduleFunction(aswGUI.handleUnloadTorpedoes, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function aswGUI.handleUnloadTorpedoes(args)
|
||||
local conf = args
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
if not theUnit then
|
||||
trigger.action.outText("+++aswG: (unload torpedoes) can't find unit <" .. conf.name .. ">", 30)
|
||||
return
|
||||
end
|
||||
local loc = theUnit:getPoint()
|
||||
local theZone = aswZones.getClosestASWZoneTo(loc)
|
||||
local inZone = cfxZones.pointInZone(loc, theZone)
|
||||
|
||||
local amount = conf.torpedoNum
|
||||
while amount > aswGUI.torpedoesPerSlot do -- future proof, any # of slots
|
||||
amount = amount - aswGUI.torpedoesPerSlot
|
||||
end
|
||||
conf.torpedoNum = conf.torpedoNum - amount
|
||||
|
||||
if inZone then
|
||||
if theZone.torpedoNum >= 0 then theZone.torpedoNum = theZone.torpedoNum + amount end
|
||||
trigger.action.outTextForGroup(conf.groupID, "Returned <" .. amount .. "> ASW Torpedoes to storage.", 30)
|
||||
else
|
||||
-- simply drop them, irrecoverable
|
||||
trigger.action.outTextForGroup(conf.groupID, "Discarded <" .. amount .. "> ASW Torpedoes.", 30)
|
||||
end
|
||||
aswGUI.processWeightFor(conf)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
--
|
||||
-- LIVE DROP
|
||||
--
|
||||
function aswGUI.xHandleBuoyDropoff(args)
|
||||
timer.scheduleFunction(aswGUI.handleBuoyDropoff, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function aswGUI.hasDropoffParams(conf)
|
||||
-- to be added later, can be curtailed for units
|
||||
return true
|
||||
end
|
||||
|
||||
function aswGUI.handleBuoyDropoff(args)
|
||||
local conf = args
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
if not theUnit or not Unit.isExist(theUnit) then
|
||||
trigger.action.outText("+++aswG: (drop buoy) unit <" .. conf.name .. "> does not exits", 30)
|
||||
return
|
||||
end
|
||||
|
||||
-- we could now make height and speed checks, but dont really do
|
||||
if not aswGUI.hasDropoffParams(conf) then
|
||||
trigger.action.outTextForGroup(conf.groupID, "You need to be below xxx knots and yyy ft AGL to drop ASW munitions", 30)
|
||||
return
|
||||
end
|
||||
|
||||
-- check that we really have some buoys left
|
||||
if conf.buoyNum < 1 then
|
||||
trigger.action.outText("+++aswG: no buoys for <" .. conf.name .. ">.", 30)
|
||||
return
|
||||
end
|
||||
|
||||
conf.buoyNum = conf.buoyNum - 1
|
||||
|
||||
-- do the deed
|
||||
asw.dropBuoyFrom(theUnit)
|
||||
trigger.action.outTextForGroup(conf.groupID, "Dropping ASW Buoy...", 30)
|
||||
|
||||
-- wrap up
|
||||
aswGUI.processWeightFor(conf)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
function aswGUI.xHandleTorpedoDropoff(args)
|
||||
timer.scheduleFunction(aswGUI.handleTorpedoDropoff, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function aswGUI.handleTorpedoDropoff(args)
|
||||
local conf = args
|
||||
local theUnit = Unit.getByName(conf.name)
|
||||
if not theUnit or not Unit.isExist(theUnit) then
|
||||
trigger.action.outText("+++aswG: (drop torpedo) unit <" .. conf.name .. "> does not exits", 30)
|
||||
return
|
||||
end
|
||||
|
||||
-- we could now make height and speed checks, but dont really do
|
||||
if not aswGUI.hasDropoffParams(conf) then
|
||||
trigger.action.outTextForGroup(conf.groupID, "You need to be below xxx knots and yyy ft AGL to drop ASW munitions", 30)
|
||||
return
|
||||
end
|
||||
|
||||
-- check that we really have some buoys left
|
||||
if conf.torpedoNum < 1 then
|
||||
trigger.action.outText("+++aswG: no torpedoes for <" .. conf.name .. ">.", 30)
|
||||
return
|
||||
end
|
||||
|
||||
conf.torpedoNum = conf.torpedoNum - 1
|
||||
|
||||
-- do the deed
|
||||
asw.dropTorpedoFrom(theUnit)
|
||||
trigger.action.outTextForGroup(conf.groupID, "Dropping ASW Torpedo...", 30)
|
||||
|
||||
-- wrap up
|
||||
aswGUI.processWeightFor(conf)
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
--
|
||||
-- Event handling
|
||||
--
|
||||
function aswGUI:onEvent(theEvent)
|
||||
--env.info("> >ENTER aswGUI:onEvent")
|
||||
if not theEvent then
|
||||
trigger.action.outText("+++aswGUI: nil theEvent", 30)
|
||||
--env.info("< <ABEND aswGUI:onEvent: nil event")
|
||||
return
|
||||
end
|
||||
local theID = theEvent.id
|
||||
if not theID then
|
||||
trigger.action.outText("+++aswGUI: nil event.ID", 30)
|
||||
--env.info("< <ABEND aswGUI:onEvent: nil event ID")
|
||||
return
|
||||
end
|
||||
local initiator = theEvent.initiator
|
||||
if not initiator then
|
||||
--env.info("< <ABEND aswGUI:onEvent: nil initiator")
|
||||
return
|
||||
end -- not interested
|
||||
local theUnit = initiator
|
||||
if not Unit.isExist(theUnit) then
|
||||
trigger.action.outText("+++aswGUI: non-unit event filtred.", 30)
|
||||
--env.info("< <ABEND aswGUI:onEvent: theUnit does not exist")
|
||||
end
|
||||
local name = theUnit:getName()
|
||||
if not name then
|
||||
trigger.action.outText("+++aswGUI: unable to access unit name in onEvent, aborting", 30)
|
||||
--env.info("< <ABEND aswGUI:onEvent: theUnit not a unit/no name")
|
||||
return
|
||||
end
|
||||
-- see if this is a player aircraft
|
||||
if not theUnit.getPlayerName then
|
||||
--env.info("< <LEAVE aswGUI:onEvent: not player unit A")
|
||||
return
|
||||
end -- not a player
|
||||
if not theUnit:getPlayerName() then
|
||||
--env.info("< <LEAVE aswGUI:onEvent: not player unit B")
|
||||
return
|
||||
end -- not a player
|
||||
-- this is a player unit. Is it ASW carrier?
|
||||
local uType = theUnit:getTypeName()
|
||||
if not dcsCommon.isTroopCarrierType(uType, aswGUI.aswCarriers) then
|
||||
if aswGUI.verbose then
|
||||
trigger.action.outText("+++aswGUI: Player <" .. theUnit:getPlayerName() .. ">'s unit <" .. name .. "> of type <" .. uType .. "> is not ASW-capable. ASW Types are:", 30)
|
||||
for idx, aType in pairs(aswGUI.aswCarriers) do
|
||||
trigger.action.outText(aType,30)
|
||||
end
|
||||
end
|
||||
--env.info("< <LEAVE aswGUI:onEvent: not troop carrier")
|
||||
return
|
||||
end
|
||||
|
||||
--env.info("> >Proccing aswGUI:onEvent event <" .. theID .. "")
|
||||
|
||||
-- now let's access it if it was
|
||||
-- used before
|
||||
local conf = aswGUI.aswCraft[name]
|
||||
if not conf then
|
||||
-- let's init it
|
||||
conf = aswGUI.initUnit(name)
|
||||
if not conf then
|
||||
-- something went wrong, abort
|
||||
return
|
||||
end
|
||||
aswGUI.aswCraft[name] = conf
|
||||
end
|
||||
|
||||
-- if we get here, theUnit is an asw craft
|
||||
if theID == 4 or -- land
|
||||
theID == 3 then -- take off
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
return
|
||||
end
|
||||
|
||||
if theID == 20 or -- player enter
|
||||
theID == 15 then -- birth (server player enter)
|
||||
|
||||
-- reset
|
||||
aswGUI.resetConf(conf)
|
||||
-- set menus
|
||||
aswGUI.setMenuForUnit(theUnit)
|
||||
end
|
||||
|
||||
if theID == 21 then -- player leave
|
||||
aswGUI.resetConf(conf)
|
||||
end
|
||||
--env.info("< <Proccing complete asw event <" .. theID .. "")
|
||||
end
|
||||
|
||||
--
|
||||
-- Config & start
|
||||
--
|
||||
function aswGUI.readConfigZone()
|
||||
local theZone = cfxZones.getZoneByName("aswGUIConfig")
|
||||
|
||||
if not theZone then
|
||||
if aswGUI.verbose then
|
||||
trigger.action.outText("+++aswGUI: no config zone!", 30)
|
||||
end
|
||||
theZone = cfxZones.createSimpleZone("aswGUIConfig")
|
||||
end
|
||||
aswGUI.verbose = theZone.verbose
|
||||
|
||||
-- read & set defaults
|
||||
if cfxZones.hasProperty(theZone, "aswCarriers") then
|
||||
local carr = cfxZones.getStringFromZoneProperty(theZone, "aswCarriers", "")
|
||||
carr = dcsCommon.splitString(carr, ",")
|
||||
aswGUI.aswCarriers = dcsCommon.trimArray(carr)
|
||||
end
|
||||
|
||||
aswGUI.buoysPerSlot = 10
|
||||
aswGUI.torpedoesPerSlot = 2
|
||||
aswGUI.buoyWeight = 50 -- kg, 10x = 500, 20x = 1000
|
||||
aswGUI.buoyWeight = cfxZones.getNumberFromZoneProperty(theZone, "buoyWeight", aswGUI.buoyWeight)
|
||||
aswGUI.torpedoWeight = 700 -- kg
|
||||
aswGUI.torpedoWeight = cfxZones.getNumberFromZoneProperty(theZone, "torpedoWeight", aswGUI.torpedoWeight)
|
||||
|
||||
if aswGUI.verbose then
|
||||
trigger.action.outText("+++aswGUI: read config", 30)
|
||||
end
|
||||
end
|
||||
|
||||
function aswGUI.start()
|
||||
--env.info(">>>ENTER asw GUI start")
|
||||
if not dcsCommon.libCheck then
|
||||
trigger.action.outText("cfx aswGUI requires dcsCommon", 30)
|
||||
return false
|
||||
end
|
||||
if not dcsCommon.libCheck("cfx aswGUI", aswGUI.requiredLibs) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- read config
|
||||
aswGUI.readConfigZone()
|
||||
|
||||
-- subscribe to world events
|
||||
world.addEventHandler(aswGUI)
|
||||
|
||||
-- say Hi
|
||||
trigger.action.outText("cfx ASW GUI v" .. aswGUI.version .. " started.", 30)
|
||||
--env.info("<<<asw GUI started")
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- start up aswZones
|
||||
--
|
||||
if not aswGUI.start() then
|
||||
trigger.action.outText("cfx aswGUI aborted: missing libraries", 30)
|
||||
aswGUI = nil
|
||||
end
|
||||
192
modules/aswSubs.lua
Normal file
192
modules/aswSubs.lua
Normal file
@ -0,0 +1,192 @@
|
||||
aswSubs = {}
|
||||
aswSubs.version = "1.0.0"
|
||||
aswSubs.verbose = false
|
||||
aswSubs.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
"cfxZones", -- Zones, of course
|
||||
}
|
||||
|
||||
--[[--
|
||||
Version History
|
||||
1.0.0 - initial version
|
||||
|
||||
--]]--
|
||||
|
||||
aswSubs.groupsToWatch = {} -- subs attack any group in here if they are of a different coalition and not neutral
|
||||
aswSubs.unitsHit = {} -- the goners
|
||||
|
||||
function aswSubs.addWatchgroup(name)
|
||||
if Group.getByName(name) then
|
||||
aswSubs.groupsToWatch[name] = name
|
||||
else
|
||||
trigger.action.outText("+++aswSubs: no group named <" .. name .. "> to watch over", 30)
|
||||
end
|
||||
end
|
||||
|
||||
function aswSubs.gatherSubs()
|
||||
local allCoas = {0, 1, 2}
|
||||
local subs = {}
|
||||
for idx, coa in pairs(allCoas) do
|
||||
local allGroups = coalition.getGroups(coa, 3) -- ships only
|
||||
for idy, aGroup in pairs(allGroups) do
|
||||
allUnits = aGroup:getUnits()
|
||||
for idz, aUnit in pairs(allUnits) do
|
||||
-- see if this unit is a sub
|
||||
if aUnit and Unit.isExist(aUnit) then
|
||||
if (dcsCommon.getUnitAGL(aUnit) < -5) then -- submerged contact.
|
||||
local contact = {}
|
||||
contact.theUnit = aUnit
|
||||
contact.coalition = coa
|
||||
contact.name = aUnit:getName()
|
||||
contact.loc = aUnit:getPoint()
|
||||
subs[contact.name] = contact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return subs
|
||||
end
|
||||
|
||||
function aswSubs.boom(args)
|
||||
|
||||
local uName = args.name
|
||||
local loc = args.loc
|
||||
local theUnit = Unit.getByName(uName)
|
||||
if theUnit and theUnit.isExist(theUnit) then
|
||||
loc = theUnit:getPoint()
|
||||
end
|
||||
|
||||
trigger.action.explosion(loc, aswSubs.explosionDamage)
|
||||
end
|
||||
|
||||
function aswSubs.alert(theUnit, theContact)
|
||||
-- note: we dont need theContact right now
|
||||
if not theUnit or not Unit.isExist(theUnit) then
|
||||
return
|
||||
end
|
||||
|
||||
-- see if this was hit before
|
||||
local uName = theUnit:getName()
|
||||
if aswSubs.unitsHit[uName] then return end
|
||||
|
||||
-- mark it as hit
|
||||
aswSubs.unitsHit[uName] = theContact.name
|
||||
|
||||
-- schedule a few explosions
|
||||
local args = {}
|
||||
args.name = uName
|
||||
args.loc = theUnit:getPoint()
|
||||
local salvoSize = tonumber(aswSubs.salvoMin)
|
||||
local varPart = tonumber(aswSubs.salvoMax) - tonumber(aswSubs.salvoMin)
|
||||
if varPart > 0 then
|
||||
varPart = dcsCommon.smallRandom(varPart)
|
||||
salvoSize = salvoSize + varPart
|
||||
end
|
||||
|
||||
for i=1, tonumber(salvoSize) do
|
||||
timer.scheduleFunction(aswSubs.boom, args, timer.getTime() + i*2 + 4)
|
||||
end
|
||||
|
||||
-- theContact has come within crit dist of theUnit
|
||||
local coa = theUnit:getCoalition()
|
||||
trigger.action.outTextForCoalition(coa, theUnit:getName() .. " reports " .. salvoSize .. " incoming torpedoes!", 30)
|
||||
end
|
||||
|
||||
function aswSubs.update()
|
||||
--env.info("-->Enter asw Subs update")
|
||||
timer.scheduleFunction(aswSubs.update, {}, timer.getTime() + 1)
|
||||
|
||||
-- get all current subs
|
||||
local allSubs = aswSubs.gatherSubs()
|
||||
|
||||
-- now iterate all watch groups
|
||||
for idx, name in pairs(aswSubs.groupsToWatch) do
|
||||
local theGroup = Group.getByName(name)
|
||||
if theGroup and Group.isExist(theGroup) then
|
||||
local groupCoa = theGroup:getCoalition()
|
||||
if theGroup and Group.isExist(theGroup) then
|
||||
allUnits = theGroup:getUnits()
|
||||
for idx, aUnit in pairs(allUnits) do
|
||||
-- check against all subs
|
||||
if aUnit and Unit.isExist(aUnit) then
|
||||
local loc = aUnit:getPoint()
|
||||
for cName, contact in pairs(allSubs) do
|
||||
-- attack other side but not neutral
|
||||
if groupCoa ~= contact.coalition and groupCoa ~= 0 then
|
||||
-- ok, go check
|
||||
local dist = dcsCommon.dist(loc, contact.loc)
|
||||
if dist < aswSubs.critDist then
|
||||
aswSubs.alert(aUnit, contact)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
--env.info("<--Levae asw Subs update")
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Config & start
|
||||
--
|
||||
function aswSubs.readConfigZone()
|
||||
local theZone = cfxZones.getZoneByName("aswSubsConfig")
|
||||
if not theZone then
|
||||
if aswSubs.verbose then
|
||||
trigger.action.outText("+++aswSubs: no config zone!", 30)
|
||||
end
|
||||
theZone = cfxZones.createSimpleZone("aswSubsConfig")
|
||||
end
|
||||
|
||||
-- read & set defaults
|
||||
aswSubs.critDist = 4000
|
||||
aswSubs.critDist = cfxZones.getNumberFromZoneProperty(theZone, "critDist", aswSubs.critDist)
|
||||
aswSubs.explosionDamage = 1000
|
||||
aswSubs.explosionDamage = cfxZones.getNumberFromZoneProperty(theZone, "explosionDamage", aswSubs.explosionDamage)
|
||||
|
||||
aswSubs.salvoMin, aswSubs.salvoMax = cfxZones.getPositiveRangeFromZoneProperty(theZone, "salvoSize", 4, 4)
|
||||
--trigger.action.outText("salvo: min <" .. aswSubs.salvoMin .. ">, max <" .. aswSubs.salvoMax .. ">", 30)
|
||||
local targets = cfxZones.getStringFromZoneProperty(theZone, "targets", "")
|
||||
local t2 = dcsCommon.string2Array(targets, ",")
|
||||
for idx, targetName in pairs (t2) do
|
||||
aswSubs.addWatchgroup(targetName)
|
||||
end
|
||||
|
||||
if aswSubs.verbose then
|
||||
trigger.action.outText("+++aswSubs: read config", 30)
|
||||
end
|
||||
end
|
||||
|
||||
function aswSubs.start()
|
||||
if not dcsCommon.libCheck then
|
||||
trigger.action.outText("cfx aswSubs requires dcsCommon", 30)
|
||||
return false
|
||||
end
|
||||
if not dcsCommon.libCheck("cfx aswSubs", aswSubs.requiredLibs) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- read config
|
||||
aswSubs.readConfigZone()
|
||||
|
||||
-- start the script
|
||||
aswSubs.update()
|
||||
|
||||
-- all is good
|
||||
trigger.action.outText("cfx ASW Subs v" .. aswGUI.version .. " started.", 30)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- start up aswSubs
|
||||
--
|
||||
if not aswSubs.start() then
|
||||
trigger.action.outText("cfx aswSubs aborted: missing libraries", 30)
|
||||
aswSubs = nil
|
||||
end
|
||||
|
||||
193
modules/aswZones.lua
Normal file
193
modules/aswZones.lua
Normal file
@ -0,0 +1,193 @@
|
||||
aswZones = {}
|
||||
aswZones.version = "1.0.0"
|
||||
aswZones.verbose = false
|
||||
aswZones.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
"cfxZones", -- Zones, of course
|
||||
"asw", -- needs asw module
|
||||
}
|
||||
--[[--
|
||||
Version History
|
||||
1.0.0 - initial version
|
||||
|
||||
--]]--
|
||||
|
||||
aswZones.ups = 1 -- = once every second
|
||||
aswZones.zones = {} -- all zones, by name
|
||||
|
||||
function aswZones.addZone(theZone)
|
||||
if not theZone then
|
||||
trigger.action.outText("aswZ: nil zone in addZone", 30)
|
||||
return
|
||||
end
|
||||
aswZones.zones[theZone.name] = theZone
|
||||
end
|
||||
|
||||
function aswZones.getZoneNamed(theName)
|
||||
if not theName then return nil end
|
||||
return aswZones[theName]
|
||||
end
|
||||
|
||||
function aswZones.getClosestASWZoneTo(loc)
|
||||
local closestZone = nil
|
||||
local loDist = math.huge
|
||||
for name, theZone in pairs(aswZones.zones) do
|
||||
local zp = cfxZones.getPoint(theZone)
|
||||
local d = dcsCommon.distFlat(zp, loc)
|
||||
if d < loDist then
|
||||
loDist = d
|
||||
closestZone = theZone
|
||||
end
|
||||
end
|
||||
return closestZone, loDist
|
||||
end
|
||||
|
||||
function aswZones.createASWZone(theZone)
|
||||
-- get inventory of buoys
|
||||
theZone.buoyNum = cfxZones.getNumberFromZoneProperty(theZone, "buoyS", -1) -- also used as supply for helos if they land in zone
|
||||
theZone.torpedoNum = cfxZones.getNumberFromZoneProperty(theZone, "torpedoes", -1) -- also used as supply for helos if they land in zone
|
||||
|
||||
theZone.coalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0)
|
||||
|
||||
-- trigger method
|
||||
theZone.aswTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change")
|
||||
if cfxZones.hasProperty(theZone, "aswTriggerMethod") then
|
||||
theZone.aswTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "aswTriggerMethod", "change")
|
||||
end
|
||||
|
||||
if cfxZones.hasProperty(theZone, "buoy?") then
|
||||
theZone.buoyFlag = cfxZones.getStringFromZoneProperty(theZone, "buoy?", "none")
|
||||
theZone.lastBuoyValue = cfxZones.getFlagValue(theZone.buoyFlag, theZone)
|
||||
end
|
||||
|
||||
if cfxZones.hasProperty(theZone, "torpedo?") then
|
||||
theZone.torpedoFlag = cfxZones.getStringFromZoneProperty(theZone, "torpedo?", "none")
|
||||
theZone.lastTorpedoValue = cfxZones.getFlagValue(theZone.torpedoFlag, theZone)
|
||||
end
|
||||
|
||||
if theZone.verbose or aswZones.verbose then
|
||||
trigger.action.outText("+++aswZ: new asw zone <" .. theZone.name .. ">", 30)
|
||||
trigger.action.outText("has coalition " .. theZone.coalition, 30)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- responding to triggers
|
||||
--
|
||||
function aswZones.dropBuoy(theZone)
|
||||
if theZone.buoyNum == 0 then
|
||||
-- we are fresh out. no launch
|
||||
if theZone.verbose or aswZones.verbose then
|
||||
trigger.action.outText("+++aswZ: zone <" .. theZone.name .. "> is out of buoys, can't drop", 30)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local theBuoy = asw.dropBuoyFromZone(theZone)
|
||||
if theZone.buoyNum > 0 then
|
||||
theZone.buoyNum = theZone.buoyNum - 1
|
||||
end
|
||||
end
|
||||
|
||||
function aswZones.dropTorpedo(theZone)
|
||||
if theZone.torpedoNum == 0 then
|
||||
-- we are fresh out. no launch
|
||||
if theZone.verbose or aswZones.verbose then
|
||||
trigger.action.outText("+++aswZ: zone <" .. theZone.name .. "> is out of torpedoes, can't drop", 30)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local theTorpedo = asw.dropTorpedoFromZone(theZone)
|
||||
if theZone.torpedoNum > 0 then
|
||||
theZone.torpedoNum = theZone.torpedoNum - 1
|
||||
end
|
||||
end
|
||||
--
|
||||
-- Update
|
||||
--
|
||||
function aswZones.update()
|
||||
--env.info("-->Enter asw ZONES update")
|
||||
-- first, schedule next invocation
|
||||
timer.scheduleFunction(aswZones.update, {}, timer.getTime() + 1/aswZones.ups)
|
||||
|
||||
for zName, theZone in pairs(aswZones.zones) do
|
||||
if theZone.buoyFlag and cfxZones.testZoneFlag(theZone, theZone.buoyFlag, theZone.aswTriggerMethod, "lastBuoyValue") then
|
||||
trigger.action.outText("zone <" .. theZone.name .. "> will now drop a buoy", 30)
|
||||
aswZones.dropBuoy(theZone)
|
||||
end
|
||||
|
||||
if theZone.torpedoFlag and cfxZones.testZoneFlag(theZone, theZone.torpedoFlag, theZone.aswTriggerMethod, "lastTorpedoValue") then
|
||||
trigger.action.outText("zone <" .. theZone.name .. "> will now drop a TORPEDO", 30)
|
||||
aswZones.dropTorpedo(theZone)
|
||||
end
|
||||
end
|
||||
|
||||
--env.info("<--Leave asw ZONES update")
|
||||
end
|
||||
|
||||
--
|
||||
-- Config & start
|
||||
--
|
||||
function aswZones.readConfigZone()
|
||||
local theZone = cfxZones.getZoneByName("aswZonesConfig")
|
||||
if not theZone then
|
||||
if aswZones.verbose then
|
||||
trigger.action.outText("+++aswZ: no config zone!", 30)
|
||||
end
|
||||
theZone = cfxZones.createSimpleZone("aswZonesConfig")
|
||||
end
|
||||
aswZones.verbose = theZone.verbose
|
||||
|
||||
-- set defaults, later do the reading
|
||||
|
||||
|
||||
if aswZones.verbose then
|
||||
trigger.action.outText("+++aswZ: read config", 30)
|
||||
end
|
||||
end
|
||||
|
||||
function aswZones.start()
|
||||
if not dcsCommon.libCheck then
|
||||
trigger.action.outText("cfx aswZones requires dcsCommon", 30)
|
||||
return false
|
||||
end
|
||||
if not dcsCommon.libCheck("cfx aswZones", aswZones.requiredLibs) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- read config
|
||||
aswZones.readConfigZone()
|
||||
|
||||
-- read zones
|
||||
local attrZones = cfxZones.getZonesWithAttributeNamed("asw")
|
||||
|
||||
-- collect my zones
|
||||
for k, aZone in pairs(attrZones) do
|
||||
aswZones.createASWZone(aZone) -- process attributes
|
||||
aswZones.addZone(aZone) -- add to inventory
|
||||
end
|
||||
|
||||
-- start update
|
||||
aswZones.update()
|
||||
|
||||
-- say hi
|
||||
trigger.action.outText("cfx aswZones v" .. aswZones.version .. " started.", 30)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- start up aswZones
|
||||
--
|
||||
if not aswZones.start() then
|
||||
trigger.action.outText("cfx aswZones aborted: missing libraries", 30)
|
||||
aswZones = nil
|
||||
end
|
||||
|
||||
-- add asw.helper with zones that can
|
||||
-- drop torps
|
||||
-- have inventory per zone or -1 as infinite
|
||||
-- have an event when a buoy finds something
|
||||
-- hav an event when a buoy times out
|
||||
-- have buoyOut! and torpedoOut! events
|
||||
@ -4,7 +4,7 @@
|
||||
-- *** EXTENDS ZONES: 'pathing' attribute
|
||||
--
|
||||
cfxCommander = {}
|
||||
cfxCommander.version = "1.1.2"
|
||||
cfxCommander.version = "1.1.3"
|
||||
--[[-- VERSION HISTORY
|
||||
- 1.0.5 - createWPListForGroupToPointViaRoads: detect no road found
|
||||
- 1.0.6 - build in more group checks in assign wp list
|
||||
@ -27,6 +27,9 @@ cfxCommander.version = "1.1.2"
|
||||
- makeGroupStopTransmitting
|
||||
- verbose check before path warning
|
||||
- added delay defaulting for most scheduling functions
|
||||
- 1.1.3 - isExist() guard improvements for multiple methods
|
||||
- cleaned up comments
|
||||
|
||||
--]]--
|
||||
|
||||
cfxCommander.requiredLibs = {
|
||||
@ -199,7 +202,8 @@ function cfxCommander.doScheduledTask(data)
|
||||
end
|
||||
local theGroup = data.group
|
||||
if not theGroup then return end
|
||||
if not theGroup.isExist then return end
|
||||
if not Group.isExist(theGroup) then return end
|
||||
-- if not theGroup.isExist then return end
|
||||
|
||||
local theController = theGroup:getController()
|
||||
theController:pushTask(data.task)
|
||||
@ -290,7 +294,7 @@ function cfxCommander.assignWPListToGroup(group, wpList, delay)
|
||||
group = Group.getByName(group)
|
||||
end
|
||||
if not group then return end
|
||||
if not group:isExist() then return end
|
||||
if not Group.isExist(group) then return end
|
||||
|
||||
local theTask = cfxCommander.buildTaskFromWPList(wpList)
|
||||
local ctrl = group:getController()
|
||||
@ -427,7 +431,6 @@ function cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, delay
|
||||
if oRide and oRide.pathing == "offroad" then
|
||||
-- yup, override road preference
|
||||
cfxCommander.makeGroupGoThere(group, there, speed, "Off Road", delay)
|
||||
--trigger.action.outText("pathing: override offroad")
|
||||
return
|
||||
end
|
||||
end
|
||||
@ -441,7 +444,7 @@ end
|
||||
|
||||
function cfxCommander.makeGroupHalt(group, delay)
|
||||
if not group then return end
|
||||
if not group:isExist() then return end
|
||||
if not Group.isExist(group) then return end
|
||||
if not delay then delay = 0 end
|
||||
local theTask = {id = 'Hold', params = {}}
|
||||
cfxCommander.scheduleTaskForGroup(group, theTask, delay)
|
||||
|
||||
@ -21,84 +21,90 @@ cfxGroundTroops.requiredLibs = {
|
||||
-- module and addTroopsToPool to have them then managed by this
|
||||
-- module
|
||||
|
||||
cfxGroundTroops.deployedTroops = {}
|
||||
cfxGroundTroops.deployedTroops = {} -- indexed by group name
|
||||
|
||||
-- version history
|
||||
-- 1.3.0 - added "wait-" prefix to have toops do nothing
|
||||
-- - added lazing
|
||||
-- 1.3.1 - sound for lazing msg is "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav"
|
||||
-- - lazing --> lasing in text
|
||||
-- 1.3.2 - set ups to 2
|
||||
-- 1.4.0 - queued updates except for lazers
|
||||
-- 1.4.1 - makeTroopsEngageZone now issues hold before moving on 5 seconds later
|
||||
-- - getTroopReport
|
||||
-- - include size of group
|
||||
-- 1.4.2 - uses unitIsInfantry from dcsCommon
|
||||
-- 1.5.0 - new scheduled updates per troop to reduce processor load
|
||||
-- - tiebreak code
|
||||
-- 1.5.1 - small bugfix in scheduled code
|
||||
-- 1.5.2 - checkSchedule
|
||||
-- - speed warning in scheduler
|
||||
-- - go off road when speed warning too much
|
||||
-- 1.5.3 - monitor troops
|
||||
-- - managed queue for ground troops
|
||||
-- - on second switch to offroad now removed from MQ
|
||||
-- 1.5.4 - removed debugging messages
|
||||
-- 1.5.5 - removed bug in troop report reading nil destination
|
||||
-- 1.6.0 - check modules
|
||||
-- 1.6.1 - troopsCallback management so you can be informed if a
|
||||
-- troop you have added to the pool is dead or has achieved a goal.
|
||||
-- callback will list reasons "dead" and "arrived"
|
||||
-- updateAttackers
|
||||
-- 1.6.2 - also accept 'lase' as 'laze', translate directly
|
||||
-- 1.7.0 - now can use groundTroopsConfig zone
|
||||
-- 1.7.1 - addTroopsDeadCallback() renamed to addTroopsCallback()
|
||||
-- - invokeCallbacksFor also accepts and passes on data block
|
||||
-- - troops is always passed in data block as .troops
|
||||
-- 1.7.2 - callback when group is neutralized on guard orders
|
||||
-- - callback when group is being engaged under guard orders
|
||||
-- 1.7.3 - callbacks for lase:tracking and lase:stop
|
||||
-- 1.7.4 - verbose flag, warnings suppressed
|
||||
-- 1.7.5 - some troop.group hardening with isExist()
|
||||
-- 1.7.6 - fixed switchToOffroad
|
||||
-- 1.7.7 - no longer case sensitive for orders
|
||||
--[[--
|
||||
version history
|
||||
1.3.0 - added "wait-" prefix to have toops do nothing
|
||||
- added lazing
|
||||
1.3.1 - sound for lazing msg is "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav"
|
||||
- lazing --> lasing in text
|
||||
1.3.2 - set ups to 2
|
||||
1.4.0 - queued updates except for lazers
|
||||
1.4.1 - makeTroopsEngageZone now issues hold before moving on 5 seconds later
|
||||
- getTroopReport
|
||||
- include size of group
|
||||
1.4.2 - uses unitIsInfantry from dcsCommon
|
||||
1.5.0 - new scheduled updates per troop to reduce processor load
|
||||
- tiebreak code
|
||||
1.5.1 - small bugfix in scheduled code
|
||||
1.5.2 - checkSchedule
|
||||
- speed warning in scheduler
|
||||
- go off road when speed warning too much
|
||||
1.5.3 - monitor troops
|
||||
- managed queue for ground troops
|
||||
- on second switch to offroad now removed from MQ
|
||||
1.5.4 - removed debugging messages
|
||||
1.5.5 - removed bug in troop report reading nil destination
|
||||
1.6.0 - check modules
|
||||
1.6.1 - troopsCallback management so you can be informed if a
|
||||
troop you have added to the pool is dead or has achieved a goal.
|
||||
callback will list reasons "dead" and "arrived"
|
||||
updateAttackers
|
||||
1.6.2 - also accept 'lase' as 'laze', translate directly
|
||||
1.7.0 - now can use groundTroopsConfig zone
|
||||
1.7.1 - addTroopsDeadCallback() renamed to addTroopsCallback()
|
||||
- invokeCallbacksFor also accepts and passes on data block
|
||||
- troops is always passed in data block as .troops
|
||||
1.7.2 - callback when group is neutralized on guard orders
|
||||
- callback when group is being engaged under guard orders
|
||||
1.7.3 - callbacks for lase:tracking and lase:stop
|
||||
1.7.4 - verbose flag, warnings suppressed
|
||||
1.7.5 - some troop.group hardening with isExist()
|
||||
1.7.6 - fixed switchToOffroad
|
||||
1.7.7 - no longer case sensitive for orders
|
||||
1.7.7 - updateAttackers() now inspects 'moving' status and invokes makeTroopsEngageZone
|
||||
- makeTroopsEngageZone() sets 'moving' status to true
|
||||
- createGroundTroops() sets moving status to false
|
||||
- updateZoneAttackers() uses moving
|
||||
|
||||
|
||||
-- an entry into the deployed troop has the following attributes
|
||||
-- - group - the group
|
||||
-- - orders: "guard" - will guard the spot and look for enemies in range
|
||||
-- "patrol" - will walk between way points back and forth
|
||||
-- "laze" - will stay in place and try to laze visible vehicles in range
|
||||
-- "attackOwnedZone" - interface to cfxOwnedZones module, seeks out
|
||||
-- enemy zones to attack and capture them
|
||||
-- "wait-<some other orders>" do nothing. the "wait" prefix will be removed some time and <some other order> then revealed. Used at least by heloTroops
|
||||
-- "train" - target dummies. ROE=HOLD, no ground loop
|
||||
-- "attack" - transition to destination, once there, stop and
|
||||
-- switch to guard. requires destination zone be sez to a valid cfxZone
|
||||
-- - coalition - the coalition from the group
|
||||
-- - enemy - if set, the group this group it is engaging. this means the group is fighting and not idle
|
||||
-- - name - name of group, dan be freely changed
|
||||
-- - signature - "cfx" to tell apart from dcs groups
|
||||
-- - range = range to look for enemies. default is 300m. In "laze" orders, range to laze
|
||||
-- - lazeTarget - target currently lazing
|
||||
-- - lazeCode - laser code. default is 1688
|
||||
an entry into the deployed troop table has the following attributes
|
||||
- group - the group
|
||||
- orders: "guard" - will guard the spot and look for enemies in range
|
||||
"patrol" - will walk between way points back and forth
|
||||
"laze" - will stay in place and try to laze visible vehicles in range
|
||||
"attackOwnedZone" - interface to cfxOwnedZones module, seeks out
|
||||
enemy zones to attack and capture them
|
||||
"wait-<some other orders>" do nothing. the "wait" prefix will be removed some time and <some other order> then revealed. Used at least by heloTroops
|
||||
"train" - target dummies. ROE=HOLD, no ground loop
|
||||
"attack" - transition to destination, once there, stop and
|
||||
switch to guard. requires destination zone be set to a valid cfxZone
|
||||
- coalition - the coalition from the group
|
||||
- enemy - if set, the group this group it is engaging. this means the group is fighting and not idle
|
||||
- name - name of group, dan be freely changed
|
||||
- signature - "cfx" to tell apart from dcs groups
|
||||
- range = range to look for enemies. default is 300m. In "laze" orders, range to laze
|
||||
- lazeTarget - target currently lazing
|
||||
- lazeCode - laser code. default is 1688
|
||||
- moving - has been given orders to move somewhere already. used for first movement order with attack orders
|
||||
|
||||
--
|
||||
-- usage:
|
||||
-- take a dcs group of ground troops and create a cfx ground troop record with
|
||||
-- createGroundTroops()
|
||||
-- then add this to the manager with
|
||||
-- addGroundTroopsToPool()
|
||||
--
|
||||
-- you can control what the group is to do by changing the cfx troop attribute orders
|
||||
-- you can install a callback that will notify you if a troop reached a goal or
|
||||
-- was killed with addTroopsCallback() which will also give a reason
|
||||
-- callback pattern is myCallback(reason, theGroup, orders, data) with troop being the
|
||||
-- group, and orders the original orders, and reason a string containing why the
|
||||
-- callback was invoked. Currently defined reasons are
|
||||
-- - "dead" - entire group was killed
|
||||
-- - "arrived" - at least a part of group arrived at destination (only with some orders)
|
||||
--
|
||||
|
||||
usage:
|
||||
take a dcs group of ground troops and create a cfx ground troop record with
|
||||
createGroundTroops()
|
||||
then add this to the manager with
|
||||
addGroundTroopsToPool()
|
||||
|
||||
you can control what the group is to do by changing the cfx troop attribute orders
|
||||
you can install a callback that will notify you if a troop reached a goal or
|
||||
was killed with addTroopsCallback() which will also give a reason
|
||||
callback pattern is myCallback(reason, theGroup, orders, data) with troop being the
|
||||
group, and orders the original orders, and reason a string containing why the
|
||||
callback was invoked. Currently defined reasons are
|
||||
- "dead" - entire group was killed
|
||||
- "arrived" - at least a part of group arrived at destination (only with some orders)
|
||||
--]]--
|
||||
|
||||
--
|
||||
-- UPDATE MODELS
|
||||
@ -129,7 +135,7 @@ function cfxGroundTroops.readConfigZone()
|
||||
if cfxGroundTroops.verbose then
|
||||
trigger.action.outText("***gndT: NO config zone!", 30)
|
||||
end
|
||||
return
|
||||
theZone = cfxZones.createSimpleZone("groundTroopsConfig")
|
||||
end
|
||||
|
||||
-- ok, for each property, load it if it exists
|
||||
@ -198,9 +204,10 @@ end
|
||||
|
||||
-- create controller commands to attack a group "enemies"
|
||||
-- enemies are an attribute of the troop structure
|
||||
-- usually called from a group on guard when idling
|
||||
function cfxGroundTroops.makeTroopsEngageEnemies(troop)
|
||||
local group = troop.group
|
||||
if not group:isExist() then
|
||||
if not Group.isExist(group) then
|
||||
trigger.action.outText("+++gndT: troup don't exist, dropping", 30)
|
||||
return
|
||||
end
|
||||
@ -214,10 +221,11 @@ function cfxGroundTroops.makeTroopsEngageEnemies(troop)
|
||||
-- we lerp to 2/3 of enemy location
|
||||
there = dcsCommon.vLerp(from, there, 0.66)
|
||||
|
||||
local speed = 10 -- m/s = 10 km/h
|
||||
local speed = 10 -- m/s = 10 km/h -- wait. 10 m/s is 36 km/h
|
||||
cfxCommander.makeGroupGoThere(group, there, speed)
|
||||
local attask = cfxCommander.createAttackGroupCommand(enemies)
|
||||
cfxCommander.scheduleTaskForGroup(group, attask, 0.5)
|
||||
troop.moving = true
|
||||
end
|
||||
|
||||
-- make the troops engage a cfxZone passed in the destination
|
||||
@ -225,7 +233,7 @@ end
|
||||
function cfxGroundTroops.makeTroopsEngageZone(troop)
|
||||
local group = troop.group
|
||||
if not group:isExist() then
|
||||
trigger.action.outText("+++gndT: make engage zone: troops do not exist, exiting", 30)
|
||||
trigger.action.outText("+++gndT: make troops engage zone: troops do not exist, exiting", 30)
|
||||
return
|
||||
end
|
||||
|
||||
@ -234,23 +242,15 @@ function cfxGroundTroops.makeTroopsEngageZone(troop)
|
||||
if not from then return end -- the group died
|
||||
local there = enemyZone.point -- access zone position
|
||||
if not there then return end
|
||||
|
||||
-- we lerp to 102% of enemy location to force overshoot and engagement
|
||||
--there = dcsCommon.vLerp(from, there, 1.02)
|
||||
|
||||
|
||||
local speed = 14 -- m/s; 10 m/s = 36 km/h
|
||||
-- we prefer going over roads since we don't know
|
||||
-- what is there
|
||||
|
||||
-- make troops stop in 1 second, then start in 5 seconds to give AI respite
|
||||
cfxCommander.makeGroupHalt(group, 1) -- 1 second delay
|
||||
cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, 5)
|
||||
-- no attack command since we don't know what is there
|
||||
-- but mayhaps we should issue weapons free?
|
||||
-- we'll soon test that by sticking in a troop on the way
|
||||
|
||||
-- local attask = cfxCommander.createAttackGroupCommand(enemies)
|
||||
-- cfxCommander.scheduleTaskForGroup(group, attask, 0.5)
|
||||
|
||||
-- remember that we have issued a move order
|
||||
troop.moving = true
|
||||
end
|
||||
|
||||
function cfxGroundTroops.switchToOffroad(troops)
|
||||
@ -301,13 +301,11 @@ function cfxGroundTroops.updateZoneAttackers(troop)
|
||||
local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop)
|
||||
if not newTargetZone then
|
||||
-- all target zones are friendly, go to guard mode
|
||||
-- trigger.action.outTextForCoalition(troop.side, troop.name .. " holding position", 30)
|
||||
troop.orders = "guard"
|
||||
return
|
||||
end
|
||||
|
||||
if newTargetZone ~= troop.destination then
|
||||
-- trigger.action.outTextForCoalition(troop.side, troop.name .. " enroute to " .. newTargetZone.name, 30)
|
||||
troop.destination = newTargetZone
|
||||
cfxGroundTroops.makeTroopsEngageZone(troop)
|
||||
troop.lastOrderDate = timer.getTime()
|
||||
@ -315,23 +313,37 @@ function cfxGroundTroops.updateZoneAttackers(troop)
|
||||
return
|
||||
end
|
||||
|
||||
-- if we get here, we should be under way to our nearest enemy zone
|
||||
if not troop.moving then
|
||||
cfxGroundTroops.makeTroopsEngageZone(troop)
|
||||
return
|
||||
end
|
||||
|
||||
-- if we get here, we are under way to troop.destination
|
||||
-- check if we are inside the zone, and if so, set variable to true
|
||||
local p = dcsCommon.getGroupLocation(troop.group)
|
||||
troop.insideDestination = cfxZones.isPointInsideZone(p, troop.destination)
|
||||
|
||||
-- if we get here, we need no change
|
||||
|
||||
-- if we get here, we need no change
|
||||
|
||||
end
|
||||
|
||||
-- attackers simply travel to their destination, and then switch to
|
||||
-- attackers simply travel to their destination (zone), and then switch to
|
||||
-- guard orders once they arrive
|
||||
function cfxGroundTroops.updateAttackers(troop)
|
||||
if not troop then return end
|
||||
if not troop.destination then return end
|
||||
if not troop.group:isExist() then return end
|
||||
|
||||
-- if we are not moving, we need to issue move oders now
|
||||
-- this can happen if previously, there was a 'wait' command
|
||||
-- and this now was removed so we end up in the method
|
||||
if not troop.moving then
|
||||
cfxGroundTroops.makeTroopsEngageZone(troop)
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
if cfxZones.isGroupPartiallyInZone(troop.group, troop.destination) then
|
||||
-- we have arrived
|
||||
-- we could now also initiate a general callback with reason
|
||||
@ -613,21 +625,18 @@ function cfxGroundTroops.update()
|
||||
cfxGroundTroops.updateSchedule = timer.scheduleFunction(cfxGroundTroops.update, {}, timer.getTime() + 1/cfxGroundTroops.ups)
|
||||
-- iterate all my troops and build next
|
||||
-- versions pool
|
||||
local liveTroops = {}
|
||||
local liveTroops = {} -- filtered table, indexed by name
|
||||
for idx, troop in pairs(cfxGroundTroops.deployedTroops) do
|
||||
local group = troop.group
|
||||
if not dcsCommon.isGroupAlive(group) then
|
||||
-- group dead. remove from pool
|
||||
-- this happens by not copying it into the poos
|
||||
-- trigger.action.outText("+++ removing ground troops " .. troop.name, 30)
|
||||
cfxGroundTroops.invokeCallbacksFor("dead", troop) -- notify anyone who is interested that we are no longer proccing these
|
||||
else
|
||||
-- work with this groop according to its orders
|
||||
cfxGroundTroops.updateTroops(troop)
|
||||
-- trigger.action.outText("+++ updated troops " .. troop.name, 30)
|
||||
-- since group is alive remember it for next loop
|
||||
--table.insert(liveTroops, troop)
|
||||
liveTroops[idx] = troop -- do NOT use insert as we have indexed table
|
||||
liveTroops[idx] = troop -- do NOT use insert as we have indexed table by name
|
||||
end
|
||||
end
|
||||
-- liveTroops holds all troops that are still alive and will
|
||||
@ -958,6 +967,7 @@ function cfxGroundTroops.createGroundTroops(inGroup, range, orders)
|
||||
newTroops.coalition = inGroup:getCoalition()
|
||||
newTroops.side = newTroops.coalition -- because we'e been using both.
|
||||
newTroops.name = inGroup:getName()
|
||||
newTroops.moving = false -- set to not have received move orders yet
|
||||
newTroops.signature = "cfx" -- to verify this is groundTroop group, not dcs groups
|
||||
if not range then range = 300 end
|
||||
newTroops.range = range
|
||||
@ -985,7 +995,6 @@ function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table
|
||||
if cfxGroundTroops.maxManagedTroops > 0 and dcsCommon.getSizeOfTable(cfxGroundTroops.deployedTroops) >= cfxGroundTroops.maxManagedTroops then
|
||||
-- we need to queue
|
||||
table.insert(cfxGroundTroops.troopQueue, troops)
|
||||
-- trigger.action.outText("enqued " .. troops.group:getName() .. " at pos ".. #cfxGroundTroops.troopQueue ..", manage cap surpassed.", 30)
|
||||
else
|
||||
-- add to deployed set
|
||||
cfxGroundTroops.deployedTroops[troops.group:getName()] = troops
|
||||
|
||||
@ -1,37 +1,43 @@
|
||||
cfxHeloTroops = {}
|
||||
cfxHeloTroops.version = "2.3.0"
|
||||
cfxHeloTroops.version = "2.4.0"
|
||||
cfxHeloTroops.verbose = false
|
||||
cfxHeloTroops.autoDrop = true
|
||||
cfxHeloTroops.autoPickup = false
|
||||
cfxHeloTroops.pickupRange = 100 -- meters
|
||||
--
|
||||
--[[--
|
||||
VERSION HISTORY
|
||||
1.1.3 - repaired forgetting 'wait-' when loading/disembarking
|
||||
1.1.4 - corrected coalition bug in deployTroopsFromHelicopter
|
||||
2.0.0 - added weight change when troops enter and leave the helicopter
|
||||
- idividual troop capa max per helicopter
|
||||
2.0.1 - lib loader verification
|
||||
- uses dcsCommon.isTroopCarrier(theUnit)
|
||||
2.0.2 - can now deploy from spawners with "requestable" attribute
|
||||
2.1.0 - supports config zones
|
||||
- check spawner legality by types
|
||||
- updated types to include 2.7.6 additions to infantry
|
||||
- updated types to include stinger/manpads
|
||||
2.2.0 - minor maintenance (dcsCommon)
|
||||
- (re?) connected readConfigZone (wtf?)
|
||||
- persistence support
|
||||
- made legalTroops entrirely optional and defer to dcsComon else
|
||||
2.3.0 - interface with owned zones and playerScore when
|
||||
- combat-dropping troops into non-owned owned zone.
|
||||
- prevent auto-load from pre-empting loading csar troops
|
||||
2.3.1 - added ability to self-define troopCarriers via config
|
||||
2.4.0 - added missing support for attackZone orders (destination)
|
||||
- eliminated cfxPlayer module import and all dependencies
|
||||
- added support for groupTracker / limbo
|
||||
- removed restriction to only apply to helicopters in anticipation of the C-130 Hercules appearing in the game
|
||||
|
||||
--]]--
|
||||
--
|
||||
-- VERSION HISTORY
|
||||
-- 1.1.3 -- repaired forgetting 'wait-' when loading/disembarking
|
||||
-- 1.1.4 -- corrected coalition bug in deployTroopsFromHelicopter
|
||||
-- 2.0.0 -- added weight change when troops enter and leave the helicopter
|
||||
-- idividual troop capa max per helicopter
|
||||
-- 2.0.1 -- lib loader verification
|
||||
-- -- uses dcsCommon.isTroopCarrier(theUnit)
|
||||
-- 2.0.2 -- can now deploy from spawners with "requestable" attribute
|
||||
-- 2.1.0 -- supports config zones
|
||||
-- -- check spawner legality by types
|
||||
-- -- updated types to include 2.7.6 additions to infantry
|
||||
-- -- updated types to include stinger/manpads
|
||||
-- 2.2.0 -- minor maintenance (dcsCommon)
|
||||
-- -- (re?) connected readConfigZone (wtf?)
|
||||
-- -- persistence support
|
||||
-- -- made legalTroops entrirely optional and defer to dcsComon else
|
||||
-- 2.3.0 -- interface with owned zones and playerScore when
|
||||
-- -- combat-dropping troops into non-owned owned zone.
|
||||
-- -- prevent auto-load from pre-empting loading csar troops
|
||||
--
|
||||
-- cfxHeloTroops -- a module to pick up and drop infantry. Can be used with any helo,
|
||||
-- might be used to configure to only certain
|
||||
-- currently only supports a single helicopter per group
|
||||
-- only helicopters that can transport troops will have this feature
|
||||
-- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG
|
||||
--
|
||||
-- cfxHeloTroops -- a module to pick up and drop infantry.
|
||||
-- Can be used with ANY aircraft, configured by default to be
|
||||
-- restricted to troop-carrying helicopters.
|
||||
-- might be configure to apply to any type you want using the
|
||||
-- configuration zone.
|
||||
|
||||
|
||||
cfxHeloTroops.requiredLibs = {
|
||||
@ -39,17 +45,11 @@ cfxHeloTroops.requiredLibs = {
|
||||
-- pretty stupid to check for this since we
|
||||
-- need common to invoke the check, but anyway
|
||||
"cfxZones", -- Zones, of course
|
||||
"cfxPlayer", -- player events
|
||||
"cfxCommander", -- to make troops do stuff
|
||||
"cfxGroundTroops", -- generic when dropping troops
|
||||
}
|
||||
|
||||
cfxHeloTroops.unitConfigs = {} -- all configs are stored by unit's name
|
||||
cfxHeloTroops.myEvents = {3, 4, 5} -- 3- takeoff, 4 - land, 5 - crash
|
||||
|
||||
-- legalTroops now optional, else check against dcsCommon.typeIsInfantry
|
||||
--cfxHeloTroops.legalTroops = {"Soldier AK", "Infantry AK", "Infantry AK ver2", "Infantry AK ver3", "Infantry AK Ins", "Soldier M249", "Soldier M4 GRG", "Soldier M4", "Soldier RPG", "Paratrooper AKS-74", "Paratrooper RPG-16", "Stinger comm dsr", "Stinger comm", "Soldier stinger", "SA-18 Igla-S comm", "SA-18 Igla-S manpad", "Igla manpad INS", "SA-18 Igla comm", "SA-18 Igla manpad",}
|
||||
|
||||
cfxHeloTroops.troopWeight = 100 -- kg average weight per trooper
|
||||
|
||||
-- persistence support
|
||||
@ -59,13 +59,12 @@ function cfxHeloTroops.resetConfig(conf)
|
||||
conf.autoDrop = cfxHeloTroops.autoDrop --if true, will drop troops on-board upon touchdown
|
||||
conf.autoPickup = cfxHeloTroops.autoPickup -- if true will load nearest troops upon touchdown
|
||||
conf.pickupRange = cfxHeloTroops.pickupRange --meters, maybe make per helo?
|
||||
-- maybe set up max seats by type
|
||||
conf.currentState = -1 -- 0 = landed, 1 = airborne, -1 undetermined
|
||||
conf.troopsOnBoardNum = 0 -- if not 0, we have troops and can spawnm/drop
|
||||
conf.troopCapacity = 8 -- should be depending on airframe
|
||||
-- troopsOnBoard.name contains name of group
|
||||
-- the other fields info for troops picked up
|
||||
conf.troopsOnBoard = {} -- table with the following
|
||||
|
||||
|
||||
conf.troopsOnBoard.name = "***reset***"
|
||||
conf.dropFormation = "circle_out" -- may be chosen later?
|
||||
end
|
||||
@ -80,7 +79,6 @@ function cfxHeloTroops.createDefaultConfig(theUnit)
|
||||
end
|
||||
|
||||
|
||||
|
||||
function cfxHeloTroops.getUnitConfig(theUnit) -- will create new config if not existing
|
||||
if not theUnit then
|
||||
trigger.action.outText("+++WARNING: nil unit in get config!", 30)
|
||||
@ -98,70 +96,6 @@ function cfxHeloTroops.getConfigForUnitNamed(aName)
|
||||
return cfxHeloTroops.unitConfigs[aName]
|
||||
end
|
||||
|
||||
function cfxHeloTroops.removeConfigForUnitNamed(aName)
|
||||
if cfxHeloTroops.unitConfigs[aName] then cfxHeloTroops.unitConfigs[aName] = nil end
|
||||
end
|
||||
|
||||
function cfxHeloTroops.setState(theUnit, isLanded)
|
||||
-- called to set the current state of the helicopter (group)
|
||||
-- currently one helicopter per group max
|
||||
end
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- E V E N T H A N D L I N G
|
||||
--
|
||||
function cfxHeloTroops.isInteresting(eventID)
|
||||
-- return true if we are interested in this event, false else
|
||||
for key, evType in pairs(cfxHeloTroops.myEvents) do
|
||||
if evType == eventID then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function cfxHeloTroops.preProcessor(event)
|
||||
-- make sure it has an initiator
|
||||
if not event.initiator then return false end -- no initiator
|
||||
local theUnit = event.initiator
|
||||
if not dcsCommon.isPlayerUnit(theUnit) then return false end -- not a player unit
|
||||
local cat = theUnit:getCategory()
|
||||
if cat ~= Group.Category.HELICOPTER then return false end
|
||||
|
||||
return cfxHeloTroops.isInteresting(event.id)
|
||||
end
|
||||
|
||||
function cfxHeloTroops.postProcessor(event)
|
||||
-- don't do anything
|
||||
end
|
||||
|
||||
function cfxHeloTroops.somethingHappened(event)
|
||||
-- when this is invoked, the preprocessor guarantees that
|
||||
-- it's an interesting event
|
||||
-- unit is valid and player
|
||||
-- airframe category is helicopter
|
||||
|
||||
local theUnit = event.initiator
|
||||
local ID = event.id
|
||||
|
||||
|
||||
local myType = theUnit:getTypeName()
|
||||
|
||||
if ID == 4 then
|
||||
cfxHeloTroops.heloLanded(theUnit)
|
||||
end
|
||||
|
||||
if ID == 3 then
|
||||
cfxHeloTroops.heloDeparted(theUnit)
|
||||
end
|
||||
|
||||
if ID == 5 then
|
||||
cfxHeloTroops.heloCrashed(theUnit)
|
||||
end
|
||||
|
||||
cfxHeloTroops.setCommsMenu(theUnit)
|
||||
end
|
||||
|
||||
--
|
||||
--
|
||||
-- LANDED
|
||||
@ -172,9 +106,8 @@ function cfxHeloTroops.loadClosestGroup(conf)
|
||||
local cat = Group.Category.GROUND
|
||||
local unitsToLoad = dcsCommon.getLivingGroupsAndDistInRangeToPoint(p, conf.pickupRange, conf.unit:getCoalition(), cat)
|
||||
|
||||
-- now, the groups may contain units that are not for transport.
|
||||
-- later we can filter this by weight, or other cool stuff
|
||||
-- for now we simply only troopy with legal type strings
|
||||
-- groups may contain units that are not for transport.
|
||||
-- for now we only load troops with legal type strings
|
||||
unitsToLoad = cfxHeloTroops.filterTroopsByType(unitsToLoad)
|
||||
|
||||
-- now limit the options to the five closest legal groups
|
||||
@ -190,7 +123,7 @@ end
|
||||
|
||||
function cfxHeloTroops.heloLanded(theUnit)
|
||||
-- when we have landed,
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end
|
||||
|
||||
local conf = cfxHeloTroops.getUnitConfig(theUnit)
|
||||
conf.unit = theUnit
|
||||
@ -231,7 +164,7 @@ end
|
||||
--
|
||||
|
||||
function cfxHeloTroops.heloDeparted(theUnit)
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end
|
||||
|
||||
-- when we take off, all that needs to be done is to change the state
|
||||
-- to airborne, and then set the status flag
|
||||
@ -248,17 +181,40 @@ end
|
||||
-- Helo Crashed
|
||||
--
|
||||
--
|
||||
function cfxHeloTroops.heloCrashed(theUnit)
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
|
||||
function cfxHeloTroops.cleanHelo(theUnit)
|
||||
-- clean up
|
||||
local conf = cfxHeloTroops.getUnitConfig(theUnit)
|
||||
conf.unit = theUnit
|
||||
conf.troopsOnBoardNum = 0 -- all dead
|
||||
conf.currentState = -1 -- (we don't know)
|
||||
-- conf.troopsOnBoardTypes = "" -- no troops, remember?
|
||||
|
||||
-- check if we need to interface with groupTracker
|
||||
if conf.troopsOnBoard.name and groupTracker then
|
||||
local theName = conf.troopsOnBoard.name
|
||||
-- there was (possibly) a group on board. see if it was tracked
|
||||
local isTracking, numTracking, trackers = groupTracker.groupNameTrackedBy(theName)
|
||||
|
||||
-- if so, remove it from limbo
|
||||
if isTracking then
|
||||
for idx, theTracker in pairs(trackers) do
|
||||
groupTracker.removeGroupNamedFromTracker(theName, theTracker)
|
||||
if cfxHeloTroops.verbose then
|
||||
trigger.action.outText("+++Helo: removed group <" .. theName .. "> from tracker <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
conf.troopsOnBoard = {}
|
||||
cfxHeloTroops.removeComms(conf.unit)
|
||||
end
|
||||
|
||||
function cfxHeloTroops.heloCrashed(theUnit)
|
||||
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return
|
||||
end
|
||||
|
||||
-- clean up
|
||||
cfxHeloTroops.cleanHelo(theUnit)
|
||||
|
||||
end
|
||||
|
||||
--
|
||||
@ -332,15 +288,19 @@ function cfxHeloTroops.setCommsMenu(theUnit)
|
||||
if not theUnit:isExist() then return end
|
||||
|
||||
-- we only add this menu to troop carriers
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then
|
||||
if cfxHeloTroops.verbose then
|
||||
trigger.action.outText("+++heloT - player unit <" .. theUnit:getName() .. "> type <" .. theUnit:getTypeName() .. "> is not legal troop carrier.", 30)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local group = theUnit:getGroup()
|
||||
local id = group:getID()
|
||||
local conf = cfxHeloTroops.getUnitConfig(theUnit) --cfxHeloTroops.unitConfigs[theUnit:getName()]
|
||||
local conf = cfxHeloTroops.getUnitConfig(theUnit)
|
||||
conf.id = id; -- we do this ALWAYS to it is current even after a crash
|
||||
conf.unit = theUnit -- link back
|
||||
|
||||
--local conf = cfxHeloTroops.getUnitConfig(theUnit)
|
||||
-- ok, first, if we don't have an F-10 menu, create one
|
||||
if not (conf.myMainMenu) then
|
||||
conf.myMainMenu = missionCommands.addSubMenuForGroup(id, 'Airlift Troops')
|
||||
@ -548,6 +508,7 @@ function cfxHeloTroops.filterTroopsByType(unitsToLoad)
|
||||
end
|
||||
return filteredGroups
|
||||
end
|
||||
|
||||
--
|
||||
-- T O G G L E S
|
||||
--
|
||||
@ -580,7 +541,6 @@ function cfxHeloTroops.doToggleConfig(args)
|
||||
end
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Deploying Troops
|
||||
--
|
||||
@ -629,7 +589,6 @@ function cfxHeloTroops.doDeployTroops(args)
|
||||
-- set own troops to 0 and erase type string
|
||||
conf.troopsOnBoardNum = 0
|
||||
conf.troopsOnBoard = {}
|
||||
-- conf.troopsOnBoardTypes = ""
|
||||
conf.troopsOnBoard.name = "***wasdeployed***"
|
||||
|
||||
-- reset menu
|
||||
@ -643,11 +602,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
|
||||
local unitTypes = {} -- build type names
|
||||
local theUnit = conf.unit
|
||||
local p = theUnit:getPoint()
|
||||
|
||||
--for i=1, scenario.troopSize[theUnit:getName()] do
|
||||
-- table.insert(unitTypes, "Soldier M4")
|
||||
--end
|
||||
|
||||
|
||||
-- split the conf.troopsOnBoardTypes into an array of types
|
||||
unitTypes = dcsCommon.splitString(conf.troopsOnBoard.types, ",")
|
||||
if #unitTypes < 1 then
|
||||
@ -656,6 +611,9 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
|
||||
|
||||
local range = conf.troopsOnBoard.range
|
||||
local orders = conf.troopsOnBoard.orders
|
||||
local dest = conf.troopsOnBoard.destination
|
||||
local theName = conf.troopsOnBoard.name
|
||||
|
||||
if not orders then orders = "guard" end
|
||||
|
||||
-- order processing: if the orders were pre-pended with "wait-"
|
||||
@ -671,7 +629,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
|
||||
local theCoalition = theUnit:getGroup():getCoalition() -- make it choppers COALITION
|
||||
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
|
||||
theCoalition,
|
||||
conf.troopsOnBoard.name, -- dcsCommon.uuid("Assault"), -- maybe use config name as loaded from the group
|
||||
theName, -- group name, may be tracked
|
||||
chopperZone,
|
||||
unitTypes,
|
||||
conf.dropFormation,
|
||||
@ -682,14 +640,27 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
|
||||
troopData.orders = orders -- always set
|
||||
troopData.side = theCoalition
|
||||
troopData.range = range
|
||||
troopData.destination = dest -- only for attackzone orders
|
||||
cfxHeloTroops.deployedTroops[theData.name] = troopData
|
||||
|
||||
local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders) -- use default range and orders
|
||||
-- instead of scheduling tasking in one second, we add to
|
||||
-- ground troops pool, and the troop pool manager will assign some enemies
|
||||
cfxGroundTroops.addGroundTroopsToPool(troop)
|
||||
local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders)
|
||||
troop.destination = dest -- transfer target zone for attackzone oders
|
||||
cfxGroundTroops.addGroundTroopsToPool(troop) -- will schedule move orders
|
||||
trigger.action.outTextForGroup(conf.id, "<" .. theGroup:getName() .. "> have deployed to the ground with orders " .. orders .. "!", 30)
|
||||
|
||||
-- see if this is tracked by a tracker, and pass them back so
|
||||
-- they can un-limbo
|
||||
if groupTracker then
|
||||
local isTracking, numTracking, trackers = groupTracker.groupNameTrackedBy(theName)
|
||||
if isTracking then
|
||||
for idx, theTracker in pairs (trackers) do
|
||||
groupTracker.addGroupToTracker(theGroup, theTracker)
|
||||
if cfxHeloTroops.verbose then
|
||||
trigger.action.outText("+++Helo: un-limbo and tracking group <" .. theName .. "> with tracker <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -709,21 +680,38 @@ function cfxHeloTroops.doLoadGroup(args)
|
||||
-- get the size
|
||||
conf.troopsOnBoardNum = group:getSize()
|
||||
-- and name
|
||||
conf.troopsOnBoard.name = group:getName()
|
||||
local gName = group:getName()
|
||||
conf.troopsOnBoard.name = gName
|
||||
-- and put it all into the helicopter config
|
||||
|
||||
-- now we need to destroy the group. First, remove it from the pool
|
||||
-- now we need to destroy the group. Let's prepare:
|
||||
-- if it was tracked, tell tracker to move it to limbo
|
||||
-- to remember it even if it's destroyed
|
||||
if groupTracker then
|
||||
-- only if groupTracker is active
|
||||
local isTracking, numTracking, trackers = groupTracker.groupTrackedBy(group)
|
||||
if isTracking then
|
||||
-- we need to put them in limbo for every tracker
|
||||
for idx, aTracker in pairs(trackers) do
|
||||
if cfxHeloTroops.verbose then
|
||||
trigger.action.outText("+++Helo: moving group <" .. gName .. "> to limbo for tracker <" .. aTracker.name .. ">", 30)
|
||||
end
|
||||
groupTracker.moveGroupToLimboForTracker(group, aTracker)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- then, remove it from the pool
|
||||
local pooledGroup = cfxGroundTroops.getGroundTroopsForGroup(group)
|
||||
if pooledGroup then
|
||||
-- copy some important info from the troops
|
||||
-- if they are set
|
||||
conf.troopsOnBoard.orders = pooledGroup.orders
|
||||
conf.troopsOnBoard.range = pooledGroup.range
|
||||
|
||||
conf.troopsOnBoard.destination = pooledGroup.destination -- may be nil
|
||||
cfxGroundTroops.removeTroopsFromPool(pooledGroup)
|
||||
trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' loaded and has orders <" .. conf.troopsOnBoard.orders .. ">", 30)
|
||||
else
|
||||
--trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' loaded!", 30)
|
||||
if cfxHeloTroops.verbose then
|
||||
trigger.action.outText("+++heloT: ".. conf.troopsOnBoard.name .." was not committed to ground troops", 30)
|
||||
end
|
||||
@ -731,9 +719,8 @@ function cfxHeloTroops.doLoadGroup(args)
|
||||
|
||||
-- now simply destroy the group
|
||||
-- we'll re-assemble it when we deploy it
|
||||
-- we currently can't change the weight of the helicopter
|
||||
-- TODO: add weight changing code
|
||||
-- TODO: ensure compatibility with CSAR module
|
||||
-- TODO: ensure compatibility with CSAR module
|
||||
group:destroy()
|
||||
|
||||
-- now immediately run a GC so this group is removed
|
||||
@ -783,47 +770,55 @@ function cfxHeloTroops.doSpawnGroup(args)
|
||||
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Player event callbacks
|
||||
--
|
||||
function cfxHeloTroops.playerChangeEvent(evType, description, player, data)
|
||||
if evType == "newGroup" then
|
||||
theUnit = data.primeUnit
|
||||
cfxHeloTroops.setCommsMenu(theUnit)
|
||||
|
||||
--
|
||||
-- handle events
|
||||
--
|
||||
function cfxHeloTroops:onEvent(theEvent)
|
||||
local theID = theEvent.id
|
||||
local initiator = theEvent.initiator
|
||||
if not initiator then return end -- not interested
|
||||
local theUnit = initiator
|
||||
local name = theUnit:getName()
|
||||
-- see if this is a player aircraft
|
||||
if not theUnit.getPlayerName then return end -- not a player
|
||||
if not theUnit:getPlayerName() then return end -- not a player
|
||||
|
||||
-- only for helicopters -- overridedden by troop carriers
|
||||
-- we don't check for cat any more, so any airframe
|
||||
-- can be used as long as it's ok with isTroopCarrier()
|
||||
|
||||
-- only for troop carriers
|
||||
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then
|
||||
return
|
||||
end
|
||||
|
||||
if evType == "removeGroup" then
|
||||
-- trigger.action.outText("+++Helo Troops: a group disappeared", 30)
|
||||
-- data.name contains the name of the group. nil the entry in config list, so all
|
||||
-- troops that group was carrying are gone
|
||||
-- we must remove the comms menu for this group else we try to add another one to this group later
|
||||
-- we assume a one-unit group structure, else the following may fail
|
||||
local conf = cfxHeloTroops.getConfigForUnitNamed(data.primeUnitName)
|
||||
if theID == 4 then -- land
|
||||
cfxHeloTroops.heloLanded(theUnit)
|
||||
end
|
||||
|
||||
if theID == 3 then -- take off
|
||||
cfxHeloTroops.heloDeparted(theUnit)
|
||||
end
|
||||
|
||||
if theID == 5 then -- crash
|
||||
cfxHeloTroops.heloCrashed(theUnit)
|
||||
end
|
||||
|
||||
if theID == 20 or -- player enter
|
||||
theID == 15 then -- birth
|
||||
cfxHeloTroops.cleanHelo(theUnit)
|
||||
end
|
||||
|
||||
if theID == 21 then -- player leave
|
||||
cfxHeloTroops.cleanHelo(theUnit)
|
||||
local conf = cfxHeloTroops.getConfigForUnitNamed(name)
|
||||
if conf then
|
||||
cfxHeloTroops.removeCommsFromConfig(conf)
|
||||
end
|
||||
return
|
||||
return
|
||||
end
|
||||
|
||||
if evType == "leave" then
|
||||
local conf = cfxHeloTroops.getConfigForUnitNamed(player.unitName)
|
||||
if conf then
|
||||
cfxHeloTroops.resetConfig(conf)
|
||||
end
|
||||
end
|
||||
|
||||
if evType == "unit" then
|
||||
-- player changed units. almost never in MP, but possible in solo
|
||||
-- we need to reset the conf so no troops are carried any longer
|
||||
local conf = cfxHeloTroops.getConfigForUnitNamed(data.oldUnitName)
|
||||
if conf then
|
||||
cfxHeloTroops.resetConfig(conf)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
cfxHeloTroops.setCommsMenu(theUnit)
|
||||
end
|
||||
|
||||
--
|
||||
@ -883,6 +878,13 @@ function cfxHeloTroops.readConfigZone()
|
||||
cfxHeloTroops.autoPickup = cfxZones.getBoolFromZoneProperty(theZone, "autoPickup", false)
|
||||
cfxHeloTroops.pickupRange = cfxZones.getNumberFromZoneProperty(theZone, "pickupRange", 100)
|
||||
cfxHeloTroops.combatDropScore = cfxZones.getNumberFromZoneProperty(theZone, "combatDropScore", 200)
|
||||
|
||||
-- add own troop carriers
|
||||
if cfxZones.hasProperty(theZone, "troopCarriers") then
|
||||
local tc = cfxZones.getStringFromZoneProperty(theZone, "troopCarriers", "UH-1D")
|
||||
tc = dcsCommon.splitString(tc, ",")
|
||||
cfxHeloTroops.troopCarriers = dcsCommon.trimArray(tc)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
@ -955,23 +957,12 @@ function cfxHeloTroops.start()
|
||||
-- start housekeeping
|
||||
cfxHeloTroops.houseKeeping()
|
||||
|
||||
-- install callbacks for helo-relevant events
|
||||
dcsCommon.addEventHandler(cfxHeloTroops.somethingHappened, cfxHeloTroops.preProcessor, cfxHeloTroops.postProcessor)
|
||||
|
||||
-- now iterate through all player groups and install the Assault Troop Menu
|
||||
allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
|
||||
-- contains per group a player record, use prime unit to access player's unit
|
||||
for gname, pgroup in pairs(allPlayerGroups) do
|
||||
local aUnit = pgroup.primeUnit -- get any unit of that group
|
||||
cfxHeloTroops.setCommsMenu(aUnit)
|
||||
end
|
||||
-- now install the new group notifier to install Assault Troops menu
|
||||
|
||||
cfxPlayer.addMonitor(cfxHeloTroops.playerChangeEvent)
|
||||
world.addEventHandler(cfxHeloTroops)
|
||||
trigger.action.outText("cf/x Helo Troops v" .. cfxHeloTroops.version .. " started", 30)
|
||||
|
||||
-- now load all save data and populate map with troops that
|
||||
-- we deployed last save.
|
||||
-- persistence:
|
||||
-- load all save data and populate map with troops that
|
||||
-- we deployed when we last saved.
|
||||
if persistence then
|
||||
-- sign up for persistence
|
||||
callbacks = {}
|
||||
@ -990,11 +981,5 @@ if not cfxHeloTroops.start() then
|
||||
cfxHeloTroops = nil
|
||||
end
|
||||
|
||||
--[[--
|
||||
- interface with spawnable: request troops via comms menu if
|
||||
- spawnZones defined
|
||||
- spawners in range and
|
||||
- spawner auf 'paused' und 'requestable'
|
||||
|
||||
--]]--
|
||||
|
||||
-- TODO: weight when loading troops
|
||||
@ -1,5 +1,5 @@
|
||||
cfxOwnedZones = {}
|
||||
cfxOwnedZones.version = "1.2.3"
|
||||
cfxOwnedZones.version = "1.2.4"
|
||||
cfxOwnedZones.verbose = false
|
||||
cfxOwnedZones.announcer = true
|
||||
cfxOwnedZones.name = "cfxOwnedZones"
|
||||
@ -48,6 +48,7 @@ cfxOwnedZones.name = "cfxOwnedZones"
|
||||
1.2.1 - fix in load to correctly re-establish all attackers for subsequent save
|
||||
1.2.2 - redCap! and blueCap!
|
||||
1.2.3 - fix for persistence bug when not using conquered flag
|
||||
1.2.4 - pause? and activate? inputs
|
||||
|
||||
|
||||
--]]--
|
||||
@ -235,6 +236,23 @@ function cfxOwnedZones.addOwnedZone(aZone)
|
||||
aZone.blueCap = cfxZones.getStringFromZoneProperty(aZone, "blueCap!", "none")
|
||||
end
|
||||
|
||||
-- pause? and activate?
|
||||
if cfxZones.hasProperty(aZone, "pause?") then
|
||||
aZone.pauseFlag = cfxZones.getStringFromZoneProperty(aZone, "pause?", "none")
|
||||
aZone.lastPauseValue = trigger.misc.getUserFlag(aZone.pauseFlag)
|
||||
end
|
||||
|
||||
if cfxZones.hasProperty(aZone, "activate?") then
|
||||
aZone.activateFlag = cfxZones.getStringFromZoneProperty(aZone, "activate?", "none")
|
||||
aZone.lastActivateValue = trigger.misc.getUserFlag(aZone.activateFlag)
|
||||
end
|
||||
|
||||
aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change")
|
||||
if cfxZones.hasProperty(aZone, "ownedTriggerMethod") then
|
||||
aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "ownedTriggerMethod", "change")
|
||||
end
|
||||
|
||||
|
||||
aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false)
|
||||
aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false)
|
||||
aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false)
|
||||
@ -855,6 +873,16 @@ function cfxOwnedZones.update()
|
||||
cfxOwnedZones.zoneConquered(aZone, 1, currentOwner)
|
||||
end
|
||||
end
|
||||
|
||||
-- see if pause/unpause was issued
|
||||
-- note that capping a zone will not change pause status
|
||||
if aZone.pauseFlag and cfxZones.testZoneFlag(aZone, aZone.pauseFlag, aZone.ownedTriggerMethod, "lastPauseValue") then
|
||||
aZone.paused = true
|
||||
end
|
||||
|
||||
if aZone.activateFlag and cfxZones.testZoneFlag(aZone, aZone.activateFlag, aZone.ownedTriggerMethod, "lastActivateValue") then
|
||||
aZone.paused = false
|
||||
end
|
||||
|
||||
-- now, perhaps with their new owner call updateZone()
|
||||
cfxOwnedZones.updateZone(aZone)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
cfxSpawnZones = {}
|
||||
cfxSpawnZones.version = "1.7.2"
|
||||
cfxSpawnZones.version = "1.7.4"
|
||||
cfxSpawnZones.requiredLibs = {
|
||||
"dcsCommon", -- common is of course needed for everything
|
||||
-- pretty stupid to check for this since we
|
||||
@ -20,112 +20,78 @@ cfxSpawnZones.spawnedGroups = {}
|
||||
-- Zones that conform with this requirements spawn toops automatically
|
||||
-- *** DOES NOT EXTEND ZONES *** LINKED OWNER via masterOwner ***
|
||||
--
|
||||
|
||||
--[[--
|
||||
-- version history
|
||||
-- 1.3.0
|
||||
-- - maxSpawn
|
||||
-- - orders
|
||||
-- - range
|
||||
-- 1.3.1 - spawnWithSpawner correct translation of country to coalition
|
||||
-- - createSpawner - corrected reading from properties
|
||||
-- 1.3.2 - createSpawner - correct reading 'owner' from properties, now
|
||||
-- directly reads coalition
|
||||
-- 1.4.0 - checks modules
|
||||
-- - orders 'train' or 'training' - will make the
|
||||
-- ground troops be issued HOLD WEAPS and
|
||||
-- not added to any queue. 'Training' troops
|
||||
-- are target dummies.
|
||||
-- - optional heading attribute
|
||||
-- - typeMult: repeate type this many time (can produce army in one call)
|
||||
-- 1.4.1 - 'requestable' attribute. will automatically set zone to
|
||||
-- - paused, so troops can be produced on call
|
||||
-- - getRequestableSpawnersInRange
|
||||
-- 1.4.2 - target attribute. used for
|
||||
-- - orders: attackZone
|
||||
-- - spawner internally copies name from cfxZone used for spawning (convenience only)
|
||||
-- 1.4.3 - can subscribe to callbacks. currently called when spawnForSpawner is invoked, reason is "spawned"
|
||||
-- - masterOwner to link ownership to other zone
|
||||
-- 1.4.4 - autoRemove flag to instantly start CD and respawn
|
||||
-- 1.4.5 - verify that maxSpawns ~= 0 on initial spawn on start-up
|
||||
-- 1.4.6 - getSpawnerForZoneNamed(aName)
|
||||
-- - nil-trapping orders before testing for 'training'
|
||||
-- 1.4.7 - defaulting orders to 'guard'
|
||||
-- - also accept 'dummy' and 'dummies' as substitute for training
|
||||
-- 1.4.8 - spawnWithSpawner uses getPoint to support linked spawn zones
|
||||
-- - update spawn count on initial spawn
|
||||
-- 1.5.0 - f? support to trigger spawn
|
||||
-- - spawnWithSpawner made string compatible
|
||||
-- 1.5.1 - relaxed baseName and default to dcsCommon.uuid()
|
||||
-- - verbose
|
||||
-- 1.5.2 - activate?, pause? flag
|
||||
-- 1.5.3 - spawn?, spawnUnits? flags
|
||||
-- 1.6.0 - trackwith interface for group tracker
|
||||
-- 1.7.0 - persistence support
|
||||
-- 1.7.1 - improved verbosity
|
||||
-- - spelling check
|
||||
-- 1.7.2 - baseName now can can be set to zone name by issuing "*"
|
||||
-- 1.7.3 - ability to hand off to delicates, useDelicates attribute
|
||||
--
|
||||
-- new version requires cfxGroundTroops, where they are
|
||||
--
|
||||
-- How do we recognize a spawn zone?
|
||||
-- contains a "spawner" attribute
|
||||
-- a spawner must also have the following attributes
|
||||
-- - spawner - anything, must be present to signal. put in 'ground' to be able to expand to other types
|
||||
-- - types - type strings, comma separated
|
||||
-- see here: https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB
|
||||
-- - typeMult - repeat types n times to create really LOT of troops. optional, defaults to 1
|
||||
-- - country - defaults to 2 (usa) -- see here https://wiki.hoggitworld.com/view/DCS_enum_country
|
||||
-- some important: 0 = Russia, 2 = US, 82 = UN neutral
|
||||
-- country is converted to coalition and then assigned to
|
||||
-- Joint Task Force <side> upon spawn
|
||||
-- - masterOwner - optional name of master cfxZone used to determine whom the surrounding
|
||||
-- territory belongs to. Spwaner will only spawn if the owner coalition is the
|
||||
-- the same as the coalition my own county belongs to.
|
||||
-- if not given, spawner spawns even if inside a zone owned by opposing force
|
||||
-- - baseName - for naming spawned groups - MUST BE UNIQUE!!!!
|
||||
--
|
||||
-- the following attributes are optional
|
||||
-- - cooldown, defaults to 60 (seconds) after troops are removed from zone,
|
||||
-- then the next group spawns. This means troops will only spawn after
|
||||
-- troops are removed and cooldown timed out
|
||||
-- - autoRemove - instantly removes spwaned troops, will spawn again
|
||||
-- again after colldown
|
||||
-- - formation - default is circle_out; other formations are
|
||||
-- - line - left lo right (west-east) facing north
|
||||
-- - line_V - vertical line, facing north
|
||||
-- - chevron - west-east, point growing to north
|
||||
-- - scattered, random
|
||||
-- - circle, circle_forward (all fact north)
|
||||
-- - circle-in (all face in)
|
||||
-- - circle-out (all face out)
|
||||
-- - grid, square, rect arrayed in optimal grid
|
||||
-- - 2deep, 2cols two columns, deep
|
||||
-- - 2wide 2 columns wide (2 deep)
|
||||
-- - heading in DEGREES (deafult 0 = north ) direction entire group is facing
|
||||
-- - destination - zone name to go to, no destination = stay where you are
|
||||
-- - paused - defaults to false. If present true, spawning will not happen
|
||||
-- you can then manually invoke cfxSpawnZones.spawnWithSpawner(spawner) to
|
||||
-- spawn the troops as they are described in the spawner
|
||||
-- - orders - tell them what to do. "train" makes them dummies, "guard"
|
||||
-- "laze", "wait-laze" etc
|
||||
-- other orders are as defined by cfxGroundTroops, at least
|
||||
-- guard - hold and defend (default)
|
||||
-- laze - laze targets
|
||||
-- wait-xxx for helo troops, stand by until dropped from helo
|
||||
-- attackOwnedZone - seek nearest owned zone and attack
|
||||
-- attackZone - move towards the named cfxZone. will generate error if zone not found
|
||||
-- name of zone to attack is in 'target' attribute
|
||||
-- - target - names a target cfxZone, used for orders. Troops will immediately
|
||||
-- start moving towards that zone if defined and such a zone exists
|
||||
-- - maxSpawns - limit number of spawn cycles. omit or -1 is unlimited
|
||||
-- - requestable - used with heloTroops to determine if spawning can be ordered by
|
||||
-- comms when in range
|
||||
-- respawn currently happens after theSpawn is deleted and cooldown seconds have passed
|
||||
1.3.0
|
||||
- maxSpawn
|
||||
- orders
|
||||
- range
|
||||
1.3.1 - spawnWithSpawner correct translation of country to coalition
|
||||
- createSpawner - corrected reading from properties
|
||||
1.3.2 - createSpawner - correct reading 'owner' from properties, now
|
||||
directly reads coalition
|
||||
1.4.0 - checks modules
|
||||
- orders 'train' or 'training' - will make the
|
||||
ground troops be issued HOLD WEAPS and
|
||||
not added to any queue. 'Training' troops
|
||||
are target dummies.
|
||||
- optional heading attribute
|
||||
- typeMult: repeate type this many time (can produce army in one call)
|
||||
1.4.1 - 'requestable' attribute. will automatically set zone to
|
||||
- paused, so troops can be produced on call
|
||||
- getRequestableSpawnersInRange
|
||||
1.4.2 - target attribute. used for
|
||||
- orders: attackZone
|
||||
- spawner internally copies name from cfxZone used for spawning (convenience only)
|
||||
1.4.3 - can subscribe to callbacks. currently called when spawnForSpawner is invoked, reason is "spawned"
|
||||
- masterOwner to link ownership to other zone
|
||||
1.4.4 - autoRemove flag to instantly start CD and respawn
|
||||
1.4.5 - verify that maxSpawns ~= 0 on initial spawn on start-up
|
||||
1.4.6 - getSpawnerForZoneNamed(aName)
|
||||
- nil-trapping orders before testing for 'training'
|
||||
1.4.7 - defaulting orders to 'guard'
|
||||
- also accept 'dummy' and 'dummies' as substitute for training
|
||||
1.4.8 - spawnWithSpawner uses getPoint to support linked spawn zones
|
||||
- update spawn count on initial spawn
|
||||
1.5.0 - f? support to trigger spawn
|
||||
- spawnWithSpawner made string compatible
|
||||
1.5.1 - relaxed baseName and default to dcsCommon.uuid()
|
||||
- verbose
|
||||
1.5.2 - activate?, pause? flag
|
||||
1.5.3 - spawn?, spawnUnits? flags
|
||||
1.6.0 - trackwith interface for group tracker
|
||||
1.7.0 - persistence support
|
||||
1.7.1 - improved verbosity
|
||||
- spelling check
|
||||
1.7.2 - baseName now can can be set to zone name by issuing "*"
|
||||
1.7.3 - ability to hand off to delicates, useDelicates attribute
|
||||
1.7.4 - wait-attackZone fixes
|
||||
|
||||
|
||||
- types - type strings, comma separated
|
||||
see here: https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB
|
||||
|
||||
- country - defaults to 2 (usa) -- see here https://wiki.hoggitworld.com/view/DCS_enum_country
|
||||
some important: 0 = Russia, 2 = US, 82 = UN neutral
|
||||
country is converted to coalition and then assigned to
|
||||
Joint Task Force <side> upon spawn
|
||||
|
||||
- formation - default is circle_out; other formations are
|
||||
- line - left lo right (west-east) facing north
|
||||
- line_V - vertical line, facing north
|
||||
- chevron - west-east, point growing to north
|
||||
- scattered, random
|
||||
- circle, circle_forward (all fact north)
|
||||
- circle-in (all face in)
|
||||
- circle-out (all face out)
|
||||
- grid, square, rect arrayed in optimal grid
|
||||
- 2deep, 2cols two columns, deep
|
||||
- 2wide 2 columns wide (2 deep)
|
||||
--]]--
|
||||
|
||||
cfxSpawnZones.allSpawners = {}
|
||||
cfxSpawnZones.callbacks = {} -- signature: cb(reason, group, spawner)
|
||||
|
||||
|
||||
--
|
||||
-- C A L L B A C K S
|
||||
--
|
||||
@ -225,25 +191,26 @@ function cfxSpawnZones.createSpawner(inZone)
|
||||
theSpawner.formation = "circle_out"
|
||||
theSpawner.formation = cfxZones.getStringFromZoneProperty(inZone, "formation", "circle_out")
|
||||
theSpawner.paused = cfxZones.getBoolFromZoneProperty(inZone, "paused", false)
|
||||
-- orders are always converted to all lower case
|
||||
theSpawner.orders = cfxZones.getStringFromZoneProperty(inZone, "orders", "guard"):lower()
|
||||
--theSpawner.orders = cfxZones.getZoneProperty(inZone, "orders")
|
||||
-- used to assign special orders, default is 'guard', use "laze" to make them laze targets. can be 'wait-' which may auto-convert to 'guard' after pick-up by helo, to be handled outside.
|
||||
-- used to assign orders, default is 'guard', use "laze" to make them laze targets. can be 'wait-' which may auto-convert to 'guard' after pick-up by helo, to be handled outside.
|
||||
-- use "train" to tell them to HOLD WEAPONS, don't move and don't participate in loop, so we have in effect target dummies
|
||||
-- can also use order 'dummy' or 'dummies' to switch to train
|
||||
if theSpawner.orders:lower() == "dummy" or theSpawner.orders:lower() == "dummies" then theSpawner.orders = "train" end
|
||||
if theSpawner.orders:lower() == "training" then theSpawner.orders = "train" end
|
||||
|
||||
|
||||
theSpawner.range = cfxZones.getNumberFromZoneProperty(inZone, "range", 300) -- if we have a range, for example enemy detection for Lasing or engage range
|
||||
theSpawner.maxSpawns = cfxZones.getNumberFromZoneProperty(inZone, "maxSpawns", -1) -- if there is a limit on how many troops can spawn. -1 = endless spawns
|
||||
theSpawner.requestable = cfxZones.getBoolFromZoneProperty(inZone, "requestable", false)
|
||||
if theSpawner.requestable then
|
||||
theSpawner.paused = true
|
||||
end
|
||||
theSpawner.target = cfxZones.getStringFromZoneProperty(inZone, "target", "")
|
||||
if theSpawner.target == "" then -- this is the defaut case
|
||||
theSpawner.target = nil
|
||||
end
|
||||
if cfxZones.hasProperty(inZone, "target") then
|
||||
theSpawner.target = cfxZones.getStringFromZoneProperty(inZone, "target", "")
|
||||
if theSpawner.target == "" then -- this is the defaut case
|
||||
theSpawner.target = nil
|
||||
end
|
||||
end
|
||||
|
||||
if cfxSpawnZones.verbose or inZone.verbose then
|
||||
trigger.action.outText("+++spwn: created spawner for <" .. inZone.name .. ">", 30)
|
||||
@ -331,10 +298,9 @@ function cfxSpawnZones.verifySpawnOwnership(spawner)
|
||||
|
||||
if (myCoalition ~= masterZone.owner) then
|
||||
-- can't spawn, surrounding area owned by enemy
|
||||
--trigger.action.outText("spawner " .. spawner.name .. " - spawn suppressed: area not owned: " .. " master owner is " .. masterZone.owner .. ", we are " .. myCoalition, 30)
|
||||
return false
|
||||
end
|
||||
--trigger.action.outText("spawner " .. spawner.name .. " good to go: ", 30)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
@ -362,7 +328,6 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner)
|
||||
|
||||
local theCountry = aSpawner.country
|
||||
local theCoalition = coalition.getCountryCoalition(theCountry)
|
||||
-- trigger.action.outText("+++ spawn: coal <" .. theCoalition .. "> from country <" .. theCountry .. ">", 30)
|
||||
|
||||
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
|
||||
theCoalition,
|
||||
@ -374,16 +339,17 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner)
|
||||
aSpawner.theSpawn = theGroup
|
||||
aSpawner.count = aSpawner.count + 1
|
||||
|
||||
-- isnert into collector for persistence
|
||||
-- insert into collector for persistence
|
||||
local troopData = {}
|
||||
troopData.groupData = theData
|
||||
troopData.orders = aSpawner.orders -- always set
|
||||
troopData.orders = aSpawner.orders -- always set
|
||||
troopData.side = theCoalition
|
||||
troopData.target = aSpawner.target -- can be nil!
|
||||
troopData.tracker = theZone.trackWith -- taken from ZONE!!, can be nil
|
||||
troopData.range = aSpawner.range
|
||||
cfxSpawnZones.spawnedGroups[theData.name] = troopData
|
||||
|
||||
-- remember: orders are always lower case only
|
||||
if aSpawner.orders and (
|
||||
aSpawner.orders:lower() == "training" or
|
||||
aSpawner.orders:lower() == "train" )
|
||||
@ -403,16 +369,17 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner)
|
||||
cfxGroundTroops.addGroundTroopsToPool(newTroops)
|
||||
|
||||
-- see if we have defined a target zone as destination
|
||||
-- and set it accordingly
|
||||
if aSpawner.target then
|
||||
local destZone = cfxZones.getZoneByName(aSpawner.target)
|
||||
if destZone then
|
||||
newTroops.destination = destZone
|
||||
cfxGroundTroops.makeTroopsEngageZone(newTroops)
|
||||
newTroops.destination = destZone
|
||||
else
|
||||
trigger.action.outText("+++ spawner " .. aSpawner.name .. " has illegal target " .. aSpawner.target .. ". Pausing.", 30)
|
||||
trigger.action.outText("+++ spawner " .. aSpawner.name .. " has illegal (unknown) target zone <" .. aSpawner.target .. ">. Pausing.", 30)
|
||||
aSpawner.paused = true
|
||||
end
|
||||
elseif aSpawner.orders == "attackZone" then
|
||||
elseif aSpawner.orders == "attackzone" then
|
||||
-- attackZone command but no zone given
|
||||
trigger.action.outText("+++ spawner " .. aSpawner.name .. " has no target but attackZone command. Pausing.", 30)
|
||||
aSpawner.paused = true
|
||||
end
|
||||
@ -459,7 +426,7 @@ function cfxSpawnZones.handoffTracking(theGroup, theZone)
|
||||
return
|
||||
end
|
||||
local trackerName = theZone.trackWith
|
||||
--if trackerName == "*" then trackerName = theZone.name end
|
||||
|
||||
-- now assemble a list of all trackers
|
||||
if cfxSpawnZones.verbose or theZone.verbose then
|
||||
trigger.action.outText("+++spawner: spawn pass-off: " .. trackerName, 30)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
cfxZones = {}
|
||||
cfxZones.version = "3.0.2"
|
||||
cfxZones.version = "3.0.3"
|
||||
|
||||
-- cf/x zone management module
|
||||
-- reads dcs zones and makes them accessible and mutable
|
||||
@ -119,6 +119,9 @@ cfxZones.version = "3.0.2"
|
||||
- 3.0.2 - maxRadius for all zones, only differs from radius in polyZones
|
||||
- re-factoring zone-base string processing from messenger module
|
||||
- new processStringWildcards() that does almost all that messenger can
|
||||
- 3.0.3 - new getLinkedUnit()
|
||||
- 3.0.4 - new createRandomPointOnZoneBoundary()
|
||||
- 3.0.5 - getPositiveRangeFromZoneProperty() now also supports upper bound (optional)
|
||||
|
||||
|
||||
--]]--
|
||||
@ -392,6 +395,17 @@ function cfxZones.createRandomPointInsideBounds(bounds)
|
||||
return cfxZones.createPoint(x, 0, z)
|
||||
end
|
||||
|
||||
function cfxZones.createRandomPointOnZoneBoundary(theZone)
|
||||
if not theZone then return nil end
|
||||
if theZone.isPoly then
|
||||
local loc, dx, dy = cfxZones.createRandomPointInPolyZone(theZone, true)
|
||||
return loc, dx, dy
|
||||
else
|
||||
local loc, dx, dy = cfxZones.createRandomPointInCircleZone(theZone, true)
|
||||
return loc, dx, dy
|
||||
end
|
||||
end
|
||||
|
||||
function cfxZones.createRandomPointInZone(theZone)
|
||||
if not theZone then return nil end
|
||||
if theZone.isPoly then
|
||||
@ -408,7 +422,7 @@ function cfxZones.randomPointInZone(theZone)
|
||||
return loc, dx, dy
|
||||
end
|
||||
|
||||
function cfxZones.createRandomPointInCircleZone(theZone)
|
||||
function cfxZones.createRandomPointInCircleZone(theZone, onEdge)
|
||||
if not theZone.isCircle then
|
||||
trigger.action.outText("+++Zones: warning - createRandomPointInCircleZone called for non-circle zone <" .. theZone.name .. ">", 30)
|
||||
return {x=theZone.point.x, y=0, z=theZone.point.z}
|
||||
@ -417,7 +431,10 @@ function cfxZones.createRandomPointInCircleZone(theZone)
|
||||
-- ok, let's first create a random percentage value for the new radius
|
||||
-- now lets get a random degree
|
||||
local degrees = math.random() * 2 * 3.14152 -- radiants.
|
||||
local r = theZone.radius * math.random()
|
||||
local r = theZone.radius
|
||||
if not onEdge then
|
||||
r = r * math.random()
|
||||
end
|
||||
local p = cfxZones.getPoint(theZone) -- force update of zone if linked
|
||||
local dx = r * math.cos(degrees)
|
||||
local dz = r * math.sin(degrees)
|
||||
@ -426,7 +443,7 @@ function cfxZones.createRandomPointInCircleZone(theZone)
|
||||
return {x=px, y=0, z = pz}, dx, dz -- returns loc and offsets to theZone.point
|
||||
end
|
||||
|
||||
function cfxZones.createRandomPointInPolyZone(theZone)
|
||||
function cfxZones.createRandomPointInPolyZone(theZone, onEdge)
|
||||
if not theZone.isPoly then
|
||||
trigger.action.outText("+++Zones: warning - createRandomPointInPolyZone called for non-poly zone <" .. theZone.name .. ">", 30)
|
||||
return cfxZones.createPoint(theZone.point.x, 0, theZone.point.z)
|
||||
@ -446,6 +463,11 @@ function cfxZones.createRandomPointInPolyZone(theZone)
|
||||
local b = theZone.poly[lineIdxA]
|
||||
local randompercent = math.random()
|
||||
local sourceA = dcsCommon.vLerp (a, b, randompercent)
|
||||
-- if all we want is a point on an edge, we are done
|
||||
if onEdge then
|
||||
local polyPoint = sourceA
|
||||
return polyPoint, polyPoint.x - p.x, polyPoint.z - p.z -- return loc, dx, dz
|
||||
end
|
||||
|
||||
-- now get point on second line
|
||||
a = theZone.poly[lineIdxB]
|
||||
@ -1962,13 +1984,15 @@ function cfxZones.randomDelayFromPositiveRange(minVal, maxVal)
|
||||
return delay
|
||||
end
|
||||
|
||||
function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default)
|
||||
function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default, defaultmax)
|
||||
-- reads property as string, and interprets as range 'a-b'.
|
||||
-- if not a range but single number, returns both for upper and lower
|
||||
--trigger.action.outText("***Zne: enter with <" .. theZone.name .. ">: range for property <" .. theProperty .. ">!", 30)
|
||||
if not default then default = 0 end
|
||||
if not defaultmax then defaultmax = default end
|
||||
|
||||
local lowerBound = default
|
||||
local upperBound = default
|
||||
local upperBound = defaultmax
|
||||
|
||||
local rangeString = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
|
||||
if dcsCommon.containsString(rangeString, "-") then
|
||||
@ -1987,12 +2011,12 @@ function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default
|
||||
|
||||
else
|
||||
-- bounds illegal
|
||||
trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. default, 30)
|
||||
trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. defaultmax, 30)
|
||||
lowerBound = default
|
||||
upperBound = default
|
||||
upperBound = defaultmax
|
||||
end
|
||||
else
|
||||
upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) -- between pulses
|
||||
upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, defaultmax) -- between pulses
|
||||
lowerBound = upperBound
|
||||
end
|
||||
|
||||
@ -2455,11 +2479,18 @@ function cfxZones.getDCSOrigin(aZone)
|
||||
return o
|
||||
end
|
||||
|
||||
function cfxZones.getLinkedUnit(theZone)
|
||||
if not theZone then return nil end
|
||||
if not theZone.linkedUnit then return nil end
|
||||
if not Unit.isExist(theZone.linkedUnit) then return nil end
|
||||
return theZone.linkedUnit
|
||||
end
|
||||
|
||||
function cfxZones.getPoint(aZone) -- always works, even linked, returned point can be reused
|
||||
if aZone.linkedUnit then
|
||||
local theUnit = aZone.linkedUnit
|
||||
-- has a link. is link existing?
|
||||
if theUnit:isExist() then
|
||||
if Unit.isExist(theUnit) then
|
||||
-- updates zone position
|
||||
cfxZones.centerZoneOnUnit(aZone, theUnit)
|
||||
local dx = aZone.dx
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
changer = {}
|
||||
changer.version = "1.0.4"
|
||||
changer.version = "1.0.5"
|
||||
changer.verbose = false
|
||||
changer.ups = 1
|
||||
changer.requiredLibs = {
|
||||
@ -14,6 +14,7 @@ changer.changers = {}
|
||||
1.0.2 - on/off: verbosity
|
||||
1.0.3 - NOT on/off
|
||||
1.0.4 - a little bit more conversation
|
||||
1.0.5 - fixed a bug in verbosity
|
||||
|
||||
Transmogrify an incoming signal to an output signal
|
||||
- not
|
||||
@ -241,7 +242,7 @@ function changer.update()
|
||||
end
|
||||
else
|
||||
if aZone.verbose then
|
||||
trigger.action.outText("+++chgr: <" .. aZone.name .. "> is paused.")
|
||||
trigger.action.outText("+++chgr: <" .. aZone.name .. "> is paused.", 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
civAir = {}
|
||||
civAir.version = "1.5.1"
|
||||
civAir.version = "1.5.2"
|
||||
--[[--
|
||||
1.0.0 initial version
|
||||
1.1.0 exclude list for airfields
|
||||
@ -22,6 +22,7 @@ civAir.version = "1.5.1"
|
||||
massive simplifications: always between zoned airfieds
|
||||
exclude list and include list
|
||||
1.5.1 added depart only and arrive only options for airfields
|
||||
1.5.2 fixed bugs inb verbosity
|
||||
|
||||
|
||||
--]]--
|
||||
@ -181,7 +182,7 @@ function civAir.getTwoAirbases()
|
||||
local filteredAB = civAir.filterAirfields(departAB, civAir.excludeAirfields)
|
||||
-- if none left, error
|
||||
if #filteredAB < 1 then
|
||||
trigger.action.outText("+++civA: too few departure airfields")
|
||||
trigger.action.outText("+++civA: too few departure airfields", 30)
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
@ -195,7 +196,7 @@ function civAir.getTwoAirbases()
|
||||
|
||||
-- if one left use it twice, boring flight.
|
||||
if #filteredAB < 1 then
|
||||
trigger.action.outText("+++civA: too few arrival airfields")
|
||||
trigger.action.outText("+++civA: too few arrival airfields", 30)
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
|
||||
@ -93,6 +93,7 @@ cloneZones.respawnOnGroupID = true
|
||||
- masterOwner "*" convenience shortcut
|
||||
1.7.1 - useDelicates handOff for delicates
|
||||
- forcedRespawn passes zone instead of verbose
|
||||
1.7.2 - onPerimeter attribute
|
||||
|
||||
--]]--
|
||||
|
||||
@ -361,6 +362,8 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner"
|
||||
theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false)
|
||||
|
||||
theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false)
|
||||
|
||||
theZone.onPerimeter = cfxZones.getBoolFromZoneProperty(theZone, "onPerimeter", false)
|
||||
|
||||
-- check for name scheme and / or identical
|
||||
if cfxZones.hasProperty(theZone, "identical") then
|
||||
@ -1119,12 +1122,21 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
|
||||
-- calculate the entire group's displacement
|
||||
local units = rawData.units
|
||||
|
||||
local loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones
|
||||
local loc, dx, dy
|
||||
if spawnZone.onPerimeter then
|
||||
loc, dx, dy = cfxZones.createRandomPointOnZoneBoundary(spawnZone)
|
||||
else
|
||||
loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones
|
||||
end
|
||||
|
||||
for idx, aUnit in pairs(units) do
|
||||
if not spawnZone.centerOnly then
|
||||
-- *every unit's displacement is randomized
|
||||
loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone)
|
||||
if spawnZone.onPerimeter then
|
||||
loc, dx, dy = cfxZones.createRandomPointOnZoneBoundary(spawnZone)
|
||||
else
|
||||
loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone)
|
||||
end
|
||||
aUnit.x = loc.x
|
||||
aUnit.y = loc.z
|
||||
else
|
||||
@ -1363,9 +1375,14 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
|
||||
-- randomize if enabled
|
||||
if spawnZone.rndLoc then
|
||||
|
||||
local loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones
|
||||
rawData.x = rawData.x + dx
|
||||
rawData.y = rawData.y + dy
|
||||
local loc, dx, dy
|
||||
if spawnZone.onPerimeter then
|
||||
loc, dx, dy = cfxZones.createRandomPointOnZoneBoundary(spawnZone)
|
||||
else
|
||||
loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones
|
||||
end
|
||||
rawData.x = rawData.x + dx -- might want to use loc
|
||||
rawData.y = rawData.y + dy -- directly
|
||||
end
|
||||
|
||||
if spawnZone.rndHeading then
|
||||
@ -1963,7 +1980,7 @@ function cloneZones.start()
|
||||
-- to our watchlist
|
||||
for k, aZone in pairs(attrZones) do
|
||||
cloneZones.createClonerWithZone(aZone) -- process attribute and add to zone
|
||||
cloneZones.addCloneZone(aZone) -- remember it so we can smoke it
|
||||
cloneZones.addCloneZone(aZone)
|
||||
end
|
||||
|
||||
-- update all cloners and spawned clones from file
|
||||
|
||||
@ -49,13 +49,15 @@ csarManager.ups = 1
|
||||
- integration with playerScore
|
||||
- score global and per-mission
|
||||
- isCSARTarget API
|
||||
- 2.2.1 - added troopCarriers attribute to config
|
||||
- passes own troop carriers to dcsCommin.isTroopCarrier()
|
||||
|
||||
|
||||
--]]--
|
||||
-- modules that need to be loaded BEFORE I run
|
||||
csarManager.requiredLibs = {
|
||||
"dcsCommon", -- common is of course needed for everything
|
||||
"cfxZones", -- zones management foc CSAR and CSAR Mission zones
|
||||
"cfxZones", -- zones management for CSAR and CSAR Mission zones
|
||||
"cfxPlayer", -- player monitoring and group monitoring
|
||||
"nameStats", -- generic data module for weight
|
||||
"cargoSuper",
|
||||
@ -385,7 +387,7 @@ end
|
||||
|
||||
function csarManager.heloLanded(theUnit)
|
||||
-- when we have landed,
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
local conf = csarManager.getUnitConfig(theUnit)
|
||||
conf.unit = theUnit
|
||||
local theGroup = theUnit:getGroup()
|
||||
@ -532,7 +534,7 @@ end
|
||||
--
|
||||
--
|
||||
function csarManager.heloDeparted(theUnit)
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
-- if we have timed extractions (i.e. not instantaneous),
|
||||
-- then we need to check if we take off after the timer runs out
|
||||
|
||||
@ -555,7 +557,7 @@ end
|
||||
--
|
||||
|
||||
function csarManager.heloCrashed(theUnit)
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
-- problem: this isn't called on network games.
|
||||
|
||||
-- clean up
|
||||
@ -573,7 +575,7 @@ end
|
||||
|
||||
function csarManager.airframeCrashed(theUnit)
|
||||
-- called from airframe manager
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
local conf = csarManager.getUnitConfig(theUnit)
|
||||
conf.unit = theUnit
|
||||
local theGroup = theUnit:getGroup()
|
||||
@ -584,7 +586,7 @@ end
|
||||
|
||||
function csarManager.airframeDitched(theUnit)
|
||||
-- called from airframe manager
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
|
||||
local conf = csarManager.getUnitConfig(theUnit)
|
||||
conf.unit = theUnit
|
||||
@ -652,7 +654,7 @@ function csarManager.setCommsMenu(theUnit)
|
||||
|
||||
-- we only add this menu to helicopter troop carriers
|
||||
-- will also filter out all non-helicopters as nice side effect
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
|
||||
local group = theUnit:getGroup()
|
||||
local id = group:getID()
|
||||
@ -822,7 +824,7 @@ end
|
||||
function csarManager.playerChangeEvent(evType, description, player, data)
|
||||
if evType == "newGroup" then
|
||||
local theUnit = data.primeUnit
|
||||
if not dcsCommon.isTroopCarrier(theUnit) then return end
|
||||
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
|
||||
|
||||
csarManager.setCommsMenu(theUnit) -- allocates new config
|
||||
-- trigger.action.outText("+++csar: added " .. theUnit:getName() .. " to comms menu", 30)
|
||||
@ -939,7 +941,7 @@ function csarManager.update() -- every second
|
||||
local uID = uGroup:getID()
|
||||
local uSide = aUnit:getCoalition()
|
||||
local agl = dcsCommon.getUnitAGL(aUnit)
|
||||
if dcsCommon.isTroopCarrier(aUnit) then
|
||||
if dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers) then
|
||||
-- scan through all available csar missions to see if we are close
|
||||
-- enough to trigger comms
|
||||
for idx, csarMission in pairs (csarManager.openMissions) do
|
||||
@ -1277,7 +1279,7 @@ function csarManager.readConfigZone()
|
||||
|
||||
if cfxZones.hasProperty(theZone, "csarDelivered!") then
|
||||
csarManager.csarDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarDelivered!", "*<none>")
|
||||
--trigger.action.outText("+++csar: will bang csarDelivered: <" .. csarManager.csarDelivered .. ">", 30)
|
||||
|
||||
end
|
||||
|
||||
csarManager.rescueRadius = cfxZones.getNumberFromZoneProperty(theZone, "rescueRadius", 70) --70 -- must land within 50m to rescue
|
||||
@ -1293,6 +1295,19 @@ function csarManager.readConfigZone()
|
||||
csarManager.actionSound = cfxZones.getStringFromZoneProperty(theZone, "actionSound", "Quest Snare 3.wav")
|
||||
csarManager.vectoring = cfxZones.getBoolFromZoneProperty(theZone, "vectoring", true)
|
||||
|
||||
-- add own troop carriers
|
||||
if cfxZones.hasProperty(theZone, "troopCarriers") then
|
||||
local tc = cfxZones.getStringFromZoneProperty(theZone, "troopCarriers", "UH-1D")
|
||||
tc = dcsCommon.splitString(tc, ",")
|
||||
csarManager.troopCarriers = dcsCommon.trimArray(tc)
|
||||
if csarManager.verbose then
|
||||
trigger.action.outText("+++casr: redefined troop carriers to types:", 30)
|
||||
for idx, aType in pairs(csarManager.troopCarriers) do
|
||||
trigger.action.outText(aType, 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if csarManager.verbose then
|
||||
trigger.action.outText("+++csar: read config", 30)
|
||||
end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
dcsCommon = {}
|
||||
dcsCommon.version = "2.8.1"
|
||||
dcsCommon.version = "2.8.2"
|
||||
--[[-- VERSION HISTORY
|
||||
2.2.6 - compassPositionOfARelativeToB
|
||||
- clockPositionOfARelativeToB
|
||||
@ -127,6 +127,18 @@ dcsCommon.version = "2.8.1"
|
||||
- processStringWildcards()
|
||||
- new wildArrayContainsString()
|
||||
- fix for stringStartsWith oddity with aircraft types
|
||||
2.8.2 - better fixes for string.find() in stringStartsWith and containsString
|
||||
- dcsCommon.isTroopCarrier(theUnit, carriers) new carriers optional param
|
||||
- better guards for getUnitAlt and getUnitAGL
|
||||
- new newPointAtDegreesRange()
|
||||
- new newPointAtAngleRange()
|
||||
- new isTroopCarrierType()
|
||||
- stringStartsWith now supports case insensitive match
|
||||
- isTroopCarrier() supports 'any' and 'all'
|
||||
- made getEnemyCoalitionFor() more resilient
|
||||
- fix to smallRandom for negative numbers
|
||||
- isTroopCarrierType uses wildArrayContainsString
|
||||
|
||||
--]]--
|
||||
|
||||
-- dcsCommon is a library of common lua functions
|
||||
@ -139,7 +151,7 @@ dcsCommon.version = "2.8.1"
|
||||
|
||||
-- globals
|
||||
dcsCommon.cbID = 0 -- callback id for simple callback scheduling
|
||||
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50 and Gazelle can't carry troops
|
||||
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50, Apache and Gazelle can't carry troops
|
||||
dcsCommon.coalitionSides = {0, 1, 2}
|
||||
|
||||
-- lookup tables
|
||||
@ -270,8 +282,12 @@ dcsCommon.version = "2.8.1"
|
||||
-- 50 items (usually some more), and only then one itemis picked from
|
||||
-- that array with a random number that is from a greater range (0..50+)
|
||||
function dcsCommon.smallRandom(theNum) -- adapted from mist, only support ints
|
||||
theNum = math.floor(theNum)
|
||||
if theNum >= 50 then return math.random(theNum) end
|
||||
|
||||
if theNum < 1 then
|
||||
trigger.action.outText("smallRandom: invoke with argument < 1 (" .. theNum .. "), using 1", 30)
|
||||
theNum = 1
|
||||
end
|
||||
-- for small randoms (<50)
|
||||
local lowNum, highNum
|
||||
highNum = theNum
|
||||
@ -886,6 +902,20 @@ dcsCommon.version = "2.8.1"
|
||||
return thePoint, degrees
|
||||
end
|
||||
|
||||
function dcsCommon.newPointAtDegreesRange(p1, degrees, radius)
|
||||
local rads = degrees * 3.14152 / 180
|
||||
local p2 = dcsCommon.newPointAtAngleRange(p1, rads, radius)
|
||||
return p2
|
||||
end
|
||||
|
||||
function dcsCommon.newPointAtAngleRange(p1, angle, radius)
|
||||
local p2 = {}
|
||||
p2.x = p1.x + radius * math.cos(angle)
|
||||
p2.y = p1.y
|
||||
p2.z = p1.z + radius * math.sin(angle)
|
||||
return p2
|
||||
end
|
||||
|
||||
-- get group location: get the group's location by
|
||||
-- accessing the fist existing, alive member of the group that it finds
|
||||
function dcsCommon.getGroupLocation(group)
|
||||
@ -1005,13 +1035,14 @@ dcsCommon.version = "2.8.1"
|
||||
end
|
||||
|
||||
function dcsCommon.getEnemyCoalitionFor(aCoalition)
|
||||
if aCoalition == 1 then return 2 end
|
||||
if aCoalition == 2 then return 1 end
|
||||
if type(aCoalition) == "string" then
|
||||
aCoalition = aCoalition:lower()
|
||||
if aCoalition == "red" then return 2 end
|
||||
if aCoalition == "blue" then return 1 end
|
||||
return nil
|
||||
end
|
||||
if aCoalition == 1 then return 2 end
|
||||
if aCoalition == 2 then return 1 end
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -1818,7 +1849,7 @@ dcsCommon.version = "2.8.1"
|
||||
theUnit.x = theUnit.x + cx -- MOVE BACK
|
||||
theUnit.y = theUnit.y + cy
|
||||
|
||||
-- may also want to increase heading by degreess
|
||||
-- may also want to increase heading by degrees
|
||||
theUnit.heading = theUnit.heading + rads
|
||||
end
|
||||
end
|
||||
@ -1840,7 +1871,7 @@ dcsCommon.version = "2.8.1"
|
||||
theUnit.x = theUnit.x + cx -- MOVE BACK
|
||||
theUnit.y = theUnit.y + cy
|
||||
|
||||
-- may also want to increase heading by degreess
|
||||
-- may also want to increase heading by degrees
|
||||
theUnit.heading = theUnit.heading + rads
|
||||
-- now kill psi if it existed before
|
||||
-- theUnit.psi = nil
|
||||
@ -2011,29 +2042,30 @@ end
|
||||
|
||||
--trigger.action.outText("wildACS: theString = <" .. theString .. ">, theArray contains <" .. #theArray .. "> elements", 30)
|
||||
local wildIn = dcsCommon.stringEndsWith(theString, "*")
|
||||
if wildIn then dcsCommon.removeEnding(thestring, "*") end
|
||||
for i = 1, #theArray do
|
||||
local theElement = theArray[i]
|
||||
if caseSensitive then theElement = string.upper(theElement) end
|
||||
if wildIn then dcsCommon.removeEnding(theString, "*") end
|
||||
for idx, theElement in pairs(theArray) do -- i = 1, #theArray do
|
||||
--local theElement = theArray[i]
|
||||
--trigger.action.outText("test e <" .. theElement .. "> against s <" .. theString .. ">", 30)
|
||||
if not caseSensitive then theElement = string.upper(theElement) end
|
||||
local wildEle = dcsCommon.stringEndsWith(theElement, "*")
|
||||
if wildEle then theElement = dcsCommon.removeEnding(theElement, "*") end
|
||||
--trigger.action.outText("matching s=<" .. theString .. "> with e=<" .. theElement .. ">", 30)
|
||||
if wildEle and wildIn then
|
||||
-- both end on wildcards, partial match for both
|
||||
if dcsCommon.stringStartsWith(theElement. theString) then return true end
|
||||
if dcsCommon.stringStartsWith(theElement, theString) then return true end
|
||||
if dcsCommon.stringStartsWith(theString, theElement) then return true end
|
||||
--trigger.action.outText("match e* with s* failed.", 30)
|
||||
elseif wildEle then
|
||||
-- Element is a wildcard, partial match
|
||||
if dcsCommon.stringStartsWith(theString, theElement) then return true end
|
||||
--trigger.action.outText("match e* with s failed.", 30)
|
||||
--trigger.action.outText("startswith - match e* <" .. theElement .. "> with s <" .. theString .. "> failed.", 30)
|
||||
elseif wildIn then
|
||||
-- theString is a wildcard. partial match
|
||||
if dcsCommon.stringStartsWith(theElement. theString) then return true end
|
||||
if dcsCommon.stringStartsWith(theElement, theString) then return true end
|
||||
--trigger.action.outText("match e with s* failed.", 30)
|
||||
else
|
||||
-- standard: no wildcards, full match
|
||||
if theArray[i] == theString then return true end
|
||||
if theElement == theString then return true end
|
||||
--trigger.action.outText("match e with s (straight) failed.", 30)
|
||||
end
|
||||
|
||||
@ -2165,13 +2197,22 @@ end
|
||||
return false
|
||||
end
|
||||
|
||||
function dcsCommon.stringStartsWith(theString, thePrefix)
|
||||
function dcsCommon.stringStartsWith(theString, thePrefix, caseInsensitive)
|
||||
if not theString then return false end
|
||||
if not thePrefix then return false end
|
||||
if not caseInsensitive then caseInsensitive = false end
|
||||
|
||||
if caseInsensitive then
|
||||
theString = string.upper(theString)
|
||||
thePrefix = string.upper(theString)
|
||||
end
|
||||
-- new code because old 'string.find' had some really
|
||||
-- strange results with aircraft types. Prefix "A-10" did not
|
||||
-- match string "A-10A" etc.
|
||||
|
||||
-- superseded: string.find (s, pattern [, init [, plain]]) solves the problem
|
||||
|
||||
--[[
|
||||
local pl = string.len(thePrefix)
|
||||
if pl > string.len(theString) then return false end
|
||||
if pl < 1 then return false end
|
||||
@ -2184,11 +2225,12 @@ end
|
||||
end
|
||||
|
||||
return true
|
||||
--[[-- trigger.action.outText("---- OK???", 30)
|
||||
--]]-- trigger.action.outText("---- OK???", 30)
|
||||
-- strange stuff happening with some strings, let's investigate
|
||||
|
||||
|
||||
local res = string.find(theString, thePrefix) == 1
|
||||
local i, j = string.find(theString, thePrefix, 1, true)
|
||||
return (i == 1)
|
||||
--[[--
|
||||
if res then
|
||||
trigger.action.outText("startswith: <" .. theString .. "> pre <" .. thePrefix .. "> --> YES", 30)
|
||||
else
|
||||
@ -2222,7 +2264,7 @@ end
|
||||
what = string.upper(what)
|
||||
end
|
||||
if inString == what then return true end -- when entire match
|
||||
return string.find(inString, what)
|
||||
return string.find(inString, what, 1, true) -- 1, true means start at 1, plaintext
|
||||
end
|
||||
|
||||
function dcsCommon.bool2Text(theBool)
|
||||
@ -2441,6 +2483,7 @@ end
|
||||
end
|
||||
|
||||
function dcsCommon.markPointWithSmoke(p, smokeColor)
|
||||
if not smokeColor then smokeColor = 0 end
|
||||
local x = p.x
|
||||
local z = p.z -- do NOT change the point directly
|
||||
-- height-correct
|
||||
@ -2628,17 +2671,35 @@ function dcsCommon.isSceneryObject(theUnit)
|
||||
return theUnit.getCoalition == nil -- scenery objects do not return a coalition
|
||||
end
|
||||
|
||||
function dcsCommon.isTroopCarrier(theUnit)
|
||||
-- return true if conf can carry troups
|
||||
if not theUnit then return false end
|
||||
local uType = theUnit:getTypeName()
|
||||
if dcsCommon.arrayContainsString(dcsCommon.troopCarriers, uType) then
|
||||
function dcsCommon.isTroopCarrierType(theType, carriers)
|
||||
if not theType then return false end
|
||||
if not carriers then carriers = dcsCommon.troopCarriers
|
||||
end
|
||||
-- remember that arrayContainsString is case INsensitive by default
|
||||
if dcsCommon.wildArrayContainsString(carriers, theType) then
|
||||
-- may add additional tests before returning true
|
||||
return true
|
||||
end
|
||||
|
||||
-- see if user wanted 'any' or 'all' supported
|
||||
if dcsCommon.arrayContainsString(carriers, "any") then
|
||||
return true
|
||||
end
|
||||
|
||||
if dcsCommon.arrayContainsString(carriers, "all") then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function dcsCommon.isTroopCarrier(theUnit, carriers)
|
||||
-- return true if conf can carry troups
|
||||
if not theUnit then return false end
|
||||
local uType = theUnit:getTypeName()
|
||||
return dcsCommon.isTroopCarrierType(uType, carriers)
|
||||
end
|
||||
|
||||
function dcsCommon.isPlayerUnit(theUnit)
|
||||
-- new patch. simply check if getPlayerName returns something
|
||||
if not theUnit then return false end
|
||||
@ -2664,14 +2725,14 @@ end
|
||||
|
||||
function dcsCommon.getUnitAlt(theUnit)
|
||||
if not theUnit then return 0 end
|
||||
if not theUnit:isExist() then return 0 end
|
||||
if not Unit.isExist(theUnit) then return 0 end -- safer
|
||||
local p = theUnit:getPoint()
|
||||
return p.y
|
||||
end
|
||||
|
||||
function dcsCommon.getUnitAGL(theUnit)
|
||||
if not theUnit then return 0 end
|
||||
if not theUnit:isExist() then return 0 end
|
||||
if not Unit.isExist(theUnit) then return 0 end -- safe fix
|
||||
local p = theUnit:getPoint()
|
||||
local alt = p.y
|
||||
local loc = {x = p.x, y = p.z}
|
||||
|
||||
@ -29,9 +29,22 @@ groupTracker.trackers = {}
|
||||
- numUnits output
|
||||
- persistence
|
||||
1.2.1 - allGone! bug removed
|
||||
1.2.2 - new groupTrackedBy() method
|
||||
- limbo for storing a unit in limbo so it is
|
||||
- not counted as missing when being transported
|
||||
|
||||
--]]--
|
||||
|
||||
-- 'limbo'
|
||||
-- is a special storage in tracker indexed by name that is used
|
||||
-- to temporarily suspend a groups tracking while it's not in
|
||||
-- the mission, e.g. because it's being transported by heloTroops
|
||||
-- in limbo, only the number of units is preserved
|
||||
-- addGroup will automatically move a group back from limbo
|
||||
-- to move into limbo, you must use moveGroupToLimboForTracker
|
||||
-- to remove a group in limbo, use removeGroupNamedFromTracker
|
||||
--
|
||||
|
||||
function groupTracker.addTracker(theZone)
|
||||
table.insert(groupTracker.trackers, theZone)
|
||||
end
|
||||
@ -54,6 +67,8 @@ end
|
||||
--
|
||||
-- adding a group to a tracker - called by other modules and API
|
||||
--
|
||||
-- addGroupToTracker will automatically also move a group from
|
||||
-- limbo to tracker if it already existed in limbo
|
||||
function groupTracker.addGroupToTracker(theGroup, theTracker)
|
||||
-- check if filtering is enabled for this tracker
|
||||
if theTracker.groupFilter then
|
||||
@ -83,19 +98,30 @@ function groupTracker.addGroupToTracker(theGroup, theTracker)
|
||||
if gName == theName then exists = true end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if not exists then
|
||||
table.insert(theTracker.trackedGroups, theGroup)
|
||||
|
||||
-- now bang/invoke addGroup!
|
||||
if theTracker.tAddGroup then
|
||||
cfxZones.pollFlag(theTracker.tAddGroup, "inc", theTracker)
|
||||
|
||||
-- see if we merely transfer group back from limbo
|
||||
-- to tracked
|
||||
if theTracker.limbo[theName] then
|
||||
-- group of that name is in limbo
|
||||
if theTracker.verbose then
|
||||
trigger.action.outText("+++gTrk: moving shelved group <" .. theName .. "> back to normal tracking for <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
theTracker.limbo[theName] = nil -- remove from limbo
|
||||
else
|
||||
-- now bang/invoke addGroup!
|
||||
if theTracker.tAddGroup then
|
||||
cfxZones.pollFlag(theTracker.tAddGroup, "inc", theTracker)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- now set numGroups
|
||||
if theTracker.tNumGroups then
|
||||
cfxZones.setFlagValue(theTracker.tNumGroups, #theTracker.trackedGroups, theTracker)
|
||||
cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker)
|
||||
end
|
||||
|
||||
-- count all units
|
||||
@ -105,6 +131,9 @@ function groupTracker.addGroupToTracker(theGroup, theTracker)
|
||||
totalUnits = totalUnits + aGroup:getSize()
|
||||
end
|
||||
end
|
||||
for idx, limboNum in pairs(theTracker.limbo) do
|
||||
totalUnits = totalUnits + limboNum
|
||||
end
|
||||
|
||||
-- update unit count
|
||||
if theTracker.tNumUnits then
|
||||
@ -113,6 +142,7 @@ function groupTracker.addGroupToTracker(theGroup, theTracker)
|
||||
-- invoke callbacks
|
||||
end
|
||||
|
||||
|
||||
function groupTracker.addGroupToTrackerNamed(theGroup, trackerName)
|
||||
if not trackerName then
|
||||
trigger.action.outText("+++gTrk: nil tracker in addGroupToTrackerNamed", 30)
|
||||
@ -133,6 +163,93 @@ function groupTracker.addGroupToTrackerNamed(theGroup, trackerName)
|
||||
groupTracker.addGroupToTracker(theGroup, theTracker)
|
||||
end
|
||||
|
||||
function groupTracker.moveGroupToLimboForTracker(theGroup, theTracker)
|
||||
if not theGroup then return end
|
||||
if not theTracker then return end
|
||||
if not Group.isExist(theGroup) then return end
|
||||
|
||||
local gName = theGroup:getName()
|
||||
local filtered = {}
|
||||
if theTracker.trackedGroups then
|
||||
for idx, aGroup in pairs(theTracker.trackedGroups) do
|
||||
if Group.isExist(aGroup) and aGroup:getName() == gName then
|
||||
-- move this to limbo
|
||||
theTracker.limbo[gName] = aGroup:getSize()
|
||||
if theTracker.verbose then
|
||||
trigger.action.outText("+++gTrk: moved group <" .. gName .. "> to limbo for <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
-- filtered
|
||||
else
|
||||
table.insert(filtered, aGroup)
|
||||
end
|
||||
end
|
||||
theTracker.trackedGroups = filtered
|
||||
end
|
||||
end
|
||||
|
||||
function groupTracker.removeGroupNamedFromTracker(gName, theTracker)
|
||||
if not gName then return end
|
||||
if not theTracker then return end
|
||||
|
||||
local filteredGroups = {}
|
||||
local foundOne = false
|
||||
local totalUnits = 0
|
||||
if not theTracker.trackedGroups then theTracker.trackedGroups = {} end
|
||||
for idx, aGroup in pairs(theTracker.trackedGroups) do
|
||||
if Group.isExist(aGroup) and aGroup:getName() == gName then
|
||||
-- skip and remember
|
||||
foundOne = true
|
||||
else
|
||||
table.insert(filteredGroups, aGroup)
|
||||
if Group.isExist(aGroup) then
|
||||
totalUnits = totalUnits + aGroup:getSize()
|
||||
end
|
||||
end
|
||||
end
|
||||
-- also check limbo
|
||||
for limboName, limboNum in pairs (theTracker.limbo) do
|
||||
if gName == limboName then
|
||||
-- don't count, but remember that it existed
|
||||
foundOne = true
|
||||
if theTracker.verbose then
|
||||
trigger.action.outText("+++gTrk: removed group <" .. gName .. "> from limbo for <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
else
|
||||
totalUnits = totalUnits + limboNum
|
||||
end
|
||||
end
|
||||
-- remove from limbo
|
||||
theTracker.limbo[gName] = nil
|
||||
|
||||
if (not foundOne) and (theTracker.verbose or groupTracker.verbose) then
|
||||
trigger.action.outText("+++gTrk: Removal Request Note: group <" .. gName .. "> wasn't tracked by <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
|
||||
-- remember the new, cleanded set
|
||||
theTracker.trackedGroups = filteredGroups
|
||||
|
||||
-- update number of tracked units. do it in any case
|
||||
if theTracker.tNumUnits then
|
||||
cfxZones.setFlagValue(theTracker.tNumUnits, totalUnits, theTracker)
|
||||
end
|
||||
|
||||
if foundOne then
|
||||
if theTracker.verbose or groupTracker.verbose then
|
||||
trigger.action.outText("+++gTrk: removed group <" .. gName .. "> from tracker <" .. theTracker.name .. ">", 30)
|
||||
end
|
||||
|
||||
-- now bang/invoke removeGroup!
|
||||
if theTracker.tRemoveGroup then
|
||||
cfxZones.pollFlag(theTracker.tRemoveGroup, "inc", theTracker)
|
||||
end
|
||||
|
||||
-- now set numGroups
|
||||
if theTracker.tNumGroups then
|
||||
cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName)
|
||||
local theTracker = groupTracker.getTrackerByName(trackerName)
|
||||
if not theTracker then return end
|
||||
@ -141,6 +258,8 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName)
|
||||
return
|
||||
end
|
||||
|
||||
groupTracker.removeGroupNamedFromTracker(gName, theTracker)
|
||||
--[[--
|
||||
local filteredGroups = {}
|
||||
local foundOne = false
|
||||
local totalUnits = 0
|
||||
@ -156,6 +275,18 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- also check limbo
|
||||
for limboName, limboNum in pairs (theTracker.limbo) do
|
||||
if gName == limboName then
|
||||
-- don't count, but remember that it existed
|
||||
foundOne = true
|
||||
else
|
||||
totalUnits = totalUnits + limboNum
|
||||
end
|
||||
end
|
||||
-- remove from limbo
|
||||
theTracker.limbo[gName] = nil
|
||||
|
||||
if (not foundOne) and (theTracker.verbose or groupTracker.verbose) then
|
||||
trigger.action.outText("+++gTrk: Removal Request Note: group <" .. gName .. "> wasn't tracked by <" .. trackerName .. ">", 30)
|
||||
end
|
||||
@ -180,16 +311,59 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName)
|
||||
|
||||
-- now set numGroups
|
||||
if theTracker.tNumGroups then
|
||||
cfxZones.setFlagValue(theTracker.tNumGroups, #theTracker.trackedGroups, theTracker)
|
||||
cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker)
|
||||
end
|
||||
end
|
||||
--]]--
|
||||
end
|
||||
|
||||
-- groupTrackedBy - return trackers that track group theGroup
|
||||
-- returns 3 values: true/false (is tracking), number of trackers, array of trackers
|
||||
function groupTracker.groupNameTrackedBy(theName)
|
||||
local isTracking = false
|
||||
|
||||
-- now iterate all trackers
|
||||
local tracking = {}
|
||||
for idx, aTracker in pairs(groupTracker.trackers) do
|
||||
-- only look at tracked groups if that tracker has an
|
||||
-- initialized tracker (lazy init)
|
||||
if aTracker.trackedGroups then
|
||||
for idy, aGroup in pairs (aTracker.trackedGroups) do
|
||||
if Group.isExist(aGroup) and aGroup:getName() == theName then
|
||||
table.insert(tracking, aTracker)
|
||||
isTracking = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for aName, aNum in pairs(aTracker.limbo) do
|
||||
if aName == theName then
|
||||
table.insert(tracking, aTracker)
|
||||
isTracking = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return isTracking, #tracking, tracking
|
||||
end
|
||||
|
||||
function groupTracker.groupTrackedBy(theGroup)
|
||||
if not theGroup then return false,0, nil end
|
||||
if not Group.isExist(theGroup) then return false, 0, nil end
|
||||
local theName = theGroup:getName()
|
||||
local isTracking, numTracks, trackers = groupTracker.groupNameTrackedBy(theName)
|
||||
return isTracking, numTracks, trackers
|
||||
|
||||
end
|
||||
|
||||
--
|
||||
-- read zone
|
||||
--
|
||||
function groupTracker.createTrackerWithZone(theZone)
|
||||
-- init group tracking set
|
||||
theZone.trackedGroups = {}
|
||||
|
||||
theZone.limbo = {} -- name based, for groups that are tracked
|
||||
-- although technically off the map (helo etc)
|
||||
|
||||
theZone.trackerMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc")
|
||||
if cfxZones.hasProperty(theZone, "trackerMethod") then
|
||||
@ -264,6 +438,10 @@ function groupTracker.destroyAllInZone(theZone)
|
||||
theGroup:destroy()
|
||||
end
|
||||
end
|
||||
for aName, aNum in pairs(theZone.limbo) do
|
||||
theZone.limbo[aName] = 0 -- <1 is special for 'remove me and detect kill on next checkGroups'
|
||||
end
|
||||
|
||||
-- we keep all groups in trackedGroups so we
|
||||
-- generate a host of destroy events when we run through
|
||||
-- checkGroups next
|
||||
@ -300,12 +478,26 @@ function groupTracker.checkGroups(theZone)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local newLimbo = {}
|
||||
for aName, aNum in pairs (theZone.limbo) do
|
||||
if aNum < 1 then
|
||||
if groupTracker.verbose or theZone.verbose then
|
||||
trigger.action.outText("+++gTrk: dead group <" .. aName .. "> detected in LIMBO for " .. theZone.name .. ", removing.", 30)
|
||||
end
|
||||
else
|
||||
newLimbo[aName] = aNum
|
||||
totalUnits = totalUnits + aNum
|
||||
end
|
||||
end
|
||||
theZone.limbo = newLimbo
|
||||
|
||||
-- now exchange filtered for current
|
||||
theZone.trackedGroups = filteredGroups
|
||||
--set new group value
|
||||
-- now set numGroups if defined
|
||||
if theZone.tNumGroups then
|
||||
cfxZones.setFlagValue(theZone.tNumGroups, #filteredGroups, theZone)
|
||||
cfxZones.setFlagValue(theZone.tNumGroups, dcsCommon.getSizeOfTable(theZone.limbo) + #filteredGroups, theZone)
|
||||
end
|
||||
|
||||
-- and update unit count if defined
|
||||
@ -361,7 +553,7 @@ function groupTracker.update()
|
||||
groupTracker.checkGroups(theZone)
|
||||
|
||||
-- see if we need to bang on empty!
|
||||
local currCount = #theZone.trackedGroups
|
||||
local currCount = #theZone.trackedGroups + dcsCommon.getSizeOfTable(theZone.limbo)
|
||||
if theZone.allGoneFlag and currCount == 0 and currCount ~= theZone.lastGroupCount then
|
||||
cfxZones.pollFlag(theZone.allGoneFlag, theZone.trackerMethod, theZone)
|
||||
end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
messenger = {}
|
||||
messenger.version = "2.2.0"
|
||||
messenger.version = "2.2.1"
|
||||
messenger.verbose = false
|
||||
messenger.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
@ -64,6 +64,9 @@ messenger.messengers = {}
|
||||
2.2.0 - <player: unit>
|
||||
- made dynamic string gen more portable in prep for move to cfxZones
|
||||
- refactoring wildcard processing: moved to cfxZones
|
||||
2.2.1 - when messenger is linked to a unit, it can use the linked
|
||||
unit as reference point for relative wildcards. Always broadcasts to coalition. Can be used to broadcase 'eye in the sky' type information
|
||||
- fixed verbosity bug
|
||||
|
||||
--]]--
|
||||
|
||||
@ -318,7 +321,7 @@ function messenger.createMessengerWithZone(theZone)
|
||||
-- flag whose value can be read: to be deprecated
|
||||
if cfxZones.hasProperty(theZone, "messageValue?") then
|
||||
theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "<none>")
|
||||
trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to <v:<flag> now!")
|
||||
trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to <v:<flag> now!", 30)
|
||||
end
|
||||
|
||||
-- time format for new <t: flagname>
|
||||
@ -418,6 +421,20 @@ function messenger.isTriggered(theZone)
|
||||
end
|
||||
trigger.action.outSoundForUnit(ID, fileName)
|
||||
end
|
||||
elseif cfxZones.getLinkedUnit(theZone) then
|
||||
-- this only works if the zone is linked to a unit
|
||||
-- and not using group or unit
|
||||
-- the linked unit is then used as reference
|
||||
-- outputs to all of same coalition as the linked
|
||||
-- unit
|
||||
local theUnit = cfxZones.getLinkedUnit(theZone)
|
||||
local ID = theUnit:getID()
|
||||
local coa = theUnit:getCoalition()
|
||||
msg = messenger.dynamicUnitProcessing(msg, theZone, theUnit)
|
||||
if #msg > 0 or theZone.clearScreen then
|
||||
trigger.action.outTextForCoalition(coa, msg, theZone.duration, theZone.clearScreen)
|
||||
end
|
||||
trigger.action.outSoundForCoalition(coa, fileName)
|
||||
else
|
||||
-- out to all
|
||||
if #msg > 0 or theZone.clearScreen then
|
||||
@ -518,3 +535,8 @@ if not messenger.start() then
|
||||
messenger = nil
|
||||
end
|
||||
|
||||
|
||||
--[[--
|
||||
Ideas:
|
||||
- when messenger is ties to a unit, that unit can also be base for all relative references. Only checked if neither group nor unit is set
|
||||
--]]--
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
persistence = {}
|
||||
persistence.version = "1.0.4"
|
||||
persistence.version = "1.0.6"
|
||||
persistence.ups = 1 -- once every 1 seconds
|
||||
persistence.verbose = false
|
||||
persistence.active = false
|
||||
@ -26,6 +26,7 @@ persistence.requiredLibs = {
|
||||
new 'saveNotification" can be off
|
||||
1.0.4 - new optional 'root' property
|
||||
1.0.5 - desanitize check on readConfig to early-abort
|
||||
1.0.6 - removed potential verbosity bug
|
||||
|
||||
|
||||
PROVIDES LOAD/SAVE ABILITY TO MODULES
|
||||
@ -132,7 +133,7 @@ end
|
||||
function persistence.saveText(theString, fileName, shared, append)
|
||||
if not persistence.active then return false end
|
||||
if not fileName then
|
||||
trigger.action.outText("+++persistence: saveText without fileName")
|
||||
trigger.action.outText("+++persistence: saveText without fileName", 30)
|
||||
return false
|
||||
end
|
||||
if not shared then shared = flase end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
pulseFlags = {}
|
||||
pulseFlags.version = "1.3.1"
|
||||
pulseFlags.version = "1.3.2"
|
||||
pulseFlags.verbose = false
|
||||
pulseFlags.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
@ -37,6 +37,7 @@ pulseFlags.requiredLibs = {
|
||||
returned onStart, defaulting to true
|
||||
- 1.3.0 persistence
|
||||
- 1.3.1 typos corrected
|
||||
- 1.3.2 removed last pulse's timeID upon entry in doPulse
|
||||
|
||||
--]]--
|
||||
|
||||
@ -165,6 +166,7 @@ end
|
||||
|
||||
|
||||
function pulseFlags.doPulse(args)
|
||||
|
||||
local theZone = args[1]
|
||||
-- check if we have been paused. if so, simply
|
||||
-- exit with no new schedule
|
||||
@ -172,6 +174,8 @@ function pulseFlags.doPulse(args)
|
||||
theZone.pulsing = false
|
||||
return
|
||||
end
|
||||
-- erase old timerID, since we completed that
|
||||
theZone.timerID = nil
|
||||
|
||||
-- do a poll on flags
|
||||
-- first, we only do an initial pulse if zeroPulse is set
|
||||
@ -268,7 +272,7 @@ function pulseFlags.update()
|
||||
-- pausePulseFlag
|
||||
if cfxZones.testZoneFlag(aZone, aZone.pausePulseFlag, aZone.pulseTriggerMethod, "lastPauseValue") then
|
||||
if pulseFlags.verbose or aZone.verbose then
|
||||
trigger.action.outText("+++pulF: pausing <" .. aZone.name .. ">", 30)
|
||||
trigger.action.outText("+++pulF: pausing <" .. aZone.name .. ">", 30)
|
||||
end
|
||||
aZone.pulsePaused = true -- prevents new start
|
||||
if aZone.timerID then
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
valet = {}
|
||||
valet.version = "1.0.0"
|
||||
valet.version = "1.0.2"
|
||||
valet.verbose = false
|
||||
valet.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
@ -10,6 +10,9 @@ valet.valets = {}
|
||||
--[[--
|
||||
Version History
|
||||
1.0.0 - initial version
|
||||
1.0.1 - typos in verbosity corrected
|
||||
1.0.2 - also scan birth events
|
||||
|
||||
--]]--
|
||||
|
||||
function valet.addValet(theZone)
|
||||
@ -374,16 +377,20 @@ function valet.checkPlayerSpawn(playerName, theUnit)
|
||||
end
|
||||
|
||||
function valet:onEvent(event)
|
||||
if event.id == 20 then
|
||||
if (event.id == 20) or (event.id == 15) then
|
||||
if not event.initiator then return end
|
||||
local theUnit = event.initiator
|
||||
if not theUnit.getPlayerName then
|
||||
trigger.action.outText("+++valet: non player event 20(?)", 30)
|
||||
if not theUnit.getPlayerName then
|
||||
if event.id == 20 then
|
||||
trigger.action.outText("+++valet: non player event 20(?)", 30)
|
||||
end -- 15 (birth can happen to all)
|
||||
return
|
||||
end
|
||||
local pName = theUnit:getPlayerName()
|
||||
if not pName then
|
||||
trigger.action.outText("+++valet: nil player name on event 20 (!)", 30)
|
||||
if event.id == 20 then
|
||||
trigger.action.outText("+++valet: nil player name on event 20 (!)", 30)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@ -398,7 +405,7 @@ function valet.readConfigZone()
|
||||
local theZone = cfxZones.getZoneByName("valetConfig")
|
||||
if not theZone then
|
||||
if valet.verbose then
|
||||
trigger.action.outText("+++msgr: NO config zone!", 30)
|
||||
trigger.action.outText("+++valet: NO config zone!", 30)
|
||||
end
|
||||
theZone = cfxZones.createSimpleZone("valetConfig")
|
||||
end
|
||||
@ -406,7 +413,7 @@ function valet.readConfigZone()
|
||||
valet.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
|
||||
|
||||
if valet.verbose then
|
||||
trigger.action.outText("+++msgr: read config", 30)
|
||||
trigger.action.outText("+++valet: read config", 30)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
BIN
sound FX/beacon beep-beep.ogg
Normal file
BIN
sound FX/beacon beep-beep.ogg
Normal file
Binary file not shown.
BIN
sound FX/submarine ping.ogg
Normal file
BIN
sound FX/submarine ping.ogg
Normal file
Binary file not shown.
Binary file not shown.
BIN
tutorial & demo missions/demo - Davy Jones' Rocker.miz
Normal file
BIN
tutorial & demo missions/demo - Davy Jones' Rocker.miz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user