----------------------------------------
-- File: 'maraudertactic.ai'
-- Created by Gambit @ 02.11.2019
class 'MarauderTactic' (GuardVehicleTactic)
Marauder = {}
function MarauderTactic:__init( squad_ai ) super( squad_ai )
self:SetName("Marauder Tactic")
-- Modifiable Stats
Marauder.G_Time_Between_Successive_Attacks = 0 -- If multiple bombers, do not have them attack simultaneously. Keep lower than 6, if enabled.
self.iMinAttackTimeInterval = 16 -- For EACH bomber, the time between successive attack (keep in mind the time each attack takes!).
Marauder.G_Proximity_Return_Distance = 38 -- The distance from base that we deem minimum to consider the flyer is "back to base".
Marauder.G_Max_Attacking_Range_From_Base = 280 -- The max range OF THE ENEMY, that the flyer will attempt a fly-over.
Marauder.G_AttackInfantry = true -- Self-explanatory.
Marauder.G_AttackVehicles = true -- Self-explanatory. If both true, target will be chosen randomly.
Marauder.G_AttackInfiltratedUnits = false -- Self-explanatory.
Marauder.G_AttackOnlyUnitsWeCanSee = false -- Self-explanatory. Experiential. Better keep it to [false].
Marauder.G_AttackOnlyUnitsWeCanSeeRange = 35 -- If previous is true, this is the detecting range (proximity) of our nearby troops.
Marauder.G_DoNotAttackIfEnemyBuildingsAtLandingProximity = true -- Self-explanatory.
Marauder.G_DoNotAttackIfEnemyBuildingsWithin = 35 -- If the above is enabled, this is the range.
Marauder.G_DoNotAttackIfEnemyTroopsAtLandingProximity = true -- Self-explanatory.
Marauder.G_DoNotAttackIfEnemyTroopsWithin = 30 -- If the above is enabled, this is the range. The flyer will try to land far.
Marauder.G_HQ_Name = "guard_hq" -- The name of the HQ of the player. It is the AE name. No need to change, for Guard.
-- Other Stats
self.initialPosition = self.squad_ai:GetPosition()
Marauder.G_Proximity_Return_Distance_Sqr = Marauder.G_Proximity_Return_Distance * Marauder.G_Proximity_Return_Distance
Marauder.G_Proximity_Return_Distance_Triangulation = Marauder.G_Proximity_Return_Distance * 0.6
Marauder.G_NextAttackTMR = g_iGMT
self.iAttackTMR = g_iGMT
if Marauder.G_Update_HQsTMR == nil then
Marauder.G_Update_HQsTMR = g_iGMT
end
if Marauder.G_Player_HQsPositions == nil then
Marauder.G_Player_HQsPositions = {}
end
end
function MarauderTactic:InitAbilities()
--[[ Init ability ID's / ABILITIES NO LONGER USED!
if Marauder.smoke_id == nil then
Marauder.smoke_id = cpu_manager.stats:GetAbilityID( "guard_smoke_bombs" )
Marauder.krak_id = cpu_manager.stats:GetAbilityID( "guard_krak_bombs" )
Marauder.incendiary_id = cpu_manager.stats:GetAbilityID( "guard_incendiary_bombs" )
end]]
end
function MarauderTactic:DoAbilities()
-- First, update HQs positions every 8 secs
if g_iGMT > Marauder.G_Update_HQsTMR + 8 then
Marauder.G_Update_HQsTMR = g_iGMT
self:UpdateHQs()
end
-- Now check if we must return (after an attack), or we are at a base
self.initialPosition = self.squad_ai:GetPosition()
local must_retrun = true
for i = 1, table.getn(Marauder.G_Player_HQsPositions) do
if distance_sqr(Marauder.G_Player_HQsPositions[i],self.initialPosition) < Marauder.G_Proximity_Return_Distance_Sqr then
must_retrun = false
break
end
end
if self.squad_ai:CanJump() then
-- In case we are away from the base, return to a random valid place (HQ)
if must_retrun then
local all_bases = table.getn(Marauder.G_Player_HQsPositions)
if all_bases > 0 then
local iBasePos = Marauder.G_Player_HQsPositions[math.random(1,all_bases)]
self:ForceSquadJumpNearBack(iBasePos)
end
-- We are at base. We must try to perform an attack
else
if g_iGMT < Marauder.G_NextAttackTMR + Marauder.G_Time_Between_Successive_Attacks
or g_iGMT < self.iAttackTMR + self.iMinAttackTimeInterval then
return
end
local iEnemySquadInf = nil
local iEnemySquadVeh = nil
local iEnemySquad = nil
if Marauder.G_AttackInfantry then
iEnemySquadInf = Ability.Filters.CloseInfantryEnemy(self.initialPosition, Marauder.G_Max_Attacking_Range_From_Base, 5)
--cpu_manager.cpu_player:FindFirstInfantryEnemy(self.initialPosition, Marauder.G_Max_Attacking_Range_From_Base, 5)
end
if Marauder.G_AttackVehicles then
iEnemySquadVeh = Ability.Filters.CloseVehicleEnemy(self.initialPosition, Marauder.G_Max_Attacking_Range_From_Base, 1)
--cpu_manager.cpu_player:FindFirstVehicleEnemy(self.initialPosition, Marauder.G_Max_Attacking_Range_From_Base, 1)
end
if iEnemySquadInf ~= nil and iEnemySquadVeh ~= nil then
if math.random(1,2) == 1 then
iEnemySquad = iEnemySquadInf
else
iEnemySquad = iEnemySquadVeh
end
elseif iEnemySquadInf ~= nil and iEnemySquadVeh == nil then
iEnemySquad = iEnemySquadInf
else
iEnemySquad = iEnemySquadVeh
end
if iEnemySquad ~= nil then
if Marauder.G_AttackInfiltratedUnits or (not iEnemySquad:IsInfiltrating()) then
local iEnemyPos = iEnemySquad:GetPosition()
if (not Marauder.G_AttackOnlyUnitsWeCanSee) or self:WeCanSeePos(iEnemyPos) then
-- Do NOT perform a flyover, if we have our troops nearby!
if cpu_manager.cpu_player:FindFirstHurtSquad( iEnemyPos, 6 ) == nil then
self:ForceSquadAttackJumpNear(iEnemyPos)
Marauder.G_NextAttackTMR = g_iGMT
end
end
end
end
end
end
--[[ Check if we can deploy smoke / ABILITIES NO LONGER USED!
if (self.squad_ai:CanDoAbility(Marauder.smoke_id)) then
-- Search a squad
local iRange = self.squad_ai:GetAbilityRange(Marauder.smoke_id)
local oUnit = Ability.Filters.CloseHurt(self.squad_ai:GetPosition(), iRange, 1)
if (oUnit ~= nil and oUnit:IsInCombat() and cpu_manager:GetUnitStrength(oUnit) > 150) then
self.squad_ai:DoSpecialAbilitySquad(Marauder.smoke_id, oUnit:GetSquad())
end
end
-- Check if we're in close combat - Krak
local oEnemySquad = Ability.Filters.CloseVehicleEnemy(self.squad_ai:GetPosition(), 0, 1)
if (oEnemySquad ~= nil) then
-- Check if we can drop Krak Bombs
if (self.squad_ai:CanDoAbility(Marauder.krak_id)) then
self.squad_ai:DoSpecialAbility(Marauder.krak_id)
end
end
-- Check if we're in close combat - Incendiary
oEnemySquad = Ability.Filters.CloseInfantryEnemy(self.squad_ai:GetPosition(), 0, 5)
if (oEnemySquad ~= nil and not oEnemySquad:IsBroken()) then
-- Check if we can drop Incendiary Bombs
if (self.squad_ai:CanDoAbility(Marauder.incendiary_id)) then
self.squad_ai:DoSpecialAbility(Marauder.incendiary_id)
end
end]]
--[[ Checks jump-able stuck squads, and force them to jump nearby / NO LONGER USED!
if self.squad_ai:CanJump() then
self:SolveStuckCase()
end]]
end
function MarauderTactic:UpdateHQs()
Marauder.G_Player_HQsPositions = {}
for oBuilding in military_manager:GetBases() do
if (oBuilding:IsValid() and oBuilding:GetBaseName() == Marauder.G_HQ_Name) then
table.insert(Marauder.G_Player_HQsPositions,oBuilding:GetPosition())
end
end
end
-- Unstuck Code --------------------------------------------------
function MarauderTactic:SolveStuckCase()
local iPosition = self.squad_ai:GetPosition()
if iPosition.x ~= self.initialPosition.x or iPosition.z ~= self.initialPosition.z then
-- NOT stuck, update previous position and return, we are all good
self.initialPosition = iPosition
return
end
-- If we got here, the squad is NOT moving. See if it is simply waiting, or is stuck!
local state = self.squad_ai:GetTactic():GetState()
if (self.squad_ai:IsInStateMove() or self.squad_ai:IsInStateAttackMove() or state == "Attack") and not self.squad_ai:IsInCombat()
and iPosition.x == self.initialPosition.x and iPosition.z == self.initialPosition.z then
-- STUCK!!!!! Run the unstuck code
self:ForceSquadJumpNear(iPosition)
end
-- Update previous position anyway
self.initialPosition = self.squad_ai:GetPosition()
end
function MarauderTactic:ForceSquadJumpNear(pos)
local iPos = self.squad_ai:GetPosition()
local vJumpPosition = self.squad_ai:GetPosition()
local jumpDist = self.squad_ai:GetJumpDistance()
local jumpDistSqr = jumpDist * jumpDist
local vDir = cpu_manager:GetDirectionToEnemy(pos)
-- First, try an unstuck jump TOWARDS the enemy
-- Try to jump somewhere near, perform 30 checks in total, for a viable position
for i = 1, 12 do
-- Create a jump position
vJumpPosition.x = pos.x + vDir.x * math.random(10, jumpDist)
vJumpPosition.z = pos.z + vDir.z * math.random(10, jumpDist)
-- Check if target position is in range and if unit is able to jump to target position
local iDistanceSqr = distance_sqr(vJumpPosition, iPos)
if iDistanceSqr < jumpDistSqr and self.squad_ai:CanJumpToPosition(vJumpPosition) then
-- Jump to position
self.squad_ai:DoJump(vJumpPosition)
self.last_jump = g_iGMT
self.m_iLastGatherMove = self.last_jump - 10
return
end
end
-- Then try any random nearby place, as a secondary option
for i = 1, 18 do
-- Create a jump position
vJumpPosition.x = pos.x + 0.7 * math.random(-jumpDist, jumpDist)
vJumpPosition.z = pos.z + 0.7 * math.random(-jumpDist, jumpDist)
-- Check if target position is in range and if unit is able to jump to target position
local iDistanceSqr = distance_sqr(vJumpPosition, iPos)
if iDistanceSqr < jumpDistSqr and self.squad_ai:CanJumpToPosition(vJumpPosition) then
-- Jump to position
self.squad_ai:DoJump(vJumpPosition)
self.last_jump = g_iGMT
self.m_iLastGatherMove = self.last_jump - 10
return
end
end
end
function MarauderTactic:ForceSquadJumpNearBack(pos)
local iPos = self.squad_ai:GetPosition()
local vJumpPosition = self.squad_ai:GetPosition()
local jumpDist = self.squad_ai:GetJumpDistance()
local jumpDistSqr = jumpDist * jumpDist
local vDir = cpu_manager:GetDirectionToEnemy(pos)
-- Try to jump somewhere near, perform 15 checks in total, for a viable position, AWAY from the enemy
for i = 1, 15 do
-- Create a jump position
vJumpPosition.x = pos.x - vDir.x * math.random(4, Marauder.G_Proximity_Return_Distance_Triangulation)
vJumpPosition.z = pos.z - vDir.z * math.random(4, Marauder.G_Proximity_Return_Distance_Triangulation)
-- Check if target position is in range and if unit is able to jump to target position
local iDistanceSqr = distance_sqr(vJumpPosition, iPos)
if iDistanceSqr < jumpDistSqr and self.squad_ai:CanJumpToPosition(vJumpPosition) then
-- Jump to position
self.squad_ai:DoJump(vJumpPosition)
--self.last_jump = g_iGMT
--self.m_iLastGatherMove = self.last_jump - 10
return
end
end
end
function MarauderTactic:ForceSquadAttackJumpNear(pos)
local vJumpPosition = self.squad_ai:GetPosition()
local jumpDist = self.squad_ai:GetJumpDistance()
local jumpDistSqr = jumpDist * jumpDist
local ix = 0; local iz = 0;
if sqr(pos.x-vJumpPosition.x) < 0.0001 then
iz = 1
else
local a = (pos.z-vJumpPosition.z)/(pos.x-vJumpPosition.x)
ix = math.sqrt(1/(sqr(a) + 1))
iz = math.abs(a*ix)
end
if vJumpPosition.x > pos.x then ix = -1*ix end
if vJumpPosition.z > pos.z then iz = -1*iz end
-- Try to jump somewhere near, perform 10 checks in total, for a viable position
local rndm = math.random(20, 30)
for i = 1, 10 do
-- Create a jump position
if Marauder.G_DoNotAttackIfEnemyTroopsAtLandingProximity then
rndm = rndm + 4
else
rndm = math.random(25, 40)
end
vJumpPosition.x = pos.x + ix*rndm
vJumpPosition.z = pos.z + iz*rndm
-- Check if target position is in range and if unit is able to jump to target position
local iDistanceSqr = distance_sqr(vJumpPosition, self.initialPosition)
if iDistanceSqr < jumpDistSqr and self.squad_ai:CanJumpToPosition(vJumpPosition) then
local perform_the_attack = true
if Marauder.G_DoNotAttackIfEnemyBuildingsAtLandingProximity then
-- Do NOT attempt the jump, if we are to land near enemy buildings!
local iBuilding = Ability.EntityFilters.CloseBaseEntityEnemy(vJumpPosition, Marauder.G_DoNotAttackIfEnemyBuildingsWithin, 1)
if iBuilding ~= nil then perform_the_attack = false end
end
if perform_the_attack and Marauder.G_DoNotAttackIfEnemyTroopsAtLandingProximity then
-- Do NOT attempt the jump, if we are to land near enemy troops!
local iSquad = Ability.Filters.CloseEnemy(vJumpPosition, Marauder.G_DoNotAttackIfEnemyTroopsWithin, 3)
if iSquad ~= nil then perform_the_attack = false end
end
-- Now, Jump to position, if we must
if perform_the_attack then
self.squad_ai:DoJump(vJumpPosition)
self.iAttackTMR = g_iGMT
--self.last_jump = g_iGMT
--self.m_iLastGatherMove = self.last_jump - 10
return
end
end
end
end
function MarauderTactic:WeCanSeePos(iPos)
local iRangeSqr = Marauder.G_AttackOnlyUnitsWeCanSeeRange * Marauder.G_AttackOnlyUnitsWeCanSeeRange
for oUnit in military_manager:GetSquads() do
if oUnit:IsValid() then
if distance_sqr(oUnit:GetPosition(),iPos) < iRangeSqr then
return true
end
end
end
return false
end

