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