|
| 1 | +WT.ScenicFire = {} |
| 2 | +WT.ScenicFire.__index = WT.ScenicFire |
| 3 | + |
| 4 | +-- Utility to get a random value in [-variance, +variance] |
| 5 | +function WT.ScenicFire.randomVariance(base, variance) |
| 6 | + return base + (math.random() * 2 - 1) * variance |
| 7 | +end |
| 8 | + |
| 9 | +-- Utility to get a random point above the target within a dome of errorSize |
| 10 | +function WT.ScenicFire.randomErrorPoint(targetPoint, errorSize) |
| 11 | + -- Random direction |
| 12 | + local theta = math.random() * 2 * math.pi |
| 13 | + local phi = math.acos(math.random()) -- [0, pi], but we want only above the target (phi in [0, pi/2]) |
| 14 | + phi = phi / 2 |
| 15 | + local r = math.random() * errorSize |
| 16 | + local dx = r * math.sin(phi) * math.cos(theta) |
| 17 | + local dy = r * math.sin(phi) * math.sin(theta) |
| 18 | + local dz = r * math.cos(phi) |
| 19 | + return { |
| 20 | + x = targetPoint.x + dx, |
| 21 | + y = targetPoint.y + dz, -- y is up in DCS |
| 22 | + z = targetPoint.z + dy |
| 23 | + } |
| 24 | +end |
| 25 | + |
| 26 | +function WT.ScenicFire.new(group, roundCount, roundVariance, interval, intervalVariance, errorSize, maxRange, fallbackPrefix) |
| 27 | + local self = setmetatable({}, WT.ScenicFire) |
| 28 | + self.groupName = group:getName() |
| 29 | + self.roundCount = roundCount or 5 |
| 30 | + self.roundVariance = roundVariance or 2 |
| 31 | + self.interval = interval or 10 |
| 32 | + self.intervalVariance = intervalVariance or 3 |
| 33 | + self.errorSize = errorSize or 50 |
| 34 | + self.maxRange = maxRange -- optional, can be nil |
| 35 | + self.fallbackPrefix = fallbackPrefix -- optional, can be nil |
| 36 | + self.active = false |
| 37 | + return self |
| 38 | +end |
| 39 | + |
| 40 | +function WT.ScenicFire:start() |
| 41 | + self.active = true |
| 42 | + self:scheduleNext() |
| 43 | +end |
| 44 | + |
| 45 | +function WT.ScenicFire:stop() |
| 46 | + self.active = false |
| 47 | +end |
| 48 | + |
| 49 | +function WT.ScenicFire:scheduleNext() |
| 50 | + if not self.active then return end |
| 51 | + local delay = WT.ScenicFire.randomVariance(self.interval, self.intervalVariance) |
| 52 | + timer.scheduleFunction(function() |
| 53 | + local continue = self:fireAtTarget() |
| 54 | + if continue then |
| 55 | + self:scheduleNext() |
| 56 | + else |
| 57 | + self:stop() |
| 58 | + end |
| 59 | + end, {}, timer.getTime() + delay) |
| 60 | +end |
| 61 | + |
| 62 | +function WT.ScenicFire:fireAtTarget() |
| 63 | + local group = WT.utils.p(Group.getByName, self.groupName) |
| 64 | + if not group or not group:isExist() then |
| 65 | + return false -- stop scheduling |
| 66 | + end |
| 67 | + local controller = group:getController() |
| 68 | + if not controller then return true end |
| 69 | + local detected = controller:getDetectedTargets() |
| 70 | + local target = nil |
| 71 | + |
| 72 | + if detected and #detected > 0 then |
| 73 | + -- Filter targets by maxRange if set |
| 74 | + local filteredTargets = {} |
| 75 | + local groupPos = group:getUnits()[1]:getPoint() |
| 76 | + for _, dt in ipairs(detected) do |
| 77 | + if dt.object and (not self.maxRange or |
| 78 | + WT.utils.VecMag({ |
| 79 | + x = dt.object:getPoint().x - groupPos.x, |
| 80 | + y = dt.object:getPoint().y - groupPos.y, |
| 81 | + z = dt.object:getPoint().z - groupPos.z |
| 82 | + }) <= self.maxRange) |
| 83 | + then |
| 84 | + table.insert(filteredTargets, dt) |
| 85 | + end |
| 86 | + end |
| 87 | + if #filteredTargets > 0 then |
| 88 | + -- Prefer visible targets |
| 89 | + for _, dt in ipairs(filteredTargets) do |
| 90 | + if dt.visible then |
| 91 | + target = dt |
| 92 | + break |
| 93 | + end |
| 94 | + end |
| 95 | + if not target then |
| 96 | + target = filteredTargets[1] |
| 97 | + end |
| 98 | + end |
| 99 | + end |
| 100 | + |
| 101 | + -- Fallback: no detected targets, use fallbackPrefix if provided |
| 102 | + if not target and self.fallbackPrefix then |
| 103 | + local enemy_co = 1 |
| 104 | + if group:getCoalition() == 1 then |
| 105 | + enemy_co = 2 |
| 106 | + end |
| 107 | + local allGroups = coalition.getGroups(enemy_co, 2) -- ground groups only |
| 108 | + local groupPos = group:getUnits()[1]:getPoint() |
| 109 | + local shooterPos = { x = groupPos.x, y = groupPos.y + 1.5, z = groupPos.z } |
| 110 | + local candidates = {} |
| 111 | + for _, g in ipairs(allGroups) do |
| 112 | + local gName = g:getName() |
| 113 | + if string.starts(gName, self.fallbackPrefix) and g:isExist() and g:getSize() > 0 then |
| 114 | + local gPosRaw = g:getUnits()[1]:getPoint() |
| 115 | + local gPos = { x = gPosRaw.x, y = gPosRaw.y + 1.5, z = gPosRaw.z } |
| 116 | + local dist = WT.utils.VecMag({ |
| 117 | + x = gPosRaw.x - groupPos.x, |
| 118 | + y = gPosRaw.y - groupPos.y, |
| 119 | + z = gPosRaw.z - groupPos.z |
| 120 | + }) |
| 121 | + if (not self.maxRange or dist <= self.maxRange) and land.isVisible(shooterPos, gPos) then |
| 122 | + table.insert(candidates, g) |
| 123 | + end |
| 124 | + end |
| 125 | + end |
| 126 | + if #candidates > 0 then |
| 127 | + local idx = math.random(1, #candidates) |
| 128 | + local fallbackGroup = candidates[idx] |
| 129 | + local tgtPointRaw = fallbackGroup:getUnits()[1]:getPoint() |
| 130 | + local tgtPoint = { x = tgtPointRaw.x, y = tgtPointRaw.y + 1.5, z = tgtPointRaw.z } |
| 131 | + target = { object = { getPoint = function() return tgtPoint end } } |
| 132 | + end |
| 133 | + end |
| 134 | + |
| 135 | + if not target or not target.object then return true end |
| 136 | + |
| 137 | + local tgtPoint = target.object:getPoint() |
| 138 | + local firePoint = WT.ScenicFire.randomErrorPoint(tgtPoint, self.errorSize) |
| 139 | + local rounds = math.max(1, math.floor(WT.ScenicFire.randomVariance(self.roundCount, self.roundVariance))) |
| 140 | + |
| 141 | + local fireTask = { |
| 142 | + id = 'FireAtPoint', |
| 143 | + params = { |
| 144 | + point = {x=firePoint.x, y=firePoint.z}, |
| 145 | + altitude = firePoint.y, |
| 146 | + alt_type=0, |
| 147 | + expendQtyEnabled = true, |
| 148 | + expendQty = rounds |
| 149 | + } |
| 150 | + } |
| 151 | + controller:setTask(fireTask) |
| 152 | + return true |
| 153 | +end |
| 154 | + |
| 155 | +-- Usage: |
| 156 | +-- local sf = WT.ScenicFire.new(Group.getByName("MyGroup"), 5, 2, 10, 3, 50, 1000) |
| 157 | + |
0 commit comments