Version 2.2.2

Maintenance update
This commit is contained in:
Christian Franz 2024-04-18 11:31:07 +02:00
parent 8f7371825d
commit 7e4c147071
20 changed files with 1830 additions and 353 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
FARPZones = {}
FARPZones.version = "1.2.1"
FARPZones.version = "2.0.0"
FARPZones.verbose = false
--[[--
Version History
@ -15,7 +15,7 @@ FARPZones.verbose = false
- handles contested state
1.2.1 - now gracefully handles a FARP Zone that does not
contain a FARP, but is placed beside it
2.0.0 - dmlZones
--]]--
@ -120,9 +120,9 @@ function FARPZones.createFARPFromZone(aZone)
local theFarp = {}
theFarp.zone = aZone
theFarp.name = aZone.name
theFarp.point = cfxZones.getPoint(aZone) -- failsafe
theFarp.point = aZone:getPoint() -- failsafe
-- find the FARPS that belong to this zone
local thePoint = cfxZones.getPoint(aZone)
local thePoint = aZone:getPoint()
local mapFarps = dcsCommon.getAirbasesInRangeOfPoint(
thePoint,
aZone.radius,
@ -156,10 +156,7 @@ function FARPZones.createFARPFromZone(aZone)
-- end
-- get r and phi for defenders
local rPhi = cfxZones.getVectorFromZoneProperty(
aZone,
"rPhiHDef",
3)
local rPhi = aZone:getVectorFromZoneProperty("rPhiHDef",3)
-- get r and phi for facilities
-- create a new defenderzone for this
@ -167,17 +164,14 @@ function FARPZones.createFARPFromZone(aZone)
local phi = rPhi[2] * 0.0174533 -- 1 degree = 0.0174533 rad
local dx = aZone.point.x + r * math.cos(phi)
local dz = aZone.point.z + r * math.sin(phi)
local formRad = cfxZones.getNumberFromZoneProperty(aZone, "rFormation", 100)
local formRad = aZone:getNumberFromZoneProperty("rFormation", 100)
theFarp.defZone = cfxZones.createSimpleZone(aZone.name .. "-Def", {x=dx, y = 0, z=dz}, formRad)
theFarp.defHeading = rPhi[3]
rPhi = {}
rPhi = cfxZones.getVectorFromZoneProperty(
aZone,
"rPhiHRes",
3)
--trigger.action.outText("*** RES rPhi are " .. rPhi[1] .. " and " .. rPhi[2] .. " heading " .. rPhi[3], 30)
rPhi = aZone:getVectorFromZoneProperty("rPhiHRes", 3)
r = rPhi[1]
phi = rPhi[2] * 0.0174533 -- 1 degree = 0.0174533 rad
dx = aZone.point.x + r * math.cos(phi)
@ -187,17 +181,18 @@ function FARPZones.createFARPFromZone(aZone)
theFarp.resHeading = rPhi[3]
-- get redDefenders - defenders produced when red owned
theFarp.redDefenders = cfxZones.getStringFromZoneProperty(aZone, "redDefenders", "none")
theFarp.redDefenders = aZone:getStringFromZoneProperty( "redDefenders", "none")
-- get blueDefenders - defenders produced when blue owned
theFarp.blueDefenders = cfxZones.getStringFromZoneProperty(aZone, "blueDefenders", "none")
theFarp.blueDefenders = aZone:getStringFromZoneProperty( "blueDefenders", "none")
-- get formation for defenders
theFarp.formation = cfxZones.getStringFromZoneProperty(aZone, "formation", "circle_out")
theFarp.formation = aZone:getStringFromZoneProperty("formation", "circle_out")
theFarp.count = 0 -- for uniqueness
theFarp.hideRed = cfxZones.getBoolFromZoneProperty(aZone, "hideRed")
theFarp.hideBlue = cfxZones.getBoolFromZoneProperty(aZone, "hideBlue")
theFarp.hideGrey = cfxZones.getBoolFromZoneProperty(aZone, "hideGrey")
theFarp.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden")
theFarp.hideRed = aZone:getBoolFromZoneProperty("hideRed", false)
theFarp.hideBlue = aZone:getBoolFromZoneProperty("hideBlue", false)
theFarp.hideGrey = aZone:getBoolFromZoneProperty("hideGrey", false)
theFarp.hidden = aZone:getBoolFromZoneProperty("hidden", false)
theFarp.neutralProduction = aZone:getBoolFromZoneProperty("neutralProduction", false)
return theFarp
end
@ -225,7 +220,7 @@ function FARPZones.drawFARPCircleInMap(theFarp)
if theFarp.hideGrey and
theFarp.owner == 0 then
-- hide only when blue
-- hide only when grey
return
end
@ -258,7 +253,7 @@ function FARPZones.drawFARPCircleInMap(theFarp)
aZone.markID = markID
end
--[[--
function FARPZones.drawZoneInMap(aZone, owner)
-- owner is 0 = neutral, 1 = red, 2 = blue
-- will save markID in zone's markID
@ -288,6 +283,7 @@ function FARPZones.drawZoneInMap(aZone, owner)
aZone.markID = markID
end
--]]--
function FARPZones.scheduedProduction(args)
-- args contain [aFarp, owner]
@ -311,6 +307,16 @@ function FARPZones.scheduedProduction(args)
end
function FARPZones.produceVehicles(theFarp)
local theZone = theFarp.zone
-- trigger.action.outText("entering veh prod run for farp zone <" .. theZone.name .. ">, owner is <" .. theFarp.owner .. ">", 30)
--end
-- abort production if farp is owned by neutral and
-- neutralproduction is false
if theFarp.owner == 0 and not theFarp.neutralProduction then
return
end
-- first, remove anything that may still be there
if theFarp.defenders and theFarp.defenders:isExist() then
theFarp.defenders:destroy()
@ -529,20 +535,13 @@ end
function FARPZones.readConfig()
local theZone = cfxZones.getZoneByName("farpZonesConfig")
if not theZone then
if FARPZones.verbose then
trigger.action.outText("***frpZ: NO config zone!", 30)
end
return
theZone = cfxZones.createSimpleZone("farpZonesConfig")
end
FARPZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
FARPZones.verbose = theZone.verbose
FARPZones.spinUpDelay = cfxZones.getNumberFromZoneProperty(theZone, "spinUpDelay", 30)
FARPZones.spinUpDelay = theZone:getNumberFromZoneProperty( "spinUpDelay", 30)
if FARPZones.verbose then
trigger.action.outText("***frpZ: read config", 30)
end
end
@ -618,4 +617,5 @@ Improvements:
make farps repair their service vehicles after a time, or simply refresh them every x minutes, to make the algo simpler
allow for ownership control via the airfield module?
--]]--

View File

@ -1,5 +1,5 @@
autoCSAR = {}
autoCSAR.version = "2.0.0"
autoCSAR.version = "2.0.1"
autoCSAR.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
@ -14,6 +14,8 @@ autoCSAR.trackedEjects = {} -- we start tracking on eject
1.1.0 - allow open water CSAR, fake pilot with GRG Soldier
- can be disabled by seaCSAR = false
2.0.0 - OOP, code clean-up
2.0.1 - fix for coalition change when ejected player changes coas or is forced to neutral
- GC
--]]--
function autoCSAR.removeGuy(args)
@ -30,13 +32,16 @@ function autoCSAR.isOverWater(theUnit)
return surf == 2 or surf == 3
end
function autoCSAR.createNewCSAR(theUnit)
function autoCSAR.createNewCSAR(theUnit, coa)
if not csarManager then
trigger.action.outText("+++aCSAR: CSAR Manager not loaded, aborting", 30)
end
-- enter with unit from landing_after_eject event
-- unit has no group
local coa = theUnit:getCoalition()
if not coa then
trigger.action.outText("+++autoCSAR: unresolved coalition, assumed neutral", 30)
coa = 0
end
if coa == 0 then -- neutral
trigger.action.outText("Neutral Pilot made it safely to ground.", 30)
return
@ -75,7 +80,7 @@ function autoCSAR.createNewCSAR(theUnit)
theUnit = allUnits[1] -- get first (and only) unit
end
-- create a CSAR mission now
csarManager.createCSARForParachutist(theUnit, "Xray-" .. autoCSAR.counter)
csarManager.createCSARForParachutist(theUnit, "Xray-" .. autoCSAR.counter, coa)
autoCSAR.counter = autoCSAR.counter + 1
-- schedule removal of pilot
@ -88,37 +93,67 @@ function autoCSAR.createNewCSAR(theUnit)
end
end
-- we backtrack the pilot to their seat to their plane if they have ejector seat
autoCSAR.pilotInfo = {}
function autoCSAR:onEvent(event)
if not event.initiator then return end
local initiator = event.initiator
if event.id == 31 then -- landing_after_eject, does not happen at sea
-- to prevent double invocations for same process
-- check that we are still tracking this ejection
if event.initiator then
local uid = tonumber(event.initiator:getID())
if autoCSAR.trackedEjects[uid] then
trigger.action.outText("aCSAR: filtered double sea csar (player) event for uid = <" .. uid .. ">", 30)
autoCSAR.trackedEjects[uid] = nil -- reset
return
end
autoCSAR.createNewCSAR(event.initiator)
local uid = tonumber(initiator:getID())
if autoCSAR.trackedEjects[uid] then
trigger.action.outText("aCSAR: filtered double sea csar (player) event for uid = <" .. uid .. ">", 30)
autoCSAR.trackedEjects[uid] = nil -- reset
return
end
-- now get the coalition of the pilot.
-- if pilot had an ejection seat, we need to get the seat's coa
local coa = initiator:getCoalition()
for idx, info in pairs(autoCSAR.pilotInfo) do
if info.pilot == initiator then
coa = info.coa
info.matched = true -- for GC
end
end
autoCSAR.createNewCSAR(initiator, coa)
end
if event.id == 6 and autoCSAR.seaCSAR then -- eject, start tracking
if event.initiator then
if event.id == 33 then -- discard chair, connect pilot with seat
for idx, info in pairs(autoCSAR.pilotInfo) do
if info.seat == event.target then
info.pilot = initiator
end
end
end
if event.id == 6 then -- eject, start tracking, remember coa
local coa = event.initiator:getCoalition()
-- see if pilot has ejector seat and prepare to connect one with the other
local info = nil
if event.target and event.target:isExist() then
info = {}
info.coa = coa
info.seat = event.target
table.insert(autoCSAR.pilotInfo, info)
end
local uid = tonumber(event.initiator:getID())
autoCSAR.trackedEjects[uid] = nil -- set to not handled (yet)
if autoCSAR.seaCSAR then
-- see if this happened over open water and immediately
-- create a seaCSAR
if autoCSAR.isOverWater(event.initiator) then
autoCSAR.createNewCSAR(event.initiator)
-- create a seaCSAR immediately
if autoCSAR.isOverWater(initiator) then
autoCSAR.createNewCSAR(initiator, initiator:getCoalition())
-- mark this one as completed
autoCSAR.trackedEjects[uid] = "processed" -- remember, so to not proc again
if info then info.matched = true end -- discard this one too in next GC
end
-- also mark this one as completed
local uid = tonumber(event.initiator:getID())
autoCSAR.trackedEjects[uid] = "processed"
end
end
end
function autoCSAR.readConfigZone()
@ -147,6 +182,19 @@ function autoCSAR.readConfigZone()
end
end
function autoCSAR.GC()
timer.scheduleFunction(autoCSAR.GC, {}, timer.getTime() + 30 * 60) -- once every half hour
local filtered = {}
for idx, info in pairs(autoCSAR.pilotInfo) do
if info.matched then
-- skip it for next round
else
table.insert(filtered, info)
end
end
autoCSAR.pilotInfo = filtered
end
function autoCSAR.start()
-- lib check
if not dcsCommon.libCheck then
@ -163,6 +211,9 @@ function autoCSAR.start()
-- connect event handler
world.addEventHandler(autoCSAR)
-- start GC
timer.scheduleFunction(autoCSAR.GC, {}, timer.getTime() + 1)
trigger.action.outText("cfx autoCSAR v" .. autoCSAR.version .. " started.", 30)
return true
end

105
modules/bank.lua Normal file
View File

