cfxArtilleryZones = {} cfxArtilleryZones.version = "2.2.1" 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 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, 2022 by Christian Franz and cf/x AG USAGE Via ME: Add the relevant attributes to the zone Via Script: Use createArtilleryTarget() Callbacks when fire at target is invoked, a callback can be invoked so your code knows that fire control has been given a command or that projectiles are impacting. Signature callback(rason, zone, data), with reason: 'firing' - fire command given for zone 'impact' a projectile has hit zone: artilleryZone data: empty on 'fire' .point where impact point .strength power of explosion --]]-- 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 = cfxZones.getStringFromZoneProperty(aZone, "artilleryTarget", aZone.name) aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) -- side that marks it on map, and who fires arty aZone.spotRange = cfxZones.getNumberFromZoneProperty(aZone, "spotRange", 3000) -- FO max range to direct fire 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.addMark = cfxZones.getBoolFromZoneProperty(aZone, "addMark", true) -- note: defaults to true aZone.shellVariance = cfxZones.getNumberFromZoneProperty(aZone, "shellVariance", 0.2) -- strength of explosion can vary by +/- this amount -- watchflag: -- triggerMethod aZone.artyTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "artyTriggerMethod", "change") if cfxZones.hasProperty(aZone, "triggerMethod") then aZone.artyTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") end if cfxZones.hasProperty(aZone, "f?") then aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "f?", "none") end if cfxZones.hasProperty(aZone, "artillery?") then aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "artillery?", "none") end if cfxZones.hasProperty(aZone, "in?") then aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "in?", "none") end if aZone.artyTriggerFlag then aZone.lastTriggerValue = trigger.misc.getUserFlag(aZone.artyTriggerFlag) -- save last value end aZone.cooldown =cfxZones.getNumberFromZoneProperty(aZone, "cooldown", 120) -- seconds aZone.baseAccuracy = cfxZones.getNumberFromZoneProperty(aZone, "baseAccuracy", aZone.radius) -- meters from center radius shell impact -- use zone radius as mase accuracy for simple placement aZone.silent = cfxZones.getBoolFromZoneProperty(aZone, "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 = cfxZones.getPoint(aZone) 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 command -- 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.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 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 --trigger.action.smoke(center, 2) -- mark location visually 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 = cfxZones.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 cfxZones.testZoneFlag(aZone, 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) -- trigger.misc.getUserFlag(aZone.artyTriggerFlag) 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") -- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not -- paused 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