I added/coded both requests.

Now that code is VERY nasty!!! See the variables and modify the accordingly.

The additions:
1] Each flyer will have its own Timer between successive attacks. I have set it at self.iMinAttackTimeInterval = 16.

But 16 is TOO small, because we must also count the time each attack requres fr the flyer!! I would set it to 30.

2] I wrote a very agressive code, so that the flyers WILL NOT land near enemies. And it proved to be more effeicient that the previous itteration!! In short, it check for laning possitions even FAR AFTER the enemy it flyes over. This is the Marauder.G_DoNotAttackIfEnemyTroopsAtLandingProximity, by the way.

Sorry for the delay, it was/is a very busy week.

EDIT: If we are striving for perfection, I think we can squeeze some more nastiness from the code.

Specifically, from the point where the flyers WON'T land if enemy buildings are nearby.

In this case, I can allow it to STILL LAND, if the enemy buildings have no weapons

This will require much CPU horsepower, however.

And it involves the usage of the HasWeapons() function, that I have only seen it working on LPs, and NOT "every" building... But I do not see why it shouldn't.

I've been trying different settings with your latest code and having some issues with consistency, I've seen them land too close to stormboys and the ork listing post, but keep their distance from other buildings. I'm testing with G_AttackOnlyUnitsWeCanSee =false