@ -0,0 +1,105 @@
bank = {}
bank.version = "0.0.0"
bank.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
bank.acts = {}
function bank.addFunds(act, amt)
if not act then act = "!!NIL!!" end
if act == 1 then act = "red" end
if act == 2 then act = "blue" end
if act == 0 then act = "neutral" end
act = string.lower(act)
local curVal = bank.acts[act]
if not curVal then
trigger.action.outText("+++Bank: no account <" .. act .. "> found. No transaction", 30)
return false
end
bank.acts[act] = curVal + amt
return true
end
function bank.withdawFunds(act, amt)
if not act then act = "!!NIL!!" end
if act == 1 then act = "red" end
if act == 2 then act = "blue" end
if act == 0 then act = "neutral" end
act = string.lower(act)
local curVal = bank.acts[act]
if not curVal then
trigger.action.outText("+++Bank: no account <" .. act .. "> found. No transaction", 30)
return false
end
if amt > curVal then return false end
bank.acts[act] = curVal - amt
return true
end
function bank.getBalance(act)
if not act then act = "!!NIL!!" end
if act == 1 then act = "red" end
if act == 2 then act = "blue" end
if act == 0 then act = "neutral" end
act = string.lower(act)
local curVal = bank.acts[act]
if not curVal then
trigger.action.outText("+++Bank: no account <" .. act .. "> found. No transaction", 30)
return false, 0
end
return true, curVal
end
function bank.openAccount(act, amount)
if not amount then amount = 0 end
if bank.acts[act] then return false end -- account exists
bank.acts[act] = amount
return true
end
function bank.readConfigZone()
local theZone = cfxZones.getZoneByName("bankConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("bankConfig")
end
-- set initial balances
bank.red = theZone:getNumberFromZoneProperty ("red", 1000)
bank.blue = theZone:getNumberFromZoneProperty ("blue", 1000)
bank.neutral = theZone:getNumberFromZoneProperty ("neutral", 1000)
bank.acts["red"] = bank.red
bank.acts["blue"] = bank.blue
bank.acts["neutral"] = bank.neutral
bank.verbose = theZone.verbose
end
function bank.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("bank requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("bank", bank.requiredLibs) then
return false
end
-- read config
bank.readConfigZone()
trigger.action.outText("bank v" .. bank.version .. " started.", 30)
return true
end
if not bank.start() then
trigger.action.outText("bank aborted: missing libraries", 30)
bank = nil
end

467
modules/camp.lua Normal file
View File

@ -0,0 +1,467 @@
camp = {}
camp.ups = 1
camp.version = "0.0.0"
camp.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
"cfxMX",
"bank"
}
--
-- CURRENTLY REQUIRES SINGLE-UNIT PLAYER GROUPS
--
camp.camps = {} -- all camps on the map
camp.roots = {} -- all player group comms roots
function camp.addCamp(theZone)
camp.camps[theZone.name] = theZone
end
function camp.getMyCurrentCamp(theUnit) -- returns first hit plaayer is in
local p = theUnit:getPoint()
for idx, theCamp in pairs(camp.camps) do
if theCamp:pointInZone(p) then
return theCamp
end
end
return nil
end
function camp.createCampWithZone(theZone)
-- look for all cloners inside my zone
if theZone.verbose or camp.verbose then
trigger.action.outText("+++camp: processing <" .. theZone.name .. ">, owner is <" .. theZone.owner .. ">", 30)
end
local allZones = cfxZones.getAllZonesInsideZone(theZone)
local cloners = {}
local redCloners = {}
local blueCloners = {}
for idx, aZone in pairs(allZones) do
if aZone:hasProperty("nocamp") then
-- this zone cannot be part of a camp
elseif aZone:hasProperty("cloner") then
-- this is a clone zone and part of my camp
table.insert(cloners, aZone)
if not aZone:hasProperty("blueOnly") then
table.insert(redCloners, aZone)
end
if not aZone:hasProperty("redOnly") then
table.insert(blueCloners, aZone)
end
if theZone.verbose or camp.verbose then
trigger.action.outText("Cloner <" .. aZone.name .. "> is part of camp <" .. theZone.name .. ">", 30)
end
end
end
if #cloners < 1 then
trigger.action.outText("+++camp: warning: camp <" .. theZone.name .. "> has no cloners, can't be improved or repaired", 30)
else
if camp.verbose or theZone.verbose then
trigger.action.outText("Camp <" .. theZone.name .. ">: <" .. #cloners .. "> reinforcable points, <" .. #redCloners .. "> for red and <" .. #blueCloners .. "> blue", 30)
end
end
theZone.cloners = cloners
theZone.redCloners = redCloners
theZone.blueCloners = blueCloners
theZone.repairable = theZone:getBoolFromZoneProperty("repair", true)
theZone.upgradable = theZone:getBoolFromZoneProperty("upgrade", true)
theZone.repairCost = theZone:getNumberFromZoneProperty("repairCost", 100)
theZone.upgradeCost = theZone:getNumberFromZoneProperty("upgradeCost", 3 * theZone.repairCost)
end
--
-- update and event
--
function camp.update()
-- call me in a second to poll triggers
timer.scheduleFunction(camp.update, {}, timer.getTime() + 1/camp.ups)
end
function camp:onEvent(theEvent)
end
--
-- Comms
--
function camp.processPlayers()
-- install coms stump for all players. they will be switched in/out
-- whenever it is apropriate
for idx, gData in pairs(cfxMX.playerGroupByName) do
gID = gData.groupId
gName = gData.name
local theRoot = missionCommands.addSubMenuForGroup(gID, "Ground Repairs / Upgrades")
camp.roots[gName] = theRoot
local c00 = missionCommands.addCommandForGroup(gID, "Theatre Overview", theRoot, camp.redirectTFunds, {gName, gID, "tfunds"})
local c0 = missionCommands.addCommandForGroup(gID, "Local Funds & Status Overview", theRoot, camp.redirectFunds, {gName, gID, "funds"})
local c1 = missionCommands.addCommandForGroup(gID, "REPAIRS: Purchase local repairs", theRoot, camp.redirectRepairs, {gName, gID, "repair"})
local c2 = missionCommands.addCommandForGroup(gID, "UPGRADE: Purchase local upgrades", theRoot, camp.redirectUpgrades, {gName, gID, "upgrade"})
end
end
function camp.redirectRepairs(args)
timer.scheduleFunction(camp.doRepairs, args, timer.getTime() + 0.1)
end
function camp.redirectUpgrades(args)
timer.scheduleFunction(camp.doUpgrades, args, timer.getTime() + 0.1)
end
function camp.redirectTFunds(args)
timer.scheduleFunction(camp.doTFunds, args, timer.getTime() + 0.1 )
end
function camp.redirectFunds(args)
timer.scheduleFunction(camp.doFunds, args, timer.getTime() + 0.1 )
end
function camp.doTFunds(args)
local gName = args[1]
local gID = args[2]
local theGroup = Group.getByName(gName)
local coa = theGroup:getCoalition()
local hasBalance, amount = bank.getBalance(coa)
if not hasBalance then return end
local msg = "\nYour faction currently has §" .. amount .. " available for repairs/upgrades.\n"
-- now iterate all camps that are on my side
for idx, theZone in pairs(camp.camps) do
if theZone.owner == coa then
msg = msg .. "\n - <" .. theZone.name .. ">"
if theZone.repairable and theZone.upgradable then
msg = msg .. "" .. theZone.repairCost .. "" .. theZone.upgradeCost .. ")"
if camp.zoneNeedsRepairs(theZone, coa) then
msg = msg .. " requests repairs and"
else
msg = msg .. " is running and"
end
if camp.zoneNeedsUpgrades(theZone, coa) then
msg = msg .. " can be upgraded"
else
msg = msg .. " is fully upgraded"
end
elseif theZone.repairable then
if camp.zoneNeedsRepairs(theZone, coa) then
msg = msg .. " needs repairs (§" .. theZone.repairCost .. ")"
else
msg = msg .. " is fully operational"
end
elseif theZone.upgradable then
if camp.zoneNeedsUpgrades(theZone, coa) then
msg = msg .. " can be upgraded (§" .. theZone.upgradeCost .. ")"
else
msg = msg .. " is fully upgraded"
end
else
-- can be neither repaired nor upgraded
msg = msg .. " is owned"
end
end
end
msg = msg .. "\n"
trigger.action.outTextForGroup(gID, msg, 30)
end
function camp.doFunds(args)
local gName = args[1]
local gID = args[2]
local theGroup = Group.getByName(gName)
local coa = theGroup:getCoalition()
local hasBalance, amount = bank.getBalance(coa)
if not hasBalance then return end
local msg = "\nYour faction currently has §" .. amount .. " available for repairs/upgrades.\n"
local allUnits = theGroup:getUnits()
local theUnit = allUnits[1] -- always first unit until we get playerCommands
if not Unit.isExist(theUnit) or theUnit:getLife() < 1 or
theUnit:inAir() or dcsCommon.getUnitSpeed(theUnit) > 1 then
trigger.action.outTextForGroup(gID, msg, 30)
return
end
local theZone = camp.getMyCurrentCamp(theUnit)
if not theZone or (not theZone.repairable) or theZone.owner ~= theUnit:getCoalition() then
trigger.action.outTextForGroup(gID, msg, 30)
return
end
if camp.zoneNeedsRepairs(theZone, coa) then
msg = msg .. "\nZone <" .. theZone.name .. "> needs repairs (§" .. theZone.repairCost .. " per repair)\n"
elseif theZone.repairable then
msg = msg .. "\nZone <" .. theZone.name .. "> has no outstanding repairs.\n"
else
-- say nothing
end
if camp.zoneNeedsUpgrades(theZone, coa) then
msg = msg .. "\nZone <" .. theZone.name .. "> can be upgraded (§" .. theZone.upgradeCost .. " per upgrade)\n"
elseif theZone.upgradable then
msg = msg .. "\nZone <" .. theZone.name .. "> is fully upgraded.\n"
end
trigger.action.outTextForGroup(gID, msg, 30)
end
--
-- REPAIRS
--
function camp.zoneNeedsRepairs(theZone, coa)
-- return true if this zone needs repairs, i.e. it has cloners that have a damaged clone set
local myCloners = theZone.cloners
if not coa then
trigger.action.outText("+++camp: warning: no coa on zoneNeedsRepair for zone <" .. theZone.name .. ">", 30)
elseif coa == 1 then
myCloners = theZone.redCloners
elseif coa == 2 then
myCloners = theZone.blueCloners
end
if not theZone.repairable then return nil end
for idx, theCloner in pairs(myCloners) do
if theCloner.oSize and theCloner.oSize > 0 then
local currSize = cloneZones.countLiveAIUnits(theCloner)
if currSize > 0 and currSize < theCloner.oSize then
if theZone.verbose then
trigger.action.outText("+++camp: camp <" .. theZone.name .. "> has point <" .. theCloner.name .. "> that needs repair.", 30)
end
return theCloner
else
end
end
end
return nil
end
function camp.doRepairs(args)
local gName = args[1]
local gID = args[2]
local theGroup = Group.getByName(gName)
local coa = theGroup:getCoalition()
local allUnits = theGroup:getUnits()
local theUnit = allUnits[1] -- always first unit until we get playerCommands
if not Unit.isExist(theUnit) then return end
local pName = "<Error>"
if theUnit.getPlayerName then pName = theUnit:getPlayerName() end
if not pName then pName = "<Big Err>" end
if theUnit:getLife() < 1 then return end
if theUnit:inAir() then
trigger.action.outTextForGroup(gID, "\nPlease land inside a fortified zone to order repairs\n", 30)
return
end
if dcsCommon.getUnitSpeed(theUnit) > 1 then
trigger.action.outTextForGroup(gID, "\nYou must come to a complete stop before being able to order repairs\n", 30)
return
end
local theZone = camp.getMyCurrentCamp(theUnit)
if not theZone or not theZone.repairable then
trigger.action.outTextForGroup(gID, "\nYou are not inside a zone that can be repaired.\n", 30)
return
end
if theZone.owner ~= theUnit:getCoalition() then
trigger.action.outTextForGroup(gID, "\nYou currently do not own zone <" .. theZone.name .. ">. Capture it first.\n", 30)
return
end
-- if we get here, we are inside a zone that can be repaired. see if it needs repair and then get repair cost and see if we have enough fund to repair
if not camp.zoneNeedsRepairs(theZone, coa) then
local msg = "\nZone <" .. theZone.name .. "> is already fully repaired.\n"
if camp.zoneNeedsUpgrades(theZone, coa) then
msg = msg .. "\nZone <" .. theZone.name .. "> can be upgraded.\n"
end
trigger.action.outTextForGroup(gID, msg, 30)
return
end
-- see if we have enough funds
local hasBalance, amount = bank.getBalance(coa)
if not hasBalance then
trigger.action.outText("+++camp: no balance for upgrade!", 30)
return
end
if amount < theZone.repairCost then
trigger.action.outTextForGroup(gID, "\nYou curently cannot afford repairs here\n", 30)
return
end
-- finally, let's repair
camp.repairZone(theZone, coa)
-- theCloner = camp.zoneNeedsRepairs(theZone)
-- cloneZones.despawnAll(theCloner)
-- cloneZones.spawnWithCloner(theCloner)
bank.withdawFunds(coa, theZone.repairCost)
local ignore, remain = bank.getBalance(coa)
trigger.action.outTextForCoalition(coa, "\nZone <" .. theZone.name .. "> was repaired by <" .. pName ..
"> for §" .. theZone.repairCost .. ".\nFaction has §" .. remain .. " remaining funds.\n", 30)
end
function camp.repairZone(theZone, coa)
theCloner = camp.zoneNeedsRepairs(theZone, coa)
if not theCloner then return end
cloneZones.despawnAll(theCloner)
cloneZones.spawnWithCloner(theCloner)
end
--
-- UPGRADES
--
function camp.zoneNeedsUpgrades(theZone, coa)
-- return true if this zone can be upgraded, i.e. it has cloners that have an empty clone set
if not theZone.upgradable then return nil end
local myCloners = theZone.cloners
if not coa then
trigger.action.outText("+++camp: warning: no coa on zoneNeedsUpgrades for zone <" .. theZone.name .. ">", 30)
elseif coa == 1 then
myCloners = theZone.redCloners
elseif coa == 2 then
myCloners = theZone.blueCloners
end
for idx, theCloner in pairs(myCloners) do
local currSize = cloneZones.countLiveAIUnits(theCloner)
if currSize < 1 then
if theZone.verbose then
trigger.action.outText("+++camp: camp <" .. theZone.name .. "> has point <" .. theCloner.name .. "> that can be upgraded.", 30)
end
return theCloner
else
end
end
return nil
end
function camp.doUpgrades(args)
local gName = args[1]
local gID = args[2]
local theGroup = Group.getByName(gName)
local coa = theGroup:getCoalition()
local allUnits = theGroup:getUnits()
local theUnit = allUnits[1] -- always first unit until we get playerCommands
if not Unit.isExist(theUnit) then return end
if theUnit:getLife() < 1 then return end
local pName = "<Error>"
if theUnit.getPlayerName then pName = theUnit:getPlayerName() end
if not pName then pName = "<Big Err>" end
if theUnit:inAir() then
trigger.action.outTextForGroup(gID, "\nPlease land inside a fortified zone to order upgrades.\n", 30)
return
end
if dcsCommon.getUnitSpeed(theUnit) > 1 then
trigger.action.outTextForGroup(gID, "\nYou must come to a complete stop before being able to order upgrades\n", 30)
return
end
local theZone = camp.getMyCurrentCamp(theUnit)
if not theZone or not theZone.upgradable then
trigger.action.outTextForGroup(gID, "\nYou are not inside a zone that can be upgraded.\n", 30)
return
end
if theZone.owner ~= theUnit:getCoalition() then
trigger.action.outTextForGroup(gID, "\nYou currently do not own zone <" .. theZone.name .. ">. Capture it first.\n", 30)
return
end
if camp.zoneNeedsRepairs(theZone, coa) then
trigger.action.outTextForGroup(gID, "\nZone <" .. theZone.name .. "> requires repairs before it can be upgraded.\n", 30)
return
end
-- if we get here, we are inside a zone that can be upgraded. see if it needs upgrades and then get upgrade cost and see if we have enough fund to do it
if not camp.zoneNeedsUpgrades(theZone, coa) then
trigger.action.outTextForGroup(gID, "\nZone <" .. theZone.name .. "> has been fully upgraded.\n", 30)
return
end
-- see if we have enough funds
local hasBalance, amount = bank.getBalance(coa)
if not hasBalance then
trigger.action.outText("+++camp: no balance for upgrade!", 30)
return
end
if amount < theZone.upgradeCost then
trigger.action.outTextForGroup(gID, "\nYou curently cannot afford an upgrade here\n", 30)
return
end
-- finally, let's upgrade
--theCloner = camp.zoneNeedsUpgrades(theZone)
--cloneZones.spawnWithCloner(theCloner)
camp.upgradeZone(theZone, coa)
-- bill it to side
bank.withdawFunds(coa, theZone.upgradeCost)
local ignore, remain = bank.getBalance(coa)
trigger.action.outTextForCoalition(coa, "\nZone <" .. theZone.name .. "> was upgraded by <" .. pName ..
"> for §" .. theZone.upgradeCost .. ".\nFaction has §" .. remain .. " remaining funds.\n", 30)
end
-- can be called externally
function camp.upgradeZone(theZone, coa)
theCloner = camp.zoneNeedsUpgrades(theZone, coa)
if not theCloner then return end
cloneZones.spawnWithCloner(theCloner)
end
--
-- Config & Go
--
function camp.readConfigZone()
local theZone = cfxZones.getZoneByName("campConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("campConfig")
end
camp.verbose = theZone.verbose
end
function camp.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("camp requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("camp", camp.requiredLibs) then
return false
end
-- read config
camp.readConfigZone()
-- read zones
local attrZones = cfxZones.getZonesWithAttributeNamed("camp")
for k, aZone in pairs(attrZones) do
camp.createCampWithZone(aZone) -- process attributes
camp.addCamp(aZone) -- add to list
end
-- process all players
camp.processPlayers()
-- start update
camp.update()
-- connect event handler
world.addEventHandler(camp)
trigger.action.outText("camp v" .. camp.version .. " started.", 30)
return true
end
if not camp.start() then
trigger.action.outText("camp aborted: missing libraries", 30)
camp = nil
end
--[[--
Ideas:
re-supply: will restore REMAINING units at all points with fresh
units so that they can have full mags
costs as much as a full upgrade? hald way between upgrade and repair
--]]--

View File

@ -48,7 +48,8 @@ cfxZones.version = "4.3.1"
- 4.3.0 - boolean supports maybe, random, rnd, ?
- small optimization for randomInRange()
- randomDelayFromPositiveRange also allows 0
- 4.3.1 - new drawText() for zones
- dmlZones:getClosestZone() bridge
--]]--
--
@ -1208,6 +1209,11 @@ function cfxZones.getClosestZone(point, theZones)
return closestZone, currDelta
end
function dmlZones:getClosestZone(theZones)
local closestZone, currDelta = cfxZones.getClosestZone(self:getPoint(), theZones)
return closestZone, currDelta
end
-- return a random zone from the table passed in zones
function cfxZones.pickRandomZoneFrom(zones)
if not zones then zones = cfxZones.zones end
@ -2108,6 +2114,21 @@ function dmlZone:drawZone(lineColor, fillColor, markID)
return cfxZones.drawZone(self, lineColor, fillColor, markID)
end
function cfxZones.drawText(theZone, theText, fSize, lineColor, fillColor)
if not theZone then return end
if not fSize then fSize = 12 end
if not lineColor then lineColor = {0.8, 0.8, 0.8, 1.0} end
if not fillColor then fillColor = lineColor end
local markID = dcsCommon.numberUUID()
local p = theZone:getPoint()
local offset = {x = p.x, y = 0, z = p.z}
trigger.action.textToAll(-1, markID, offset, lineColor , fillColor , fSize, true , theText)
return markID
end
function dmlZone:drawText(theText, fSize, lineColor, fillColor)
return cfxZones.drawText(self, theText, fSize, lineColor, fillColor)
end
--
-- ===================
-- PROPERTY PROCESSING

View File

@ -1,5 +1,5 @@
cloneZones = {}
cloneZones.version = "2.1.0"
cloneZones.version = "2.2.0"
cloneZones.verbose = false
cloneZones.requiredLibs = {
"dcsCommon", -- always
@ -44,6 +44,12 @@ cloneZones.respawnOnGroupID = true
when pre-wipe is active
2.1.0 - despawnIn option
- inBuiltup option for rndLoc
2.2.0 - oSize
- countLiveUnits() performace optimization
- new countLiveAIUnits()
- damaged! output
- health# output
- persistence: persist oSize and set lastSize
--]]--
--
@ -337,7 +343,16 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner"
if theZone:hasProperty("despawnIn") then
theZone.despawnInMin, theZone.despawnInMax = theZone:getPositiveRangeFromZoneProperty("despawnIn", 2,2)
end
-- damaged and health interface
if theZone:hasProperty("damaged!") then
theZone.damaged = theZone:getStringFromZoneProperty("damaged!")
end
if theZone:hasProperty("health#") then
theZone.health = theZone:getStringFromZoneProperty("health#")
end
-- we end with clear plate
theZone.lastSize = 0 -- no units here
end
--
@ -348,6 +363,7 @@ function cloneZones.despawnAll(theZone)
if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: despawn all - wiping zone <" .. theZone.name .. ">", 30)
end
theZone.oSize = 0 -- original spawn size
for idx, aGroup in pairs(theZone.mySpawns) do
if aGroup:isExist() then
if theZone.verbose then
@ -1017,7 +1033,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
trigger.action.outText("+++clnZ: spawning with template <" .. theZone.name .. "> for spawner <" .. spawnZone.name .. ">", 30)
end
-- theZone is the cloner with the TEMPLATE (source)
-- spawnZone is the spawner with SETTINGS and DESTINATION (target location) where the clones are poofed into existence
-- spawnZone is the actual spawner with SETTINGS and DESTINATION (target location) where the clones are poofed into existence
local newCenter = spawnZone:getPoint() -- includes zone following updates
local oCenter = theZone:getDCSOrigin() -- get original coords on map for cloning offsets
-- calculate zoneDelta, is added to all vectors
@ -1190,6 +1206,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
cloneZones.resolveReferences(theZone, dataToSpawn)
-- now spawn all raw data
spawnZone.oSize = 0 -- original size reset
local groupCollector = {} -- to detect cross-group conflicts
local unitCollector = {} -- to detect cross-group conflicts
local theGroup = nil -- init to empty, on this level
@ -1219,6 +1236,8 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
-- SPAWN NOW!!!!
theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData)
table.insert(spawnedGroups, theGroup)
-- increment oSize by number of spawns
spawnZone.oSize = spawnZone.oSize + theGroup:getSize()
-- see if this is an auto-despawner
if spawnZone.despawnInMin then
@ -1465,6 +1484,8 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
end
end
end
-- reset lastSize to oSize
spawnZone.lastSize = spawnZone.oSize
local args = {}
args.groups = spawnedGroups
args.statics = spawnedStatics
@ -1593,19 +1614,30 @@ function cloneZones.doClone(args)
end
end
function cloneZones.countLiveAIUnits(theZone)
-- like countLiveUnits, but disregards statics
if not theZone then return 0 end
local count = 0
if not theZone.mySpawns then return 0 end
-- count units
if theZone.mySpawns then
for idx, aGroup in pairs(theZone.mySpawns) do
if Group.isExist(aGroup) then
count = count + aGroup:getSize()
end
end
end
return count
end
function cloneZones.countLiveUnits(theZone)
if not theZone then return 0 end
local count = 0
-- count units
if theZone.mySpawns then
for idx, aGroup in pairs(theZone.mySpawns) do
if aGroup:isExist() then
local allUnits = aGroup:getUnits()
for idy, aUnit in pairs(allUnits) do
if aUnit:isExist() and aUnit:getLife() >= 1 then
count = count + 1
end
end
if Group.isExist(aGroup) then --aGroup:isExist() then
count = count + aGroup:getSize()
end
end
end
@ -1715,6 +1747,28 @@ function cloneZones.update()
-- can mess with empty, so we tell empty to skip
end
-- handling of damaged! and #health
if aZone.damaged or aZone.health then
-- calculate current health
local currSize = cloneZones.countLiveAIUnits(aZone)
if aZone.oSize < 1 then
if aZone.verbose or cloneZones.verbose then
trigger.action.outText("+++clnZ: Warning: zero oZize for cloner <" .. aZone.name .. ">, no health info, no damage alert", 30)
end
else
local percent = math.floor(currSize * 100 / aZone.oSize)
if aZone.health then
aZone:setFlagValue(aZone.health, percent)
end
if aZone.lastSize > currSize then
if aZone.damaged then
aZone:pollFlag(aZone.damaged, aZone.cloneMethod)
end
end
end
aZone.lastSize = currSize
end
-- empty handling
local isEmpty = cloneZones.countLiveUnits(aZone) < 1 and aZone.hasClones
if isEmpty and (willSpawn == false) then
@ -1885,7 +1939,8 @@ function cloneZones.saveData()
local cData = {}
local cName = theCloner.name
cData.myUniqueCounter = theCloner.myUniqueCounter
cData.oSize = theCloner.oSize
cData.lastSize = theCloner.lastSize
-- mySpawns: all groups i'm curently observing for empty!
-- myStatics: dto for objects
local mySpawns = {}
@ -1980,6 +2035,8 @@ function cloneZones.loadData()
if cData.myUniqueCounter then
theCloner.myUniqueCounter = cData.myUniqueCounter
end
if cData.oSize then theCloner.oSize = cData.oSize end
if cData.lastSize then theCloner.lastSize = cData.lastSize end
local mySpawns = {}
for idx, aName in pairs(cData.mySpawns) do

