-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcmd_area_attack_tweak.lua
328 lines (305 loc) · 11.7 KB
/
cmd_area_attack_tweak.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
function widget:GetInfo()
return {
name = "Area Attack Tweak",
desc = "CTRL+Attack splits targets. AA automatically drops ground targets.",
author = "msafwan, Tom Fyuri",
version = "2023",
license = "GNU GPL, v2 or later",
layer = 0,
enabled = true -- loaded by default?
}
end
-- Zero-K classics now in BAR!
-- select shurikens, press attack, draw a circle over enemy units, watch all shurikens pick their own target
-- entire enemy batch of units are now stunned ready for pick up and reclaiming. :>
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
VFS.Include("LuaRules/Configs/customcmds.h.lua")
local SPLIT_ATTACK_SINGLE = false
local MAX_TOTAL_QUEUE = 2000
local MAX_ENGINE_HANDLE = 10
local MIN_QUEUE = 8
local defaultCommands = {
[CMD.ATTACK] = true,
[CMD.AREA_ATTACK] = true,
[CMD.FIGHT] = true,
[CMD.PATROL] = true,
[CMD.GUARD] = true,
[CMD.MANUALFIRE] = true,
[CMD.MOVE] = true,
[CMD_RAW_MOVE] = true,
[CMD_UNIT_SET_TARGET] = true,
-- [CMD_UNIT_SET_TARGET_CIRCLE] = true,
-- [CMD_RAW_BUILD] = true,
-- [CMD.REMOVE] = true,
-- [CMD.INSERT] = true,
-- [CMD_REARM] = true,
-- [CMD_FIND_PAD] = true,
}
local spGetUnitPosition = Spring.GetUnitPosition
local unitsSplitAttackQueue = {nil} --just in case user press SHIFT after doing split attack, we need to remove these queue
local handledCount = 0
function widget:CommandNotify(id, params, options) --ref: gui_tacticalCalculator.lua by msafwan, and central_build_AI.lua by Troy H. Creek
if not defaultCommands[id] or options.internal then --only process user's command
return false
end
if Spring.GetSelectedUnitsCount() == 0 then --skip whole thing if no selection
return false
end
local units
if SPLIT_ATTACK_SINGLE and handledCount > 0 then
--This remove all but 1st attack order from CTRL+Area_attack if user choose to append new order to unit (eg: SHIFT+move),
--this is to be consistent with bomber_command (rearm-able bombers), which only shoot 1st target and move on to next order.
--Known limitation: not able to remove order if user queued faster than network delay (it need to see unit's current command queue)
units = Spring.GetSelectedUnits()
local unitID, attackList
for i=1,#units do
unitID = units[i]
attackList = GetAndRemoveHandledHistory(unitID)
if attackList and options.shift then
RevertAllButOneAttackQueue(unitID,attackList)
end
end
end
if (id == CMD.ATTACK or id == CMD_UNIT_SET_TARGET or id == CMD_UNIT_SET_TARGET_CIRCLE) then
local cx, cy, cz, cr = params[1], params[2], params[3], params[4]
if (cr or 0) == 0 then --not area command
return false
end
if (cx == nil or cy == nil or cz == nil) then --outside of map
return false
end
--The following code filter out ground unit from dedicated AA, and
--split target among selected unit if user press CTRL+Area_attack
local cx2, cy2, cz2 = params[4], params[5], params[6]
units = units or Spring.GetSelectedUnits()
local targetUnits
if cz2 then
targetUnits = Spring.GetUnitsInRectangle(math.min(cx,cx2), math.min(cz,cz2), math.max(cx,cx2), math.max(cz,cz2))
else
targetUnits = Spring.GetUnitsInCylinder(cx, cz, cr)
end
local antiAirUnits,normalUnits = GetAAUnitList(units)
local airTargets, allTargets = ReturnAllAirTarget(targetUnits, Spring.GetUnitAllyTeam(units[1]),(#antiAirUnits>1)) -- get all air target for selected area-command
if #allTargets>0 then --skip if no target
return ReIssueCommandsToUnits(antiAirUnits,airTargets,normalUnits,allTargets,id,options)
end
end
return false
end
if SPLIT_ATTACK_SINGLE then
function widget:UnitGiven(unitID)
GetAndRemoveHandledHistory(unitID)
end
function widget:UnitDestroyed(unitID)
GetAndRemoveHandledHistory(unitID)
end
--------------------------------------------------------------------------------
function GetAndRemoveHandledHistory(unitID)
if unitsSplitAttackQueue[unitID] then
local attackList = unitsSplitAttackQueue[unitID]
unitsSplitAttackQueue[unitID] = nil
handledCount = handledCount - 1
return attackList
end
return nil
end
end
function RevertAllButOneAttackQueue(unitID,attackList)
local queue = Spring.GetCommandQueue(unitID, -1)
if queue then
local toRemoveCount = 0
local queueToRemove = {}
for j=1,#queue do
command = queue[j]
if command.id == CMD.ATTACK and attackList[command.params[1] ] then
if toRemoveCount > 0 then --skip first queue
queueToRemove[toRemoveCount] = command.tag
end
toRemoveCount = toRemoveCount + 1
end
end
Spring.GiveOrderToUnit (unitID,CMD.REMOVE, queueToRemove, 0)
end
end
function ReturnAllAirTarget(targetUnits, selectedAlly,checkAir)
local filteredTargets = {}
local nonFilteredTargets = {}
for i=1, #targetUnits,1 do --see if targets can fly and if they are enemy or ally.
local unitID = targetUnits[i]
local enemyAllyID = Spring.GetUnitAllyTeam(unitID)
if (selectedAlly ~= enemyAllyID) then --differentiate between selected unit, targeted units, and enemyteam. Filter out ally and owned units
if checkAir then
local unitDefID = Spring.GetUnitDefID(unitID)
local unitDef = UnitDefs[unitDefID]
if not unitDef then
if GetDotsFloating(unitID) then --check & remember floating radar dots in new table.
filteredTargets[#filteredTargets +1] = unitID
end
else
if unitDef["canFly"] then --check & remember flying units in new table
filteredTargets[#filteredTargets +1] = unitID
end
end
end
nonFilteredTargets[#nonFilteredTargets +1] = unitID --also copy all target to a non-filtered table
end
end
return filteredTargets, nonFilteredTargets
end
function GetAAUnitList(units)
local antiAirUnits = {nil}
local normalUnits = {nil}
for i=1, #units,1 do --catalog AA and non-AA
local unitID = units[i]
local unitDefID = Spring.GetUnitDefID(unitID)
if unitDefID then
local unitDef_primaryWeapon = UnitDefs[unitDefID].weapons[1]
if (unitDef_primaryWeapon~= nil) then
local primaryWeapon_target = UnitDefs[unitDefID].weapons[1].onlyTargets
local exclusiveAA = (primaryWeapon_target["vtol"]) and
not (primaryWeapon_target["sink"] or primaryWeapon_target["surface"] or primaryWeapon_target["sub"])
--local exclusiveAA = (primaryWeapon_target["fixedwing"] and primaryWeapon_target["gunship"]) and
-- not (primaryWeapon_target["sink"] or primaryWeapon_target["land"] or primaryWeapon_target["sub"])
--[[
Spring.Echo(UnitDefs[unitDefID].weapons[1].onlyTargets)
for name,content in pairs(UnitDefs[unitDefID].weapons[1].onlyTargets) do
Spring.Echo(name)
Spring.Echo(content)
end
--]]
if (exclusiveAA) then
antiAirUnits[#antiAirUnits +1]= unitID
else
normalUnits[#normalUnits +1]= unitID
end
else
normalUnits[#normalUnits +1]= unitID
end
end
end
return antiAirUnits, normalUnits
end
function ReIssueCommandsToUnits(antiAirUnits,airTargets,normalUnits,allTargets,cmdID,options)
local isHandled = false
if options.ctrl then -- split attacks between units
--split between AA and ground,
IssueSplitedCommand(antiAirUnits,airTargets,cmdID,options)
IssueSplitedCommand(normalUnits,allTargets,cmdID,options)
isHandled = true
else -- normal queue
if #antiAirUnits > 1 or #normalUnits > MAX_ENGINE_HANDLE then
--split between AA and ground,
IssueCommand(antiAirUnits,airTargets,cmdID,options)
IssueCommand(normalUnits,allTargets,cmdID,options)
isHandled = true
else
isHandled = false --nothing need to be done, let spring handle
end
end
return isHandled
end
--------------------------------------------------------------------------------
function IssueCommand(selectedUnits, allTargets, cmdID, options)
if #allTargets >= 1 and #selectedUnits >= 1 then
local toQueue = math.max(MIN_QUEUE, math.ceil(MAX_TOTAL_QUEUE / #selectedUnits))
local aveX, aveZ, count, ux, uz = 0, 0, 0, 0, 0
for i = 1, #selectedUnits do
ux, _, uz = spGetUnitPosition(selectedUnits[i])
if ux then
aveX = aveX + ux
aveZ = aveZ + uz
count = count + 1
end
end
if count > 0 then
aveX = aveX/count
aveZ = aveZ/count
end
local attackCommandListAll = PrepareCommandArray(allTargets, cmdID, options, 1, false, toQueue, aveX, aveZ) -- prepare a normal queue (like regular SHIFT)
Spring.GiveOrderArrayToUnitArray(selectedUnits, attackCommandListAll)
end
end
function IssueSplitedCommand(selectedUnits,allTargets,cmdID,options)
if #allTargets >= 1 then
if SPLIT_ATTACK_SINGLE then
local targetsUnordered = {}
if cmdID == CMD.ATTACK then --and not CMD_UNIT_SET_TARGET. Note: only CMD.ATTACK support split attack queue, and in such case we also need to remember the queue so we can revert later if user decided to do SHIFT+Move
for i = 1,#allTargets do
targetsUnordered[allTargets[i] ] = true
end
for i = 1, #selectedUnits do
unitsSplitAttackQueue[selectedUnits[i] ] = targetsUnordered --note: all units in this loop was refer to same table to avoid duplication
handledCount = handledCount + 1
end
end
end
local toQueue = math.max(MIN_QUEUE, math.ceil(MAX_TOTAL_QUEUE / #selectedUnits))
for i = 1, #selectedUnits do
local attackCommandListAll = PrepareCommandArray(allTargets, cmdID, options, i, true, toQueue) --prepare a shuffled queue for target splitting
Spring.GiveOrderArrayToUnitArray ({selectedUnits[i]}, attackCommandListAll)
end
end
end
--------------------------------------------------------------------------------
function GetDotsFloating (unitID) --ref: gui_vertLineAid.lua by msafwan
local x, y, z = spGetUnitPosition(unitID)
if x == nil then
return false
end
local isFloating = false
local groundY = Spring.GetGroundHeight(x,z)
local surfaceY = math.max (groundY, 0) --//select water, or select terrain height depending on which is higher.
if (y-surfaceY) >= 100 then --//mark unit as flying if it appears to float far above surface, if this fail then player can force attack it with single-(non-area)-attack or scout its ID first.
isFloating = true
end
return isFloating
end
function PrepareCommandArray(targetUnits, cmdID, options, indx, shuffle, toQueue, nearX, nearZ)
if nearX then
local dist = {}
local ux, uz = 0, 0
for i = 1, #targetUnits do
ux, _, uz = spGetUnitPosition(targetUnits[i])
ux = (ux or 0) - nearX
uz = (uz or 0) - nearZ
dist[targetUnits[i]] = ux*ux + uz*uz
end
local function SortFunc(a, b)
return dist[a] < dist[b]
end
table.sort(targetUnits, SortFunc)
end
indx = LoopAroundIndex(indx, #targetUnits)
local stepSkip = 1
if shuffle then
stepSkip = (#targetUnits%(2))*-1 +3
--stepSkip is 3 if #targetUnits is EVEN number (eg: 4,6,8), or 2 if #targetUnits is ODD (eg: 3,5,7)
--this will shuffle the target sequence appropriately
end
local attackCommandList = {}
local j = 1
attackCommandList[j] = {cmdID, {targetUnits[indx]}, {((options.shift and "shift") or nil)}}
if cmdID == CMD.ATTACK then
toQueue = (toQueue and math.min(toQueue, #targetUnits - 1)) or (#targetUnits - 1)
for i = 1, toQueue do
j= j + 1
indx = indx + stepSkip --stepSkip>1 will shuffle the queue
indx = LoopAroundIndex(indx, #targetUnits)
attackCommandList[j] = {cmdID, {targetUnits[indx]}, {"shift"}} --every unit get to attack every target
end
end
return attackCommandList
end
--------------------------------------------------------------------------------
function LoopAroundIndex(indx, maxIndex)
----
-- Example:
-- if maxIndex is 3 and input indx is 1,2,3,4,5,6
-- the following 3 line of code will convert indx into: 1,2,3,1,2,3 (which will avoid "index out-of-bound" case)
indx = indx - 1
indx = indx%(maxIndex)
indx = indx + 1
-- also: if indx is (maxIndex + extraValue), then it would convert into: (1 + extraValue).
return indx
end