Its ok as is. I was just hoping to hide the turnaround beyond LOS because its unrealistic for bombers to land behind enemy lines. If its too much work you could do the tyranids scar instead. Theres no rush, I'm still at least month from releasing the tyranids update.

Easy to do of course, but requires some time. I will try to have it ready asap, brother

I have much to do this weekend though. And there is more in line.

Is "the next weekend", OK with you?

Some questions:

1] What is the exact name of the Tyranid race we are dealing with (just to make sure)?

2] Do you want the code to "run silently" (in the background), or to be a win condition (that will be [always on])? The latter is self-explanatory, the former will require me to slightly modify the Data/scar/scarutil.scar file, as well. If you have not touched that, or you do not know what I am talking about, then you portably have the Vanilla one - so we are OK. In this case, you just have to decide among the "silent" or "win condition" implementations.

3] While a squad devours, you want the resources to increase proportionally to the squad member size, right?

Also, note that a recourse increment of "10 per corpse" is not easily set, so I will use a global var as a factor, and you can set it at the value that suits you.

Put the two files, directly into your SCaR folder (Data\scar\). NOT in the winconditions. They will work in the background, silently.

NOTES

1] The Rule works every one sec. Which means that you may find the resource increase "non-sequential". Meaning that you will have "sudden", non-smooth resource increases, evert sec. If you want the increase to be smooth, it CAN be done so - but will require more CPU. It is no problem (the code is already light), but I preferred a balanced approach, hence the implementation.

If you have a light config, or you do not like "sudden increases", say so and I will update it.

The end result is irrelevant to implementation anyway - only the visual representation of the increase r each resource will be smoother, that is all.

(I know, I am an expert in making simple things sound complex!)

2] I have set the increase (g_TyrRace_Factor) to 1 per second per entity devouring. It seemed MUCH to me. A value of 0.25 would be much better.