396 lines
11 KiB
Lua
Executable File
396 lines
11 KiB
Lua
Executable File
--[[ Alchemist AI ]]
|
|
|
|
require( "ai/ai_core" )
|
|
|
|
function Spawn( entityKeyValues )
|
|
local hCurrentEncounter = GameRules.Aghanim:GetCurrentRoom():GetEncounter()
|
|
if hCurrentEncounter.activeTargets == nil then
|
|
hCurrentEncounter.activeTargets = {}
|
|
end
|
|
|
|
thisEntity:AddNewModifier( thisEntity, nil, "modifier_attack_speed_unslowable", { attack_speed_reduction_pct = 20 } )
|
|
thisEntity:AddNewModifier( thisEntity, nil, "modifier_move_speed_unslowable", { move_speed_reduction_pct = 20 } )
|
|
|
|
thisEntity:SetContextThink( "AIThink", AIThink, 0.25 )
|
|
behaviorSystem = AICore:CreateBehaviorSystem( thisEntity, { BehaviorNone, BehaviorConcoction, BehaviorAcidSpray } ) --, BehaviorRunAway } )
|
|
thisEntity.nextTargetTime = GameRules:GetGameTime()
|
|
end
|
|
|
|
function UpdateOnRemove()
|
|
behaviorSystem:Destroy()
|
|
end
|
|
|
|
function AIThink() -- For some reason AddThinkToEnt doesn't accept member functions
|
|
return behaviorSystem:Think( )
|
|
end
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
function RemoveAvailableTarget( availableHeroes, nEntIndexToRemove )
|
|
|
|
if #availableHeroes == 1 then
|
|
return
|
|
end
|
|
|
|
for i=1,#availableHeroes do
|
|
if availableHeroes[i]:entindex() == nEntIndexToRemove then
|
|
table.remove( availableHeroes, i )
|
|
break
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
function SelectNewTarget( )
|
|
|
|
local hCurrentEncounter = GameRules.Aghanim:GetCurrentRoom():GetEncounter()
|
|
|
|
-- Mark current target as not active any more
|
|
-- Important to do prior to the code that grab a target which is not already being chased
|
|
local nEntIndex = -1
|
|
if thisEntity.hTarget ~= nil then
|
|
nEntIndex = thisEntity.hTarget:entindex()
|
|
hCurrentEncounter.activeTargets[ tostring( nEntIndex ) ] = nil
|
|
end
|
|
|
|
local availableHeroes = GetAliveHeroesInRoom()
|
|
|
|
-- Remove not visible heroes
|
|
for i=#availableHeroes,1,-1 do
|
|
if not thisEntity:CanEntityBeSeenByMyTeam( availableHeroes[i] ) then
|
|
table.remove( availableHeroes, i )
|
|
end
|
|
end
|
|
|
|
-- Prefer to grab a target which is not already being chased by another alchemist
|
|
for szEntIndex,bIsActive in pairs( hCurrentEncounter.activeTargets ) do
|
|
if bIsActive == true then
|
|
RemoveAvailableTarget( availableHeroes, tonumber( szEntIndex ) )
|
|
end
|
|
end
|
|
|
|
-- Prefer to pick a different target from last time
|
|
RemoveAvailableTarget( availableHeroes, nEntIndex )
|
|
|
|
-- Select a random target from the available ones
|
|
local hNewTarget = nil
|
|
if #availableHeroes > 0 then
|
|
hNewTarget = availableHeroes[ math.random( 1, #availableHeroes ) ]
|
|
end
|
|
|
|
thisEntity.hTarget = hNewTarget
|
|
thisEntity.nextTargetTime = GameRules:GetGameTime() + 10 -- Keep on this guy for 10 seconds at least
|
|
|
|
if thisEntity.hTarget ~= nil then
|
|
hCurrentEncounter.activeTargets[ tostring( thisEntity.hTarget:entindex() ) ] = true
|
|
end
|
|
|
|
end
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
BehaviorNone = {}
|
|
|
|
function BehaviorNone:Evaluate()
|
|
return 1 -- must return a value > 0, so we have a default
|
|
end
|
|
|
|
function BehaviorNone:Destroy()
|
|
|
|
local hCurrentEncounter = GameRules.Aghanim:GetCurrentRoom():GetEncounter()
|
|
|
|
-- Mark current target as not active any more
|
|
if thisEntity.hTarget ~= nil then
|
|
hCurrentEncounter.activeTargets[ tostring( thisEntity.hTarget:entindex() ) ] = nil
|
|
thisEntity.hTarget = nil
|
|
end
|
|
|
|
end
|
|
|
|
function BehaviorNone:Begin()
|
|
--print( "BehaviorNone:Begin()" )
|
|
local orders = nil
|
|
|
|
-- Acquire a new target if necessary
|
|
if thisEntity.hTarget == nil or ( thisEntity.nextTargetTime <= GameRules:GetGameTime() ) or
|
|
not thisEntity.hTarget:IsAlive() or
|
|
( not thisEntity:CanEntityBeSeenByMyTeam( thisEntity.hTarget ) and ( ( thisEntity:GetAbsOrigin() - thisEntity.lastTargetPosition ):Length2D() < 250 ) ) then
|
|
SelectNewTarget()
|
|
end
|
|
|
|
if thisEntity.hTarget ~= nil then
|
|
thisEntity.lastTargetPosition = thisEntity.hTarget:GetAbsOrigin()
|
|
orders =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET,
|
|
TargetIndex = thisEntity.hTarget:entindex()
|
|
}
|
|
elseif thisEntity.lastTargetPosition ~= nil then
|
|
orders =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE,
|
|
Position = thisEntity.lastTargetPosition
|
|
}
|
|
else
|
|
orders =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_STOP
|
|
}
|
|
end
|
|
|
|
return orders
|
|
end
|
|
|
|
BehaviorNone.Continue = BehaviorNone.Begin
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
BehaviorConcoction = {}
|
|
|
|
function BehaviorConcoction:Evaluate()
|
|
--print( "BehaviorConcoction:Evaluate()" )
|
|
local desire = 0
|
|
|
|
-- let's not choose this twice in a row
|
|
if behaviorSystem.currentBehavior == self then
|
|
return desire
|
|
end
|
|
|
|
-- Got to have a target
|
|
if thisEntity.hTarget == nil or not thisEntity.hTarget:IsAlive() or not thisEntity:CanEntityBeSeenByMyTeam( thisEntity.hTarget ) then
|
|
return desire
|
|
end
|
|
|
|
self.concoctionAbility = thisEntity:FindAbilityByName( "alchemist_unstable_concoction" )
|
|
self.nBrewTime = self.concoctionAbility:GetSpecialValueFor( "brew_time")
|
|
self.nSelfExplodeTime = self.concoctionAbility:GetSpecialValueFor( "brew_explosion")
|
|
|
|
if self.concoctionAbility and self.concoctionAbility:IsFullyCastable() then
|
|
|
|
-- Don't do it if other alchemists are currently conconting
|
|
local friends = FindUnitsInRadius( DOTA_TEAM_BADGUYS, thisEntity:GetOrigin(), thisEntity, 5000, DOTA_UNIT_TARGET_TEAM_FRIENDLY, DOTA_UNIT_TARGET_HERO, 0, 0, false )
|
|
for i = 1,#friends do
|
|
if friends[i] ~= thisEntity and friends[i].bInConcontion ~= nil and friends[i].bInConcontion == true then
|
|
return desire
|
|
end
|
|
end
|
|
|
|
--print( "Concoction:Evaluate: thisEntity.hTarget:entindex() == " .. thisEntity.hTarget:entindex() )
|
|
thisEntity.lastTargetPosition = thisEntity.hTarget:GetAbsOrigin()
|
|
if thisEntity.hTarget:IsStunned() then
|
|
desire = 2
|
|
else
|
|
desire = 4
|
|
end
|
|
end
|
|
|
|
return desire
|
|
end
|
|
|
|
function BehaviorConcoction:Begin()
|
|
--print( "BehaviorConcoction:Begin()" )
|
|
|
|
thisEntity.bInConcontion = true
|
|
|
|
if self.startConcoctionTime ~= nil then
|
|
return nil
|
|
end
|
|
|
|
self.shivasAbility = nil
|
|
self.phaseAbility = nil
|
|
for i = 0, DOTA_ITEM_MAX - 1 do
|
|
local item = thisEntity:GetItemInSlot( i )
|
|
if item and item:GetAbilityName() == "item_shivas_guard" then
|
|
self.shivasAbility = item
|
|
end
|
|
if item and item:GetAbilityName() == "item_phase_boots" then
|
|
self.phaseAbility = item
|
|
end
|
|
end
|
|
|
|
self.concoctionThrowAbility = thisEntity:FindAbilityByName( "alchemist_unstable_concoction_throw" )
|
|
self.chemicalRageAbility = thisEntity:FindAbilityByName( "alchemist_chemical_rage" )
|
|
|
|
if self.concoctionAbility and self.concoctionAbility:IsFullyCastable() then
|
|
self.startConcoctionTime = GameRules:GetGameTime()
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET,
|
|
AbilityIndex = self.concoctionAbility:entindex(),
|
|
}
|
|
return order
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
BehaviorConcoction.Continue = BehaviorConcoction.Begin
|
|
|
|
function BehaviorConcoction:End()
|
|
thisEntity.bInConcontion = false
|
|
end
|
|
|
|
function BehaviorConcoction:IsDone()
|
|
return ( self.startConcoctionTime == nil )
|
|
end
|
|
|
|
function BehaviorConcoction:Think( )
|
|
if self.startConcoctionTime == nil then
|
|
return nil
|
|
end
|
|
|
|
--print( "BehaviorConcoction:Think( )" )
|
|
--print( "-----------------------------------" )
|
|
--print( "self.startConcoctionTime == " .. self.startConcoctionTime )
|
|
--print( "GameRules:GetGameTime() == " .. GameRules:GetGameTime() )
|
|
|
|
-- reacquire target if possible
|
|
if thisEntity.hTarget == nil or not thisEntity.hTarget:IsAlive() then
|
|
SelectNewTarget()
|
|
if thisEntity.hTarget == nil then
|
|
-- No target? Move to last valid target position
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE,
|
|
Position = thisEntity.lastTargetPosition
|
|
}
|
|
return order
|
|
end
|
|
end
|
|
|
|
thisEntity.lastTargetPosition = thisEntity.hTarget:GetAbsOrigin()
|
|
|
|
-- if we missed our cast window for some reason
|
|
if GameRules:GetGameTime() >= ( self.startConcoctionTime + self.nSelfExplodeTime ) then
|
|
--print( "ending")
|
|
self.startConcoctionTime = nil
|
|
return nil
|
|
end
|
|
|
|
-- If we're still waiting to throw, then try to close...
|
|
if GameRules:GetGameTime() < ( self.startConcoctionTime + self.nBrewTime ) then
|
|
|
|
-- Cast phase if we can
|
|
if self.phaseAbility and self.phaseAbility:IsFullyCastable() then
|
|
--print( "Casting phase" )
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET,
|
|
AbilityIndex = self.phaseAbility:entindex()
|
|
}
|
|
return order
|
|
end
|
|
|
|
-- Cast Shiva if we can
|
|
if self.shivasAbility and self.shivasAbility:IsFullyCastable() then
|
|
--print( "Casting shiva" )
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET,
|
|
AbilityIndex = self.shivasAbility:entindex()
|
|
}
|
|
return order
|
|
end
|
|
|
|
else
|
|
|
|
-- Ok, we're able to throw, so lets throw
|
|
if self.concoctionThrowAbility and not self.concoctionThrowAbility:IsHidden() and self.concoctionThrowAbility:IsFullyCastable() then
|
|
--print( "Casting throw" )
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_CAST_TARGET,
|
|
AbilityIndex = self.concoctionThrowAbility:entindex(),
|
|
TargetIndex = thisEntity.hTarget:entindex()
|
|
}
|
|
|
|
return order
|
|
end
|
|
|
|
-- Otherwise, try to cast chemical rage
|
|
if self.chemicalRageAbility and self.chemicalRageAbility:IsFullyCastable() then
|
|
--print( "Casting Chemical Rage" )
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET,
|
|
AbilityIndex = self.chemicalRageAbility:entindex(),
|
|
}
|
|
return order
|
|
end
|
|
|
|
end
|
|
|
|
-- Nothing better to do? Chase our target
|
|
--print( "Attacking" )
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET,
|
|
TargetIndex = thisEntity.hTarget:entindex()
|
|
}
|
|
return order
|
|
|
|
end
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
BehaviorAcidSpray = {}
|
|
|
|
function BehaviorAcidSpray:Evaluate()
|
|
--print( "BehaviorAcidSpray:Evaluate()" )
|
|
local desire = 0
|
|
|
|
-- let's not choose this twice in a row
|
|
if behaviorSystem.currentBehavior == self then
|
|
return desire
|
|
end
|
|
|
|
-- Got to have a target
|
|
if thisEntity.hTarget == nil or not thisEntity.hTarget:IsAlive() or not thisEntity:CanEntityBeSeenByMyTeam( thisEntity.hTarget ) then
|
|
return desire
|
|
end
|
|
|
|
self.acidSprayAbility = thisEntity:FindAbilityByName( "alchemist_acid_spray" )
|
|
if self.acidSprayAbility and self.acidSprayAbility:IsFullyCastable() then
|
|
if thisEntity.hTarget:IsStunned() then
|
|
desire = 6
|
|
else
|
|
desire = 4
|
|
end
|
|
end
|
|
|
|
return desire
|
|
end
|
|
|
|
function BehaviorAcidSpray:Begin()
|
|
--print( "BehaviorAcidSpray:Begin()" )
|
|
|
|
if self.acidSprayAbility and self.acidSprayAbility:IsFullyCastable() then
|
|
--print( "Casting Acid Spray" )
|
|
local targetPoint = thisEntity.lastTargetPosition + RandomVector( 100 )
|
|
local order =
|
|
{
|
|
UnitIndex = thisEntity:entindex(),
|
|
OrderType = DOTA_UNIT_ORDER_CAST_POSITION,
|
|
AbilityIndex = self.acidSprayAbility:entindex(),
|
|
Position = targetPoint
|
|
}
|
|
return order
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
BehaviorAcidSpray.Continue = BehaviorAcidSpray.Begin
|