diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 65eeb5e..4918a4e 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index 533c06e..f62eeb3 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index e719338..e9d3436 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -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? --]]-- \ No newline at end of file diff --git a/modules/autoCSAR.lua b/modules/autoCSAR.lua index d02e469..1eda9f5 100644 --- a/modules/autoCSAR.lua +++ b/modules/autoCSAR.lua @@ -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 diff --git a/modules/bank.lua b/modules/bank.lua new file mode 100644 index 0000000..30342b4 --- /dev/null +++ b/modules/bank.lua @@ -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 \ No newline at end of file diff --git a/modules/camp.lua b/modules/camp.lua new file mode 100644 index 0000000..364a04d --- /dev/null +++ b/modules/camp.lua @@ -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 = "" + if theUnit.getPlayerName then pName = theUnit:getPlayerName() end + if not pName then pName = "" 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 = "" + if theUnit.getPlayerName then pName = theUnit:getPlayerName() end + if not pName then pName = "" 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 +--]]-- \ No newline at end of file diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index f979f61..a3aff6e 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -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 diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index d219342..ba75efe 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -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 diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index a747fbb..2cebf1c 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -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 diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 642828c..a248068 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -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 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 diff --git a/modules/factoryZone.lua b/modules/factoryZone.lua index b3b2c8a..71ec4d5 100644 --- a/modules/factoryZone.lua +++ b/modules/factoryZone.lua @@ -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 diff --git a/modules/groundTroops.lua b/modules/groundTroops.lua index f97a3ec..cab5984 100644 --- a/modules/groundTroops.lua +++ b/modules/groundTroops.lua @@ -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 diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index 523d0d1..781362c 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -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! diff --git a/modules/income.lua b/modules/income.lua new file mode 100644 index 0000000..949e1f5 --- /dev/null +++ b/modules/income.lua @@ -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 \ No newline at end of file diff --git a/modules/milHelo.lua b/modules/milHelo.lua index 988f6d7..866895c 100644 --- a/modules/milHelo.lua +++ b/modules/milHelo.lua @@ -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) +--]]-- diff --git a/modules/ownedZones.lua b/modules/ownedZones.lua index 252a1c5..c08888a 100644 --- a/modules/ownedZones.lua +++ b/modules/ownedZones.lua @@ -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!", "*") @@ -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 + + --]]-- diff --git a/modules/playerScore.lua b/modules/playerScore.lua index 97ac4bd..13fe13f 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -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) diff --git a/modules/stopGaps standalone.lua b/modules/stopGaps standalone.lua index 84db1e4..27e215a 100644 --- a/modules/stopGaps standalone.lua +++ b/modules/stopGaps standalone.lua @@ -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) diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua index 0e169ee..5f986d2 100644 --- a/modules/stopGaps.lua +++ b/modules/stopGaps.lua @@ -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 diff --git a/modules/xpStrat.lua b/modules/xpStrat.lua new file mode 100644 index 0000000..8b9c9b9 --- /dev/null +++ b/modules/xpStrat.lua @@ -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 \ No newline at end of file