As for my AI scripts, the possessed units are easy to control thanks to a separate function that, each second of the function loop, uses the AddAssaultUnit and include that unitID type which is not Undead. The problem is, when units are being resurrected or reincarnated, the AI can't gain control of them, since the AI picks on random units of the same UnitIDs, which is so unlike the triggers in WE Trigger Editor that allow us to pick on specific units. But still, I'll give it a try, and see what happens.
Well I looked into this matter a little bit further and have come up with a
solution for timed, possessed, charmed and resurrected units
The real problem like you found out lies within resurrected units. You have no event from which you can detect resurrected units. You can however for Heroes but that isn't useful for detecting unit resurrection from an ability like Resurrection. The following is the code I used to do it. It's a combination of triggers and AI script code. It shows why the combined power of trigger code and AI script code makes the AI so much more flexible.
JASS:
function Trig_CheckPossession_Conditions takes nothing returns boolean
local integer ability_id = GetSpellAbilityId()
return ability_id == 'ANch' or ability_id == 'ACch' or ability_id == 'Apos' or ability_id == 'ACps'
endfunction
function Trig_CheckPossession_Actions takes nothing returns nothing
local unit caster_unit = GetSpellAbilityUnit()
local unit target_unit = GetSpellTargetUnit()
call DisplayTextToPlayer(udg_user, 0, 0, "Caster is: " + GetUnitName(caster_unit))
if target_unit != null then
call DisplayTextToPlayer(udg_user, 0, 0, "Target is: " + GetUnitName(target_unit))
set udg_last_caster_unit = caster_unit
set udg_last_target_unit = target_unit
else
call DisplayTextToPlayer(udg_user, 0, 0, "Invalid target!")
endif
set caster_unit = null
set target_unit = null
endfunction
//===========================================================================
function InitTrig_CheckPossession takes nothing returns nothing
set gg_trg_CheckPossession = CreateTrigger( )
call TriggerRegisterAnyUnitEventBJ( gg_trg_CheckPossession, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition( gg_trg_CheckPossession, Condition( function Trig_CheckPossession_Conditions ) )
call TriggerAddAction( gg_trg_CheckPossession, function Trig_CheckPossession_Actions )
endfunction
CheckPossession: This trigger I used to catch possession and charm casters, and the target. It still needs an unit changed owner event because there is no way to be sure the unit is actually possessed or charmed. The caster could die or select a unit which is already being charmed, possessed by another caster.
JASS:
function Trig_CheckOwnerState_Actions takes nothing returns nothing
local unit changing_unit = GetChangingUnit()
local integer unit_id = GetUnitTypeId(changing_unit)
local player prev_owner = GetChangingUnitPrevOwner()
local player new_owner = GetOwningPlayer(changing_unit)
if udg_last_target_unit != null then
if changing_unit == udg_last_target_unit then
//call RemoveGuardPosition(changing_unit)
//call RecycleGuardPosition(changing_unit)
//call DisplayTextToPlayer(udg_user, 0, 0, "Removed AI guard position from unit.")
call DisplayTextToPlayer(udg_user, 0, 0, "Possession succesfull.")
call DisplayTextToPlayer(udg_user, 0, 0, "Previous unit owner was " + GetPlayerName(prev_owner))
if prev_owner != udg_user then
call CommandAI(prev_owner, 9, unit_id)
endif
if new_owner != udg_user then
call CommandAI(new_owner, 8, unit_id)
endif
set udg_last_caster_unit = null
set udg_last_target_unit = null
endif
endif
set changing_unit = null
set prev_owner = null
set new_owner = null
endfunction
//===========================================================================
function InitTrig_CheckOwnerState takes nothing returns nothing
set gg_trg_CheckOwnerState = CreateTrigger( )
call TriggerRegisterAnyUnitEventBJ( gg_trg_CheckOwnerState, EVENT_PLAYER_UNIT_CHANGE_OWNER )
call TriggerAddAction( gg_trg_CheckOwnerState, function Trig_CheckOwnerState_Actions )
endfunction
CheckOwnerState: This trigger is used to actually check if the target unit was possessed successfully. This isn't a foolproof system, however. I don't know what happens if two target units get possessed by two different casters at the exact same time. But this happening is very unlikely. Even when this happens it won't mess up things. The worst scenario being that a possessed unit isn't tracked. However during tests I didn't encounter any problems. I've actually seen possessions occurring at the same time, however we can't distinguish events in micro seconds or less, so for a human it seems like it's occurring at the same moment.
It sends an AI command with the unit id of the unit that's possessed. The AI script stores that unit in a possessions array. I'll explain that later.
JASS:
function Trig_PossessionDies_Actions takes nothing returns nothing
local unit u = GetDyingUnit()
local player owner = GetOwningPlayer(u)
if u == udg_last_caster_unit then
call DisplayTextToPlayer(udg_user, 0, 0, "Caster unit died before finishing possession!")
set udg_last_caster_unit = null
set udg_last_target_unit = null
endif
if owner != udg_user then
call CommandAI(owner, 9, GetUnitTypeId(u))
set udg_dead_units[udg_index] = u
set udg_index = udg_index + 1
if udg_index > 256 then
set udg_index = 0
endif
endif
set u = null
set owner = null
endfunction
//===========================================================================
function InitTrig_PossessionDies takes nothing returns nothing
set gg_trg_PossessionDies = CreateTrigger( )
call TriggerRegisterAnyUnitEventBJ( gg_trg_PossessionDies, EVENT_PLAYER_UNIT_DEATH )
call TriggerAddAction( gg_trg_PossessionDies, function Trig_PossessionDies_Actions )
endfunction
PossessionDies: Here it checks if the caster unit died before completing the ability. If the caster dies the AI shouldn't be informed of a new unit. When a unit dies a command is sent to the AI player owner of the dying unit. This is needed so the AI script can check if the unit type is present in the possessions array. This won't track the exact unit object. This isn't necessary because there aren't any natives in common.ai that can send a specific unit to attack a player. You could send a unit object through though abusing the return bug and sending an integer pointer to the AI script. However this isn't very usefull in this case. The main purpose is to be sure the AI maintains the same amount of a unit type it has for defending the base. This can occur in Undead vs Undead scenarios.
This also keeps track of AI units dying. This system I used to track resurrected units. I keep an array of 256 unit handles to unit objects of units that died. The next trigger shows why.
JASS:
function Trig_CheckResurrections_Actions takes nothing returns nothing
local integer index = 0
local unit u
loop
exitwhen index == udg_index
set u = udg_dead_units[index]
if u != null and GetUnitState(u, UNIT_STATE_LIFE) > 0 then
if IsUnitType(u, UNIT_TYPE_HERO) then
call DisplayTextToPlayer(udg_user, 0, 0, "Hero is resurrected!")
call SetUnitState(u, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MAX_MANA))
else
call DisplayTextToPlayer(udg_user, 0, 0, "Unit is resurrected!")
call CommandAI(GetOwningPlayer(u), 10, GetUnitTypeId(u))
endif
set udg_dead_units[index] = null
endif
set index = index + 1
endloop
set u = null
endfunction
//===========================================================================
function InitTrig_CheckResurrections takes nothing returns nothing
set gg_trg_CheckResurrections = CreateTrigger( )
call TriggerRegisterTimerEvent(gg_trg_CheckResurrections, 0.50, true)
call TriggerAddAction( gg_trg_CheckResurrections, function Trig_CheckResurrections_Actions )
endfunction
CheckResurrections: This trigger checks if the units that died are resurrected. The hero part I used for debugging purposes. I used it to fill mana so the hero can use his resurrect ability. If a unit is resurrected it normally tries to go back to his main base and stay there as a defender. With this periodic timer you can now check for resurrected units and command the AI to do something with those units.
Code:
function SuicideUnitOnBattlefield takes integer unit_id returns nothing
if CaptainInCombat(true) and not CaptainIsHome() then
call DisplayTextToPlayer(dbg, 0, 0, "Sending unit to the battlefield.")
call SuicideUnitEx(1, unit_id, pid)
else
call DisplayTextToPlayer(dbg, 0, 0, "Captain isn't in combat on the battlefield.")
endif
endfunction
SuicideUnitOnBattlefield is a function from my AI script. Whenever the AI received a command that a unit was possessed, charmed or resurrected it calls this function. This is where you get the difference between suicide loops and triggered suicides. When you use suicide loops the AI tries to suicide more than one unit of the unit type. What's nice about this, is that it will check the nearest unused unit near the combat zone to send out on an attack. It won't pull units from his base, in case of Undead vs Undead where possessions etc. can be of the same unit type of the race the AI plays. That's why I've included a check for attack captain in combat and if the captain is currently in his main base. If he's in his main base and in combat then don't suicide. In that case you include the possessed etc. in the next attack wave, and that's the reason why you keep track of the possessed etc. units in the possessions array.
Code:
function SetPossessedUnit takes boolean add_unit, integer unit_id returns nothing
local integer index = 0
loop
exitwhen possessions[index] == -1 or possessions[index] == unit_id
set index = index + 2
endloop
if add_unit then
if possessions[index] == -1 then
set possessions[index] = unit_id
set possessions[index + 1] = 1
set possessions[index + 2] = -1
call DisplayTextToPlayer(dbg, 0, 0, "Added new possessed unit type.")
else
set possessions[index + 1] = possessions[index + 1] + 1
call DisplayTextToPlayer(dbg, 0, 0, "Updated existing possessed unit type count.")
endif
call SuicideUnitOnBattlefield(unit_id)
elseif possessions[index] == unit_id then
if possessions[index + 1] > 0 then
set possessions[index + 1] = possessions[index + 1] - 1
call DisplayTextToPlayer(dbg, 0, 0, "Removed one possessed unit.")
endif
endif
endfunction
SetPossessedUnit is another function the AI script uses. It stores the possessed and charmed units. It won't store resurrected units, because they're from the AI anyways. I don't find that necessary.
Code:
function FormAssaultGroup takes real seconds, boolean check_hp, integer hp_perc returns boolean
local integer index
local integer count
local integer unit_id
local boolean captain_ready
if wave_prep_time < seconds or seconds <= 0 then
call DisplayTextToPlayer(dbg, 0, 0, "Group forming wait time is too high!")
set seconds = wave_prep_time
endif
if not check_hp then
set hp_perc = 0
endif
loop
set index = 0
set wave_prep_time = wave_prep_time - seconds
call Wait(seconds)
call InitAssault()
loop
exitwhen index == attack_count
set unit_id = attack_units[index]
set count = attack_units[index + 1]
call AddAssault(count, unit_id)
set index = index + 2
endloop
set index = 0
loop
exitwhen not do_possessions
exitwhen possessions[index] == -1
set unit_id = possessions[index]
set count = TownCountDone(unit_id)
if count > 0 and possessions[index + 1] > 0 then
if count > possessions[index + 1] then
set count = possessions[index + 1]
endif
call AddAssault(count, unit_id)
endif
set index = index + 2
endloop
call DisplayTextToPlayer(dbg, 0, 0, "Captain ready check...")
set captain_ready = CaptainIsFull() and CaptainReadiness() >= hp_perc
exitwhen captain_ready or wave_prep_time <= 0
call DisplayTextToPlayer(dbg, 0, 0, "Captain in combat check, or form group timeout...")
exitwhen abort
endloop
return captain_ready
endfunction
FormAssaultGroup is the custom function I use to create attack groups. It also includes the possessions that are stored in the possessions array that're not sent into combat yet. This happens for example when possessions occur when the attack captain isn't in combat on the battlefield. Most of the time this is caused by the defense captain combating attackers.
As for the GroupTimedLife, I never use it, because rarely, or sometimes, the AI ends up screwing around with their attack waves, and their attack timings rarely get messed up.
Well it's actually quite useful to set GroupTimedLife during the setup of your AI script. It ensures that the AI always includes timed units, like summons and parasites and such in attack waves. If you don't set this the AI doesn't make proper use of those units. For example if you have an attack wave with Meat wagons and Necromancers. The Necromancers raise skeletons from the corpses in the Meat wagons. This happens when the attack party is in combat. However when those skeletons are raised too far from the combat area they do nothing. If you set GroupTimedLife they will be included in the attack party and will automatically move into the combat zone. The same goes for summons from other races.
I just had to come up with something for this lol..
I don't know why, I just can't stand it when faced with a problem and a solution isn't available yet.