Snippet #124806

TTL: forever — WordwrapView raw

on 2022/11/02 13:10:52 (UTC) by Anonymous as Lua

  1. local widgetName = "Next Gen Bombers"
  2.  
  3. function widget:GetInfo()
  4.    return {
  5. 	name      = widgetName,
  6. 	desc      = "Adds several features to Likho, Raven, Phoenix and Thunderbird:\n1. If an attacked unit becomes cloaked, bombers will hit its presumptive location (accounting for last seen position and velocity)\n2. Submerged units can be targeted, the water surface above the target will be hit, accounting for units speed. Raven may hit fast submerged units like Seawolf.\n3. If a targeted unit got destroyed by something else, and there was not a queued command, the bomber will return to air factory or airpad, to avoid dangerous circling at the frontline.\n4. In contrast to 'Smart Bombers' widget (which should be disabled to use this one), this widget not only temporarily turns on the 'Free Fire' state when Attack Move is issued, but also discards the Attack Move command after firing. Thus the Attack Move becomes one-time action rather than a kind of a state.",
  7. 	author    = "rollmops",
  8. 	date      = "2022",
  9. 	license   = "GNU GPL, v2 or later",
  10. 	layer     = 0,
  11. 	enabled   = true  --  loaded by default
  12.  }
  13. end
  14.  
  15. --------------------------------------------------------------------------------
  16. -- Speedups
  17. --------------------------------------------------------------------------------
  18.  
  19. local sqrt = math.sqrt
  20.  
  21. local spGetUnitPosition         = Spring.GetUnitPosition
  22. local spGiveOrderToUnit         = Spring.GiveOrderToUnit
  23. local spGetTeamUnits            = Spring.GetTeamUnits
  24. local spGetUnitDefID            = Spring.GetUnitDefID
  25. local spEcho                    = Spring.Echo
  26. local spGetUnitCommands         = Spring.GetUnitCommands
  27. local spGetUnitVelocity         = Spring.GetUnitVelocity
  28. local spGetUnitHealth           = Spring.GetUnitHealth
  29. local spGetSpecState            = Spring.GetSpectatingState
  30. local spIsUnitInLos             = Spring.IsUnitInLos
  31. local spGetUnitTeam             = Spring.GetUnitTeam
  32. local spValidUnitID				= Spring.ValidUnitID
  33. local spGetGameFrame			= Spring.GetGameFrame
  34.  
  35. local CMD_FORCE_FIRE            = CMD.ATTACK -- same number (20) as LOOPBACKATTACK
  36. local CMD_REMOVE                = CMD.REMOVE
  37. local CMD_INSERT                = CMD.INSERT
  38. local CMD_OPT_ALT               = CMD.OPT_ALT
  39. local CMD_STOP                  = CMD.STOP
  40. local CMD_OPT_INTERNAL          = CMD.OPT_INTERNAL
  41. local CMD_OPT_SHIFT             = CMD.OPT_SHIFT
  42. local CMD_ATTACK_MOVE           = CMD.FIGHT
  43. local CMD_FIRE_STATE            = CMD.FIRE_STATE
  44. local CMD_FIRESTATE_HOLDFIRE    = CMD.FIRESTATE_HOLDFIRE
  45. local CMD_FIRESTATE_FIREATWILL  = CMD.FIRESTATE_FIREATWILL
  46.  
  47. local customCmds                = VFS.Include("LuaRules/Configs/customcmds.lua")
  48. local CMD_REARM                 = customCmds.REARM		-- 33410
  49. local CMD_RAW_MOVE              = customCmds.RAW_MOVE	-- 31109
  50.  
  51.  
  52. --------------------------------------------------------------------------------
  53. -- Config
  54. --------------------------------------------------------------------------------
  55.  
  56. local gameFramesInterval = 8
  57.  
  58. local debug = false     -- see Echo()
  59.  
  60. local bombersDefID = { -- The four managed bombers types. Projectile speed adjusted manually.
  61. 	[UnitDefNames.bomberheavy.id ] = {name = UnitDefNames.bomberheavy.humanName,  projectileSpeed = 12.5, },
  62. 	[UnitDefNames.bomberdisarm.id] = {name = UnitDefNames.bomberdisarm.humanName, projectileSpeed = 100,},
  63. 	[UnitDefNames.bomberriot.id  ] = {name = UnitDefNames.bomberriot.humanName,   projectileSpeed = 9,  },
  64. 	[UnitDefNames.bomberprec.id  ] = {name = UnitDefNames.bomberprec.humanName,   projectileSpeed = 5.5,},
  65. }
  66.  
  67.  
  68. --------------------------------------------------------------------------------
  69. -- Constants
  70. --------------------------------------------------------------------------------
  71.  
  72. local myTeamID      	= Spring.GetMyTeamID()
  73. local myAllyTeamID		= Spring.GetMyAllyTeamID()
  74.  
  75. local airpadDefID       = UnitDefNames.staticrearm.id
  76. local airFactoryDefID   = UnitDefNames.factoryplane.id
  77. local airPalteDefID     = UnitDefNames.plateplane.id
  78.  
  79. --------------------------------------------------------------------------------
  80. -- Globals
  81. --------------------------------------------------------------------------------
  82.  
  83. local bombers = {}  -- Bomber Class Objects, per bomber
  84.  
  85. local targets = {}
  86. -- Targeted units. Several bombers could have the same target, so targets' data resides in this table:
  87. -- [unitID] -> { inLos=bool, inRadar=bool, inWater=bool, lastKnownPos={x,y,z}, vel={x,y,z,speed}, lastSeen=gameFrame }
  88. -- inLos updated by UnitEnteredLos/UnitLeftLos
  89. -- inRadar updated by UnitEnteredRadar/UnitLeftRadar
  90. -- inWater, lastKnownPos, vel(ocity), and lastSeen updated repeatedly in GameFrame() while target is in LoS,
  91. -- that's because, while position can be obtained in UnitLeftLos(), velocity cannot, so
  92. -- for data consistency, all are obtained in GameFrame().
  93.  
  94.  
  95. -- following are own only, not allied; used as a destination to return if the target gone and there are no queued commands.
  96. local airpads   = {}    -- [unitID] -> {x,y,z} (position)
  97. local airFacs   = {}    -- [unitID] -> {x,y,z}
  98. local airPlates = {}    -- [unitID] -> {x,y,z}
  99.  
  100.  
  101. --------------------------------------------------------------------------------
  102. -- Functions
  103. --------------------------------------------------------------------------------
  104.  
  105.  
  106. local function Echo(...)
  107. -- if 'debug' (defined in Config section) is true,
  108. -- accepts any number of arguments, concatenates them to a space-delimited string, then spEcho() it.
  109.  
  110. 	if not debug then return end
  111.  
  112. 	local msg = "NGB:"	-- original widget name is too long
  113. 	--local msg = widgetName..":"
  114.  
  115. 	for _, s in pairs{...} do
  116. 		msg = msg .. " " .. tostring(s)
  117. 	end
  118. 	spEcho(msg)
  119. end
  120.  
  121. local function GetHumanName(unitID)
  122. 	return spGetUnitDefID(unitID) and UnitDefs[spGetUnitDefID(unitID)] and UnitDefs[spGetUnitDefID(unitID)].humanName or "noname"
  123. end
  124.  
  125. local function GetAimPosition(unitID)
  126. 	local _,_,_,x,y,z = spGetUnitPosition(unitID, false, true) -- last 2 args: bool return midPos , bool return aimPos
  127. 	return {x=x, y=y, z=z}
  128. end
  129.  
  130. local function DiscardOrphanedTargets()
  131. -- remove target objects which no bomber has them anymore as the target (current or queued)
  132.  
  133. 	for targetID in pairs(targets) do
  134. 		local orphaned = true
  135. 		for _, bomber in pairs(bombers) do
  136. 			if bomber.target == targetID then
  137. 				orphaned = false
  138. 				break
  139. 			end
  140. 			for _, cmd in pairs(bomber.queue) do
  141. 				if cmd.target == targetID then
  142. 					orphaned = false
  143. 					break
  144. 				end
  145. 			end
  146. 		end
  147. 		if orphaned then
  148. 			targets[targetID] = nil
  149. 			--Echo("orphaned target discarded")
  150. 		end
  151. 	end
  152. end
  153.  
  154. local function BombersHitTargetPosition(targetID, targetPos)
  155. 	for _, bomber in pairs(bombers) do
  156. 		if bomber.target == targetID then
  157. 			bomber:HitTargetPosition({x=targetPos.x, y=targetPos.y, z=targetPos.z})
  158. 		end
  159. 	end
  160. end
  161.  
  162. local function BombersHitTargetID(targetID)
  163. 	for _, bomber in pairs(bombers) do
  164. 		if bomber.target == targetID then
  165. 			spGiveOrderToUnit(bomber.id, CMD_FORCE_FIRE, targetID, CMD_OPT_INTERNAL)
  166. 		end
  167. 	end
  168. end
  169.  
  170. local function TargetIsGone(targetID)
  171. -- called when target is destroyed by something else, or never been in LoS and left radar
  172. 	--Echo("Target is gone", targetID)
  173. 	for _, bomber in pairs(bombers) do
  174. 		if bomber.target == targetID then
  175. 			bomber.target = nil
  176. 			spGiveOrderToUnit(bomber.id, CMD_REMOVE, CMD_FORCE_FIRE, CMD_OPT_ALT)
  177. 			bomber:RestoreQueuedCmds(false) -- arg is needRearm
  178. 		else
  179. 			for k, cmd in pairs(bomber.queue) do
  180. 				if cmd.target == targetID then
  181. 					table.remove(bomber.queue, k)
  182. 					-- shifts the queue up, in contrast to 'bomber.queue[k]=nil' which makes a hole.
  183. 				end
  184. 			end
  185. 		end
  186. 	end
  187. 	targets[targetID] = nil
  188. end
  189.  
  190. -- to debug "Bad command from..." messages in log; see AllowCommandParams in gadgets.lua which issues these msgs.
  191. --local SIZE_LIMIT = 10^8
  192. --local function CheckCommandParams(cmdParams)
  193. 	--for i = 1, #cmdParams do
  194. 		--if (not cmdParams[i]) or cmdParams[i] ~= cmdParams[i] or cmdParams[i] < -SIZE_LIMIT or cmdParams[i] > SIZE_LIMIT then
  195. 			--Echo("Bad command: i=",i,"param=",cmdParams[i])
  196. 		--end
  197. 	--end
  198. --end
  199. --local function DebugGiveOrder(...)
  200. 	--CheckCommandParams(select(3, ...))
  201. 	--spGiveOrderToUnit(...)
  202. --end
  203.  
  204.  
  205.  
  206. --------------------------------------------------------------------------------
  207. -- Bomber Class and its Methods
  208. --------------------------------------------------------------------------------
  209.  
  210. local bomberClass = {id, defid, name, weaponSpeed, attackMove, pos = {}, queue = {}, target}
  211.  
  212. function bomberClass:New(unitID, unitDefID)
  213. 	local o = {}
  214. 	setmetatable(o, self)
  215. 	self.__index = self
  216. 	o.id            = unitID
  217. 	o.defid         = unitDefID or spGetUnitDefID(unitID)
  218. 	o.name          = bombersDefID[o.defid].name
  219. 	o.weaponSpeed   = bombersDefID[o.defid].projectileSpeed
  220. 	o.attackMove	= false
  221. 	o.pos = {}
  222. 	o.queue = {}    -- Keeps commands added with SHIFT. [n] -> {cmdID, params={}, target=unitID/nil}
  223. 					-- Commands added by UnitCommand, removed (restored) by RestoreQueuedCmds
  224. 	return o
  225. 	--Echo("added:", o.id, o.name)
  226. end
  227.  
  228. function bomberClass:HitTargetPosition(targetPos)
  229.  
  230. -- For cloaked and submerged targets, hit their position with "Force Fire Point".
  231. -- Approximating ballistic trajectory with constant speed trajectory, see:
  232. -- https://playtechs.blogspot.com/2007/04/aiming-at-moving-target.html
  233.  
  234.  
  235. 	if not (targetPos and targets[self.target].vel) then return end
  236.  
  237. 	x, y, z = spGetUnitPosition(self.id)
  238.  
  239. 	-- relative position of the target (in relation to bomber's position)
  240. 	local px = targetPos.x - x
  241. 	local py = targetPos.y - y
  242. 	local pz = targetPos.z - z
  243.  
  244. 	-- original algorithm uses relative velocity,
  245. 	-- but it seems bombers' weapons speed doesn't depend on bomber's speed,
  246. 	-- hence using target's absolute velocity.
  247. 	local vx = targets[self.target].vel.x
  248. 	local vy = targets[self.target].vel.y
  249. 	local vz = targets[self.target].vel.z
  250.  
  251.  
  252. 	local a = self.weaponSpeed * self.weaponSpeed - targets[self.target].vel.speed * targets[self.target].vel.speed
  253. 	if a < 0.01 then return end -- should not happen as weapon speed > target speed
  254. 	local b = px * vx + py * vy + pz * vz
  255. 	local c = px * px + py * py + pz * pz
  256. 	local d = b * b + a * c
  257. 	if d>= 0 then
  258. 		local t = (b + sqrt(d)) / a
  259. 		--local t2 = (b - sqrt(d)) / a
  260. 		--if t2 > 0 then Echo("T2 POSITIVE: t=", t, "t2 = ", t2) end    -- should not happen
  261. 		local aimX = targetPos.x + vx * t
  262. 		local aimY = targetPos.y + vy * t
  263. 		local aimZ = targetPos.z + vz * t
  264. 		spGiveOrderToUnit(self.id, CMD_FORCE_FIRE, {aimX, aimY, aimZ }, CMD_OPT_INTERNAL)
  265. 	else
  266. 		--Echo ("d is negative")  -- should not happen
  267. 	end
  268. end
  269.  
  270. function bomberClass:RestoreQueuedCmds(needRearm)
  271. -- Since for submerged and cloaked units the widget repeatedly issues Force Fire To Point command,
  272. -- the player's commands added with SHIFT are kept in bomber.queue and are restored when the target is hit or destroyed.
  273. -- Queue is FIFO (commands are pushed to the end of it in UnitCommand, here they are popped from the beginning).
  274. -- Arg needRearm is bool, to know whether rearm is needed, so to restore only move commands or fight commands too.
  275.  
  276. 	--Echo("Restoring queue for",self.id)
  277.  
  278. 	local cmd = table.remove(self.queue, 1)
  279.  
  280. 	if not cmd then
  281. 		--Echo("no cmd in queue")
  282. 		if not needRearm then	-- if needs rearm, it will get rearm command in GameFrame
  283. 			--Echo("Fly to Base", self.id)
  284. 			local padPos = airpads[next(airpads)] or airFacs[next(airFacs)] or airPlates[next(airPlates)]
  285. 			if padPos then
  286. 				spGiveOrderToUnit(self.id, CMD_RAW_MOVE, padPos, CMD_OPT_INTERNAL)
  287. 			end
  288. 		end
  289. 		return
  290. 	end
  291.  
  292. 	if cmd.target then
  293.  
  294. 		if needRearm then
  295. 			-- can't fire, need rearm; try to find non-FFU cmd such as Move
  296. 			DiscardOrphanedTargets()	-- may be only this bomber has cmd.target
  297. 			self:RestoreQueuedCmds(needRearm) -- try next command as first (recursion!)
  298. 			return
  299. 		end
  300.  
  301. 		self.target = cmd.target
  302.  
  303. 		--Echo("queued target restored:", GetHumanName(cmd.target), cmd.target, "for", self.id)
  304. 		if targets[cmd.target].inRadar then
  305. 			local pos = GetAimPosition(cmd.target)
  306. 			if pos and pos.y and pos.y >= 0 then  -- if y<0 (submerged), will get commands from GameFrame
  307. 				spGiveOrderToUnit(self.id, CMD_FORCE_FIRE, cmd.target, CMD_OPT_INTERNAL)
  308. 			end
  309. 		elseif targets[cmd.target].lastKnownPos then
  310. 			self:HitTargetPosition(targets[cmd.target].lastKnownPos) -- will continue to get adjusted commands from GameFrame()
  311. 		else
  312. 			TargetIsGone(unitID)	-- not in radar and we have no data about it
  313. 			self:RestoreQueuedCmds(needRearm) -- try next command as first (recursion!)
  314. 		end
  315. 		return -- if queued cmds are MOVE, can continue to give orders, but for Force Fire Unit need to break, keeping the rest of the queue
  316. 	else
  317. 		--Echo("First cmd in queue restored",cmd.cmdID,"for",self.id)
  318. 		spGiveOrderToUnit(self.id, cmd.cmdID, cmd.params, CMD_OPT_INTERNAL)
  319. 	end
  320.  
  321. 	while #self.queue > 0 do    -- rest of the commands are added with shift
  322.  
  323. 		local cmd = table.remove(self.queue, 1)
  324. 		if cmd.target then
  325. 			self.queue = nil    -- Force Fire Unit should not be queued after Move command(s)
  326. 			return
  327. 		else
  328. 			--Echo("Next cmd in queue restored",cmd.cmdID,"for",self.id)
  329. 			spGiveOrderToUnit(self.id, cmd.cmdID, cmd.params, CMD_OPT_SHIFT+CMD_OPT_INTERNAL)
  330. 		end
  331. 	end
  332. end
  333.  
  334.  
  335. --------------------------------------------------------------------------------
  336. -- Callins
  337. --------------------------------------------------------------------------------
  338.  
  339.  
  340. function widget:UnitCommand(unitID, unitDefID, unitTeam, cmdID, cmdParams, cmdOpts, cmdTag)
  341.  
  342. 	if not bombers[unitID] then return end
  343. 	--Echo("UC:",CMD[cmdID],cmdID,"p1=",cmdParams[1],"p2=",cmdParams[2],"p3=",cmdParams[3],"p4=",cmdParams[4],"shift=",cmdOpts.shift,"internal=",cmdOpts.internal,"alt=",cmdOpts.alt)
  344.  
  345. 	-- Catching player-issued commands.
  346. 	-- Placed here and not in CommandNotify as to also catch commands issued as a result of ForceFire+drag
  347. 	if not (cmdID==CMD_INSERT or cmdID==CMD_REMOVE or cmdID==CMD_FIRE_STATE or cmdOpts.internal) then
  348.  
  349. 		-- if targetID is not nil, then targetID is unitID targeted by the bomber (may be a radar point as well)
  350. 		local targetID = cmdID==CMD_FORCE_FIRE and cmdParams and #cmdParams==1 and spValidUnitID(cmdParams[1]) and cmdParams[1]
  351.  
  352. 		if targetID and not targets[targetID] then
  353. 			targets[targetID] = {inRadar = true, inLos = spIsUnitInLos(targetID, myAllyTeamID)}
  354. 			--Echo(cmdOpts.shift and "queued" or "", "target added:", GetHumanName(targetID), cmdParams[1], "for", unitID)
  355. 		end
  356.  
  357. 		if not cmdOpts.shift then
  358. 			if cmdID~=CMD_FORCE_FIRE then
  359. 				spGiveOrderToUnit(unitID, CMD_REMOVE, CMD_FORCE_FIRE, CMD_OPT_ALT)
  360. 				--CMD_OPT_ALT means "use the parameters as commandIDs, not as tags"
  361. 			end
  362. 			bombers[unitID].target = targetID
  363. 			bombers[unitID].queue = {}
  364. 			DiscardOrphanedTargets()
  365. 		else
  366. 			--Echo("Queued cmd",cmdID,"added for",unitID)
  367. 			bombers[unitID].queue[#bombers[unitID].queue+1] = {cmdID = cmdID, params = cmdParams, target = targetID}
  368. 		end
  369.  
  370. 		-- similar functionality to "Smart Bombers" widget,
  371. 		-- but also discards the Attack command after firing (see elseif REARM clause below)
  372. 		if cmdID == CMD_ATTACK_MOVE then
  373. 			--Echo("Set Fire At Will")
  374. 			spGiveOrderToUnit(unitID, CMD_FIRE_STATE, CMD_FIRESTATE_FIREATWILL, CMD_OPT_INTERNAL)
  375. 			bombers[unitID].attackMove = true
  376. 		elseif bombers[unitID].attackMove then
  377. 			spGiveOrderToUnit(unitID, CMD_FIRE_STATE, CMD_FIRESTATE_HOLDFIRE, CMD_OPT_INTERNAL)
  378. 			spGiveOrderToUnit(unitID, CMD_REMOVE, CMD_ATTACK_MOVE, CMD_OPT_ALT)
  379. 			bombers[unitID].attackMove = false
  380. 		end
  381.  
  382. 	-- LuaRules/Gadgets/unit_bomber_command.lua inserts REARM command after a bomber fired, where:
  383. 	-- cmdParams[3] is REARM options = CMD_OPT_SHIFT + CMD_OPT_INTERNAL;
  384. 	-- cmdParams[4] is REARM params[1] = auto-chosen airpad (own or allied closest and having free slot AirPad/Factory or Reef).
  385. 	-- REARM options are tested to distinguish gadget-inserted REARM from one that this block inserts, to avoid endless loop.
  386. 	elseif  cmdID == CMD_INSERT and cmdParams[4]	and
  387. 		cmdParams[2] and cmdParams[2] == CMD_REARM	and
  388. 		cmdParams[3] and cmdParams[3] == CMD_OPT_SHIFT + CMD_OPT_INTERNAL   then
  389.  
  390. 		--Echo("AUTO-REARM INSERTED")
  391.  
  392. 		spGiveOrderToUnit(unitID, CMD_REMOVE, CMD_FORCE_FIRE, CMD_OPT_ALT)
  393. 		bombers[unitID].target = nil
  394. 		DiscardOrphanedTargets()
  395.  
  396. 		spGiveOrderToUnit(unitID, CMD_FIRE_STATE, CMD_FIRESTATE_HOLDFIRE, CMD_OPT_INTERNAL)
  397. 		spGiveOrderToUnit(unitID, CMD_REMOVE, CMD_ATTACK_MOVE, CMD_OPT_ALT)
  398.  
  399. 		if bombers[unitID].queue[1] then
  400. 			bombers[unitID]:RestoreQueuedCmds(true) -- arg is needRearm
  401.  
  402. 			--  move rearm command after restored commands
  403. 			spGiveOrderToUnit(unitID, CMD_REMOVE, CMD_REARM, CMD_OPT_ALT)
  404. 			spGiveOrderToUnit(unitID, CMD_INSERT, {-1, CMD_REARM, CMD_OPT_SHIFT, cmdParams[4]}, CMD_OPT_ALT)
  405. 		end
  406. 	end
  407. end
  408.  
  409. function widget:UnitLeftLos(unitID, unitTeam, allyTeam, unitDefID)
  410. -- For widgets, this one is called just before the unit leaves los,
  411. -- so you can still get the position of a unit that left los,
  412. -- but not the velocity. Hence, target's data obtained in GameFrame()
  413. -- Btw, seems that it doesn't provide unitDefID, doesn't matter here.
  414.  
  415. 	if targets[unitID] then
  416. 		--Echo("Target Left Los:", GetHumanName(unitID), unitID)
  417. 		targets[unitID].inLos = false
  418. 	end
  419. end
  420.  
  421. function widget:UnitEnteredLos(unitID, unitTeam, allyTeam, unitDefID)
  422. 	if targets[unitID] then
  423. 		--Echo("Target Entered Los:", GetHumanName(unitID), unitID)
  424. 		targets[unitID].inLos = true
  425. 	end
  426. end
  427.  
  428. function widget:UnitLeftRadar(unitID)
  429. -- Also called when a unit leaves LOS without any radar coverage.
  430. -- For widgets, this is called just after a unit leaves radar coverage,
  431. -- so widgets cannot get the position of units that left their radar.
  432.  
  433. 	if targets[unitID] then
  434. 		--Echo("Target Left Radar:", GetHumanName(unitID), unitID)
  435. 		if targets[unitID].lastKnownPos then
  436. 			targets[unitID].inRadar = false
  437. 			BombersHitTargetPosition(unitID, targets[unitID].lastKnownPos) -- will continue to get adjusted commands from GameFrame()
  438. 		else
  439. 			TargetIsGone(unitID)
  440. 		end
  441. 	end
  442. end
  443.  
  444. function widget:UnitEnteredRadar(unitID, unitTeam, allyTeam, unitDefID)
  445. -- Also called when a unit enters LOS without any radar coverage.
  446.  
  447. 	if targets[unitID] then
  448. 		--Echo("Target Entered Radar:", GetHumanName(unitID), unitID)
  449. 		targets[unitID].inRadar = true
  450. 		local pos = GetAimPosition(unitID)
  451. 		if pos and pos.y and pos.y >= 0 then -- if y<0 (submerged), will get commands from GameFrame
  452. 			BombersHitTargetID(unitID)
  453. 		end
  454. 	end
  455. end
  456.  
  457. function widget:GameFrame(gameFrame)
  458. 	if gameFrame % gameFramesInterval ~= gameFramesInterval - 1 then return end
  459.  
  460. 	if next(targets) == nil then return end
  461.  
  462. 	for targetID, target in pairs(targets) do
  463.  
  464. 		if target.inLos then
  465.  
  466. 			local vx,vy,vz,v = spGetUnitVelocity(targetID)
  467. 			local pos 		 = GetAimPosition(targetID)
  468.  
  469. 			if pos and pos.x and pos.y and pos.z and vx and vy and vz and v then
  470. 				target.vel 			= {x=vx, y=vy, z=vz, speed=v}
  471. 				target.lastKnownPos = pos
  472. 				target.lastSeen	 	= gameFrame
  473. 				--Echo("target data updated")
  474. 				if pos.y < 0 then
  475. 					target.inWater = true
  476. 					BombersHitTargetPosition(targetID, pos)
  477. 				elseif target.inWater then -- was submerged but not now
  478. 					target.inWater = false
  479. 					BombersHitTargetID(targetID)
  480. 				end
  481. 			else
  482. 				--Echo("target data missing")
  483. 			end
  484.  
  485. 		elseif not target.inRadar then -- if its in Radar, continue with the regular Force Fire
  486.  
  487. 			if target.lastSeen then
  488.  
  489. 				local framesPassedSinceSeen = gameFrame - target.lastSeen
  490.  
  491. 				-- predict target's position by last known position and velocity
  492. 				BombersHitTargetPosition(targetID, {x = target.lastKnownPos.x + target.vel.x * framesPassedSinceSeen,
  493. 													y = target.lastKnownPos.y + target.vel.y * framesPassedSinceSeen,
  494. 													z = target.lastKnownPos.z + target.vel.z * framesPassedSinceSeen})
  495. 			else
  496. 				TargetIsGone(targetID)
  497. 			end
  498. 		end
  499. 	end
  500. end
  501.  
  502. --function widget:UnitCmdDone(unitID, unitDefID, unitTeam, cmdID, cmdParams, cmdOpts, cmdTag)
  503. 	--Echo("UnitCmdDone:", unitID, cmdID, cmdParams[1], cmdOpts)
  504. --end
  505.  
  506. function widget:UnitFinished(unitID, unitDefID, unitTeam)
  507. 	if unitTeam == myTeamID then
  508. 		if bombersDefID[unitDefID] and not bombers[unitID] then
  509. 			bombers[unitID] = bomberClass:New(unitID, unitDefID)
  510. 		elseif unitDefID == airpadDefID then
  511. 			local x,y,z = spGetUnitPosition(unitID)
  512. 			airpads[unitID] = {x,y,z}
  513. 		elseif unitDefID == airFactoryDefID then
  514. 			local x,y,z = spGetUnitPosition(unitID)
  515. 			airFacs[unitID] = {x,y,z}
  516. 		elseif unitDefID == airPalteDefID then
  517. 			local x,y,z = spGetUnitPosition(unitID)
  518. 			airPlates[unitID] = {x,y,z}
  519. 		end
  520. 	end
  521. end
  522.  
  523. function widget:UnitGiven(unitID, unitDefID, unitTeam)
  524. 	local _,_,_,_,buildProgress = spGetUnitHealth (unitID)
  525. 	if buildProgress and buildProgress == 1 then
  526. 		widget:UnitFinished(unitID, unitDefID, unitTeam)
  527. 	end
  528. end
  529.  
  530. function widget:UnitDestroyed(unitID)
  531. 	--Echo("unit destroyed")  -- takes long time to arrive? (after destroying cloaked target)
  532.  
  533. 	if targets[unitID] then
  534. 		--Echo("target destroyed", GetHumanName(unitID))
  535. 		TargetIsGone(unitID)
  536.  
  537. 	elseif bombers[unitID] then
  538. 		--Echo("bomber destroyed")
  539. 		bombers[unitID] = nil
  540. 		DiscardOrphanedTargets()
  541.  
  542. 	elseif airpads[unitID] then
  543. 		airpads[unitID] = nil
  544. 	elseif airFacs[unitID] then
  545. 		airFacs[unitID] = nil
  546. 	elseif airPlates[unitID] then
  547. 		airPlates[unitID] = nil
  548. 	end
  549. end
  550.  
  551. function widget:UnitTaken(unitID)
  552. 	widget:UnitDestroyed(unitID)
  553. end
  554.  
  555. function widget:Initialize()
  556. 	if (Spring.GetSpectatingState() or Spring.IsReplay()) then
  557. 		widgetHandler:RemoveWidget(widget)
  558. 	end
  559. 	local myUnits = spGetTeamUnits(myTeamID)
  560. 	if myUnits then
  561. 		for _, unitID in pairs(myUnits) do
  562. 			widget:UnitGiven(unitID, spGetUnitDefID(unitID), myTeamID)
  563. 		end
  564. 	end
  565. end
  566.  
  567. function widget:PlayerChanged(playerID)
  568. 	if spGetSpecState() then widgetHandler:RemoveWidget(widget) end
  569. end

Recent Snippets