Skip to content

Commit ca7b69d

Browse files
committed
add small features, small fixes
1 parent a2b7548 commit ca7b69d

4 files changed

Lines changed: 515 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- Progressive destruction of units in a group using DCS timer.scheduleFunction
2+
-- group: DCS group reference
3+
-- minPower, maxPower: explosion power range
4+
-- totalTime: seconds over which to destroy all units
5+
function WT.progressiveDestruction(group, minPower, maxPower, totalTime)
6+
local units = group:getUnits()
7+
local unitCount = #units
8+
if unitCount == 0 then return end
9+
10+
-- Shuffle units for random order
11+
local shuffled = {}
12+
for i = 1, unitCount do shuffled[i] = units[i] end
13+
for i = unitCount, 2, -1 do
14+
local j = math.random(1, i)
15+
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
16+
end
17+
18+
-- Assign kill times: evenly spaced, last unit at totalTime
19+
local killTimes = {}
20+
for i = 1, unitCount do
21+
killTimes[i] = ((i-1) / (unitCount-1)) * totalTime
22+
end
23+
24+
-- Store initial life for each unit
25+
local initialLife = {}
26+
for i, unit in ipairs(shuffled) do
27+
initialLife[unit:getName()] = unit:getLife()
28+
end
29+
30+
local function confirmKill(args)
31+
local unit = args.u
32+
local power = args.p
33+
local initialLifeVal = args.i
34+
if unit and unit:isExist() then
35+
local life = unit:getLife()
36+
if life > 0.05 * initialLifeVal then
37+
-- Repeat explosion immediately
38+
local point = unit:getPoint()
39+
WT.utils.explodePoint({ point = point, power = power })
40+
timer.scheduleFunction(confirmKill, {u=unit, p=power, i=initialLifeVal}, timer.getTime() + 0.01)
41+
end
42+
end
43+
return nil
44+
end
45+
46+
-- Helper: explosion and check
47+
local function explodeAndCheck(unit, power, initialLifeVal)
48+
local point = unit:getPoint()
49+
WT.utils.explodePoint({ point = point, power = power })
50+
timer.scheduleFunction(confirmKill, {u=unit, p=power, i=initialLifeVal}, timer.getTime() + 0.01)
51+
return nil
52+
end
53+
54+
-- Schedule explosions
55+
for i, unit in ipairs(shuffled) do
56+
local name = unit:getName()
57+
timer.scheduleFunction(function()
58+
explodeAndCheck(unit, math.random() * (maxPower - minPower) + minPower, initialLife[name])
59+
end, {}, timer.getTime() + killTimes[i])
60+
end
61+
end

Features/ProxVis.lua

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---------------------------------------------------------------------
2+
--proxVis.lua
3+
--Required Notice: Copyright WirtsLegs 2024, (https://github.com/WirtsLegs/WirtsTools)
4+
---------------------------------------------------------------------
5+
6+
WT.proxVis = {}
7+
WT.proxVis.groups = {}
8+
WT.proxVis.status = {}
9+
10+
11+
12+
function WT.proxVis.getPlayerGroups(co)
13+
local players = coalition.getPlayers(co)
14+
local groups = {}
15+
for p = 1, #players do
16+
local grp = players[p]:getGroup()
17+
groups[grp:getName()] = 1
18+
end
19+
return groups
20+
end
21+
22+
function WT.proxVis.getAIUnits(co)
23+
local grps = coalition.getGroups(co)
24+
local units = {}
25+
for g = 1, #grps do
26+
local gUnits = grps[g]:getUnits()
27+
for u = 1, #gUnits do
28+
if gUnits[u]:isActive() and gUnits[u]:getPlayerName() == nil then
29+
units[#units + 1] = gUnits[u]
30+
end
31+
end
32+
end
33+
return units
34+
end
35+
36+
function WT.proxVis.checkGroups(group, co)
37+
local group1 = WT.utils.p(Group.getByName, group)
38+
if not (group1) then
39+
return -1
40+
end
41+
local g1_units = WT.utils.p(Group.getUnits, group1)
42+
local g2_units = WT.proxVis.getAIUnits(co) --p(Group.getUnits,group2)
43+
if not (g1_units and g2_units) then
44+
return -1
45+
end
46+
local shortest = -1
47+
48+
for i = 1, #g1_units do
49+
local p1 = WT.utils.p(Unit.getPoint, g1_units[i])
50+
if p1 then
51+
for j = 1, #g2_units do
52+
local p2 = WT.utils.p(Unit.getPoint, g2_units[j])
53+
if p2 then
54+
local dist = WT.utils.VecMag({ x = p1.x - p2.x, y = p1.y - p2.y, z = p1.z - p2.z })
55+
if shortest > -1 then
56+
if dist < shortest then
57+
shortest = dist
58+
end
59+
else
60+
shortest = dist
61+
end
62+
end
63+
end
64+
end
65+
end
66+
return shortest
67+
end
68+
69+
function WT.proxVis.check(co, time)
70+
for g = 1, #WT.proxVis.groups do
71+
if WT.proxVis.groups[g].group == 1 or WT.proxVis.groups[g].group == 2 then
72+
local grps = WT.proxVis.getPlayerGroups(WT.proxVis.groups[g].group)
73+
for r, s in pairs(grps) do
74+
local dist = WT.proxVis.checkGroups(r, WT.proxVis.groups[g].co)
75+
if dist ~= -1 then
76+
if dist > WT.proxVis.groups[g].distance then
77+
Group.getByName(r):getController():setCommand(WT.tasks.setInvisible)
78+
else
79+
Group.getByName(r):getController():setCommand(WT.tasks.setVisible)
80+
end
81+
else
82+
Group.getByName(r):getController():setCommand(WT.tasks.setVisible)
83+
end
84+
end
85+
else
86+
if not WT.proxVis.groups[g].group then
87+
return time + 1
88+
end
89+
for r = 1, #WT.proxVis.groups[g].group do
90+
local dist = WT.proxVis.checkGroups(WT.proxVis.groups[g].group[r], WT.proxVis.groups[g].co)
91+
if dist ~= -1 then
92+
if dist > WT.proxVis.groups[g].distance then
93+
Group.getByName(r):getController():setCommand(WT.tasks.setInvisible)
94+
else
95+
Group.getByName(r):getController():setCommand(WT.tasks.setVisible)
96+
end
97+
else
98+
Group.getByName(r):getController():setCommand(WT.tasks.setVisible)
99+
end
100+
end
101+
end
102+
end
103+
return time + 1
104+
end
105+
106+
-----------------------------
107+
--proxVis: renders players invisible when they are more than a set distance from any hostiles
108+
--group: group to function on (1 or 2 for all player redfor or player blufor respectively)
109+
--coalition: coalition of AI units that should be used for vis
110+
--distance: distance in meters they must be within to be visible
111+
-----------------------------
112+
function WT.proxVis.setup(group, coalition, distance)
113+
if #WT.proxVis.groups < 1 then
114+
timer.scheduleFunction(WT.proxVis.check, 2, timer.getTime() + 1)
115+
end
116+
WT.proxVis.groups[#WT.proxVis.groups + 1] = { group = group, co = coalition, distance = distance, covered = false }
117+
end

Features/ScenicFire.lua

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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

Comments
 (0)