View File

@ -1,5 +1,5 @@
csarManager = {}
csarManager.version = "3.2.5"
csarManager.version = "3.2.7"
csarManager.ups = 1
--[[-- VERSION HISTORY
@ -41,6 +41,7 @@ csarManager.ups = 1
3.2.5 - smoke callbacks
- useRanks option
3.2.6 - inBuiltup analogon to cloner
3.2.7 - createCSARForParachutist now supports optional coa (autoCSAR)
@ -1330,8 +1331,8 @@ function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent, score
end
end
function csarManager.createCSARForParachutist(theUnit, name) -- invoked with parachute guy on ground as theUnit
local coa = theUnit:getCoalition()
function csarManager.createCSARForParachutist(theUnit, name, coa) -- invoked with parachute guy on ground as theUnit
if not coa then coa = theUnit:getCoalition() end
local pos = theUnit:getPoint()
-- unit DOES NOT HAVE GROUP!!! (unless water splashdown)
-- create a CSAR mission now

View File

@ -16,12 +16,13 @@ dcsCommon.version = "3.0.5"
3.0.4 - getGroupLocation() hardened, optional verbose
3.0.5 - new getNthItem()
- new getFirstItem()
- arrayContainsString() can handle dicts
- new pointXpercentYdegOffAB()
--]]--
-- dcsCommon is a library of common lua functions
-- for easy access and simple mission programming
-- (c) 2021 - 2023 by Chritian Franz and cf/x AG
-- (c) 2021 - 2024 by Christian Franz and cf/x AG
dcsCommon.verbose = false -- set to true to see debug messages. Lots of them
dcsCommon.uuidStr = "uuid-"
@ -1830,7 +1831,7 @@ dcsCommon.version = "3.0.5"
end;
function dcsCommon.pointInDirectionOfPointXYY(dir, dist, p) -- dir in rad, p in XYZ returns XYY
function dcsCommon.pointInDirectionOfPointXYY(dir, dist, p) -- dir in rad, p in XYZ returns XZZ
local fx = math.cos(dir)
local fy = math.sin(dir)
local p2 = {}
@ -1840,6 +1841,15 @@ dcsCommon.version = "3.0.5"
return p2
end
function dcsCommon.pointXpercentYdegOffAB(A, B, xPer, yDeg) -- rets xzz point
local bearingRad = dcsCommon.bearingFromAtoB(A, B)
local dist = dcsCommon.dist(A, B)
local deviation = bearingRad + yDeg * 0.0174533
local newDist = dist * xPer/100
local newPoint = dcsCommon.pointInDirectionOfPointXYY(deviation, newDist, A)
return newPoint
end
function dcsCommon.rotatePointAroundOriginRad(inX, inY, angle) -- angle in degrees
local c = math.cos(angle)
local s = math.sin(angle)
@ -2078,7 +2088,7 @@ end
if not theString then return false end
if not caseSensitive then caseSensitive = false end
if type(theArray) ~= "table" then
trigger.action.outText("***arrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30)
trigger.action.outText("***wildArrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30)
end
if not caseSensitive then theString = string.upper(theString) end
@ -2114,10 +2124,11 @@ end
if not theArray then return false end
if not theString then return false end
if type(theArray) ~= "table" then
trigger.action.outText("***arrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30)
trigger.action.outText("***arrayContainsString: theArray is not type <table> but <" .. type(theArray) .. ">", 30)
end
for i = 1, #theArray do
if theArray[i] == theString then return true end
for idx, item in pairs(theArray) do
-- for i = 1, #theArray do
if item == theString then return true end
end
return false
end
@ -2418,7 +2429,7 @@ end
if getmetatable(value) then
if type(value) == "string" then
else
trigger.action.outText(prefix .. key (" .. type(value) .. ") .. " HAS META", 30)
trigger.action.outText(prefix .. key .. (" .. type(value) .. ") .. " HAS META", 30)
end
end
if type(value) == "table" then

View File

@ -1,5 +1,5 @@
factoryZone = {}
factoryZone.version = "3.1.1"
factoryZone.version = "3.1.2"
factoryZone.verbose = false
factoryZone.name = "factoryZone"
@ -18,6 +18,7 @@ factoryZone.name = "factoryZone"
- defendMe? attribute
- triggered 'shocked' mode via defendMe
3.1.1 - fixed a big with persistence
3.1.2 - fixed a verbosity bug
--]]--
factoryZone.requiredLibs = {
@ -253,14 +254,14 @@ function factoryZone.sendOutAttackers(aZone)
-- bang on xxxP!
if aZone.owner == 1 and aZone.redP then
if aZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: polling redP! <" .. aZone.redP .. "> for factrory <" .. aZone.name .. ">")
trigger.action.outText("+++factZ: polling redP! <" .. aZone.redP .. "> for factrory <" .. aZone.name .. ">", 30)
end
aZone:pollFlag(aZone.redP, aZone.factoryMethod)
end
if aZone.owner == 2 and aZone.blueP then
if aZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: polling blueP! <" .. aZone.blueP .. "> for factrory <" .. aZone.name .. ">")
trigger.action.outText("+++factZ: polling blueP! <" .. aZone.blueP .. "> for factrory <" .. aZone.name .. ">", 30)
end
aZone:pollFlag(aZone.blueP, aZone.factoryMethod)
end

View File

@ -1,5 +1,5 @@
cfxGroundTroops = {}
cfxGroundTroops.version = "2.0.0"
cfxGroundTroops.version = "2.0.1"
cfxGroundTroops.ups = 1
cfxGroundTroops.verbose = false
cfxGroundTroops.requiredLibs = {
@ -31,6 +31,8 @@ cfxGroundTroops.jtacCB = {} -- jtac callbacks, to be implemented
- jtacSound
- clanup
- jtacVerbose
2.0.1 - small fiex ti checkPileUp()
an entry into the deployed troop table has the following attributes
- group - the group
@ -755,7 +757,8 @@ function cfxGroundTroops.checkPileUp()
end
-- create a list of all piles
for idx, oz in pairs(cfxOwnedZones.zones) do
-- for idx, oz in pairs(cfxOwnedZones.zones) do
for idx, oz in pairs(cfxOwnedZones.allManagedOwnedZones) do
local newPile = {}
newPile[1] = 0 -- no red inZone here
newPile[2] = 0 -- no blue inZone here

View File

@ -1,5 +1,5 @@
cfxHeloTroops = {}
cfxHeloTroops.version = "3.0.2"
cfxHeloTroops.version = "3.0.3"
cfxHeloTroops.verbose = false
cfxHeloTroops.autoDrop = true
cfxHeloTroops.autoPickup = false
@ -39,6 +39,7 @@ cfxHeloTroops.requestRange = 500 -- meters
- requestRange attribute
3.0.1 - fixed a bug with legalTroops attribute
3.0.2 - fixed a typo in in-air menu
3.0.3 - pointInZone check for insertion rather than radius
--]]--
--
@ -585,7 +586,7 @@ function cfxHeloTroops.scoreWhenCapturing(theUnit)
local theGroup = theUnit:getGroup()
local ID = theGroup:getID()
local nearestZone, dist = cfxOwnedZones.getNearestOwnedZoneToPoint(p)
if nearestZone and dist < nearestZone.radius then
if nearestZone and nearestZone:pointInZone(p) then -- dist < nearestZone.radius then
-- we are inside an owned zone!
if nearestZone.owner ~= coa then
-- yup, combat drop!

110
modules/income.lua Normal file
View File

@ -0,0 +1,110 @@
income = {}
income.version = "0.0.0"
income.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
"bank"
}
income.sources = {}
function income.addIncomeZone(theZone)
income.sources[theZone.name] = theZone
end
function income.createIncomeWithZone(theZone)
theZone.income = theZone:getNumberFromZoneProperty("income")
-- we may add enablers and prohibitors and shared income
-- for example a building or upgrade must exist in order
-- to provide income
end
function income.getIncomeForZoneAndCoa(theZone, coa)
-- process this zone's status (see which upgrades exist)
-- and return the amount of income for this zone and coa
-- currently very primitive: own it, get it
if theZone.owner == coa then
return theZone.income
else
return 0
end
end
function income.update()
-- schedule next round
timer.scheduleFunction(income.update, {}, timer.getTime() + income.interval)
-- base income
bank.addFunds(0, income.neutral)
bank.addFunds(1, income.red)
bank.addFunds(2, income.blue)
for idx, theZone in pairs(income.sources) do
bank.addFunds(0, income.getIncomeForZoneAndCoa(theZone, 0))
bank.addFunds(1, income.getIncomeForZoneAndCoa(theZone, 1))
bank.addFunds(2, income.getIncomeForZoneAndCoa(theZone, 2))
end
if income.announceTicks then
-- trigger.action.outText(income.tickMessage, 30)
local has, balance = bank.getBalance(0)
trigger.action.outTextForCoalition(0, "\n" .. income.tickMessage .. "\nNew balance: §" .. balance .. "\n", 30)
has, balance = bank.getBalance(1)
trigger.action.outTextForCoalition(1, "\n" .. income.tickMessage .. "\nNew balance: §" .. balance .. "\n", 30)
has, balance = bank.getBalance(2)
trigger.action.outTextForCoalition(2, "\n" .. income.tickMessage .. "\nNew balance: §" .. balance .. "\n", 30)
end
end
function income.readConfigZone()
local theZone = cfxZones.getZoneByName("incomeConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("incomeConfig")
end
income.base = theZone:getNumberFromZoneProperty ("base", 10)
income.red = theZone:getNumberFromZoneProperty ("red", income.base)
income.blue = theZone:getNumberFromZoneProperty ("blue", income.base)
income.neutral = theZone:getNumberFromZoneProperty ("neutral", income.base)
income.interval = theZone:getNumberFromZoneProperty("interval", 10 * 60) -- every 10 minutes
income.tickMessage = theZone:getStringFromZoneProperty("tickMessage", "New funds from income available.")
income.announceTicks = theZone:getBoolFromZoneProperty("announceTicks", true)
income.verbose = theZone.verbose
end
function income.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("income requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("income", income.requiredLibs) then
return false
end
-- read config
income.readConfigZone()
-- read income zones
local attrZones = cfxZones.getZonesWithAttributeNamed("income")
for k, aZone in pairs(attrZones) do
income.createIncomeWithZone(aZone) -- process attributes
income.addIncomeZone(aZone) -- add to list
end
-- schedule first tick
timer.scheduleFunction(income.update, {}, timer.getTime() + income.interval)
trigger.action.outText("income v" .. income.version .. " started.", 30)
return true
end
if not income.start() then
trigger.action.outText("income aborted: missing libraries", 30)
income = nil
end

View File

@ -6,15 +6,30 @@ milHelo.requiredLibs = {
"cfxMX",
}
milHelo.zones = {}
milHelo.targetKeywords = {
"milTarget", -- my own
"camp", -- camps
"airfield", -- airfields
"FARP", -- FARPzones
}
milHelo.targets = {}
milHelo.flights = {} -- all currently active mil helo flights
milHelo.ups = 1
milHelo.missionTypes = {
"cas", -- standard cas
"patrol", -- orbit over zone for duration
"insert", -- insert one of the ground groups in the src zone after landing
"casz", -- engage in zone for target zone's radius
-- missing csar
}
function milHelo.addMilHeloZone(theZone)
milHelo.zones[theZone.name] = theZone
end
function milHelo.addMilTargetZone(theZone)
milHelo.targets[theZone.name] = theZone
milHelo.targets[theZone.name] = theZone -- overwrite if duplicate
end
function milHelo.partOfGroupDataInZone(theZone, theUnits) -- move to mx?
@ -51,15 +66,48 @@ end
function milHelo.readMilHeloZone(theZone) -- process attributes
-- get mission type. part of milHelo
theZone.msnType = string.lower(theZone:getStringFromZoneProperty("milHelo", "cas"))
theZone.msnType = string.lower(theZone:getStringFromZoneProperty("milHelo", "cas"))
if dcsCommon.arrayContainsString(milHelo.missionTypes, theZone.msnType) then
-- great, mission type is known
else
trigger.action.outText("+++milH: zone <" .. theZone.name .. ">: unknown mission type <" .. theZone.msnType .. ">, defaulting to 'CAS'", 30)
theZone.msnType = "cas"
end
-- get all groups inside me
local myGroups, count = milHelo.allGroupsInZoneByData(theZone)
theZone.myGroups = myGroups
theZone.groupCount = count
theZone.hGroups = {}
theZone.hCount = 0
theZone.gGroups = {}
theZone.gCount = 0
theZone.fGroups = {}
theZone.fCount = 0
-- sort into ground, helo and fixed
for groupName, data in pairs(myGroups) do
local catRaw = cfxMX.groupTypeByName[groupName]
if theZone.verbose then
trigger.action.outText("Proccing zone <" .. theZone.name .. ">: group <" .. groupName .. "> - type <" .. catRaw .. ">", 30)
end
if catRaw == "helicopter" then
theZone.hGroups[groupName] = data
theZone.hCount = theZone.hCount + 1
elseif catRaw == "plane" then
theZone.fGroups[groupName] = data
theZone.fCount = theZone.fCount + 1
elseif catRaw == "vehicle" then
theZone.gGroups[groupName] = data
theZone.gCount = theZone.gCount + 1
else
trigger.action.outText("+++milH: ignored group <" .. groupName .. ">: unknown type <" .. catRaw .. ">", 30)
end
end
theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0)
theZone.hot = theZone:getBoolFromZoneProperty("hot", true)
theZone.hot = theZone:getBoolFromZoneProperty("hot", false)
theZone.speed = theZone:getNumberFromZoneProperty("speed", 50) -- 110 mph
theZone.alt = theZone:getNumberFromZoneProperty("alt", 100) -- we are always radar alt
theZone.loiter = theZone:getNumberFromZoneProperty("loiter", 3600) -- 1 hour loiter default
-- wipe all existing
for groupName, data in pairs(myGroups) do
local g = Group.getByName(groupName)
@ -73,25 +121,24 @@ function milHelo.readMilHeloZone(theZone) -- process attributes
end
function milHelo.readMilTargetZone(theZone)
-- can also be "camp", "farp", "airfield"
theZone.casRadius = theZone:getNumberFromZoneProperty("casRadius", theZone.radius)
if (not theZone.isCircle) and not theZone:hasProperty("casRadius") then
-- often when we have a camp there is no cas radius, use 10km
-- and zone is ploygonal
if theZone.verbose then
trigger.action.outText("+++milH: Warning - milH target zone <" .. theZone.name .. "> is polygonal and has no CAS radius attribute. Defaulting to 10km", 30)
end
theZone.casRadius = 10000
end
if theZone.verbose or milHelo.verbose then
trigger.action.outText("+++milH: processed TARGET zone <" .. theZone.name .. ">", 30)
trigger.action.outText("+++milH: processed milHelo TARGET zone <" .. theZone.name .. ">", 30)
end
end
--
-- Spawning for a zone
--
--[[--
function milHelo.getNthItem(theSet, n)
local count = 1
for key, value in pairs(theSet) do
if count == n then return value end
count = count + 1
end
return nil
end
--]]--
function milHelo.createCASTask(num, auto)
if not auto then auto = false end
@ -104,7 +151,6 @@ function milHelo.createCASTask(num, auto)
task.auto = auto
local params = {}
params.priority = 0
-- params.targetTypes = {"Helicopters", "Ground Units", "Light armed ships"}
local targetTypes = {[1] = "Helicopters", [2] = "Ground Units", [3] = "Light armed ships",}
params.targetTypes = targetTypes
@ -132,6 +178,29 @@ function milHelo.createROETask(num, roe)
return task
end
function milHelo.createEngageIZTask(num, theZone)
local p = theZone:getPoint()
if not num then num = 1 end
local task = {}
task.number = num
task.enabled = true
task.auto = false
task.id = "EngageTargetsInZone"
local params = {}
targetTypes = {}
targetTypes[1] = "All"
params.targetTypes = targetTypes
params.x = p.x
params.y = p.z -- !!!!
params.value = "All;"
params.noTargetTypes = {}
params.priority = 0
local radius = theZone.casRadius
params.zoneRadius = radius
task.params = params
return task
end
function milHelo.createOrbitTask(num, duration, theZone)
if not num then num = 1 end
local task = {}
@ -155,23 +224,72 @@ function milHelo.createOrbitTask(num, duration, theZone)
return task
end
function milHelo.createTakeOffWP(theZone)
function milHelo.createLandTask(p, duration, num)
if not num then num = 1 end
local t = {}
t.enabled = true
t.auto = false
t.id = "ControlledTask"
t.number = num
local params = {}
t.params = params
local ptsk = {}
params.task = ptsk
ptsk.id = "Land"
local ptp = {}
ptsk.params = ptp
ptp.x = p.x
ptp.y = p.z
ptp.duration = "300" -- not sure why
ptp.durationFlag = false -- off anyway
local stopCon = {}
stopCon.duration = duration
params.stopCondition = stopCon
return t
end
function milHelo.createCommandTask(theCommand, num)
if not num then num = 1 end
local t = {}
t.enabled = true
t.auto = false
t.id = "WrappedAction"
t.number = num
local params = {}
t.params = params
local action = {}
params.action = action
action.id = "Script"
local p2 = {}
action.params = p2
p2.command = theCommand
return t
end
function milHelo.createTakeOffWP(theZone, engageInZone, engageZone)
local WP = {}
WP.alt = theZone.alt
WP.alt_type = "RADIO"
WP.alt = 500 -- theZone.alt
WP.alt_type = "BARO"
WP.properties = {}
WP.properties.addopt = {}
WP.action = "From Ground Area"
if theZone.hot then WP.action = "From Ground Area Hot" end
WP.speed = theZone.speed
WP.speed = 0 -- theZone.speed
WP.task = {}
WP.task.id = "ComboTask"
WP.task.params = {}
local tasks = {}
-- local casTask = milHelo.createCASTask(1)
-- tasks[1] = casTask
local roeTask = milHelo.createROETask(1,0) -- 0 = weapons free
tasks[1] = roeTask
local casTask = milHelo.createCASTask(1)
tasks[1] = casTask
local roeTask = milHelo.createROETask(2,0) -- 0 = weapons free
tasks[2] = roeTask
if engageInZone then
if not engageZone then
trigger.action.outText("+++milH: Warning - caz task with no engage zone!", 30)
end
local eiz = milHelo.createEngageIZTask(3, engageZone)
tasks[3] = eiz
end
WP.task.params.tasks = tasks
--
WP.type = "TakeOffGround"
@ -180,7 +298,7 @@ function milHelo.createTakeOffWP(theZone)
WP.x = p.x
WP.y = p.z
WP.ETA = 0
WP.ETA_locked = false
WP.ETA_locked = true
WP.speed_locked = true
WP.formation_template = ""
return WP
@ -201,9 +319,9 @@ function milHelo.createOrbitWP(theZone, targetPoint)
WP.task.params = {}
-- start params construct
local tasks = {}
local casTask = milHelo.createCASTask(1, false)
local casTask = milHelo.createCASTask(1)
tasks[1] = casTask
local oTask = milHelo.createOrbitTask(2, 3600, theZone)
local oTask = milHelo.createOrbitTask(2, theZone.loiter, theZone)
tasks[2] = oTask
WP.task.params.tasks = tasks
WP.type = "Turning Point"
@ -217,41 +335,123 @@ function milHelo.createOrbitWP(theZone, targetPoint)
return WP
end
function milHelo.createLandWP(gName, theZone, targetZone)
local toWP
toWP = dcsCommon.createSimpleRoutePointData(targetZone:getPoint(), theZone.alt, theZone.speed)
toWP.alt_type = "RADIO"
local task = {}
task.id = "ComboTask"
task.params = {}
local ttsk = {}
local p = targetZone:getPoint()
ttsk[1] = milHelo.createLandTask(p, milHelo.landingDuration, 1)
local command = "milHelo.landedCB('" .. gName .. "', '" .. targetZone:getName() .. "', '" .. theZone:getName() .. "')"
ttsk[2] = milHelo.createCommandTask(command,2)
task.params.tasks = ttsk
toWP.task = task
return toWP
end
function milHelo.createOMWCallbackWP(gName, number, pt, alt, speed, action) -- name is group name
if not action then action = "none" end
local omwWP = dcsCommon.createSimpleRoutePointData(pt, alt, speed)
omwWP.alt_type = "RADIO"
-- create a command waypoint
local task = {}
task.id = "ComboTask"
task.params = {}
local ttsk = {}
local command = "milHelo.reachedWP('" .. gName .. "', '" .. number .. "', '" .. action .."')"
ttsk[1] = milHelo.createCommandTask(command,1)
task.params.tasks = ttsk
omwWP.task = task
return omwWP
end
-- a point yDegrees off the path from AB, xPercent of the total distance
-- between A and B away from A
--[[--
function milHelo.pointXpercentYdegOffAB(A, B, xPer, yDeg) -- rets xzz point
local bearingRad = dcsCommon.bearingFromAtoB(A, B)
local dist = dcsCommon.dist(A, B)
local deviation = bearingRad + yDeg * 0.0174533
local newDist = dist * xPer/100
local newPoint = dcsCommon.pointInDirectionOfPointXYY(deviation, newDist, A)
return newPoint
end
--]]--
function milHelo.spawnForZone(theZone, targetZone)
local theRawData = dcsCommon.getNthItem(theZone.myGroups, 1)
local n = dcsCommon.randomBetween(1, theZone.hCount)
local theRawData = dcsCommon.getNthItem(theZone.hGroups, n)
local gData = dcsCommon.clone(theRawData)
--[[--
local oName = gData.name
-- pre-process gData: names, id etc
gData.name = dcsCommon.uuid(gData.name)
local gName = gData.name
for idx, uData in pairs(gData.units) do
uData.name = dcsCommon.uuid(uData.name)
uData.alt = 10
uData.alt_type = "RADIO"
uData.speed = 0
uData.unitId = nil
end
gData.groupId = nil
-- change task according to missionType in Zone
-- we currently use CAS for all
gData.task = "CAS"
-- create and process route
local route = {}
route.points = {}
-- gData.route = route
gData.route = route
-- create take-off waypoint
local wpTOff = milHelo.createTakeOffWP(theZone)
local casInZone = theZone.msnType == "casz"
if theZone.verbose and casInZone then
trigger.action.outText("Setting up casZ for <" .. theZone.name .. "> to <" .. targetZone.name .. ">", 30)
end
local wpTOff = milHelo.createTakeOffWP(theZone, casInZone, targetZone)
-- depending on mission, create an orbit or land WP
local dest = targetZone:getPoint()
local wpDest = milHelo.createOrbitWP(theZone, dest)
-- move group to WP1 and add WP1 and WP2 to route
-- dcsCommon.moveGroupDataTo(theGroup,
-- fromWP.x,
-- fromWP.y)
----
dcsCommon.addRoutePointForGroupData(gData, wpTOff)
dcsCommon.addRoutePointForGroupData(gData, wpDest)
--]]--
dcsCommon.dumpVar2Str("route", gData.route)
local B = dest
local A = theZone:getPoint()
if theZone.msnType == "cas" or theZone.msnType == "patrol" then
dcsCommon.addRoutePointForGroupData(gData, wpTOff)
local wpDest = milHelo.createOrbitWP(theZone, dest)
dcsCommon.addRoutePointForGroupData(gData, wpDest)
local retPt = milHelo.createLandWP(gName, theZone, theZone)
dcsCommon.addRoutePointForGroupData(gData, retPt)
--dcsCommon.dumpVar2Str("caser group", gData)
elseif theZone.msnType == "casz" then
dcsCommon.addRoutePointForGroupData(gData, wpTOff)
-- go to CAS destination with Engage in Zone active
-- we may want to make ingress and egress wp before heading to
-- the 'real' CASZ point
-- make ingress point, in direction of target, 30 degrees to the right, half distance.
local ingress = dcsCommon.pointXpercentYdegOffAB(A, B, math.random(50,80), math.random(20,50))
--local pt = targetZone:getPoint()
local omw1 = milHelo.createOMWCallbackWP(gName, 2, ingress, theZone.alt, theZone.speed, "none")
dcsCommon.addRoutePointForGroupData(gData, omw1)
local omw2 = milHelo.createOMWCallbackWP(gName, 3, B, theZone.alt, theZone.speed, "none")
dcsCommon.addRoutePointForGroupData(gData, omw2)
-- egress point
local egress = dcsCommon.pointXpercentYdegOffAB(B, A, math.random(20, 50), math.random(20,50))
local omw3 = milHelo.createOMWCallbackWP(gName, 4, egress, theZone.alt, theZone.speed, "none")
dcsCommon.addRoutePointForGroupData(gData, omw3)
local retPt = milHelo.createLandWP(gName, theZone, theZone)
dcsCommon.addRoutePointForGroupData(gData, retPt)
elseif theZone.msnType == "insert" then
local wpDest = milHelo.createLandWP(gName, theZone, targetZone)
dcsCommon.addRoutePointForGroupData(gData, wpTOff)
dcsCommon.addRoutePointForGroupData(gData, wpDest)
end
-- make it a cty
-- make coa a cty
if theZone.coa == 0 then
trigger.action.outText("+++milH: WARNING - zone <" .. theZone.name .. "> is NEUTRAL", 30)
end
@ -259,9 +459,162 @@ function milHelo.spawnForZone(theZone, targetZone)
-- spawn
local groupCat = Group.Category.HELICOPTER
local theSpawnedGroup = coalition.addGroup(cty, groupCat, gData)
local theFlight = {}
theFlight.oName = oName
theFlight.spawn = theSpawnedGroup
theFlight.origin = theZone
theFlight.destination = targetZone
milHelo.flights[gName] = theFlight --theSpawnedGroup
return theSpawnedGroup, gData
end
--
-- mil helo landed callback (insertion)
--
function milHelo.insertTroops(theUnit, targetZone, srcZone)
local theZone = srcZone
local n = dcsCommon.randomBetween(1, theZone.gCount)
local theRawData = dcsCommon.getNthItem(theZone.gGroups, n)
-- local theRawData = dcsCommon.getNthItem(srcZone.gGroups, 1)
if not theRawData then
trigger.action.outText("+++milH: WARNING: no troops to insert for zone <" .. srcZone.name .. ">", 30)
return
end
local gData = dcsCommon.clone(theRawData)
-- deploy in ring formation
-- remove all routes
-- mayhaps prepare for orders and formation
local p = theUnit:getPoint()
gData.route = nil -- no more route. stand in place
gData.name = dcsCommon.uuid(gData.name)
local gName = gData.name
for idx, uData in pairs(gData.units) do
uData.name = dcsCommon.uuid(uData.name)
uData.speed = 0
uData.heading = 0
uData.unitId = nil
end
gData.groupId = nil
dcsCommon.moveGroupDataTo(gData, 0, 0) -- move to origin so we can arrange them
dcsCommon.arrangeGroupDataIntoFormation(gData, 20, nil, "CIRCLE_OUT")
dcsCommon.moveGroupDataTo(gData, p.x, p.z) -- move arranged group to helo
-- make coa a cty
if theZone.coa == 0 then
trigger.action.outText("+++milH: WARNING - zone <" .. theZone.name .. "> is NEUTRAL", 30)
end
local cty = dcsCommon.getACountryForCoalition(theZone.coa)
-- spawn
local groupCat = Group.Category.GROUND
local theSpawnedGroup = coalition.addGroup(cty, groupCat, gData)
trigger.action.outText("Inserted troops <" .. gName .. ">", 30)
return theSpawnedGroup, gData
end
function milHelo.replaceUnitsWithStatics(gName)
end
function milHelo.getRawDataFromGroupNamed(gName, oName)
local theGroup = Group.getByName(gName)
local groupName = gName
local cat = theGroup:getCategory()
-- access mxdata for livery because getDesc does not return the livery
local liveries = {}
local mxData = cfxMX.getGroupFromDCSbyName(oName)
for idx, theUnit in pairs (mxData.units) do
liveries[theUnit.name] = theUnit.livery_id
end
local ctry
local gID = theGroup:getID()
local allUnits = theGroup:getUnits()
local rawGroup = {}
rawGroup.name = groupName
local rawUnits = {}
for idx, theUnit in pairs(allUnits) do
local ir = {}
local unitData = theUnit:getDesc()
-- build record
ir.heading = dcsCommon.getUnitHeading(theUnit)
ir.name = theUnit:getName()
ir.type = unitData.typeName -- warning: fields are called differently! typename vs type
ir.livery_id = liveries[ir.name] -- getDesc does not return livery
ir.groupId = gID
ir.unitId = theUnit:getID()
local up = theUnit:getPoint()
ir.x = up.x
ir.y = up.z -- !!! warning!
-- see if any zones are linked to this unit
ir.linkedZones = cfxZones.zonesLinkedToUnit(theUnit)
table.insert(rawUnits, ir)
ctry = theUnit:getCountry()
end
rawGroup.ctry = ctry
rawGroup.cat = cat
rawGroup.units = rawUnits
return rawGroup, cat, ctry
end
function milHelo.spawnImpostorsFromData(rawData, cat, ctry)
for idx, unitData in pairs(rawData.units) do
-- build impostor record
local ir = {}
ir.heading = unitData.heading
ir.type = unitData.type
ir.name = dcsCommon.uuid(rawData.name) -- .. "-" .. tostring(impostors.uniqueID())
ir.groupID = nil -- impostors.uniqueID()
ir.unitId = nil -- impostors.uniqueID()
ir.x = unitData.x
ir.y = unitData.y
ir.livery_id = unitData.livery_id
-- spawn the impostor
local theImp = coalition.addStaticObject(ctry, ir)
end
end
function milHelo.reachedWP(gName, wpNum, action)
trigger.action.outText("MilH group <" .. gName .. " reached wp #" .. wpNum .. ".", 30)
end
function milHelo.landedCB(who, where, from) -- who group name, where a zone
trigger.action.outText("milhelo landed CB for group <" .. who .. ">", 30)
-- step 1: remove the flight
local theGroup = Group.getByName(who)
if theGroup then
if Group.isExist(theGroup) then
Group.destroy(theGroup)
end
else
trigger.action.outText("+++milH: cannot find group <" .. who .. ">", 30)
end
-- step 3: replace with static helo
local aGroup = theGroup
local theFlight = milHelo.flights[who]
local oName = theFlight.oName
local theZone = theFlight.origin
if theZone.msn == "insertion" then
-- create a static stand-in for scenery
local rawData, cat, ctry = milHelo.getRawDataFromGroupNamed(who, oName)
Group.destroy(aGroup)
milHelo.spawnImpostorsFromData(rawData, cat, ctry)
else
-- remove group
Group.destroy(aGroup)
end
-- remove flight from list of active flights
milHelo.flights[who] = nil
end
--
-- update and event
--
@ -269,10 +622,98 @@ function milHelo.update()
timer.scheduleFunction(milHelo.update, {}, timer.getTime() + 1)
end
function milHelo.onEvent(theEvent)
function milHelo.GCcollected(gName)
-- do some housekeeping?
trigger.action.outText("removed flight <" .. gName .. ">", 30)
end
function milHelo.GC()
timer.scheduleFunction(milHelo.GC, {}, timer.getTime() + 1)
local filtered = {}
for gName, theFlight in pairs(milHelo.flights) do
local theGroup = Group.getByName(gName)
if theGroup and Group.isExist(theGroup) then
-- all fine, keep it
filtered[gName] = theFlight
else
milHelo.GCcollected(gName)
end
end
milHelo.flights = filtered
end
function milHelo:onEvent(theEvent)
if not theEvent then return end
if not theEvent.initiator then return end
local theUnit = theEvent.initiator
if not theUnit.getGroup then return end
local theGroup = theUnit:getGroup()
if not theGroup then
-- trigger.action.outText("event <" .. theEvent.id .. ">: group shenenigans for unit detected", 30)
return
end
local gName = theGroup:getName()
local theFlight = milHelo.flights[gName]
if not theFlight then return end
local id = theEvent.id
if id == 4 then
-- flight landed
-- did it land in target zone?
local p = theUnit:getPoint()
local srcZone = theFlight.origin
local tgtZone = theFlight.destination
if tgtZone:pointInZone(p) then
trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed in zone <" .. tgtZone.name .. ">", 30)
if srcZone.msnType == "insert" then
trigger.action.outText("Commencing Troop Insertion", 30)
milHelo.insertTroops(theUnit, tgtZone, srcZone)
end
else
-- maybe its a return flight
if srcZone:pointInZone(p) then
trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed back home", 30)
else
trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed OUTSIDE of src or target zone <" .. tgtZone.name .. ">", 30)
end
end
end
-- trigger.action.outText("Event <" .. theEvent.id .. "> for milHelo flight <" .. gName .. ">", 30)
end
--
-- API
--
function milHelo.getMilSources(side, msnType) -- msnType is optional
if side == "red" then side = 1 end -- better safe...
if side == "blue" then side = 2 end
local sources = {}
for idx, theZone in pairs(milHelo.zones) do
if theZone.owner == side then -- must be owned by same side
if msnType then
if theZone.msnType == msnType then
table.insert(sources, theZone)
end
else
table.insert(sources, theZone)
end
end
end
return sources -- an array, NOT dict so we can pickrandom
end
function milHelo.getMilTargets(side) -- gets mil targets that DO NOT belong to side
if side == "red" then side = 1 end -- better safe...
if side == "blue" then side = 2 end
local tgt = {}
for idx, theZone in pairs(milHelo.targets) do
if theZone.owner ~= side then -- must NOT be owned by same side
table.insert(tgt, theZone)
end
end
return tgt
end
--
-- Config & start
--
@ -282,6 +723,7 @@ function milHelo.readConfigZone()
theZone = cfxZones.createSimpleZone("milHeloConfig")
end
milHelo.verbose = theZone.verbose
milHelo.landingDuration = theZone:getNumberFromZoneProperty("landingDuration", 180) -- seconds = 3 minutes
end
@ -305,11 +747,13 @@ function milHelo.start()
milHelo.addMilHeloZone(aZone) -- add to list
end
attrZones = cfxZones.getZonesWithAttributeNamed("milTarget")
for k, aZone in pairs(attrZones) do
milHelo.readMilTargetZone(aZone) -- process attributes
milHelo.addMilTargetZone(aZone) -- add to list
end
for idx, keyWord in pairs(milHelo.targetKeywords) do
attrZones = cfxZones.getZonesWithAttributeNamed(keyWord)
for k, aZone in pairs(attrZones) do
milHelo.readMilTargetZone(aZone) -- process attributes
milHelo.addMilTargetZone(aZone) -- add to list
end
end
-- start update in 5 seconds
timer.scheduleFunction(milHelo.update, {}, timer.getTime() + 1/milHelo.ups)
@ -327,7 +771,21 @@ if not milHelo.start() then
milHelo = nil
end
--[[
function milHelo.latestuff()
trigger.action.outText("doing stuff", 30)
local theZone = cfxZones.getZoneByName("milCAS") --dcsCommon.getFirstItem(milHelo.zones)
local targetZone = cfxZones.getZoneByName("mh Target") -- dcsCommon.getFirstItem(milHelo.targets)
milHelo.spawnForZone(theZone, targetZone)
theZone = cfxZones.getZoneByName("milInsert") --dcsCommon.getNthItem(milHelo.zones, 2)
milHelo.spawnForZone(theZone, targetZone)
theZone = cfxZones.getZoneByName("doCASZ")
targetZone = cfxZones.getZoneByName("milTarget Z")
if not theZone then trigger.action.outText("Not theZone", 30) end
if not targetZone then trigger.action.OutText("Not targetZone", 30) end
milHelo.spawnForZone(theZone, targetZone)
end
-- do some one-time stuff
local theZone = dcsCommon.getFirstItem(milHelo.zones)
local targetZone = dcsCommon.getFirstItem(milHelo.targets)
milHelo.spawnForZone(theZone, targetZone)
timer.scheduleFunction(milHelo.latestuff, {}, timer.getTime() + 1)
--]]--

View File

@ -1,5 +1,5 @@
cfxOwnedZones = {}
cfxOwnedZones.version = "2.2.0"
cfxOwnedZones.version = "2.3.0"
cfxOwnedZones.verbose = false
cfxOwnedZones.announcer = true
cfxOwnedZones.name = "cfxOwnedZones"
@ -30,25 +30,30 @@ cfxOwnedZones.name = "cfxOwnedZones"
- method support for global (config) output
- moved drawZone to cfxZones
2.2.0 - excludedTypes option in config
2.3.0 - include airfield zones (module) in collectZones()
- if airfield is defined.
- allManagedOwnedZones
- gatherAllManagedOwnedZones()
- commented out unused (?) methods
- optmized getNearestEnemyOwnedZone
- collectZones now uses gatherAllManagedOwnedZones
- sideOwnsAll can use allManagedOwnedZones
- per-zone local numCap
- per-zone local numkeep
- title attribute
- code clean-up
--]]--
cfxOwnedZones.requiredLibs = {
"dcsCommon",
"cfxZones",
}
cfxOwnedZones.zones = {}
cfxOwnedZones.zones = {} -- ownedZones FROM THIS module
cfxOwnedZones.allManagedOwnedZones = {} -- superset, indexed by name
cfxOwnedZones.ups = 1
cfxOwnedZones.initialized = false
--[[--
owned zones is a module that manages conquerable zones and keeps a record
of who owns the zone based on rules
*** EXTENTDS ZONES ***
when a zone changes hands, a callback can be installed to be told of that fact
callback has the format (zone, newOwner, formerOwner) with zone being the Zone, and new owner and former owners
--]]--
-- *** EXTENTDS ZONES *** --
cfxOwnedZones.conqueredCallbacks = {}
@ -100,11 +105,13 @@ function cfxOwnedZones.drawZoneInMap(aZone)
if aZone.markID then
trigger.action.removeMark(aZone.markID)
end
if aZone.hidden then return end
if aZone.titleID then
trigger.action.removeMark(aZone.titleID)
end
local lineColor = aZone.redLine -- {1.0, 0, 0, 1.0} -- red
local fillColor = aZone.redFill -- {1.0, 0, 0, 0.2} -- red
local owner = aZone.owner -- cfxOwnedZones.getOwnerForZone(aZone)
local owner = aZone.owner
if owner == 2 then
lineColor = aZone.blueLine -- {0.0, 0, 1.0, 1.0}
fillColor = aZone.blueFill -- {0.0, 0, 1.0, 0.2}
@ -112,7 +119,12 @@ function cfxOwnedZones.drawZoneInMap(aZone)
lineColor = aZone.neutralLine -- {0.8, 0.8, 0.8, 1.0}
fillColor = aZone.neutralFill -- {0.8, 0.8, 0.8, 0.2}
end
if aZone.title then
aZone.titleID = aZone:drawText(aZone.title, 18, lineColor, {0, 0, 0, 0})
end
if aZone.hidden then return end
aZone.markID = aZone:drawZone(lineColor, fillColor) -- markID
end
@ -154,6 +166,9 @@ function cfxOwnedZones.addOwnedZone(aZone)
aZone.untargetable = aZone:getBoolFromZoneProperty("untargetable", false)
aZone.hidden = aZone:getBoolFromZoneProperty("hidden", false)
-- numCap, numKeep
aZone.numCap = aZone:getNumberFromZoneProperty("numCap", cfxOwnedZones.numCap)
aZone.numKeep = aZone:getNumberFromZoneProperty("numKeep", cfxOwnedZones.numKeep)
-- individual colors, else default from config
aZone.redLine = aZone:getRGBAVectorFromZoneProperty("redLine", cfxOwnedZones.redLine)
@ -163,6 +178,31 @@ function cfxOwnedZones.addOwnedZone(aZone)
aZone.neutralLine = aZone:getRGBAVectorFromZoneProperty("neutralLine", cfxOwnedZones.neutralLine)
aZone.neutralFill = aZone:getRGBAVectorFromZoneProperty("neutralFill", cfxOwnedZones.neutralFill)
-- masterOwner
if aZone:hasProperty("masterOwner") then
local masterZone = aZone:getStringFromZoneProperty("masterOwner", "cfxNoneErr")
local theMaster = cfxZones.getZoneByName(masterZone)
if not theMaster then
trigger.action.outText("+++owdZ: WARNING: owned zone <" .. aZone.name .. ">'s masterOwner <" .. masterZone .. "> does not exist, not connecting!", 30)
else
aZone.masterOwner = theMaster
aZone.owner = theMaster.owner
if aZone.verbose or cfxOwnedZones.verbose then
trigger.action.outText("+++OwdZ: owned zone <" .. aZone.name .. "> inherits ownership from master zone <" .. masterZone .. ">", 30)
end
end
end
aZone.announcer = aZone:getBoolFromZoneProperty("announcer", cfxZones.announcer)
if aZone:hasProperty("announce") then
aZone.announcer = aZone:getBoolFromZoneProperty("announce", cfxZones.announcer)
end
-- title
if aZone:hasProperty("title") then
aZone.title = aZone:getStringFromZoneProperty("title")
if aZone.title == "*" then aZone.title = aZone.name end
end
aZone.method = aZone:getStringFromZoneProperty("method", "inc")
cfxOwnedZones.zones[aZone] = aZone
@ -178,23 +218,17 @@ end
function cfxOwnedZones.bangNeutral(value)
if not cfxOwnedZones.neutralTriggerFlag then return end
--local newVal = trigger.misc.getUserFlag(cfxOwnedZones.neutralTriggerFlag) + value
--trigger.action.setUserFlag(cfxOwnedZones.neutralTriggerFlag, newVal)
cfxZones.pollFlag(cfxOwnedZones.neutralTriggerFlag, cfxOwnedZones.method, cfxOwnedZones)
end
function cfxOwnedZones.bangRed(value, theZone)
if not cfxOwnedZones.redTriggerFlag then return end
--local newVal = trigger.misc.getUserFlag(cfxOwnedZones.redTriggerFlag) + value
--trigger.action.setUserFlag(cfxOwnedZones.redTriggerFlag, newVal)
cfxZones.pollFlag(cfxOwnedZones.redTriggerFlag, cfxOwnedZones.method, cfxOwnedZones)
end
function cfxOwnedZones.bangBlue(value, theZone)
if not cfxOwnedZones.blueTriggerFlag then return end
local newVal = trigger.misc.getUserFlag(cfxOwnedZones.blueTriggerFlag) + value
-- trigger.action.setUserFlag(cfxOwnedZones.blueTriggerFlag, newVal)
-- cfxZones.setFlagValue(cfxOwnedZones.blueTriggerFlag, newVal, cfxOwnedZones)
cfxZones.pollFlag(cfxOwnedZones.blueTriggerFlag, cfxOwnedZones.method, cfxOwnedZones)
end
@ -214,14 +248,15 @@ function cfxOwnedZones.zoneConquered(aZone, theSide, formerOwner) -- 0 = neutral
local who = "REDFORCE"
if theSide == 2 then who = "BLUEFORCE"
elseif theSide == 0 then who = "NEUTRAL" end
aZone.owner = theSide -- just to be sure
if cfxOwnedZones.announcer then
if aZone.announcer then
if theSide == 0 then
trigger.action.outText(aZone.name .. " has become NEUTRAL", 30)
else
trigger.action.outText(who .. " have secured zone " .. aZone.name, 30)
end
aZone.owner = theSide -- just to be sure
-- play different sounds depending on who's won
if theSide == 1 then
trigger.action.outSoundForCoalition(1, cfxOwnedZones.winSound)
@ -307,47 +342,38 @@ function cfxOwnedZones.update()
if cfxOwnedZones.fixWingCap then
allBlue = dcsCommon.combineTables(allBlue, coalition.getGroups(2, Group.Category.AIRPLANE))
end
-- WARNING: we only proc ownedZones, NOT airfield nor FARP or other
for idz, theZone in pairs(cfxOwnedZones.zones) do
theZone.numRed = 0
theZone.numBlue = 0
local lastOwner = theZone.owner
if not lastOwner then
trigger.action.outText("+++owdZ: WARNING - zone <" .. theZone.name .. "> has NIL owner", 30)
return
end
if theZone.verbose then
trigger.action.outText("Zone <" .. theZone.name .. "> lastOwner is <" .. lastOwner .. ">", 30)
end
local newOwner = 0 -- neutral is default
-- count red units in zone
for idx, aGroup in pairs(allRed) do
if Group.isExist(aGroup) then
if cfxOwnedZones.fastEval then
-- we only check first unit that is alive
local theUnit = dcsCommon.getGroupUnit(aGroup)
if theUnit and (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
if cfxOwnedZones.excludedTypes then
-- special carve-out for exclduding some
-- unit types to prevent them from capping
local uType = theUnit:getTypeName()
local forbidden = false
for idx, aType in pairs(cfxOwnedZones.excludedTypes) do
if uType == aType then
forbidden = true
else
end
end
if not forbidden then
theZone.numRed = theZone.numRed + aGroup:getSize()
end
else
theZone.numRed = theZone.numRed + aGroup:getSize()
end
end
else -- full eval
local allUnits = aGroup:getUnits()
for idy, theUnit in pairs(allUnits) do
if (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
-- theZone.numRed = theZone.numRed + 1
if not theZone.masterOwner then
for idx, aGroup in pairs(allRed) do
if Group.isExist(aGroup) then
if cfxOwnedZones.fastEval then
-- we only check first unit that is alive
local theUnit = dcsCommon.getGroupUnit(aGroup)
if theUnit and (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
if cfxOwnedZones.excludedTypes then
-- special carve-out for exclduding some
-- unit types to prevent them from capping
local uType = theUnit:getTypeName()
local forbidden = false
for idx, aType in pairs(cfxOwnedZones.excludedTypes) do
if uType == aType then forbidden = true end
if uType == aType then
forbidden = true
else
end
end
if not forbidden then
theZone.numRed = theZone.numRed + aGroup:getSize()
@ -356,48 +382,47 @@ function cfxOwnedZones.update()
theZone.numRed = theZone.numRed + aGroup:getSize()
end
end
else -- full eval
local allUnits = aGroup:getUnits()
for idy, theUnit in pairs(allUnits) do
if (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
if cfxOwnedZones.excludedTypes then
-- special carve-out for exclduding some
-- unit types to prevent them from capping
local uType = theUnit:getTypeName()
local forbidden = false
for idx, aType in pairs(cfxOwnedZones.excludedTypes) do
if uType == aType then forbidden = true end
end
if not forbidden then
theZone.numRed = theZone.numRed + aGroup:getSize()
end
else
theZone.numRed = theZone.numRed + aGroup:getSize()
end
end
end
end
end
end
end
-- count blue units
for idx, aGroup in pairs(allBlue) do
if Group.isExist(aGroup) then
if cfxOwnedZones.fastEval then
-- we only check first unit that is alive
local theUnit = dcsCommon.getGroupUnit(aGroup)
if theUnit and (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
if cfxOwnedZones.excludedTypes then
-- special carve-out for exclduding some
-- unit types to prevent them from capping
local uType = theUnit:getTypeName()
local forbidden = false
for idx, aType in pairs(cfxOwnedZones.excludedTypes) do
if uType == aType then
forbidden = true
else
end
end
if not forbidden then
theZone.numBlue = theZone.numBlue + aGroup:getSize()
end
else
theZone.numBlue = theZone.numBlue + aGroup:getSize()
end
end
else
local allUnits = aGroup:getUnits()
for idy, theUnit in pairs(allUnits) do
if (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
-- theZone.numBlue = theZone.numBlue + 1
-- count blue units
for idx, aGroup in pairs(allBlue) do
if Group.isExist(aGroup) then
if cfxOwnedZones.fastEval then
-- we only check first unit that is alive
local theUnit = dcsCommon.getGroupUnit(aGroup)
if theUnit and (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
if cfxOwnedZones.excludedTypes then
-- special carve-out for exclduding some
-- unit types to prevent them from capping
local uType = theUnit:getTypeName()
local forbidden = false
for idx, aType in pairs(cfxOwnedZones.excludedTypes) do
if uType == aType then forbidden = true end
if uType == aType then
forbidden = true
else
end
end
if not forbidden then
theZone.numBlue = theZone.numBlue + aGroup:getSize()
@ -406,28 +431,50 @@ function cfxOwnedZones.update()
theZone.numBlue = theZone.numBlue + aGroup:getSize()
end
end
else
local allUnits = aGroup:getUnits()
for idy, theUnit in pairs(allUnits) do
if (not theUnit:inAir()) and theZone:unitInZone(theUnit) then
if cfxOwnedZones.excludedTypes then
-- special carve-out for exclduding some
-- unit types to prevent them from capping
local uType = theUnit:getTypeName()
local forbidden = false
for idx, aType in pairs(cfxOwnedZones.excludedTypes) do
if uType == aType then forbidden = true end
end
if not forbidden then
theZone.numBlue = theZone.numBlue + aGroup:getSize()
end
else
theZone.numBlue = theZone.numBlue + aGroup:getSize()
end
end
end
end
end
end
end
if theZone.verbose then
trigger.action.outText("+++owdZ: zone <" .. theZone.name .. ">: red inside: <" .. theZone.numRed .. ">, blue inside: <>" .. theZone.numBlue, 30)
end
else
-- zone has master owner, no counting done
end
if theZone.verbose then
trigger.action.outText("+++owdZ: zone <" .. theZone.name .. ">: red inside: <" .. theZone.numRed .. ">, blue inside: <>" .. theZone.numBlue, 30)
end
-- trigger.action.outText(theZone.name .. " blue: " .. theZone.numBlue .. " red " .. theZone.numRed, 30)
local lastOwner = theZone.owner
local newOwner = 0 -- neutral is default
if theZone.unbeatable then -- Parker Lewis can't lose. Neither this zone.
newOwner = lastOwner
end
-- determine new owner
if theZone.unbeatable then
-- we do nothing
-- we do nothing
elseif theZone.masterOwner then
-- inherit from my master
newOwner = theZone.masterOwner.owner
elseif theZone.numRed < 1 and theZone.numBlue < 1 then
-- no troops here. Become neutral?
if cfxOwnedZones.numKeep < 1 then
if theZone.numKeep < 1 then
newOwner = lastOwner -- keep it, else turns neutral
else
-- noone here, zone becomes neutral
@ -435,9 +482,9 @@ function cfxOwnedZones.update()
end
elseif theZone.numRed < 1 then
-- only blue here. enough to keep?
if theZone.numBlue >= cfxOwnedZones.numCap then
if theZone.numBlue >= theZone.numCap then
newOwner = 2 -- blue owns it
elseif lastOwner == 2 and theZone.numBlue >= cfxOwnedZones.numKeep then
elseif lastOwner == 2 and theZone.numBlue >= theZone.numKeep then
-- enough to keep if owned before
newOwner = 2
else
@ -445,9 +492,9 @@ function cfxOwnedZones.update()
end
elseif theZone.numBlue < 1 then
-- only red here. enough to keep?
if theZone.numRed >= cfxOwnedZones.numCap then
if theZone.numRed >= theZone.numCap then
newOwner = 1
elseif lastOwner == 1 and theZone.numRed >= cfxOwnedZones.numKeep then
elseif lastOwner == 1 and theZone.numRed >= theZone.numKeep then
newOwner = 1
else
newOwner = 0
@ -459,18 +506,18 @@ function cfxOwnedZones.update()
if cfxOwnedZones.easyContest then
-- this zone is immediately contested
newOwner = 0 -- just to be explicit
elseif cfxOwnedZones.numKeep < 1 then
elseif theZone.numKeep < 1 then
-- old owner keeps it until none left
newOwner = lastOwner
else
if lastOwner == 1 then
-- red can keep it as long as enough units here
if theZone.numRed >= cfxOwnedZones.numKeep then
if theZone.numRed >= theZone.numKeep then
newOwner = 1
end -- else 0
elseif lastOwner == 2 then
-- blue can keep it if enough units here
if theZone.numBlue >= cfxOwnedZones.numKeep then
if theZone.numBlue >= theZone.numKeep then
newOwner = 2
end -- else 0
else -- stay 0
@ -525,14 +572,14 @@ function cfxOwnedZones.update()
-- see if one side owns all and bang the flags if requiredLibs
if cfxOwnedZones.allBlue and not cfxOwnedZones.hasAllBlue then
if cfxOwnedZones.sideOwnsAll(2) then
if cfxOwnedZones.sideOwnsAll(2) then -- ignores other owner-managed zones
cfxZones.pollFlag(cfxOwnedZones.allBlue, cfxOwnedZones.method, cfxOwnedZones)
cfxOwnedZones.hasAllBlue = true
end
end
if cfxOwnedZones.allRed and not cfxOwnedZones.hasAllRed then
if cfxOwnedZones.sideOwnsAll(1) then
if cfxOwnedZones.sideOwnsAll(1) then -- ignores other managed owner zones
cfxZones.pollFlag(cfxOwnedZones.allRed, cfxOwnedZones.method, cfxOwnedZones)
cfxOwnedZones.hasAllRed = true
end
@ -540,8 +587,10 @@ function cfxOwnedZones.update()
end
function cfxOwnedZones.sideOwnsAll(theSide)
for key, aZone in pairs(cfxOwnedZones.zones) do
function cfxOwnedZones.sideOwnsAll(theSide, useAllManaged)
local themAll = cfxOwnedZones.zones
if useAllManaged then themAll = cfxZones.allManagedOwnedZones end
for key, aZone in pairs(themAll) do
if aZone.owner ~= theSide then
return false
end
@ -550,27 +599,44 @@ function cfxOwnedZones.sideOwnsAll(theSide)
return true
end
function cfxOwnedZones.hasOwnedZones()
for idx, zone in pairs (cfxOwnedZones.zones) do
return true -- even the first returns true
end
-- no owned zones
return false
end
-- getting closest owned zones etc
-- required for groundTroops and factory attackers
-- methods provided only for other modules (e.g. cfxGroundTroops or
-- factoryZone
--
function cfxOwnedZones.gatherAllManagedOwnedZones()
-- we collect all zones with 'owner'
local all = {}
local pZones = cfxZones.zonesWithProperty("owner")
for k, theZone in pairs(pZones) do
all[theZone.name] = theZone
end
-- and add all zones with airfield
local pZones = cfxZones.zonesWithProperty("airfield")
for k, theZone in pairs(pZones) do
all[theZone.name] = theZone
end
-- and all zones with 'FARP'
local pZones = cfxZones.zonesWithProperty("FARP")
for k, theZone in pairs(pZones) do
all[theZone.name] = theZone
end
-- and all with ownAll?
-- not yet
cfxOwnedZones.allManagedOwnedZones = all
end
-- collect zones can filter owned zones.
-- by default it filters all zones that are in water
-- includes all managed-owner zones
function cfxOwnedZones.collectZones(mode)
if not mode then mode = "land" end
if mode == "land" then
local landZones = {}
for idx, theZone in pairs(cfxOwnedZones.zones) do
for idx, theZone in pairs(cfxOwnedZones.allManagedOwnedZones) do
p = theZone:getPoint()
p.y = p.z
local surfType = land.getSurfaceType(p)
@ -580,64 +646,12 @@ function cfxOwnedZones.collectZones(mode)
end
end
return landZones
else
return cfxOwnedZones.allManagedOwnedZones
end
-- return all zones
return cfxOwnedZones.zones
--if not mode then mode = "OWNED" end
-- Note: since cfxGroundTroops currently simply uses owner flag
-- we cannot migrate to a differentiation between factory and
-- owned. All produced attackers always attack owned zones.
end
function cfxOwnedZones.getEnemyZonesFor(aCoalition)
local enemyZones = {}
local allZones = cfxOwnedZones.collectZones()
local ourEnemy = dcsCommon.getEnemyCoalitionFor(aCoalition)
for zKey, aZone in pairs(allZones) do
if aZone.owner == ourEnemy then -- only check enemy owned zones
-- note: will include untargetable zones
table.insert(enemyZones, aZone)
end
end
return enemyZones
end
function cfxOwnedZones.getNearestOwnedZoneToPoint(aPoint)
local shortestDist = math.huge
local closestZone = nil
local allZones = cfxOwnedZones.collectZones()
for zKey, aZone in pairs(allZones) do
local zPoint = aZone:getPoint()
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and
currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestOwnedZone(theZone)
local shortestDist = math.huge
local closestZone = nil
local aPoint = theZone:getPoint()
local allZones = cfxOwnedZones.collectZones()
for zKey, aZone in pairs(allZones) do
local zPoint = aZone:getPoint()
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
end
-- getNearestEnemyOwnedZone invoked by cfxGroundTroops
function cfxOwnedZones.getNearestEnemyOwnedZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
@ -650,56 +664,20 @@ function cfxOwnedZones.getNearestEnemyOwnedZone(theZone, targetNeutral)
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- return all zones that do not belong to us
if aZone.owner ~= theZone.owner then
if aZone.owner ~= theZone.owner and not aZone.untargetable then
local aPoint = aZone:getPoint()
currDist = dcsCommon.dist(aPoint, zPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
if currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- return zones that are taken by the Enenmy
if aZone.owner == ourEnemy then -- only check own zones
if aZone.owner == ourEnemy and not aZone.untargetable then -- only check own zones
local aPoint = aZone:getPoint()
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestFriendlyZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies nor friends, all zones would be legal.
local zPoint = theZone:getPoint()
local allZones = cfxOwnedZones.collectZones()
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- target all zones that do not belong to the enemy
if aZone.owner ~= ourEnemy then
local aPoint = aZone:getPoint()
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- only target zones that are taken by us
if aZone.owner == theZone.owner then -- only check own zones
local aPoint = aZone:getPoint()
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
if currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
@ -710,11 +688,13 @@ function cfxOwnedZones.getNearestFriendlyZone(theZone, targetNeutral)
return closestZone, shortestDist
end
-- invoked by factory
function cfxOwnedZones.enemiesRemaining(aZone)
if cfxOwnedZones.getNearestEnemyOwnedZone(aZone) then return true end
return false
end
--
-- load / save data
--
@ -796,6 +776,9 @@ function cfxOwnedZones.readConfigZone(theZone)
cfxOwnedZones.name = "cfxOwnedZones" -- just in case, so we can access with cfxZones
cfxOwnedZones.verbose = theZone.verbose -- cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxOwnedZones.announcer = theZone:getBoolFromZoneProperty("announcer", true)
if theZone:hasProperty("announce") then
cfxZones.announcer = theZone:getBoolFromZoneProperty("announce", true)
end
if theZone:hasProperty("r!") then
cfxOwnedZones.redTriggerFlag = theZone:getStringFromZoneProperty("r!", "*<cfxnone>")
@ -892,6 +875,9 @@ function cfxOwnedZones.init()
cfxOwnedZones.addOwnedZone(aZone)
end
-- gather ALL managed owner zones
cfxOwnedZones.gatherAllManagedOwnedZones()
if persistence then
-- sign up for persistence
callbacks = {}
@ -917,6 +903,11 @@ end
masterOwner input for zones, overrides all else when not neutral
dont count zones that cant be conquered for allBlue/allRed
noRed, noBlue options to prevent a zone to become that color
black color for dead. dead status to be defined. dead can't be capped and do not attact
--]]--

View File

@ -1,5 +1,5 @@
cfxPlayerScore = {}
cfxPlayerScore.version = "3.1.0"
cfxPlayerScore.version = "3.2.0"
cfxPlayerScore.name = "cfxPlayerScore" -- compatibility with flag bangers
cfxPlayerScore.badSound = "Death BRASS.wav"
cfxPlayerScore.scoreSound = "Quest Snare 3.wav"
@ -14,7 +14,7 @@ cfxPlayerScore.firstSave = true -- to force overwrite
3.0.1 - cleanup
3.0.2 - interface with ObjectDestructDetector for scoring scenery objects
3.1.0 - shared data for persistence
3.2.0 - integration with bank
--]]--
cfxPlayerScore.requiredLibs = {
@ -333,10 +333,16 @@ function cfxPlayerScore.updateScoreForPlayerImmediate(playerName, score)
-- only on positive score
if (score > 0) and pFaction > 0 then
cfxPlayerScore.coalitionScore[pFaction] = cfxPlayerScore.coalitionScore[pFaction] + score
if bank and bank.addFunds then
bank.addFunds(pFaction, cfxPlayerScore.score2finance * score)
end
end
else
if pFaction > 0 then
cfxPlayerScore.coalitionScore[pFaction] = cfxPlayerScore.coalitionScore[pFaction] + score
if bank and bank.addFunds then
bank.addFunds(pFaction, cfxPlayerScore.score2finance * score)
end
end
end
return thePlayerScore.score
@ -1015,6 +1021,9 @@ function cfxPlayerScore.scheduledAward(args)
theScore.score = theScore.score + theScore.scoreaccu
desc = desc .. " score: " .. theScore.scoreaccu .. " for a new total of " .. theScore.score .. "\n"
cfxPlayerScore.coalitionScore[playerSide] = cfxPlayerScore.coalitionScore[playerSide] + theScore.scoreaccu
if bank and bank.addFunds then
bank.addFunds(playerSide, cfxPlayerScore.score2finance * theScore.scoreaccu)
end
theScore.scoreaccu = 0
hasAward = true
end
@ -1202,6 +1211,8 @@ function cfxPlayerScore.readConfigZone(theZone)
if theZone:hasProperty("sharedData") then
cfxPlayerScore.sharedData = theZone:getStringFromZoneProperty("sharedData", "cfxNameMissing")
end
cfxPlayerScore.score2finance = theZone:getNumberFromZoneProperty("score2finance", 1) -- factor to convert points to bank finance
end
--
@ -1349,6 +1360,10 @@ function cfxPlayerScore.update()
-- score!
cfxPlayerScore.coalitionScore[coa] = cfxPlayerScore.coalitionScore[coa] + cfxPlayerScore.blueTriggerScore[tName]
cfxPlayerScore.blueTriggerFlags[tName] = newVal
-- bank it if exists
if bank and bank.addFunds then
bank.addFunds(coa, cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName])
end
if cfxPlayerScore.announcer then
trigger.action.outTextForCoalition(coa, "BLUE goal [" .. tName .. "] achieved, new BLUE coalition score is " .. cfxPlayerScore.coalitionScore[coa], 30)
trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound)
@ -1365,6 +1380,9 @@ function cfxPlayerScore.update()
cfxPlayerScore.coalitionScore[coa] = cfxPlayerScore.coalitionScore[coa] + cfxPlayerScore.redTriggerScore[tName]
cfxPlayerScore.redTriggerFlags[tName] = newVal
if bank and bank.addFunds then
bank.addFunds(coa, cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName])
end
if cfxPlayerScore.announcer then
trigger.action.outTextForCoalition(coa, "RED goal [" .. tName .. "] achieved, new RED coalition score is " .. cfxPlayerScore.coalitionScore[coa], 30)
trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound)

View File

@ -286,7 +286,11 @@ end
stopGap.kicks = {}
function stopGap.kickplayer(args)
if not stopGap.kickTheDead then return end
if not stopGap.kickTheDead then
-- trigger.action.outText("+++sg: Let em rest, no kick", 30)
return
end
-- trigger.action.outText("Kick'em while they are down!", 30)
local pName = args
for i,slot in pairs(net.get_player_list()) do
local nn = net.get_name(slot)

View File

@ -256,7 +256,11 @@ function stopGap:onEvent(event)
-- is now slotted into
trigger.action.setUserFlag("SG"..gName, 0)
end
if id == 6 then -- eject
-- trigger.action.outText("+++SG: not handled: Eject Eject Eject!", 30)
end
if (id == 9) or (id == 30) or (id == 5) then -- dead, lost, crash
-- trigger.action.outText("+++sg: event <" .. id .. ">, handing off to kicker", 30)
local pName = theUnit:getPlayerName()
timer.scheduleFunction(stopGap.kickplayer, pName, timer.getTime() + 1)
end
@ -265,6 +269,7 @@ end
stopGap.kicks = {}
function stopGap.kickplayer(args)
-- trigger.action.outText("+++sg: enter kicker!", 30)
if not stopGap.kickTheDead then return end
local pName = args
for i,slot in pairs(net.get_player_list()) do

173
modules/xpStrat.lua Normal file
View File

@ -0,0 +1,173 @@
xpStrat = {}
-- AI strategy module for expansion.
xpStrat.version = "0.0.0"
xpStrat.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
"milHelo", -- for helo attack and capture missions
}
xpStrat.zones = {} -- all the zones that interest me
xpStrat.AI = {} -- red, blue -- if true, that side has pure ai
function xpStrat.addXPZone(theZone)
xpStrat.zones[theZone.name] = theZone
end
--
-- Strategy
--
function xpStrat.fullAIStrategy(coa, cname)
if xpStrat.verbose then
trigger.action.outText("FULL AI Strategy for (" .. coa .. "/" .. cname .. ")", 30)
end
end
function xpStrat.getMilHSource(coa, msnType, nearZone)
if not msnType then
msnType = dcsCommon.pickRandom(milHelo.missionTypes)
elseif msnType == "*" then
msnType = nil -- get all.
end
-- get all sources for coa and msnTypes
local sources = milHelo.getMilSources(coa, msnType)
if #sources < 1 then
trigger.action.outText("Strat: no sources found for <" .. msnType .. "> helo mission", 30)
return nil
end
local theSource = nil
if nearZone then
theSource = nearZone:getClosestZone(sources)
else
-- pick one by random
theSource = dcsCommon.pickRandom(sources)
end
return theSource
end
function xpStrat.createHeloMission(coa, msnType, theSource, theTarget) -- msnType = "*" means any, nil means pick random
if not theSource then
theSource = xpStrat.getMilHSource(coa, msnType)
end
if not theSource then
trigger.action.outText("Strat: cannot find coa <" .. coa .. "> source for <" .. msnType .. "> mission", 30)
return nil
end
msnType = theSource.msnType
-- now gather all destinations
if not theTarget then
local targets = milHelo.getMilTargets(coa)
if #targets < 1 then
trigger.action.outText("Strat: no destinations for side " .. side, 30)
return nil
end
-- TODO: choose nearest target to source
-- and prefer neutral
theTarget = theSource:getClosestZone(targets)--targets[1]
end
-- if we get here, we have a source and target
trigger.action.outText("Strat: identified Coa <" .. coa .. "> - Starting <" .. msnType .. "> mission from <" .. theSource.name .. "> to <" .. theTarget.name .. ">)", 30)
return theSource, theTarget, msnType
end
function xpStrat.playerAIStrategy(coa, cname)
if xpStrat.verbose then
trigger.action.outText("Player-assisted AI Strategy for (" .. coa .. "/" .. cname .. ")", 30)
end
-- strategy in general (ha!, pun)
-- check which missions are done first
-- select one mission unless still running and initiate own support flights
-- select one aggressive flight and start it, no matter what
end
function xpStrat.update()
-- schedule next round
timer.scheduleFunction(xpStrat.update, {}, timer.getTime() + xpStrat.interval)
local sides = {"red", "blue"}
for idx, sideName in pairs(sides) do
local coa = 1
if sideName == "blue" then coa = 2 end
if xpStrat.AI[sideName] then
xpStrat.fullAIStrategy(coa, sideName)
else
xpStrat.playerAIStrategy(coa, sideName)
end
end
end
function xpStrat.readConfigZone()
local theZone = cfxZones.getZoneByName("expansionConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("expansionConfig")
end
xpStrat.redAI = theZone:getBoolFromZoneProperty("redAI", true) -- is red player or AI?
xpStrat.AI["red"] = xpStrat.redAI
xpStrat.blueAI = theZone:getBoolFromZoneProperty("blueAI", true) -- is red player or AI?
xpStrat.AI["blue"] = xpStrat.blueAI
xpStrat.interval = theZone:getNumberFromZoneProperty("interval", 600)
xpStrat.difficulty = theZone:getNumberFromZoneProperty("difficulty", 1)
xpStrat.verbose = theZone.verbose
end
function xpStrat.init()
-- gather data etc
trigger.action.outText("'Expansion' Core v" .. xpStrat.version .. " (" .. dcsCommon.getMapName() .. ") started.", 30)
local msg = ""
if xpStrat.redAI then msg = msg .. "\nRed side controlled by AI General"
else msg = msg .. "\nRed side controlled by Player-Assisted General" end
if xpStrat.blueAI then msg = msg .. "\nBlue side controlled by AI General"
else msg = msg .. "\nBlue side controlled by Player-Assisted General" end
msg = msg .. "\ndifficulty level set to " .. xpStrat.difficulty
msg = msg .. "\n"
trigger.action.outText(msg, 30)
-- schedule first round of AI in 10 seconds
timer.scheduleFunction(xpStrat.update, {}, timer.getTime() + 10)
end
function xpStrat.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("xpStrat requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("xpStrat", xpStrat.requiredLibs) then
return false
end
-- read config
xpStrat.readConfigZone()
-- read income zones
--[[-- local attrZones = cfxZones.getZonesWithAttributeNamed("income")
for k, aZone in pairs(attrZones) do
income.createIncomeWithZone(aZone) -- process attributes
income.addIncomeZone(aZone) -- add to list
end
--]]--
-- schedule init for 5 seconds after mission start
timer.scheduleFunction(xpStrat.init, {}, timer.getTime() + 5)
trigger.action.outText("xpStrat v" .. xpStrat.version .. " started.", 30)
return true
end
if not xpStrat.start() then
trigger.action.outText("xpStrat aborted: missing libraries", 30)
xpStrat = nil
end