diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index cf5ba87..1d686ba 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 37f6728..b84007a 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/LZ.lua b/modules/LZ.lua index 25ea81d..3b88816 100644 --- a/modules/LZ.lua +++ b/modules/LZ.lua @@ -1,5 +1,5 @@ LZ = {} -LZ.version = "1.0.0" +LZ.version = "1.1.0" LZ.verbose = false LZ.ups = 1 LZ.requiredLibs = { @@ -14,6 +14,7 @@ LZ.LZs = {} Version History 1.0.0 - initial version + 1.1.0 - persistence --]]-- @@ -131,13 +132,12 @@ end -- Misc Processing -- function LZ.unitIsInterestingForZone(theUnit, theZone) - --trigger.action.outText("enter isInterestingB4pause for <" .. theUnit:getName() .. ">", 40) -- see if zone is interested in this unit. if theZone.isPaused then return false end --- trigger.action.outText("enter isinteresting for <" .. theUnit:getName() .. ">", 40) + if theZone.lzPlayerOnly then if not dcsCommon.isPlayerUnit(theUnit) then if theZone.verbose or LZ.verbose then @@ -198,11 +198,7 @@ function LZ.unitIsInterestingForZone(theUnit, theZone) else -- we can return true since player and coa mismatch -- have already been filtered ---[[-- -- neither type, unit, nor group - local theGroup = theUnit:getGroup() - local coa = theGroup:getCoalition() - -- ---]]-- + return true -- theZone.coalition == coa end end @@ -223,19 +219,11 @@ function LZ:onEvent(event) event.id ~= world.event.S_EVENT_LAND then return end - - --if LZ.verbose or true then - -- trigger.action.outText("+++LZ: on event proccing", 30) - --end local theUnit = event.initiator if not Unit.isExist(theUnit) then return end local p = theUnit:getPoint() - --if LZ.verbose or true then - -- trigger.action.outText("+++LZ: before iterating zones", 30) - --end - for idx, aZone in pairs(LZ.LZs) do -- see if inside the zone local inZone, percent, dist = cfxZones.pointInZone(p, aZone) @@ -258,8 +246,8 @@ function LZ:onEvent(event) end end -- if interesting else - if LZ.verbose or true then - -- trigger.action.outText("+++LZ: unit <" .. theUnit:getName() .. "> not in zone <" .. aZone.name .. ">", 30) + if LZ.verbose or aZone.verbose then + --trigger.action.outText("+++LZ: unit <" .. theUnit:getName() .. "> not in zone <" .. aZone.name .. ">", 30) end end -- if in zone @@ -292,13 +280,58 @@ function LZ.update() end +-- +-- LOAD / SAVE +-- +function LZ.saveData() + local theData = {} + local allLZ = {} + for idx, theLZ in pairs(LZ.LZs) do + local theName = theLZ.name + local LZData = {} + LZData.isPaused = theLZ.isPaused + + allLZ[theName] = LZData + end + theData.allLZ = allLZ + return theData +end + +function LZ.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("LZ") + if not theData then + if LZ.verbose then + trigger.action.outText("+++LZ persistence: no save data received, skipping.", 30) + end + return + end + + local allLZ = theData.allLZ + if not allLZ then + if LZ.verbose then + trigger.action.outText("+++LZ persistence: no LZ data, skipping", 30) + end + return + end + + for theName, theData in pairs(allLZ) do + local theLZ = LZ.getLZByName(theName) + if theLZ then + theLZ.isPaused = theData.isPaused + else + trigger.action.outText("+++LZ: persistence: cannot synch LZ <" .. theName .. ">, skipping", 40) + end + end +end + -- -- Config & Start -- function LZ.readConfigZone() local theZone = cfxZones.getZoneByName("LZConfig") if not theZone then - theZone = cfxZones.createSimpleZone(LZConfig) + theZone = cfxZones.createSimpleZone("LZConfig") if LZ.verbose then trigger.action.outText("+++LZ: NO config zone!", 30) end @@ -336,6 +369,16 @@ function LZ.start() -- connect event handler world.addEventHandler(LZ) + -- load any saved data + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = LZ.saveData + persistence.registerModule("LZ", callbacks) + -- now load my data + LZ.loadData() + end + -- start update LZ.update() diff --git a/modules/autoCSAR.lua b/modules/autoCSAR.lua new file mode 100644 index 0000000..04ef3d5 --- /dev/null +++ b/modules/autoCSAR.lua @@ -0,0 +1,112 @@ +autoCSAR = {} +autoCSAR.version = "1.0.0" +autoCSAR.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} +autoCSAR.killDelay = 2 * 60 +autoCSAR.counter = 31 -- any nuber is good, to kick-off counting +--[[-- + VERSION HISTORY + 1.0.0 - Initial Version +--]]-- + +function autoCSAR.removeGuy(args) + local theGuy = args.theGuy + if theGuy and theGuy:isExist() then + Unit.destroy(theGuy) + end +end + +function autoCSAR.createNewCSAR(theUnit) + if not csarManager then + trigger.action.outText("+++aCSAR: CSAR Manager not loaded, aborting", 30) + -- return + end + -- enter with unit from landing_after_eject event + -- unit has no group + local coa = theUnit:getCoalition() + if coa == 0 then -- neutral + trigger.action.outText("Neutal Pilot made it safely to ground.", 30) + return + end + if coa == 1 and not autoCSAR.redCSAR then + return -- we don't do red + end + if coa == 2 and not autoCSAR.blueCSAR then + return -- no blue rescue + end + + -- for later expansion + local theGroup = theUnit:getGroup() + if theGroup then + trigger.action.outText("We have a group for <" .. theUnit:getName() .. ">", 30) + end + + -- create a CSAR mission now + csarManager.createCSARForParachutist(theUnit, "Xray-" .. autoCSAR.counter) + autoCSAR.counter = autoCSAR.counter + 1 + + -- schedule removal of pilot + local args = {} + args.theGuy = theUnit + timer.scheduleFunction(autoCSAR.removeGuy, args, timer.getTime() + autoCSAR.killDelay) +end + +function autoCSAR:onEvent(event) + if event.id == 31 then -- landing_after_eject + if event.initiator then + autoCSAR.createNewCSAR(event.initiator) + end + end +end + +function autoCSAR.readConfigZone() + local theZone = cfxZones.getZoneByName("autoCSARConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("autoCSARConfig") + if autoCSAR.verbose then + trigger.action.outText("+++aCSAR: NO config zone!", 30) + end + end + + autoCSAR.redCSAR = cfxZones.getBoolFromZoneProperty(theZone, "red", true) + if cfxZones.hasProperty(theZone, "redCSAR") then + autoCSAR.redCSAR = cfxZones.getBoolFromZoneProperty(theZone, "redCSAR", true) + end + + autoCSAR.blueCSAR = cfxZones.getBoolFromZoneProperty(theZone, "blue", true) + if cfxZones.hasProperty(theZone, "blueCSAR") then + autoCSAR.blueCSAR = cfxZones.getBoolFromZoneProperty(theZone, "blueCSAR", true) + end + + if autoCSAR.verbose then + trigger.action.outText("+++aCSAR: read config", 30) + end +end + +function autoCSAR.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx autoCSAR requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx autoCSAR", autoCSAR.requiredLibs) then + return false + end + + -- read config + autoCSAR.readConfigZone() + + -- connect event handler + world.addEventHandler(autoCSAR) + + trigger.action.outText("cfx autoCSAR v" .. autoCSAR.version .. " started.", 30) + return true +end + +-- let's go! +if not autoCSAR.start() then + trigger.action.outText("cfx autoCSAR aborted: missing libraries", 30) + autoCSAR = nil +end diff --git a/modules/cfxArtilleryZones.lua b/modules/cfxArtilleryZones.lua index cbc21cb..4126f0b 100644 --- a/modules/cfxArtilleryZones.lua +++ b/modules/cfxArtilleryZones.lua @@ -1,5 +1,5 @@ cfxArtilleryZones = {} -cfxArtilleryZones.version = "2.2.1" +cfxArtilleryZones.version = "2.2.2" cfxArtilleryZones.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -30,6 +30,7 @@ cfxArtilleryZones.verbose = false - code cleanup 2.2.0 - DML Watchflag integration 2.2.1 - minor code clean-up + 2.2.2 - new doParametricFireAt() Artillery Target Zones *** EXTENDS ZONES *** Target Zones for artillery. Can determine which zones are in range and visible and then handle artillery barrage to this zone @@ -258,6 +259,37 @@ function cfxArtilleryZones.doBoom(args) cfxArtilleryZones.invokeCallbacksFor('impact', args.zone, data) end +function cfxArtilleryZones.doParametricFireAt(aPoint, accuracy, shellNum, shellBaseStrength, shellVariance, transitionTime) + -- accuracy is meters from center + if not aPoint then return end + if not accuracy then accuracy = 100 end + if not shellNum then shellNum = 17 end + if not shellBaseStrength then shellBaseStrength = 500 end + if not shellVariance then shellVariance = 0.2 end + if not transitionTime then transitionTime = 17 end + + local alt = land.getHeight({x=aPoint.x, y=aPoint.z}) + local center = {x=aPoint.x, y=alt, z=aPoint.z} + + for i=1, shellNum do + local thePoint = dcsCommon.randomPointInCircle(accuracy, 0, center.x, center.z) + thePoint.y = land.getHeight({x=thePoint.x, y=thePoint.z}) + local boomArgs = {} + local strVar = shellBaseStrength * shellVariance + strVar = strVar * (2 * dcsCommon.randomPercent() - 1.0) -- go from -1 to 1 + + boomArgs.strength = shellBaseStrength + strVar + thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1 + boomArgs.point = thePoint + boomArgs.zone = aZone + local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds + if timeVar < 0 then timeVar = -timeVar end + + timer.scheduleFunction(cfxArtilleryZones.doBoom, boomArgs, timer.getTime() + transitionTime + timeVar) + end +end + + function cfxArtilleryZones.doFireAt(aZone, maxDistFromCenter) if type(aZone) == "string" then local mZone = cfxArtilleryZones.findArtilleryZoneNamed(aZone) diff --git a/modules/cfxHeloTroops.lua b/modules/cfxHeloTroops.lua index a4525bd..4d95c8c 100644 --- a/modules/cfxHeloTroops.lua +++ b/modules/cfxHeloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "2.2.0" +cfxHeloTroops.version = "2.3.0" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -22,7 +22,9 @@ cfxHeloTroops.pickupRange = 100 -- meters -- -- (re?) connected readConfigZone (wtf?) -- -- persistence support -- -- made legalTroops entrirely optional and defer to dcsComon else - +-- 2.3.0 -- interface with owned zones and playerScore when +-- -- combat-dropping troops into non-owned owned zone. +-- -- prevent auto-load from pre-empting loading csar troops -- -- cfxHeloTroops -- a module to pick up and drop infantry. Can be used with any helo, -- might be used to configure to only certain @@ -511,30 +513,39 @@ end function cfxHeloTroops.filterTroopsByType(unitsToLoad) local filteredGroups = {} - for idx, aTeam in pairs(unitsToLoad) do - local group = aTeam.group - local theTypes = dcsCommon.getGroupTypeString(group) + for idx, aTeam in pairs(unitsToLoad) do + local group = aTeam.group + local theTypes = dcsCommon.getGroupTypeString(group) - local aT = dcsCommon.splitString(theTypes, ",") - local pass = true - for iT, sT in pairs(aT) do - -- check if this is a valid type - if cfxHeloTroops.legalTroops then - if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, sT) then - pass = false - break - end - else - if not dcsCommon.typeIsInfantry(sT) then - pass = false - break - end + local aT = dcsCommon.splitString(theTypes, ",") + local pass = true + for iT, sT in pairs(aT) do + -- check if this is a valid type + if cfxHeloTroops.legalTroops then + if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, sT) then + pass = false + break end - end - if pass then - table.insert(filteredGroups, aTeam) + else + if not dcsCommon.typeIsInfantry(sT) then + pass = false + break + end + end + end + -- check if we are about to pre-empt a CSAR mission + if csarManager then + if csarManager.isCSARTarget(group) then + -- this one is managed by csarManager, + -- don't load it for helo troops + pass = false end end + + if pass then + table.insert(filteredGroups, aTeam) + end + end return filteredGroups end -- @@ -578,12 +589,43 @@ function cfxHeloTroops.redirectDeployTroops(args) timer.scheduleFunction(cfxHeloTroops.doDeployTroops, args, timer.getTime() + 0.1) end +function cfxHeloTroops.scoreWhenCapturing(theUnit) + if theUnit and Unit.isExist(theUnit) and theUnit.getPlayerName then + -- see if wer are inside a non-alinged zone + -- and this includes a neutral zone + local coa = theUnit:getCoalition() + local p = theUnit:getPoint() + local theGroup = theUnit:getGroup() + local ID = theGroup:getID() + local nearestZone, dist = cfxOwnedZones.getNearestOwnedZoneToPoint(p) + if nearestZone and dist < nearestZone.radius then + -- we are inside an owned zone! + if nearestZone.owner ~= coa then + -- yup, combat drop! + local theScore = cfxHeloTroops.combatDropScore + local pName = theUnit:getPlayerName() + if pName then + cfxPlayerScore.updateScoreForPlayer(pName, theScore) + cfxPlayerScore.logFeatForPlayer(pName, "Combat Troop Insertion at " .. nearestZone.name, coa) + end + end + end + end +end + function cfxHeloTroops.doDeployTroops(args) local conf = args[1] local what = args[2] -- deploy the troops I have on board in formation cfxHeloTroops.deployTroopsFromHelicopter(conf) + -- interface with playerscore if we dropped + -- inside an enemy-owned zone + if cfxPlayerScore and cfxOwnedZones then + local theUnit = conf.unit + cfxHeloTroops.scoreWhenCapturing(theUnit) + end + -- set own troops to 0 and erase type string conf.troopsOnBoardNum = 0 conf.troopsOnBoard = {} @@ -840,7 +882,7 @@ function cfxHeloTroops.readConfigZone() cfxHeloTroops.autoDrop = cfxZones.getBoolFromZoneProperty(theZone, "autoDrop", false) cfxHeloTroops.autoPickup = cfxZones.getBoolFromZoneProperty(theZone, "autoPickup", false) cfxHeloTroops.pickupRange = cfxZones.getNumberFromZoneProperty(theZone, "pickupRange", 100) - + cfxHeloTroops.combatDropScore = cfxZones.getNumberFromZoneProperty(theZone, "combatDropScore", 200) end -- diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index c411f87..d127f40 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "1.2.3" +cfxMX.version = "1.2.4" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -23,7 +23,7 @@ cfxMX.verbose = false - playerUnitByName 1.2.3 - groupTypeByName - groupCoalitionByName - + 1.2.4 - playerUnit2Group cross index --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} @@ -40,6 +40,7 @@ cfxMX.allStaticByName = {} cfxMX.playerGroupByName = {} -- returns data only if a player is in group cfxMX.playerUnitByName = {} -- returns data only if this is a player unit +cfxMX.playerUnit2Group = {} -- returns a group data for player units. function cfxMX.getGroupFromDCSbyName(aName, fetchOriginal) if not fetchOriginal then fetchOriginal = false end @@ -240,6 +241,7 @@ function cfxMX.createCrossReferences() -- player unit cfxMX.playerUnitByName[unit_data.name] = unit_data cfxMX.playerGroupByName[aName] = group_data -- inefficient, but works + cfxMX.playerUnit2Group[unit_data.name] = group_data end -- if unit skill client end -- if has skill end -- for all units diff --git a/modules/cfxOwnedZones.lua b/modules/cfxOwnedZones.lua index 2cf280d..f38baa6 100644 --- a/modules/cfxOwnedZones.lua +++ b/modules/cfxOwnedZones.lua @@ -1,5 +1,5 @@ cfxOwnedZones = {} -cfxOwnedZones.version = "1.2.1" +cfxOwnedZones.version = "1.2.2" cfxOwnedZones.verbose = false cfxOwnedZones.announcer = true cfxOwnedZones.name = "cfxOwnedZones" @@ -46,6 +46,8 @@ cfxOwnedZones.name = "cfxOwnedZones" - conq+1 --> conquered! - no cfxGroundTroop bug (no delay) 1.2.1 - fix in load to correctly re-establish all attackers for subsequent save +1.2.2 - redCap! and blueCap! + --]]-- cfxOwnedZones.requiredLibs = { @@ -218,11 +220,20 @@ function cfxOwnedZones.addOwnedZone(aZone) local paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false) aZone.paused = paused - aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*") - if cfxZones.hasProperty(aZone, "conq+1") then + if cfxZones.hasProperty(aZone, "conquered!") then + aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*") + elseif cfxZones.hasProperty(aZone, "conq+1") then aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conq+1", "*") end + if cfxZones.hasProperty(aZone, "redCap!") then + aZone.redCap = cfxZones.getStringFromZoneProperty(aZone, "redCap!", "none") + end + + if cfxZones.hasProperty(aZone, "blueCap!") then + aZone.blueCap = cfxZones.getStringFromZoneProperty(aZone, "blueCap!", "none") + end + aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false) aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false) aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false) @@ -517,12 +528,19 @@ function cfxOwnedZones.zoneConquered(aZone, theSide, formerOwner) -- 0 = neutral trigger.action.outSoundForCoalition(1, "Death BRASS.wav") end end - -- increase conq flag --- if aZone.conqueredFlag then - -- local lastVal = trigger.misc.getUserFlag(aZone.conqueredFlag) - -- trigger.action.setUserFlag(aZone.conqueredFlag, lastVal + 1) - cfxZones.pollFlag(aZone.conqueredFlag, "inc", aZone) --- end + + if aZone.conqueredFlag then + cfxZones.pollFlag(aZone.conqueredFlag, "inc", aZone) + end + + if theSide == 1 and aZone.redCap then + cfxZones.pollFlag(aZone.redCap, "inc", aZone) + end + + if theSide == 2 and aZone.blueCap then + cfxZones.pollFlag(aZone.blueCap, "inc", aZone) + end + -- invoke callbacks now cfxOwnedZones.invokeConqueredCallbacks(aZone, theSide, formerOwner) diff --git a/modules/cfxPlayerScore.lua b/modules/cfxPlayerScore.lua index 393cd5b..8537813 100644 --- a/modules/cfxPlayerScore.lua +++ b/modules/cfxPlayerScore.lua @@ -1,5 +1,5 @@ cfxPlayerScore = {} -cfxPlayerScore.version = "1.4.0" +cfxPlayerScore.version = "1.5.1" cfxPlayerScore.badSound = "Death BRASS.wav" cfxPlayerScore.scoreSound = "Quest Snare 3.wav" cfxPlayerScore.announcer = true @@ -27,9 +27,13 @@ cfxPlayerScore.announcer = true - removed dependency to cfxPlayer 1.4.0 - persistence support - better unit-->static switch support for generic type kill - + 1.5.0 - added feats to score + - feats API + - logFeatForPlayer(playerName, theFeat, coa) + 1.5.1 - init feats before reading --]]-- + cfxPlayerScore.requiredLibs = { "dcsCommon", -- this is doing score keeping "cfxZones", -- zones for config @@ -148,8 +152,10 @@ function cfxPlayerScore.getPlayerScore(playerName) thePlayerScore = {} thePlayerScore.name = playerName thePlayerScore.score = 0 -- score - thePlayerScore.killTypes = {} -- the type strings killed + thePlayerScore.killTypes = {} -- the type strings killed, dict thePlayerScore.totalKills = 0 -- number of kills total + thePlayerScore.featTypes = {} -- dict of other things player did + thePlayerScore.totalFeats = 0 end return thePlayerScore end @@ -184,12 +190,51 @@ function cfxPlayerScore.logKillForPlayer(playerName, theUnit) cfxPlayerScore.setPlayerScore(playerName, thePlayerScore) end +function cfxPlayerScore.logFeatForPlayer(playerName, theFeat, coa) + if not theFeat then return end + if not playerName then return end + -- access player's record. will alloc if new by itself + local thePlayerScore = cfxPlayerScore.getPlayerScore(playerName) + if not thePlayerScore.featTypes then thePlayerScore.featTypes = {} end + local featCount = thePlayerScore.featTypes[theFeat] + if featCount == nil then + featCount = 0 + end + featCount = featCount + 1 + thePlayerScore.totalFeats = thePlayerScore.totalFeats + 1 + thePlayerScore.featTypes[theFeat] = featCount + + if coa then + trigger.action.outTextForCoalition(coa, playerName .. " achieved " .. theFeat, 30) + trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound) + end + cfxPlayerScore.setPlayerScore(playerName, thePlayerScore) +end + function cfxPlayerScore.playerScore2text(thePlayerScore) local desc = thePlayerScore.name .. " - score: ".. thePlayerScore.score .. " - kills: " .. thePlayerScore.totalKills .. "\n" -- now go through all killSide + if dcsCommon.getSizeOfTable(thePlayerScore.killTypes) < 1 then + desc = desc .. " - NONE -\n" + end for theType, quantity in pairs(thePlayerScore.killTypes) do desc = desc .. " - " .. theType .. ": " .. quantity .. "\n" end + + -- now enumerate all feats + if not thePlayerScore.featTypes then thePlayerScore.featTypes = {} end + + desc = desc .. "\nOther Accomplishments:\n" + if dcsCommon.getSizeOfTable(thePlayerScore.featTypes) < 1 then + desc = desc .. " - NONE -\n" + end + for theFeat, quantity in pairs(thePlayerScore.featTypes) do + desc = desc .. " - " .. theFeat + if quantity > 1 then + desc = desc .. " (x" .. quantity .. ")" + end + desc = desc .. "\n" + end return desc end diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index 0d6389e..fc3177c 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.1.2" +cfxReconMode.version = "2.1.3" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -18,6 +18,7 @@ cfxReconMode.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } +cfxReconMode.name = "cfxReconMode" -- to be compatible with test flags --[[-- VERSION HISTORY @@ -82,6 +83,7 @@ VERSION HISTORY 2.1.2 - imperialUnits for elevation - wildcard in message format - fix for mgrs bug in message (zone coords, not unit) + 2.1.3 - added cfxReconMode.name to allow direct acces with test zone flag cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 534ecd5..105ecd7 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "2.8.5" +cfxZones.version = "2.8.7" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -92,6 +92,9 @@ cfxZones.version = "2.8.5" - 2.8.5 - createGroundUnitsInZoneForCoalition() now always passes back a copy of the group data - data also contains cty = country and cat = category for easy spawn - getFlagValue additional zone name guards +- 2.8.6 - fix in getFlagValue for missing delay +- 2.8.7 - update isPointInsideZone(thePoint, theZone, radiusIncrease) - new radiusIncrease + - isPointInsideZone() returns delta as well --]]-- cfxZones.verbose = false @@ -507,17 +510,19 @@ function cfxZones.isPointInsidePoly(thePoint, poly) return true end; -function cfxZones.isPointInsideZone(thePoint, theZone) +function cfxZones.isPointInsideZone(thePoint, theZone, radiusIncrease) + -- radiusIncrease only works for circle zones + if not radiusIncrease then radiusIncrease = 0 end local p = {x=thePoint.x, y = 0, z = thePoint.z} -- zones have no altitude if (theZone.isCircle) then local zp = cfxZones.getPoint(theZone) local d = dcsCommon.dist(p, theZone.point) - return d < theZone.radius + return d < theZone.radius + radiusIncrease, d end if (theZone.isPoly) then --trigger.action.outText("zne: isPointInside: " .. theZone.name .. " is Polyzone!", 30) - return (cfxZones.isPointInsidePoly(p, theZone.poly)) + return (cfxZones.isPointInsidePoly(p, theZone.poly)), 0 -- always returns delta 0 end trigger.action.outText("isPointInsideZone: Unknown zone type for " .. outerZone.name, 10) @@ -1324,7 +1329,7 @@ end function cfxZones.getFlagValue(theFlag, theZone) local zoneName = "" if not theZone or not theZone.name then - trigger.action.outText("+++Zne: no zone or zone name on getFlagValue") + trigger.action.outText("+++Zne: no zone or zone name on getFlagValue", 30) else zoneName = theZone.name -- for flag wildcards end diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index af8b40c..c788958 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "2.1.3" +csarManager.version = "2.2.0" csarManager.verbose = false csarManager.ups = 1 @@ -41,6 +41,14 @@ csarManager.ups = 1 - 2.1.3 - theMassObject now local - winch pickup now also adds weight so they can be returned - made some improvements to performance by making vars local + - 2.2.0 - interface for autoCSAR + createDownedPilot() - added existingUnit option + createCSARMissionData() - added existinUnit option + - when no config zone, runs through empty zone + - actionSound + - integration with playerScore + - score global and per-mission + - isCSARTarget API --]]-- @@ -53,79 +61,13 @@ csarManager.requiredLibs = { "cargoSuper", -- "cfxCommander", -- needed if you want to hand-create CSAR missions } +-- integrates automatically with playerScore if installed --- *** DOES NOT EXTEND ZONES *** BUT USES OWN STRUCT --- *** extends zones for csarMission zones though ---[[-- - CSAR MANAGER - ============ - - This module can create and manage CSAR missions, i.e. - create a unit on the ground, mark it on the map, handle - if the unit is killed, create enemies in the vicinity - - It will install a menu in any troop helicopter as - determined by dcsCommon.isTroopCarrier() with the - option to list available csar mission. for each created mission - it will give range and frequency for ADF - When a helicopter is in range, it will set smoke to better - visually identify the location. - - When the helicopter lands close enough to a downed pilot, - the pilot is picket up automatically. Their weight is added - to the unit, so it may overload! - - When the helicopter than lands in a CSARBASE Zone, the mission is - a success and a success callback is invoked automatically for - all picked up groups. All zones that have the CSARBASE property are - CSAR Bases, but their coalition must be either neutral or match the - one of the unit that landed - - On start, it scans all zones for a CSAR property, and creates - a CSAR mission with data taken from the properties in the - zone so you can easily create CSAR missions in ME - - WARNING: ASSUMES SINGLE UNIT PLAYER GROUPS - ========================================== - - Main Interface - - createCSARMission(location, side, numCrew, mark, clearing, timeout) - creates a csar mission that can be tracked. - location is the position on the map - side is the side the unit is on (neutal is for any side) - numCrew the number of people (1-4) - mark true if marked on map - clearing will create a clearing - timeout - time in seconds until pilots die. timer stops on pickup - RETURNS true, "ok" -- false, "fail reason" (string) - - - createCSARAdversaries(location, side, numEnemies, radius, maxRadius) - creates some random infantery randomized on a circle around the location - location - center, usually where the downed pilot is - side - side of the enemy red/blue - numEnemies - number of infantry - radius[, maxRadius] distance of the enemy troops - - - in ME, create at least one zone with a property named "CSARBASE" for - each side that supports csar missions. This is where the players - can drop off pilots that they rescued. If you have no CSARBASE zone - defined, you'll receive a warning for that side when you attempt a - rescue - - - in ME you can place zones with a CSAR attribute that will generate - a scar mission. Further attributes are "coalition" (red/blue), "name" (any name you like) and "freq" (for elt ADR, leave empty for random) - - NOTE: - CSARBASE is compatible with the FARP Attribute of - FARP Zones - - ---]]-- -- -- OPTIONS -- -csarManager.useSmoke = false -- smoke is a performance killer, so you can turn it off +csarManager.useSmoke = true -- smoke is a performance killer, so you can turn it off csarManager.smokeColor = 4 -- when using smoke @@ -158,39 +100,49 @@ csarManager.csarCompleteCB = {} -- -- CREATING A CSAR -- -function csarManager.createDownedPilot(theMission) +function csarManager.createDownedPilot(theMission, existingUnit) + if not cfxCommander then trigger.action.outText("+++CSAR: can't create mission, module cfxCommander is missing.", 30) return end - local aLocation = {} - local aHeading = 0 -- in rads - local newTargetZone = theMission.zone - aLocation, aHeading = dcsCommon.randomPointOnPerimeter(newTargetZone.radius / 2 + 3, newTargetZone.point.x, newTargetZone.point.z) + if not existingUnit then + local aLocation = {} + local aHeading = 0 -- in rads + local newTargetZone = theMission.zone + if newTargetZone.radius > 1 then + aLocation, aHeading = dcsCommon.randomPointOnPerimeter(newTargetZone.radius / 2 + 3, newTargetZone.point.x, newTargetZone.point.z) + else + aLocation.x = newTargetZone.point.x + aLocation.z = newTargetZone.point.z + aHeading = math.random(360)/360 * 2 * 3.1415 + end - local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name, - "Soldier M4 GRG", -- "Soldier M4 GRG", - aLocation.x, - aLocation.z, - -aHeading + 1.5) -- + 1.5 to turn inwards - - -- WARNING: - -- coalition.addGroup takes the COUNTRY of the group, and derives the - -- coalition from that. So if mission.sie is 0, we use UN, if it is 1 (red) it - -- is joint red, if 2 it is joint blue - local theSideCJTF = dcsCommon.coalition2county(theMission.side) -- get the correct county CJTF - theMission.group = coalition.addGroup(theSideCJTF, - Group.Category.GROUND, - theBoyGroup) - - if theBoyGroup then + local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name, + "Soldier M4 GRG", -- "Soldier M4 GRG", + aLocation.x, + aLocation.z, + -aHeading + 1.5) -- + 1.5 to turn inwards + + -- WARNING: + -- coalition.addGroup takes the COUNTRY of the group, and derives the + -- coalition from that. So if mission.sie is 0, we use UN, if it is 1 (red) it + -- is joint red, if 2 it is joint blue + local theSideCJTF = dcsCommon.coalition2county(theMission.side) -- get the correct county CJTF + theMission.group = coalition.addGroup(theSideCJTF, + Group.Category.GROUND, + theBoyGroup) + + if theBoyGroup then + else + trigger.action.outText("+++csar: FAILED to create csar!", 30) + end else - trigger.action.outText("+++csar: FAILED to create csar!", 30) + theMission.group = existingUnit:getGroup() end - -- we now use commands to send radio transmissions local ADF = 20 + math.random(90) if theMission.freq then ADF = theMission.freq else theMission.freq = ADF end @@ -202,7 +154,7 @@ function csarManager.createDownedPilot(theMission) cfxCommander.scheduleCommands(theCommands, 2) -- in 2 seconds, so unit has time to percolate through DCS end -function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, timeLimit, mapMarker, inRadius) +function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, timeLimit, mapMarker, inRadius, parashootUnit) -- if parashootUnit is set, will not allocate new -- create a type if not timeLimit then timeLimit = -1 end if not point then return nil end @@ -228,7 +180,7 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, newMission.freq = freq -- if nil will make random -- allocate units - csarManager.createDownedPilot(newMission) + csarManager.createDownedPilot(newMission, parashootUnit) -- update counter and return csarManager.missionID = csarManager.missionID + 1 @@ -262,6 +214,13 @@ function csarManager.removeMissionForGroup(theDownedGroup) end csarManager.openMissions = newMissions -- this is the new batch end + +function csarManager.isCSARTarget(theGroup) + for idx, theMission in pairs(csarManager.openMissions) do + if theMission.group == theGroup then return true end + end + return false +end -- -- UNIT CONFIG -- @@ -308,7 +267,9 @@ end function csarManager.removeConfigForUnitNamed(aName) if not aName then return end - if csarManager.unitConfigs[aName] then csarManager.unitConfigs[aName] = nil end + if csarManager.unitConfigs[aName] then + csarManager.unitConfigs[aName] = nil + end end -- @@ -376,6 +337,25 @@ end -- function csarManager.successMission(who, where, theMission) + -- who is + -- where is + -- theMission is mission table + + -- playerScore integration + if cfxPlayerScore then + local theScore = theMission.score + if not theScore then theScore = csarManager.rescueScore end + + local theUnit = Unit.getByName(who) + if theUnit and theUnit.getPlayerName then + local pName = theUnit:getPlayerName() + if pName then + cfxPlayerScore.updateScoreForPlayer(pName, theScore) + cfxPlayerScore.logFeatForPlayer(pName, "Evacuated " .. theMission.name) + end + end + end + trigger.action.outTextForCoalition(theMission.side, who .. " successfully evacuated " .. theMission.name .. " to " .. where .. "!", 30) @@ -385,7 +365,7 @@ function csarManager.successMission(who, where, theMission) csarManager.invokeCallbacks(theMission.side, true, 1, "success") - trigger.action.outSoundForCoalition(theMission.side, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(theMission.side, csarManager.actionSound) -- "Quest Snare 3.wav") if csarManager.csarRedDelivered and theMission.side == 1 then cfxZones.pollFlag(csarManager.csarRedDelivered, "inc", csarManager.configZone) @@ -536,7 +516,7 @@ function csarManager.heloLanded(theUnit) theMassObject) end if didPickup then - trigger.action.outSoundForCoalition(mySide, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(mySide, csarManager.actionSound) -- "Quest Snare 3.wav") end -- reset unit's weight based on people on board local totalMass = cargoSuper.calculateTotalMassFor(myName) @@ -756,7 +736,7 @@ function csarManager.doListCSARRequests(args) report = report .. "\n" trigger.action.outTextForGroup(conf.id, report, 30) - trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") end function csarManager.redirectStatusCarrying(args) @@ -789,7 +769,7 @@ function csarManager.doStatusCarrying(args) report = report .. "\n" trigger.action.outTextForGroup(conf.id, report, 30) - trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") end function csarManager.redirectUnloadOne(args) @@ -807,14 +787,14 @@ function csarManager.unloadOne(args) if theUnit:inAir() then report = "STRONGLY recommend we land first, Sir!" trigger.action.outTextForGroup(conf.id, report, 30) - trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") return end if #conf.troopsOnBoard < 1 then report = "No evacuees on board." trigger.action.outTextForGroup(conf.id, report, 30) - trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") else -- simulate a crash but for one unit @@ -829,7 +809,7 @@ function csarManager.unloadOne(args) --TODO: remove weight for this pilot! trigger.action.outTextForCoalition(theSide, myName .. " has aborted evacuating " .. msn.name .. ". New CSAR available.", 30) - trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(theSide, csarManager.actionSound) -- "Quest Snare 3.wav") -- recalc weight trigger.action.setUnitInternalCargo(myName, 10 + #conf.troopsOnBoard * csarManager.pilotWeight) -- 10 kg as empty + per-unit time people @@ -933,7 +913,7 @@ function csarManager.updateCSARMissions() else local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR." trigger.action.outTextForCoalition(aMission.side, msg, 30) - trigger.action.outSoundForCoalition(aMission.side, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(aMission.side, csarManager.actionSound) -- "Quest Snare 3.wav") end end csarManager.openMissions = newMissions -- this is the new batch @@ -981,7 +961,7 @@ function csarManager.update() -- every second end msg = msg .. "\n" trigger.action.outTextForGroup(uID, msg, 30) - trigger.action.outSoundForGroup(uID, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(uID, csarManager.actionSound) -- "Quest Snare 3.wav") table.insert(csarMission.messagedUnits, uName) -- remember that we messaged them so we don't do again end -- also pop smoke if not popped already, or more than 5 minutes ago @@ -1046,7 +1026,7 @@ function csarManager.update() -- every second end trigger.action.outTextForGroup(uID, hoverMsg, 30, true) - trigger.action.outSoundForGroup(uID, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(uID, csarManager.actionSound) --"Quest Snare 3.wav") return -- we only ever rescue one end -- hovered long enough @@ -1104,7 +1084,7 @@ end -- -- create a CSAR Mission for a unit -- -function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent) +function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent, score) -- invoked with aircraft as theUnit, usually still in air if not silent then silent = false end if not radius then radius = 1000 end if not pilotName then pilotName = "Eddie" end @@ -1121,7 +1101,7 @@ function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent) if surf == 2 or surf == 3 then if not silent then trigger.action.outTextForCoalition(coal, "Bad chute! Bad chute! ".. pilotName .. " did not survive ejection out of their " .. theUnit:getTypeName(), 30) - trigger.action.outSoundForGroup(coal, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(coal, csarManager.actionSound) -- "Quest Snare 3.wav") end return end @@ -1137,13 +1117,26 @@ function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent) 1, nil, nil) + theMission.score = score csarManager.addMission(theMission) if not silent then trigger.action.outTextForCoalition(coal, "MAYDAY MAYDAY MAYDAY! ".. pilotName .. " in " .. theUnit:getTypeName() .. " ejected, report good chute. Prepare CSAR!", 30) - trigger.action.outSoundForGroup(coal, "Quest Snare 3.wav") + trigger.action.outSoundForGroup(coal, csarManager.actionSound) -- "Quest Snare 3.wav") end end +function csarManager.createCSARForParachutist(theUnit, name) -- invoked with parachute guy on ground as theUnit + local coa = theUnit:getCoalition() + local pos = theUnit:getPoint() + -- unit DOES NOT HAVE GROUP!!! + -- create a CSAR mission now + local theMission = csarManager.createCSARMissionData(pos, coa, nil, name, nil, nil, nil, 0.1, nil) + csarManager.addMission(theMission) +-- if not silent then + trigger.action.outTextForCoalition(coa, "MAYDAY MAYDAY MAYDAY! ".. name .. " requesting extraction after eject!", 30) + trigger.action.outSoundForGroup(coa, csarManager.actionSound) -- "Quest Snare 3.wav") +-- end +end -- -- csar (mission) zones @@ -1199,6 +1192,9 @@ function csarManager.readCSARZone(theZone) theZone.lastCSARVal = cfxZones.getFlagValue(theZone.startCSAR, theZone) end + if cfxZones.hasProperty(theZone, "score") then + theZone.score = cfxZones.getNumberFromZoneProperty(theZone, "score", 100) + end if (not deferred) then local theMission = csarManager.createCSARMissionData( @@ -1240,9 +1236,6 @@ end -- Init & Start -- - - - function csarManager.invokeCallbacks(theCoalition, success, numRescued, notes) -- invoke anyone who wants to know that a group -- of people was rescued. @@ -1261,7 +1254,7 @@ function csarManager.readConfigZone() if csarManager.verbose then trigger.action.outText("+++csar: NO config zone!", 30) end - return + theZone = cfxZones.createSimpleZone("csarManagerConfig") end csarManager.configZone = theZone -- save for flag banging compatibility @@ -1284,7 +1277,7 @@ function csarManager.readConfigZone() if cfxZones.hasProperty(theZone, "csarDelivered!") then csarManager.csarDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarDelivered!", "*") - trigger.action.outText("+++csar: will bang csarDelivered: <" .. csarManager.csarDelivered .. ">", 30) + --trigger.action.outText("+++csar: will bang csarDelivered: <" .. csarManager.csarDelivered .. ">", 30) end csarManager.rescueRadius = cfxZones.getNumberFromZoneProperty(theZone, "rescueRadius", 70) --70 -- must land within 50m to rescue @@ -1295,6 +1288,9 @@ function csarManager.readConfigZone() csarManager.beaconSound = cfxZones.getStringFromZoneProperty(theZone, "beaconSound", "Radio_beacon_of_distress_on_121.ogg") --"Radio_beacon_of_distress_on_121,5_MHz.ogg" csarManager.pilotWeight = cfxZones.getNumberFromZoneProperty(theZone, "pilotWeight", 120) -- 120 + csarManager.rescueScore = cfxZones.getNumberFromZoneProperty(theZone, "rescueScore", 100) + + csarManager.actionSound = cfxZones.getStringFromZoneProperty(theZone, "actionSound", "Quest Snare 3.wav") csarManager.vectoring = cfxZones.getBoolFromZoneProperty(theZone, "vectoring", true) if csarManager.verbose then @@ -1373,4 +1369,5 @@ end - when unloading one by menu, update weight!!! + -- allow neutral pick-up --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 9a3bb0e..6acab1c 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.7.4" +dcsCommon.version = "2.7.5" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -101,6 +101,9 @@ dcsCommon.version = "2.7.4" 2.7.3 - new string2Array() - additional guard for isPlayerUnit 2.7.4 - new array2string() + 2.7.5 - new bitAND32() + - new LSR() + - new num2bin() --]]-- @@ -2720,6 +2723,62 @@ function dcsCommon.getSceneryObjectInZoneByName(theName, theZone) -- DCS ZONE!!! return nil end +-- +-- bitwise operators +-- +function dcsCommon.bitAND32(a, b) + if not a then a = 0 end + if not b then b = 0 end + local z = 0 + local e = 1 + for i = 0, 31 do + local a1 = a % 2 -- 0 or 1 + local b1 = b % 2 -- 0 or 1 + if a1 == 1 and b1 == 1 then + a = a - 1 -- remove bit + b = b - 1 + z = z + e + else + if a1 == 1 then a = a - 1 end -- remove bit + if b1 == 1 then b = b - 1 end + end + a = a / 2 -- shift right + b = b / 2 + e = e * 2 -- raise e by 1 + end + return z +end + +function dcsCommon.num2bin(a) + if not a then a = 0 end + local z = "" + for i = 0, 31 do + local a1 = a % 2 -- 0 or 1 + if a1 == 1 then + a = a - 1 -- remove bit + z = "1"..z + else + z = "0"..z + end + a = a / 2 -- shift right + end + return z +end + +function dcsCommon.LSR(a, num) + if not a then a = 0 end + if not num then num = 16 end + for i = 1, num do + local a1 = a % 2 -- 0 or 1 + if a1 == 1 then + a = a - 1 -- remove bit + end + a = a / 2 -- shift right + end + return a +end + + -- -- -- INIT diff --git a/modules/fireFX.lua b/modules/fireFX.lua index a79baa7..e039a91 100644 --- a/modules/fireFX.lua +++ b/modules/fireFX.lua @@ -1,5 +1,5 @@ fireFX = {} -fireFX.version = "1.0.0" +fireFX.version = "1.1.0" fireFX.verbose = false fireFX.ups = 1 fireFX.requiredLibs = { @@ -8,6 +8,13 @@ fireFX.requiredLibs = { } fireFX.fx = {} +--[[-- + Version History + 1.0.0 - Initial version + 1.1.0 - persistence + +--]]-- + function fireFX.addFX(theZone) table.insert(fireFX.fx, theZone) end @@ -127,13 +134,62 @@ function fireFX.update() end +-- +-- LOAD / SAVE +-- +function fireFX.saveData() + local theData = {} + local allFX = {} + for idx, theFX in pairs(fireFX.fx) do + local theName = theFX.name + local FXData = {} + FXData.burning = theFX.burning + + allFX[theName] = FXData + end + theData.allFX = allFX + return theData +end + +function fireFX.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("fireFX") + if not theData then + if fireFX.verbose then + trigger.action.outText("+++ffx persistence: no save data received, skipping.", 30) + end + return + end + + local allFX = theData.allFX + if not allFX then + if fireFX.verbose then + trigger.action.outText("+++ffx persistence: no fire FX data, skipping", 30) + end + return + end + + for theName, theData in pairs(allFX) do + local theFX = fireFX.getFXByName(theName) + if theFX then + if theData.burning then + fireFX.startTheFire(theFX) + end + theFX.inited = true -- ensure no onStart overwrite + else + trigger.action.outText("+++ffx: persistence: cannot synch fire FX <" .. theName .. ">, skipping", 40) + end + end +end + + -- -- Config & Start -- function fireFX.readConfigZone() local theZone = cfxZones.getZoneByName("fireFXConfig") if not theZone then - theZone = cfxZones.createSimpleZone(LZConfig) + theZone = cfxZones.createSimpleZone("fireFX") if fireFX.verbose then trigger.action.outText("+++ffx: NO config zone!", 30) end @@ -165,9 +221,20 @@ function fireFX.start() fireFX.addFX(aZone) -- add to list end + -- load any saved data + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = fireFX.saveData + persistence.registerModule("fireFX", callbacks) + -- now load my data + fireFX.loadData() + end + -- handle onStart for idx, theZone in pairs(fireFX.fx) do - if theZone.fxOnStart then + if (not theZone.inited) and (theZone.fxOnStart) then + -- only if we did not init them with loaded data fireFX.startTheFire(theZone) end end diff --git a/modules/limitedAirframes.lua b/modules/limitedAirframes.lua index 97371b6..c5793be 100644 --- a/modules/limitedAirframes.lua +++ b/modules/limitedAirframes.lua @@ -52,6 +52,8 @@ limitedAirframes.requiredLibs = { - 1.4.1 - removed dependency to cfxPlayer - 1.5.0 - persistence support - 1.5.1 - new "announcer" attribute + - 1.5.2 - integration with autoCSAR: prevent limitedAF from creating csar + when autoCSAR is active --]]-- @@ -665,6 +667,9 @@ end function limitedAirframes.createCSAR(theUnit) + -- override if autoCSAR is installed + if autoCSAR then return end + -- only do this if we have installed CSAR Manager if csarManager and csarManager.createCSARforUnit then csarManager.createCSARforUnit(theUnit, diff --git a/modules/messenger.lua b/modules/messenger.lua index 626d573..87b7f82 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "2.0.0" +messenger.version = "2.0.1" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -42,6 +42,7 @@ messenger.messengers = {} - messageError - unit - group + 2.0.1 - config optimization --]]-- @@ -480,7 +481,7 @@ function messenger.readConfigZone() if messenger.verbose then trigger.action.outText("+++msgr: NO config zone!", 30) end - return + theZone = cfxZones.createSimpleZone("messengerConfig") end messenger.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) diff --git a/modules/pulseFlags.lua b/modules/pulseFlags.lua index 10b5bef..dc511d6 100644 --- a/modules/pulseFlags.lua +++ b/modules/pulseFlags.lua @@ -1,5 +1,5 @@ pulseFlags = {} -pulseFlags.version = "1.3.0" +pulseFlags.version = "1.3.1" pulseFlags.verbose = false pulseFlags.requiredLibs = { "dcsCommon", -- always @@ -36,6 +36,7 @@ pulseFlags.requiredLibs = { - 1.2.3 deprecated paused/pulsePaused returned onStart, defaulting to true - 1.3.0 persistence + - 1.3.1 typos corrected --]]-- @@ -326,7 +327,7 @@ function pulseFlags.loadData() local theData = persistence.getSavedDataForModule("pulseFlags") if not theData then if pulseFlags.verbose then - trigger.action.outText("+++pulF Persistence: no save date received, skipping.", 30) + trigger.action.outText("+++pulF Persistence: no save data received, skipping.", 30) end return end diff --git a/modules/radioTrigger.lua b/modules/radioTrigger.lua index 9914014..c239e86 100644 --- a/modules/radioTrigger.lua +++ b/modules/radioTrigger.lua @@ -1,5 +1,5 @@ radioTrigger = {} -radioTrigger.version = "1.0.0" +radioTrigger.version = "1.0.1" radioTrigger.verbose = false radioTrigger.ups = 1 radioTrigger.requiredLibs = { @@ -11,6 +11,7 @@ radioTrigger.radioTriggers = {} --[[-- Version History 1.0.0 - initial version + 1.0.1 - guarding rtOut to not bang on flags when not set --]]-- @@ -50,8 +51,9 @@ function radioTrigger.createRadioTriggerWithZone(theZone) end -- out flag - theZone.rtOutFlag = cfxZones.getStringFromZoneProperty(theZone, "out!", "*") - if cfxZones.hasProperty(theZone, "rtOut!") then + if cfxZones.hasProperty(theZone, "out!") then + theZone.rtOutFlag = cfxZones.getStringFromZoneProperty(theZone, "out!", "*") + elseif cfxZones.hasProperty(theZone, "rtOut!") then theZone.rtOutFlag = cfxZones.getStringFromZoneProperty(theZone, "rtOut!", "*") end @@ -66,8 +68,9 @@ end -- function radioTrigger.process(theZone) -- we are triggered, simply poll the out flag - cfxZones.pollFlag(theZone.rtOutFlag, theZone.rtMethod, theZone) - + if theZone.rtOutFlag then + cfxZones.pollFlag(theZone.rtOutFlag, theZone.rtMethod, theZone) + end end -- diff --git a/modules/williePete.lua b/modules/williePete.lua new file mode 100644 index 0000000..aa79fc2 --- /dev/null +++ b/modules/williePete.lua @@ -0,0 +1,648 @@ +williePete = {} +williePete.version = "1.0.0" +williePete.ups = 10 -- we update at 10 fps, so accuracy of a +-- mach two missile is within 33 meters, with interpolation even less + +williePete.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course + "cfxMX", +} + +williePete.willies = {} +williePete.wpZones = {} +williePete.playerGUIs = {} -- used for unit guis + +williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM"} + +function williePete.addWillie(theWillie) + table.insert(williePete.willies, theWillie) +end + +function williePete.addWPZone(theZone) + table.insert(williePete.wpZones, theZone) +end + +function williePete.closestCheckInTgtZoneForCoa(point, coa) + -- returns the closest zone that point is inside. + -- first tries directly, then, if none found, + -- with added check-in radius + local lPoint = {x=point.x, y=0, z=point.z} + local currDelta = math.huge + local closestZone = nil + -- first, we try if outright inside + for zName, zData in pairs(williePete.wpZones) do + if zData.coalition == coa then + -- local zPoint = cfxZones.getPoint(zData) + local inZone, delta = cfxZones.isPointInsideZone(lPoint, zData) + if inZone and (delta < currDelta) then + currDelta = delta + closestZone = zData + end + end + end + -- if we got one, we return that zone + if closestZone then return closestZone, currDelta end + + for zName, zData in pairs(williePete.wpZones) do + if zData.coalition == coa then + -- local zPoint = cfxZones.getPoint(zData) + local inZone, delta = cfxZones.isPointInsideZone(lPoint, zData, zData.checkInRange) + if inZone and (delta < currDelta) then + currDelta = delta + closestZone = zData + end + end + end + if closestZone then return closestZone, currDelta end + + return nil, -1 + +end + +function williePete.getClosestZoneForCoa(point, coa) + local lPoint = {x=point.x, y=0, z=point.z} + local currDelta = math.huge + local closestZone = nil + for zName, zData in pairs(williePete.wpZones) do + if zData.coalition == coa then + local zPoint = cfxZones.getPoint(zData) + local delta = dcsCommon.dist(lPoint, zPoint) -- emulate flag compare + if (delta < currDelta) then + currDelta = delta + closestZone = zData + end + else +-- trigger.outText("Zone <" .. zData.name .. ">, coa <" .. zData.coalition .. "> does not match <" .. coa .. ">", 30) + end + end + return closestZone, currDelta +end + + +function williePete.createWPZone(aZone) + aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "wpTarget", 0) -- side that marks it on map, and who fires arty + aZone.shellStrength = cfxZones.getNumberFromZoneProperty(aZone, "shellStrength", 500) -- power of shells (strength) + aZone.shellNum = cfxZones.getNumberFromZoneProperty(aZone, "shellNum", 17) -- number of shells in bombardment + aZone.transitionTime = cfxZones.getNumberFromZoneProperty(aZone, "transitionTime", 20) -- average time of travel for projectiles + aZone.coolDown = cfxZones.getNumberFromZoneProperty(aZone, "coolDown", 180) -- cooldown after arty fire, used to set readyTime + aZone.baseAccuracy = cfxZones.getNumberFromZoneProperty(aZone, "baseAccuracy", 50) + + aZone.readyTime = 0 -- if readyTime > now we are not ready + aZone.trackingPlayer = nil -- name player's unit who is being tracked for wp. may not be neccessary + aZone.checkedIn = {} -- dict of all planes currently checked in + aZone.trackingEndsTime = 0 -- if now > trackingends, we remove player and send a message + + aZone.wpTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "wpTriggerMethod", "change") + + aZone.FACTypes = cfxZones.getStringFromZoneProperty(aZone, "facTypes", "all") + + aZone.checkInRange = cfxZones.getNumberFromZoneProperty(aZone, "checkInRange", williePete.checkInRange) -- default to my default + + aZone.ackSound = cfxZones.getStringFromZoneProperty(aZone, "ackSound", williePete.ackSound) + aZone.guiSound = cfxZones.getStringFromZoneProperty(aZone, "guiSound", williePete.guiSound) + + if cfxZones.hasProperty(aZone, "triggerMethod") then + aZone.wpTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") + end + + if aZone.verbose then + trigger.action.outText("Added wpTarget zone <" .. aZone.name .. ">", 30) + end +end + +-- +-- player management +-- +function williePete.startPlayerGUI() + -- scan all mx players + -- note: currently assumes single-player groups + -- in preparation of single-player 'commandForUnit' + for uName, uData in pairs(cfxMX.playerUnitByName) do + local unitInfo = {} + -- try and access each unit even if we know that the + -- unit does not exist in-game right now + local gData = cfxMX.playerUnit2Group[uName] + local gName = gData.name + local coa = cfxMX.groupCoalitionByName[gName] + local theType = uData.type + + if williePete.verbose then + trigger.action.outText("unit <" .. uName .. ">: type <" .. theType .. "> coa <" .. coa .. ">, group <" .. gName .. ">", 30) + end + + unitInfo.name = uName -- needed for reverse-lookup + unitInfo.coa = coa + unitInfo.gID = gData.groupId + unitInfo.uID = uData.unitId + unitInfo.theType = theType + unitInfo.cat = cfxMX.groupTypeByName[gName] + -- now check type against willie pete config for allowable types + local pass = false + for idx, aType in pairs(williePete.facTypes) do + if aType == "ALL" then pass = true end + if aType == "ANY" then pass = true end + if aType == theType then pass = true end + if dcsCommon.stringStartsWith(aType, "HEL") and unitInfo.cat == "helicopter" then pass = true end + if dcsCommon.stringStartsWith(aType, "PLAN") and unitInfo.cat == "plane" then pass = true end + end + + if pass then -- we install a menu for this group + -- we may not want check in stuff, but it could be cool + unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, "FAC") + unitInfo.checkIn = missionCommands.addCommandForGroup(unitInfo.gID, "Check In", unitInfo.root, williePete.redirectCheckIn, unitInfo) + end + + -- store it + williePete.playerGUIs[uName] = unitInfo + end +end + +-- +-- BOOM +-- + +-- +-- BOOM command +-- +function williePete.doBoom(args) + trigger.action.explosion(args.point, args.strength) + data = {} + data.point = args.point + data.strength = args.strength +-- cfxArtilleryZones.invokeCallbacksFor('impact', args.zone, data) +end + +function williePete.doParametricFireAt(aPoint, accuracy, shellNum, shellBaseStrength, shellVariance, transitionTime) + if williePete.verbose then + trigger.action.outText("fire with accuracy <" .. accuracy .. "> shellNum <" .. shellNum .. "> baseStren <" .. shellBaseStrength .. "> variance <" .. shellVariance .. ">, ttime <" .. transitionTime .. ">", 30) + end + + -- accuracy is meters from center + if not aPoint then return end + if not accuracy then accuracy = 100 end + if not shellNum then shellNum = 17 end + if not shellBaseStrength then shellBaseStrength = 500 end + if not shellVariance then shellVariance = 0.2 end + if not transitionTime then transitionTime = 17 end + + local alt = land.getHeight({x=aPoint.x, y=aPoint.z}) + local center = {x=aPoint.x, y=alt, z=aPoint.z} + + for i=1, shellNum do + local thePoint = dcsCommon.randomPointInCircle(accuracy, 0, center.x, center.z) + thePoint.y = land.getHeight({x=thePoint.x, y=thePoint.z}) + local boomArgs = {} + local strVar = shellBaseStrength * shellVariance + strVar = strVar * (2 * dcsCommon.randomPercent() - 1.0) -- go from -1 to 1 + + boomArgs.strength = shellBaseStrength + strVar + thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1 + boomArgs.point = thePoint + boomArgs.zone = aZone + local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds + if timeVar < 0 then timeVar = -timeVar end + + timer.scheduleFunction(williePete.doBoom, boomArgs, timer.getTime() + transitionTime + timeVar) + end +end + +-- +-- COMMS +-- + +function williePete.redirectCheckIn(unitInfo) + timer.scheduleFunction(williePete.doCheckIn, unitInfo, timer.getTime() + 0.1) +end + +function williePete.doCheckIn(unitInfo) + --trigger.action.outText("check-in received", 30) + local theUnit = Unit.getByName(unitInfo.name) + local p = theUnit:getPoint() + local theZone, dist = williePete.closestCheckInTgtZoneForCoa(p, unitInfo.coa) + + if not theZone then + theZone, dist = williePete.getClosestZoneForCoa(p, unitInfo.coa) + if not theZone then + trigger.action.outTextForGroup(unitInfo.gID, "No target zone in range.", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + return + end + + trigger.action.outTextForGroup(unitInfo.gID, "Too far from target zone, closest target zone is " .. theZone.name, 30) + trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) + return + end + + -- we are now checked in to zone -- unless we are already checked in + if theZone.checkedIn[unitInfo.name] then + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.name .. ", " .. theZone.name .. ", we heard you the first time, proceed.", 30) + trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) + return + end + + -- we now check in + theZone.checkedIn[unitInfo.name] = unitInfo + + -- add the 'Target marked' menu + unitInfo.targetMarked = missionCommands.addCommandForGroup(unitInfo.gID, "Target Marked", unitInfo.root, williePete.redirectTargetMarked, unitInfo) + -- remove 'check in' + missionCommands.removeItemForGroup(unitInfo.gID, unitInfo.checkIn) + unitInfo.checkIn = nil + -- add 'check out' + unitInfo.checkOut = missionCommands.addCommandForGroup(unitInfo.gID, "Check Out", unitInfo.root, williePete.redirectCheckOut, unitInfo) + + trigger.action.outTextForGroup(unitInfo.gID, "Roger " .. unitInfo.name .. ", " .. theZone.name .. " tracks you, standing by for target data.", 30) + trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) +end + +function williePete.redirectCheckOut(unitInfo) + timer.scheduleFunction(williePete.doCheckOut, unitInfo, timer.getTime() + 0.1) +end + +function williePete.doCheckOut(unitInfo) + --trigger.action.outText("check-out received", 30) + + -- check out of all zones + local wasCheckedIn = false + local fromZone = "" + for idx, theZone in pairs(williePete.wpZones) do + if theZone.checkedIn[unitInfo.name] then + wasCheckedIn = true + fromZone = theZone.name + end + theZone.checkedIn[unitInfo.name] = nil + end + if not wasCheckedIn then + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.name .. ", roger cecked-out. Good hunting!", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + else + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.name .. "has checked out of " .. fromZone ..".", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + end + + -- remove checkOut and targetMarked + missionCommands.removeItemForGroup(unitInfo.gID, unitInfo.checkOut) + unitInfo.checkOut = nil + missionCommands.removeItemForGroup(unitInfo.gID, unitInfo.targetMarked) + unitInfo.targetMarked = nil + + -- add check in + unitInfo.checkIn = missionCommands.addCommandForGroup(unitInfo.gID, "Check In", unitInfo.root, williePete.redirectCheckIn, unitInfo) +end + + +function williePete.redirectTargetMarked(unitInfo) + timer.scheduleFunction(williePete.doTargetMarked, unitInfo, timer.getTime() + 0.1) +end + +function williePete.rogerDodger(args) + local unitInfo = args[1] + local theZone = args[2] + + trigger.action.outTextForCoalition(unitInfo.coa, "Roger " .. unitInfo.name .. ", good copy, firing.", 30) + trigger.action.outSoundForCoalition(unitInfo.coa, theZone.ackSound) +end + +function williePete.doTargetMarked(unitInfo) + --trigger.action.outText("mark received", 30) + -- first, check if we are past the time-out + local now = timer.getTime() + + -- now check if zone matches check-in + if not unitInfo.expiryTime or unitInfo.expiryTime < now then + trigger.action.outTextForGroup(unitInfo.gID, "Target mark stale or ambiguous, set fresh mark", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + return + end + + -- now, check if the zone is ready to receive + if not unitInfo.wpInZone or not unitInfo.pos then + -- should not happen, but better safe than sorry + trigger.action.outTextForGroup(unitInfo.gID, "Lost sight of target location, set new mark", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + return + end + + local tgtZone = unitInfo.wpInZone + -- see if we are checked into that zone + if not tgtZone.checkedIn[unitInfo.name] then + -- zones don't match + trigger.action.outTextForGroup(unitInfo.gID, "Say again " .. unitInfo.name .. ", we have crosstalk. Try and reset coms", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + return + end + + -- see if zone is ready to receive + local timeRemaining = math.floor(tgtZone.readyTime - now) + if timeRemaining > 0 then + -- zone not ready + trigger.action.outTextForGroup(unitInfo.gID, "Stand by " .. unitInfo.name .. ", artillery not ready. Expect " .. timeRemaining + math.random(1, 5) .. " seconds.", 30) + trigger.action.outSoundForGroup(unitInfo.gID, tgtZone.guiSound) + return + end + + -- if we get here, we are fire at mark + local alt = math.floor(land.getHeight({x = unitInfo.pos.x, y = unitInfo.pos.z})) + local grid = coord.LLtoMGRS(coord.LOtoLL(unitInfo.pos)) + local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing + local theLoc = mgrs + trigger.action.outTextForCoalition(unitInfo.coa, tgtZone.name ..", " .. unitInfo.name .." is transmitting target location. Fire at " .. theLoc .. ", elevation " .. alt .. " meters, target marked.", 30) + trigger.action.outSoundForCoalition(unitInfo.coa, tgtZone.guiSound) + timer.scheduleFunction(williePete.rogerDodger, {unitInfo, tgtZone},timer.getTime() + math.random(2, 5)) + + -- collect zone's fire params & fire + local shellStrength = tgtZone.shellStrength + local shellNum = tgtZone.shellNum + local transitionTime = tgtZone.transitionTime + local accuracy = tgtZone.baseAccuracy + + williePete.doParametricFireAt(unitInfo.pos, accuracy, shellNum, shellStrength, 0.2, transitionTime) + + -- set zone's cooldown + tgtZone.readyTime = now + tgtZone.coolDown + -- erase player's wp mark + unitInfo.wpInZone = nil + unitInfo.pos = nil +end +-- return true if a zone is actively tracking theUnit to place +-- a wp + +function williePete.zoneIsTracking(theUnit) + local uName = theUnit:getName() + for idx, theZone in pairs(williePete.wpZones) do + if theZone.checkedIn[uName] then return true end + end + return false +end + +function williePete.isWP(theWeapon) + local theDesc = theWeapon:getTypeName() + for idx, wpw in pairs(williePete.smokeWeapons) do + if theDesc == wpw then return true end + end + return false +end + +function williePete:onEvent(event) + if not event.initiator then + --trigger.action.outText("onEvent - " .. event.id .. ": no initiator",30) + return + end + if not event.weapon then + --trigger.action.outText("onEvent - " .. event.id .. ": no WEAPON",30) + return + end + + local theUnit = event.initiator + local pType = "(AI)" + if theUnit.getPlayerName then pType = "(" .. theUnit:getName() .. ")" end + + if event.id == 1 then -- S_EVENT_SHOT + -- initiator is who fired. maybe want to test if player + + --trigger.action.outText(theUnit:getName() .. " " .. pType .. " fired " .. event.weapon:getTypeName() .. ".", 30) + + if not williePete.isWP(event.weapon) then + --trigger.action.outText("<" .. event.weapon:getTypeName() .. "> not a smoke weapon", 30) + return + end + + -- make sure that whoever fired it is being tracked by + -- a zone + if not williePete.zoneIsTracking(theUnit) then + --trigger.action.outText("<" .. event.weapon:getTypeName() .. "> fired while not being tracked by zone", 30) + return + end + + -- assuming it's a willie, let's track it + local theWillie = {} + theWillie.firedBy = theUnit:getName() + theWillie.theUnit = theUnit + theWillie.weapon = event.weapon + theWillie.wt = theWillie.weapon:getTypeName() + theWillie.pos = theWillie.weapon:getPoint() + theWillie.v = theWillie.weapon:getVelocity() + + williePete.addWillie(theWillie) + end + + if event.id == 2 then -- hit + local what = "something" + if event.target then what = event.target:getName() end + --trigger.action.outText("Weapon " .. event.weapon:getTypeName() .. " fired by unit ".. theUnit:getName() .. " " .. pType .. " hit " .. what, 30) + -- may need to remove willie from willies + end + +end + +-- test if a projectile hit ground inside a wp zone +function williePete.isInside(theWillie) + local thePoint = theWillie.pos + local theUnitName = theWillie.firedBy -- may be dead already, but who cares + local theUnit = Unit.getByName(theUnitName) + if not theUnit then return false end -- unit dead + if not Unit.isExist(theUnit) then return false end -- dito + + local thePlayer = williePete.playerGUIs[theUnitName] + if not thePlayer then return nil end + for idx, theZone in pairs(williePete.wpZones) do + if cfxZones.pointInZone(thePoint, theZone) then + -- we are inside. but is this the right coalition? + if thePlayer.coa == theZone.coalition then + --trigger.action.outText("Willie in " .. theZone.name, 30) + return theZone + else + --trigger.action.outText("Willie wrong coa", 30) + end + -- if we want to allow neutral zones (doens't make sense) + -- add another guard below + else + --trigger.action.outText("willie outside " .. theZone.name, 30) + end + end + return nil +end + + +-- update + +function williePete.projectileHit(theWillie) + -- interpolate pos: half time between updates times last velocity + local vmod = dcsCommon.vMultScalar(theWillie.v, 0.5 / williePete.ups) + theWillie.pos = dcsCommon.vAdd(theWillie.pos, vmod) + --trigger.action.outText("Willie " .. theWillie.wt .. " expired at " .. dcsCommon.point2text(theWillie.pos) .. " interpolated.", 30) + + -- reset last mark for player + local thePlayer = williePete.playerGUIs[theWillie.firedBy] + thePlayer.pos = nil + thePlayer.wpInZone = nil + + -- check if this is within a wpZones + local theZone = williePete.isInside(theWillie) + if not theZone then + if williePete.verbose then + trigger.action.outText("+++wp: wp expired outside zone", 30) + end + return + end + + -- if we receive a zone, we know that the player's + -- coalition matches the one of the zone + thePlayer.expiryTime = timer.getTime() + williePete.wpMaxTime -- set timeout in which player can give fire command + thePlayer.pos = theWillie.pos -- remember the loc + thePlayer.wpInZone = theZone -- remember the zone + + -- mark point with smoke blue + --dcsCommon.markPointWithSmoke(theWillie.pos, 4) + + if cfxArtilleryZones then + --cfxArtilleryZones.doParametricFireAt(theWillie.pos, 50, 10) + else + -- mark point with smoke blue + --dcsCommon.markPointWithSmoke(theWillie.pos, 4) + end +end + +function williePete.updateWP() + timer.scheduleFunction(williePete.updateWP, {}, timer.getTime() + 1/williePete.ups) + + local nextPete = {} + for idx, theWillie in pairs(williePete.willies) do + -- check if it still exists + if Weapon.isExist(theWillie.weapon) then + -- update loc, proceed to next round + theWillie.pos = theWillie.weapon:getPoint() + theWillie.v = theWillie.weapon:getVelocity() + table.insert(nextPete, theWillie) + else + -- weapon disappeared: it has hit something + -- but unguided rockets do not create an event for that + williePete.projectileHit(theWillie) + -- no longer propagates to next round + end + end + williePete.willies = nextPete +end + +function williePete.playerUpdate() + timer.scheduleFunction(williePete.playerUpdate, {}, timer.getTime() + 2) -- check 30 times a minute + -- zone still checked in updates for zones + for idx, theZone in pairs(williePete.wpZones) do + -- make sure any unit checked in is still inside + -- the zone that they checked in, or they are checked out + --local zp = cfxZones.getPoint(theZone) + for idy, unitInfo in pairs(theZone.checkedIn) do + -- make sure unit still exists + local dropUnit = true + local theUnit = Unit.getByName(unitInfo.name) + if theUnit and Unit.isExist(theUnit) then + local up = theUnit:getPoint() + up.y = 0 + local isInside, dist = cfxZones.isPointInsideZone(up, theZone, theZone.checkInRange) + + if isInside then + dropUnit = false + end + end + if dropUnit then + -- remove from zone check-in + -- williePete.doCheckOut(unitInfo) + timer.scheduleFunction(williePete.doCheckOut, unitInfo, timer.getTime() + 0.1) -- to not muck up iteration + end + end + end + + -- menu updates for all players +end + +-- +-- Config & Start +-- +function williePete.readConfigZone() + local theZone = cfxZones.getZoneByName("wpConfig") + if not theZone then + if williePete.verbose then + trigger.action.outText("+++wp: NO config zone!", 30) + end + theZone = cfxZones.createSimpleZone("wpConfig") + end + + local facTypes = cfxZones.getStringFromZoneProperty(theZone, "facTypes", "all") + facTypes = string.upper(facTypes) + + -- make this an array + local allTypes = {} + if dcsCommon.containsString(facTypes, ",") then + allTypes = dcsCommon.splitString(facTypes, ",") + else + table.insert(allTypes, facTypes) + end + williePete.facTypes = dcsCommon.trimArray(allTypes) + + -- how long a wp is active. must not be more than 5 minutes + williePete.wpMaxTime = cfxZones.getNumberFromZoneProperty(theZone, "wpMaxTime", 3 * 60) + + -- default check-in range, added to target zone's range and used + -- for auto-check-out + williePete.checkInRange = cfxZones.getNumberFromZoneProperty(theZone, "checkInRange", 10000) -- 10 km outside + + williePete.ackSound = cfxZones.getStringFromZoneProperty(theZone, "ackSound", "some") + williePete.guiSound = cfxZones.getStringFromZoneProperty(theZone, "guiSound", "some") + + williePete.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + + if williePete.verbose then + trigger.action.outText("+++msgr: read config", 30) + end +end + +function williePete.start() + if not dcsCommon.libCheck("cfx williePete", + williePete.requiredLibs) then + return false + end + + -- read config + williePete.readConfigZone() + + -- collect all wp target zones + local attrZones = cfxZones.getZonesWithAttributeNamed("wpTarget") + + for k, aZone in pairs(attrZones) do + williePete.createWPZone(aZone) -- process attribute and add to zone + williePete.addWPZone(aZone) -- remember it so we can smoke it + end + + -- add event handler + world.addEventHandler(williePete) + + -- initialize all players from MX + williePete.startPlayerGUI() + + -- start updates + williePete.updateWP() -- for tracking wp, at ups + williePete.playerUpdate() -- for tracking players, at 1/s + + + trigger.action.outText("williePete v" .. williePete.version .. " loaded.", 30) + + return true +end + +-- let's go +if not williePete.start() then + trigger.action.outText("cf/x Willie Pete aborted: missing libraries", 30) + williePete = nil +end + +--[[-- + Mechanics: + - unit checks in with arty zone. if not in range of arty zone + safe dist, error 'not in range' is returned, else is sent. . Zone will advise on status change when checked in. + - if unit leaves arty zone + safe dist, is displayed and is invoked + - unit can check out any time + - when checked in, any wp hitting the ground is remembered if still inside target zone + - player then gives 'target marked' + - if artillery on cooldown, or wp not inside zone error, else fire sequence starts, and cooldown starts for entire zone + +--]]-- \ No newline at end of file diff --git a/tutorial & demo missions/demo - radio go go.miz b/tutorial & demo missions/demo - radio go go.miz index e06391b..734278f 100644 Binary files a/tutorial & demo missions/demo - radio go go.miz and b/tutorial & demo missions/demo - radio go go.miz differ