Name | Type | is_array | initial_value |
ID | unitcode | No | |
TIME | real | No |
//TESH.scrollpos=49
//TESH.alwaysfold=0
//------------------------------------------------------------------------------------------------------//
// ZTS - ZWIEBELCHEN'S THREAT SYSTEM v. 2.6 //
//------------------------------------------------------------------------------------------------------//
//------------------------------------------------------------------------------------------------------//
// //
// Requires: vJass Pre-Processor //
// //
// Special thanks to TEC-Ghost, who inspired me on creating this system. //
//------------------------------------------------------------------------------------------------------//
// MANUAL //
//------------------------------------------------------------------------------------------------------//
// 1. How to Install:
//
// - Create a trigger called "ZTS"
// - Convert it to custom Text
// - Replace everything inside with this code
//
//
// 2. How to set it up:
//
// 2.1 Constants
//
// There are a bunch of global constants below this manual, you can edit to your liking.
// I commented everything you need to know about those constants right beside them. If you need additional information,
// please tell me, so I can improve this manual. However, I think most of it should be pretty clear.
//
// 2.2. Gameplay Constants
//
// It is recommended to edit certain Gameplay Constants entries, to use the full potential of
// the system.
// The most important entries are: (with selected "Show Raw-Data")
// CallForHelp --> Set this to 0, if possible; the system will manage this - it isn't a problem if you don't do this, though
// You don't have to do it if your threat-system controlled units are neutral hostile
// CreepCallForHelp --> Set this to 0, if possible; the system will manage this - it isn't a problem if you don't do this, though
// GuardDistance --> Set this to something higher than ReturnRange (see below)
// MaxGuardDistance --> Set this to something higher than ReturnRange (see below)
// GuardReturnTime --> Set this to something very high, so that the standard AI doesn't interfere (i.e. 60 seconds)
//
// 2.3. Damage Detection
//
// Of course, a threat system is pretty useless without a trigger, that adds threat depending on
// the damage dealt by a player unit.
// I recommend using a damage detection script like IDDS:
// (http://www.wc3c.net/showthread.php?t=100618)
// Check up the demo map for a very simple (but leaking) damage detection trigger
// The only function you actually need then is:
// call ModifyThreat(GetEventDamageSource(), GetTriggerUnit(), GetEventDamage(), true)
// The function does all required checks on its own. There is no need to run something else.
//
// 3. How to use it:
//
// 3.1. Core functions
//
// call ZTS_AddThreatUnit(unit npc, boolean includeCombatCamps) returns nothing:
//
// This function registers the unit as an AI-controlled unit.
// ThreatUnits will automaticly attack the highest-in-threat attacker.
// When adding a ThreatUnit, its current position gets saved and be considered camp-position.
// It will always return to this position if pulled to far or on victory.
// Nearby units will be considered in the same camp group. Camp members will always retreat and attack together.
// If includeCombatCamps is true, the unit will be added to already fighting camps. If it is false, the unit will
// create its own camp group, if it can't find any non-fighting units nearby.
// This should be false in most cases, but it can be useful when you have bosses that summon units infight, so that
// the summons will be added to the bossfight correctly instead of getting their own seperate group.
//
// call ZTS_AddPlayerUnit(unit pu) returns nothing:
//
// Units add by this way will generate threat on ThreatUnits.
// If the unit is not registered as a PlayerUnit, it will not be attacked by ThreatUnits.
//
// call ZTS_RemoveThreatUnit(unit npc) returns nothing:
//
// Removes a ThreatUnit from the system. The unit will no longer be controlled by the threat system.
// Also, the threat list for that unit will be cleared.
// Dead or removed units will automaticly be cleared. You need to add them again after revival/recreation.
//
// call ZTS_RemovePlayerUnit(unit pu) returns nothing:
//
// Removes a player unit from the system. The unit will no longer generate threat on ThreatUnits.
// The unit will also be instantly removed from all threat lists.
// If the unit was the last unit in combat with the same hostile camp, all units
// of that camp group will immediately return to their camp positions.
// You can use this, followed by AddPlayerUnit to that unit out of combat and reset all threat.
// Dead or removed units will automaticly be cleared. You need to add them again after revival/recreation.
//
// call ZTS_ModifyThreat(unit pu, unit npc, real amount, boolean add) returns nothing:
//
// Adds, sets or substracts threat from npc's threat list caused by pu.
// Set 'add' to true to add or substract amount from the current value.
// Set 'add' to false to set the new threat value to amount.
// To reduce threat, use negative amount values with add == true.
// Remember: If a unit has 0 threat, it is still considered in-combat -
// this also means, that adding "0" to the units threat causes them to attack!
//
// call ZTS_ApplyHealThreat(unit pu, unit ally, real amount, boolean add, boolean divide) returns nothing:
//
// Adds Healing Threat to all units, that have ally on threat-list
// This can be abused to apply global threat to a unit by passing the same unit to p and ally.
// Parameter divide = true means that the amount is split by the number of units attacking the target;
// for example if 3 units are currently attacking the targeted ally, it adds amount/3 threat from pu to all of them.
// Parameter divide = false means that every attacking unit gets 'amount' of threat applied.
// use add = false to set the amount of threat to 'amount', instead of increasing/decreasing it
// negative values are allowed in combination with 'add' to reduce threat.
// You can also use this with add = false and amount = 0 with pu = ally to set total threat generated back to zero for this unit.
//
//
// 3.2. Getter functions
//
// call ZTS_GetCombatState(unit U) returns boolean:
//
// Returns the combat state of a player or npc unit.
// Returns true, if the unit is registered and in combat.
// Returns false, if the unit is not registered or out of combat.
//
// call ZTS_GetCombatTime(unit NPC) returns boolean:
//
// Returns the incombat time of the npc.
// Does not work for player units.
// Returns "0" if the unit is not in combat or currently returning to camp position.
//
// call ZTS_GetThreatUnitPosition(unit NPC, unit PU) returns integer:
//
// Returns the position of unit PU in unit NPC's threat list
// Returns "0" if the unit was not found, NPC does not feature a threat list or in case of invalid input data
//
// call ZTS_GetThreatUnitAmount(unit NPC, unit PU) returns real:
//
// Returns the amount of threat unit PU has in unit NPC's threat list
// Returns "0" if the unit was not found, NPC does not feature a threat list or in case of invalid input data
//
// call ZTS_GetThreatSlotUnit(unit NPC, integer position) returns unit:
//
// Returns the unit in threat-slot position
// Returns null if the NPC does not feature a threat list, the number is too large
// or in case of invalid input data
//
// call ZTS_GetThreatSlotAmount(unit NPC, integer position) returns real:
//
// Returns the threat amount of the threat-slot position
// Returns "0" if the NPC does not feature a threat list, the number is too large
// or in case of invalid input data
//
// call ZTS_GetAttackers(unit U) returns group:
//
// If used on a ThreatUnit, this returns a group of all units in threat list;
// if used on a PlayerUnit, this returns a group of all units aggroed.
// Returns an empty group, in case of invalid input data or empty lists.
//
//
// 3.3. Advanced User features
//
// call ZTS_IsEvent() returns boolean
//
// When using "A unit is issued an order without target" or "A unit is issued a target order" events,
// this function returns true when the order was issued by the threat system.
// You can use this to setup your own spell-AI for units.
// Let's say you want the unit to cast Summon Water Elemental whenever the cooldown is ready:
// Just use the mentioned events and add:
// Custom script: if not ZTS_IsEvent() then
// Custom script: return
// Custom script: endif
// at the beginning of you trigger's actions and you're done.
// You can now issue the order to the triggering unit:
// Unit - Order (Triggering unit) to Human Archmage - Summon Water Elemental
// In combination with some of the Getter functions, you can trigger nice spell AI like this.
// NOTE: ZTS_IsEvent will only return true once(!) for every fired event, so if you need it again inside that trigger,
// make sure to save it to a variable.
//
//------------------------------------------------------------------------------------------------------//
library ZTS initializer InitThreatSystem
globals
private constant real UpdateIntervall = 0.5 //The intervall for issueing orders and performing AttackRange check. recommended value: 0.5
private constant real HelpRange = 400 //The range between units considered being in the same camp. If a unit of the same camp gets attacked, all others will help.
//Set CallForHelp to something lower in Gameplay Constants.
private constant real OrderReturnRange = 4000 //The range the unit's target can be away from the original camping position, before being ordered to return.
private constant real ReturnRange = 1500 //The range the unit can move away from the original camping position, before being ordered to return.
private constant real TimeToPort = 10 //This timer expires once a unit tries to return to its camping position.
//If it reaches 0 before reaching the camp position, the unit will be teleported immediately.
private constant boolean HealUnitsOnReturn = true //If this is true, returning units will be healed to 100% health.
// Do not edit below here!
//------------------------------------------------------------------------------------------------------//
private boolexpr BOOLEXPR = null
private constant timer Updater = CreateTimer()
private constant group NPCgroup = CreateGroup()
private constant hashtable NPClist = InitHashtable()
private constant hashtable PUlist = InitHashtable()
//temporary variables for enumerations and forgroups
private unit TSub = null
private unit TMod = null
private constant group TGroupSub = CreateGroup()
private unit THealer = null
private real THealthreat = 0
private boolean TBool = false
private integer TState = 0
private constant group TGroupUpd = CreateGroup()
private group TGroupGet = null
private boolean EventBool = false
endglobals
public function IsEvent takes nothing returns boolean
if EventBool then
set EventBool = false
return true
endif
return false
endfunction
private function Pos2Key takes integer position returns integer //converts threat list position into hashtable childkey
return 8+(position*2)
endfunction
private function Key2Pos takes integer key returns integer //converts hashtable childkey into threat list position
return (key-8)/2
endfunction
public function GetCombatState takes unit u returns boolean
if GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
return false
elseif HaveSavedInteger(NPClist, GetHandleId(u), 0) then //unit is npc
return LoadInteger(NPClist, GetHandleId(u), 0) > 0
elseif HaveSavedHandle(PUlist, GetHandleId(u), 0) then //unit is player unit
return LoadInteger(PUlist, GetHandleId(u), 1) > 0
endif
return false
endfunction
public function GetCombatTime takes unit u returns real
if GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
return 0.
elseif HaveSavedInteger(NPClist, GetHandleId(u), 0) then //unit is npc
if LoadInteger(NPClist, GetHandleId(u), 0) == 1 then //only return a time when the unit is in combat
return LoadReal(NPClist, GetHandleId(u), 3)
endif
endif
return 0.
endfunction
public function GetThreatUnitPosition takes unit npc, unit pu returns integer
if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or GetUnitTypeId(pu) == 0 or IsUnitType(pu, UNIT_TYPE_DEAD) then //units dead or null
return 0
elseif not (HaveSavedInteger(NPClist, GetHandleId(npc), 0) and HaveSavedHandle(PUlist, GetHandleId(pu), 0)) then //units not added
return 0
elseif HaveSavedInteger(PUlist, GetHandleId(pu), GetHandleId(npc)) then
return LoadInteger(PUlist, GetHandleId(pu), GetHandleId(npc))
endif
return 0
endfunction
public function GetThreatUnitAmount takes unit npc, unit pu returns real
if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or GetUnitTypeId(pu) == 0 or IsUnitType(pu, UNIT_TYPE_DEAD) then //units dead or null
return 0.
elseif not (HaveSavedInteger(NPClist, GetHandleId(npc), 0) and HaveSavedHandle(PUlist, GetHandleId(pu), 0)) then //units not added
return 0.
elseif HaveSavedInteger(PUlist, GetHandleId(pu), GetHandleId(npc)) then
return LoadReal(NPClist, GetHandleId(npc), Pos2Key(LoadInteger(PUlist, GetHandleId(pu), GetHandleId(npc)))+1)
endif
return 0.
endfunction
public function GetThreatSlotUnit takes unit npc, integer position returns unit
if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or position <= 0 then //unit dead or null or invalid slot
return null
elseif not HaveSavedInteger(NPClist, GetHandleId(npc), 0) then //unit not added
return null
elseif HaveSavedHandle(NPClist, GetHandleId(npc), Pos2Key(position)) then
return LoadUnitHandle(NPClist, GetHandleId(npc), Pos2Key(position))
endif
return null
endfunction
public function GetThreatSlotAmount takes unit npc, integer position returns real
if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or position <= 0 then //unit dead or null or invalid slot
return 0.
elseif not HaveSavedInteger(NPClist, GetHandleId(npc), 0) then //unit not added
return 0.
elseif HaveSavedReal(NPClist, GetHandleId(npc), Pos2Key(position)+1) then
return LoadReal(NPClist, GetHandleId(npc), Pos2Key(position)+1)
endif
return 0.
endfunction
private function GetAttackersSub takes nothing returns nothing
call GroupAddUnit(TGroupGet, GetEnumUnit())
endfunction
public function GetAttackers takes unit u returns group
local group g = CreateGroup()
local integer key = 10
local integer max
if GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
return g
endif
if HaveSavedInteger(NPClist, GetHandleId(u), 0) then //unit is npc
set max = Pos2Key(LoadInteger(NPClist, GetHandleId(u), 5))
loop
exitwhen key > max
call GroupAddUnit(g, LoadUnitHandle(NPClist, GetHandleId(u), key))
set key = key+2
endloop
elseif HaveSavedHandle(PUlist, GetHandleId(u), 0) then //unit is player unit
set TGroupGet = g
call ForGroup(LoadGroupHandle(PUlist, GetHandleId(u), 0), function GetAttackersSub)
set g = TGroupGet
set TGroupGet = null
endif
return g
endfunction
private function Swap takes integer npcID, integer key1, integer key2 returns nothing
local unit u = LoadUnitHandle(NPClist, npcID, key1)
local real r = LoadReal(NPClist, npcID, key1+1)
call SaveUnitHandle(NPClist, npcID, key1, LoadUnitHandle(NPClist, npcID, key2))
call SaveReal(NPClist, npcID, key1+1, LoadReal(NPClist, npcID, key2+1))
call SaveInteger(PUlist, GetHandleId(LoadUnitHandle(NPClist, npcID, key1)), npcID, Key2Pos(key1)) //update position list
call SaveUnitHandle(NPClist, npcID, key2, u)
call SaveReal(NPClist, npcID, key2+1, r)
call SaveInteger(PUlist, GetHandleId(u), npcID, Key2Pos(key2)) //update position list
set u = null
endfunction
private function CampThreat takes nothing returns nothing
local integer npcID = GetHandleId(GetEnumUnit())
local integer puID = GetHandleId(TMod)
local integer key
local integer listlength
if GetEnumUnit() == TSub then
return
elseif HaveSavedInteger(PUlist, puID, npcID) then //original pu unit already listed in EnumUnit's threat list
return
elseif LoadInteger(NPClist, npcID, 0) > 1 or IsUnitType(GetEnumUnit(), UNIT_TYPE_DEAD) then //do not add threat to dead or units that are status: returning
return
endif
set listlength = LoadInteger(NPClist, npcID, 5)+1
call SaveInteger(NPClist, npcID, 5, listlength) //add to list length of EnumUnit
set key = Pos2Key(listlength)
call SaveUnitHandle(NPClist, npcID, key, TMod) //add original pu unit to end of EnumUnit's threat list
call SaveReal(NPClist, npcID, key+1, 0)
call SaveInteger(PUlist, puID, npcID, listlength) //add EnumUnit to slot list
call GroupAddUnit(LoadGroupHandle(PUlist, puID, 0), GetEnumUnit()) //add EnumUnit to slot list group
call SaveInteger(PUlist, puID, 1, LoadInteger(PUlist, puID, 1)+1) //increase group size count
if LoadInteger(NPClist, npcID, 0) == 0 then
call SaveInteger(NPClist, npcID, 0, 1) //set unit status: combat
call GroupAddUnit(NPCgroup, GetEnumUnit()) //add the unit to incombat group
endif
endfunction
public function ModifyThreat takes unit pu, unit npc, real amount, boolean add returns nothing
local integer npcID = GetHandleId(npc)
local integer puID = GetHandleId(pu)
local integer key
local integer listlength
local integer i = 0
local real newamount
local real oldamount = 0
local boolean b = false
if not (HaveSavedInteger(NPClist, npcID, 0) and HaveSavedHandle(PUlist, puID, 0)) then //units not added
return
elseif IsUnitType(pu, UNIT_TYPE_DEAD) or IsUnitType(npc, UNIT_TYPE_DEAD) then //units dead
return
elseif GetUnitTypeId(pu) == 0 or GetUnitTypeId(npc) == 0 then //null units
return
elseif LoadInteger(NPClist, npcID, 0) > 1 then //do not add threat to units that are status: returning
return
endif
if not HaveSavedInteger(PUlist, puID, npcID) then //pu not listed in npc's threat list
set listlength = LoadInteger(NPClist, npcID, 5)+1
call SaveInteger(NPClist, npcID, 5, listlength) //add to list length of npc
set key = Pos2Key(listlength)
call SaveUnitHandle(NPClist, npcID, key, pu) //add pu to end of npc's threat list
call SaveInteger(PUlist, puID, npcID, listlength) //add npc to slot list
call GroupAddUnit(LoadGroupHandle(PUlist, puID, 0), npc) //add npc to slot list group
call SaveInteger(PUlist, puID, 1, LoadInteger(PUlist, puID, 1)+1) //increase group size count
if LoadInteger(NPClist, npcID, 0) == 0 then
call SaveInteger(NPClist, npcID, 0, 1) //set unit status: combat
call GroupAddUnit(NPCgroup, npc) //add the unit to incombat group
endif
set b = true
else
set key = Pos2Key(LoadInteger(PUlist, puID, npcID))
set oldamount = LoadReal(NPClist, npcID, key+1)
endif
if add then
set newamount = oldamount+amount
else
set newamount = amount
endif
if newamount < 0 then
set newamount = 0
endif
call SaveReal(NPClist, npcID, key+1, newamount)
if newamount > oldamount then //check lower keys
loop
if HaveSavedReal(NPClist, npcID, key-1-i) then
if LoadReal(NPClist, npcID, key-1-i) < newamount then //lower key amount is smaller
call Swap(npcID, key-2-i, key-i)
else
exitwhen true
endif
set i = i + 2
else
exitwhen true
endif
endloop
elseif newamount < oldamount then //check higher keys
loop
if HaveSavedReal(NPClist, npcID, key+3+i) then
if LoadReal(NPClist, npcID, key+3+i) > newamount then //upper key amount is larger
call Swap(npcID, key+2+i, key+i)
else
exitwhen true
endif
set i = i + 2
else
exitwhen true
endif
endloop
endif
if b then //set all units of the same camp to status: combat and apply 0 threat from pu to them
set TSub = npc
set TMod = pu
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampThreat)
endif
endfunction
public function AddPlayerUnit takes unit u returns nothing
local integer ID = GetHandleId(u)
if HaveSavedInteger(NPClist, ID, 0) or HaveSavedHandle(PUlist, ID, 0) then //unit already added
return
elseif GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
return
endif
call SaveGroupHandle(PUlist, ID, 0, CreateGroup()) //slot list group
call SaveInteger(PUlist, ID, 1, 0) //list group count
endfunction
private function AcquireTarget takes nothing returns boolean
local unit npc = GetTriggerUnit()
local unit pu
if GetEventTargetUnit() != null then
set pu = GetEventTargetUnit()
else
set pu = GetOrderTargetUnit()
endif
if IsUnitEnemy(pu, GetOwningPlayer(npc)) then
if LoadInteger(NPClist, GetHandleId(npc), 0) == 0 then //pull out of combat units only
call ModifyThreat(pu, npc, 0, true)
endif
endif
set pu = null
set npc = null
return false
endfunction
private function FilterUnitsWithCampGroup takes nothing returns boolean
return HaveSavedHandle(NPClist, GetHandleId(GetFilterUnit()), 4) and IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD) == false and LoadInteger(NPClist, GetHandleId(GetFilterUnit()), 0) <= TState
endfunction
public function AddThreatUnit takes unit u, boolean includeCombatCamps returns nothing
local integer ID = GetHandleId(u)
local group g = null
local trigger t = null
local unit other = null
local integer otherID = 0
local unit temp = null
local integer i = 0
local integer listlength = 0
if HaveSavedInteger(NPClist, ID, 0) or HaveSavedHandle(PUlist, ID, 0) then //unit already added
return
elseif GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
return
endif
call SaveInteger(NPClist, ID, 0, 0) //status
call SaveReal(NPClist, ID, 1, GetUnitX(u)) //return X
call SaveReal(NPClist, ID, 2, GetUnitY(u)) //return Y
call SaveReal(NPClist, ID, 3, 0) //return countdown and incombat timer
call SaveInteger(NPClist, ID, 5, 0) //list length
set t = CreateTrigger()
call TriggerRegisterUnitEvent(t, u, EVENT_UNIT_ISSUED_TARGET_ORDER)
call TriggerRegisterUnitEvent(t, u, EVENT_UNIT_ACQUIRED_TARGET)
call TriggerAddCondition(t, Condition(function AcquireTarget))
call SaveTriggerHandle(NPClist, ID, 6, t) //acquire target event trigger
if includeCombatCamps then
set TState = 1
else
set TState = 0
endif
call GroupEnumUnitsInRange(TGroupSub, GetUnitX(u), GetUnitY(u), HelpRange, Condition(function FilterUnitsWithCampGroup))
set other = FirstOfGroup(TGroupSub)
if other != null then
set otherID = GetHandleId(other)
set g = LoadGroupHandle(NPClist, otherID, 4)
if includeCombatCamps then
//don't forget to inherit the camp unit's threat list...
if LoadInteger(NPClist, otherID, 0) == 1 then //...but only if filtered unit is actually infight
set listlength = LoadInteger(NPClist, otherID, 5)
call SaveInteger(NPClist, ID, 5, listlength) //copy list length
loop //copy all list entries as the newly added unit has an empty list and will cause the camp to reset almost instantly
set i = i + 1
exitwhen i > listlength
set temp = LoadUnitHandle(NPClist, otherID, Pos2Key(i))
call SaveUnitHandle(NPClist, ID, Pos2Key(i), temp)
call SaveReal(NPClist, ID, Pos2Key(i)+1, 0)
call SaveInteger(PUlist, GetHandleId(temp), ID, i) //assign the threat position to the player unit's reference list
call GroupAddUnit(LoadGroupHandle(PUlist, GetHandleId(temp), 0), u) //add the unit to the player unit's threat group
call SaveInteger(PUlist, GetHandleId(temp), 1, LoadInteger(PUlist, GetHandleId(temp), 1)+1) //increase group size count
endloop
call SaveInteger(NPClist, ID, 0, 1) //set unit status: combat
call GroupAddUnit(NPCgroup, u) //add the unit to incombat group
set temp = null
endif
endif
else //no unit in range has a camp group assigned, so create a new one
set g = CreateGroup()
endif
call GroupAddUnit(g, u)
call SaveGroupHandle(NPClist, ID, 4, g) //camp group
set t = null
set g = null
set other = null
endfunction
public function RemoveThreatUnit takes unit u returns nothing
local integer ID = GetHandleId(u)
local integer OtherID
local group g = null
local integer key = 10
if not HaveSavedInteger(NPClist, ID, 0) then //unit not added
return
elseif GetUnitTypeId(u) == 0 then
return
endif
if LoadInteger(NPClist, ID, 0) > 1 then //unit status is: returning
call IssueImmediateOrder(u, "stop")
call SetUnitInvulnerable(u, false)
if IsUnitPaused(u) then
call PauseUnit(u, false)
endif
endif
loop //remove the entry in the player unit's position list and list group and decrease list group count
if HaveSavedHandle(NPClist, ID, key) then
set OtherID = GetHandleId(LoadUnitHandle(NPClist, ID, key))
call RemoveSavedInteger(PUlist, OtherID, ID)
call GroupRemoveUnit(LoadGroupHandle(PUlist, OtherID, 0), u)
call SaveInteger(PUlist, OtherID, 1, LoadInteger(PUlist, OtherID, 1)-1)
set key = key+2
else //last entry reached
exitwhen true
endif
endloop
set g = LoadGroupHandle(NPClist, ID, 4)
call GroupRemoveUnit(g, u)
if FirstOfGroup(g) == null then //camp group is empty
call DestroyGroup(g)
endif
call DestroyTrigger(LoadTriggerHandle(NPClist, ID, 6))
call FlushChildHashtable(NPClist, ID)
if IsUnitInGroup(u, NPCgroup) then
call GroupRemoveUnit(NPCgroup, u) //remove unit from incombat group
endif
set g = null
endfunction
private function RemovePlayerUnitEntries takes nothing returns nothing
local integer ID = GetHandleId(TSub)
local integer OtherID = GetHandleId(GetEnumUnit())
local integer key = Pos2Key(LoadInteger(PUlist, ID, OtherID))
loop //remove the entry in u's threat list and fill the gap
if HaveSavedHandle(NPClist, OtherID, key+2) then //move up next entry
call SaveUnitHandle(NPClist, OtherID, key, LoadUnitHandle(NPClist, OtherID, key+2))
call SaveReal(NPClist, OtherID, key+1, LoadReal(NPClist, OtherID, key+3))
call SaveInteger(PUlist, GetHandleId(LoadUnitHandle(NPClist, OtherID, key)), OtherID, Key2Pos(key)) //update position in player unit list
set key = key+2
else //last entry reached
call RemoveSavedHandle(NPClist, OtherID, key)
call RemoveSavedReal(NPClist, OtherID, key+1)
call SaveInteger(NPClist, OtherID, 5, Key2Pos(key-2)) //decrease list length
exitwhen true
endif
endloop
endfunction
public function RemovePlayerUnit takes unit u returns nothing
local integer ID = GetHandleId(u)
if not HaveSavedHandle(PUlist, ID, 0) then //unit not added
return
elseif GetUnitTypeId(u) == 0 then
return
endif
set TSub = u
call ForGroup(LoadGroupHandle(PUlist, ID, 0), function RemovePlayerUnitEntries)
call DestroyGroup(LoadGroupHandle(PUlist, ID, 0))
call FlushChildHashtable(PUlist, ID)
endfunction
private function HealThreatSub takes nothing returns nothing
call ModifyThreat(THealer, GetEnumUnit(), THealthreat, TBool)
endfunction
public function ApplyHealThreat takes unit pu, unit ally, real amount, boolean add, boolean divide returns nothing
local integer puID = GetHandleId(pu)
local integer allyID = GetHandleId(ally)
if not (HaveSavedHandle(PUlist, puID, 0) and HaveSavedHandle(PUlist, allyID, 0)) then //units not added
return
elseif IsUnitType(pu, UNIT_TYPE_DEAD) or IsUnitType(ally, UNIT_TYPE_DEAD) then //units dead
return
elseif GetUnitTypeId(pu) == 0 or GetUnitTypeId(ally) == 0 then //null units
return
endif
if divide and LoadInteger(PUlist, allyID, 1) > 1 then
set THealthreat = amount/LoadInteger(PUlist, allyID, 1)
else
set THealthreat = amount
endif
set TBool = add
set THealer = pu
call ForGroup(LoadGroupHandle(PUlist, allyID, 0), function HealThreatSub)
endfunction
private function CampCommand takes nothing returns nothing
local unit u = GetEnumUnit()
local integer ID = GetHandleId(u)
local integer OtherID
local integer status = LoadInteger(NPClist, ID, 0)
local integer key = 10
if status == 1 then
call SaveInteger(NPClist, GetHandleId(u), 0, 2) //set status: returning
loop //remove the entry in the player unit's position list and list group and decrease list group count
if HaveSavedHandle(NPClist, ID, key) then
set OtherID = GetHandleId(LoadUnitHandle(NPClist, ID, key))
call RemoveSavedInteger(PUlist, OtherID, ID)
call GroupRemoveUnit(LoadGroupHandle(PUlist, OtherID, 0), u)
call SaveInteger(PUlist, OtherID, 1, LoadInteger(PUlist, OtherID, 1)-1)
call RemoveSavedHandle(NPClist, ID, key)
call RemoveSavedReal(NPClist, ID, key+1)
set key = key+2
else //last entry reached
exitwhen true
endif
endloop
call SaveInteger(NPClist, ID, 5, 0) //also set list length to zero
call IssueImmediateOrder(u, "stop") //cancels even spellcast with casting time
call IssuePointOrder(u, "move", LoadReal(NPClist, ID, 1), LoadReal(NPClist, ID, 2))
call SaveReal(NPClist, ID, 3, TimeToPort)
call SetUnitInvulnerable(u, true)
if HealUnitsOnReturn then
call SetUnitState(u, UNIT_STATE_LIFE, GetUnitState(u, UNIT_STATE_MAX_LIFE))
call SetUnitState(u, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MAX_MANA))
endif
elseif status == 3 then
call SaveInteger(NPClist, GetHandleId(u), 0, 0) //set status: out of combat
call SaveReal(NPClist, GetHandleId(u), 3, 0.) //reset incombat and return timer
call SetUnitInvulnerable(u, false)
call PauseUnit(u, false)
if HealUnitsOnReturn then
call SetUnitState(u, UNIT_STATE_LIFE, GetUnitState(u, UNIT_STATE_MAX_LIFE))
call SetUnitState(u, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MAX_MANA))
endif
call GroupRemoveUnit(NPCgroup, u) //remove from combat group
endif
set u = null
endfunction
private function CampStatus takes nothing returns nothing
if LoadInteger(NPClist, GetHandleId(GetEnumUnit()), 0) != 3 then
set TBool = false
endif
endfunction
private function IssueOrder takes nothing returns nothing
local unit npc = GetEnumUnit()
local integer npcID = GetHandleId(npc)
local integer status = LoadInteger(NPClist, npcID, 0)
local integer i = 0
local boolean b = true
local unit target = null
if status == 1 then //unit in combat
call SaveReal(NPClist, npcID, 3, LoadReal(NPClist, npcID, 3) + UpdateIntervall) //increase the combat timer
if IsUnitInRangeXY(npc, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2), ReturnRange) and HaveSavedHandle(NPClist, npcID, 10) then
set target = LoadUnitHandle(NPClist, npcID, 10)
if IsUnitInRangeXY(target, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2), OrderReturnRange) then
if GetUnitCurrentOrder(npc) == 851983 or GetUnitCurrentOrder(npc) == 0 or GetUnitCurrentOrder(npc) == 851971 then //attack order or no order or smart order
set EventBool = true
call IssueTargetOrder(npc, "smart", target)
set EventBool = false
endif
else //target of unit to far away from camp position
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp returning
endif
else //unit left return range or killed all player units
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp returning
endif
elseif status == 2 then //unit is returning
if LoadReal(NPClist, npcID, 3) > 0 then
if not IsUnitInRangeXY(npc, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2), 35) then
call IssuePointOrder(npc, "move", LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2))
call SaveReal(NPClist, npcID, 3, LoadReal(NPClist, npcID, 3) - UpdateIntervall)
call SetUnitInvulnerable(npc, true)
else //unit within close range to camp position
if GetUnitCurrentOrder(npc) == 851986 then //move order
call SaveReal(NPClist, npcID, 3, LoadReal(NPClist, npcID, 3) - UpdateIntervall)
call SetUnitInvulnerable(npc, true)
else //Something blocks the exact spot or the unit has arrived
call SaveInteger(NPClist, npcID, 0, 3) //set status: returned
set TBool = true
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampStatus)
if TBool then //all units in camp have status: returned to camp position
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp ooc
else
call PauseUnit(npc, true) //make sure it doesn't move or attack when invulnerable
endif
endif
endif
else //counter expired - perform instant teleport
call SetUnitPosition(npc, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2))
call SaveInteger(NPClist, npcID, 0, 3) //set status: returned
set TBool = true
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampStatus)
if TBool then //all units in camp have status: returned to camp position
call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp ooc
else
call PauseUnit(npc, true) //make sure it doesn't move or attack when invulnerable
endif
endif
endif
set npc = null
set target = null
endfunction
private function Update takes nothing returns nothing
call ForGroup(NPCgroup, function IssueOrder) //issues orders to all units in combat
endfunction
private function rettrue takes nothing returns boolean
return true
endfunction
private function RemovedUnitFound takes unit u returns nothing
if HaveSavedInteger(NPClist, GetHandleId(u), 0) then
call RemoveThreatUnit(u)
endif
if HaveSavedHandle(PUlist, GetHandleId(u), 0) then
call RemovePlayerUnit(u)
endif
endfunction
hook RemoveUnit RemovedUnitFound
private function OnDeath takes nothing returns nothing
if HaveSavedInteger(NPClist, GetHandleId(GetTriggerUnit()), 0) then
call RemoveThreatUnit(GetTriggerUnit())
endif
if HaveSavedHandle(PUlist, GetHandleId(GetTriggerUnit()), 0) then
call RemovePlayerUnit(GetTriggerUnit())
endif
endfunction
private function InitThreatSystem takes nothing returns nothing
local trigger t = CreateTrigger()
local integer index = 0
set BOOLEXPR=Condition(function rettrue) //prevent booleanexpressions from leaking
call TimerStart(Updater, UpdateIntervall, true, function Update)
loop
call TriggerRegisterPlayerUnitEvent(t, Player(index), EVENT_PLAYER_UNIT_DEATH, BOOLEXPR)
set index = index + 1
exitwhen index == bj_MAX_PLAYER_SLOTS
endloop
call TriggerAddAction(t, function OnDeath)
set t = null
endfunction
endlibrary
//TESH.scrollpos=0
//TESH.alwaysfold=0
library TestAI requires BAIS
private struct TC_AGRO extends Behaviour
method castUpon takes nothing returns nothing
local unit target = GetSpellTargetUnit()
local unit source = GetTriggerUnit()
local real tx = GetUnitX(target)
local real ty = GetUnitY(target)
local real sx = GetUnitX(source)
local real sy = GetUnitY(source)
call CustomOrder.registerOrder(target, "berserk", 6.0)
if(SquareRoot((tx-sx)*(tx-sx) + (ty-sy)*(ty-sy)) < 250) then
call CustomOrder.registerInstantOrder(target, "berserk", 2)
endif
set target = null
set source = null
endmethod
method onDamageTaken takes nothing returns nothing
local unit u = GetTriggerUnit()
local unit source = GetEventDamageSource()
local item i
if(GetWidgetLife(u) < 180 and GetUnitAbilityLevel(u, 'BSTN') == 0) then
set i = GetItemOfTypeFromUnitBJ(u, 'phea')
if(i != null) then
call UnitUseItem(u, i)
set i = null
endif
endif
if(GetUnitState(u, UNIT_STATE_MANA) < 180) then
set i = GetItemOfTypeFromUnitBJ(u, 'pman')
if(i != null) then
call UnitUseItem(u, i)
set i = null
endif
endif
call CustomOrder.registerOrder(u, "shockwave", 8.0)
call CustomOrder.registerTargetOrder(u, source, "shockwave", 10)
set u = null
set source = null
endmethod
private static method onInit takes nothing returns nothing
local Behaviour b = TC_AGRO.create()
call BAIS.attachBehaviourType('Otch', b)
endmethod
endstruct
private struct TC_MEGA extends Behaviour
method castUpon takes nothing returns nothing
local unit u = GetSpellTargetUnit()
local unit source = GetTriggerUnit()
call CustomOrder.registerOrder(u, "silence", 20.0)
call CustomOrder.registerTargetOrder(u, source, "silence", 20)
set u = null
set source = null
endmethod
method onDamageTaken takes nothing returns nothing
local unit source = GetEventDamageSource()
local unit target = GetTriggerUnit()
call CustomOrder.registerOrder(target, "drunkenhaze", 12.0)
call CustomOrder.registerTargetOrder(target, source, "drunkenhaze", 5)
if(GetUnitAbilityLevel(source, 'BNdh') > 0) then
call CustomOrder.registerOrder(target, "breathoffire", 12.0)
call CustomOrder.registerTargetOrder(target, source, "breathoffire", 15)
endif
set source = null
set target = null
endmethod
private static method onInit takes nothing returns nothing
local Behaviour b = TC_MEGA.create()
call BAIS.attachBehaviourType('Otch', b)
endmethod
endstruct
endlibrary
//TESH.scrollpos=58
//TESH.alwaysfold=0
library ZTSExample initializer setupThreatHandler requires BAIS, ZTS
private struct FarSeer extends Behaviour
method onDamageDealt takes nothing returns nothing
local unit target = GetTriggerUnit()
local unit source = GetEventDamageSource()
call CustomOrder.registerOrder(source, "chainlightning", 9.0)
call CustomOrder.registerTargetOrder(source, target, "chainlightning", 20)
set target = null
set source = null
endmethod
method onDamageTaken takes nothing returns nothing
local unit u = GetTriggerUnit()
local unit source = GetEventDamageSource()
call CustomOrder.registerOrder(u, "spiritwolf", 30.0)
call CustomOrder.registerInstantOrder(u, "spiritwolf", 10)
set u = null
set source = null
endmethod
private static method onInit takes nothing returns nothing
local Behaviour b = FarSeer.create()
call BAIS.attachBehaviourType('Ofar', b)
endmethod
endstruct
private struct ShadowHunter extends Behaviour
method castUpon takes nothing returns nothing
local unit target = GetSpellTargetUnit()
local unit source = GetTriggerUnit()
call CustomOrder.registerOrder(target, "hex", 7.0)
call CustomOrder.registerTargetOrder(target, source, "hex", 20)
set target = null
set source = null
endmethod
method onDamageTaken takes nothing returns nothing
local unit u = GetTriggerUnit()
local unit source = GetEventDamageSource()
if(GetWidgetLife(u) <= 0.5*GetUnitState(u, UNIT_STATE_MAX_LIFE)) then
call CustomOrder.registerOrder(u, "healingwave", 9.0)
call CustomOrder.registerTargetOrder(u, u, "healingwave", 100)
endif
set u = null
set source = null
endmethod
method onDamageDealt takes nothing returns nothing
local unit target = GetTriggerUnit()
local unit source = GetEventDamageSource()
call CustomOrder.registerOrder(source, "ward", 6.5)
call CustomOrder.registerTargetOrder(source, target, "ward", 2)
set target = null
set source = null
endmethod
private static method onInit takes nothing returns nothing
local Behaviour b = ShadowHunter.create()
call BAIS.attachBehaviourType('Oshd', b)
endmethod
endstruct
private function threatHandler takes nothing returns nothing
call ZTS_ModifyThreat(GetEventDamageSource(), GetTriggerUnit(), GetEventDamage(), true)
endfunction
private function setupThreatHandler takes nothing returns nothing
local group ENUM = CreateGroup()
local unit u
call StructuredDD.addHandler(function threatHandler)
// player is red
call GroupEnumUnitsOfPlayer(ENUM, Player(0), null)
set u = FirstOfGroup(ENUM)
loop
exitwhen u == null
call GroupRemoveUnit(ENUM, u)
call ZTS_AddPlayerUnit(u)
set u = FirstOfGroup(ENUM)
endloop
// blue is enemy
call GroupEnumUnitsOfPlayer(ENUM, Player(1), null)
set u = FirstOfGroup(ENUM)
loop
exitwhen u == null
call GroupRemoveUnit(ENUM, u)
call ZTS_AddThreatUnit(u, false)
set u = FirstOfGroup(ENUM)
endloop
endfunction
endlibrary
//TESH.scrollpos=0
//TESH.alwaysfold=0
library BAIExt
module aiAdditions
private static real ALLY_SEARCH_RANGE = 512.0
private static real ENEMY_SEARCH_RANGE = 512.0
endmodule
module BAIS_allyAdditions
stub method onAllyDamage takes unit ally returns nothing
endmethod
stub method onAllyDeath takes unit ally returns nothing
endmethod
stub method allyCastOn takes unit ally returns nothing
endmethod
stub method allyCasts takes unit ally returns nothing
endmethod
endmodule
module BAIS_enemyAdditions
stub method onEnemyDamage takes unit enemy returns nothing
endmethod
stub method onEnemyDeath takes unit enemy returns nothing
endmethod
stub method enemyCastOn takes unit enemy returns nothing
endmethod
stub method enemyCasts takes unit enemy returns nothing
endmethod
endmodule
endlibrary
//TESH.scrollpos=424
//TESH.alwaysfold=0
library BAIS uses StructuredDD
/************************************************************************************************/
/* Behaviour AI system */
/* by Kingz */
/* */
/* - Requires: StructuredDD by Cokemonkey11 */
/* Credits to Cokemonkey11 for his nice way of handling damage detection */
/* */
/* Info: */
/* The system works by defining your own behaviour struct by extending the supplied Behaviour */
/* struct and defining wanted methods. After doing so you attach it to the system. */
/* You can attach multiple behaviour structs to a single unit / unit type. */
/* Behaviours will fire one after the other, on a principle of first attached first served. */
/* Regulating multiple behaviours is left to the end user. */
/* The system might be provided with an extension to detect allyDamage / allyDeath in the */
/* nearby vicinity if needed/requested. */
/* Another feature i might add in is global behaviours which get registered to every unit. */
/* Reason i didn't implement it in is that i do not see much use for it. */
/* The goal of the system is to allow easy vJass custom AI coding */
/* */
/* */
/* API: */
/* */
/* Struct BAIS: */
/* */
/* public static method attachBehaviour takes unit u, Behaviour b returns nothing */
/* */
/* public static method attachBehaviourType takes integer unitId, Behaviour b returns nothing */
/* */
/* */
/* public static method removeBehaviour takes unit u, Behaviour b returns boolean */
/* */
/* public static method removeBehaviourType takes integer unitId, Behaviour b returns boolean */
/* */
/* */
/* struct Behaviour: */
/*
stub method periodic takes nothing returns nothing
^ Use GetEnumUnit() to access the unit
stub method onDamageTaken takes nothing returns nothing
^ use GetTriggerUnit() to access the unit, GetEventDamageSource() to access the damage dealer
stub method onDamageDealt takes nothing returns nothing
^ use GetEventDamageSource() to access the unit, GetTriggerUnit() to access the damage receiver
stub method castUpon takes nothing returns nothing
^ use GetSpellTargetUnit() to access the unit, GetTriggerUnit() to access the caster
stub method onCast takes nothing returns nothing
^ use GetTriggerUnit() to access the unit, GetSpellTargetUnit() to access the victim
stub method onDeath takes nothing returns nothing
^ use GetTriggerUnit() to access the unit, GetKillingUnit() to access the killer
stub method onKill takes nothing returns nothing
^ use GetKillingUnit() to access the unit, GetTriggerUnit() to access the victim
*/
/* */
/* struct CustomOrder */
/*
static method registerOrder takes unit u, string order, real cooltime returns nothing
^Registers a internal cooldown for AI skills for the unit using the order string
^BIG NOTE: At the moment you cannot register an order for a unit type, if you want something like that you have to put a call to registerOrder inside the behaviour
before the registerTargetOrder/registerInstantOrder/registerPointOrder call
^BIG NOTE2: All registered orders start on cooldown, it's just the way it works. This can be resolved by setting the TIME_OFFSET to a higher value
static method registerTargetOrder takes unit source, widget target, string orderstr, integer priority returns boolean
^Registers a widget target based order, returns false if a higher priority order is in place or if it cannot be issued due to internal cooldown
static method registerPointOrder takes unit source, real targetX, real targetY, string orderStr, integer priority returns boolean
^Registers a location/point based order. returns false if a higher priority order is in place or if it cannot be issued due to internal cooldown
static method registerInstantOrder takes unit source, string orderStr, integer priority returns boolean
^Registers an instant unit order, returns false if a higher priority order is in place or if it cannot be issued due to internal cooldown
private static method canOrder takes unit u, string order returns boolean
^Called internally before trying to register a target/instant/point order checking the internal cooldown
*/
/* */
/* */
/* Example: */
/*
struct TC_AGRO extends Behaviour
method castUpon takes nothing returns nothing
local unit target = GetSpellTargetUnit()
local unit source = GetTriggerUnit()
local real tx = GetUnitX(target)
local real ty = GetUnitY(target)
local real sx = GetUnitX(source)
local real sy = GetUnitY(source)
call CustomOrder.registerOrder(target, "berserk", 6.0)
if(SquareRoot((tx-sx)*(tx-sx) + (ty-sy)*(ty-sy)) < 250) then
call CustomOrder.registerInstantOrder(target, "berserk", 2)
endif
set target = null
set source = null
endmethod
method onDamageTaken takes nothing returns nothing
local unit u = GetTriggerUnit()
local unit source = GetEventDamageSource()
local item i
if(GetWidgetLife(u) < 180 and GetUnitAbilityLevel(u, 'BSTN') == 0) then
set i = GetItemOfTypeFromUnitBJ(u, 'phea')
if(i != null) then
call UnitUseItem(u, i)
set i = null
endif
endif
if(GetUnitState(u, UNIT_STATE_MANA) < 180) then
set i = GetItemOfTypeFromUnitBJ(u, 'pman')
if(i != null) then
call UnitUseItem(u, i)
set i = null
endif
endif
call CustomOrder.registerOrder(u, "shockwave", 8.0)
call CustomOrder.registerTargetOrder(u, source, "shockwave", 10)
set u = null
set source = null
endmethod
private static method onInit takes nothing returns nothing
local Behaviour b = TC_AGRO.create()
call BAIS.attachBehaviourType('Otch', b)
endmethod
endstruct
*/
/* */
/* */
/************************************************************************************************/
native UnitAlive takes unit u returns boolean
globals
/****************************************************************************************************/
/* CONFIGURATION */
/****************************************************************************************************/
private constant real TICK = 0.3 // global timer period for periodic behaviour
private constant real TIME_OFFSET = 30.0 // if set to 0 all custom orders start on cooldown, set to higher value to reduce initial cooldown
private constant real CLEANUP_TIME = 60 // after this amount of time a dead unit is unregistered from the AI system unless specified otherwise
private constant real CLEANUP_PERIOD = 60 // every CLEANUP_PERIOD of time run cleaner
private constant boolean NEVER_UNREGISTER_HERO = true // if set to true, hero units never release their AI behaviours
private constant boolean NEVER_UNREGISTER_UNIT = false // if set to true, AI behaviours are never released, even if dead
private constant boolean PERIODIC_SKIPS_DEAD = true // if the unit is dead, skip periodic behaviour, if false periodic behaviour always fires
private constant boolean IGNORE_USER_UNITS = true // if set to false AI will be used for user controlled units also
private constant boolean USE_CUSTOM_ORDER = true // if set to true allows the use of CustomOrder struct, else the struct (and it's methods/members) will not be generated
/****************************************************************************************************/
/* DO NOT TOUCH BELOW UNLESS YOU KNOW WHAT YOU ARE DOING */
/****************************************************************************************************/
private constant integer ORDER_KEY = 0 // [CustomOrder hashtables]
private constant integer OFFSET_KEY = 1 // [BAIS hashtables] offset used for behaviour storage
private constant integer DEAD_KEY = 0 // [BAIS hashtables] key used to store amount of seconds the unit is dead
private constant integer DATA_KEY = 3 // [BAIS hashtables] data key start
endglobals
struct Behaviour
stub method periodic takes nothing returns nothing
endmethod
stub method onDamageTaken takes nothing returns nothing
endmethod
stub method onDamageDealt takes nothing returns nothing
endmethod
stub method castUpon takes nothing returns nothing
endmethod
stub method onCast takes nothing returns nothing
endmethod
stub method onDeath takes nothing returns nothing
endmethod
stub method onKill takes nothing returns nothing
endmethod
endstruct
static if USE_CUSTOM_ORDER then
struct CustomOrder
private static hashtable orderData = InitHashtable()
private integer pkey
private unit source
private widget target
private string order
private real tx
private real ty
private boolean locBased
private integer priority
method destroy takes nothing returns nothing
set target = null
set source = null
call this.deallocate()
endmethod
static method executeOrder takes unit u returns nothing
local thistype this
local integer hkey = GetHandleId(u)
if(HaveSavedInteger(thistype.orderData, hkey, ORDER_KEY) == false or LoadInteger(thistype.orderData, hkey, ORDER_KEY) == -1) then
return
endif
set this = LoadInteger(thistype.orderData, hkey, ORDER_KEY)
if(target == null) then
if(locBased == true) then
call IssuePointOrder(source, order, tx, ty)
else
call IssueImmediateOrder(source, order)
endif
else
call IssueTargetOrder(source, order, target)
endif
call SaveReal(thistype.orderData, pkey, -OrderId(order), BAIS.getElapsedTime())
call SaveInteger(thistype.orderData, pkey, ORDER_KEY, -1)
call this.destroy()
endmethod
static method registerOrder takes unit u, string order, real cooltime returns nothing
local integer oid = OrderId(order)
call SaveReal(thistype.orderData, GetHandleId(u), oid+ORDER_KEY+1, cooltime)
if(HaveSavedReal(thistype.orderData, GetHandleId(u), -oid) == false) then
call SaveReal(thistype.orderData, GetHandleId(u), -oid, 0)
endif
endmethod
private static method canOrder takes unit u, string order returns boolean
local real pass_time = 0
local real cool_time = 0
local integer uHandle = GetHandleId(u)
local integer oId = OrderId(order)
if(HaveSavedReal(thistype.orderData, uHandle, oId+ORDER_KEY+1) == false) then
return false
endif
set cool_time = LoadReal(thistype.orderData, uHandle, oId+ORDER_KEY+1)
set pass_time = BAIS.getElapsedTime() - LoadReal(thistype.orderData, uHandle,-oId)
return (pass_time >= cool_time)
endmethod
private static method create takes unit u, string orderstr, widget target, real x, real y, integer prio, boolean isLoc returns thistype
local thistype this = thistype.allocate()
set this.source = u
set this.order = orderstr
set this.target = target
set this.tx = x
set this.ty = y
set this.priority = prio
set this.locBased = isLoc
set this.pkey = GetHandleId(u)
call SaveInteger(thistype.orderData, this.pkey, ORDER_KEY,this)
return this
endmethod
private static method registerGeneralOrder takes unit u, widget target, string orderstr, real x, real y, boolean locOrder, integer priority returns boolean
local thistype co
local integer saved_prio
local integer pkey = GetHandleId(u)
if(thistype.canOrder(u, orderstr) == false) then
return false
endif
if(HaveSavedInteger(thistype.orderData, pkey, ORDER_KEY) == true and LoadInteger(thistype.orderData, pkey, ORDER_KEY) != -1) then
set co = LoadInteger(thistype.orderData, pkey, ORDER_KEY)
if(priority > co.priority) then
call co.destroy()
set co = CustomOrder.create(u, orderstr, target, x, y, priority, locOrder)
return true
endif
else
set co = CustomOrder.create(u, orderstr, target, x, y, priority, locOrder)
return true
endif
return false
endmethod
static method registerTargetOrder takes unit u, widget target, string orderstr, integer priority returns boolean
return registerGeneralOrder(u, target, orderstr, 0, 0, false, priority)
endmethod
static method registerPointOrder takes unit u, real x, real y, string orderstr, integer priority returns boolean
return registerGeneralOrder(u, null, orderstr, x, y, true, priority)
endmethod
static method registerInstantOrder takes unit u, string orderstr, integer priority returns boolean
return registerGeneralOrder(u, null, orderstr, 0, 0, false, priority)
endmethod
endstruct
endif
struct BAIS extends array
private static hashtable unitData = InitHashtable()
private static hashtable unitIdData = InitHashtable()
private static group unitPool = CreateGroup()
private static timer looper = CreateTimer()
private static timer cleanup = CreateTimer()
private static real timeElapsed = TIME_OFFSET
//! textmacro EVENT_HANDLE takes opName
set pkey = GetUnitTypeId(u)
if(u != null and ((GetPlayerController(GetOwningPlayer(u)) == MAP_CONTROL_USER and IGNORE_USER_UNITS == false) or (GetPlayerController(GetOwningPlayer(u)) != MAP_CONTROL_USER and IGNORE_USER_UNITS == true))) then
set offset = LoadInteger(thistype.unitIdData, pkey, OFFSET_KEY)
set i = 0
loop
exitwhen i >= offset
set b = LoadInteger(thistype.unitIdData, pkey, DATA_KEY+i)
call b.$opName$()
set i = i+1
endloop
set pkey = GetHandleId(u)
set offset = LoadInteger(thistype.unitData, pkey, OFFSET_KEY)
set i = 0
loop
exitwhen i >= offset
set b = LoadInteger(thistype.unitData, pkey, DATA_KEY+i)
call b.$opName$()
set i = i+1
endloop
static if(USE_CUSTOM_ORDER) then
call CustomOrder.executeOrder(u)
endif
endif
//! endtextmacro
public static constant method getTimerPeriod takes nothing returns real
return TICK
endmethod
public static constant method getElapsedTime takes nothing returns real
return thistype.timeElapsed
endmethod
public static method attachBehaviour takes unit u, Behaviour b returns nothing
local integer pkey = GetHandleId(u)
local integer offset = LoadInteger(thistype.unitData, pkey, OFFSET_KEY)
call SaveInteger(thistype.unitData, pkey, DATA_KEY+offset, b)
call SaveInteger(thistype.unitData, pkey, OFFSET_KEY, offset+1)
call GroupAddUnit(thistype.unitPool, u)
endmethod
public static method attachBehaviourType takes integer ut, Behaviour b returns nothing
local integer pkey = ut
local integer offset = LoadInteger(thistype.unitIdData, pkey, OFFSET_KEY)
call SaveInteger(thistype.unitIdData, pkey, DATA_KEY+offset, b)
call SaveInteger(thistype.unitIdData, pkey, OFFSET_KEY, offset+1)
endmethod
private static method removeBehaviourEx takes hashtable h, integer pkey, Behaviour b returns boolean
local integer offset = LoadInteger(h, pkey, OFFSET_KEY)
local integer i = 0
local Behaviour temp = 0
loop
exitwhen i >= offset
set temp = LoadInteger(h, pkey, DATA_KEY+i)
if(temp == b) then
set temp = LoadInteger(h, pkey, DATA_KEY+offset-1)
call SaveInteger(h, pkey, DATA_KEY+i, temp)
call SaveInteger(h, pkey, OFFSET_KEY, offset-1)
return true
endif
set i = i +1
endloop
return false
endmethod
public static method removeBehaviour takes unit u, Behaviour b returns boolean
return removeBehaviourEx(thistype.unitData, GetHandleId(u), b)
endmethod
public static method removeBehaviourType takes integer ut, Behaviour b returns boolean
return removeBehaviourEx(thistype.unitIdData, ut, b)
endmethod
private static method deathHandler takes nothing returns boolean
local unit u = GetTriggerUnit()
local integer pkey = GetUnitTypeId(u)
local Behaviour b
local integer offset = 0
local integer i = 0
//! runtextmacro EVENT_HANDLE("onDeath")
set u = GetKillingUnit()
//! runtextmacro EVENT_HANDLE("onKill")
set u = null
return false
endmethod
private static method castHandler takes nothing returns boolean
local unit u = GetTriggerUnit()
local integer pkey = GetUnitTypeId(u)
local Behaviour b
local integer offset = 0
local integer i = 0
//! runtextmacro EVENT_HANDLE("onCast")
set u = GetSpellTargetUnit()
//! runtextmacro EVENT_HANDLE("castUpon")
set u = null
return false
endmethod
private static method ddHandler takes nothing returns nothing
local unit u = GetTriggerUnit()
local integer pkey = GetUnitTypeId(u)
local integer offset = 0
local integer i = 0
local Behaviour b
//! runtextmacro EVENT_HANDLE("onDamageTaken")
set u = GetEventDamageSource()
//! runtextmacro EVENT_HANDLE("onDamageDealt")
set u = null
endmethod
private static method handleUnit takes nothing returns nothing
local unit u = GetEnumUnit()
local integer uId = GetUnitTypeId(u)
local integer pkey = uId
local integer offset = 0
local integer i = 0
local Behaviour b
if(UnitAlive(u) == false and PERIODIC_SKIPS_DEAD) then
set u = null
else
//! runtextmacro EVENT_HANDLE("periodic")
endif
if(NEVER_UNREGISTER_UNIT == false and (IsHeroUnitId(uId) and NEVER_UNREGISTER_HERO)) then
set i = LoadInteger(thistype.unitData, pkey, DEAD_KEY)
call SaveInteger(thistype.unitData, pkey, DEAD_KEY, i +1)
endif
set u = null
endmethod
private static method loopHandler takes nothing returns nothing
call ForGroup(thistype.unitPool, function thistype.handleUnit)
set thistype.timeElapsed = thistype.timeElapsed + TICK
endmethod
private static method cleanUnit takes nothing returns nothing
local unit u = GetEnumUnit()
local integer uHandle = GetHandleId(u)
local boolean isHero = IsHeroUnitId(GetUnitTypeId(u))
local real timeDead = LoadInteger(thistype.unitData, uHandle, DEAD_KEY)
if(NEVER_UNREGISTER_UNIT or (isHero and NEVER_UNREGISTER_HERO)) then
set u = null
return
endif
if(timeDead >= CLEANUP_TIME) then
call GroupRemoveUnit(thistype.unitPool, u)
call FlushChildHashtable(thistype.unitData, uHandle)
endif
set u = null
endmethod
private static method clean takes nothing returns nothing
call ForGroup(thistype.unitPool, function thistype.cleanUnit)
endmethod
private static method onInit takes nothing returns nothing
local trigger castTracker = CreateTrigger()
local trigger deathTracker = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(castTracker, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition(castTracker, function thistype.castHandler)
call TriggerRegisterAnyUnitEventBJ(deathTracker, EVENT_PLAYER_UNIT_DEATH)
call TriggerAddCondition(deathTracker, function thistype.deathHandler)
call TimerStart(thistype.looper, TICK, true, function thistype.loopHandler)
call StructuredDD.addHandler(function thistype.ddHandler)
call TimerStart(thistype.cleanup, CLEANUP_PERIOD, true, function thistype.clean)
endmethod
endstruct
endlibrary
//TESH.scrollpos=0
//TESH.alwaysfold=0
//* API:
//* boolean ADD_ALL_UNITS: If enabled, a trigger turns on which automatically
//* registers all units in the map.
//* integer BUCKET_SIZE: How many units to add to each 'bucket' - a larger
//* bucket will have their trigger refresh less frequently but will be
//* more computationally expensive. A good starting value is about 20.
//* real PER_CLEANUP_TIMEOUT: How many seconds to wait in between each
//* scan for empty buckets. This value should be lower if units die often
//* in your map. A good starting value is about 60.
//* static method addHandler: Registers a callback function to the generic
//* unit damage event. Example: call StructuredDD.addHandler(function h)
//* static method add: Adds a unit to a bucket. If ADD_ALL_UNITS is enabled,
//* this method need not be used.
library StructuredDD
globals
//<< BEGIN SETTINGS SECTION
//* Set this to true if you want all units in your map to be
//* automatically added to StructuredDD. Otherwise you will have to
//* manually add them with StructuredDD.add(u).
private constant boolean ADD_ALL_UNITS=true
//* This is the amount of units that exist in each trigger bucket.
//* This number should be something between 5 and 30. A good starting
//* value will be an estimate of your map's average count of units,
//* divided by 10. When in doubt, just use 20.
private constant integer BUCKET_SIZE=20
//* This is how often StructuredDD will search for empty buckets. If
//* your map has units being created and dying often, a lower value
//* is better. Anything between 10 and 180 is good. When in doubt,
//* just use 60.
private constant real PER_CLEANUP_TIMEOUT=60.
//>> END SETTINGS SECTION
endglobals
//* Our bucket struct which contains a trigger and its associated contents.
private struct bucket
integer bucketIndex=0
trigger trig=CreateTrigger()
unit array members[BUCKET_SIZE]
endstruct
//* Our wrapper struct. We never intend to actually instanciate "a
//* StructuredDD", we just use this for a pretty, java-like API :3
struct StructuredDD extends array
private static boolexpr array conditions
private static bucket array bucketDB
private static integer conditionsIndex=-1
private static integer dbIndex=-1
private static integer maxDBIndex=-1
//* This method gets a readily available bucket for a unit to be added.
//* If the "current" bucket is full, it returns a new one, otherwise
//* it just returns the current bucket.
private static method getBucket takes nothing returns integer
local integer index=0
local integer returner=-1
local bucket tempDat
if thistype.dbIndex!=-1 and thistype.bucketDB[thistype.dbIndex].bucketIndex<BUCKET_SIZE then
return thistype.dbIndex
else
set thistype.maxDBIndex=thistype.maxDBIndex+1
set thistype.dbIndex=thistype.maxDBIndex
set tempDat=bucket.create()
set thistype.bucketDB[.maxDBIndex]=tempDat
loop
exitwhen index>thistype.conditionsIndex
call TriggerAddCondition(tempDat.trig,thistype.conditions[index])
set index=index+1
endloop
return thistype.dbIndex
endif
return -1
endmethod
//* This method is for adding a handler to the system. Whenever a
//* handler is added, damage detection will immediately trigger that
//* handler. There is no way to deallocate a handler, so don't try to
//* do this dynamically (!) Support for handler deallocation is
//* feasible (please contact me)
public static method addHandler takes code func returns nothing
local bucket tempDat
local integer index=0
set thistype.conditionsIndex=thistype.conditionsIndex+1
set thistype.conditions[thistype.conditionsIndex]=Condition(func)
loop
exitwhen index>thistype.maxDBIndex
set tempDat=thistype.bucketDB[index]
call TriggerAddCondition(tempDat.trig,thistype.conditions[thistype.conditionsIndex])
set index=index+1
endloop
endmethod
//* This method adds a unit to the damage detection system. If
//* ADD_ALL_UNITS is enabled, this method need not be used.
public static method add takes unit member returns nothing
local bucket tempDat
local integer whichBucket=thistype.getBucket()
set tempDat=thistype.bucketDB[whichBucket]
set tempDat.bucketIndex=tempDat.bucketIndex+1
set tempDat.members[tempDat.bucketIndex]=member
call TriggerRegisterUnitEvent(tempDat.trig,member,EVENT_UNIT_DAMAGED)
endmethod
//* This is just an auxillary function for ADD_ALL_UNITS' implementation
static if ADD_ALL_UNITS then
private static method autoAddC takes nothing returns boolean
call thistype.add(GetTriggerUnit())
return false
endmethod
endif
//* This method is used to check if a given bucket is empty (and thus
//* can be deallocated) - this is an auxillary reoutine for the
//* periodic cleanup system.
private static method bucketIsEmpty takes integer which returns boolean
local bucket tempDat=thistype.bucketDB[which]
local integer index=0
loop
exitwhen index==BUCKET_SIZE
//GetUnitTypeId(unit)==0 means that the unit has been removed.
if GetUnitTypeId(tempDat.members[index])!=0 then
return false
endif
set index=index+1
endloop
return true
endmethod
//* This method cleans up any empty buckets periodically by checking
//* if it has been fully allocated and then checking if all its
//* members no longer exist.
private static method perCleanup takes nothing returns nothing
local integer index=0
loop
exitwhen index>thistype.maxDBIndex
if index!=thistype.dbIndex and thistype.bucketIsEmpty(index) then
call DestroyTrigger(thistype.bucketDB[index].trig)
call thistype.bucketDB[index].destroy()
set thistype.bucketDB[index]=thistype.bucketDB[thistype.maxDBIndex]
set thistype.maxDBIndex=thistype.maxDBIndex-1
if thistype.maxDBIndex==thistype.dbIndex then
set thistype.dbIndex=index
endif
set index=index-1
endif
set index=index+1
endloop
endmethod
//* This is a initialization function necessary for the setup of
//* StructuredDD.
private static method onInit takes nothing returns nothing
local group grp
local region reg
local trigger autoAddUnits
local timer perCleanup
local unit FoG
static if ADD_ALL_UNITS then
//Add starting units
set grp=CreateGroup()
call GroupEnumUnitsInRect(grp,bj_mapInitialPlayableArea,null)
loop
set FoG=FirstOfGroup(grp)
exitwhen FoG==null
call thistype.add(FoG)
call GroupRemoveUnit(grp,FoG)
endloop
//Add entering units
set autoAddUnits=CreateTrigger()
set reg=CreateRegion()
call RegionAddRect(reg,bj_mapInitialPlayableArea)
call TriggerRegisterEnterRegion(autoAddUnits,reg,null)
call TriggerAddCondition(autoAddUnits,Condition(function thistype.autoAddC))
set autoAddUnits=null
set reg=null
endif
//enable periodic cleanup:
set perCleanup=CreateTimer()
call TimerStart(perCleanup,PER_CLEANUP_TIMEOUT,true,function thistype.perCleanup)
set perCleanup=null
endmethod
endstruct
endlibrary