cfxArtilleryZones = {} cfxArtilleryZones.version = "3.0.0" cfxArtilleryZones.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } cfxArtilleryZones.verbose = false --[[-- Version History 1.0.0 - initial version 1.0.1 - simSmokeZone 2.0.0 - zone attributes for shellNum, shellVariance, cooldown, addMark, transitionTime - doFireAt method - simFireAt now calls doFireAt - added all params to crteateArtilleryTarget - createArtillerTarget replaced createArtilleryZone - addMark now used so arty zones can be hidden on map - added triggerFlag attribute - update now fires every time when flag changes 2.0.1 - added verbose setting - base accuracy now derived from radius - added coalition check for ZonesInRange - att transition time to zone info mark - made compatible with linked zones - added silent attribute - added transition time to arty command chatter 2.0.2 - boom?, arty? synonyms 2.1.0 - DML Flag Support - code cleanup 2.2.0 - DML Watchflag integration 2.2.1 - minor code clean-up 2.2.2 - new doParametricFireAt() 3.0.0 - dmlZones, OOP - cleanup 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 Copyright (c) 2021 - 2024 by Christian Franz and cf/x AG --]]-- cfxArtilleryZones.artilleryZones = {} cfxArtilleryZones.updateDelay = 1 -- every second -- -- C A L L B A C K S -- cfxArtilleryZones.callbacks = {} function cfxArtilleryZones.addCallback(theCallback) table.insert(cfxArtilleryZones.callbacks, theCallback) end function cfxArtilleryZones.invokeCallbacksFor(reason, zone, data) for idx, theCB in pairs (cfxArtilleryZones.callbacks) do theCB(reason, zone, data) end end function cfxArtilleryZones.demoCallback(reason, zone, data) -- reason: 'fire' or 'impact' -- fire has no data, impact has data.point and data.strength end function cfxArtilleryZones.createArtilleryTarget(name, point, coalition, spotRange, transitionTime, baseAccuracy, shellNum, shellStrength, shellVariance, triggerFlag, addMark, cooldown, silent, autoAdd) -- was: createArtilleryZone, changed params list if not point then return end if not autoAdd then autoAdd = false end if not coalition then coalition = 0 end if not spotRange then spotRange = 3000 end if not shellStrength then shellStrength = 500 end if not transitionTime then transitionTime = 20 end if not shellNum then shellNum = 17 end if not addMark then addMark = false end if not name then name = "dftZName" end if not shellVariance then shellVariance = 0.2 end if not cooldown then cooldown = 120 end if not baseAccuracy then baseAccuracy = 100 end if not silent then silent = false end name = cfxZones.createUniqueZoneName(name) local newZone = cfxZones.createSimpleZone(name, point, 100, autoAdd) newZone.spotRange = spotRange newZone.coalition = coalition newZone.landHeight = land.getHeight({x = newZone.point.x, y= newZone.point.z}) newZone.transitionTime = transitionTime newZone.shellNum = shellNum newZone.shellStrength = shellStrength newZone.triggerFlag = triggerFlag -- can be nil if triggerFlag then newZone.lastTriggerValue = trigger.misc.getUserFlag(triggerFlag) -- save last value end newZone.addMark = addMark if autoAdd then cfxArtilleryZones.addArtilleryZone(newZone) end newZone.shellVariance = shellVariance newZone.cooldown = cooldown newZone.silent = silent end function cfxArtilleryZones.processArtilleryZone(aZone) aZone.artilleryTarget = aZone:getStringFromZoneProperty( "artilleryTarget", aZone.name) aZone.coalition = aZone:getCoalitionFromZoneProperty("coalition", 0) -- side that marks it on map, and who fires arty aZone.spotRange = aZone:getNumberFromZoneProperty("spotRange", 3000) -- FO max range to direct fire aZone.shellStrength = aZone:getNumberFromZoneProperty("shellStrength", 500) -- power of shells (strength) aZone.shellNum = aZone:getNumberFromZoneProperty("shellNum", 17) -- number of shells in bombardment aZone.transitionTime = aZone:getNumberFromZoneProperty( "transitionTime", 20) -- average time of travel for projectiles aZone.addMark = aZone:getBoolFromZoneProperty("addMark", true) -- note: defaults to true aZone.shellVariance = aZone:getNumberFromZoneProperty( "shellVariance", 0.2) -- strength of explosion can vary by +/- this amount -- watchflag: -- triggerMethod aZone.artyTriggerMethod = aZone:getStringFromZoneProperty( "artyTriggerMethod", "change") if aZone:hasProperty("triggerMethod") then aZone.artyTriggerMethod = aZone:getStringFromZoneProperty("triggerMethod", "change") end if aZone:hasProperty("f?") then aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty("f?", "none") elseif aZone:hasProperty("artillery?") then aZone.artyTriggerFlag = aZone:getStringFromZoneProperty("artillery?", "none") elseif aZone:hasProperty("in?") then aZone.artyTriggerFlag = aZone:getStringFromZoneProperty("in?", "none") end if aZone.artyTriggerFlag then aZone.lastTriggerValue = trigger.misc.getUserFlag(aZone.artyTriggerFlag) -- save last value end aZone.cooldown = aZone:getNumberFromZoneProperty("cooldown", 120) -- seconds aZone.baseAccuracy = aZone:getNumberFromZoneProperty( "baseAccuracy", aZone.radius) -- meters from center radius shell impact -- use zone radius as mase accuracy for simple placement aZone.silent = aZone:getBoolFromZoneProperty("silent", false) end function cfxArtilleryZones.addArtilleryZone(aZone) -- add landHeight to this zone aZone.landHeight = land.getHeight({x = aZone.point.x, y= aZone.point.z}) -- mark it on the map aZone.artyCooldownTimer = -1000 cfxArtilleryZones.placeMarkForSide(aZone.point, aZone.coalition, aZone.name .. ", FO=" .. aZone.spotRange .. "m" .. ", tt=" .. aZone.transitionTime) table.insert(cfxArtilleryZones.artilleryZones, aZone) end function cfxArtilleryZones.findArtilleryZoneNamed(aName) aZone = cfxZones.getZoneByName(aName) if not aZone then return nil end -- check if it is an arty zone if not aZone.artilleryTarget then return nil end -- all is well return aZone end function cfxArtilleryZones.removeArtilleryZone(aZone) if type(aZone) == "string" then aZone = cfxArtilleryZones.findArtilleryZoneNamed(aZone) end if not aZone then return end -- now create new table local filtered = {} for idx, theZone in pairs(cfxArtilleryZones.artilleryZones) do if theZone ~= aZone then table.insert(filtered, theZone) end end cfxArtilleryZones.artilleryZones = filtered end function cfxArtilleryZones.artilleryZonesInRangeOfUnit(theUnit) if not theUnit then return {} end if not theUnit:isExist() then return {} end local myCoalition = theUnit:getCoalition() local zonesInRange = {} local p = theUnit:getPoint() for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do -- is it one of mine? if aZone.coalition == myCoalition then -- is it close enough? local zP = aZone:getPoint() aZone.landHeight = land.getHeight({x = zP.x, y= zP.z}) local zonePoint = {x = zP.x, y = aZone.landHeight, z = zP.z} local d = dcsCommon.dist(p,zonePoint) if d < aZone.spotRange then -- LOS check if land.isVisible(p, zonePoint) then -- yeah, add to list table.insert(zonesInRange, aZone) end end end end return zonesInRange end -- -- MARK ON MAP -- cfxArtilleryZones.uuidCount = 0 function cfxArtilleryZones.uuid() cfxArtilleryZones.uuidCount = cfxArtilleryZones.uuidCount + 1 return cfxArtilleryZones.uuidCount end function cfxArtilleryZones.placeMarkForSide(location, theSide, theDesc) local theID = cfxArtilleryZones.uuid() local theDesc = "ARTY: ".. theDesc trigger.action.markToCoalition( theID, theDesc, location, theSide, false, nil) return theID end function cfxArtilleryZones.removeMarkForArgs(args) local theID = args[1] trigger.action.removeMark(theID) end -- -- FIRE AT A ZONE -- -- -- BOOM method -- function cfxArtilleryZones.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 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) aZone = mZone end if not aZone then return end if not maxDistFromCenter then maxDistFromCenter = aZone.baseAccuracy end local accuracy = maxDistFromCenter local zP = cfxZones.getPoint(aZone) aZone.landHeight = land.getHeight({x = zP.x, y= zP.z}) local center = {x=zP.x, y=aZone.landHeight, z=zP.z} -- center of where shells hit local shellNum = aZone.shellNum local shellBaseStrength = aZone.shellStrength local shellVariance = aZone.shellVariance local transitionTime = aZone.transitionTime 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 -- invoke callbacks cfxArtilleryZones.invokeCallbacksFor('fire', aZone, {}) end -- -- API main entry call for firing at zone -- invokes doFireAt() -- function cfxArtilleryZones.simFireAtZone(aZone, aGroup, dist) if not dist then dist = aZone.spotRange end local shellBaseStrength = aZone.shellStrength local maxAccuracy = 100 -- m radius when close local minAccuracy = 500 -- m radius whan at max sport dist local currAccuracy = minAccuracy if dist <= 1000 then currAccuracy = maxAccuracy else local percent = (dist-1000) / (aZone.spotRange-1000) currAccuracy = dcsCommon.lerp(maxAccuracy, minAccuracy, percent) end currAccuracy = math.floor(currAccuracy) cfxArtilleryZones.doFireAt(aZone, currAccuracy) aZone.artyCooldownTimer = timer.getTime() + aZone.cooldown -- 120 -- 2 minutes reload if not aZone.silent then local addInfo = " with d=" .. dist .. ", var = " .. currAccuracy .. " pB=" .. shellBaseStrength .. " tt=" .. aZone.transitionTime trigger.action.outTextForCoalition(aGroup:getCoalition(), "Artillery firing on ".. aZone.name .. addInfo, 30) end end function cfxArtilleryZones.simSmokeZone(aZone, aGroup, aColor) -- this is simsmoke: transition time is fixed, and we do not -- use arty units. all very simple. we merely place smoke on -- ground if not aColor then aColor = "red" end if type(aColor) == "string" then aColor = dcsCommon.smokeColor2Num(aColor) end local zP = aZone:getPoint(aZone) aZone.landHeight = land.getHeight({x = zP.x, y= zP.z}) local transitionTime = aZone.transitionTime --17 -- seconds until phosphor lands local center = {x = zP.x, y =aZone.landHeight + 3, z = zP.z } -- center of where shells hit -- we now can 'dirty' the position by something. not yet local currAccuracy = 200 local thePoint = dcsCommon.randomPointInCircle(currAccuracy, 50, center.x, center.z) timer.scheduleFunction(cfxArtilleryZones.doSmoke, {thePoint, aColor}, timer.getTime() + transitionTime) if not aGroup then return end if aZone.silent then return end trigger.action.outTextForCoalition(aGroup:getCoalition(), "Artillery firing single phosphor round at ".. aZone.name, 30) end function cfxArtilleryZones.doSmoke(args) local thePoint = args[1] local aColor = args[2] dcsCommon.markPointWithSmoke(thePoint, aColor) end -- -- UPDATE -- function cfxArtilleryZones.update() -- call me in a couple of minutes to 'rekindle' timer.scheduleFunction(cfxArtilleryZones.update, {}, timer.getTime() + cfxArtilleryZones.updateDelay) -- iterate all zones to see if a trigger has changed for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do if aZone:testZoneFlag(aZone.artyTriggerFlag, aZone.artyTriggerMethod, "lastTriggerValue") then -- a triggered release! cfxArtilleryZones.doFireAt(aZone) -- all from zone vars! if cfxArtilleryZones.verbose then local addInfo = " with var = " .. aZone.baseAccuracy .. " pB=" .. aZone.shellStrength trigger.action.outText("Artillery T-Firing on ".. aZone.name .. addInfo, 30) end end -- old code --[[-- if aZone.artyTriggerFlag then local currTriggerVal = cfxZones.getFlagValue(aZone.artyTriggerFlag, aZone) if currTriggerVal ~= aZone.lastTriggerValue then -- a triggered release! cfxArtilleryZones.doFireAt(aZone) -- all from zone vars! if cfxArtilleryZones.verbose then local addInfo = " with var = " .. aZone.baseAccuracy .. " pB=" .. aZone.shellStrength trigger.action.outText("Artillery T-Firing on ".. aZone.name .. addInfo, 30) end aZone.lastTriggerValue = currTriggerVal end end --]]-- end end -- -- START -- function cfxArtilleryZones.start() if not dcsCommon.libCheck("cfx Artillery Zones", cfxArtilleryZones.requiredLibs) then return false end -- collect all spawn zones local attrZones = cfxZones.getZonesWithAttributeNamed("artilleryTarget") for k, aZone in pairs(attrZones) do cfxArtilleryZones.processArtilleryZone(aZone) -- process attribute and add to zone cfxArtilleryZones.addArtilleryZone(aZone) -- remember it so we can smoke it end -- start update loop cfxArtilleryZones.update() -- say hi trigger.action.outText("cfx Artillery Zones v" .. cfxArtilleryZones.version .. " started.", 30) return true end -- let's go if not cfxArtilleryZones.start() then trigger.action.outText("cf/x Artillery Zones aborted: missing libraries", 30) cfxArtilleryZones = nil end