• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[JASS] JASS Campaign AI 2.0

JASS CAMPAIGN AI 2.0

Description

Code

Tutorial

Known Issues


We all know JASS Campaign AI is... clunky, at best.
I always loved its ease of use and even tried to imitate it in Starcraft 2 in the past. However, while working on Warcraft 3 Re-Reforged, I soon realized some of the built-in limitations were an hindrance to my game design process.

For example, using the Exodus of the Horde campaign as reference, it was impossible for me to prevent Kul Tiras Human AI, in chapter three Riders on the Storm, from attacking player's unit groups around the map. While it happened only sometimes, actually quite rarely, it was breaking the map flow of defending the Darkspear Trolls against scripted waves of enemies.
Likewise, in chapter five, I had to resort to an insane set of tricks with pathing blockers to make the Underworld Minions AIs attack all the various entrances of the player base, at random.

Finally, there was another known issue, which often prevents the AI players from continuing their attack wave if such wave is actually winning. Basically, when the AI targets a certain place, it clears its proximity and then it returns to base, unless attacked, even if other enemies structure are present nearby (but just a bit further away then what the original designers planned when writing the natives).
This is very bad because the AI is basically not exploiting its own advantage and stop its successful attack when the player cannot defend itself.

So, after dealing for quite some time with these issues using clunky workarounds, I decided that they were tolerable no longer, and started to study the only real insight we have into Warcraft 3 default JASS AI, the file common.ai:


JASS:
//==================================================================================================
//  $Id: common.ai,v 1.68 2003/05/12 02:34:18 bfitch Exp $
//==================================================================================================
native DebugS               takes string str                            returns nothing
native DebugFI              takes string str, integer val               returns nothing
native DebugUnitID          takes string str, integer val               returns nothing
native DisplayText          takes integer p, string str                 returns nothing
native DisplayTextI         takes integer p, string str, integer val    returns nothing
native DisplayTextII        takes integer p, string str, integer v1, integer v2 returns nothing
native DisplayTextIII       takes integer p, string str, integer v1, integer v2, integer v3 returns nothing
native DoAiScriptDebug      takes nothing                               returns boolean

native GetAiPlayer          takes nothing                               returns integer
native GetHeroId            takes nothing                               returns integer
native GetHeroLevelAI       takes nothing                               returns integer

native GetUnitCount         takes integer unitid                        returns integer
native GetPlayerUnitTypeCount takes player p, integer unitid            returns integer
native GetUnitCountDone     takes integer unitid                        returns integer
native GetTownUnitCount     takes integer id, integer tn, boolean dn    returns integer
native GetUnitGoldCost      takes integer unitid                        returns integer
native GetUnitWoodCost      takes integer unitid                        returns integer
native GetUnitBuildTime     takes integer unitid                        returns integer

native GetMinesOwned        takes nothing                               returns integer
native GetGoldOwned         takes nothing                               returns integer
native TownWithMine         takes nothing                               returns integer
native TownHasMine          takes integer townid                        returns boolean
native TownHasHall          takes integer townid                        returns boolean

native GetUpgradeLevel      takes integer id                            returns integer
native GetUpgradeGoldCost   takes integer id                            returns integer
native GetUpgradeWoodCost   takes integer id                            returns integer
native GetNextExpansion     takes nothing                               returns integer
native GetMegaTarget        takes nothing                               returns unit
native GetBuilding          takes player p                              returns unit
native GetEnemyPower        takes nothing                               returns integer
native SetAllianceTarget    takes unit id                               returns nothing
native GetAllianceTarget    takes nothing                               returns unit

native SetProduce           takes integer qty, integer id, integer town returns boolean
native Unsummon             takes unit unitid                           returns nothing
native SetExpansion         takes unit peon, integer id                 returns boolean
native SetUpgrade           takes integer id                            returns boolean
native SetHeroLevels        takes code func                             returns nothing
native SetNewHeroes         takes boolean state                         returns nothing
native PurchaseZeppelin     takes nothing                               returns nothing

native MergeUnits           takes integer qty, integer a, integer b, integer make returns boolean
native ConvertUnits         takes integer qty, integer id               returns boolean

native SetCampaignAI        takes nothing                               returns nothing
native SetMeleeAI           takes nothing                               returns nothing
native SetTargetHeroes      takes boolean state                         returns nothing
native SetPeonsRepair       takes boolean state                         returns nothing
native SetRandomPaths       takes boolean state                         returns nothing
native SetDefendPlayer      takes boolean state                         returns nothing
native SetHeroesFlee        takes boolean state                         returns nothing
native SetHeroesBuyItems    takes boolean state                         returns nothing
native SetWatchMegaTargets  takes boolean state                         returns nothing
native SetIgnoreInjured     takes boolean state                         returns nothing
native SetHeroesTakeItems   takes boolean state                         returns nothing
native SetUnitsFlee         takes boolean state                         returns nothing
native SetGroupsFlee        takes boolean state                         returns nothing
native SetSlowChopping      takes boolean state                         returns nothing
native SetCaptainChanges    takes boolean allow                         returns nothing
native SetSmartArtillery    takes boolean state                         returns nothing
native SetReplacementCount  takes integer qty                           returns nothing
native GroupTimedLife       takes boolean allow                         returns nothing
native RemoveInjuries       takes nothing                               returns nothing
native RemoveSiege          takes nothing                               returns nothing

native InitAssault          takes nothing                               returns nothing
native AddAssault           takes integer qty, integer id               returns boolean
native AddDefenders         takes integer qty, integer id               returns boolean

native GetCreepCamp         takes integer min, integer max, boolean flyers_ok returns unit

native StartGetEnemyBase    takes nothing                               returns nothing
native WaitGetEnemyBase     takes nothing                               returns boolean
native GetEnemyBase         takes nothing                               returns unit
native GetExpansionFoe      takes nothing                               returns unit
native GetEnemyExpansion    takes nothing                               returns unit
native GetExpansionX        takes nothing                               returns integer
native GetExpansionY        takes nothing                               returns integer
native SetStagePoint        takes real x, real y                        returns nothing
native AttackMoveKill       takes unit target                           returns nothing
native AttackMoveXY         takes integer x, integer y                  returns nothing
native LoadZepWave          takes integer x, integer y                  returns nothing
native SuicidePlayer        takes player id, boolean check_full         returns boolean
native SuicidePlayerUnits   takes player id, boolean check_full         returns boolean
native CaptainInCombat      takes boolean attack_captain                returns boolean
native IsTowered            takes unit target                           returns boolean

native ClearHarvestAI       takes nothing                               returns nothing
native HarvestGold          takes integer town, integer peons           returns nothing
native HarvestWood          takes integer town, integer peons           returns nothing
native GetExpansionPeon     takes nothing                               returns unit

native StopGathering        takes nothing                               returns nothing
native AddGuardPost         takes integer id, real x, real y            returns nothing
native FillGuardPosts       takes nothing                               returns nothing
native ReturnGuardPosts     takes nothing                               returns nothing
native CreateCaptains       takes nothing                               returns nothing
native SetCaptainHome       takes integer which, real x, real y         returns nothing
native ResetCaptainLocs     takes nothing                               returns nothing
native ShiftTownSpot        takes real x, real y                        returns nothing
native TeleportCaptain      takes real x, real y                        returns nothing
native ClearCaptainTargets  takes nothing                               returns nothing
native CaptainAttack        takes real x, real y                        returns nothing
native CaptainVsUnits       takes player id                             returns nothing
native CaptainVsPlayer      takes player id                             returns nothing
native CaptainGoHome        takes nothing                               returns nothing
native CaptainIsHome        takes nothing                               returns boolean
native CaptainIsFull        takes nothing                               returns boolean
native CaptainIsEmpty       takes nothing                               returns boolean
native CaptainGroupSize     takes nothing                               returns integer
native CaptainReadiness     takes nothing                               returns integer
native CaptainRetreating    takes nothing                               returns boolean
native CaptainReadinessHP   takes nothing                               returns integer
native CaptainReadinessMa   takes nothing                               returns integer
native CaptainAtGoal        takes nothing                               returns boolean
native CreepsOnMap          takes nothing                               returns boolean
native SuicideUnit          takes integer count, integer unitid         returns nothing
native SuicideUnitEx        takes integer ct, integer uid, integer pid  returns nothing
native StartThread          takes code func                             returns nothing
native Sleep                takes real seconds                          returns nothing
native UnitAlive            takes unit id                               returns boolean
native UnitInvis            takes unit id                               returns boolean
native IgnoredUnits         takes integer unitid                        returns integer
native TownThreatened       takes nothing                               returns boolean
native DisablePathing       takes nothing                               returns nothing
native SetAmphibious        takes nothing                               returns nothing

native CommandsWaiting      takes nothing                               returns integer
native GetLastCommand       takes nothing                               returns integer
native GetLastData          takes nothing                               returns integer
native PopLastCommand       takes nothing                               returns nothing
native MeleeDifficulty      takes nothing                               returns integer

//============================================================================
//  Globals for all AI scripts
//============================================================================
globals

    //--------------------------------------------------------------------
    //  HUMANS
    //--------------------------------------------------------------------

    // human heroes
    constant integer ARCHMAGE           = 'Hamg'
    constant integer PALADIN            = 'Hpal'
    constant integer MTN_KING           = 'Hmkg'
    constant integer BLOOD_MAGE         = 'Hblm'

    // human hero abilities
    constant integer AVATAR             = 'AHav'
    constant integer BASH               = 'AHbh'
    constant integer THUNDER_BOLT       = 'AHtb'
    constant integer THUNDER_CLAP       = 'AHtc'

    constant integer DEVOTION_AURA      = 'AHad'
    constant integer DIVINE_SHIELD      = 'AHds'
    constant integer HOLY_BOLT          = 'AHhb'
    constant integer RESURRECTION       = 'AHre'

    constant integer BLIZZARD           = 'AHbz'
    constant integer BRILLIANCE_AURA    = 'AHab'
    constant integer MASS_TELEPORT      = 'AHmt'
    constant integer WATER_ELEMENTAL    = 'AHwe'

    constant integer BANISH             = 'AHbn'
    constant integer FLAME_STRIKE       = 'AHfs'
    constant integer SUMMON_PHOENIX     = 'AHpx'
    constant integer SIPHON_MANA        = 'AHdr'

    // special human heroes
    constant integer JAINA              = 'Hjai'
    constant integer MURADIN            = 'Hmbr'
    constant integer GARITHOS           = 'Hlgr'
    constant integer KAEL               = 'Hkal'

    // human units
    constant integer COPTER             = 'hgyr'
    constant integer GYRO               =  COPTER
    constant integer ELEMENTAL          = 'hwat'
    constant integer FOOTMAN            = 'hfoo'
    constant integer FOOTMEN            =  FOOTMAN
    constant integer GRYPHON            = 'hgry'
    constant integer KNIGHT             = 'hkni'
    constant integer MORTAR             = 'hmtm'
    constant integer PEASANT            = 'hpea'
    constant integer PRIEST             = 'hmpr'
    constant integer RIFLEMAN           = 'hrif'
    constant integer RIFLEMEN           =  RIFLEMAN
    constant integer SORCERESS          = 'hsor'
    constant integer TANK               = 'hmtt'
    constant integer STEAM_TANK         =  TANK
    constant integer ROCKET_TANK        = 'hrtt'
    constant integer MILITIA            = 'hmil'
    constant integer SPELL_BREAKER      = 'hspt'
    constant integer HUMAN_DRAGON_HAWK  = 'hdhw'

    // special human units
    constant integer BLOOD_PRIEST       = 'hbep'
    constant integer BLOOD_SORCERESS    = 'hbes'
    constant integer BLOOD_PEASANT      = 'nhew'

    // human buildings
    constant integer AVIARY             = 'hgra'
    constant integer BARRACKS           = 'hbar'
    constant integer BLACKSMITH         = 'hbla'
    constant integer CANNON_TOWER       = 'hctw'
    constant integer CASTLE             = 'hcas'
    constant integer CHURCH             = 'htws'
    constant integer MAGE_TOWER         =  CHURCH
    constant integer GUARD_TOWER        = 'hgtw'
    constant integer HOUSE              = 'hhou'
    constant integer HUMAN_ALTAR        = 'halt'
    constant integer KEEP               = 'hkee'
    constant integer LUMBER_MILL        = 'hlum'
    constant integer SANCTUM            = 'hars'
    constant integer ARCANE_SANCTUM     =  SANCTUM
    constant integer TOWN_HALL          = 'htow'
    constant integer WATCH_TOWER        = 'hwtw'
    constant integer WORKSHOP           = 'harm'
    constant integer ARCANE_VAULT       = 'hvlt'
    constant integer ARCANE_TOWER       = 'hatw'

    // human upgrades
    constant integer UPG_MELEE          = 'Rhme'
    constant integer UPG_RANGED         = 'Rhra'
    constant integer UPG_ARTILLERY      = 'Rhaa'
    constant integer UPG_ARMOR          = 'Rhar'
    constant integer UPG_GOLD           = 'Rhmi'
    constant integer UPG_MASONRY        = 'Rhac'
    constant integer UPG_SIGHT          = 'Rhss'
    constant integer UPG_DEFEND         = 'Rhde'
    constant integer UPG_BREEDING       = 'Rhan'
    constant integer UPG_PRAYING        = 'Rhpt'
    constant integer UPG_SORCERY        = 'Rhst'
    constant integer UPG_LEATHER        = 'Rhla'
    constant integer UPG_GUN_RANGE      = 'Rhri'
    constant integer UPG_WOOD           = 'Rhlh'
    constant integer UPG_SENTINEL       = 'Rhse'
    constant integer UPG_SCATTER        = 'Rhsr'
    constant integer UPG_BOMBS          = 'Rhgb'
    constant integer UPG_HAMMERS        = 'Rhhb'
    constant integer UPG_CONT_MAGIC     = 'Rhss'
    constant integer UPG_FRAGS          = 'Rhfs'
    constant integer UPG_TANK           = 'Rhrt'
    constant integer UPG_FLAK           = 'Rhfc'
    constant integer UPG_CLOUD          = 'Rhcd'

    //--------------------------------------------------------------------
    //  ORCS
    //--------------------------------------------------------------------

    // orc heroes
    constant integer BLADE_MASTER       = 'Obla'
    constant integer FAR_SEER           = 'Ofar'
    constant integer TAUREN_CHIEF       = 'Otch'
    constant integer SHADOW_HUNTER      = 'Oshd'

    // special orc heroes
    constant integer GROM               = 'Ogrh'
    constant integer THRALL             = 'Othr'

    // orc hero abilities
    constant integer CRITICAL_STRIKE    = 'AOcr'
    constant integer MIRROR_IMAGE       = 'AOmi'
    constant integer BLADE_STORM        = 'AOww'
    constant integer WIND_WALK          = 'AOwk'

    constant integer CHAIN_LIGHTNING    = 'AOcl'
    constant integer EARTHQUAKE         = 'AOeq'
    constant integer FAR_SIGHT          = 'AOfs'
    constant integer SPIRIT_WOLF        = 'AOsf'

    constant integer ENDURANE_AURA      = 'AOae'
    constant integer REINCARNATION      = 'AOre'
    constant integer SHOCKWAVE          = 'AOsh'
    constant integer WAR_STOMP          = 'AOws'

    constant integer HEALING_WAVE       = 'AOhw'
    constant integer HEX                = 'AOhx'
    constant integer SERPENT_WARD       = 'AOsw'
    constant integer VOODOO             = 'AOvd'

    // orc units
    constant integer GUARDIAN           = 'oang'
    constant integer CATAPULT           = 'ocat'
    constant integer WITCH_DOCTOR       = 'odoc'
    constant integer GRUNT              = 'ogru'
    constant integer HEAD_HUNTER        = 'ohun'
    constant integer BERSERKER          = 'otbk'
    constant integer KODO_BEAST         = 'okod'
    constant integer PEON               = 'opeo'
    constant integer RAIDER             = 'orai'
    constant integer SHAMAN             = 'oshm'
    constant integer TAUREN             = 'otau'
    constant integer WYVERN             = 'owyv'
    constant integer BATRIDER           = 'otbr'
    constant integer SPIRIT_WALKER      = 'ospw'
    constant integer SPIRIT_WALKER_M    = 'ospm'

    // orc buildings
    constant integer ORC_ALTAR          = 'oalt'
    constant integer ORC_BARRACKS       = 'obar'
    constant integer BESTIARY           = 'obea'
    constant integer FORGE              = 'ofor'
    constant integer FORTRESS           = 'ofrt'
    constant integer GREAT_HALL         = 'ogre'
    constant integer LODGE              = 'osld'
    constant integer STRONGHOLD         = 'ostr'
    constant integer BURROW             = 'otrb'
    constant integer TOTEM              = 'otto'
    constant integer ORC_WATCH_TOWER    = 'owtw'
    constant integer VOODOO_LOUNGE      = 'ovln'

    // orc upgrades
    constant integer UPG_ORC_MELEE      = 'Rome'
    constant integer UPG_ORC_RANGED     = 'Rora'
    constant integer UPG_ORC_ARTILLERY  = 'Roaa'
    constant integer UPG_ORC_ARMOR      = 'Roar'
    constant integer UPG_ORC_WAR_DRUMS  = 'Rwdm'
    constant integer UPG_ORC_PILLAGE    = 'Ropg'
    constant integer UPG_ORC_BERSERK    = 'Robs'
    constant integer UPG_ORC_PULVERIZE  = 'Rows'
    constant integer UPG_ORC_ENSNARE    = 'Roen'
    constant integer UPG_ORC_VENOM      = 'Rovs'
    constant integer UPG_ORC_DOCS       = 'Rowd'
    constant integer UPG_ORC_SHAMAN     = 'Rost'
    constant integer UPG_ORC_SPIKES     = 'Rosp'
    constant integer UPG_ORC_BURROWS    = 'Rorb'
    constant integer UPG_ORC_REGEN      = 'Rotr'
    constant integer UPG_ORC_FIRE       = 'Rolf'
    constant integer UPG_ORC_SWALKER    = 'Rowt'
    constant integer UPG_ORC_BERSERKER  = 'Robk'
    constant integer UPG_ORC_NAPTHA     = 'Robf'
    constant integer UPG_ORC_CHAOS      = 'Roch'

    // Warcraft 2 orc units
    constant integer OGRE_MAGI          = 'nomg'
    constant integer ORC_DRAGON         = 'nrwm'
    constant integer SAPPER             = 'ngsp'
    constant integer ZEPPLIN            = 'nzep'
    constant integer ZEPPELIN           =  ZEPPLIN
    constant integer W2_WARLOCK         = 'nw2w'
    constant integer PIG_FARM           = 'npgf'
    constant integer FOREST_TROLL       = 'nftr'

    // special orc units
    constant integer CHAOS_GRUNT        = 'nchg'
    constant integer CHAOS_WARLOCK      = 'nchw'
    constant integer CHAOS_RAIDER       = 'nchr'
    constant integer CHAOS_PEON         = 'ncpn'
    constant integer CHAOS_KODO         = 'nckb'
    constant integer CHAOS_GROM         = 'Opgh'
    constant integer CHAOS_BLADEMASTER  = 'Nbbc'
    constant integer CHAOS_BURROW       = 'ocbw'

    //--------------------------------------------------------------------
    //  UNDEAD
    //--------------------------------------------------------------------

    // undead heroes
    constant integer DEATH_KNIGHT       = 'Udea'
    constant integer DREAD_LORD         = 'Udre'
    constant integer LICH               = 'Ulic'
    constant integer CRYPT_LORD         = 'Ucrl'

    // special undead heroes
    constant integer MALGANIS           = 'Umal'
    constant integer TICHONDRIUS        = 'Utic'
    constant integer PIT_LORD           = 'Npld'
    constant integer DETHEROC           = 'Udth'

    // undead hero abilities
    constant integer SLEEP              = 'AUsl'
    constant integer VAMP_AURA          = 'AUav'
    constant integer CARRION_SWARM      = 'AUcs'
    constant integer INFERNO            = 'AUin'

    constant integer DARK_RITUAL        = 'AUdr'
    constant integer DEATH_DECAY        = 'AUdd'
    constant integer FROST_ARMOR        = 'AUfu'
    constant integer FROST_NOVA         = 'AUfn'

    constant integer ANIM_DEAD          = 'AUan'
    constant integer DEATH_COIL         = 'AUdc'
    constant integer DEATH_PACT         = 'AUdp'
    constant integer UNHOLY_AURA        = 'AUau'

    constant integer CARRION_SCARAB     = 'AUcb'
    constant integer IMPALE             = 'AUim'
    constant integer LOCUST_SWARM       = 'AUls'
    constant integer THORNY_SHIELD      = 'AUts'

    // undead units
    constant integer ABOMINATION        = 'uabo'
    constant integer ACOLYTE            = 'uaco'
    constant integer BANSHEE            = 'uban'
    constant integer PIT_FIEND          = 'ucry'
    constant integer CRYPT_FIEND        =  PIT_FIEND
    constant integer FROST_WYRM         = 'ufro'
    constant integer GARGOYLE           = 'ugar'
    constant integer GARGOYLE_MORPH     = 'ugrm'
    constant integer GHOUL              = 'ugho'
    constant integer MEAT_WAGON         = 'umtw'
    constant integer NECRO              = 'unec'
    constant integer SKEL_WARRIOR       = 'uske'
    constant integer SHADE              = 'ushd'
    constant integer UNDEAD_BARGE       = 'uarb'
    constant integer OBSIDIAN_STATUE    = 'uobs'
    constant integer OBS_STATUE         =  OBSIDIAN_STATUE
    constant integer BLK_SPHINX         = 'ubsp'

    // undead buildings
    constant integer UNDEAD_MINE        = 'ugol'
    constant integer UNDEAD_ALTAR       = 'uaod'
    constant integer BONEYARD           = 'ubon'
    constant integer GARG_SPIRE         = 'ugsp'
    constant integer NECROPOLIS_1       = 'unpl'    // normal
    constant integer NECROPOLIS_2       = 'unp1'    // upgraded once
    constant integer NECROPOLIS_3       = 'unp2'    // full upgrade
    constant integer SAC_PIT            = 'usap'
    constant integer CRYPT              = 'usep'
    constant integer SLAUGHTERHOUSE     = 'uslh'
    constant integer DAMNED_TEMPLE      = 'utod'
    constant integer ZIGGURAT_1         = 'uzig'    // normal
    constant integer ZIGGURAT_2         = 'uzg1'    // upgraded
    constant integer ZIGGURAT_FROST     = 'uzg2'    // frost tower
    constant integer GRAVEYARD          = 'ugrv'
    constant integer TOMB_OF_RELICS     = 'utom'

    // undead upgrades
    constant integer UPG_UNHOLY_STR     = 'Rume'
    constant integer UPG_CR_ATTACK      = 'Rura'
    constant integer UPG_UNHOLY_ARMOR   = 'Ruar'
    constant integer UPG_CANNIBALIZE    = 'Ruac'
    constant integer UPG_GHOUL_FRENZY   = 'Rugf'
    constant integer UPG_FIEND_WEB      = 'Ruwb'
    constant integer UPG_ABOM           = 'Ruab'
    constant integer UPG_STONE_FORM     = 'Rusf'
    constant integer UPG_NECROS         = 'Rune'
    constant integer UPG_BANSHEE        = 'Ruba'
    constant integer UPG_MEAT_WAGON     = 'Rump'
    constant integer UPG_WYRM_BREATH    = 'Rufb'
    constant integer UPG_SKEL_LIFE      = 'Rusl'
    constant integer UPG_SKEL_MASTERY   = 'Rusm'
    constant integer UPG_EXHUME         = 'Ruex'
    constant integer UPG_SACRIFICE      = 'Rurs'
    constant integer UPG_ABOM_EXPL      = 'Ruax'
    constant integer UPG_CR_ARMOR       = 'Rucr'
    constant integer UPG_PLAGUE         = 'Rupc'
    constant integer UPG_BLK_SPHINX     = 'Rusp'
    constant integer UPG_BURROWING      = 'Rubu'

    //--------------------------------------------------------------------
    //  ELVES
    //--------------------------------------------------------------------

    // elf heroes
    constant integer DEMON_HUNTER       = 'Edem'
    constant integer DEMON_HUNTER_M     = 'Edmm'
    constant integer KEEPER             = 'Ekee'
    constant integer MOON_CHICK         = 'Emoo'
    constant integer MOON_BABE          =  MOON_CHICK
    constant integer MOON_HONEY         =  MOON_CHICK
    constant integer WARDEN             = 'Ewar'

    // special elf heroes
    constant integer SYLVANUS           = 'Hvwd'
    constant integer CENARIUS           = 'Ecen'
    constant integer ILLIDAN            = 'Eevi'
    constant integer ILLIDAN_DEMON      = 'Eevm'
    constant integer MAIEV              = 'Ewrd'

    // elf hero abilities
    constant integer FORCE_NATURE       = 'AEfn'
    constant integer ENT_ROOTS          = 'AEer'
    constant integer THORNS_AURA        = 'AEah'
    constant integer TRANQUILITY        = 'AEtq'

    constant integer EVASION            = 'AEev'
    constant integer IMMOLATION         = 'AEim'
    constant integer MANA_BURN          = 'AEmb'
    constant integer METAMORPHOSIS      = 'AEme'

    constant integer SEARING_ARROWS     = 'AHfa'
    constant integer SCOUT              = 'AEst'
    constant integer STARFALL           = 'AEsf'
    constant integer TRUESHOT           = 'AEar'

    constant integer BLINK              = 'AEbl'
    constant integer FAN_KNIVES         = 'AEfk'
    constant integer SHADOW_TOUCH       = 'AEsh'
    constant integer VENGEANCE          = 'AEsv'

    // elf units
    constant integer WISP               = 'ewsp'
    constant integer ARCHER             = 'earc'
    constant integer DRUID_TALON        = 'edot'
    constant integer DRUID_TALON_M      = 'edtm'
    constant integer BALLISTA           = 'ebal'
    constant integer DRUID_CLAW         = 'edoc'
    constant integer DRUID_CLAW_M       = 'edcm'
    constant integer DRYAD              = 'edry'
    constant integer HIPPO              = 'ehip'
    constant integer HIPPO_RIDER        = 'ehpr'
    constant integer HUNTRESS           = 'esen'
    constant integer CHIMAERA           = 'echm'
    constant integer ENT                = 'efon'
    constant integer MOUNTAIN_GIANT     = 'emtg'
    constant integer FAERIE_DRAGON      = 'efdr'

    // special elf units
    constant integer HIGH_ARCHER        = 'nhea'
    constant integer HIGH_FOOTMAN       = 'hcth'
    constant integer HIGH_FOOTMEN       =  HIGH_FOOTMAN
    constant integer HIGH_SWORDMAN      = 'hhes'
    constant integer DRAGON_HAWK        = 'nws1'
    constant integer CORRUPT_TREANT     = 'nenc'
    constant integer POISON_TREANT      = 'nenp'
    constant integer PLAGUE_TREANT      = 'nepl'
    constant integer SHANDRIS           = 'eshd'

    // elf buildings
    constant integer ANCIENT_LORE       = 'eaoe'
    constant integer ANCIENT_WAR        = 'eaom'
    constant integer ANCIENT_WIND       = 'eaow'
    constant integer TREE_AGES          = 'etoa'
    constant integer TREE_ETERNITY      = 'etoe'
    constant integer TREE_LIFE          = 'etol'
    constant integer ANCIENT_PROTECT    = 'etrp'
    constant integer ELF_ALTAR          = 'eate'
    constant integer BEAR_DEN           = 'edol'
    constant integer CHIMAERA_ROOST     = 'edos'
    constant integer HUNTERS_HALL       = 'edob'
    constant integer MOON_WELL          = 'emow'
    constant integer ELF_MINE           = 'egol'
    constant integer DEN_OF_WONDERS     = 'eden'

    // special elf buildings
    constant integer ELF_FARM           = 'nefm'
    constant integer ELF_GUARD_TOWER    = 'negt'
    constant integer HIGH_SKY           = 'negm'
    constant integer HIGH_EARTH         = 'negf'
    constant integer HIGH_TOWER         = 'negt'
    constant integer ELF_HIGH_BARRACKS  = 'nheb'
    constant integer CORRUPT_LIFE       = 'nctl'
    constant integer CORRUPT_WELL       = 'ncmw'
    constant integer CORRUPT_PROTECTOR  = 'ncap'
    constant integer CORRUPT_WAR        = 'ncaw'

    // elf upgrades
    constant integer UPG_STR_MOON       = 'Resm'
    constant integer UPG_STR_WILD       = 'Resw'
    constant integer UPG_MOON_ARMOR     = 'Rema'
    constant integer UPG_HIDES          = 'Rerh'
    constant integer UPG_ULTRAVISION    = 'Reuv'
    constant integer UPG_BLESSING       = 'Renb'
    constant integer UPG_SCOUT          = 'Resc'
    constant integer UPG_GLAIVE         = 'Remg'
    constant integer UPG_BOWS           = 'Reib'
    constant integer UPG_MARKSMAN       = 'Remk'
    constant integer UPG_DRUID_TALON    = 'Redt'
    constant integer UPG_DRUID_CLAW     = 'Redc'
    constant integer UPG_ABOLISH        = 'Resi'
    constant integer UPG_CHIM_ACID      = 'Recb'
    constant integer UPG_HIPPO_TAME     = 'Reht'
    constant integer UPG_BOLT           = 'Repd'
    constant integer UPG_MARK_CLAW      = 'Reeb'
    constant integer UPG_MARK_TALON     = 'Reec'
    constant integer UPG_HARD_SKIN      = 'Rehs'
    constant integer UPG_RESIST_SKIN    = 'Rers'
    constant integer UPG_WELL_SPRING    = 'Rews'

    //--------------------------------------------------------------------
    // Neutral
    //--------------------------------------------------------------------
    constant integer DEMON_GATE         = 'ndmg'
    constant integer FELLHOUND          = 'nfel'
    constant integer INFERNAL           = 'ninf'
    constant integer DOOMGUARD          = 'nbal'
    constant integer SATYR              = 'nsty'
    constant integer TRICKSTER          = 'nsat'
    constant integer SHADOWDANCER       = 'nsts'
    constant integer SOULSTEALER        = 'nstl'
    constant integer HELLCALLER         = 'nsth'
    constant integer SKEL_ARCHER        = 'nska'
    constant integer SKEL_MARKSMAN      = 'nskm'
    constant integer SKEL_BURNING       = 'nskf'
    constant integer SKEL_GIANT         = 'nskg'
    constant integer FURBOLG            = 'nfrl'
    constant integer FURBOLG_TRACKER    = 'nfrb'
    constant integer FURBOLG_SHAMAN     = 'nfrs'
    constant integer FURBOLG_CHAMP      = 'nfrg'
    constant integer FURBOLG_ELDER      = 'nfre'

    //--------------------------------------------------------------------
    // NAGA
    //--------------------------------------------------------------------

    // naga heroes
    constant integer NAGA_SORCERESS     = 'Nngs'
    constant integer NAGA_VASHJ         = 'Hvsh'

    // naga units
    constant integer NAGA_DRAGON        = 'nsnp'        // old names
    constant integer NAGA_WITCH         = 'nnsw'
    constant integer NAGA_SERPENT       = 'nwgs'
    constant integer NAGA_HYDRA         = 'nhyc'

    constant integer NAGA_SLAVE         = 'nmpe'        // peon
    constant integer NAGA_SNAP_DRAGON   =  NAGA_DRAGON  // weak ranged
    constant integer NAGA_COUATL        =  NAGA_SERPENT // weak air
    constant integer NAGA_SIREN         =  NAGA_WITCH   // caster
    constant integer NAGA_MYRMIDON      = 'nmyr'        // knight
    constant integer NAGA_REAVER        = 'nnmg'        // footman
    constant integer NAGA_TURTLE        =  NAGA_HYDRA   // siege
    constant integer NAGA_ROYAL         = 'nnrg'        // royal guard

    // naga buildings
    constant integer NAGA_TEMPLE        = 'nntt'        // town hall
    constant integer NAGA_CORAL         = 'nnfm'        // farm
    constant integer NAGA_SHRINE        = 'nnsa'        // sirens & couatls
    constant integer NAGA_SPAWNING      = 'nnsg'        // myrm, snap dragon, hydra
    constant integer NAGA_GUARDIAN      = 'nntg'        // tower
    constant integer NAGA_ALTAR         = 'nnad'        // altar

    // naga upgrades
    constant integer UPG_NAGA_ARMOR     = 'Rnam'
    constant integer UPG_NAGA_ATTACK    = 'Rnat'
    constant integer UPG_NAGA_ABOLISH   = 'Rnsi'
    constant integer UPG_SIREN          = 'Rnsw'
    constant integer UPG_NAGA_ENSNARE   = 'Rnen'


    //--------------------------------------------------------------------
    constant integer M1                 =    60
    constant integer M2                 =  2*60
    constant integer M3                 =  3*60
    constant integer M4                 =  4*60
    constant integer M5                 =  5*60
    constant integer M6                 =  6*60
    constant integer M7                 =  7*60
    constant integer M8                 =  8*60
    constant integer M9                 =  9*60
    constant integer M10                = 10*60
    constant integer M11                = 11*60
    constant integer M12                = 12*60
    constant integer M13                = 13*60
    constant integer M14                = 14*60
    constant integer M15                = 15*60

    constant integer EASY               = 1
    constant integer NORMAL             = 2
    constant integer HARD               = 3
    constant integer INSANE             = 4 // not used

    constant integer MELEE_NEWBIE       = 1
    constant integer MELEE_NORMAL       = 2
    constant integer MELEE_INSANE       = 3

    constant integer ATTACK_CAPTAIN     = 1
    constant integer DEFENSE_CAPTAIN    = 2
    constant integer BOTH_CAPTAINS      = 3

    constant integer BUILD_UNIT         = 1
    constant integer BUILD_UPGRADE      = 2
    constant integer BUILD_EXPAND       = 3

    constant integer UPKEEP_TIER1       = 50
    constant integer UPKEEP_TIER2       = 80

    //--------------------------------------------------------------------

    player  ai_player

    integer sleep_seconds
    integer total_gold              = 0
    integer total_wood              = 0
    integer gold_buffer             = 0 // usually for potion money
    integer difficulty              = NORMAL
    integer exp_seen                = 0
    integer racial_farm             = 'hhou'
    integer hero_id                 = 'Hamg'
    integer hero_id2                = 'Hmkg'
    integer hero_id3                = 'Hpal'
    integer array skill
    integer array skills1
    integer array skills2
    integer array skills3
    integer max_hero_level          = 0

    integer array harass_qty
    integer array harass_max
    integer array harass_units
    integer harass_length           = 0

    integer array defense_qty
    integer array defense_units
    integer defense_length          = 0

    integer array build_qty
    integer array build_type
    integer array build_item
    integer array build_town
    integer build_length            = 0

    integer campaign_gold_peons     = 5
    integer campaign_wood_peons     = 3
    integer campaign_basics_speed   = 5

    integer min_creeps              = -1
    integer max_creeps              = -1

    boolean harvest_town1           = true
    boolean harvest_town2           = true
    boolean harvest_town3           = true
    boolean do_campaign_farms       = true
    boolean two_heroes              = false
    boolean allow_air_creeps        = false
    boolean take_exp                = false
    boolean allow_signal_abort      = false
    boolean ready_for_zeppelin      = true
    boolean get_zeppelin            = false

    boolean build_campaign_attackers = true

    boolean do_debug_cheats         = false
    boolean trace_on                = true
    boolean zep_next_wave           = false
    boolean form_group_timeouts     = true
endglobals

//============================================================================
function PlayerEx takes integer slot returns player
    return Player(slot-1)
endfunction

//============================================================================
function Trace takes string message returns nothing
    if trace_on then
        call DisplayText(GetAiPlayer(),message)
    endif
endfunction

//============================================================================
function TraceI takes string message, integer val returns nothing
    if trace_on then
        call DisplayTextI(GetAiPlayer(),message,val)
    endif
endfunction

//============================================================================
function TraceII takes string message, integer v1, integer v2 returns nothing
    if trace_on then
        call DisplayTextII(GetAiPlayer(),message,v1,v2)
    endif
endfunction

//============================================================================
function TraceIII takes string message, integer v1, integer v2, integer v3 returns nothing
    if trace_on then
        call DisplayTextIII(GetAiPlayer(),message,v1,v2,v3)
    endif
endfunction

//============================================================================
function InitAI takes nothing returns nothing
    set ai_player = Player(GetAiPlayer())
    set sleep_seconds = 0
    call StopGathering()
endfunction

//============================================================================
function StandardAI takes code heroes, code peons, code attacks returns nothing

    local boolean isNewbie = (MeleeDifficulty() == MELEE_NEWBIE)

    call InitAI()

    call SetMeleeAI()

    call SetDefendPlayer(true)
    call SetGroupsFlee(not isNewbie)
    call SetHeroesBuyItems(not isNewbie)
    call SetHeroesFlee(true)
    call SetHeroesTakeItems(true)
    call SetIgnoreInjured(true)
    call SetPeonsRepair(true)
    call SetSmartArtillery(not isNewbie)
    call SetTargetHeroes(not isNewbie)
    call SetUnitsFlee(not isNewbie)
    call SetWatchMegaTargets(true)

    call CreateCaptains()

    call SetHeroLevels(heroes)

    call Sleep(0.1)
    call StartThread(peons)
    call StartThread(attacks)
endfunction

//============================================================================
//  Utility Functions
//============================================================================
function Min takes integer A, integer B returns integer
    if A < B then
        return A
    else
        return B
    endif
endfunction

function Max takes integer A, integer B returns integer
    if A > B then
        return A
    else
        return B
    endif
endfunction

function SetZepNextWave takes nothing returns nothing
    set zep_next_wave = true
endfunction

function SuicideSleep takes integer seconds returns nothing
    set sleep_seconds = sleep_seconds - seconds
    loop
        exitwhen seconds <= 0
        exitwhen allow_signal_abort and CommandsWaiting() != 0

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
    endloop
endfunction

//============================================================================
function WaitForSignal takes nothing returns integer
    local integer cmd
    local boolean display = false //xxx
    loop
        exitwhen CommandsWaiting() != 0

        //xxx
        call Trace("waiting for a signal to begin AI script...\n")
        set display = true
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        //xxx

    endloop

    //xxx
    if display then
        call Trace("signal received, beginning AI script\n")
    endif
    //xxx

    set cmd = GetLastCommand()
    call PopLastCommand()
    return cmd
endfunction

//============================================================================
function SetWoodPeons takes integer count returns nothing
    set campaign_wood_peons = count
endfunction

//============================================================================
function SetGoldPeons takes integer count returns nothing
    set campaign_gold_peons = count
endfunction

//============================================================================
function SetHarvestLumber takes boolean harvest returns nothing
    if harvest then
        set campaign_wood_peons = 3
    else
        set campaign_wood_peons = 0
    endif
endfunction

//============================================================================
function SetFormGroupTimeouts takes boolean state returns nothing
    set form_group_timeouts = state
endfunction

//============================================================================
function DoCampaignFarms takes boolean state returns nothing
    set do_campaign_farms = state
endfunction

//============================================================================
function GetMinorCreep takes nothing returns unit
    return GetCreepCamp(0,9,false)
endfunction

//============================================================================
function GetMajorCreep takes nothing returns unit
    return GetCreepCamp(10,100,allow_air_creeps)
endfunction

//============================================================================
function GetGold takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_GOLD)
endfunction

//============================================================================
function GetWood takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_LUMBER)
endfunction

//============================================================================
function InitBuildArray takes nothing returns nothing
    set build_length = 0
endfunction

//============================================================================
function InitAssaultGroup takes nothing returns nothing
    set harass_length = 0
endfunction

//============================================================================
function InitDefenseGroup takes nothing returns nothing
    set defense_length = 0
endfunction

//============================================================================
function InitMeleeGroup takes nothing returns nothing
    call InitAssaultGroup()
    call RemoveInjuries()
    call RemoveSiege()
endfunction

//============================================================================
function PrepFullSuicide takes nothing returns nothing
    call InitAssaultGroup()
    call InitDefenseGroup()
    set campaign_gold_peons = 0
    set campaign_wood_peons = 0
endfunction

//============================================================================
function SetReplacements takes integer easy, integer med, integer hard returns nothing
    if difficulty == EASY then
        call SetReplacementCount(easy)
    elseif difficulty == NORMAL then
        call SetReplacementCount(med)
    else
        call SetReplacementCount(hard)
    endif
endfunction

//============================================================================
function StartTownBuilder takes code func returns nothing
    call StartThread(func)
endfunction

//============================================================================
function SetBuildAll takes integer t, integer qty, integer unitid, integer town returns nothing
    if qty > 0 then
        set build_qty[build_length] = qty
        set build_type[build_length] = t
        set build_item[build_length] = unitid
        set build_town[build_length] = town
        set build_length = build_length + 1
    endif
endfunction

//============================================================================
function SetBuildUnit takes integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_UNIT,qty,unitid,-1)
endfunction

//============================================================================
function SetBuildNext takes integer qty, integer unitid returns nothing
    local integer has = GetUnitCount(unitid)
    if has >= qty then
        return
    endif
    call SetBuildAll(BUILD_UNIT,GetUnitCountDone(unitid)+1,unitid,-1)
endfunction

//============================================================================
function SetBuildUnitEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call SetBuildAll(BUILD_UNIT,easy,unitid,-1)
    elseif difficulty == NORMAL then
        call SetBuildAll(BUILD_UNIT,med,unitid,-1)
    else
        call SetBuildAll(BUILD_UNIT,hard,unitid,-1)
    endif
endfunction

//============================================================================
function SecondaryTown takes integer town, integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_UNIT,qty,unitid,town)
endfunction

//============================================================================
function SecTown takes integer town, integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_UNIT,qty,unitid,town)
endfunction

//============================================================================
function SetBuildUpgr takes integer qty, integer unitid returns nothing
    if MeleeDifficulty() != MELEE_NEWBIE or qty == 1 then
        call SetBuildAll(BUILD_UPGRADE,qty,unitid,-1)
    endif
endfunction

//============================================================================
function SetBuildUpgrEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call SetBuildAll(BUILD_UPGRADE,easy,unitid,-1)
    elseif difficulty == NORMAL then
        call SetBuildAll(BUILD_UPGRADE,med,unitid,-1)
    else
        call SetBuildAll(BUILD_UPGRADE,hard,unitid,-1)
    endif
endfunction

//============================================================================
function SetBuildExpa takes integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_EXPAND,qty,unitid,-1)
endfunction

//============================================================================
function StartUpgrade takes integer level, integer upgid returns boolean
    local integer gold_cost
    local integer wood_cost

    if GetUpgradeLevel(upgid) >= level then
        return true
    endif

    set gold_cost = GetUpgradeGoldCost(upgid)
    if total_gold < gold_cost then
        return false
    endif

    set wood_cost = GetUpgradeWoodCost(upgid)
    if total_wood < wood_cost then
        return false
    endif

    return SetUpgrade(upgid)
endfunction

//============================================================================
function BuildFactory takes integer unitid returns nothing
    if GetGold() > 1000 and GetWood() > 500 then
        call SetBuildUnit( 2, unitid )
    else
        call SetBuildUnit( 1, unitid )
    endif
endfunction

//============================================================================
function HallsCompleted takes integer unitid returns boolean
    return GetUnitCount(unitid) == GetUnitCountDone(unitid)
endfunction

//============================================================================
function GuardSecondary takes integer townid, integer qty, integer unitid returns nothing
    if TownHasHall(townid) and TownHasMine(townid) then
        call SecondaryTown( townid, qty, unitid )
    endif
endfunction

//============================================================================
function GetUnitCountEx takes integer unitid, boolean only_done, integer townid returns integer
    if townid == -1 then
        if only_done then
            return GetUnitCountDone(unitid)
        else
            return GetUnitCount(unitid)
        endif
    else
        return GetTownUnitCount(unitid,townid,only_done)
    endif
endfunction

//============================================================================
function TownCountEx takes integer unitid, boolean only_done, integer townid returns integer

    local integer have_qty = GetUnitCountEx(unitid,only_done,townid)

    if unitid == TOWN_HALL then
        set have_qty = have_qty + GetUnitCountEx(KEEP,false,townid) + GetUnitCountEx(CASTLE,false,townid)
    elseif unitid == KEEP then
        set have_qty = have_qty  + GetUnitCountEx(CASTLE,false,townid)

    elseif unitid == WATCH_TOWER then
        set have_qty = have_qty + GetUnitCountEx(GUARD_TOWER,false,townid) + GetUnitCountEx(CANNON_TOWER,false,townid) + GetUnitCountEx(ARCANE_TOWER,false,townid)

    elseif unitid == PEASANT then
        set have_qty = have_qty + GetUnitCountEx(MILITIA,false,townid)

    elseif unitid == GREAT_HALL then
        set have_qty = have_qty + GetUnitCountEx(STRONGHOLD,false,townid) + GetUnitCountEx(FORTRESS,false,townid)
    elseif unitid == STRONGHOLD then
        set have_qty = have_qty + GetUnitCountEx(FORTRESS,false,townid)

    elseif unitid == HEAD_HUNTER then
        set have_qty = have_qty + GetUnitCountEx(BERSERKER,false,townid)

    elseif unitid == SPIRIT_WALKER then
        set have_qty = have_qty + GetUnitCountEx(SPIRIT_WALKER_M,false,townid)
    elseif unitid == SPIRIT_WALKER_M then
        set have_qty = have_qty + GetUnitCountEx(SPIRIT_WALKER,only_done,townid)

    elseif unitid == NECROPOLIS_1 then
        set have_qty = have_qty + GetUnitCountEx(NECROPOLIS_2,false,townid) + GetUnitCountEx(NECROPOLIS_3,false,townid)
    elseif unitid == NECROPOLIS_2 then
        set have_qty = have_qty + GetUnitCountEx(NECROPOLIS_3,false,townid)

    elseif unitid == ZIGGURAT_1 then
        set have_qty = have_qty + GetUnitCountEx(ZIGGURAT_2,false,townid) + GetUnitCountEx(ZIGGURAT_FROST,false,townid)

    elseif unitid == GARGOYLE then
        set have_qty = have_qty + GetUnitCountEx(GARGOYLE_MORPH,false,townid)

    elseif unitid == TREE_LIFE then
        set have_qty = have_qty + GetUnitCountEx(TREE_AGES,false,townid) + GetUnitCountEx(TREE_ETERNITY,false,townid)
    elseif unitid == TREE_AGES then
        set have_qty = have_qty + GetUnitCountEx(TREE_ETERNITY,false,townid)

    elseif unitid == DRUID_TALON then
        set have_qty = have_qty + GetUnitCountEx(DRUID_TALON_M,false,townid)
    elseif unitid == DRUID_TALON_M then
        set have_qty = have_qty + GetUnitCountEx(DRUID_TALON,only_done,townid)

    elseif unitid == DRUID_CLAW then
        set have_qty = have_qty + GetUnitCountEx(DRUID_CLAW_M,false,townid)
    elseif unitid == DRUID_CLAW_M then
        set have_qty = have_qty + GetUnitCountEx(DRUID_CLAW,only_done,townid)

    elseif unitid == ILLIDAN then
        set have_qty = have_qty + GetUnitCountEx(ILLIDAN_DEMON,false,townid)

    endif
    return have_qty
endfunction

//============================================================================
function TownCountDone takes integer base returns integer
    return TownCountEx(base,true,-1)
endfunction

//============================================================================
function TownCount takes integer base returns integer
    return TownCountEx(base,false,-1)
endfunction

//============================================================================
function BasicExpansion takes boolean build_it, integer unitid returns nothing
    if build_it and HallsCompleted(unitid) then
        call SetBuildExpa( TownCount(unitid)+1, unitid )
    endif
endfunction

//============================================================================
function UpgradeAll takes integer baseid, integer newid returns nothing
    call SetBuildUnit( TownCountDone(baseid), newid )
endfunction

//============================================================================
function TownCountTown takes integer base, integer townid returns integer
    return TownCountEx(base,false,townid)
endfunction

//============================================================================
//  FoodPool
//============================================================================
function FoodPool takes integer food, boolean weak, integer id1, integer use1, boolean strong, integer id2, integer use2 returns nothing
    if strong then
        call SetBuildUnit( (food - use1 * TownCount(id1)) / use2, id2 )
    elseif weak then
        call SetBuildUnit( (food - use2 * TownCount(id2)) / use1, id1 )
    endif
endfunction

//============================================================================
//  MeleeTownHall
//============================================================================
function MeleeTownHall takes integer townid, integer unitid returns nothing
    if TownHasMine(townid) and not TownHasHall(townid) then
        call SecondaryTown ( townid, 1, unitid )
    endif
endfunction

//============================================================================
function WaitForUnits takes integer unitid, integer qty returns nothing
    loop
        exitwhen TownCountDone(unitid) == qty
        call Sleep(2)
    endloop
endfunction

//============================================================================
function StartUnit takes integer ask_qty, integer unitid, integer town returns boolean
    local integer have_qty
    local integer need_qty
    local integer afford_gold
    local integer afford_wood
    local integer afford_qty
    local integer gold_cost
    local integer wood_cost

    //------------------------------------------------------------------------
    // if we have all we're asking for then make nothing
    //
    if town == -1 then
        set have_qty = TownCount(unitid)
    else
        set have_qty = TownCountTown(unitid,town)
    endif

    if have_qty >= ask_qty then
        return true
    endif
    set need_qty = ask_qty - have_qty

    //------------------------------------------------------------------------
    // limit the qty we're requesting to the amount of resources available
    //
    set gold_cost = GetUnitGoldCost(unitid)
    set wood_cost = GetUnitWoodCost(unitid)

    if gold_cost == 0 then
        set afford_gold = need_qty
    else
        set afford_gold = total_gold / gold_cost
    endif
    if afford_gold < need_qty then
        set afford_qty = afford_gold
    else
        set afford_qty = need_qty
    endif

    if wood_cost == 0 then
        set afford_wood = need_qty
    else
        set afford_wood = total_wood / wood_cost
    endif
    if afford_wood < afford_qty then
        set afford_qty = afford_wood
    endif

    // if we're waiting on gold/wood; pause build orders
    if afford_qty < 1 then
        return false
    endif

    //------------------------------------------------------------------------
    // whether we make right now what we're requesting or not, assume we will
    // and deduct the cost of the units from our fake gold total right away
    //
    set total_gold = total_gold - gold_cost * need_qty
    set total_wood = total_wood - wood_cost * need_qty

    if total_gold < 0 then
        set total_gold = 0
    endif
    if total_wood < 0 then
        set total_wood = 0
    endif

    //------------------------------------------------------------------------
    // give the AI a chance to make the units (it may not be able to right now
    // but that doesn't stop us from trying other units after this as long
    // as we have enough money to make this AND the needed, unbuilt ones)
    //
    return SetProduce(afford_qty,unitid,town)
endfunction

//============================================================================
function WaitForTown takes integer towns, integer townid returns nothing
    local integer i = 0
    loop
        call Sleep(10)
        exitwhen TownCount(townid) >= towns
        set i = i + 1
        exitwhen i == 12
    endloop
endfunction

//============================================================================
function StartExpansion takes integer qty, integer hall returns boolean
    local integer count
    local integer town
    local unit    peon
    local integer gold_cost

    set count = TownCount(hall)
    if count >= qty then
        return true
    endif

    set town = GetNextExpansion()
    if town == -1 then
        return true
    endif

    set take_exp = true

    set gold_cost = GetUnitGoldCost(hall)
    if gold_cost > total_gold then
        return false
    endif
    set total_gold = total_gold - gold_cost

    if GetExpansionFoe() != null then
        return true
    endif

    set peon = GetExpansionPeon()
    if peon != null then
        return SetExpansion(peon,hall)
    endif

    return true
endfunction

//============================================================================
function OneBuildLoop takes nothing returns nothing
    local integer index = 0
    local integer qty
    local integer id
    local integer tp

    set total_gold = GetGold() - gold_buffer
    set total_wood = GetWood()

    loop
        exitwhen index == build_length

        set qty = build_qty [index]
        set id  = build_item[index]
        set tp  = build_type[index]

        //--------------------------------------------------------------------
        if tp == BUILD_UNIT then
            if not StartUnit(qty,id,build_town[index]) then
                return
            endif

        //--------------------------------------------------------------------
        elseif tp == BUILD_UPGRADE then
            call StartUpgrade(qty,id)

        //--------------------------------------------------------------------
        else // tp == BUILD_EXPAND
            if not StartExpansion(qty,id) then
                return
            endif
        endif

        set index = index + 1
    endloop
endfunction

//============================================================================
function StaggerSleep takes real base, real spread returns nothing
    call Sleep(base + spread * I2R(GetAiPlayer()) / I2R(GetPlayers()))
endfunction

//============================================================================
function BuildLoop takes nothing returns nothing
    call OneBuildLoop()
    call StaggerSleep(1,2)
    loop
        call OneBuildLoop()
        call Sleep(2)
    endloop
endfunction

//============================================================================
function StartBuildLoop takes nothing returns nothing
    call StartThread(function BuildLoop)
endfunction

//============================================================================
function SetInitialWave takes integer seconds returns nothing
    set sleep_seconds = seconds
endfunction

//============================================================================
function AddSleepSeconds takes integer seconds returns nothing
    set sleep_seconds = sleep_seconds + seconds
endfunction

//============================================================================
function SleepForever takes nothing returns nothing
    call Trace("going to sleep forever\n") //xxx
    loop
        call Sleep(100)
    endloop
endfunction

//============================================================================
function PlayGame takes nothing returns nothing
    call StartBuildLoop()
    call SleepForever()
endfunction

//============================================================================
function ConvertNeeds takes integer unitid returns nothing
    if GetUnitCount(unitid) < 1 then
        call StartUnit(1,unitid,-1)
    endif
endfunction

//============================================================================
function Conversions takes integer desire, integer unitid returns nothing

    if GetUnitCount(unitid) >= desire then
        return
    endif

    if unitid == HIPPO_RIDER then
        call ConvertNeeds(ARCHER)
        call ConvertNeeds(HIPPO)
        call MergeUnits(desire,ARCHER,HIPPO,HIPPO_RIDER)

    elseif unitid == BLK_SPHINX then
        call ConvertNeeds(OBS_STATUE)
        call ConvertUnits(desire,OBS_STATUE)
    endif
endfunction

//============================================================================
function SetAssaultGroup takes integer qty, integer max, integer unitid returns nothing
    call Conversions(max,unitid)

    if qty <= 0 and TownCountDone(unitid) == 0 then
        return
    endif
    set harass_qty[harass_length] = qty
    set harass_max[harass_length] = max
    set harass_units[harass_length] = unitid
    set harass_length = harass_length + 1
endfunction

//============================================================================
function Interleave3 takes integer e1, integer m1, integer h1, integer u1, integer e2, integer m2, integer h2, integer u2, integer e3, integer m3, integer h3, integer u3 returns nothing
    local integer i1 = 1
    local integer i2 = 1
    local integer i3 = 1
    local integer q1
    local integer q2
    local integer q3

    if difficulty == EASY then
        set q1 = e1
        set q2 = e2
        set q3 = e3
    elseif difficulty == NORMAL then
        set q1 = m1
        set q2 = m2
        set q3 = m3
    else // difficulty == HARD
        set q1 = h1
        set q2 = h2
        set q3 = h3
    endif

    loop
        exitwhen q1<=0 and q2<=0 and q3<=0

        if q1 > 0 then
            call SetAssaultGroup(i1,i1,u1)
            set q1 = q1 - 1
            set i1 = i1 + 1
        endif

        if q2 > 0 then
            call SetAssaultGroup(i2,i2,u2)
            set q2 = q2 - 1
            set i2 = i2 + 1
        endif

        if q3 > 0 then
            call SetAssaultGroup(i3,i3,u3)
            set q3 = q3 - 1
            set i3 = i3 + 1
        endif
    endloop
endfunction

//============================================================================
function SetMeleeGroup takes integer unitid returns nothing
    if unitid == hero_id then
        call SetAssaultGroup(1,9,unitid)
    else
        call SetAssaultGroup((TownCountDone(unitid)*3)/4,20,unitid)
    endif
endfunction

//============================================================================
function CampaignDefender takes integer level, integer qty, integer unitid returns nothing
    if qty > 0 and difficulty >= level then
        set defense_qty[defense_length] = qty
        set defense_units[defense_length] = unitid
        set defense_length = defense_length + 1
        call Conversions(qty,unitid)
        call SetBuildUnit(qty,unitid)
    endif
endfunction

//============================================================================
function CampaignDefenderEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call CampaignDefender(EASY,easy,unitid)
    elseif difficulty == NORMAL then
        call CampaignDefender(NORMAL,med,unitid)
    else
        call CampaignDefender(HARD,hard,unitid)
    endif
endfunction

//============================================================================
function CampaignAttacker takes integer level, integer qty, integer unitid returns nothing
    if qty > 0 and difficulty >= level then
        call SetAssaultGroup(qty,qty,unitid)
    endif
endfunction

//============================================================================
function CampaignAttackerEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call CampaignAttacker(EASY,easy,unitid)
    elseif difficulty == NORMAL then
        call CampaignAttacker(NORMAL,med,unitid)
    else
        call CampaignAttacker(HARD,hard,unitid)
    endif
endfunction

//============================================================================
function FormGroup takes integer seconds, boolean testReady returns nothing
    local integer index
    local integer count
    local integer unitid
    local integer desire
    local integer readyPercent

    // normally test for CaptainReadiness() of 50%
    if testReady == true then
        set readyPercent = 50
        call Trace("forming group, requiring healthy guys\n") //xxx
    else
        set readyPercent = 0
        call Trace("forming group, unit health not important\n") //xxx
    endif

    call Trace("trying to gather forces\n") //xxx

    loop
        call SuicideSleep(seconds)
        call InitAssault()

        set index = 0
        loop
            exitwhen index == harass_length

            set unitid = harass_units[index]
            set desire = harass_max[index]
            set count  = TownCountDone(unitid)

            call Conversions(desire,unitid)

            if count >= desire then
                call AddAssault(desire,unitid)
            else
                set desire = harass_qty[index]

                if count < desire then
                    call AddAssault(desire,unitid)
                else
                    call AddAssault(count,unitid)
                endif
            endif

            set index = index + 1
        endloop

        //xxx
        if form_group_timeouts and (sleep_seconds < -60) then
            call Trace("exit form group -- timeout\n")
        elseif CaptainInCombat(true) then
            call Trace("exit form group -- can't form while already in combat\n")
        elseif CaptainIsFull() and CaptainReadiness() >= readyPercent then
            call Trace("exit form group -- ready\n")
        endif
        //xxx

        // time out and send group anyway if time has already expired
        exitwhen form_group_timeouts and (sleep_seconds < -60)
        exitwhen CaptainInCombat(true)
        exitwhen CaptainIsFull() and CaptainReadiness() >= readyPercent
    endloop
endfunction

//============================================================================
function WavePrepare takes integer unitid returns integer
    return GetUnitBuildTime(unitid)
endfunction

//============================================================================
function PrepTime takes nothing returns integer
    local integer unitid
    local integer missing
    local integer prep
    local integer count
    local integer largest = 30
    local integer index = 0

    loop
        exitwhen index == harass_length

        set unitid  = harass_units[index]
        set missing = harass_qty[index] + IgnoredUnits(unitid) - TownCount(unitid)
        set prep    = WavePrepare(unitid) * missing

        if prep > largest then
            set largest = prep
        endif

        set index = index + 1
    endloop
    call TraceI("next wave will require around %d seconds to build and gather\n",largest) //xxx

    return largest
endfunction

//============================================================================
function PrepSuicideOnPlayer takes integer seconds returns boolean
    local integer wave_prep   = PrepTime()
    local integer save_length

    set save_length = harass_length
    set harass_length = 0

    call AddSleepSeconds(seconds)
    if sleep_seconds-wave_prep > 0 then
        call TraceI("going to sleep for %d seconds before gathering next attack wave\n",sleep_seconds-wave_prep) //xxx
        call SuicideSleep(sleep_seconds-wave_prep)
    endif

    call Trace("preparing suicide attack wave\n") //xxx

    set harass_length = save_length
    if harass_length < 1 then
        call Trace("ERROR - no units specificed, exiting early\n") //xxx
        return false
    endif

    return true
endfunction

//============================================================================
function SleepUntilAtGoal takes nothing returns nothing
    loop
        exitwhen CaptainRetreating()
        exitwhen CaptainAtGoal()        // reached goal
        exitwhen CaptainIsHome()        // failed to path and returned home
        exitwhen CaptainIsEmpty()       // all units died
        call SuicideSleep(3)
    endloop
endfunction

//============================================================================
function SleepInCombat takes nothing returns nothing
    local integer count = 0
    debug call Trace("SleepInCombat\n")
    loop
        loop
            exitwhen not CaptainInCombat(true)  // goal is cleared
            exitwhen CaptainIsEmpty()           // duh
            call SuicideSleep(1)
        endloop

        set count = count + 1
        exitwhen count >= 8

        //xxx this is what it should have been; do this for next patch?
        //call SuicideSleep(1)
    endloop
    debug call Trace("exit SleepInCombat\n")
endfunction

//============================================================================
function AttackMoveXYA takes integer x, integer y returns nothing

    if zep_next_wave then
        call LoadZepWave(x,y)
        set zep_next_wave = false
    endif

    call AttackMoveXY(x,y)
    call SleepUntilAtGoal()
    call SleepInCombat()
endfunction

//============================================================================
function SuicideOnPlayerWave takes nothing returns nothing
    call Trace("waiting for attack wave to enter combat\n") //xxx
    loop
        //xxx
        if allow_signal_abort and CommandsWaiting() != 0 then
            call Trace("ABORT -- attack wave override\n")
        endif

        if CaptainInCombat(true) then
            call Trace("done - captain has entered combat\n")
        endif

        if CaptainIsEmpty() then
            call Trace("done - all units are dead\n")
        endif

        if sleep_seconds < -300 then
            call Trace("done - timeout, took too long to reach engage the enemy\n")
        endif
        //xxx

        exitwhen allow_signal_abort and CommandsWaiting() != 0

        exitwhen CaptainInCombat(true)
        exitwhen CaptainIsEmpty()
        call SuicideSleep(10)
        exitwhen sleep_seconds < -300
    endloop

    call Trace("waiting for attack wave to die\n") //xxx
    loop
        //xxx
        if allow_signal_abort and CommandsWaiting() != 0 then
            call Trace("ABORT - attack wave override\n")
        endif

        if CaptainIsEmpty() then
            call Trace("done - all units are dead\n")
        endif

        if sleep_seconds < -300 then
            call Trace("done - timeout, took too long to reach engage the enemy\n")
        endif
        //xxx

        exitwhen allow_signal_abort and CommandsWaiting() != 0

        exitwhen CaptainIsEmpty()
        call SuicideSleep(10)
        exitwhen sleep_seconds < -300
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function CommonSuicideOnPlayer takes boolean standard, boolean bldgs, integer seconds, player p, integer x, integer y returns nothing
    local integer save_peons

    if not PrepSuicideOnPlayer(seconds) then
        return
    endif

    set save_peons = campaign_wood_peons
    set campaign_wood_peons = 0

    loop
        //xxx
        if allow_signal_abort and CommandsWaiting() != 0 then
            call Trace("ABORT -- attack wave override\n")
        endif
        //xxx

        exitwhen allow_signal_abort and CommandsWaiting() != 0

        loop
            exitwhen allow_signal_abort and CommandsWaiting() != 0

            call FormGroup(5,true)
            exitwhen sleep_seconds <= 0
            call TraceI("waiting %d seconds before suicide\n",sleep_seconds) //xxx
        endloop

        if standard then
            if bldgs then
                exitwhen SuicidePlayer(p,sleep_seconds >= -60)
            else
                exitwhen SuicidePlayerUnits(p,sleep_seconds >= -60)
            endif
        else
            call AttackMoveXYA(x,y)
        endif

        call TraceI("waiting %d seconds before timeout\n",60+sleep_seconds) //xxx
        call SuicideSleep(5)
    endloop

    set campaign_wood_peons = save_peons
    set harass_length = 0

    call SuicideOnPlayerWave()
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPlayer takes integer seconds, player p returns nothing
    call CommonSuicideOnPlayer(true,true,seconds,p,0,0)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnUnits takes integer seconds, player p returns nothing
    call CommonSuicideOnPlayer(true,false,seconds,p,0,0)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPoint takes integer seconds, player p, integer x, integer y returns nothing
    call CommonSuicideOnPlayer(false,false,seconds,p,x,y)
endfunction

//============================================================================
function SuicideUntilSignal takes integer seconds, player p returns nothing
    local integer save
    local integer wave_prep = PrepTime()

    loop
        call AddSleepSeconds(seconds)
        if sleep_seconds-wave_prep > 0 then
            call SuicideSleep(sleep_seconds-wave_prep)
        endif

        set save = campaign_wood_peons
        set campaign_wood_peons = 0
        loop
            loop
                call FormGroup(5, true)
                exitwhen sleep_seconds <= 0
                exitwhen CommandsWaiting() != 0
            endloop
            exitwhen SuicidePlayer(p,sleep_seconds >= -60)
            exitwhen CommandsWaiting() != 0
            call SuicideSleep(3)
        endloop
        set campaign_wood_peons = save

        loop
            exitwhen CaptainIsEmpty()
            exitwhen CommandsWaiting() != 0
            call SuicideSleep(5)
        endloop
        exitwhen CommandsWaiting() != 0
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnce takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call SuicideUnit(easy,unitid)
    elseif difficulty == NORMAL then
        call SuicideUnit(med,unitid)
    else
        call SuicideUnit(hard,unitid)
    endif
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnitA takes integer unitid returns nothing
    if unitid != 0 then
        call SuicideUnit(1,unitid)
    endif
    call Sleep(0.1)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnitB takes integer unitid, integer playerid returns nothing
    if unitid != 0 then
        call SuicideUnitEx(1,unitid,playerid)
    endif
    call Sleep(0.1)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnits takes integer u1, integer u2, integer u3, integer u4, integer u5, integer u6, integer u7, integer u8, integer u9, integer uA returns nothing
    call Trace("MASS SUICIDE - this script is now technically done\n") //xxx

    call PrepFullSuicide()
    loop
        call SuicideUnitA(u1)
        call SuicideUnitA(u2)
        call SuicideUnitA(u3)
        call SuicideUnitA(u4)
        call SuicideUnitA(u5)
        call SuicideUnitA(u6)
        call SuicideUnitA(u7)
        call SuicideUnitA(u8)
        call SuicideUnitA(u9)
        call SuicideUnitA(uA)
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnitsEx takes integer playerid, integer u1, integer u2, integer u3, integer u4, integer u5, integer u6, integer u7, integer u8, integer u9, integer uA returns nothing
    call Trace("MASS SUICIDE - this script is now technically done\n") //xxx

    call PrepFullSuicide()
    loop
        call SuicideUnitB(u1,playerid)
        call SuicideUnitB(u2,playerid)
        call SuicideUnitB(u3,playerid)
        call SuicideUnitB(u4,playerid)
        call SuicideUnitB(u5,playerid)
        call SuicideUnitB(u6,playerid)
        call SuicideUnitB(u7,playerid)
        call SuicideUnitB(u8,playerid)
        call SuicideUnitB(u9,playerid)
        call SuicideUnitB(uA,playerid)
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPlayerEx takes integer easy, integer med, integer hard, player p returns nothing
    if difficulty == EASY then
        call SuicideOnPlayer(easy,p)
    elseif difficulty == NORMAL then
        call SuicideOnPlayer(med,p)
    else
        call SuicideOnPlayer(hard,p)
    endif
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnUnitsEx takes integer easy, integer med, integer hard, player p returns nothing
    if difficulty == EASY then
        call SuicideOnUnits(easy,p)
    elseif difficulty == NORMAL then
        call SuicideOnUnits(med,p)
    else
        call SuicideOnUnits(hard,p)
    endif
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPointEx takes integer easy, integer med, integer hard, player p, integer x, integer y returns nothing
    if difficulty == EASY then
        call SuicideOnPoint(easy,p,x,y)
    elseif difficulty == NORMAL then
        call SuicideOnPoint(med,p,x,y)
    else
        call SuicideOnPoint(hard,p,x,y)
    endif
endfunction

//============================================================================
function ForeverSuicideOnPlayer takes integer seconds, player p returns nothing
    local integer length = harass_length
    loop
        exitwhen allow_signal_abort and CommandsWaiting() != 0
        call SuicideOnPlayer(seconds,p)
        set harass_length = length
    endloop
endfunction

//============================================================================
function CommonSleepUntilTargetDead takes unit target, boolean reform returns nothing
    loop
        exitwhen CaptainRetreating()
        exitwhen CaptainReadinessHP() <= 40

        exitwhen not UnitAlive(target)
        exitwhen UnitInvis(target) and not IsUnitDetected(target,ai_player)

        if not TownThreatened() then
            call AttackMoveKill(target)
        endif

        call SuicideSleep(3)

        if reform and sleep_seconds < -40 then
            if CaptainInCombat(true) then
                set sleep_seconds = sleep_seconds + 5
            else
                set sleep_seconds = 0
                call FormGroup(1,false)
            endif
        endif
    endloop
endfunction

//============================================================================
function SleepUntilTargetDead takes unit target returns nothing
    call CommonSleepUntilTargetDead(target,false)
endfunction

//============================================================================
function ReformUntilTargetDead takes unit target returns nothing
    debug call Trace("ReformUntilTargetDead\n")
    call CommonSleepUntilTargetDead(target,true)
endfunction

//============================================================================
function AttackMoveKillA takes unit target returns nothing
    if target == null then
        call SuicideSleep(3)
        return
    endif

    debug call Trace("AttackMoveKillA\n")
    call AttackMoveKill(target)
    call ReformUntilTargetDead(target)
    call SleepInCombat()
endfunction

//============================================================================
function MinorCreepAttack takes nothing returns nothing
    local unit target = GetMinorCreep()
    call SetAllianceTarget(target)
    call FormGroup(3, true)
    call AttackMoveKillA(target)
endfunction

//============================================================================
function MajorCreepAttack takes nothing returns nothing
    local unit target = GetMajorCreep()
    call SetAllianceTarget(target)
    call FormGroup(3,true)
    call AttackMoveKillA(target)
endfunction

//============================================================================
function CreepAttackEx takes nothing returns nothing
    local unit target = GetCreepCamp(min_creeps,max_creeps,allow_air_creeps)
    call SetAllianceTarget(target)
    call FormGroup(3,true)
    call AttackMoveKillA(target)
endfunction

//============================================================================
function AnyPlayerAttack takes nothing returns nothing
    local unit hall

    set hall = GetEnemyExpansion()
    if hall == null then
        call StartGetEnemyBase()
        loop
            exitwhen not WaitGetEnemyBase()
            call SuicideSleep(1)
        endloop
        set hall = GetEnemyBase()
    endif

    call SetAllianceTarget(hall)
    call FormGroup(3,true)
    call AttackMoveKillA(hall)
endfunction

//============================================================================
function ExpansionAttack takes nothing returns nothing
    local unit creep = GetExpansionFoe()
    local integer x

    call FormGroup(3, true)
    if creep == null then
        set x = GetExpansionX()
        if x != -1 then
            call AttackMoveXYA(x,GetExpansionY())
        endif
    else
        call AttackMoveKillA(creep)
    endif
endfunction

//============================================================================
//  AddSiege
//============================================================================
function AddSiege takes nothing returns nothing
    call SetAssaultGroup( 0, 9, SHADE       )
    call SetAssaultGroup( 0, 9, MEAT_WAGON  )
    call SetAssaultGroup( 0, 9, MORTAR      )
    call SetAssaultGroup( 0, 9, TANK        )
    call SetAssaultGroup( 0, 9, BALLISTA    )
    call SetAssaultGroup( 0, 9, CATAPULT    )
endfunction

//===========================================================================
//  GetAllyCount
//============================================================================
function GetAllyCount takes player whichPlayer returns integer
    local integer    playerIndex = 0
    local integer    count = 0
    local player     indexPlayer

    loop
        set indexPlayer = Player(playerIndex)

        if whichPlayer != indexPlayer then
            if GetPlayerAlliance(whichPlayer,indexPlayer,ALLIANCE_PASSIVE) then
                if GetPlayerAlliance(indexPlayer,whichPlayer,ALLIANCE_PASSIVE) then
                    if GetPlayerStructureCount(indexPlayer,true) > 0 then
                        set count = count + 1
                    endif
                endif
            endif
        endif

        set playerIndex = playerIndex + 1
        exitwhen playerIndex == 12
    endloop

    return count
endfunction

//============================================================================
//  SingleMeleeAttack
//============================================================================
function SingleMeleeAttack takes boolean needs_exp, boolean has_siege, boolean major_ok, boolean air_units returns nothing
    local boolean   can_siege
    local real      daytime
    local unit      hall
    local unit      mega
    local unit      creep
    local unit      common
    local integer   minimum
    local boolean   allies

    call Trace("===SingleMeleeAttack===\n") //xxx

    if TownThreatened() then
        call Trace("sleep 2, town threatened\n") //xxx
        call Sleep(2)
        return
    endif

    // purchase zeppelins
    //
    if get_zeppelin and GetGold() > 300 and GetWood() > 100 then
        call Trace("purchase zep\n") //xxx
        call PurchaseZeppelin()
        set get_zeppelin = false
        set ready_for_zeppelin = false
        return
    endif
    set ready_for_zeppelin = true

    // coordinate with allies
    //
    set allies = GetAllyCount(ai_player) > 0
    if allies and MeleeDifficulty() != MELEE_NEWBIE then
        set common = GetAllianceTarget()
        if common != null then
            call Trace("join ally force\n") //xxx
            if GetMegaTarget() != null then
                call AddSiege()
            endif
            call FormGroup(3,true)
            call AttackMoveKillA(common)
            call SetAllianceTarget(null)
            return
        endif
    endif

    // take expansions as needed
    //
    if needs_exp then
        call Trace("needs exp\n") //xxx
        set creep = GetExpansionFoe()
        if creep != null then
            call Trace("attack exp\n") //xxx
            call SetAllianceTarget(creep)
            call FormGroup(3,true)
            call AttackMoveKillA(creep)
            call Sleep(20)
            set take_exp = false
            return
        endif
    endif

    // all-out attack if the player is weak
    //
    if MeleeDifficulty() != MELEE_NEWBIE then
        set mega = GetMegaTarget()
        if mega != null then
            call Trace("MEGA TARGET!!!\n") //xxx
            call AddSiege()
            call FormGroup(3,true)
            call AttackMoveKillA(mega)
            return
        endif
    endif

    // deny player an expansion
    //
    set hall = GetEnemyExpansion()
    set daytime = GetFloatGameState(GAME_STATE_TIME_OF_DAY)
    set can_siege = has_siege and (air_units or (daytime>=4 and daytime<=12))

    if hall!=null and (can_siege or not IsTowered(hall)) then

        call Trace("test player town attack\n") //xxx

        if MeleeDifficulty() == MELEE_NEWBIE then
            set minimum = 3
        elseif allies and MeleeDifficulty() == MELEE_NORMAL then
            set minimum = 1
        else
            set minimum = 0 // HARD, INSANE, and NORMAL with no allies
        endif

        if exp_seen >= minimum then
            call Trace("do player town attack\n") //xxx
            set exp_seen = 0
            call AddSiege()
            call SetAllianceTarget(hall)
            call FormGroup(3,true)
            call AttackMoveKillA(hall)
            return
        endif

        set exp_seen = exp_seen + 1
    endif

    // attack player's main base when siege is available
    //
    if can_siege then
        call Trace("attack player's town\n") //xxx
        call AddSiege()
        call AnyPlayerAttack()
        return
    endif

    // extended, more specific method of determining creep levels
    //
    if min_creeps != -1 then
        call TraceI("custom creep attack %d\n",max_creeps) //xxx
        call CreepAttackEx()
        return
    endif

    // nothing better to do, so kill a creep camp
    //
    if major_ok then
        call Trace("major creep attack\n") //xxx
        call MajorCreepAttack()
        return
    endif

    call Trace("minor creep attack\n") //xxx
    call MinorCreepAttack()
endfunction

//============================================================================
function GetZeppelin takes nothing returns nothing
    if ready_for_zeppelin then
        set get_zeppelin = true
    endif
endfunction

//============================================================================
function FoodUsed takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_FOOD_USED)
endfunction

//============================================================================
function FoodCap takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_FOOD_CAP)
endfunction

//============================================================================
function FoodSpace takes nothing returns integer
    return FoodCap() - FoodUsed()
endfunction

//============================================================================
function FoodAvail takes integer base returns integer
    return GetFoodMade(racial_farm) * TownCount(racial_farm) + GetFoodMade(base) * TownCount(base)
endfunction

//============================================================================
function BuildAttackers takes nothing returns nothing
    local integer index = 0
    local integer unitid
    local integer desire
    local integer count

    loop
        exitwhen index == harass_length

        set unitid = harass_units[index]
        set desire = harass_qty[index] + IgnoredUnits(unitid)
        set count  = TownCount(unitid)

        if count != desire then
            if not StartUnit(desire,unitid,-1) then
                return
            endif
        endif

        set index = index + 1
    endloop
endfunction

//============================================================================
function BuildDefenders takes nothing returns nothing
    local integer index = 0
    local integer unitid
    local integer qty
    loop
        exitwhen index == defense_length

        set unitid = defense_units[index]
        set qty = defense_qty[index]

        call Conversions(qty,unitid)
        call AddDefenders(qty,unitid)

        set index = index + 1
    endloop
endfunction

//============================================================================
function CampaignBasicsA takes nothing returns nothing
    local integer food_each = GetFoodMade(racial_farm)
    local integer on_wood

    call ClearHarvestAI()

    if CaptainInCombat(false) then
        set on_wood = 0
    else
        set on_wood = campaign_wood_peons
    endif

    call HarvestGold(0,campaign_gold_peons)
    call HarvestWood(0,on_wood)

    if harvest_town1 then
        call HarvestGold(1,campaign_gold_peons)
        call HarvestWood(1,on_wood)
    endif

    if harvest_town2 then
        call HarvestGold(2,campaign_gold_peons)
        call HarvestWood(2,on_wood)
    endif

    if harvest_town3 then
        call HarvestGold(3,campaign_gold_peons)
        call HarvestWood(3,on_wood)
    endif

    if do_campaign_farms and FoodUsed()+food_each-1 > food_each*(TownCount(racial_farm)+1) then
        call StartUnit(TownCount(racial_farm)+1,racial_farm,-1)
    endif

    if build_campaign_attackers then
        call BuildAttackers()
    endif

    if not CaptainInCombat(false) then
        call BuildDefenders()
    endif

    call FillGuardPosts()
    call ReturnGuardPosts()
endfunction

//============================================================================
function CampaignBasics takes nothing returns nothing
    call Sleep(1)
    call CampaignBasicsA()
    call StaggerSleep(1,5)
    loop
        call CampaignBasicsA()
        call Sleep(campaign_basics_speed)
    endloop
endfunction

//============================================================================
function CampaignAI takes integer farms, code heroes returns nothing
    if GetGameDifficulty() == MAP_DIFFICULTY_EASY then
        set difficulty = EASY

        call SetTargetHeroes(false)
        call SetUnitsFlee(false)

    elseif GetGameDifficulty() == MAP_DIFFICULTY_NORMAL then
        set difficulty = NORMAL

        call SetTargetHeroes(false)
        call SetUnitsFlee(false)

    elseif GetGameDifficulty() == MAP_DIFFICULTY_HARD then
        set difficulty = HARD

        call SetPeonsRepair(true)
    else
        set difficulty = INSANE
    endif

    call InitAI()
    call InitBuildArray()
    call InitAssaultGroup()
    call CreateCaptains()

    call SetNewHeroes(false)
    if heroes != null then
        call SetHeroLevels(heroes)
    endif

    call SetHeroesFlee(false)
    call SetGroupsFlee(false)
    call SetSlowChopping(true)
    call GroupTimedLife(false)
    call SetCampaignAI()
    call Sleep(0.1)

    set racial_farm = farms
    call StartThread(function CampaignBasics)
    call StartBuildLoop()
endfunction

//============================================================================
function UnsummonAll takes nothing returns nothing
    local unit bldg
    loop
        set bldg = GetBuilding(ai_player)
        exitwhen bldg==null
        call Unsummon(bldg)
        call Sleep(2)
    endloop
endfunction

//============================================================================
//  SkillArrays
//============================================================================
function SkillArrays takes nothing returns integer
    local integer level = GetHeroLevelAI()

    if level > max_hero_level then
        set max_hero_level = level
    endif

    if GetHeroId() == hero_id then
        return skills1[level]
    elseif GetHeroId() == hero_id2 then
        return skills2[level]
    else
        return skills3[level]
    endif
endfunction

//--------------------------------------------------------------------------------------------------
//  SetSkillArray
//--------------------------------------------------------------------------------------------------
function SetSkillArray takes integer index, integer id returns nothing
    local integer i = 1

    if index == 1 then
        if hero_id != id then
            return
        endif
        loop
            set skills1[i] = skill[i]
            exitwhen i == 10
            set i = i + 1
        endloop
    elseif index == 2 then
        if hero_id2 != id then
            return
        endif
        loop
            set skills2[i] = skill[i]
            exitwhen i == 10
            set i = i + 1
        endloop
    else
        if hero_id3 != id then
            return
        endif
        loop
            set skills3[i] = skill[i]
            exitwhen i == 10
            set i = i + 1
        endloop
    endif
endfunction

//============================================================================
//  AwaitMeleeHeroes
//============================================================================
function AwaitMeleeHeroes takes nothing returns nothing
    if GetUnitCountDone(hero_id2) > 0 then
        set two_heroes = true
    endif
    loop
        exitwhen GetUnitCountDone(hero_id)>0 and (take_exp or (not two_heroes or GetUnitCountDone(hero_id2)>0))
        call Sleep(1)
    endloop
endfunction

//============================================================================
//  PickMeleeHero
//============================================================================
function PickMeleeHero takes race raceid returns integer
    local integer first
    local integer second
    local integer third
    local integer last
    local integer array heroes

    //------------------------------------------------------------------------
    if raceid == RACE_HUMAN then
    //------------------------------------------------------------------------
        set heroes[1] = ARCHMAGE
        set heroes[2] = MTN_KING
        set heroes[3] = PALADIN
        set heroes[4] = BLOOD_MAGE

    //------------------------------------------------------------------------
    elseif raceid == RACE_ORC then
    //------------------------------------------------------------------------
        set heroes[1] = BLADE_MASTER
        set heroes[2] = FAR_SEER
        set heroes[3] = TAUREN_CHIEF
        set heroes[4] = SHADOW_HUNTER

    //------------------------------------------------------------------------
    elseif raceid == RACE_NIGHTELF then
    //------------------------------------------------------------------------
        set heroes[1] = DEMON_HUNTER
        set heroes[2] = KEEPER
        set heroes[3] = MOON_BABE
        set heroes[4] = WARDEN

    //------------------------------------------------------------------------
    elseif raceid == RACE_UNDEAD then
    //------------------------------------------------------------------------
        set heroes[1] = DEATH_KNIGHT
        set heroes[2] = DREAD_LORD
        set heroes[3] = LICH
        set heroes[4] = CRYPT_LORD
    else
        set hero_id = 0
    endif

    if VersionCompatible(VERSION_FROZEN_THRONE) then
        set last = 4
    else
        set last = 3
    endif

    set first  = GetRandomInt(1,last)
    set second = GetRandomInt(1,last-1)
    set third  = GetRandomInt(1,last-2)

    set hero_id        = heroes[first]
    set heroes[first]  = heroes[last]
    set hero_id2       = heroes[second]
    set heroes[second] = heroes[last-1]
    set hero_id3       = heroes[third]

    return hero_id
endfunction

This file contains all the main routines, both for melee AIs and campaign AIs. Some are easy to understand, some are not. They all end in the so-called natives, code which we cannot see nor edit (they are just calls to internal engine code). You can find this file in the scripts folder of your Warcraft 3 root directory using Ladik's Casc Viewer.
Since I was always satisfied with default AI's defensive behaviour, I concentrate myself on offensive behaviours, specifically, the attack waves routines.
After around a week of development and another week of testing, I came up with a new set of routines, additional to and fully compatible with the default one, to greatly enhance the customization power of my AIs... and their overall attack intelligence.

They are all collected in what I call common AI 2.0 file, which is what I am sharing with this system. After over 18 years, the common.ai file is finally updated from v1.68 to v2.0, and I belive it will be very useful to the community, especially to single player maps/campaigns makers. All the new functions use the prefix "Adv", standing for Advanced. This is done to easily tell them apart the vanilla routines. You can find this file in the code section of this system.

Consider it a retrocompatible and additive update to default routines. Among the other things, it will allow your AI players to target specific locations or unit types in an intelligent way, while also being able to continue the attack wave towards nearby or further away enemies. All of that, and much more, fully configurable.

To import the new AI routines into your map is very easy and you just need to do the following:
  1. Download the common_v2.ai file attached to this system (or just make your own .ai using the script in the code section, it's just a text file!), which contains all the advanced routines
  2. Import this file into your map using the Asset Manager
  3. Change the file path to scripts\common.ai
By doing so, each .ai script will have the new routines included, so you can use the new routines in all AI scripts for that specific map.
You can also check out the in-depth tutorial at the third page to learn how the system works and cover how each routine works.

Finally, please note that this system was only tested on 1.32, but it should be compatible with 1.31.x versions of the game!


JASS:
//==================================================================================================
//  $Id: common.ai, v 2.0 2021/06/04 12:00:00 - InsaneMonster (Luca Pasqualini) $
//==================================================================================================
native DebugS               takes string str                            returns nothing
native DebugFI              takes string str, integer val               returns nothing
native DebugUnitID          takes string str, integer val               returns nothing
native DisplayText          takes integer p, string str                 returns nothing
native DisplayTextI         takes integer p, string str, integer val    returns nothing
native DisplayTextII        takes integer p, string str, integer v1, integer v2 returns nothing
native DisplayTextIII       takes integer p, string str, integer v1, integer v2, integer v3 returns nothing
native DoAiScriptDebug      takes nothing                               returns boolean

native GetAiPlayer          takes nothing                               returns integer
native GetHeroId            takes nothing                               returns integer
native GetHeroLevelAI       takes nothing                               returns integer

native GetUnitCount         takes integer unitid                        returns integer
native GetPlayerUnitTypeCount takes player p, integer unitid            returns integer
native GetUnitCountDone     takes integer unitid                        returns integer
native GetTownUnitCount     takes integer id, integer tn, boolean dn    returns integer
native GetUnitGoldCost      takes integer unitid                        returns integer
native GetUnitWoodCost      takes integer unitid                        returns integer
native GetUnitBuildTime     takes integer unitid                        returns integer

native GetMinesOwned        takes nothing                               returns integer
native GetGoldOwned         takes nothing                               returns integer
native TownWithMine         takes nothing                               returns integer
native TownHasMine          takes integer townid                        returns boolean
native TownHasHall          takes integer townid                        returns boolean

native GetUpgradeLevel      takes integer id                            returns integer
native GetUpgradeGoldCost   takes integer id                            returns integer
native GetUpgradeWoodCost   takes integer id                            returns integer
native GetNextExpansion     takes nothing                               returns integer
native GetMegaTarget        takes nothing                               returns unit
native GetBuilding          takes player p                              returns unit
native GetEnemyPower        takes nothing                               returns integer
native SetAllianceTarget    takes unit id                               returns nothing
native GetAllianceTarget    takes nothing                               returns unit

native SetProduce           takes integer qty, integer id, integer town returns boolean
native Unsummon             takes unit unitid                           returns nothing
native SetExpansion         takes unit peon, integer id                 returns boolean
native SetUpgrade           takes integer id                            returns boolean
native SetHeroLevels        takes code func                             returns nothing
native SetNewHeroes         takes boolean state                         returns nothing
native PurchaseZeppelin     takes nothing                               returns nothing

native MergeUnits           takes integer qty, integer a, integer b, integer make returns boolean
native ConvertUnits         takes integer qty, integer id               returns boolean

native SetCampaignAI        takes nothing                               returns nothing
native SetMeleeAI           takes nothing                               returns nothing
native SetTargetHeroes      takes boolean state                         returns nothing
native SetPeonsRepair       takes boolean state                         returns nothing
native SetRandomPaths       takes boolean state                         returns nothing
native SetDefendPlayer      takes boolean state                         returns nothing
native SetHeroesFlee        takes boolean state                         returns nothing
native SetHeroesBuyItems    takes boolean state                         returns nothing
native SetWatchMegaTargets  takes boolean state                         returns nothing
native SetIgnoreInjured     takes boolean state                         returns nothing
native SetHeroesTakeItems   takes boolean state                         returns nothing
native SetUnitsFlee         takes boolean state                         returns nothing
native SetGroupsFlee        takes boolean state                         returns nothing
native SetSlowChopping      takes boolean state                         returns nothing
native SetCaptainChanges    takes boolean allow                         returns nothing
native SetSmartArtillery    takes boolean state                         returns nothing
native SetReplacementCount  takes integer qty                           returns nothing
native GroupTimedLife       takes boolean allow                         returns nothing
native RemoveInjuries       takes nothing                               returns nothing
native RemoveSiege          takes nothing                               returns nothing

native InitAssault          takes nothing                               returns nothing
native AddAssault           takes integer qty, integer id               returns boolean
native AddDefenders         takes integer qty, integer id               returns boolean

native GetCreepCamp         takes integer min, integer max, boolean flyers_ok returns unit

native StartGetEnemyBase    takes nothing                               returns nothing
native WaitGetEnemyBase     takes nothing                               returns boolean
native GetEnemyBase         takes nothing                               returns unit
native GetExpansionFoe      takes nothing                               returns unit
native GetEnemyExpansion    takes nothing                               returns unit
native GetExpansionX        takes nothing                               returns integer
native GetExpansionY        takes nothing                               returns integer
native SetStagePoint        takes real x, real y                        returns nothing
native AttackMoveKill       takes unit target                           returns nothing
native AttackMoveXY         takes integer x, integer y                  returns nothing
native LoadZepWave          takes integer x, integer y                  returns nothing
native SuicidePlayer        takes player id, boolean check_full         returns boolean
native SuicidePlayerUnits   takes player id, boolean check_full         returns boolean
native CaptainInCombat      takes boolean attack_captain                returns boolean
native IsTowered            takes unit target                           returns boolean

native ClearHarvestAI       takes nothing                               returns nothing
native HarvestGold          takes integer town, integer peons           returns nothing
native HarvestWood          takes integer town, integer peons           returns nothing
native GetExpansionPeon     takes nothing                               returns unit

native StopGathering        takes nothing                               returns nothing
native AddGuardPost         takes integer id, real x, real y            returns nothing
native FillGuardPosts       takes nothing                               returns nothing
native ReturnGuardPosts     takes nothing                               returns nothing
native CreateCaptains       takes nothing                               returns nothing
native SetCaptainHome       takes integer which, real x, real y         returns nothing
native ResetCaptainLocs     takes nothing                               returns nothing
native ShiftTownSpot        takes real x, real y                        returns nothing
native TeleportCaptain      takes real x, real y                        returns nothing
native ClearCaptainTargets  takes nothing                               returns nothing
native CaptainAttack        takes real x, real y                        returns nothing
native CaptainVsUnits       takes player id                             returns nothing
native CaptainVsPlayer      takes player id                             returns nothing
native CaptainGoHome        takes nothing                               returns nothing
native CaptainIsHome        takes nothing                               returns boolean
native CaptainIsFull        takes nothing                               returns boolean
native CaptainIsEmpty       takes nothing                               returns boolean
native CaptainGroupSize     takes nothing                               returns integer
native CaptainReadiness     takes nothing                               returns integer
native CaptainRetreating    takes nothing                               returns boolean
native CaptainReadinessHP   takes nothing                               returns integer
native CaptainReadinessMa   takes nothing                               returns integer
native CaptainAtGoal        takes nothing                               returns boolean
native CreepsOnMap          takes nothing                               returns boolean
native SuicideUnit          takes integer count, integer unitid         returns nothing
native SuicideUnitEx        takes integer ct, integer uid, integer pid  returns nothing
native StartThread          takes code func                             returns nothing
native Sleep                takes real seconds                          returns nothing
native UnitAlive            takes unit id                               returns boolean
native UnitInvis            takes unit id                               returns boolean
native IgnoredUnits         takes integer unitid                        returns integer
native TownThreatened       takes nothing                               returns boolean
native DisablePathing       takes nothing                               returns nothing
native SetAmphibious        takes nothing                               returns nothing

native CommandsWaiting      takes nothing                               returns integer
native GetLastCommand       takes nothing                               returns integer
native GetLastData          takes nothing                               returns integer
native PopLastCommand       takes nothing                               returns nothing
native MeleeDifficulty      takes nothing                               returns integer

//============================================================================
//  Globals for all AI scripts
//============================================================================
globals

    //--------------------------------------------------------------------
    //  HUMANS
    //--------------------------------------------------------------------

    // human heroes
    constant integer ARCHMAGE           = 'Hamg'
    constant integer PALADIN            = 'Hpal'
    constant integer MTN_KING           = 'Hmkg'
    constant integer BLOOD_MAGE         = 'Hblm'

    // human hero abilities
    constant integer AVATAR             = 'AHav'
    constant integer BASH               = 'AHbh'
    constant integer THUNDER_BOLT       = 'AHtb'
    constant integer THUNDER_CLAP       = 'AHtc'

    constant integer DEVOTION_AURA      = 'AHad'
    constant integer DIVINE_SHIELD      = 'AHds'
    constant integer HOLY_BOLT          = 'AHhb'
    constant integer RESURRECTION       = 'AHre'

    constant integer BLIZZARD           = 'AHbz'
    constant integer BRILLIANCE_AURA    = 'AHab'
    constant integer MASS_TELEPORT      = 'AHmt'
    constant integer WATER_ELEMENTAL    = 'AHwe'

    constant integer BANISH             = 'AHbn'
    constant integer FLAME_STRIKE       = 'AHfs'
    constant integer SUMMON_PHOENIX     = 'AHpx'
    constant integer SIPHON_MANA        = 'AHdr'

    // special human heroes
    constant integer JAINA              = 'Hjai'
    constant integer MURADIN            = 'Hmbr'
    constant integer GARITHOS           = 'Hlgr'
    constant integer KAEL               = 'Hkal'

    // human units
    constant integer COPTER             = 'hgyr'
    constant integer GYRO               =  COPTER
    constant integer ELEMENTAL          = 'hwat'
    constant integer FOOTMAN            = 'hfoo'
    constant integer FOOTMEN            =  FOOTMAN
    constant integer GRYPHON            = 'hgry'
    constant integer KNIGHT             = 'hkni'
    constant integer MORTAR             = 'hmtm'
    constant integer PEASANT            = 'hpea'
    constant integer PRIEST             = 'hmpr'
    constant integer RIFLEMAN           = 'hrif'
    constant integer RIFLEMEN           =  RIFLEMAN
    constant integer SORCERESS          = 'hsor'
    constant integer TANK               = 'hmtt'
    constant integer STEAM_TANK         =  TANK
    constant integer ROCKET_TANK        = 'hrtt'
    constant integer MILITIA            = 'hmil'
    constant integer SPELL_BREAKER      = 'hspt'
    constant integer HUMAN_DRAGON_HAWK  = 'hdhw'

    // special human units
    constant integer BLOOD_PRIEST       = 'hbep'
    constant integer BLOOD_SORCERESS    = 'hbes'
    constant integer BLOOD_PEASANT      = 'nhew'

    // human buildings
    constant integer AVIARY             = 'hgra'
    constant integer BARRACKS           = 'hbar'
    constant integer BLACKSMITH         = 'hbla'
    constant integer CANNON_TOWER       = 'hctw'
    constant integer CASTLE             = 'hcas'
    constant integer CHURCH             = 'htws'
    constant integer MAGE_TOWER         =  CHURCH
    constant integer GUARD_TOWER        = 'hgtw'
    constant integer HOUSE              = 'hhou'
    constant integer HUMAN_ALTAR        = 'halt'
    constant integer KEEP               = 'hkee'
    constant integer LUMBER_MILL        = 'hlum'
    constant integer SANCTUM            = 'hars'
    constant integer ARCANE_SANCTUM     =  SANCTUM
    constant integer TOWN_HALL          = 'htow'
    constant integer WATCH_TOWER        = 'hwtw'
    constant integer WORKSHOP           = 'harm'
    constant integer ARCANE_VAULT       = 'hvlt'
    constant integer ARCANE_TOWER       = 'hatw'

    // human upgrades
    constant integer UPG_MELEE          = 'Rhme'
    constant integer UPG_RANGED         = 'Rhra'
    constant integer UPG_ARTILLERY      = 'Rhaa'
    constant integer UPG_ARMOR          = 'Rhar'
    constant integer UPG_GOLD           = 'Rhmi'
    constant integer UPG_MASONRY        = 'Rhac'
    constant integer UPG_SIGHT          = 'Rhss'
    constant integer UPG_DEFEND         = 'Rhde'
    constant integer UPG_BREEDING       = 'Rhan'
    constant integer UPG_PRAYING        = 'Rhpt'
    constant integer UPG_SORCERY        = 'Rhst'
    constant integer UPG_LEATHER        = 'Rhla'
    constant integer UPG_GUN_RANGE      = 'Rhri'
    constant integer UPG_WOOD           = 'Rhlh'
    constant integer UPG_SENTINEL       = 'Rhse'
    constant integer UPG_SCATTER        = 'Rhsr'
    constant integer UPG_BOMBS          = 'Rhgb'
    constant integer UPG_HAMMERS        = 'Rhhb'
    constant integer UPG_CONT_MAGIC     = 'Rhss'
    constant integer UPG_FRAGS          = 'Rhfs'
    constant integer UPG_TANK           = 'Rhrt'
    constant integer UPG_FLAK           = 'Rhfc'
    constant integer UPG_CLOUD          = 'Rhcd'

    //--------------------------------------------------------------------
    //  ORCS
    //--------------------------------------------------------------------

    // orc heroes
    constant integer BLADE_MASTER       = 'Obla'
    constant integer FAR_SEER           = 'Ofar'
    constant integer TAUREN_CHIEF       = 'Otch'
    constant integer SHADOW_HUNTER      = 'Oshd'

    // special orc heroes
    constant integer GROM               = 'Ogrh'
    constant integer THRALL             = 'Othr'

    // orc hero abilities
    constant integer CRITICAL_STRIKE    = 'AOcr'
    constant integer MIRROR_IMAGE       = 'AOmi'
    constant integer BLADE_STORM        = 'AOww'
    constant integer WIND_WALK          = 'AOwk'

    constant integer CHAIN_LIGHTNING    = 'AOcl'
    constant integer EARTHQUAKE         = 'AOeq'
    constant integer FAR_SIGHT          = 'AOfs'
    constant integer SPIRIT_WOLF        = 'AOsf'

    constant integer ENDURANE_AURA      = 'AOae'
    constant integer REINCARNATION      = 'AOre'
    constant integer SHOCKWAVE          = 'AOsh'
    constant integer WAR_STOMP          = 'AOws'

    constant integer HEALING_WAVE       = 'AOhw'
    constant integer HEX                = 'AOhx'
    constant integer SERPENT_WARD       = 'AOsw'
    constant integer VOODOO             = 'AOvd'

    // orc units
    constant integer GUARDIAN           = 'oang'
    constant integer CATAPULT           = 'ocat'
    constant integer WITCH_DOCTOR       = 'odoc'
    constant integer GRUNT              = 'ogru'
    constant integer HEAD_HUNTER        = 'ohun'
    constant integer BERSERKER          = 'otbk'
    constant integer KODO_BEAST         = 'okod'
    constant integer PEON               = 'opeo'
    constant integer RAIDER             = 'orai'
    constant integer SHAMAN             = 'oshm'
    constant integer TAUREN             = 'otau'
    constant integer WYVERN             = 'owyv'
    constant integer BATRIDER           = 'otbr'
    constant integer SPIRIT_WALKER      = 'ospw'
    constant integer SPIRIT_WALKER_M    = 'ospm'

    // orc buildings
    constant integer ORC_ALTAR          = 'oalt'
    constant integer ORC_BARRACKS       = 'obar'
    constant integer BESTIARY           = 'obea'
    constant integer FORGE              = 'ofor'
    constant integer FORTRESS           = 'ofrt'
    constant integer GREAT_HALL         = 'ogre'
    constant integer LODGE              = 'osld'
    constant integer STRONGHOLD         = 'ostr'
    constant integer BURROW             = 'otrb'
    constant integer TOTEM              = 'otto'
    constant integer ORC_WATCH_TOWER    = 'owtw'
    constant integer VOODOO_LOUNGE      = 'ovln'

    // orc upgrades
    constant integer UPG_ORC_MELEE      = 'Rome'
    constant integer UPG_ORC_RANGED     = 'Rora'
    constant integer UPG_ORC_ARTILLERY  = 'Roaa'
    constant integer UPG_ORC_ARMOR      = 'Roar'
    constant integer UPG_ORC_WAR_DRUMS  = 'Rwdm'
    constant integer UPG_ORC_PILLAGE    = 'Ropg'
    constant integer UPG_ORC_BERSERK    = 'Robs'
    constant integer UPG_ORC_PULVERIZE  = 'Rows'
    constant integer UPG_ORC_ENSNARE    = 'Roen'
    constant integer UPG_ORC_VENOM      = 'Rovs'
    constant integer UPG_ORC_DOCS       = 'Rowd'
    constant integer UPG_ORC_SHAMAN     = 'Rost'
    constant integer UPG_ORC_SPIKES     = 'Rosp'
    constant integer UPG_ORC_BURROWS    = 'Rorb'
    constant integer UPG_ORC_REGEN      = 'Rotr'
    constant integer UPG_ORC_FIRE       = 'Rolf'
    constant integer UPG_ORC_SWALKER    = 'Rowt'
    constant integer UPG_ORC_BERSERKER  = 'Robk'
    constant integer UPG_ORC_NAPTHA     = 'Robf'
    constant integer UPG_ORC_CHAOS      = 'Roch'

    // Warcraft 2 orc units
    constant integer OGRE_MAGI          = 'nomg'
    constant integer ORC_DRAGON         = 'nrwm'
    constant integer SAPPER             = 'ngsp'
    constant integer ZEPPLIN            = 'nzep'
    constant integer ZEPPELIN           =  ZEPPLIN
    constant integer W2_WARLOCK         = 'nw2w'
    constant integer PIG_FARM           = 'npgf'
    constant integer FOREST_TROLL       = 'nftr'

    // special orc units
    constant integer CHAOS_GRUNT        = 'nchg'
    constant integer CHAOS_WARLOCK      = 'nchw'
    constant integer CHAOS_RAIDER       = 'nchr'
    constant integer CHAOS_PEON         = 'ncpn'
    constant integer CHAOS_KODO         = 'nckb'
    constant integer CHAOS_GROM         = 'Opgh'
    constant integer CHAOS_BLADEMASTER  = 'Nbbc'
    constant integer CHAOS_BURROW       = 'ocbw'

    //--------------------------------------------------------------------
    //  UNDEAD
    //--------------------------------------------------------------------

    // undead heroes
    constant integer DEATH_KNIGHT       = 'Udea'
    constant integer DREAD_LORD         = 'Udre'
    constant integer LICH               = 'Ulic'
    constant integer CRYPT_LORD         = 'Ucrl'

    // special undead heroes
    constant integer MALGANIS           = 'Umal'
    constant integer TICHONDRIUS        = 'Utic'
    constant integer PIT_LORD           = 'Npld'
    constant integer DETHEROC           = 'Udth'

    // undead hero abilities
    constant integer SLEEP              = 'AUsl'
    constant integer VAMP_AURA          = 'AUav'
    constant integer CARRION_SWARM      = 'AUcs'
    constant integer INFERNO            = 'AUin'

    constant integer DARK_RITUAL        = 'AUdr'
    constant integer DEATH_DECAY        = 'AUdd'
    constant integer FROST_ARMOR        = 'AUfu'
    constant integer FROST_NOVA         = 'AUfn'

    constant integer ANIM_DEAD          = 'AUan'
    constant integer DEATH_COIL         = 'AUdc'
    constant integer DEATH_PACT         = 'AUdp'
    constant integer UNHOLY_AURA        = 'AUau'

    constant integer CARRION_SCARAB     = 'AUcb'
    constant integer IMPALE             = 'AUim'
    constant integer LOCUST_SWARM       = 'AUls'
    constant integer THORNY_SHIELD      = 'AUts'

    // undead units
    constant integer ABOMINATION        = 'uabo'
    constant integer ACOLYTE            = 'uaco'
    constant integer BANSHEE            = 'uban'
    constant integer PIT_FIEND          = 'ucry'
    constant integer CRYPT_FIEND        =  PIT_FIEND
    constant integer FROST_WYRM         = 'ufro'
    constant integer GARGOYLE           = 'ugar'
    constant integer GARGOYLE_MORPH     = 'ugrm'
    constant integer GHOUL              = 'ugho'
    constant integer MEAT_WAGON         = 'umtw'
    constant integer NECRO              = 'unec'
    constant integer SKEL_WARRIOR       = 'uske'
    constant integer SHADE              = 'ushd'
    constant integer UNDEAD_BARGE       = 'uarb'
    constant integer OBSIDIAN_STATUE    = 'uobs'
    constant integer OBS_STATUE         =  OBSIDIAN_STATUE
    constant integer BLK_SPHINX         = 'ubsp'

    // undead buildings
    constant integer UNDEAD_MINE        = 'ugol'
    constant integer UNDEAD_ALTAR       = 'uaod'
    constant integer BONEYARD           = 'ubon'
    constant integer GARG_SPIRE         = 'ugsp'
    constant integer NECROPOLIS_1       = 'unpl'    // normal
    constant integer NECROPOLIS_2       = 'unp1'    // upgraded once
    constant integer NECROPOLIS_3       = 'unp2'    // full upgrade
    constant integer SAC_PIT            = 'usap'
    constant integer CRYPT              = 'usep'
    constant integer SLAUGHTERHOUSE     = 'uslh'
    constant integer DAMNED_TEMPLE      = 'utod'
    constant integer ZIGGURAT_1         = 'uzig'    // normal
    constant integer ZIGGURAT_2         = 'uzg1'    // upgraded
    constant integer ZIGGURAT_FROST     = 'uzg2'    // frost tower
    constant integer GRAVEYARD          = 'ugrv'
    constant integer TOMB_OF_RELICS     = 'utom'

    // undead upgrades
    constant integer UPG_UNHOLY_STR     = 'Rume'
    constant integer UPG_CR_ATTACK      = 'Rura'
    constant integer UPG_UNHOLY_ARMOR   = 'Ruar'
    constant integer UPG_CANNIBALIZE    = 'Ruac'
    constant integer UPG_GHOUL_FRENZY   = 'Rugf'
    constant integer UPG_FIEND_WEB      = 'Ruwb'
    constant integer UPG_ABOM           = 'Ruab'
    constant integer UPG_STONE_FORM     = 'Rusf'
    constant integer UPG_NECROS         = 'Rune'
    constant integer UPG_BANSHEE        = 'Ruba'
    constant integer UPG_MEAT_WAGON     = 'Rump'
    constant integer UPG_WYRM_BREATH    = 'Rufb'
    constant integer UPG_SKEL_LIFE      = 'Rusl'
    constant integer UPG_SKEL_MASTERY   = 'Rusm'
    constant integer UPG_EXHUME         = 'Ruex'
    constant integer UPG_SACRIFICE      = 'Rurs'
    constant integer UPG_ABOM_EXPL      = 'Ruax'
    constant integer UPG_CR_ARMOR       = 'Rucr'
    constant integer UPG_PLAGUE         = 'Rupc'
    constant integer UPG_BLK_SPHINX     = 'Rusp'
    constant integer UPG_BURROWING      = 'Rubu'

    //--------------------------------------------------------------------
    //  ELVES
    //--------------------------------------------------------------------

    // elf heroes
    constant integer DEMON_HUNTER       = 'Edem'
    constant integer DEMON_HUNTER_M     = 'Edmm'
    constant integer KEEPER             = 'Ekee'
    constant integer MOON_CHICK         = 'Emoo'
    constant integer MOON_BABE          =  MOON_CHICK
    constant integer MOON_HONEY         =  MOON_CHICK
    constant integer WARDEN             = 'Ewar'

    // special elf heroes
    constant integer SYLVANUS           = 'Hvwd'
    constant integer CENARIUS           = 'Ecen'
    constant integer ILLIDAN            = 'Eevi'
    constant integer ILLIDAN_DEMON      = 'Eevm'
    constant integer MAIEV              = 'Ewrd'

    // elf hero abilities
    constant integer FORCE_NATURE       = 'AEfn'
    constant integer ENT_ROOTS          = 'AEer'
    constant integer THORNS_AURA        = 'AEah'
    constant integer TRANQUILITY        = 'AEtq'

    constant integer EVASION            = 'AEev'
    constant integer IMMOLATION         = 'AEim'
    constant integer MANA_BURN          = 'AEmb'
    constant integer METAMORPHOSIS      = 'AEme'

    constant integer SEARING_ARROWS     = 'AHfa'
    constant integer SCOUT              = 'AEst'
    constant integer STARFALL           = 'AEsf'
    constant integer TRUESHOT           = 'AEar'

    constant integer BLINK              = 'AEbl'
    constant integer FAN_KNIVES         = 'AEfk'
    constant integer SHADOW_TOUCH       = 'AEsh'
    constant integer VENGEANCE          = 'AEsv'

    // elf units
    constant integer WISP               = 'ewsp'
    constant integer ARCHER             = 'earc'
    constant integer DRUID_TALON        = 'edot'
    constant integer DRUID_TALON_M      = 'edtm'
    constant integer BALLISTA           = 'ebal'
    constant integer DRUID_CLAW         = 'edoc'
    constant integer DRUID_CLAW_M       = 'edcm'
    constant integer DRYAD              = 'edry'
    constant integer HIPPO              = 'ehip'
    constant integer HIPPO_RIDER        = 'ehpr'
    constant integer HUNTRESS           = 'esen'
    constant integer CHIMAERA           = 'echm'
    constant integer ENT                = 'efon'
    constant integer MOUNTAIN_GIANT     = 'emtg'
    constant integer FAERIE_DRAGON      = 'efdr'

    // special elf units
    constant integer HIGH_ARCHER        = 'nhea'
    constant integer HIGH_FOOTMAN       = 'hcth'
    constant integer HIGH_FOOTMEN       =  HIGH_FOOTMAN
    constant integer HIGH_SWORDMAN      = 'hhes'
    constant integer DRAGON_HAWK        = 'nws1'
    constant integer CORRUPT_TREANT     = 'nenc'
    constant integer POISON_TREANT      = 'nenp'
    constant integer PLAGUE_TREANT      = 'nepl'
    constant integer SHANDRIS           = 'eshd'

    // elf buildings
    constant integer ANCIENT_LORE       = 'eaoe'
    constant integer ANCIENT_WAR        = 'eaom'
    constant integer ANCIENT_WIND       = 'eaow'
    constant integer TREE_AGES          = 'etoa'
    constant integer TREE_ETERNITY      = 'etoe'
    constant integer TREE_LIFE          = 'etol'
    constant integer ANCIENT_PROTECT    = 'etrp'
    constant integer ELF_ALTAR          = 'eate'
    constant integer BEAR_DEN           = 'edol'
    constant integer CHIMAERA_ROOST     = 'edos'
    constant integer HUNTERS_HALL       = 'edob'
    constant integer MOON_WELL          = 'emow'
    constant integer ELF_MINE           = 'egol'
    constant integer DEN_OF_WONDERS     = 'eden'

    // special elf buildings
    constant integer ELF_FARM           = 'nefm'
    constant integer ELF_GUARD_TOWER    = 'negt'
    constant integer HIGH_SKY           = 'negm'
    constant integer HIGH_EARTH         = 'negf'
    constant integer HIGH_TOWER         = 'negt'
    constant integer ELF_HIGH_BARRACKS  = 'nheb'
    constant integer CORRUPT_LIFE       = 'nctl'
    constant integer CORRUPT_WELL       = 'ncmw'
    constant integer CORRUPT_PROTECTOR  = 'ncap'
    constant integer CORRUPT_WAR        = 'ncaw'

    // elf upgrades
    constant integer UPG_STR_MOON       = 'Resm'
    constant integer UPG_STR_WILD       = 'Resw'
    constant integer UPG_MOON_ARMOR     = 'Rema'
    constant integer UPG_HIDES          = 'Rerh'
    constant integer UPG_ULTRAVISION    = 'Reuv'
    constant integer UPG_BLESSING       = 'Renb'
    constant integer UPG_SCOUT          = 'Resc'
    constant integer UPG_GLAIVE         = 'Remg'
    constant integer UPG_BOWS           = 'Reib'
    constant integer UPG_MARKSMAN       = 'Remk'
    constant integer UPG_DRUID_TALON    = 'Redt'
    constant integer UPG_DRUID_CLAW     = 'Redc'
    constant integer UPG_ABOLISH        = 'Resi'
    constant integer UPG_CHIM_ACID      = 'Recb'
    constant integer UPG_HIPPO_TAME     = 'Reht'
    constant integer UPG_BOLT           = 'Repd'
    constant integer UPG_MARK_CLAW      = 'Reeb'
    constant integer UPG_MARK_TALON     = 'Reec'
    constant integer UPG_HARD_SKIN      = 'Rehs'
    constant integer UPG_RESIST_SKIN    = 'Rers'
    constant integer UPG_WELL_SPRING    = 'Rews'

    //--------------------------------------------------------------------
    // Neutral
    //--------------------------------------------------------------------
    constant integer DEMON_GATE         = 'ndmg'
    constant integer FELLHOUND          = 'nfel'
    constant integer INFERNAL           = 'ninf'
    constant integer DOOMGUARD          = 'nbal'
    constant integer SATYR              = 'nsty'
    constant integer TRICKSTER          = 'nsat'
    constant integer SHADOWDANCER       = 'nsts'
    constant integer SOULSTEALER        = 'nstl'
    constant integer HELLCALLER         = 'nsth'
    constant integer SKEL_ARCHER        = 'nska'
    constant integer SKEL_MARKSMAN      = 'nskm'
    constant integer SKEL_BURNING       = 'nskf'
    constant integer SKEL_GIANT         = 'nskg'
    constant integer FURBOLG            = 'nfrl'
    constant integer FURBOLG_TRACKER    = 'nfrb'
    constant integer FURBOLG_SHAMAN     = 'nfrs'
    constant integer FURBOLG_CHAMP      = 'nfrg'
    constant integer FURBOLG_ELDER      = 'nfre'

    //--------------------------------------------------------------------
    // NAGA
    //--------------------------------------------------------------------

    // naga heroes
    constant integer NAGA_SORCERESS     = 'Nngs'
    constant integer NAGA_VASHJ         = 'Hvsh'

    // naga units
    constant integer NAGA_DRAGON        = 'nsnp'        // old names
    constant integer NAGA_WITCH         = 'nnsw'
    constant integer NAGA_SERPENT       = 'nwgs'
    constant integer NAGA_HYDRA         = 'nhyc'

    constant integer NAGA_SLAVE         = 'nmpe'        // peon
    constant integer NAGA_SNAP_DRAGON   =  NAGA_DRAGON  // weak ranged
    constant integer NAGA_COUATL        =  NAGA_SERPENT // weak air
    constant integer NAGA_SIREN         =  NAGA_WITCH   // caster
    constant integer NAGA_MYRMIDON      = 'nmyr'        // knight
    constant integer NAGA_REAVER        = 'nnmg'        // footman
    constant integer NAGA_TURTLE        =  NAGA_HYDRA   // siege
    constant integer NAGA_ROYAL         = 'nnrg'        // royal guard

    // naga buildings
    constant integer NAGA_TEMPLE        = 'nntt'        // town hall
    constant integer NAGA_CORAL         = 'nnfm'        // farm
    constant integer NAGA_SHRINE        = 'nnsa'        // sirens & couatls
    constant integer NAGA_SPAWNING      = 'nnsg'        // myrm, snap dragon, hydra
    constant integer NAGA_GUARDIAN      = 'nntg'        // tower
    constant integer NAGA_ALTAR         = 'nnad'        // altar

    // naga upgrades
    constant integer UPG_NAGA_ARMOR     = 'Rnam'
    constant integer UPG_NAGA_ATTACK    = 'Rnat'
    constant integer UPG_NAGA_ABOLISH   = 'Rnsi'
    constant integer UPG_SIREN          = 'Rnsw'
    constant integer UPG_NAGA_ENSNARE   = 'Rnen'


    //--------------------------------------------------------------------
    constant integer M1                 =    60
    constant integer M2                 =  2*60
    constant integer M3                 =  3*60
    constant integer M4                 =  4*60
    constant integer M5                 =  5*60
    constant integer M6                 =  6*60
    constant integer M7                 =  7*60
    constant integer M8                 =  8*60
    constant integer M9                 =  9*60
    constant integer M10                = 10*60
    constant integer M11                = 11*60
    constant integer M12                = 12*60
    constant integer M13                = 13*60
    constant integer M14                = 14*60
    constant integer M15                = 15*60

    constant integer EASY               = 1
    constant integer NORMAL             = 2
    constant integer HARD               = 3
    constant integer INSANE             = 4 // not used

    constant integer MELEE_NEWBIE       = 1
    constant integer MELEE_NORMAL       = 2
    constant integer MELEE_INSANE       = 3

    constant integer ATTACK_CAPTAIN     = 1
    constant integer DEFENSE_CAPTAIN    = 2
    constant integer BOTH_CAPTAINS      = 3

    constant integer BUILD_UNIT         = 1
    constant integer BUILD_UPGRADE      = 2
    constant integer BUILD_EXPAND       = 3

    constant integer UPKEEP_TIER1       = 50
    constant integer UPKEEP_TIER2       = 80

    //--------------------------------------------------------------------

    player  ai_player

    integer sleep_seconds
    integer total_gold                                      = 0
    integer total_wood                                      = 0
    integer gold_buffer                                     = 0 // usually for potion money
    integer difficulty                                      = NORMAL
    integer exp_seen                                        = 0
    integer racial_farm                                     = 'hhou'
    integer hero_id                                         = 'Hamg'
    integer hero_id2                                        = 'Hmkg'
    integer hero_id3                                        = 'Hpal'
    integer array skill
    integer array skills1
    integer array skills2
    integer array skills3
    integer max_hero_level                                  = 0

    integer array harass_qty
    integer array harass_max
    integer array harass_units
    integer harass_length                                       = 0

    integer array defense_qty
    integer array defense_units
    integer defense_length                                  = 0

    integer array build_qty
    integer array build_type
    integer array build_item
    integer array build_town
    integer build_length                                    = 0

    integer campaign_gold_peons                             = 5
    integer campaign_wood_peons                             = 3
    integer campaign_basics_speed                           = 5

    integer min_creeps                                      = -1
    integer max_creeps                                         = -1

    boolean harvest_town1                                   = true
    boolean harvest_town2                                   = true
    boolean harvest_town3                                   = true
    boolean do_campaign_farms                               = true
    boolean two_heroes                                      = false
    boolean allow_air_creeps                                = false
    boolean take_exp                                        = false
    boolean allow_signal_abort                              = false
    boolean ready_for_zeppelin                              = true
    boolean get_zeppelin                                       = false

    boolean build_campaign_attackers                         = true

    boolean do_debug_cheats                                 = false
    boolean trace_on                                        = true
    boolean zep_next_wave                                   = false
    boolean form_group_timeouts                             = true

    //--------------------------------------------------------------------

    player debug_player
    boolean debug                                             = false

    boolean suicide                                            = false

    integer wave_reach_goal_timeout_seconds                 = 300
    integer wave_enter_combat_timeout_seconds                 = 3
    integer wave_clear_goal_timeout_seconds                    = 60

    real continue_far_away_radius                             = 960.0
    real preferred_locations_radius                         = 640.0

    integer wave_reach_goal_sleep_seconds
    integer wave_enter_combat_sleep_seconds
    integer wave_clear_goal_sleep_seconds

    real defense_captain_home_x
    real defense_captain_home_y
    boolean defense_captain_home_set                         = false

    real attack_wave_gather_return_x
    real attack_wave_gather_return_y
    boolean attack_wave_gather_return_set                    = false

    location array preferred_locations
    integer preferred_locations_count                         = 0

    location array preferred_locations_current
    integer preferred_locations_count_current                 = 0

    boolean wood_peons_warriors                             = false

    boolean search_preferred_locations                         = true
    boolean search_everywhere                                 = true
    boolean can_search_units                                 = true
    boolean can_search_invulnerable                         = false
    boolean prioritize_town_halls                             = true
    boolean prioritize_nearest                                 = true

    boolean reach_goal_timeout                                 = true
    boolean enter_combat_timeout                             = true
    boolean clear_goal_timeout                                 = true
    integer continue_attack_percentage                         = 100
    boolean continue_attack_reduce_percentage_if_far_away     = true
    integer continue_attack_reduce_percentage                 = 25

    unit array targetable_town_halls
    integer targetable_town_halls_count                     = 0

    unit array targetable_buildings
    integer targetable_buildings_count                         = 0

    unit array targetable_units
    integer targetable_units_count                             = 0

    real last_target_x
    real last_target_y
    integer search_target_max_cycles_default                = 100
    integer search_target_max_cycles_last_target            = 10

endglobals

//============================================================================
function PlayerEx takes integer slot returns player
    return Player(slot-1)
endfunction

//============================================================================
function Trace takes string message returns nothing
    if trace_on then
        call DisplayText(GetAiPlayer(),message)
    endif
endfunction

//============================================================================
function TraceI takes string message, integer val returns nothing
    if trace_on then
        call DisplayTextI(GetAiPlayer(),message,val)
    endif
endfunction

//============================================================================
function TraceII takes string message, integer v1, integer v2 returns nothing
    if trace_on then
        call DisplayTextII(GetAiPlayer(),message,v1,v2)
    endif
endfunction

//============================================================================
function TraceIII takes string message, integer v1, integer v2, integer v3 returns nothing
    if trace_on then
        call DisplayTextIII(GetAiPlayer(),message,v1,v2,v3)
    endif
endfunction

//============================================================================
function InitAI takes nothing returns nothing
    set ai_player = Player(GetAiPlayer())
    set sleep_seconds = 0
    call StopGathering()
endfunction

//============================================================================
function StandardAI takes code heroes, code peons, code attacks returns nothing

    local boolean isNewbie = (MeleeDifficulty() == MELEE_NEWBIE)

    call InitAI()

    call SetMeleeAI()

    call SetDefendPlayer(true)
    call SetGroupsFlee(not isNewbie)
    call SetHeroesBuyItems(not isNewbie)
    call SetHeroesFlee(true)
    call SetHeroesTakeItems(true)
    call SetIgnoreInjured(true)
    call SetPeonsRepair(true)
    call SetSmartArtillery(not isNewbie)
    call SetTargetHeroes(not isNewbie)
    call SetUnitsFlee(not isNewbie)
    call SetWatchMegaTargets(true)

    call CreateCaptains()

    call SetHeroLevels(heroes)

    call Sleep(0.1)
    call StartThread(peons)
    call StartThread(attacks)
endfunction

//============================================================================
//  Utility Functions
//============================================================================
function Min takes integer A, integer B returns integer
    if A < B then
        return A
    else
        return B
    endif
endfunction

function Max takes integer A, integer B returns integer
    if A > B then
        return A
    else
        return B
    endif
endfunction

function SetZepNextWave takes nothing returns nothing
    set zep_next_wave = true
endfunction

function SuicideSleep takes integer seconds returns nothing
    set sleep_seconds = sleep_seconds - seconds
    loop
        exitwhen seconds <= 0
        exitwhen allow_signal_abort and CommandsWaiting() != 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
    endloop
endfunction

//============================================================================
function WaitForSignal takes nothing returns integer
    local integer cmd
    local boolean display = false //xxx
    loop
        exitwhen CommandsWaiting() != 0

        //xxx
        call Trace("waiting for a signal to begin AI script...\n")
        set display = true
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        exitwhen CommandsWaiting() != 0
        call Sleep(2)
        //xxx

    endloop

    //xxx
    if display then
        call Trace("signal received, beginning AI script\n")
    endif
    //xxx

    set cmd = GetLastCommand()
    call PopLastCommand()
    return cmd
endfunction

//============================================================================
function SetWoodPeons takes integer count returns nothing
    set campaign_wood_peons = count
endfunction

//============================================================================
function SetGoldPeons takes integer count returns nothing
    set campaign_gold_peons = count
endfunction

//============================================================================
function SetHarvestLumber takes boolean harvest returns nothing
    if harvest then
        set campaign_wood_peons = 3
    else
        set campaign_wood_peons = 0
    endif
endfunction

//============================================================================
function SetFormGroupTimeouts takes boolean state returns nothing
    set form_group_timeouts = state
endfunction

//============================================================================
function DoCampaignFarms takes boolean state returns nothing
    set do_campaign_farms = state
endfunction

//============================================================================
function GetMinorCreep takes nothing returns unit
    return GetCreepCamp(0,9,false)
endfunction

//============================================================================
function GetMajorCreep takes nothing returns unit
    return GetCreepCamp(10,100,allow_air_creeps)
endfunction

//============================================================================
function GetGold takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_GOLD)
endfunction

//============================================================================
function GetWood takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_LUMBER)
endfunction

//============================================================================
function InitBuildArray takes nothing returns nothing
    set build_length = 0
endfunction

//============================================================================
function InitAssaultGroup takes nothing returns nothing
    set harass_length = 0
endfunction

//============================================================================
function InitDefenseGroup takes nothing returns nothing
    set defense_length = 0
endfunction

//============================================================================
function InitMeleeGroup takes nothing returns nothing
    call InitAssaultGroup()
    call RemoveInjuries()
    call RemoveSiege()
endfunction

//============================================================================
function PrepFullSuicide takes nothing returns nothing
    call InitAssaultGroup()
    call InitDefenseGroup()
    set campaign_gold_peons = 0
    set campaign_wood_peons = 0
endfunction

//============================================================================
function SetReplacements takes integer easy, integer med, integer hard returns nothing
    if difficulty == EASY then
        call SetReplacementCount(easy)
    elseif difficulty == NORMAL then
        call SetReplacementCount(med)
    else
        call SetReplacementCount(hard)
    endif
endfunction

//============================================================================
function StartTownBuilder takes code func returns nothing
    call StartThread(func)
endfunction

//============================================================================
function SetBuildAll takes integer t, integer qty, integer unitid, integer town returns nothing
    if qty > 0 then
        set build_qty[build_length] = qty
        set build_type[build_length] = t
        set build_item[build_length] = unitid
        set build_town[build_length] = town
        set build_length = build_length + 1
    endif
endfunction

//============================================================================
function SetBuildUnit takes integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_UNIT,qty,unitid,-1)
endfunction

//============================================================================
function SetBuildNext takes integer qty, integer unitid returns nothing
    local integer has = GetUnitCount(unitid)
    if has >= qty then
        return
    endif
    call SetBuildAll(BUILD_UNIT,GetUnitCountDone(unitid)+1,unitid,-1)
endfunction

//============================================================================
function SetBuildUnitEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call SetBuildAll(BUILD_UNIT,easy,unitid,-1)
    elseif difficulty == NORMAL then
        call SetBuildAll(BUILD_UNIT,med,unitid,-1)
    else
        call SetBuildAll(BUILD_UNIT,hard,unitid,-1)
    endif
endfunction

//============================================================================
function SecondaryTown takes integer town, integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_UNIT,qty,unitid,town)
endfunction

//============================================================================
function SecTown takes integer town, integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_UNIT,qty,unitid,town)
endfunction

//============================================================================
function SetBuildUpgr takes integer qty, integer unitid returns nothing
    if MeleeDifficulty() != MELEE_NEWBIE or qty == 1 then
        call SetBuildAll(BUILD_UPGRADE,qty,unitid,-1)
    endif
endfunction

//============================================================================
function SetBuildUpgrEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call SetBuildAll(BUILD_UPGRADE,easy,unitid,-1)
    elseif difficulty == NORMAL then
        call SetBuildAll(BUILD_UPGRADE,med,unitid,-1)
    else
        call SetBuildAll(BUILD_UPGRADE,hard,unitid,-1)
    endif
endfunction

//============================================================================
function SetBuildExpa takes integer qty, integer unitid returns nothing
    call SetBuildAll(BUILD_EXPAND,qty,unitid,-1)
endfunction

//============================================================================
function StartUpgrade takes integer level, integer upgid returns boolean
    local integer gold_cost
    local integer wood_cost

    if GetUpgradeLevel(upgid) >= level then
        return true
    endif

    set gold_cost = GetUpgradeGoldCost(upgid)
    if total_gold < gold_cost then
        return false
    endif

    set wood_cost = GetUpgradeWoodCost(upgid)
    if total_wood < wood_cost then
        return false
    endif

    return SetUpgrade(upgid)
endfunction

//============================================================================
function BuildFactory takes integer unitid returns nothing
    if GetGold() > 1000 and GetWood() > 500 then
        call SetBuildUnit( 2, unitid )
    else
        call SetBuildUnit( 1, unitid )
    endif
endfunction

//============================================================================
function HallsCompleted takes integer unitid returns boolean
    return GetUnitCount(unitid) == GetUnitCountDone(unitid)
endfunction

//============================================================================
function GuardSecondary takes integer townid, integer qty, integer unitid returns nothing
    if TownHasHall(townid) and TownHasMine(townid) then
        call SecondaryTown( townid, qty, unitid )
    endif
endfunction

//============================================================================
function GetUnitCountEx takes integer unitid, boolean only_done, integer townid returns integer
    if townid == -1 then
        if only_done then
            return GetUnitCountDone(unitid)
        else
            return GetUnitCount(unitid)
        endif
    else
        return GetTownUnitCount(unitid,townid,only_done)
    endif
endfunction

//============================================================================
function TownCountEx takes integer unitid, boolean only_done, integer townid returns integer

    local integer have_qty = GetUnitCountEx(unitid,only_done,townid)

    if unitid == TOWN_HALL then
        set have_qty = have_qty + GetUnitCountEx(KEEP,false,townid) + GetUnitCountEx(CASTLE,false,townid)
    elseif unitid == KEEP then
        set have_qty = have_qty  + GetUnitCountEx(CASTLE,false,townid)

    elseif unitid == WATCH_TOWER then
        set have_qty = have_qty + GetUnitCountEx(GUARD_TOWER,false,townid) + GetUnitCountEx(CANNON_TOWER,false,townid) + GetUnitCountEx(ARCANE_TOWER,false,townid)

    elseif unitid == PEASANT then
        set have_qty = have_qty + GetUnitCountEx(MILITIA,false,townid)

    elseif unitid == GREAT_HALL then
        set have_qty = have_qty + GetUnitCountEx(STRONGHOLD,false,townid) + GetUnitCountEx(FORTRESS,false,townid)
    elseif unitid == STRONGHOLD then
        set have_qty = have_qty + GetUnitCountEx(FORTRESS,false,townid)

    elseif unitid == HEAD_HUNTER then
        set have_qty = have_qty + GetUnitCountEx(BERSERKER,false,townid)

    elseif unitid == SPIRIT_WALKER then
        set have_qty = have_qty + GetUnitCountEx(SPIRIT_WALKER_M,false,townid)
    elseif unitid == SPIRIT_WALKER_M then
        set have_qty = have_qty + GetUnitCountEx(SPIRIT_WALKER,only_done,townid)

    elseif unitid == NECROPOLIS_1 then
        set have_qty = have_qty + GetUnitCountEx(NECROPOLIS_2,false,townid) + GetUnitCountEx(NECROPOLIS_3,false,townid)
    elseif unitid == NECROPOLIS_2 then
        set have_qty = have_qty + GetUnitCountEx(NECROPOLIS_3,false,townid)

    elseif unitid == ZIGGURAT_1 then
        set have_qty = have_qty + GetUnitCountEx(ZIGGURAT_2,false,townid) + GetUnitCountEx(ZIGGURAT_FROST,false,townid)

    elseif unitid == GARGOYLE then
        set have_qty = have_qty + GetUnitCountEx(GARGOYLE_MORPH,false,townid)

    elseif unitid == TREE_LIFE then
        set have_qty = have_qty + GetUnitCountEx(TREE_AGES,false,townid) + GetUnitCountEx(TREE_ETERNITY,false,townid)
    elseif unitid == TREE_AGES then
        set have_qty = have_qty + GetUnitCountEx(TREE_ETERNITY,false,townid)

    elseif unitid == DRUID_TALON then
        set have_qty = have_qty + GetUnitCountEx(DRUID_TALON_M,false,townid)
    elseif unitid == DRUID_TALON_M then
        set have_qty = have_qty + GetUnitCountEx(DRUID_TALON,only_done,townid)

    elseif unitid == DRUID_CLAW then
        set have_qty = have_qty + GetUnitCountEx(DRUID_CLAW_M,false,townid)
    elseif unitid == DRUID_CLAW_M then
        set have_qty = have_qty + GetUnitCountEx(DRUID_CLAW,only_done,townid)

    elseif unitid == ILLIDAN then
        set have_qty = have_qty + GetUnitCountEx(ILLIDAN_DEMON,false,townid)

    endif
    return have_qty
endfunction

//============================================================================
function TownCountDone takes integer base returns integer
    return TownCountEx(base,true,-1)
endfunction

//============================================================================
function TownCount takes integer base returns integer
    return TownCountEx(base,false,-1)
endfunction

//============================================================================
function BasicExpansion takes boolean build_it, integer unitid returns nothing
    if build_it and HallsCompleted(unitid) then
        call SetBuildExpa( TownCount(unitid)+1, unitid )
    endif
endfunction

//============================================================================
function UpgradeAll takes integer baseid, integer newid returns nothing
    call SetBuildUnit( TownCountDone(baseid), newid )
endfunction

//============================================================================
function TownCountTown takes integer base, integer townid returns integer
    return TownCountEx(base,false,townid)
endfunction

//============================================================================
//  FoodPool
//============================================================================
function FoodPool takes integer food, boolean weak, integer id1, integer use1, boolean strong, integer id2, integer use2 returns nothing
    if strong then
        call SetBuildUnit( (food - use1 * TownCount(id1)) / use2, id2 )
    elseif weak then
        call SetBuildUnit( (food - use2 * TownCount(id2)) / use1, id1 )
    endif
endfunction

//============================================================================
//  MeleeTownHall
//============================================================================
function MeleeTownHall takes integer townid, integer unitid returns nothing
    if TownHasMine(townid) and not TownHasHall(townid) then
        call SecondaryTown ( townid, 1, unitid )
    endif
endfunction

//============================================================================
function WaitForUnits takes integer unitid, integer qty returns nothing
    loop
        exitwhen TownCountDone(unitid) == qty
        call Sleep(2)
    endloop
endfunction

//============================================================================
function StartUnit takes integer ask_qty, integer unitid, integer town returns boolean
    local integer have_qty
    local integer need_qty
    local integer afford_gold
    local integer afford_wood
    local integer afford_qty
    local integer gold_cost
    local integer wood_cost

    //------------------------------------------------------------------------
    // if we have all we're asking for then make nothing
    //
    if town == -1 then
        set have_qty = TownCount(unitid)
    else
        set have_qty = TownCountTown(unitid,town)
    endif

    if have_qty >= ask_qty then
        return true
    endif
    set need_qty = ask_qty - have_qty

    //------------------------------------------------------------------------
    // limit the qty we're requesting to the amount of resources available
    //
    set gold_cost = GetUnitGoldCost(unitid)
    set wood_cost = GetUnitWoodCost(unitid)

    if gold_cost == 0 then
        set afford_gold = need_qty
    else
        set afford_gold = total_gold / gold_cost
    endif
    if afford_gold < need_qty then
        set afford_qty = afford_gold
    else
        set afford_qty = need_qty
    endif

    if wood_cost == 0 then
        set afford_wood = need_qty
    else
        set afford_wood = total_wood / wood_cost
    endif
    if afford_wood < afford_qty then
        set afford_qty = afford_wood
    endif

    // if we're waiting on gold/wood; pause build orders
    if afford_qty < 1 then
        return false
    endif

    //------------------------------------------------------------------------
    // whether we make right now what we're requesting or not, assume we will
    // and deduct the cost of the units from our fake gold total right away
    //
    set total_gold = total_gold - gold_cost * need_qty
    set total_wood = total_wood - wood_cost * need_qty

    if total_gold < 0 then
        set total_gold = 0
    endif
    if total_wood < 0 then
        set total_wood = 0
    endif

    //------------------------------------------------------------------------
    // give the AI a chance to make the units (it may not be able to right now
    // but that doesn't stop us from trying other units after this as long
    // as we have enough money to make this AND the needed, unbuilt ones)
    //
    return SetProduce(afford_qty,unitid,town)
endfunction

//============================================================================
function WaitForTown takes integer towns, integer townid returns nothing
    local integer i = 0
    loop
        call Sleep(10)
        exitwhen TownCount(townid) >= towns
        set i = i + 1
        exitwhen i == 12
    endloop
endfunction

//============================================================================
function StartExpansion takes integer qty, integer hall returns boolean
    local integer count
    local integer town
    local unit    peon
    local integer gold_cost

    set count = TownCount(hall)
    if count >= qty then
        return true
    endif

    set town = GetNextExpansion()
    if town == -1 then
        return true
    endif

    set take_exp = true

    set gold_cost = GetUnitGoldCost(hall)
    if gold_cost > total_gold then
        return false
    endif
    set total_gold = total_gold - gold_cost

    if GetExpansionFoe() != null then
        return true
    endif

    set peon = GetExpansionPeon()
    if peon != null then
        return SetExpansion(peon,hall)
    endif

    return true
endfunction

//============================================================================
function OneBuildLoop takes nothing returns nothing
    local integer index = 0
    local integer qty
    local integer id
    local integer tp

    set total_gold = GetGold() - gold_buffer
    set total_wood = GetWood()

    loop
        exitwhen index == build_length

        set qty = build_qty [index]
        set id  = build_item[index]
        set tp  = build_type[index]

        //--------------------------------------------------------------------
        if tp == BUILD_UNIT then
            if not StartUnit(qty,id,build_town[index]) then
                return
            endif

        //--------------------------------------------------------------------
        elseif tp == BUILD_UPGRADE then
            call StartUpgrade(qty,id)

        //--------------------------------------------------------------------
        else // tp == BUILD_EXPAND
            if not StartExpansion(qty,id) then
                return
            endif
        endif

        set index = index + 1
    endloop
endfunction

//============================================================================
function StaggerSleep takes real base, real spread returns nothing
    call Sleep(base + spread * I2R(GetAiPlayer()) / I2R(GetPlayers()))
endfunction

//============================================================================
function BuildLoop takes nothing returns nothing
    call OneBuildLoop()
    call StaggerSleep(1,2)
    loop
        call OneBuildLoop()
        call Sleep(2)
    endloop
endfunction

//============================================================================
function StartBuildLoop takes nothing returns nothing
    call StartThread(function BuildLoop)
endfunction

//============================================================================
function SetInitialWave takes integer seconds returns nothing
    set sleep_seconds = seconds
endfunction

//============================================================================
function AddSleepSeconds takes integer seconds returns nothing
    set sleep_seconds = sleep_seconds + seconds
endfunction

//============================================================================
function SleepForever takes nothing returns nothing
    call Trace("going to sleep forever\n") //xxx
    loop
        call Sleep(100)
    endloop
endfunction

//============================================================================
function PlayGame takes nothing returns nothing
    call StartBuildLoop()
    call SleepForever()
endfunction

//============================================================================
function ConvertNeeds takes integer unitid returns nothing
    if GetUnitCount(unitid) < 1 then
        call StartUnit(1,unitid,-1)
    endif
endfunction

//============================================================================
function Conversions takes integer desire, integer unitid returns nothing

    if GetUnitCount(unitid) >= desire then
        return
    endif

    if unitid == HIPPO_RIDER then
        call ConvertNeeds(ARCHER)
        call ConvertNeeds(HIPPO)
        call MergeUnits(desire,ARCHER,HIPPO,HIPPO_RIDER)

    elseif unitid == BLK_SPHINX then
        call ConvertNeeds(OBS_STATUE)
        call ConvertUnits(desire,OBS_STATUE)
    endif
endfunction

//============================================================================
function SetAssaultGroup takes integer qty, integer max, integer unitid returns nothing
    call Conversions(max,unitid)

    if qty <= 0 and TownCountDone(unitid) == 0 then
        return
    endif
    set harass_qty[harass_length] = qty
    set harass_max[harass_length] = max
    set harass_units[harass_length] = unitid
    set harass_length = harass_length + 1
endfunction

//============================================================================
function Interleave3 takes integer e1, integer m1, integer h1, integer u1, integer e2, integer m2, integer h2, integer u2, integer e3, integer m3, integer h3, integer u3 returns nothing
    local integer i1 = 1
    local integer i2 = 1
    local integer i3 = 1
    local integer q1
    local integer q2
    local integer q3

    if difficulty == EASY then
        set q1 = e1
        set q2 = e2
        set q3 = e3
    elseif difficulty == NORMAL then
        set q1 = m1
        set q2 = m2
        set q3 = m3
    else // difficulty == HARD
        set q1 = h1
        set q2 = h2
        set q3 = h3
    endif

    loop
        exitwhen q1<=0 and q2<=0 and q3<=0

        if q1 > 0 then
            call SetAssaultGroup(i1,i1,u1)
            set q1 = q1 - 1
            set i1 = i1 + 1
        endif

        if q2 > 0 then
            call SetAssaultGroup(i2,i2,u2)
            set q2 = q2 - 1
            set i2 = i2 + 1
        endif

        if q3 > 0 then
            call SetAssaultGroup(i3,i3,u3)
            set q3 = q3 - 1
            set i3 = i3 + 1
        endif
    endloop
endfunction

//============================================================================
function SetMeleeGroup takes integer unitid returns nothing
    if unitid == hero_id then
        call SetAssaultGroup(1,9,unitid)
    else
        call SetAssaultGroup((TownCountDone(unitid)*3)/4,20,unitid)
    endif
endfunction

//============================================================================
function CampaignDefender takes integer level, integer qty, integer unitid returns nothing
    if qty > 0 and difficulty >= level then
        set defense_qty[defense_length] = qty
        set defense_units[defense_length] = unitid
        set defense_length = defense_length + 1
        call Conversions(qty,unitid)
        call SetBuildUnit(qty,unitid)
    endif
endfunction

//============================================================================
function CampaignDefenderEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call CampaignDefender(EASY,easy,unitid)
    elseif difficulty == NORMAL then
        call CampaignDefender(NORMAL,med,unitid)
    else
        call CampaignDefender(HARD,hard,unitid)
    endif
endfunction

//============================================================================
function CampaignAttacker takes integer level, integer qty, integer unitid returns nothing
    if qty > 0 and difficulty >= level then
        call SetAssaultGroup(qty,qty,unitid)
    endif
endfunction

//============================================================================
function CampaignAttackerEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call CampaignAttacker(EASY,easy,unitid)
    elseif difficulty == NORMAL then
        call CampaignAttacker(NORMAL,med,unitid)
    else
        call CampaignAttacker(HARD,hard,unitid)
    endif
endfunction

//============================================================================
function FormGroup takes integer seconds, boolean testReady returns nothing
    local integer index
    local integer count
    local integer unitid
    local integer desire
    local integer readyPercent

    // normally test for CaptainReadiness() of 50%
    if testReady == true then
        set readyPercent = 50
        call Trace("forming group, requiring healthy guys\n") //xxx
    else
        set readyPercent = 0
        call Trace("forming group, unit health not important\n") //xxx
    endif

    call Trace("trying to gather forces\n") //xxx

    loop
        call SuicideSleep(seconds)
        call InitAssault()

        set index = 0
        loop
            exitwhen index == harass_length

            set unitid = harass_units[index]
            set desire = harass_max[index]
            set count  = TownCountDone(unitid)

            call Conversions(desire,unitid)

            if count >= desire then
                call AddAssault(desire,unitid)
            else
                set desire = harass_qty[index]

                if count < desire then
                    call AddAssault(desire,unitid)
                else
                    call AddAssault(count,unitid)
                endif
            endif

            set index = index + 1
        endloop

        //xxx
        if form_group_timeouts and (sleep_seconds < -60) then
            call Trace("exit form group -- timeout\n")
        elseif CaptainInCombat(true) then
            call Trace("exit form group -- can't form while already in combat\n")
        elseif CaptainIsFull() and CaptainReadiness() >= readyPercent then
            call Trace("exit form group -- ready\n")
        endif
        //xxx

        // time out and send group anyway if time has already expired
        exitwhen form_group_timeouts and (sleep_seconds < -60)
        exitwhen CaptainInCombat(true)
        exitwhen CaptainIsFull() and CaptainReadiness() >= readyPercent
    endloop
endfunction

//============================================================================
function WavePrepare takes integer unitid returns integer
    return GetUnitBuildTime(unitid)
endfunction

//============================================================================
function PrepTime takes nothing returns integer
    local integer unitid
    local integer missing
    local integer prep
    local integer count
    local integer largest = 30
    local integer index = 0

    loop
        exitwhen index == harass_length

        set unitid  = harass_units[index]
        set missing = harass_qty[index] + IgnoredUnits(unitid) - TownCount(unitid)
        set prep    = WavePrepare(unitid) * missing

        if prep > largest then
            set largest = prep
        endif

        set index = index + 1
    endloop
    call TraceI("next wave will require around %d seconds to build and gather\n",largest) //xxx

    return largest
endfunction

//============================================================================
function PrepSuicideOnPlayer takes integer seconds returns boolean
    local integer wave_prep   = PrepTime()
    local integer save_length

    set save_length = harass_length
    set harass_length = 0

    call AddSleepSeconds(seconds)
    if sleep_seconds-wave_prep > 0 then
        call TraceI("going to sleep for %d seconds before gathering next attack wave\n",sleep_seconds-wave_prep) //xxx
        call SuicideSleep(sleep_seconds-wave_prep)
    endif

    call Trace("preparing suicide attack wave\n") //xxx

    set harass_length = save_length
    if harass_length < 1 then
        call Trace("ERROR - no units specificed, exiting early\n") //xxx
        return false
    endif

    return true
endfunction

//============================================================================
function SleepUntilAtGoal takes nothing returns nothing
    loop
        exitwhen CaptainRetreating()
        exitwhen CaptainAtGoal()        // reached goal
        exitwhen CaptainIsHome()        // failed to path and returned home
        exitwhen CaptainIsEmpty()       // all units died
        call SuicideSleep(3)
    endloop
endfunction

//============================================================================
function SleepInCombat takes nothing returns nothing
    local integer count = 0
    debug call Trace("SleepInCombat\n")
    loop
        loop
            exitwhen not CaptainInCombat(true)  // goal is cleared
            exitwhen CaptainIsEmpty()           // duh
            call SuicideSleep(1)
        endloop

        set count = count + 1
        exitwhen count >= 8

        //xxx this is what it should have been; do this for next patch?
        //call SuicideSleep(1)
    endloop
    debug call Trace("exit SleepInCombat\n")
endfunction

//============================================================================
function AttackMoveXYA takes integer x, integer y returns nothing

    if zep_next_wave then
        call LoadZepWave(x,y)
        set zep_next_wave = false
    endif

    call AttackMoveXY(x,y)
    call SleepUntilAtGoal()
    call SleepInCombat()
endfunction

//============================================================================
function SuicideOnPlayerWave takes nothing returns nothing
    call Trace("waiting for attack wave to enter combat\n") //xxx
    loop
        //xxx
        if allow_signal_abort and CommandsWaiting() != 0 then
            call Trace("ABORT -- attack wave override\n")
        endif

        if CaptainInCombat(true) then
            call Trace("done - captain has entered combat\n")
        endif

        if CaptainIsEmpty() then
            call Trace("done - all units are dead\n")
        endif

        if sleep_seconds < -300 then
            call Trace("done - timeout, took too long to reach engage the enemy\n")
        endif
        //xxx

        exitwhen allow_signal_abort and CommandsWaiting() != 0

        exitwhen CaptainInCombat(true)
        exitwhen CaptainIsEmpty()
        call SuicideSleep(10)
        exitwhen sleep_seconds < -300
    endloop

    call Trace("waiting for attack wave to die\n") //xxx
    loop
        //xxx
        if allow_signal_abort and CommandsWaiting() != 0 then
            call Trace("ABORT - attack wave override\n")
        endif

        if CaptainIsEmpty() then
            call Trace("done - all units are dead\n")
        endif

        if sleep_seconds < -300 then
            call Trace("done - timeout, took too long to reach engage the enemy\n")
        endif
        //xxx

        exitwhen allow_signal_abort and CommandsWaiting() != 0

        exitwhen CaptainIsEmpty()
        call SuicideSleep(10)
        exitwhen sleep_seconds < -300
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function CommonSuicideOnPlayer takes boolean standard, boolean bldgs, integer seconds, player p, integer x, integer y returns nothing
    local integer save_peons

    if not PrepSuicideOnPlayer(seconds) then
        return
    endif

    set save_peons = campaign_wood_peons
    set campaign_wood_peons = 0

    loop
        //xxx
        if allow_signal_abort and CommandsWaiting() != 0 then
            call Trace("ABORT -- attack wave override\n")
        endif
        //xxx

        exitwhen allow_signal_abort and CommandsWaiting() != 0

        loop
            exitwhen allow_signal_abort and CommandsWaiting() != 0

            call FormGroup(5,true)
            exitwhen sleep_seconds <= 0
            call TraceI("waiting %d seconds before suicide\n",sleep_seconds) //xxx
        endloop

        if standard then
            if bldgs then
                exitwhen SuicidePlayer(p,sleep_seconds >= -60)
            else
                exitwhen SuicidePlayerUnits(p,sleep_seconds >= -60)
            endif
        else
            call AttackMoveXYA(x,y)
        endif

        call TraceI("waiting %d seconds before timeout\n",60+sleep_seconds) //xxx
        call SuicideSleep(5)
    endloop

    set campaign_wood_peons = save_peons
    set harass_length = 0

    call SuicideOnPlayerWave()
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPlayer takes integer seconds, player p returns nothing
    call CommonSuicideOnPlayer(true,true,seconds,p,0,0)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnUnits takes integer seconds, player p returns nothing
    call CommonSuicideOnPlayer(true,false,seconds,p,0,0)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPoint takes integer seconds, player p, integer x, integer y returns nothing
    call CommonSuicideOnPlayer(false,false,seconds,p,x,y)
endfunction

//============================================================================
function SuicideUntilSignal takes integer seconds, player p returns nothing
    local integer save
    local integer wave_prep = PrepTime()

    loop
        call AddSleepSeconds(seconds)
        if sleep_seconds-wave_prep > 0 then
            call SuicideSleep(sleep_seconds-wave_prep)
        endif

        set save = campaign_wood_peons
        set campaign_wood_peons = 0
        loop
            loop
                call FormGroup(5, true)
                exitwhen sleep_seconds <= 0
                exitwhen CommandsWaiting() != 0
            endloop
            exitwhen SuicidePlayer(p,sleep_seconds >= -60)
            exitwhen CommandsWaiting() != 0
            call SuicideSleep(3)
        endloop
        set campaign_wood_peons = save

        loop
            exitwhen CaptainIsEmpty()
            exitwhen CommandsWaiting() != 0
            call SuicideSleep(5)
        endloop
        exitwhen CommandsWaiting() != 0
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnce takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call SuicideUnit(easy,unitid)
    elseif difficulty == NORMAL then
        call SuicideUnit(med,unitid)
    else
        call SuicideUnit(hard,unitid)
    endif
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnitA takes integer unitid returns nothing
    if unitid != 0 then
        call SuicideUnit(1,unitid)
    endif
    call Sleep(0.1)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnitB takes integer unitid, integer playerid returns nothing
    if unitid != 0 then
        call SuicideUnitEx(1,unitid,playerid)
    endif
    call Sleep(0.1)
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnits takes integer u1, integer u2, integer u3, integer u4, integer u5, integer u6, integer u7, integer u8, integer u9, integer uA returns nothing
    call Trace("MASS SUICIDE - this script is now technically done\n") //xxx

    call PrepFullSuicide()
    loop
        call SuicideUnitA(u1)
        call SuicideUnitA(u2)
        call SuicideUnitA(u3)
        call SuicideUnitA(u4)
        call SuicideUnitA(u5)
        call SuicideUnitA(u6)
        call SuicideUnitA(u7)
        call SuicideUnitA(u8)
        call SuicideUnitA(u9)
        call SuicideUnitA(uA)
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideUnitsEx takes integer playerid, integer u1, integer u2, integer u3, integer u4, integer u5, integer u6, integer u7, integer u8, integer u9, integer uA returns nothing
    call Trace("MASS SUICIDE - this script is now technically done\n") //xxx

    call PrepFullSuicide()
    loop
        call SuicideUnitB(u1,playerid)
        call SuicideUnitB(u2,playerid)
        call SuicideUnitB(u3,playerid)
        call SuicideUnitB(u4,playerid)
        call SuicideUnitB(u5,playerid)
        call SuicideUnitB(u6,playerid)
        call SuicideUnitB(u7,playerid)
        call SuicideUnitB(u8,playerid)
        call SuicideUnitB(u9,playerid)
        call SuicideUnitB(uA,playerid)
    endloop
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPlayerEx takes integer easy, integer med, integer hard, player p returns nothing
    if difficulty == EASY then
        call SuicideOnPlayer(easy,p)
    elseif difficulty == NORMAL then
        call SuicideOnPlayer(med,p)
    else
        call SuicideOnPlayer(hard,p)
    endif
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnUnitsEx takes integer easy, integer med, integer hard, player p returns nothing
    if difficulty == EASY then
        call SuicideOnUnits(easy,p)
    elseif difficulty == NORMAL then
        call SuicideOnUnits(med,p)
    else
        call SuicideOnUnits(hard,p)
    endif
endfunction

//--------------------------------------------------------------------------------------------------
function SuicideOnPointEx takes integer easy, integer med, integer hard, player p, integer x, integer y returns nothing
    if difficulty == EASY then
        call SuicideOnPoint(easy,p,x,y)
    elseif difficulty == NORMAL then
        call SuicideOnPoint(med,p,x,y)
    else
        call SuicideOnPoint(hard,p,x,y)
    endif
endfunction

//============================================================================
function ForeverSuicideOnPlayer takes integer seconds, player p returns nothing
    local integer length = harass_length
    loop
        exitwhen allow_signal_abort and CommandsWaiting() != 0
        call SuicideOnPlayer(seconds,p)
        set harass_length = length
    endloop
endfunction

//============================================================================
function CommonSleepUntilTargetDead takes unit target, boolean reform returns nothing
    loop
        exitwhen CaptainRetreating()
        exitwhen CaptainReadinessHP() <= 40

        exitwhen not UnitAlive(target)
        exitwhen UnitInvis(target) and not IsUnitDetected(target,ai_player)

        if not TownThreatened() then
            call AttackMoveKill(target)
        endif

        call SuicideSleep(3)

        if reform and sleep_seconds < -40 then
            if CaptainInCombat(true) then
                set sleep_seconds = sleep_seconds + 5
            else
                set sleep_seconds = 0
                call FormGroup(1,false)
            endif
        endif
    endloop
endfunction

//============================================================================
function SleepUntilTargetDead takes unit target returns nothing
    call CommonSleepUntilTargetDead(target,false)
endfunction

//============================================================================
function ReformUntilTargetDead takes unit target returns nothing
    debug call Trace("ReformUntilTargetDead\n")
    call CommonSleepUntilTargetDead(target,true)
endfunction

//============================================================================
function AttackMoveKillA takes unit target returns nothing
    if target == null then
        call SuicideSleep(3)
        return
    endif

    debug call Trace("AttackMoveKillA\n")
    call AttackMoveKill(target)
    call ReformUntilTargetDead(target)
    call SleepInCombat()
endfunction

//============================================================================
function MinorCreepAttack takes nothing returns nothing
    local unit target = GetMinorCreep()
    call SetAllianceTarget(target)
    call FormGroup(3, true)
    call AttackMoveKillA(target)
endfunction

//============================================================================
function MajorCreepAttack takes nothing returns nothing
    local unit target = GetMajorCreep()
    call SetAllianceTarget(target)
    call FormGroup(3,true)
    call AttackMoveKillA(target)
endfunction

//============================================================================
function CreepAttackEx takes nothing returns nothing
    local unit target = GetCreepCamp(min_creeps,max_creeps,allow_air_creeps)
    call SetAllianceTarget(target)
    call FormGroup(3,true)
    call AttackMoveKillA(target)
endfunction

//============================================================================
function AnyPlayerAttack takes nothing returns nothing
    local unit hall

    set hall = GetEnemyExpansion()
    if hall == null then
        call StartGetEnemyBase()
        loop
            exitwhen not WaitGetEnemyBase()
            call SuicideSleep(1)
        endloop
        set hall = GetEnemyBase()
    endif

    call SetAllianceTarget(hall)
    call FormGroup(3,true)
    call AttackMoveKillA(hall)
endfunction

//============================================================================
function ExpansionAttack takes nothing returns nothing
    local unit creep = GetExpansionFoe()
    local integer x

    call FormGroup(3, true)
    if creep == null then
        set x = GetExpansionX()
        if x != -1 then
            call AttackMoveXYA(x,GetExpansionY())
        endif
    else
        call AttackMoveKillA(creep)
    endif
endfunction

//============================================================================
//  AddSiege
//============================================================================
function AddSiege takes nothing returns nothing
    call SetAssaultGroup( 0, 9, SHADE       )
    call SetAssaultGroup( 0, 9, MEAT_WAGON  )
    call SetAssaultGroup( 0, 9, MORTAR      )
    call SetAssaultGroup( 0, 9, TANK        )
    call SetAssaultGroup( 0, 9, BALLISTA    )
    call SetAssaultGroup( 0, 9, CATAPULT    )
endfunction

//===========================================================================
//  GetAllyCount
//============================================================================
function GetAllyCount takes player whichPlayer returns integer
    local integer    playerIndex = 0
    local integer    count = 0
    local player     indexPlayer

    loop
        set indexPlayer = Player(playerIndex)

        if whichPlayer != indexPlayer then
            if GetPlayerAlliance(whichPlayer,indexPlayer,ALLIANCE_PASSIVE) then
                if GetPlayerAlliance(indexPlayer,whichPlayer,ALLIANCE_PASSIVE) then
                    if GetPlayerStructureCount(indexPlayer,true) > 0 then
                        set count = count + 1
                    endif
                endif
            endif
        endif

        set playerIndex = playerIndex + 1
        exitwhen playerIndex == 12
    endloop

    return count
endfunction

//============================================================================
//  SingleMeleeAttack
//============================================================================
function SingleMeleeAttack takes boolean needs_exp, boolean has_siege, boolean major_ok, boolean air_units returns nothing
    local boolean   can_siege
    local real      daytime
    local unit      hall
    local unit      mega
    local unit      creep
    local unit      common
    local integer   minimum
    local boolean   allies

    call Trace("===SingleMeleeAttack===\n") //xxx

    if TownThreatened() then
        call Trace("sleep 2, town threatened\n") //xxx
        call Sleep(2)
        return
    endif

    // purchase zeppelins
    //
    if get_zeppelin and GetGold() > 300 and GetWood() > 100 then
        call Trace("purchase zep\n") //xxx
        call PurchaseZeppelin()
        set get_zeppelin = false
        set ready_for_zeppelin = false
        return
    endif
    set ready_for_zeppelin = true

    // coordinate with allies
    //
    set allies = GetAllyCount(ai_player) > 0
    if allies and MeleeDifficulty() != MELEE_NEWBIE then
        set common = GetAllianceTarget()
        if common != null then
            call Trace("join ally force\n") //xxx
            if GetMegaTarget() != null then
                call AddSiege()
            endif
            call FormGroup(3,true)
            call AttackMoveKillA(common)
            call SetAllianceTarget(null)
            return
        endif
    endif

    // take expansions as needed
    //
    if needs_exp then
        call Trace("needs exp\n") //xxx
        set creep = GetExpansionFoe()
        if creep != null then
            call Trace("attack exp\n") //xxx
            call SetAllianceTarget(creep)
            call FormGroup(3,true)
            call AttackMoveKillA(creep)
            call Sleep(20)
            set take_exp = false
            return
        endif
    endif

    // all-out attack if the player is weak
    //
    if MeleeDifficulty() != MELEE_NEWBIE then
        set mega = GetMegaTarget()
        if mega != null then
            call Trace("MEGA TARGET!!!\n") //xxx
            call AddSiege()
            call FormGroup(3,true)
            call AttackMoveKillA(mega)
            return
        endif
    endif

    // deny player an expansion
    //
    set hall = GetEnemyExpansion()
    set daytime = GetFloatGameState(GAME_STATE_TIME_OF_DAY)
    set can_siege = has_siege and (air_units or (daytime>=4 and daytime<=12))

    if hall!=null and (can_siege or not IsTowered(hall)) then

        call Trace("test player town attack\n") //xxx

        if MeleeDifficulty() == MELEE_NEWBIE then
            set minimum = 3
        elseif allies and MeleeDifficulty() == MELEE_NORMAL then
            set minimum = 1
        else
            set minimum = 0 // HARD, INSANE, and NORMAL with no allies
        endif

        if exp_seen >= minimum then
            call Trace("do player town attack\n") //xxx
            set exp_seen = 0
            call AddSiege()
            call SetAllianceTarget(hall)
            call FormGroup(3,true)
            call AttackMoveKillA(hall)
            return
        endif

        set exp_seen = exp_seen + 1
    endif

    // attack player's main base when siege is available
    //
    if can_siege then
        call Trace("attack player's town\n") //xxx
        call AddSiege()
        call AnyPlayerAttack()
        return
    endif

    // extended, more specific method of determining creep levels
    //
    if min_creeps != -1 then
        call TraceI("custom creep attack %d\n",max_creeps) //xxx
        call CreepAttackEx()
        return
    endif

    // nothing better to do, so kill a creep camp
    //
    if major_ok then
        call Trace("major creep attack\n") //xxx
        call MajorCreepAttack()
        return
    endif

    call Trace("minor creep attack\n") //xxx
    call MinorCreepAttack()
endfunction

//============================================================================
function GetZeppelin takes nothing returns nothing
    if ready_for_zeppelin then
        set get_zeppelin = true
    endif
endfunction

//============================================================================
function FoodUsed takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_FOOD_USED)
endfunction

//============================================================================
function FoodCap takes nothing returns integer
    return GetPlayerState(ai_player,PLAYER_STATE_RESOURCE_FOOD_CAP)
endfunction

//============================================================================
function FoodSpace takes nothing returns integer
    return FoodCap() - FoodUsed()
endfunction

//============================================================================
function FoodAvail takes integer base returns integer
    return GetFoodMade(racial_farm) * TownCount(racial_farm) + GetFoodMade(base) * TownCount(base)
endfunction

//============================================================================
function BuildAttackers takes nothing returns nothing
    local integer index = 0
    local integer unitid
    local integer desire
    local integer count

    loop
        exitwhen index == harass_length

        set unitid = harass_units[index]
        set desire = harass_qty[index] + IgnoredUnits(unitid)
        set count  = TownCount(unitid)

        if count != desire then
            if not StartUnit(desire,unitid,-1) then
                return
            endif
        endif

        set index = index + 1
    endloop
endfunction

//============================================================================
function BuildDefenders takes nothing returns nothing
    local integer index = 0
    local integer unitid
    local integer qty
    loop
        exitwhen index == defense_length

        set unitid = defense_units[index]
        set qty = defense_qty[index]

        call Conversions(qty,unitid)
        call AddDefenders(qty,unitid)

        set index = index + 1
    endloop
endfunction

//============================================================================
function CampaignBasicsA takes nothing returns nothing
    local integer food_each = GetFoodMade(racial_farm)
    local integer on_wood

    call ClearHarvestAI()

    if CaptainInCombat(false) then
        set on_wood = 0
    else
        set on_wood = campaign_wood_peons
    endif

    call HarvestGold(0,campaign_gold_peons)
    call HarvestWood(0,on_wood)

    if harvest_town1 then
        call HarvestGold(1,campaign_gold_peons)
        call HarvestWood(1,on_wood)
    endif

    if harvest_town2 then
        call HarvestGold(2,campaign_gold_peons)
        call HarvestWood(2,on_wood)
    endif

    if harvest_town3 then
        call HarvestGold(3,campaign_gold_peons)
        call HarvestWood(3,on_wood)
    endif

    if do_campaign_farms and FoodUsed()+food_each-1 > food_each*(TownCount(racial_farm)+1) then
        call StartUnit(TownCount(racial_farm)+1,racial_farm,-1)
    endif

    if build_campaign_attackers then
        call BuildAttackers()
    endif

    if not CaptainInCombat(false) then
        call BuildDefenders()
    endif

    call FillGuardPosts()
    call ReturnGuardPosts()
endfunction

//============================================================================
function CampaignBasics takes nothing returns nothing
    call Sleep(1)
    call CampaignBasicsA()
    call StaggerSleep(1,5)
    loop
        call CampaignBasicsA()
        call Sleep(campaign_basics_speed)
    endloop
endfunction

//============================================================================
function CampaignAI takes integer farms, code heroes returns nothing
    if GetGameDifficulty() == MAP_DIFFICULTY_EASY then
        set difficulty = EASY

        call SetTargetHeroes(false)
        call SetUnitsFlee(false)

    elseif GetGameDifficulty() == MAP_DIFFICULTY_NORMAL then
        set difficulty = NORMAL

        call SetTargetHeroes(false)
        call SetUnitsFlee(false)

    elseif GetGameDifficulty() == MAP_DIFFICULTY_HARD then
        set difficulty = HARD

        call SetPeonsRepair(true)
    else
        set difficulty = INSANE
    endif

    call InitAI()
    call InitBuildArray()
    call InitAssaultGroup()
    call CreateCaptains()

    call SetNewHeroes(false)
    if heroes != null then
        call SetHeroLevels(heroes)
    endif

    call SetHeroesFlee(false)
    call SetGroupsFlee(false)
    call SetSlowChopping(true)
    call GroupTimedLife(false)
    call SetCampaignAI()
    call Sleep(0.1)

    set racial_farm = farms
    call StartThread(function CampaignBasics)
    call StartBuildLoop()
endfunction

//============================================================================
function UnsummonAll takes nothing returns nothing
    local unit bldg
    loop
        set bldg = GetBuilding(ai_player)
        exitwhen bldg==null
        call Unsummon(bldg)
        call Sleep(2)
    endloop
endfunction

//============================================================================
//  SkillArrays
//============================================================================
function SkillArrays takes nothing returns integer
    local integer level = GetHeroLevelAI()

    if level > max_hero_level then
        set max_hero_level = level
    endif

    if GetHeroId() == hero_id then
        return skills1[level]
    elseif GetHeroId() == hero_id2 then
        return skills2[level]
    else
        return skills3[level]
    endif
endfunction

//--------------------------------------------------------------------------------------------------
//  SetSkillArray
//--------------------------------------------------------------------------------------------------
function SetSkillArray takes integer index, integer id returns nothing
    local integer i = 1

    if index == 1 then
        if hero_id != id then
            return
        endif
        loop
            set skills1[i] = skill[i]
            exitwhen i == 10
            set i = i + 1
        endloop
    elseif index == 2 then
        if hero_id2 != id then
            return
        endif
        loop
            set skills2[i] = skill[i]
            exitwhen i == 10
            set i = i + 1
        endloop
    else
        if hero_id3 != id then
            return
        endif
        loop
            set skills3[i] = skill[i]
            exitwhen i == 10
            set i = i + 1
        endloop
    endif
endfunction

//============================================================================
//  AwaitMeleeHeroes
//============================================================================
function AwaitMeleeHeroes takes nothing returns nothing
    if GetUnitCountDone(hero_id2) > 0 then
        set two_heroes = true
    endif
    loop
        exitwhen GetUnitCountDone(hero_id)>0 and (take_exp or (not two_heroes or GetUnitCountDone(hero_id2)>0))
        call Sleep(1)
    endloop
endfunction

//============================================================================
//  PickMeleeHero
//============================================================================
function PickMeleeHero takes race raceid returns integer
    local integer first
    local integer second
    local integer third
    local integer last
    local integer array heroes

    //------------------------------------------------------------------------
    if raceid == RACE_HUMAN then
    //------------------------------------------------------------------------
        set heroes[1] = ARCHMAGE
        set heroes[2] = MTN_KING
        set heroes[3] = PALADIN
        set heroes[4] = BLOOD_MAGE

    //------------------------------------------------------------------------
    elseif raceid == RACE_ORC then
    //------------------------------------------------------------------------
        set heroes[1] = BLADE_MASTER
        set heroes[2] = FAR_SEER
        set heroes[3] = TAUREN_CHIEF
        set heroes[4] = SHADOW_HUNTER

    //------------------------------------------------------------------------
    elseif raceid == RACE_NIGHTELF then
    //------------------------------------------------------------------------
        set heroes[1] = DEMON_HUNTER
        set heroes[2] = KEEPER
        set heroes[3] = MOON_BABE
        set heroes[4] = WARDEN

    //------------------------------------------------------------------------
    elseif raceid == RACE_UNDEAD then
    //------------------------------------------------------------------------
        set heroes[1] = DEATH_KNIGHT
        set heroes[2] = DREAD_LORD
        set heroes[3] = LICH
        set heroes[4] = CRYPT_LORD
    else
        set hero_id = 0
    endif

    if VersionCompatible(VERSION_FROZEN_THRONE) then
        set last = 4
    else
        set last = 3
    endif

    set first  = GetRandomInt(1,last)
    set second = GetRandomInt(1,last-1)
    set third  = GetRandomInt(1,last-2)

    set hero_id        = heroes[first]
    set heroes[first]  = heroes[last]
    set hero_id2       = heroes[second]
    set heroes[second] = heroes[last-1]
    set hero_id3       = heroes[third]

    return hero_id
endfunction

//============================================================================
//  ADVANCED UTILITY
//============================================================================
function AdvDistance takes real x1, real y1, real x2, real y2 returns real
    return SquareRoot(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)))
endfunction

function AdvIsUnitAlive takes unit u returns boolean
    return GetUnitState(u, UNIT_STATE_LIFE) > 0.0
endfunction

function AdvDebugDisplayToPlayer takes string message returns nothing
    if debug then
        call DisplayTextToPlayer(debug_player, 0, 0, message)
    endif
    call Trace(message)
endfunction

//============================================================================
//  ADVANCED SETTINGS
//============================================================================
function AdvSetDebug takes player debugPlayer, boolean flag returns nothing
    set debug_player = debugPlayer
    set debug = flag
endfunction

//============================================================================

function AdvSetWaveReachGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_reach_goal_timeout_seconds = seconds
endfunction

function AdvSetWaveEnterCombatTimeoutSeconds takes integer seconds returns nothing
    set wave_enter_combat_timeout_seconds = seconds
endfunction

function AdvSetWaveClearGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_clear_goal_timeout_seconds = seconds
endfunction

//============================================================================

function AdvSetContinueFarAwayRadius takes real radius returns nothing
    set continue_far_away_radius = radius
endfunction

function AdvSetPreferredLocationsRadius takes real radius returns nothing
    set preferred_locations_radius = radius
endfunction

//============================================================================

function AdvSetWoodPeonsWarriors takes boolean flag returns nothing
    set wood_peons_warriors = flag
endfunction

//============================================================================

function AdvSetSearchPreferredLocations takes boolean flag returns nothing
    set search_preferred_locations = flag
endfunction

function AdvSetSearchEverywhere takes boolean flag returns nothing
    set search_everywhere = flag
endfunction

function AdvSetCanSearchUnit takes boolean flag returns nothing
    set can_search_units = flag
endfunction

function AdvSetCanSearchInvulnerable takes boolean flag returns nothing
    set can_search_invulnerable = flag
endfunction

//============================================================================

function AdvSetPrioritizeTownHalls takes boolean flag returns nothing
    set prioritize_town_halls = flag
endfunction

function AdvSetPrioritizeNearest takes boolean flag returns nothing
    set prioritize_nearest = flag
endfunction

function AdvSetSearchTargetMaxCyclesDefault takes integer value returns nothing
    set search_target_max_cycles_default = value
endfunction

function AdvSetSearchTargetMaxCyclesLastTarget takes integer value returns nothing
    set search_target_max_cycles_last_target = value
endfunction

//============================================================================

function AdvSetUseReachGoalTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction

function AdvSetUseEnterCombatTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction

function AdvSetUseClearGoalTimeout takes boolean flag returns nothing
    set enter_combat_timeout = flag
endfunction

//============================================================================

function AdvSetContinueAttackPercentage takes integer percentage returns nothing
    set continue_attack_percentage = percentage
endfunction

function AdvSetContinueAttackReducePercentageIfFarAway takes boolean flag returns nothing
    set continue_attack_reduce_percentage_if_far_away = flag
endfunction

function AdvSetContinueAttackReducePercentage takes integer percentage returns nothing
    set continue_attack_reduce_percentage = percentage
endfunction

//============================================================================

function AdvSetAttackWaveGatherReturnXY takes real x, real y returns nothing
    set attack_wave_gather_return_x = x
    set attack_wave_gather_return_y = y
    set attack_wave_gather_return_set = true
endfunction

function AdvSetDefenseCaptainHomeXY takes real x, real y returns nothing
    set defense_captain_home_x = x
    set defense_captain_home_y = y
    set defense_captain_home_set = true

endfunction

// Note: this should never be called in the script with argument different from DEFENSE_CAPTAIN
function AdvSetCaptainHome takes integer whichCaptain returns nothing

    if whichCaptain == BOTH_CAPTAINS then

        if defense_captain_home_set then
            call SetCaptainHome(DEFENSE_CAPTAIN, defense_captain_home_x, defense_captain_home_y)
        endif

        if attack_wave_gather_return_set then
            call SetCaptainHome(ATTACK_CAPTAIN, attack_wave_gather_return_x, attack_wave_gather_return_y)
        endif

        return

    endif

    if defense_captain_home_set and whichCaptain == DEFENSE_CAPTAIN then

        call SetCaptainHome(DEFENSE_CAPTAIN, defense_captain_home_x, defense_captain_home_y)
        return

    endif

    if attack_wave_gather_return_set and whichCaptain == ATTACK_CAPTAIN then

        call SetCaptainHome(ATTACK_CAPTAIN, attack_wave_gather_return_x, attack_wave_gather_return_y)
        return

    endif

endfunction

//============================================================================

function AdvAddPreferredLocation takes real x, real y returns integer
    set preferred_locations[preferred_locations_count] = Location(x, y)
    set preferred_locations_count = preferred_locations_count + 1

    return preferred_locations_count
endfunction

function AdvSetPreferredLocationXY takes integer index, real x, real y returns nothing
    if index < preferred_locations_count then
        set preferred_locations[index] = Location(x, y)
    endif
endfunction

//============================================================================

function AdvSetSuicide takes boolean flag returns nothing
    set suicide = flag
endfunction

//============================================================================
// ADVANCED SUICIDE WAVE LAUNCH
//============================================================================
function AdvGetRandomTargetableTownHall takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_town_halls_count - 1)

    if targetable_town_halls_count <= 0 then
        return null
    endif

    return targetable_town_halls[randomIndex]
endfunction

function AdvGetNearestTargetableTownHall takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance

    if targetable_town_halls_count <= 0 then
        return null
    endif

    set minDistance = AdvDistance(GetUnitX(targetable_town_halls[minDistanceIndex]), GetUnitY(targetable_town_halls[minDistanceIndex]), x, y)

    loop

        set index = index + 1
        exitwhen index >= targetable_town_halls_count

        if AdvDistance(GetUnitX(targetable_town_halls[index]), GetUnitY(targetable_town_halls[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_town_halls[minDistanceIndex]), GetUnitY(targetable_town_halls[minDistanceIndex]), x, y)
        endif

    endloop

    return targetable_town_halls[minDistanceIndex]

endfunction

//============================================================================

function AdvGetRandomTargetableBuilding takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_buildings_count - 1)

    if targetable_buildings_count <= 0 then
        return null
    endif

    return targetable_buildings[randomIndex]
endfunction

function AdvGetNearestTargetableBuilding takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance

    if targetable_buildings_count <= 0 then
        return null
    endif

    set minDistance = AdvDistance(GetUnitX(targetable_buildings[minDistanceIndex]), GetUnitY(targetable_buildings[minDistanceIndex]), x, y)

    loop

        set index = index + 1
        exitwhen index >= targetable_buildings_count

        if AdvDistance(GetUnitX(targetable_buildings[index]), GetUnitY(targetable_buildings[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_buildings[minDistanceIndex]), GetUnitY(targetable_buildings[minDistanceIndex]), x, y)
        endif

    endloop

    return targetable_buildings[minDistanceIndex]

endfunction

//============================================================================

function AdvGetRandomTargetableUnit takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_units_count - 1)

    if targetable_units_count <= 0 then
        return null
    endif

    return targetable_units[randomIndex]
endfunction

function AdvGetNearestTargetableUnit takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance

    if targetable_units_count <= 0 then
        return null
    endif

    set minDistance = AdvDistance(GetUnitX(targetable_units[minDistanceIndex]), GetUnitY(targetable_units[minDistanceIndex]), x, y)

    loop

        set index = index + 1
        exitwhen index >= targetable_units_count

        if AdvDistance(GetUnitX(targetable_units[index]), GetUnitY(targetable_units[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_units[minDistanceIndex]), GetUnitY(targetable_units[minDistanceIndex]), x, y)
        endif

    endloop

    return targetable_units[minDistanceIndex]

endfunction

//============================================================================

function AdvAssignTargetableTownHalls takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0

    set targetable_town_halls_count = 0

    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)
            call BlzGroupAddGroupFast(tempGroup, targetsGroup)

            set index = index + 1
            call GroupClear(tempGroup)

            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif

    if BlzGroupGetSize(targetsGroup) <= 0 then
        return false
    endif

    loop

        set currentUnit = FirstOfGroup(targetsGroup)

        exitwhen currentUnit == null

        if GetOwningPlayer(currentUnit) == targetPlayer and IsUnitType(currentUnit, UNIT_TYPE_TOWNHALL) and IsUnitType(currentUnit, UNIT_TYPE_STRUCTURE) and AdvIsUnitAlive(currentUnit) then
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_town_halls[targetable_town_halls_count] = currentUnit
                    set targetable_town_halls_count = targetable_town_halls_count + 1
                endif
            else
                set targetable_town_halls[targetable_town_halls_count] = currentUnit
                set targetable_town_halls_count = targetable_town_halls_count + 1
            endif
        endif

        call GroupRemoveUnit(targetsGroup, currentUnit)

    endloop

    if targetable_town_halls_count > 0 then
        return true
    endif

    return false

endfunction

//============================================================================

function AdvAssignTargetableBuildings takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0

    set targetable_buildings_count = 0

    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)
            call BlzGroupAddGroupFast(tempGroup, targetsGroup)

            set index = index + 1
            call GroupClear(tempGroup)

            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif

    if BlzGroupGetSize(targetsGroup) <= 0 then
        return false
    endif

    loop

        set currentUnit = FirstOfGroup(targetsGroup)

        exitwhen currentUnit == null

        if GetOwningPlayer(currentUnit) == targetPlayer and IsUnitType(currentUnit, UNIT_TYPE_STRUCTURE) and AdvIsUnitAlive(currentUnit) then
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_buildings[targetable_buildings_count] = currentUnit
                    set targetable_buildings_count = targetable_buildings_count + 1
                endif
            else
                set targetable_buildings[targetable_buildings_count] = currentUnit
                set targetable_buildings_count = targetable_buildings_count + 1
            endif
        endif

        call GroupRemoveUnit(targetsGroup, currentUnit)

    endloop

    if targetable_buildings_count > 0 then
        return true
    endif

    return false

endfunction

//============================================================================

function AdvAssignTargetableUnits takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0

    set targetable_units_count = 0

    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)
            call BlzGroupAddGroupFast(tempGroup, targetsGroup)

            set index = index + 1
            call GroupClear(tempGroup)

            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif

    if BlzGroupGetSize(targetsGroup) <= 0 then
        return false
    endif

    loop

        set currentUnit = FirstOfGroup(targetsGroup)

        exitwhen currentUnit == null

        if GetOwningPlayer(currentUnit) == targetPlayer and AdvIsUnitAlive(currentUnit) then
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_units[targetable_units_count] = currentUnit
                    set targetable_units_count = targetable_units_count + 1
                endif
            else
                set targetable_units[targetable_units_count] = currentUnit
                set targetable_units_count = targetable_units_count + 1
            endif
        endif

        call GroupRemoveUnit(targetsGroup, currentUnit)

    endloop

    if targetable_units_count > 0 then
        return true
    endif

    return false

endfunction

//============================================================================

function AdvSuicideWaveLaunch takes player targetPlayer, real startX, real startY, boolean searchPreferredLocations, boolean searchEverywhere, boolean canSearchUnits, boolean prioritizeTownHalls, boolean prioritizeNearest, boolean useLastTarget returns boolean
    local unit target = null
    local integer index = 0
    local real targetX
    local real targetY
    local integer searchTargetCycle = 0

    // Set the start coordinates
    if useLastTarget then
        set startX = last_target_x
        set startY = last_target_y
    endif

    call AdvDebugDisplayToPlayer("AI Info: searching for a target...")

    // --- LOOP UNTIL TARGET IS FOUND OR TIMEOUT IS REACHED ---
    loop

        exitwhen suicide

        // --- CACHE PREFERRED LOCATIONS ---
        set preferred_locations_count_current = 0
        if searchPreferredLocations and preferred_locations_count > 0 then

            set preferred_locations_count_current = preferred_locations_count
            loop

                set preferred_locations_current[index] = preferred_locations[index]

                set index = index + 1
                exitwhen index >= preferred_locations_count_current
            endloop

        endif

        // --- TOWN HALLS PREFERRED LOCATIONS ---
        if searchPreferredLocations and prioritizeTownHalls and preferred_locations_count_current > 0 then

            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls at preferred locations...")

            if AdvAssignTargetableTownHalls(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")

                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")
       
                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif

        endif

        exitwhen target != null

        // --- BUILDINGS PREFERRED LOCATIONS ---
        if searchPreferredLocations and preferred_locations_count_current > 0 then

            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings at preferred locations...")

            if AdvAssignTargetableBuildings(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")

                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")
       
                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: building target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: building target found!")
            endif

        endif

        exitwhen target != null

        // --- TOWN HALLS EVERYWHERE ----
        if searchEverywhere and prioritizeTownHalls then

            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls anywhere on the map...")

            if AdvAssignTargetableTownHalls(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")

                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")
       
                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif


        endif

        exitwhen target != null

        // --- BUILDINGS EVERYWHERE ---
        if searchEverywhere then

            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings anywhere on the map...")

            if AdvAssignTargetableBuildings(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")

                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")
       
                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif

                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: building target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: building target found!")
                endif

            endif

        endif

        exitwhen target != null

        // --- UNITS PREFERRED LOCATIONS ---
        if canSearchUnits and searchPreferredLocations and preferred_locations_count_current > 0 then

            call AdvDebugDisplayToPlayer("AI Info: looking for target units at preferred locations...")

            if AdvAssignTargetableUnits(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")

                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")
       
                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: unit target found!")
            endif

        endif

        exitwhen target != null

        // --- UNITS EVERYWHERE ---
        if canSearchUnits and searchEverywhere then

            call AdvDebugDisplayToPlayer("AI Info: looking for target units anywhere on the map...")

            if AdvAssignTargetableUnits(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")

                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")
       
                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif

                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: unit target found!")
                endif

            endif

        endif

        exitwhen target != null

        call Sleep(5)
        set searchTargetCycle = searchTargetCycle + 1

        // When using the last target as start point, we are continuing the attack, so cycle less times (a negative value means to cycle forever)
        if search_target_max_cycles_last_target > -1 then
            exitwhen useLastTarget and searchTargetCycle > search_target_max_cycles_last_target
        endif

        // Cycles timeout (a negative value means to cycle forever)
        if search_target_max_cycles_default > -1 then
            exitwhen searchTargetCycle > search_target_max_cycles_default
        endif

        call AdvDebugDisplayToPlayer("AI Info: no target found, trying again...")

    endloop

    // --- LAUNCH ATTACK IF THERE IS A TARGET---

    if target != null then

        call AdvDebugDisplayToPlayer("AI Info: assigning target coordinates and launching attack!")

        set targetX = GetUnitX(target)
        set targetY = GetUnitY(target)

        // Send the captain to attack and store last targets
        set last_target_x = targetX
        set last_target_y = targetY
        call SetCaptainHome(ATTACK_CAPTAIN, targetX,  targetY)
        call CaptainAttack(targetX, targetY)

        if not suicide then
            call Sleep(5)
        endif

        // Target found
        return true

    endif

    call AdvDebugDisplayToPlayer("AI Info: no target found, done trying!")

    // No target found
    return false

endfunction

//============================================================================
// ADVANCED SUICIDE WAVE ROUTINE
//============================================================================
function AdvWaveReachGoalSleep takes integer seconds returns nothing

    // Decrease counter seconds to reach the goal
    set wave_reach_goal_sleep_seconds = wave_reach_goal_sleep_seconds - seconds

    // Wait with precision
    loop

        exitwhen seconds <= 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif

    endloop

endfunction

function AdvWaveEnterCombatSleep takes integer seconds returns nothing

    // Decrease counter seconds to enter combat
    set wave_enter_combat_sleep_seconds = wave_enter_combat_sleep_seconds - seconds

    // Wait with precision
    loop

        exitwhen seconds <= 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif

    endloop

endfunction

function AdvWaveClearGoalSleep takes integer seconds returns nothing

    // Decrease counter seconds to die
    set wave_clear_goal_sleep_seconds = wave_clear_goal_sleep_seconds - seconds

    // Wait with precision
    loop

        exitwhen seconds <= 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif

    endloop

endfunction

//============================================================================

function AdvSuicideWaveRoutineReachGoal takes boolean timeout returns integer
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean allDead = false
    local boolean outOfTime = false

    loop

        exitwhen suicide

        // Goal reached
        if CaptainAtGoal() then
            set goalReached = true
        endif
        exitwhen CaptainAtGoal()

        // Entered combat before reaching goal
        if CaptainInCombat(true) then
            set combatEntered = true
        endif

        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()

        call AdvWaveReachGoalSleep(2)

        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds
        endif

    endloop

    // Goal is reached and fought before
    if goalReached and combatEntered then
        return 1
    // Goal is reached and not fought before
    elseif goalReached and not combatEntered then
        return 2
    // Attack wave is dead (no matter having fought)
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif

endfunction

function AdvSuicideWaveRoutineEnterCombat takes boolean timeout returns integer
    local boolean combatEntered = false
    local boolean allDead = false
    local boolean outOfTime = false

    loop

        exitwhen suicide

        // Combat
        if CaptainInCombat(true) then
            set combatEntered = true
        endif
        exitwhen CaptainInCombat(true)

        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()

        call AdvWaveEnterCombatSleep(1)

        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_enter_combat_sleep_seconds < -wave_enter_combat_timeout_seconds
        endif

    endloop

    // Combat entered
    if combatEntered then
        return 1
    // Attack wave is dead
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif

endfunction

function AdvSuicideWaveRoutineClearGoal takes boolean timeout returns integer
    local boolean goalCleared = false
    local boolean allDead = false
    local boolean outOfTime = false

    loop

        exitwhen suicide

        // Goal cleared
        if not CaptainInCombat(true) then
            set goalCleared = true
        endif
        exitwhen not CaptainInCombat(true)

        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()

        call AdvWaveClearGoalSleep(2)

        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_clear_goal_sleep_seconds < -wave_clear_goal_timeout_seconds
        endif

    endloop

    // Goal cleared
    if goalCleared then
        return 1
    // Attack wave is dead
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif

endfunction

//============================================================================

function AdvSuicideWaveRoutine takes player targetPlayer, real startX, real startY, boolean reachGoalTimeout, boolean enterCombatTimeout, boolean clearGoalTimeout, integer continuePercentage returns nothing
    local integer continuePercentageNew = continuePercentage
    local real previousTargetX = last_target_x
    local real previousTargetY = last_target_y
    local integer state = 0
    local boolean combatEnteredBeforeGoal = false
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean goalCleared = false
    local boolean continueAttackReducePercentageIfFarAway = continue_attack_reduce_percentage_if_far_away
    local boolean newTargetFound = false

    // Reset all timeout seconds
    set wave_reach_goal_sleep_seconds = 0
    set wave_enter_combat_sleep_seconds = 0
    set wave_clear_goal_sleep_seconds = 0

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to reach goal...")

    // --- WAIT TO REACH GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineReachGoal(reachGoalTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached timeout!")

        set goalReached = false
        set combatEnteredBeforeGoal = false

    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set goalReached = false
        set combatEnteredBeforeGoal = false

    // Goal reached after figthing
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached after figthing!")

        set goalReached = true
        set combatEnteredBeforeGoal = true

    // Goal reached before figthing
    elseif state == 2 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached before figthing!")

        set goalReached = true
        set combatEnteredBeforeGoal = false

    endif

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to enter combat...")

    // --- WAIT TO ENTER COMBAT (OR DIE) ---
    set state = AdvSuicideWaveRoutineEnterCombat(enterCombatTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave enter combat reached timeout!")

        set combatEntered = false

    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set combatEntered = true

    // Combat entered
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave combat entered!")

        set combatEntered = true

    endif

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to clear goal...")

    // --- WAIT TO CLEAR GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineClearGoal(clearGoalTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave clear goal reached timeout!")

        set combatEntered = false

    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set combatEntered = true

    // Goal cleared
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal cleared!")

        set goalCleared = true

    endif

    // --- GROUP DEAD ---
    if CaptainIsEmpty() then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead, going back home...")

        call SetCaptainHome(ATTACK_CAPTAIN, startX,  startY)
        call CaptainAttack(startX, startY)

        if not suicide then
            call Sleep(1)
        endif

        call TeleportCaptain(startX, startY)

        // Wait for the captain to go back home

        loop

            call Sleep(1)

            exitwhen suicide
            exitwhen CaptainAtGoal()

        endloop

        call AdvDebugDisplayToPlayer("AI Info: returned home!")

        call ClearCaptainTargets()
        call ResetCaptainLocs()
        call AdvSetCaptainHome(BOTH_CAPTAINS)

        if not suicide then
            call Sleep(2)
        endif

        return

    endif

    // --- ATTACK FINISHED CONTINUE (IF REQUIRED) ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave finished duty, checking if  should continue...")

    // If attack wave has not fought before reaching goal and then never fought again, it must continue
    if goalReached and not combatEnteredBeforeGoal and not combatEntered then

        call AdvDebugDisplayToPlayer("AI Info: attack wave never fought at goal nor to reach it, forcing continue...")

        set continuePercentage = 100
        set continueAttackReducePercentageIfFarAway = false
    endif

    if not suicide and not CaptainIsEmpty() and GetRandomInt(0, 100) <= continuePercentage then

        call AdvDebugDisplayToPlayer("AI Info: attack wave continues to another target!")

        set newTargetFound = AdvSuicideWaveLaunch(targetPlayer, 0.0, 0.0, false, true, can_search_units, false, true, true)

        if newTargetFound then
            // If target is not close to previous one, decrease the continue percentage (if required)
            if continueAttackReducePercentageIfFarAway and AdvDistance(previousTargetX, previousTargetY, last_target_x, last_target_y) > continue_far_away_radius then
                call AdvDebugDisplayToPlayer("AI Info: reducing percentage of continuing again...")

                set continuePercentageNew = continuePercentageNew - continue_attack_reduce_percentage
            endif

            call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continuePercentageNew)

            return
        endif

    endif

    // --- ATTACK FINISHED GO HOME ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave does not continue, going back home...")

    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)

    // Wait for the captain to go back home
    loop

        call Sleep(1)

        exitwhen suicide
        exitwhen CaptainAtGoal()

    endloop

    call AdvDebugDisplayToPlayer("AI Info: returned home!")

    call ClearCaptainTargets()
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)

    if not suicide then
        call Sleep(2)
    endif

endfunction

//============================================================================
// ADVANCED SUICIDE ON PLAYER
//============================================================================
function AdvSuicideOnPlayer takes integer seconds, player targetPlayer, real startX, real startY returns nothing
    local integer woodPeons
    local boolean targetFound = false

    call AdvDebugDisplayToPlayer("AI Info: waiting to attack...")

    // Make sure the captain are at right spots
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)

    if not suicide then
        call Sleep(2)
    endif

    // Prepare attack for given seconds
    if not PrepSuicideOnPlayer(seconds) then
        return
    endif

    call AdvDebugDisplayToPlayer("AI Info: wait is over!")

    // If necessary (e.g. undead race), save the wood peons before attack group is prepared
    if wood_peons_warriors then
        set woodPeons = campaign_wood_peons
        set campaign_wood_peons = 0
    endif

    call AdvDebugDisplayToPlayer("AI Info: forming group...")

    loop

        exitwhen suicide

        call FormGroup(5, true)
        exitwhen sleep_seconds <= 0

    endloop

    call AdvDebugDisplayToPlayer("AI Info: group formed!")

    // If necessary (e.g. undead race), reset the wood peons after attack group is ready
    if wood_peons_warriors then
        set campaign_wood_peons = woodPeons
    endif

    // Travel to start position
    call AdvDebugDisplayToPlayer("AI Info: traveling to start position...")

    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)

    loop

        call Sleep(1)

        exitwhen suicide
        exitwhen CaptainAtGoal()
        exitwhen CaptainIsEmpty()

    endloop

    // Stop here if captain is empty
    if CaptainIsEmpty() then
        return
    endif

    call AdvDebugDisplayToPlayer("AI Info: start position reached!")

    // Launch the wave against a suitable target
    set targetFound = AdvSuicideWaveLaunch(targetPlayer, startX, startY, search_preferred_locations, search_everywhere, can_search_units, prioritize_town_halls, prioritize_nearest, false)

    set harass_length = 0

    // Start the wave routine if there is a target
    if targetFound then
        call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continue_attack_percentage)
    endif

endfunction

//============================================================================

function AdvSuicideOnPlayerEx takes integer easy, integer med, integer hard, player targetPlayer, real startX, real startY returns nothing

    if difficulty == EASY then
        call AdvSuicideOnPlayer(easy, targetPlayer, startX, startY)
    elseif difficulty == NORMAL then
        call AdvSuicideOnPlayer(med, targetPlayer, startX, startY)
    else
        call AdvSuicideOnPlayer(hard, targetPlayer, startX, startY)
    endif

endfunction

KNOWLEDGE REQUIRED
TOOLS REQUIRED
  • Warcraft 3 World Editor
  • Notepad++ (highly recommended) or any other text editor
TABLE OF CONTENT
  1. ADVANCED SUICIDE ON PLAYER
  2. ADVANCED ATTACK WAVE ROUTINE
  3. USAGE AND CUSTOMIZATION
  4. HOW TO IMPORT AND USAGE EXAMPLE

ADVANCED SUICIDE ON PLAYER
This section quickly describes how the new SuicideOnPlayer functions, AdvSuicideOnPlayer, are structured and why they are designed this way. Please don't expect a complete description of each and every line of code, for it's too complex for this tutorial.

The core of the new attack wave routine is the so called AdvSuicideOnPlayer and the associated function AdvSuicideOnPlayerEx, where Adv stands for Advanced. These two functions mimic and reimagine the behaviour of the original SuicideOnPlayer and SuicideOnPlayerEx functions, which you can find in the common.ai file of the game. The two functions are shown below:


JASS:
//============================================================================
// ADVANCED SUICIDE ON PLAYER
//============================================================================
function AdvSuicideOnPlayer takes integer seconds, player targetPlayer, real startX, real startY returns nothing
    local integer woodPeons
    local boolean targetFound = false

    call AdvDebugDisplayToPlayer("AI Info: waiting to attack...")

    // Make sure the captain are at right spots
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)

    if not suicide then
        call Sleep(2)
    endif

    // Prepare attack for given seconds
    if not PrepSuicideOnPlayer(seconds) then
        return
    endif

    call AdvDebugDisplayToPlayer("AI Info: wait is over!")

    // If necessary (e.g. undead race), save the wood peons before attack group is prepared
    if wood_peons_warriors then
        set woodPeons = campaign_wood_peons
        set campaign_wood_peons = 0
    endif

    call AdvDebugDisplayToPlayer("AI Info: forming group...")

    loop

        exitwhen suicide

        call FormGroup(5, true)
        exitwhen sleep_seconds <= 0

    endloop

    call AdvDebugDisplayToPlayer("AI Info: group formed!")

    // If necessary (e.g. undead race), reset the wood peons after attack group is ready
    if wood_peons_warriors then
        set campaign_wood_peons = woodPeons
    endif

    // Travel to start position
    call AdvDebugDisplayToPlayer("AI Info: traveling to start position...")

    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)

    loop

        call Sleep(1)

        exitwhen suicide
        exitwhen CaptainAtGoal()
        exitwhen CaptainIsEmpty()

    endloop

    // Stop here if captain is empty
    if CaptainIsEmpty() then
        return
    endif

    call AdvDebugDisplayToPlayer("AI Info: start position reached!")

    // Launch the wave against a suitable target
    set targetFound = AdvSuicideWaveLaunch(targetPlayer, startX, startY, search_preferred_locations, search_everywhere, can_search_units, prioritize_town_halls, prioritize_nearest, false)

    set harass_length = 0

    // Start the wave routine if there is a target
    if targetFound then
        call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continue_attack_percentage)
    endif

endfunction

//============================================================================

function AdvSuicideOnPlayerEx takes integer easy, integer med, integer hard, player targetPlayer, real startX, real startY returns nothing

    if difficulty == EASY then
        call AdvSuicideOnPlayer(easy, targetPlayer, startX, startY)
    elseif difficulty == NORMAL then
        call AdvSuicideOnPlayer(med, targetPlayer, startX, startY)
    else
        call AdvSuicideOnPlayer(hard, targetPlayer, startX, startY)
    endif

endfunction

Starting from the bottom, AdvSuicideOnPlayerEx just calls the AdvSuicideOnPlayer with different timings, depending on the difficulty. This is exactly the same of the original function SuicideOnPlayerEx. You can see there are two additional parameters, startX and startY. Those two parameters are the gathering position of the attack wave once it's formed. Basically, this attack wave, once formed in the base as usual, will travel to the position (startX, startY) and launch the attack from there. It will also use that position to compute the nearest target, if required.

Now, AdvSuicideOnPlayer is the main function, whose parameters are basically shared with AdvSuicideOnPlayerEx. However, the function itself is quite more complex, lets see it in the details:
  • It prepares the attack as usual, then it checks if the peons are possible warriors for the attack wave (a configurable property) and, if not, it will avoid that strange behaviour of default AI players' peons stop chopping while the attack wave is gathering
  • It moves the attack wave to the start position and wait till it's reached or the attack wave is destroyed
  • If the attack wave is not destroyed, it launches the attack wave against a suitable target and then enables the attack wave routine, which is a blocking function (this allows the designer to write attack loops exactly the same way the default routines do)
What about the target assignment and the main attack wave routine? We'll discuss them in the next section.


ADVANCED ATTACK WAVE ROUTINE
This section quickly describes how the new attack wave routine is structured, with a brief explanation of the two main functions composing it: AdvSuicideWaveLaunch and AdvSuicideWaveRoutine. Please don't expect a complete description of each and every line of code, for it's too complex for this tutorial.

The SuicideOnPlayer functions are just the "interface" the designer calls when developing its AI scripts, but there quite some stuff going underneath.
By looking at AdvSuicideOnPlayer function in previous section, it's possible to see two functions called at the end, respectively AdvSuicideWaveLaunch, to block execution until a target is chosen, and AdvSuicideWaveRoutine, to block execution until the attack wave is successful or is defeated. This is a very rough description, because a lot of customization is possible, but we'll discuss customization in another section of this tutorial. Likewise, we will see a lot of references to preferred locations. Such locations are customizable both in their amount, (x, y) positions and radius.

The first function, AdvSuicideWaveLaunch, looks like that:


JASS:
//============================================================================

function AdvSuicideWaveLaunch takes player targetPlayer, real startX, real startY, boolean searchPreferredLocations, boolean searchEverywhere, boolean canSearchUnits, boolean prioritizeTownHalls, boolean prioritizeNearest, boolean useLastTarget returns boolean
    local unit target = null
    local integer index = 0
    local real targetX
    local real targetY
    local integer searchTargetCycle = 0

    // Set the start coordinates
    if useLastTarget then
        set startX = last_target_x
        set startY = last_target_y
    endif

    call AdvDebugDisplayToPlayer("AI Info: searching for a target...")

    // --- LOOP UNTIL TARGET IS FOUND OR TIMEOUT IS REACHED ---
    loop

        exitwhen suicide

        // --- CACHE PREFERRED LOCATIONS ---
        set preferred_locations_count_current = 0
        if searchPreferredLocations and preferred_locations_count > 0 then

            set preferred_locations_count_current = preferred_locations_count
            loop

                set preferred_locations_current[index] = preferred_locations[index]

                set index = index + 1
                exitwhen index >= preferred_locations_count_current
            endloop

        endif

        // --- TOWN HALLS PREFERRED LOCATIONS ---
        if searchPreferredLocations and prioritizeTownHalls and preferred_locations_count_current > 0 then

            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls at preferred locations...")

            if AdvAssignTargetableTownHalls(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")

                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")

                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif

        endif

        exitwhen target != null

        // --- BUILDINGS PREFERRED LOCATIONS ---
        if searchPreferredLocations and preferred_locations_count_current > 0 then

            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings at preferred locations...")

            if AdvAssignTargetableBuildings(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")

                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")

                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: building target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: building target found!")
            endif

        endif

        exitwhen target != null

        // --- TOWN HALLS EVERYWHERE ----
        if searchEverywhere and prioritizeTownHalls then

            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls anywhere on the map...")

            if AdvAssignTargetableTownHalls(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")

                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")

                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif


        endif

        exitwhen target != null

        // --- BUILDINGS EVERYWHERE ---
        if searchEverywhere then

            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings anywhere on the map...")

            if AdvAssignTargetableBuildings(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")

                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")

                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif

                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: building target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: building target found!")
                endif

            endif

        endif

        exitwhen target != null

        // --- UNITS PREFERRED LOCATIONS ---
        if canSearchUnits and searchPreferredLocations and preferred_locations_count_current > 0 then

            call AdvDebugDisplayToPlayer("AI Info: looking for target units at preferred locations...")

            if AdvAssignTargetableUnits(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")

                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")

                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif

            endif

            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: unit target found!")
            endif

        endif

        exitwhen target != null

        // --- UNITS EVERYWHERE ---
        if canSearchUnits and searchEverywhere then

            call AdvDebugDisplayToPlayer("AI Info: looking for target units anywhere on the map...")

            if AdvAssignTargetableUnits(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")

                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")

                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)

                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")

                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif

                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: unit target found!")
                endif

            endif

        endif

        exitwhen target != null

        call Sleep(5)
        set searchTargetCycle = searchTargetCycle + 1

        // When using the last target as start point, we are continuing the attack, so cycle less times (a negative value means to cycle forever)
        if search_target_max_cycles_last_target > -1 then
            exitwhen useLastTarget and searchTargetCycle > search_target_max_cycles_last_target
        endif

        // Cycles timeout (a negative value means to cycle forever)
        if search_target_max_cycles_default > -1 then
            exitwhen searchTargetCycle > search_target_max_cycles_default
        endif

        call AdvDebugDisplayToPlayer("AI Info: no target found, trying again...")

    endloop

    // --- LAUNCH ATTACK IF THERE IS A TARGET---

    if target != null then

        call AdvDebugDisplayToPlayer("AI Info: assigning target coordinates and launching attack!")

        set targetX = GetUnitX(target)
        set targetY = GetUnitY(target)

        // Send the captain to attack and store last targets
        set last_target_x = targetX
        set last_target_y = targetY
        call SetCaptainHome(ATTACK_CAPTAIN, targetX,  targetY)
        call CaptainAttack(targetX, targetY)

        if not suicide then
            call Sleep(5)
        endif

        // Target found
        return true

    endif

    call AdvDebugDisplayToPlayer("AI Info: no target found, done trying!")

    // No target found
    return false

endfunction

This function is very straightforward in its design. Basically, it does the following, in order:
  1. It searches for target unit up until a certain amount of times (possibly indefinitely) or when suicide flag is true
  2. It launches attack to location of chosen target unit, saving last target location to global variables
While the second part is quite easy to understand, the first one is composed of multiple steps in a loop, in a specific order. Please note that loop ends whenever a target is found or when the maximum amount (configurable) of possible iterations of the loop itself are reached:
  1. If there are preferred locations, cache them to avoid any kind of multithreaded trouble (AI functions can and usually do run on multiple threads).
  2. If there are preferred locations, preferred locations are search target and town halls targets are to prioritize, search for town halls at preferred locations. Choose the nearest one among all of those at any preferred location if required, or a random one (this automatically increase the chance of attacking a preferred location with more town halls).
  3. If there are preferred locations and preferred locations are search target, search for buildings at preferred locations. Choose the nearest one among all of those at any preferred location if required, or a random one (this automatically increase the chance of attacking a preferred location with more buildings).
  4. If there are no proferred locations or preferred locations are not a search target, but town halls targets are to prioritize and it's possible to search everywhere on the map, search for any town hall. Choose the nearest one if required, or a random one.
  5. If there are no proferred locations or preferred locations are not a search target, but it's possible to search everywhere on the map, search for any building. Choose the nearest one if required, or a random one.
  6. If there are preferred locations, preferred locations are search target and any units target is allowed, search for units at preferred locations. Choose the nearest one among all of those at any preferred location if required, or a random one (this automatically increase the chance of attacking a preferred location with more units).
  7. If there are no preferred locations or preferred locations are not a search target, but any units target is allowed and it's possible to search everywhere on the map, search for any unit. Choose the nearest one if required, or a random one.
All the parameters to rule this search system are cached as arguments for the function. When the function is called, global values are passed in function AdvSuicideOnPlayer as parameters so they are immutable through all the execution of the function. This way, when an attack wave is started (when the attack party reaches the start location) with certain global parameters, it retains the values of these parameters until the wave terminates (success or defeat), but the designer is free to change the values in the meantime. Please note that this doesn't hold true while the attack wave is gathering. In addition, some values are not cached, using directly the global variables, allowing the designer to change some specific behaviours of an ongoing wave. Check out these values by reading the source code.

The second function AdvSuicideWaveRoutine is the actual core routine of the attack wave and it decides whether or not the wave should continue or stop (considering the attack a success or a defeat). The function is as follows:


JASS:
//============================================================================

function AdvSuicideWaveRoutine takes player targetPlayer, real startX, real startY, boolean reachGoalTimeout, boolean enterCombatTimeout, boolean clearGoalTimeout, integer continuePercentage returns nothing
    local integer continuePercentageNew = continuePercentage
    local real previousTargetX = last_target_x
    local real previousTargetY = last_target_y
    local integer state = 0
    local boolean combatEnteredBeforeGoal = false
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean goalCleared = false
    local boolean continueAttackReducePercentageIfFarAway = continue_attack_reduce_percentage_if_far_away
    local boolean newTargetFound = false

    // Reset all timeout seconds
    set wave_reach_goal_sleep_seconds = 0
    set wave_enter_combat_sleep_seconds = 0
    set wave_clear_goal_sleep_seconds = 0

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to reach goal...")

    // --- WAIT TO REACH GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineReachGoal(reachGoalTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached timeout!")

        set goalReached = false
        set combatEnteredBeforeGoal = false

    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set goalReached = false
        set combatEnteredBeforeGoal = false

    // Goal reached after figthing
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached after figthing!")

        set goalReached = true
        set combatEnteredBeforeGoal = true

    // Goal reached before figthing
    elseif state == 2 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached before figthing!")

        set goalReached = true
        set combatEnteredBeforeGoal = false

    endif

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to enter combat...")

    // --- WAIT TO ENTER COMBAT (OR DIE) ---
    set state = AdvSuicideWaveRoutineEnterCombat(enterCombatTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave enter combat reached timeout!")

        set combatEntered = false

    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set combatEntered = true

    // Combat entered
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave combat entered!")

        set combatEntered = true

    endif

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to clear goal...")

    // --- WAIT TO CLEAR GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineClearGoal(clearGoalTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave clear goal reached timeout!")

        set combatEntered = false

    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set combatEntered = true

    // Goal cleared
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal cleared!")

        set goalCleared = true

    endif

    // --- GROUP DEAD ---
    if CaptainIsEmpty() then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead, going back home...")

        call SetCaptainHome(ATTACK_CAPTAIN, startX,  startY)
        call CaptainAttack(startX, startY)

        if not suicide then
            call Sleep(1)
        endif

        call TeleportCaptain(startX, startY)

        // Wait for the captain to go back home

        loop

            call Sleep(1)

            exitwhen suicide
            exitwhen CaptainAtGoal()

        endloop

        call AdvDebugDisplayToPlayer("AI Info: returned home!")

        call ClearCaptainTargets()
        call ResetCaptainLocs()
        call AdvSetCaptainHome(BOTH_CAPTAINS)

        if not suicide then
            call Sleep(2)
        endif

        return

    endif

    // --- ATTACK FINISHED CONTINUE (IF REQUIRED) ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave finished duty, checking if  should continue...")

    // If attack wave has not fought before reaching goal and then never fought again, it must continue
    if goalReached and not combatEnteredBeforeGoal and not combatEntered then

        call AdvDebugDisplayToPlayer("AI Info: attack wave never fought at goal nor to reach it, forcing continue...")

        set continuePercentage = 100
        set continueAttackReducePercentageIfFarAway = false
    endif

    if not suicide and not CaptainIsEmpty() and GetRandomInt(0, 100) <= continuePercentage then

        call AdvDebugDisplayToPlayer("AI Info: attack wave continues to another target!")

        set newTargetFound = AdvSuicideWaveLaunch(targetPlayer, 0.0, 0.0, false, true, can_search_units, false, true, true)

        if newTargetFound then
            // If target is not close to previous one, decrease the continue percentage (if required)
            if continueAttackReducePercentageIfFarAway and AdvDistance(previousTargetX, previousTargetY, last_target_x, last_target_y) > continue_far_away_radius then
                call AdvDebugDisplayToPlayer("AI Info: reducing percentage of continuing again...")

                set continuePercentageNew = continuePercentageNew - continue_attack_reduce_percentage
            endif

            call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continuePercentageNew)

            return
        endif

    endif

    // --- ATTACK FINISHED GO HOME ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave does not continue, going back home...")

    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)

    // Wait for the captain to go back home
    loop

        call Sleep(1)

        exitwhen suicide
        exitwhen CaptainAtGoal()

    endloop

    call AdvDebugDisplayToPlayer("AI Info: returned home!")

    call ClearCaptainTargets()
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)

    if not suicide then
        call Sleep(2)
    endif

endfunction

This function executes a set of checks contained in multiple auxiliary functions. Each one of those consists in a loop, with varying exiting conditions.
When a certain function exits, the flow returns to the AdvSuicideWaveRoutine function, which proceeds with following stages. While the description of each one of the auxiliary functions is outside of the scope of this tutorial, the general flow is as follows:
  1. Wait for the attack wave to reach goal or die trying. A timeout can be used and it's configurable as part of each script's possible customization.
  2. Wait for the attack wave to enter combat or die trying. A timeout can be used and it's configurable as part of each script's possible customization.
  3. Wait for the attack wave to clear the goal or die trying. A timeout can be used and it's configurable as part of each script's possible customization.
  4. Check if the attack wave party is dead. If it is dead, the wave is considered defeated and eventual following waves can now be prepared, exiting the wave routine.
  5. If the attack wave party is not dead, the wave is considered successful and further decision should be taken.
  6. If the wave is successful but the group never entered combat, it means enemies have been avoided or the pathfinding algorithm bugged itself. In this case, the attack wave needs to continue to another target. Such target is chosen as closest to the last target and it retains some of original configuration (e.g. if to prioritize town halls, etc.).
  7. If the attack wave is successful and the group entered combat, it means the attack wave fought to defeat the enemies. In this case, the attack wave select a new closest target with random chance, using a cached configurable parameter. This new attack wave retains some of original configuration (e.g. if to prioritize town halls, etc.) but uses last target as starting point to compute what is considered closest. If the closest target is above a configurable distance, the system can reduce the chance of that happening again in the future. Both the reduction amount and if to reduce are configurable.
  8. If no new target is available or the attack wave should not continue, the attack party goes home, exiting the wave routine.
  9. If a new target is available and the attack wave should continue, a new attack wave with such target is launched, calling recursively a new wave routine.
It is possible to note a funny behaviour, which is what I was referring to when I was talking about the pathfinding algorithm bugging itself. Basically, the native to set a target for the attack party could not work properly, considering the goal reached just after a few seconds, no matter where the attack wave party actually is on the map. This is probably why the natives used by Blizzard to choose a target of a certain player, in function CommonSuicideOnPlayer, are called inside a loop.
In the new system, there is a bit of additional delay if the pathfiding algorithm bugs out. Specifically, you will be able to see the attack party start moving towards its target and then stopping. It could do so multiple times, before actually deciding to attack. There is no way to fix this, because I would need to fix the natives, but you should not be worried. Beside the small delay, the attack party will for sure attack the destination as designed thanks to the sequence of checks described above.


USAGE AND CUSTOMIZATION
This section describes how to use and customize the new routines in the common AI 2.0, by explaining each configuration function and the relative effect on the overall AI player's behaviour.

To use the advanced routines, it's necessary first to add them into your game. To do so, see the next and final section of this tutorial, or the first page of the system. To use them in your AI script, you just need to change each SuicideOnPlayerEx function with AdvSuicideOnPlayerEx function.
It's possible to mix the two as you see fit, using default routines for certain attacks and the advanced routines for others, seamlessy.
The new function AdvSuicideOnPlayerEx has two additional parameters, specifically the (x, y) of the start position. The start position is a specific position the attack wave must reach before actually starting the attack.

One of the features of the advanced routines are preferred locations, areas the AI can prioritize when choosing targets. It's a perfect way to define multiple target areas for the AI to choose from, allowing the game designer to inject a much more configurable attack behaviour into the AI scripts. To add preferred locations, check out the following functions:


JASS:
//============================================================================

function AdvAddPreferredLocation takes real x, real y returns integer
    set preferred_locations[preferred_locations_count] = Location(x, y)
    set preferred_locations_count = preferred_locations_count + 1

    return preferred_locations_count
endfunction

function AdvSetPreferredLocationXY takes integer index, real x, real y returns nothing
    if index < preferred_locations_count then
        set preferred_locations[index] = Location(x, y)
    endif
endfunction

It's possible to use the first function to both add a new preferred location and to get the amount of preferred locations, which can be used to compute the index, parameter of the second function. The second function is used to change an existing preferred location.

One of the stronger advantages of the new routines is they are easy to customize. Indeed, the customization allows the designer to make the AI follows specific behaviours, like, but not limited to, the following:
  • Attack at certain locations or not
  • Attack buildings (possibly prioritizing town halls) or also other units
  • The time (in seconds) to wait for each stage (more info about that in previous sections) of the attack wave to complete
  • If to continue attacking (with a percentage) and what to consider far away enough to reduce such percentage
  • Easier to use debug prints
This small list does not make the configuration, or settings, functions justice. For a complete list, see what follows. You are free to experiment with them as you see fit:


JASS:
//============================================================================
//  ADVANCED SETTINGS
//============================================================================
function AdvSetDebug takes player debugPlayer, boolean flag returns nothing
    set debug_player = debugPlayer
    set debug = flag
endfunction

//============================================================================

function AdvSetWaveReachGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_reach_goal_timeout_seconds = seconds
endfunction

function AdvSetWaveEnterCombatTimeoutSeconds takes integer seconds returns nothing
    set wave_enter_combat_timeout_seconds = seconds
endfunction

function AdvSetWaveClearGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_clear_goal_timeout_seconds = seconds
endfunction

//============================================================================

function AdvSetContinueFarAwayRadius takes real radius returns nothing
    set continue_far_away_radius = radius
endfunction

function AdvSetPreferredLocationsRadius takes real radius returns nothing
    set preferred_locations_radius = radius
endfunction

//============================================================================

function AdvSetWoodPeonsWarriors takes boolean flag returns nothing
    set wood_peons_warriors = flag
endfunction

//============================================================================

function AdvSetSearchPreferredLocations takes boolean flag returns nothing
    set search_preferred_locations = flag
endfunction

function AdvSetSearchEverywhere takes boolean flag returns nothing
    set search_everywhere = flag
endfunction

function AdvSetCanSearchUnit takes boolean flag returns nothing
    set can_search_units = flag
endfunction

function AdvSetCanSearchInvulnerable takes boolean flag returns nothing
    set can_search_invulnerable = flag
endfunction

//============================================================================

function AdvSetPrioritizeTownHalls takes boolean flag returns nothing
    set prioritize_town_halls = flag
endfunction

function AdvSetPrioritizeNearest takes boolean flag returns nothing
    set prioritize_nearest = flag
endfunction

function AdvSetSearchTargetMaxCyclesDefault takes integer value returns nothing
    set search_target_max_cycles_default = value
endfunction

function AdvSetSearchTargetMaxCyclesLastTarget takes integer value returns nothing
    set search_target_max_cycles_last_target = value
endfunction

//============================================================================

function AdvSetUseReachGoalTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction

function AdvSetUseEnterCombatTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction

function AdvSetUseClearGoalTimeout takes boolean flag returns nothing
    set enter_combat_timeout = flag
endfunction

//============================================================================

function AdvSetContinueAttackPercentage takes integer percentage returns nothing
    set continue_attack_percentage = percentage
endfunction

function AdvSetContinueAttackReducePercentageIfFarAway takes boolean flag returns nothing
    set continue_attack_reduce_percentage_if_far_away = flag
endfunction

function AdvSetContinueAttackReducePercentage takes integer percentage returns nothing
    set continue_attack_reduce_percentage = percentage
endfunction

//============================================================================

function AdvSetAttackWaveGatherReturnXY takes real x, real y returns nothing
    set attack_wave_gather_return_x = x
    set attack_wave_gather_return_y = y
    set attack_wave_gather_return_set = true
endfunction

function AdvSetDefenseCaptainHomeXY takes real x, real y returns nothing
    set defense_captain_home_x = x
    set defense_captain_home_y = y
    set defense_captain_home_set = true

endfunction

Now, lets explain what each one of these functions does:
  • AdvSetDebug defines if to show debug info and to which player.
  • AdvSetWaveReachGoalTimeoutSeconds defines the maximum allowed time (in seconds) to reach the goal for the attack wave party
  • AdvSetWaveEnterCombatTimeoutSecond defines the maximum allowed time (in seconds) to enter combat after having reached the goal
  • AdvSetWaveClearGoalTimeoutSeconds defines the maximum allowed time (in seconds) to clear the goal area from enemies after having entered combat
  • AdvSetContinueFarAwayRadius defines the radius outside which a new target (during wave continuation) is considered far away, so percentage of continuing the attack wave can be reduced
  • AdvSetPreferredLocationsRadius defines the radius of preferred locations where to search for targets
  • AdvSetWoodPeonsWarriors defines is the peons are considered warriors. This should be true for undead AI players, for example
  • AdvSetSearchPreferredLocations defines whether or not to search at defined preferred locations for targets
  • AdvSetSearchEverywhere defines whether or not to search everywhere for targets
  • AdvSetCanSearchUnit defines whether or not regular units (not building) can be used as targets
  • AdvSetCanSearchInvulnerable defines whether or not invulnerable units can be used as targets
  • AdvSetPrioritizeTownHalls defines whether or not town halls should be prioritized when searching for targets
  • AdvSetPrioritizeNearest defines whether or not targets are to be chosen at random or by looking for the nearest one to the attack wave starting position
  • AdvSetSearchTargetMaxCyclesDefault defines the amount of target search cycles before exiting the routine, if no target is found. A negative value make the search go on forever, until a target is found
  • AdvSetSearchTargetMaxCyclesLastTarget defines the amount of target search cycles before exiting the routine, if no target is found. A negative value make the search go on forever, until a target is found. This is the same as before, but it's used when the attack wave is continuing its attack
  • AdvSetUseReachGoalTimeout defines whether or not the reach goal timeout defined above should be used or not, waiting possibly indefinitely for the wave to reach the goal
  • AdvSetUseEnterCombatTimeout defines whether or not the enter combat timeout defined above should be used or not, waiting possibly indefinitely for the wave to enter combat
  • AdvSetUseClearGoalTimeout defines whether or not the clear goal timeout defined above should be used or not, waiting possibly indefinitely for the wave to clear the goal
  • AdvSetContinueAttackPercentage defines the base percentage of continuing an attack, after the attack wave ended with a success. This is the value any new attack wave starts with
  • AdvSetContinueAttackReducePercentageIfFarAway defines whether or not to reduce percentage if the attack continuation chosen target is far away, according to the radius defined above
  • AdvSetContinueAttackReducePercentage defines the reduction amount of the continue attack percentage when the new target is far away (it is additive for multiple waves)
  • AdvSetAttackWaveGatherReturnXY defines the location where the attack wave gathers or returns after a successful attack
  • AdvSetDefenseCaptainHomeXY defines where the defenders group will stay while idle
It should be possible to call these functions anywhere at any time without breaking anything. I usually call them in two ways:
  1. Before a certain attack wave (for example, if I want to attack nearest targets or town halls or preferred locations and so forth), resetting them afterwards
  2. In the main function of the AI script, to define global properties that are default for all attack waves (or should be general for the AI behaviour, like the defense captain home)
All variables configurable through these functions have also default values, as follows:


JASS:
    //--------------------------------------------------------------------

    player debug_player
    boolean debug                                                       = false

    boolean suicide                                                      = false

    integer wave_reach_goal_timeout_seconds                 = 300
    integer wave_enter_combat_timeout_seconds                 = 3
    integer wave_clear_goal_timeout_seconds                    = 60

    real continue_far_away_radius                             = 960.0
    real preferred_locations_radius                         = 640.0

    integer wave_reach_goal_sleep_seconds
    integer wave_enter_combat_sleep_seconds
    integer wave_clear_goal_sleep_seconds

    real defense_captain_home_x
    real defense_captain_home_y
    boolean defense_captain_home_set                         = false

    real attack_wave_gather_return_x
    real attack_wave_gather_return_y
    boolean attack_wave_gather_return_set                    = false

    location array preferred_locations
    integer preferred_locations_count                         = 0

    location array preferred_locations_current
    integer preferred_locations_count_current                 = 0

    boolean wood_peons_warriors                             = false

    boolean search_preferred_locations                         = true
    boolean search_everywhere                                 = true
    boolean can_search_units                                 = true
    boolean can_search_invulnerable                         = false
    boolean prioritize_town_halls                             = true
    boolean prioritize_nearest                                 = true

    boolean reach_goal_timeout                                 = true
    boolean enter_combat_timeout                             = true
    boolean clear_goal_timeout                                 = true
    integer continue_attack_percentage                              = 100
    boolean continue_attack_reduce_percentage_if_far_away      = true
    integer continue_attack_reduce_percentage                  = 25

    unit array targetable_town_halls
    integer targetable_town_halls_count                     = 0

    unit array targetable_buildings
    integer targetable_buildings_count                         = 0

    unit array targetable_units
    integer targetable_units_count                             = 0

    real last_target_x
    real last_target_y
    integer search_target_max_cycles_default                = 100
    integer search_target_max_cycles_last_target            = 10


HOW TO IMPORT AND USAGE EXAMPLE
This section describes the steps required to import common AI 2.0 to your map and provides an useful usage example with real, fully functional, AI scripts.

To import common_v2.ai file inside your map, you need to overwrite the default common.ai file the game provides. To do so, you just need to do what follows:
  1. Download the common_v2.ai file attached to this system (or just make your own .ai using the script in the code section, it's just a text file!), which contains all the advanced routines
  2. Import this file into your map using the Asset Manager
  3. Change the file path to scripts\common.ai
By doing so, each .ai script will have the new routines included, so you can use the new routines in all AI scripts for that specific map.
The scripts below provide some useful usage example, using the Kul Tiras AI of Warcraft 3 Re-Reforged: Exodus of the Horde, chapter three, and one of the Underworld Minions AI of Warcraft 3 Re-Reforged: Exodus of the Horde, chapter five. For additional AI scripts (both with and without common_v2.ai), check out this thread.


JASS:
//============================================================================
//  RR Prologue 03 -- Kul Tiras Dark Green Human -- AI Script
//  by InsaneMonster (Luca Pasqualini)
//  Requires common_v2.ai
//============================================================================

// GLOBALS
//------------------------------------------------
globals

    // Tech tree

    constant integer KELEN = 'Haah'
    constant integer HYDROMANCER = 'h000'
    constant integer UPG_HYDROMANCY = 'R000'

    // Players

    player User = PlayerEx(1)
    player Trolls = PlayerEx(16)

    // Behaviour

    constant real ATTACK_WAVE_START_X = -3800.0
    constant real ATTACK_WAVE_START_Y = 3500.0

    boolean AttackUser = false
    boolean AttackTrolls = false

    // Commands

    integer COMMAND_ATTACK_TROLLS = 2
    integer DATA_ATTACK_TROLLS_ENABLE = 1
    integer DATA_ATTACK_TROLLS_DISABLE = 0

    integer COMMAND_ATTACK_USER = 1
    integer DATA_ATTACK_USER_ENABLE = 1
    integer DATA_ATTACK_USER_DISABLE = 0

endglobals

// BUILD ORDER
//------------------------------------------------
function SetBuildOrder takes nothing returns nothing

    call SetBuildUnitEx(1, 1, 1, PEASANT)
    call SetBuildUnitEx(1, 1, 1, TOWN_HALL)
    call SetBuildUnitEx(8, 8, 8, PEASANT)
    call SetBuildUnitEx(1, 1, 1, HUMAN_ALTAR)
    call SetBuildUnitEx(2, 2, 2, HOUSE)
    call SetBuildUnitEx(1, 1, 1, BARRACKS)
    call SetBuildUnitEx(4, 4, 4, HOUSE)
    call SetBuildUnitEx(0, 1, 1, LUMBER_MILL)
    call SetBuildUnitEx(0, 1, 1, BLACKSMITH)
    call SetBuildUnitEx(0, 2, 3, HOUSE)
    call SetBuildUnitEx(0, 0, 1, KEEP)
    call SetBuildUnitEx(0, 0, 1, ARCANE_SANCTUM)
    call SetBuildUnitEx(0, 0, 2, HOUSE)
    call SetBuildUnitEx(0, 0, 1, ARCANE_VAULT)

endfunction

// DEFENDERS
//------------------------------------------------
function SetDefenders takes nothing returns nothing

    call CampaignDefenderEx(1, 2, 2, FOOTMAN)
    call CampaignDefenderEx(1, 1, 2, RIFLEMAN)
    call CampaignDefenderEx(0, 0, 1, HYDROMANCER)
    call CampaignDefenderEx(1, 1, 1, KELEN)

endfunction

// RESEARCH UPGRADES
//------------------------------------------------
function ResearchUpgrades takes nothing returns nothing

    call Sleep(M5)

    call SetBuildUpgrEx(1, 1, 1, UPG_DEFEND)

    call Sleep(M4)

    call SetBuildUpgrEx(1, 1, 1, UPG_MELEE)

    call Sleep(M4)

    call SetBuildUpgrEx(1, 1, 1, UPG_ARMOR)
    call SetBuildUpgrEx(0, 1, 1, UPG_HYDROMANCY)

    call Sleep(M4)

    call SetBuildUpgrEx(1, 1, 1, UPG_RANGED)

    call Sleep(M4)

    call SetBuildUpgrEx(1, 1, 1, UPG_LEATHER)
    call SetBuildUpgrEx(0, 1, 1, UPG_GUN_RANGE)

    call Sleep(M3)

    call SetBuildUpgrEx(0, 0, 1, UPG_MASONRY)

    call Sleep(M4)

    call SetBuildUpgrEx(1, 2, 2, UPG_MELEE)

    call Sleep(M5)

    call SetBuildUpgrEx(1, 1, 2, UPG_ARMOR)

    call Sleep(M4)

    call SetBuildUpgrEx(1, 2, 2, UPG_RANGED)

    call Sleep(M5)

    call SetBuildUpgrEx(1, 1, 2, UPG_LEATHER)

endfunction

// ATTACK LOOP
//------------------------------------------------
function AttackWavesAttackUser takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    local boolean AttackWaveAttackTrolls = AttackTrolls

    call AdvDebugDisplayToPlayer("AI Info: attacking User...")
    call AdvSetPrioritizeTownHalls(false)
    call AdvSetContinueAttackReducePercentage(10)

    loop

        // *** WAVE 1 ***
        call AdvSetPrioritizeTownHalls(true)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 3, 3, FOOTMAN)
        call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
        call AdvSuicideOnPlayerEx(M4, M3, M2, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        // *** WAVE 2 ***
        call AdvSetPrioritizeTownHalls(true)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 2, FOOTMAN)
        call CampaignAttackerEx(1, 2, 2, RIFLEMAN)
        call CampaignAttackerEx(1, 1, 2, HYDROMANCER)
        call CampaignAttackerEx(1, 1, 1, KELEN)
        call AdvSuicideOnPlayerEx(M5, M4, M3, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        // *** WAVE 3 ***
        call AdvSetPrioritizeTownHalls(false)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 3, FOOTMAN)
        call CampaignAttackerEx(0, 1, 1, RIFLEMAN)
        call CampaignAttackerEx(1, 1, 1, HYDROMANCER)
        call AdvSuicideOnPlayerEx(M4, M3, M2, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        loop

            // *** WAVE 4 ***
            call AdvSetPrioritizeTownHalls(true)
            call InitAssaultGroup()
            call CampaignAttackerEx(3, 4, 5, FOOTMAN)
            call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 1, KELEN)
            call AdvSuicideOnPlayerEx(M5, M4, M3, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

            // *** WAVE 5 ***
            call AdvSetPrioritizeTownHalls(false)
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 3, 3, FOOTMAN)
            call CampaignAttackerEx(2, 2, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 2, HYDROMANCER)
            call AdvSuicideOnPlayerEx(M4, M3, M2, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

            // *** WAVE 6 ***
            call AdvSetPrioritizeTownHalls(true)
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 3, 3, FOOTMAN)
            call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 1, HYDROMANCER)
            call CampaignAttackerEx(1, 1, 1, KELEN)
            call AdvSuicideOnPlayerEx(M5, M4, M3, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        endloop

        // Make sure to leave both loops
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished attacking User!")

endfunction

function AttackWavesAttackTrolls takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    local boolean AttackWaveAttackTrolls = AttackTrolls

    call AdvDebugDisplayToPlayer("AI Info: attacking Trolls...")
    call AdvSetPrioritizeTownHalls(false)
    call AdvSetContinueAttackReducePercentageIfFarAway(false)

    loop

        // *** WAVE 1 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 3, 3, FOOTMAN)
        call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
        call AdvSuicideOnPlayerEx(M4, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        // *** WAVE 2 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 2, FOOTMAN)
        call CampaignAttackerEx(1, 2, 2, RIFLEMAN)
        call CampaignAttackerEx(1, 1, 2, HYDROMANCER)
        call CampaignAttackerEx(1, 1, 1, KELEN)
        call AdvSuicideOnPlayerEx(M5, M4, M3, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        // *** WAVE 3 ***)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 3, FOOTMAN)
        call CampaignAttackerEx(0, 1, 1, RIFLEMAN)
        call CampaignAttackerEx(1, 1, 1, HYDROMANCER)
        call AdvSuicideOnPlayerEx(M4, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        loop

            // *** WAVE 4 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(3, 4, 5, FOOTMAN)
            call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 1, KELEN)
            call AdvSuicideOnPlayerEx(M5, M4, M3, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

            // *** WAVE 5 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 3, 3, FOOTMAN)
            call CampaignAttackerEx(2, 2, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 2, HYDROMANCER)
            call AdvSuicideOnPlayerEx(M4, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

            // *** WAVE 6 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 3, 3, FOOTMAN)
            call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 1, HYDROMANCER)
            call CampaignAttackerEx(1, 1, 1, KELEN)
            call AdvSuicideOnPlayerEx(M5, M4, M3, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        endloop

        // Make sure to leave both loops
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished attacking Trolls!")

endfunction

function AttackWavesAttackAll takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    local boolean AttackWaveAttackTrolls = AttackTrolls

    call AdvDebugDisplayToPlayer("AI Info: attacking Trolls and User...")

    loop

        // *** WAVE 1 ***
        call AdvSetPrioritizeTownHalls(false)
        call AdvSetContinueAttackReducePercentageIfFarAway(false)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 3, 3, FOOTMAN)
        call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
        call AdvSuicideOnPlayerEx(M4, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        // *** WAVE 2 ***
        call AdvSetPrioritizeTownHalls(true)
        call AdvSetContinueAttackReducePercentageIfFarAway(true)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 2, FOOTMAN)
        call CampaignAttackerEx(1, 2, 2, RIFLEMAN)
        call CampaignAttackerEx(1, 1, 2, HYDROMANCER)
        call CampaignAttackerEx(1, 1, 1, KELEN)
        call AdvSuicideOnPlayerEx(M5, M4, M3, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        // *** WAVE 3 ***
        call AdvSetPrioritizeTownHalls(false)
        call AdvSetContinueAttackReducePercentageIfFarAway(false)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 3, FOOTMAN)
        call CampaignAttackerEx(0, 1, 1, RIFLEMAN)
        call CampaignAttackerEx(1, 1, 1, HYDROMANCER)
        call AdvSuicideOnPlayerEx(M4, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        loop

            // *** WAVE 4 ***
            call AdvSetPrioritizeTownHalls(true)
            call AdvSetContinueAttackReducePercentageIfFarAway(true)
            call InitAssaultGroup()
            call CampaignAttackerEx(3, 4, 5, FOOTMAN)
            call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 1, KELEN)
            call AdvSuicideOnPlayerEx(M5, M4, M3, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

            // *** WAVE 5 ***
            call AdvSetPrioritizeTownHalls(false)
            call AdvSetContinueAttackReducePercentageIfFarAway(false)
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 3, 3, FOOTMAN)
            call CampaignAttackerEx(2, 2, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 2, HYDROMANCER)
            call AdvSuicideOnPlayerEx(M4, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

            // *** WAVE 6 ***
            call AdvSetPrioritizeTownHalls(true)
            call AdvSetContinueAttackReducePercentageIfFarAway(true)
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 3, 3, FOOTMAN)
            call CampaignAttackerEx(1, 1, 2, RIFLEMAN)
            call CampaignAttackerEx(1, 1, 1, HYDROMANCER)
            call CampaignAttackerEx(1, 1, 1, KELEN)
            call AdvSuicideOnPlayerEx(M5, M4, M3, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

        endloop

        // Make sure to leave both loops
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished attacking Trolls and User!")

endfunction

function AttackLoop takes nothing returns nothing

    loop

        // Attack the user and/or the trolls depending on the condition
        if AttackUser and AttackTrolls then
            call AttackWavesAttackAll()
        else
            if AttackUser then
                call AttackWavesAttackUser()
            endif
            if AttackTrolls then
                call AttackWavesAttackTrolls()
            endif
        endif

        // Wait a little time before looping again
        call Sleep(10)

    endloop

endfunction

// COMMAND LOOP
//------------------------------------------------
function CommandFetch takes nothing returns nothing
    local integer Command
    local integer Data

    // Check if there is any command waiting

    loop

        exitwhen CommandsWaiting() <= 0

        // Get command and data
        set Command = GetLastCommand()
        set Data = GetLastData()
        call PopLastCommand()

        // Update attack trolls behaviour command (to start/stop attack against the trolls)
        if Command == COMMAND_ATTACK_TROLLS then
            if Data == DATA_ATTACK_TROLLS_ENABLE then
                set AttackTrolls = true
            elseif Data == DATA_ATTACK_TROLLS_DISABLE then
                set AttackTrolls = false
            else
                call AdvDebugDisplayToPlayer("AI Warning: Invalid attack trolls command data received!")
            endif

        // Update attack user behaviour command (to start/stop attack against the user)
        elseif Command == COMMAND_ATTACK_USER then
            if Data == DATA_ATTACK_USER_ENABLE then
                set AttackUser = true
            elseif Data == DATA_ATTACK_USER_DISABLE then
                set AttackUser = false
            else
                call AdvDebugDisplayToPlayer("AI Warning: Invalid attack user command data received!")
            endif

        else
            call AdvDebugDisplayToPlayer("AI Warning: Unknown command received!")

        endif

    endloop

endfunction

function CommandLoop takes nothing returns nothing

    // Keep fetching commands while the AI is active
    call CommandFetch()
    call StaggerSleep(1, 5)
    loop
        call CommandFetch()
        call Sleep(2)
    endloop

endfunction

// MAIN
//------------------------------------------------
function main takes nothing returns nothing

    call CampaignAI(HOUSE, null)

    // Define AI properties
    call DoCampaignFarms(false)
    call SetReplacements(1, 1, 1)
    call GroupTimedLife(true)
    call AdvSetSearchPreferredLocations(false)

    // Define build order
    call SetBuildOrder()

    // Start to research upgrades at timed intervals
    call StartThread(function ResearchUpgrades)

    // Define defenders
    call SetDefenders()

    // Start command loop for external signals
    call StartThread(function CommandLoop)

    // Launch attack waves as soon as possible (it depends on the commands received)
    call AttackLoop()

endfunction


JASS:
//============================================================================
//  RR Prologue 05 -- Underworld Minions Peach "Nagas" -- AI Script
//  by InsaneMonster (Luca Pasqualini)
//  Requires common_v2.ai
//============================================================================

// GLOBALS
//------------------------------------------------
globals

    // Tech tree

    constant integer MG_CLIFFRUNNER = 'n009'
    constant integer MG_BLOODGILL = 'n00H'
    constant integer MG_TIDEWARRIOR = 'n00I'
    constant integer MC_TIDERUNNER = 'n00G'
    constant integer MC_HUNTSMAN = 'n007'
    constant integer MC_NIGHTCRAWLER = 'n008'

    // Players

    player User = PlayerEx(1)
    player Trolls = PlayerEx(16)

    // Behaviour

    constant real ATTACK_WAVE_START_X = -10000.0
    constant real ATTACK_WAVE_START_Y = -4700.0

    boolean AttackUser = false
    boolean AttackTrolls = false
    boolean AttackSuicide = false

    // Commands

    integer COMMAND_ATTACK_TROLLS = 2
    integer DATA_ATTACK_TROLLS_ENABLE = 1
    integer DATA_ATTACK_TROLLS_DISABLE = 0

    integer COMMAND_ATTACK_USER = 1
    integer DATA_ATTACK_USER_ENABLE = 1
    integer DATA_ATTACK_USER_DISABLE = 0

    integer COMMAND_ATTACK_SUICIDE = 3
    integer DATA_ATTACK_SUICIDE_ENABLE = 1
    integer DATA_ATTACK_SUICIDE_DISABLE = 0

endglobals

// ATTACK LOOP
//------------------------------------------------
function AttackWavesAttackUser takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    local boolean AttackWaveAttackTrolls = AttackTrolls
    local boolean AttackWaveAttackSuicide = AttackSuicide

    call AdvDebugDisplayToPlayer("AI Info: attacking User...")
    call AdvSetPrioritizeNearest(false)
    call AdvSetSearchPreferredLocations(true)
    call AdvSetContinueAttackPercentage(100)

    loop

        // *** WAVE 1 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(1, 2, 2, MC_TIDERUNNER)
        call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
        call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        // *** WAVE 2 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(1, 2, 2, MC_TIDERUNNER)
        call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
        call CampaignAttackerEx(1, 1, 1, MG_CLIFFRUNNER)
        call CampaignAttackerEx(1, 1, 1, MG_BLOODGILL)
        call AdvSuicideOnPlayerEx(M1, M1, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        // *** WAVE 3 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(1, 2, 2, MG_CLIFFRUNNER)
        call CampaignAttackerEx(1, 1, 2, MG_TIDEWARRIOR)
        call CampaignAttackerEx(1, 1, 1, MG_BLOODGILL)
        call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        loop

            // *** WAVE 4 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(1, 2, 2, MC_TIDERUNNER)
            call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
            call CampaignAttackerEx(1, 1, 1, MC_NIGHTCRAWLER)
            call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 5 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(1, 2, 2, MG_TIDEWARRIOR)
            call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
            call CampaignAttackerEx(1, 1, 2, MG_BLOODGILL)
            call CampaignAttackerEx(1, 1, 1, MC_NIGHTCRAWLER)
            call AdvSuicideOnPlayerEx(M1, M1, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 6 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(1, 2, 2, MG_TIDEWARRIOR)
            call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
            call CampaignAttackerEx(1, 1, 1, MG_BLOODGILL)
            call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        endloop

        // Make sure to leave both loops
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished attacking User!")

endfunction

function AttackWavesAttackTrolls takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    local boolean AttackWaveAttackTrolls = AttackTrolls
    local boolean AttackWaveAttackSuicide = AttackSuicide

    call AdvDebugDisplayToPlayer("AI Info: attacking Trolls...")
    call AdvSetPrioritizeNearest(true)
    call AdvSetSearchPreferredLocations(false)
    call AdvSetContinueAttackPercentage(0)

    loop

        // *** WAVE 1 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 3, MC_TIDERUNNER)
        call CampaignAttackerEx(1, 2, 2, MC_HUNTSMAN)
        call AdvSuicideOnPlayerEx(M3, M2, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        loop

            // *** WAVE 2 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 2, 3, MG_CLIFFRUNNER)
            call CampaignAttackerEx(1, 2, 2, MG_TIDEWARRIOR)
            call AdvSuicideOnPlayerEx(M3, M3, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 3 ***
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 2, 3, MC_TIDERUNNER)
            call CampaignAttackerEx(1, 2, 2, MC_HUNTSMAN)
            call AdvSuicideOnPlayerEx(M3, M2, M2, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        endloop

        // Make sure to leave both loops
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished attacking Trolls!")

endfunction

function AttackWavesAttackAll takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    local boolean AttackWaveAttackTrolls = AttackTrolls
    local boolean AttackWaveAttackSuicide = AttackSuicide

    call AdvDebugDisplayToPlayer("AI Info: attacking Trolls and User...")

    loop

        // *** WAVE 1 ***
        call AdvSetPrioritizeNearest(false)
        call AdvSetSearchPreferredLocations(true)
        call AdvSetContinueAttackPercentage(100)
        call InitAssaultGroup()
        call CampaignAttackerEx(1, 2, 2, MC_TIDERUNNER)
        call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
        call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        // *** WAVE 2 ***
        call AdvSetPrioritizeNearest(true)
        call AdvSetSearchPreferredLocations(false)
        call AdvSetContinueAttackPercentage(0)
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 2, 3, MC_TIDERUNNER)
        call CampaignAttackerEx(1, 2, 2, MC_HUNTSMAN)
        call AdvSuicideOnPlayerEx(M1, M1, 30, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        // *** WAVE 3 ***
        call AdvSetPrioritizeNearest(false)
        call AdvSetSearchPreferredLocations(true)
        call AdvSetContinueAttackPercentage(100)
        call InitAssaultGroup()
        call CampaignAttackerEx(1, 2, 2, MC_TIDERUNNER)
        call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
        call CampaignAttackerEx(1, 1, 2, MG_CLIFFRUNNER)
        call CampaignAttackerEx(1, 1, 1, MG_BLOODGILL)
        call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        // *** WAVE 4 ***
        call AdvSetPrioritizeNearest(false)
        call AdvSetSearchPreferredLocations(true)
        call AdvSetContinueAttackPercentage(100)
        call InitAssaultGroup()
        call CampaignAttackerEx(1, 2, 2, MG_CLIFFRUNNER)
        call CampaignAttackerEx(1, 1, 2, MG_TIDEWARRIOR)
        call CampaignAttackerEx(1, 1, 1, MG_BLOODGILL)
        call AdvSuicideOnPlayerEx(M1, M1, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        loop

            // *** WAVE 5 ***
            call AdvSetPrioritizeNearest(false)
            call AdvSetSearchPreferredLocations(true)
            call AdvSetContinueAttackPercentage(100)
            call InitAssaultGroup()
            call CampaignAttackerEx(1, 2, 2, MC_TIDERUNNER)
            call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
            call CampaignAttackerEx(1, 1, 1, MC_NIGHTCRAWLER)
            call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 6 ***
            call AdvSetPrioritizeNearest(true)
            call AdvSetSearchPreferredLocations(false)
            call AdvSetContinueAttackPercentage(0)
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 2, 3, MG_CLIFFRUNNER)
            call CampaignAttackerEx(1, 2, 2, MG_TIDEWARRIOR)
            call AdvSuicideOnPlayerEx(M1, M1, 30, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 7 ***
            call AdvSetPrioritizeNearest(false)
            call AdvSetSearchPreferredLocations(true)
            call AdvSetContinueAttackPercentage(100)
            call InitAssaultGroup()
            call CampaignAttackerEx(1, 2, 2, MG_TIDEWARRIOR)
            call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
            call CampaignAttackerEx(1, 1, 2, MG_BLOODGILL)
            call CampaignAttackerEx(1, 1, 1, MC_NIGHTCRAWLER)
            call AdvSuicideOnPlayerEx(M1, 45, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 8 ***
            call AdvSetPrioritizeNearest(false)
            call AdvSetSearchPreferredLocations(true)
            call AdvSetContinueAttackPercentage(100)
            call InitAssaultGroup()
            call CampaignAttackerEx(1, 2, 2, MG_TIDEWARRIOR)
            call CampaignAttackerEx(1, 1, 2, MC_HUNTSMAN)
            call CampaignAttackerEx(1, 1, 1, MG_BLOODGILL)
            call AdvSuicideOnPlayerEx(M1, M1, 45, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

            // *** WAVE 9 ***
            call AdvSetPrioritizeNearest(true)
            call AdvSetSearchPreferredLocations(false)
            call AdvSetContinueAttackPercentage(0)
            call InitAssaultGroup()
            call CampaignAttackerEx(2, 2, 3, MC_TIDERUNNER)
            call CampaignAttackerEx(1, 2, 2, MC_HUNTSMAN)
            call AdvSuicideOnPlayerEx(M1, 30, 30, Trolls, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
            exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

        endloop

        // Make sure to leave both loops
        exitwhen AttackWaveAttackTrolls != AttackTrolls or AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished attacking Trolls and User!")

endfunction

// Note: the suicide attack is always only on the user
function AttackWavesAttackSuicide takes nothing returns nothing
    local boolean AttackWaveAttackSuicide = AttackSuicide
    local integer UserId = GetPlayerId(User)

    call AdvDebugDisplayToPlayer("AI Info: starting suicide...")

    // Reset the assault group
    call InitAssaultGroup()

    // Launch attack continuously
    loop

        call SuicideUnitEx(1, MC_TIDERUNNER, UserId)
        call SuicideUnitEx(1, MC_HUNTSMAN, UserId)
        call SuicideUnitEx(1, MC_NIGHTCRAWLER, UserId)
        call SuicideUnitEx(1, MG_CLIFFRUNNER, UserId)
        call SuicideUnitEx(1, MG_TIDEWARRIOR, UserId)
        call SuicideUnitEx(1, MG_BLOODGILL, UserId)

        call Sleep(2)
        exitwhen AttackSuicide != AttackWaveAttackSuicide

    endloop

    call AdvDebugDisplayToPlayer("AI Info: finished suicide!")

endfunction

function AttackLoop takes nothing returns nothing

    loop

        // If suicide is set, execute the attack suicide loop
        if AttackSuicide then
            call AttackWavesAttackSuicide()
        else
            // Attack the user and/or the trolls depending on the condition
            if AttackUser and AttackTrolls then
                call AttackWavesAttackAll()
            else
                if AttackUser then
                    call AttackWavesAttackUser()
                endif
                if AttackTrolls then
                    call AttackWavesAttackTrolls()
                endif
            endif
        endif

        // Wait a little time before looping again
        call Sleep(10)

    endloop

endfunction

// COMMAND LOOP
//------------------------------------------------
function CommandFetch takes nothing returns nothing
    local integer Command
    local integer Data

    // Check if there is any command waiting

    loop

        exitwhen CommandsWaiting() <= 0

        // Get command and data
        set Command = GetLastCommand()
        set Data = GetLastData()
        call PopLastCommand()

        // Update attack suicide behaviour command (to start/stop suicide, always against the user)
        if Command == COMMAND_ATTACK_SUICIDE then
            if Data == DATA_ATTACK_SUICIDE_ENABLE then
                set AttackSuicide = true
                call AdvSetSuicide(true)
            elseif Data == DATA_ATTACK_SUICIDE_DISABLE then
                set AttackSuicide = false
                call AdvSetSuicide(false)
            else
                call AdvDebugDisplayToPlayer("AI Warning: Invalid attack suicide command data received!")
            endif

        // Update attack trolls behaviour command (to start/stop attack against the trolls)
        elseif Command == COMMAND_ATTACK_TROLLS then
            if Data == DATA_ATTACK_TROLLS_ENABLE then
                set AttackTrolls = true
            elseif Data == DATA_ATTACK_TROLLS_DISABLE then
                set AttackTrolls = false
            else
                call AdvDebugDisplayToPlayer("AI Warning: Invalid attack trolls command data received!")
            endif

        // Update attack user behaviour command (to start/stop attack against the user)
        elseif Command == COMMAND_ATTACK_USER then
            if Data == DATA_ATTACK_USER_ENABLE then
                set AttackUser = true
            elseif Data == DATA_ATTACK_USER_DISABLE then
                set AttackUser = false
            else
                call AdvDebugDisplayToPlayer("AI Warning: Invalid attack user command data received!")
            endif

        else
            call AdvDebugDisplayToPlayer("AI Warning: Unknown command received!")

        endif

    endloop

endfunction

function CommandLoop takes nothing returns nothing

    // Keep fetching commands while the AI is active
    call CommandFetch()
    call StaggerSleep(1, 5)
    loop
        call CommandFetch()
        call Sleep(2)
    endloop

endfunction

// MAIN
//------------------------------------------------
function main takes nothing returns nothing

    call CampaignAI(NAGA_CORAL, null)

    // Define AI properties
    call DoCampaignFarms(false)
    call GroupTimedLife(true)
    call SetAmphibious()
    call AdvSetPrioritizeTownHalls(false)
    call AdvSetContinueAttackReducePercentageIfFarAway(false)

    call AdvSetAttackWaveGatherReturnXY(-9800.0, -3800.0)

    // MAIN USER BASE TOP LEFT ENTRANCE
    call AdvAddPreferredLocation(-6300, -7000.0)
    // FOUNTAIN
    call AdvAddPreferredLocation(-11300.0, -9500.0)
    // TOWER OUTPOST
    call AdvAddPreferredLocation(-8500, -9600.0)

    // Start command loop for external signals
    call StartThread(function CommandLoop)

    // Launch attack waves as soon as possible (it depends on the commands received)
    call AttackLoop()

endfunction


CONCLUSIONS
And... this is it people!
You can now properly design and customize your JASS AIs without all the drawbacks and the nasty bugs of the default routines. In addition, you can use them combined with default routines without bugs nor side effect.


Have fun, and make the AI players great again!

None so far!
 

Attachments

  • common_v2.ai
    135.3 KB · Views: 360
Last edited:
Level 5
Joined
Aug 19, 2021
Messages
10
My testing shows that this does not work in 1.29.2. When I tried adding it to one of my maps, all of my AIs lost function completely. The observed behavior was identical to what happens when one tries to run an AI script with syntax errors.

I also tried running your scripts through pjass. The result is 25 errors. 13 of these can be resolved by renaming your "debug" variable. (I think it's a reserved word). Once this is resolved, the output is:
Code:
$ pjass common.j common_v2.ai
Parse successful:     2419 lines: common.j
common_v2.ai:2969: Undeclared function BlzGroupAddGroupFast
common_v2.ai:2980: Undeclared function BlzGroupGetSize
common_v2.ai:2992: Undeclared function BlzIsUnitInvulnerable. Maybe you meant SetUnitInvulnerable or IsItemInvulnerable
common_v2.ai:3027: Undeclared function BlzGroupAddGroupFast
common_v2.ai:3038: Undeclared function BlzGroupGetSize
common_v2.ai:3050: Undeclared function BlzIsUnitInvulnerable. Maybe you meant SetUnitInvulnerable or IsItemInvulnerable
common_v2.ai:3085: Undeclared function BlzGroupAddGroupFast
common_v2.ai:3096: Undeclared function BlzGroupGetSize
common_v2.ai:3108: Undeclared function BlzIsUnitInvulnerable. Maybe you meant SetUnitInvulnerable or IsItemInvulnerable
common_v2.ai:3540: Missing return
common_v2.ai:3586: Missing return
common_v2.ai:3632: Missing return
common_v2.ai failed with 12 errors
Parse failed: 12 errors total

BlzGroupAddGroupFast and BlzGroupGetSize were both added in 1.31, so they're not going to work in previous versions. I'm not sure about BlzIsUnitInvulnerable. This script might still work in 1.31.x, but versions before that will require modification.
 
My testing shows that this does not work in 1.29.2. When I tried adding it to one of my maps, all of my AIs lost function completely. The observed behavior was identical to what happens when one tries to run an AI script with syntax errors.

I also tried running your scripts through pjass. The result is 25 errors. 13 of these can be resolved by renaming your "debug" variable. (I think it's a reserved word). Once this is resolved, the output is:
Code:
$ pjass common.j common_v2.ai
Parse successful:     2419 lines: common.j
common_v2.ai:2969: Undeclared function BlzGroupAddGroupFast
common_v2.ai:2980: Undeclared function BlzGroupGetSize
common_v2.ai:2992: Undeclared function BlzIsUnitInvulnerable. Maybe you meant SetUnitInvulnerable or IsItemInvulnerable
common_v2.ai:3027: Undeclared function BlzGroupAddGroupFast
common_v2.ai:3038: Undeclared function BlzGroupGetSize
common_v2.ai:3050: Undeclared function BlzIsUnitInvulnerable. Maybe you meant SetUnitInvulnerable or IsItemInvulnerable
common_v2.ai:3085: Undeclared function BlzGroupAddGroupFast
common_v2.ai:3096: Undeclared function BlzGroupGetSize
common_v2.ai:3108: Undeclared function BlzIsUnitInvulnerable. Maybe you meant SetUnitInvulnerable or IsItemInvulnerable
common_v2.ai:3540: Missing return
common_v2.ai:3586: Missing return
common_v2.ai:3632: Missing return
common_v2.ai failed with 12 errors
Parse failed: 12 errors total

BlzGroupAddGroupFast and BlzGroupGetSize were both added in 1.31, so they're not going to work in previous versions. I'm not sure about BlzIsUnitInvulnerable. This script might still work in 1.31.x, but versions before that will require modification.

This is very sad. I can rename the debug variable, no problem in doing that, but these functions are required to imitate the logic of the natives I've reworked. I don't know then if there exists alternatives for versions below 1.31.x.

I've updated the description to reflect that. If someone can also test this on 1.31.x, I will fix any compatibility problem that may arise, so long the required natives are present.
 
Level 5
Joined
Aug 19, 2021
Messages
10
This is very sad. I can rename the debug variable, no problem in doing that, but these functions are required to imitate the logic of the natives I've reworked. I don't know then if there exists alternatives for versions below 1.31.x.

I've updated the description to reflect that. If someone can also test this on 1.31.x, I will fix any compatibility problem that may arise, so long the required natives are present.
I decided to take a crack at trying to do a 1.29.2 compatibility patch myself. It technically works. Turns out that, at least in 1.29.2 and 1.30.x, new functions that are defined in common.ai can't be called. This means that the new library functions need to be prepended to each individual ai file, which isn't great. In principle BlzGroupAddGroupFast and BlzGroupGetSize can be replicated without 1.31 functions, but the substitute versions are less performant. Getting the size of a group should not be O(n). BlzIsUnitInvulnerable does exist in 1.29+, but I believe it's bugged in 1.29, so that might cause some Interesting Behavior. If you can deal with all of this, the system does appear to work (I haven't tested it extensively) at the price of ~40kb per AI file.
 
I decided to take a crack at trying to do a 1.29.2 compatibility patch myself. It technically works. Turns out that, at least in 1.29.2 and 1.30.x, new functions that are defined in common.ai can't be called. This means that the new library functions need to be prepended to each individual ai file, which isn't great. In principle BlzGroupAddGroupFast and BlzGroupGetSize can be replicated without 1.31 functions, but the substitute versions are less performant. Getting the size of a group should not be O(n). BlzIsUnitInvulnerable does exist in 1.29+, but I believe it's bugged in 1.29, so that might cause some Interesting Behavior. If you can deal with all of this, the system does appear to work (I haven't tested it extensively) at the price of ~40kb per AI file.

This is awesome, so basically the passages are the following:
1) Instead of importing common_v2.ai, place it's content above any .ai script in your map
2) Change debug name with some other name (I will do so for everyone in an updated version in the following days)
2) Change BlzGroupAddGroupFast and BlzGroupGetSize with a variant (can you write me the variant or share your script? So I can attach for classic users)
3) Enjoy

BlzIsUnitInvulnerable is used to prevent the AI from targeting invulnerable units. There is also a flag to stop this behaviour, so I could remove this flag for classic users and the associated check. Can you please confirm me it is bugged? (no need to use an AI script, just a check in pre-1.30 WE would be enough)
 
Level 5
Joined
Aug 19, 2021
Messages
10
Here's my code:

Example Script

Add to Globals

Prepend to Script

JASS:
globals
    player human = Player(0)

    constant integer DBALLISTA  = 'e000'
    constant integer DPEASANT  = 'h00C'
    constant integer DAWNGUARD  = 'h000'
    constant integer WITCHHUNTER  = 'h003'
    constant integer CRUSADER  = 'h001'
    constant integer DAWNPRIEST  = 'h002'
    constant integer AUGUR  = 'n608'
    constant integer BOWMAN  = 'n009'
    constant integer CARVYN  = 'H005'

    constant integer DTOWN_HALL  = 'h009'
    constant integer DKEEP  = 'h00A'
    constant integer DCASTLE  = 'h00B'
    constant integer DALTAR  = 'h00D'
    constant integer DBARRACKS  = 'h00E'
    constant integer DWORKSHOP  = 'h00G'
    constant integer DCHURCH  = 'h00F'

    constant real ATTACK_WAVE_START_X = 1500.0
    constant real ATTACK_WAVE_START_Y = -2000.0

    //--------------------------------------------------------------------
  
    player debug_player
    boolean debugTrace                                             = false
  
    boolean suicide                                            = false

    integer wave_reach_goal_timeout_seconds                 = 300
    integer wave_enter_combat_timeout_seconds                 = 3
    integer wave_clear_goal_timeout_seconds                    = 60
  
    real continue_far_away_radius                             = 960.0
    real preferred_locations_radius                         = 640.0
  
    integer wave_reach_goal_sleep_seconds
    integer wave_enter_combat_sleep_seconds
    integer wave_clear_goal_sleep_seconds
  
    real defense_captain_home_x
    real defense_captain_home_y
    boolean defense_captain_home_set                         = false
  
    real attack_wave_gather_return_x
    real attack_wave_gather_return_y
    boolean attack_wave_gather_return_set                    = false
      
    location array preferred_locations
    integer preferred_locations_count                         = 0
  
    location array preferred_locations_current
    integer preferred_locations_count_current                 = 0

    boolean wood_peons_warriors                             = false

    boolean search_preferred_locations                         = true
    boolean search_everywhere                                 = true
    boolean can_search_units                                 = true
    boolean can_search_invulnerable                         = false
    boolean prioritize_town_halls                             = true
    boolean prioritize_nearest                                 = true

    boolean reach_goal_timeout                                 = true
    boolean enter_combat_timeout                             = true
    boolean clear_goal_timeout                                 = true
    integer continue_attack_percentage                         = 100
    boolean continue_attack_reduce_percentage_if_far_away     = true
    integer continue_attack_reduce_percentage                 = 25
  
    unit array targetable_town_halls
    integer targetable_town_halls_count                     = 0

    unit array targetable_buildings
    integer targetable_buildings_count                         = 0
  
    unit array targetable_units
    integer targetable_units_count                             = 0

    real last_target_x
    real last_target_y
    integer search_target_max_cycles_default                = 100
    integer search_target_max_cycles_last_target            = 10
endglobals


//============================================================================
//  ADVANCED UTILITY
//============================================================================
function AdvDistance takes real x1, real y1, real x2, real y2 returns real
    return SquareRoot(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)))
endfunction

function AdvIsUnitAlive takes unit u returns boolean
    return GetUnitState(u, UNIT_STATE_LIFE) > 0.0
endfunction

function AdvDebugDisplayToPlayer takes string message returns nothing
    if debugTrace then
        call DisplayTextToPlayer(debug_player, 0, 0, message)
    endif
    call Trace(message)
endfunction

// Adds all units in g to h. Substitute for BlzGroupAddGroupFast
function AdvGroupAddGroup takes group g, group h returns nothing
    local unit u
    local group g2
    set g2 = CreateGroup()
    call AdvDebugDisplayToPlayer("AI Info: trying to add group!")
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupAddUnit(h, u)
        call GroupRemoveUnit(g, u)
        call GroupAddUnit(g2, u)
    endloop
    loop
        set u = FirstOfGroup(g2)
        exitwhen u == null
        call GroupAddUnit(g, u)
        call GroupRemoveUnit(g2, u)
    endloop
    call DestroyGroup(g2)
endfunction

// Counts the units in a group. Substitute for BlzGroupGetSize. No I'm not happy that this is O(n)
function AdvGroupGetSize takes group g returns integer
    local integer size
    local unit u
    local group g2
    set g2 = CreateGroup()
    set size = 0
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        set size = size + 1
        call GroupRemoveUnit(g, u)
        call GroupAddUnit(g2, u)
    endloop
    loop
        set u = FirstOfGroup(g2)
        exitwhen u == null
        call GroupAddUnit(g, u)
        call GroupRemoveUnit(g2, u)
    endloop
    call DestroyGroup(g2)
    return size
endfunction

//============================================================================
//  ADVANCED SETTINGS
//============================================================================
function AdvSetDebug takes player debugPlayer, boolean flag returns nothing
    set debug_player = debugPlayer
    set debugTrace = flag
endfunction

//============================================================================

function AdvSetWaveReachGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_reach_goal_timeout_seconds = seconds
endfunction

function AdvSetWaveEnterCombatTimeoutSeconds takes integer seconds returns nothing
    set wave_enter_combat_timeout_seconds = seconds
endfunction

function AdvSetWaveClearGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_clear_goal_timeout_seconds = seconds
endfunction

//============================================================================

function AdvSetContinueFarAwayRadius takes real radius returns nothing
    set continue_far_away_radius = radius
endfunction

function AdvSetPreferredLocationsRadius takes real radius returns nothing
    set preferred_locations_radius = radius
endfunction

//============================================================================

function AdvSetWoodPeonsWarriors takes boolean flag returns nothing
    set wood_peons_warriors = flag
endfunction

//============================================================================

function AdvSetSearchPreferredLocations takes boolean flag returns nothing
    set search_preferred_locations = flag
endfunction

function AdvSetSearchEverywhere takes boolean flag returns nothing
    set search_everywhere = flag
endfunction

function AdvSetCanSearchUnit takes boolean flag returns nothing
    set can_search_units = flag
endfunction

function AdvSetCanSearchInvulnerable takes boolean flag returns nothing
    set can_search_invulnerable = flag
endfunction

//============================================================================

function AdvSetPrioritizeTownHalls takes boolean flag returns nothing
    set prioritize_town_halls = flag
endfunction

function AdvSetPrioritizeNearest takes boolean flag returns nothing
    set prioritize_nearest = flag
endfunction

function AdvSetSearchTargetMaxCyclesDefault takes integer value returns nothing
    set search_target_max_cycles_default = value
endfunction

function AdvSetSearchTargetMaxCyclesLastTarget takes integer value returns nothing
    set search_target_max_cycles_last_target = value
endfunction

//============================================================================

function AdvSetUseReachGoalTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction

function AdvSetUseEnterCombatTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction

function AdvSetUseClearGoalTimeout takes boolean flag returns nothing
    set enter_combat_timeout = flag
endfunction

//============================================================================

function AdvSetContinueAttackPercentage takes integer percentage returns nothing
    set continue_attack_percentage = percentage
endfunction

function AdvSetContinueAttackReducePercentageIfFarAway takes boolean flag returns nothing
    set continue_attack_reduce_percentage_if_far_away = flag
endfunction

function AdvSetContinueAttackReducePercentage takes integer percentage returns nothing
    set continue_attack_reduce_percentage = percentage
endfunction

//============================================================================

function AdvSetAttackWaveGatherReturnXY takes real x, real y returns nothing
    set attack_wave_gather_return_x = x
    set attack_wave_gather_return_y = y
    set attack_wave_gather_return_set = true
endfunction

function AdvSetDefenseCaptainHomeXY takes real x, real y returns nothing
    set defense_captain_home_x = x
    set defense_captain_home_y = y
    set defense_captain_home_set = true

endfunction

// Note: this should never be called in the script with argument different from DEFENSE_CAPTAIN
function AdvSetCaptainHome takes integer whichCaptain returns nothing

    if whichCaptain == BOTH_CAPTAINS then
  
        if defense_captain_home_set then
            call SetCaptainHome(DEFENSE_CAPTAIN, defense_captain_home_x, defense_captain_home_y)
        endif
      
        if attack_wave_gather_return_set then
            call SetCaptainHome(ATTACK_CAPTAIN, attack_wave_gather_return_x, attack_wave_gather_return_y)
        endif
      
        return
      
    endif

    if defense_captain_home_set and whichCaptain == DEFENSE_CAPTAIN then
  
        call SetCaptainHome(DEFENSE_CAPTAIN, defense_captain_home_x, defense_captain_home_y)
        return
      
    endif
  
    if attack_wave_gather_return_set and whichCaptain == ATTACK_CAPTAIN then
  
        call SetCaptainHome(ATTACK_CAPTAIN, attack_wave_gather_return_x, attack_wave_gather_return_y)
        return
      
    endif
      
endfunction

//============================================================================

function AdvAddPreferredLocation takes real x, real y returns integer
    set preferred_locations[preferred_locations_count] = Location(x, y)
    set preferred_locations_count = preferred_locations_count + 1
  
    return preferred_locations_count
endfunction

function AdvSetPreferredLocationXY takes integer index, real x, real y returns nothing
    if index < preferred_locations_count then
        set preferred_locations[index] = Location(x, y)
    endif
endfunction

//============================================================================

function AdvSetSuicide takes boolean flag returns nothing
    set suicide = flag
endfunction

//============================================================================
// ADVANCED SUICIDE WAVE LAUNCH
//============================================================================
function AdvGetRandomTargetableTownHall takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_town_halls_count - 1)
  
    if targetable_town_halls_count <= 0 then
        return null
    endif

    return targetable_town_halls[randomIndex]
endfunction

function AdvGetNearestTargetableTownHall takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance
  
    if targetable_town_halls_count <= 0 then
        return null
    endif
  
    set minDistance = AdvDistance(GetUnitX(targetable_town_halls[minDistanceIndex]), GetUnitY(targetable_town_halls[minDistanceIndex]), x, y)
      
    loop
  
        set index = index + 1          
        exitwhen index >= targetable_town_halls_count
      
        if AdvDistance(GetUnitX(targetable_town_halls[index]), GetUnitY(targetable_town_halls[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_town_halls[minDistanceIndex]), GetUnitY(targetable_town_halls[minDistanceIndex]), x, y)
        endif
      
    endloop
  
    return targetable_town_halls[minDistanceIndex]
  
endfunction

//============================================================================

function AdvGetRandomTargetableBuilding takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_buildings_count - 1)
  
    if targetable_buildings_count <= 0 then
        return null
    endif

    return targetable_buildings[randomIndex]
endfunction

function AdvGetNearestTargetableBuilding takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance
  
    if targetable_buildings_count <= 0 then
        return null
    endif
  
    set minDistance = AdvDistance(GetUnitX(targetable_buildings[minDistanceIndex]), GetUnitY(targetable_buildings[minDistanceIndex]), x, y)
      
    loop
  
        set index = index + 1
        exitwhen index >= targetable_buildings_count
              
        if AdvDistance(GetUnitX(targetable_buildings[index]), GetUnitY(targetable_buildings[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_buildings[minDistanceIndex]), GetUnitY(targetable_buildings[minDistanceIndex]), x, y)
        endif
      
    endloop
  
    return targetable_buildings[minDistanceIndex]
  
endfunction

//============================================================================

function AdvGetRandomTargetableUnit takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_units_count - 1)
  
    if targetable_units_count <= 0 then
        return null
    endif

    return targetable_units[randomIndex]
endfunction

function AdvGetNearestTargetableUnit takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance
  
    if targetable_units_count <= 0 then
        return null
    endif
  
    set minDistance = AdvDistance(GetUnitX(targetable_units[minDistanceIndex]), GetUnitY(targetable_units[minDistanceIndex]), x, y)
      
    loop
  
        set index = index + 1
        exitwhen index >= targetable_units_count
              
        if AdvDistance(GetUnitX(targetable_units[index]), GetUnitY(targetable_units[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_units[minDistanceIndex]), GetUnitY(targetable_units[minDistanceIndex]), x, y)
        endif
      
    endloop
  
    return targetable_units[minDistanceIndex]
  
endfunction

//============================================================================

function AdvAssignTargetableTownHalls takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0
  
    set targetable_town_halls_count = 0
      
    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)       
            call AdvGroupAddGroup(tempGroup, targetsGroup)
          
            set index = index + 1      
            call GroupClear(tempGroup)
          
            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif
  
    if AdvGroupGetSize(targetsGroup) <= 0 then
        return false
    endif
  
    loop
  
        set currentUnit = FirstOfGroup(targetsGroup)
      
        exitwhen currentUnit == null
      
        if GetOwningPlayer(currentUnit) == targetPlayer and IsUnitType(currentUnit, UNIT_TYPE_TOWNHALL) and IsUnitType(currentUnit, UNIT_TYPE_STRUCTURE) and AdvIsUnitAlive(currentUnit) then          
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_town_halls[targetable_town_halls_count] = currentUnit
                    set targetable_town_halls_count = targetable_town_halls_count + 1
                endif
            else
                set targetable_town_halls[targetable_town_halls_count] = currentUnit
                set targetable_town_halls_count = targetable_town_halls_count + 1
            endif
        endif
      
        call GroupRemoveUnit(targetsGroup, currentUnit)
  
    endloop
  
    if targetable_town_halls_count > 0 then
        return true
    endif
  
    return false
  
endfunction

//============================================================================

function AdvAssignTargetableBuildings takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0
  
    set targetable_buildings_count = 0
  
    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)       
            call AdvGroupAddGroup(tempGroup, targetsGroup)
          
            set index = index + 1      
            call GroupClear(tempGroup)
          
            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif
  
    if AdvGroupGetSize(targetsGroup) <= 0 then
        return false
    endif
  
    loop
  
        set currentUnit = FirstOfGroup(targetsGroup)
      
        exitwhen currentUnit == null
      
        if GetOwningPlayer(currentUnit) == targetPlayer and IsUnitType(currentUnit, UNIT_TYPE_STRUCTURE) and AdvIsUnitAlive(currentUnit) then      
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_buildings[targetable_buildings_count] = currentUnit
                    set targetable_buildings_count = targetable_buildings_count + 1  
                endif
            else
                set targetable_buildings[targetable_buildings_count] = currentUnit
                set targetable_buildings_count = targetable_buildings_count + 1  
            endif
        endif
      
        call GroupRemoveUnit(targetsGroup, currentUnit)
  
    endloop
  
    if targetable_buildings_count > 0 then
        return true
    endif
  
    return false
  
endfunction

//============================================================================

function AdvAssignTargetableUnits takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0
  
    set targetable_units_count = 0
  
    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)       
            call AdvGroupAddGroup(tempGroup, targetsGroup)
          
            set index = index + 1      
            call GroupClear(tempGroup)
          
            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif
  
    if AdvGroupGetSize(targetsGroup) <= 0 then
        return false
    endif
  
    loop
  
        set currentUnit = FirstOfGroup(targetsGroup)
      
        exitwhen currentUnit == null
      
        if GetOwningPlayer(currentUnit) == targetPlayer and AdvIsUnitAlive(currentUnit) then
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_units[targetable_units_count] = currentUnit
                    set targetable_units_count = targetable_units_count + 1  
                endif
            else
                set targetable_units[targetable_units_count] = currentUnit
                set targetable_units_count = targetable_units_count + 1  
            endif
        endif
      
        call GroupRemoveUnit(targetsGroup, currentUnit)
  
    endloop
  
    if targetable_units_count > 0 then
        return true
    endif
  
    return false
  
endfunction

//============================================================================

function AdvSuicideWaveLaunch takes player targetPlayer, real startX, real startY, boolean searchPreferredLocations, boolean searchEverywhere, boolean canSearchUnits, boolean prioritizeTownHalls, boolean prioritizeNearest, boolean useLastTarget returns boolean
    local unit target = null
    local integer index = 0
    local real targetX
    local real targetY
    local integer searchTargetCycle = 0
  
    // Set the start coordinates
    if useLastTarget then
        set startX = last_target_x
        set startY = last_target_y
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: searching for a target...")
  
    // --- LOOP UNTIL TARGET IS FOUND OR TIMEOUT IS REACHED ---
    loop
  
        exitwhen suicide
  
        // --- CACHE PREFERRED LOCATIONS ---
        set preferred_locations_count_current = 0  
        if searchPreferredLocations and preferred_locations_count > 0 then
      
            set preferred_locations_count_current = preferred_locations_count
            loop
          
                set preferred_locations_current[index] = preferred_locations[index]
          
                set index = index + 1
                exitwhen index >= preferred_locations_count_current
            endloop
          
        endif
      
        // --- TOWN HALLS PREFERRED LOCATIONS ---  
        if searchPreferredLocations and prioritizeTownHalls and preferred_locations_count_current > 0 then
      
            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls at preferred locations...")
          
            if AdvAssignTargetableTownHalls(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")
              
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")
                  
                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)
              
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")
                          
                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif  
          
            endif
      
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif
      
        endif
      
        exitwhen target != null
      
        // --- BUILDINGS PREFERRED LOCATIONS ---  
        if searchPreferredLocations and preferred_locations_count_current > 0 then
      
            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings at preferred locations...")
          
            if AdvAssignTargetableBuildings(targetPlayer, true) then  
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")
          
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")
                  
                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)
              
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")
                          
                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif  
              
            endif
              
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: building target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: building target found!")
            endif
      
        endif
      
        exitwhen target != null
  
        // --- TOWN HALLS EVERYWHERE ----  
        if searchEverywhere and prioritizeTownHalls then
      
            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls anywhere on the map...")
          
            if AdvAssignTargetableTownHalls(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")
              
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")
                  
                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)
              
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")
                          
                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif  
          
            endif
      
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif
      
          
        endif
      
        exitwhen target != null
      
        // --- BUILDINGS EVERYWHERE ---  
        if searchEverywhere then
      
            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings anywhere on the map...")
          
            if AdvAssignTargetableBuildings(targetPlayer, false) then  
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")
          
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")
                  
                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)
              
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")
                          
                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif  
              
                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: building target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: building target found!")
                endif
              
            endif
          
        endif
      
        exitwhen target != null
      
        // --- UNITS PREFERRED LOCATIONS ---  
        if canSearchUnits and searchPreferredLocations and preferred_locations_count_current > 0 then
      
            call AdvDebugDisplayToPlayer("AI Info: looking for target units at preferred locations...")
          
            if AdvAssignTargetableUnits(targetPlayer, true) then  
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")
          
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")
                  
                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)
              
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")
                          
                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif  
              
            endif
              
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: unit target found!")
            endif
      
        endif
      
        exitwhen target != null
      
        // --- UNITS EVERYWHERE ---  
        if canSearchUnits and searchEverywhere then
      
            call AdvDebugDisplayToPlayer("AI Info: looking for target units anywhere on the map...")
          
            if AdvAssignTargetableUnits(targetPlayer, false) then  
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")
          
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")
                  
                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)
              
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")
                          
                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif  
              
                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: unit target found!")
                endif
              
            endif
          
        endif
      
        exitwhen target != null  
      
        call Sleep(5)
        set searchTargetCycle = searchTargetCycle + 1
      
        // When using the last target as start point, we are continuing the attack, so cycle less times (a negative value means to cycle forever)
        if search_target_max_cycles_last_target > -1 then
            exitwhen useLastTarget and searchTargetCycle > search_target_max_cycles_last_target
        endif
      
        // Cycles timeout (a negative value means to cycle forever)
        if search_target_max_cycles_default > -1 then
            exitwhen searchTargetCycle > search_target_max_cycles_default
        endif
      
        call AdvDebugDisplayToPlayer("AI Info: no target found, trying again...")
      
    endloop
  
    // --- LAUNCH ATTACK IF THERE IS A TARGET---
  
    if target != null then
  
        call AdvDebugDisplayToPlayer("AI Info: assigning target coordinates and launching attack!")
      
        set targetX = GetUnitX(target)
        set targetY = GetUnitY(target)
      
        // Send the captain to attack and store last targets
        set last_target_x = targetX
        set last_target_y = targetY
        call SetCaptainHome(ATTACK_CAPTAIN, targetX,  targetY)
        call CaptainAttack(targetX, targetY)
      
        if not suicide then
            call Sleep(5)
        endif
      
        // Target found
        return true
      
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: no target found, done trying!")
  
    // No target found
    return false

endfunction

//============================================================================
// ADVANCED SUICIDE WAVE ROUTINE
//============================================================================
function AdvWaveReachGoalSleep takes integer seconds returns nothing

    // Decrease counter seconds to reach the goal
    set wave_reach_goal_sleep_seconds = wave_reach_goal_sleep_seconds - seconds
  
    // Wait with precision
    loop
  
        exitwhen seconds <= 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
      
    endloop
  
endfunction

function AdvWaveEnterCombatSleep takes integer seconds returns nothing

    // Decrease counter seconds to enter combat
    set wave_enter_combat_sleep_seconds = wave_enter_combat_sleep_seconds - seconds
  
    // Wait with precision
    loop
  
        exitwhen seconds <= 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
      
    endloop
  
endfunction

function AdvWaveClearGoalSleep takes integer seconds returns nothing

    // Decrease counter seconds to die
    set wave_clear_goal_sleep_seconds = wave_clear_goal_sleep_seconds - seconds
  
    // Wait with precision
    loop
  
        exitwhen seconds <= 0
        exitwhen suicide

        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
      
    endloop
  
endfunction

//============================================================================

function AdvSuicideWaveRoutineReachGoal takes boolean timeout returns integer
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean allDead = false
    local boolean outOfTime = false

    loop
  
        exitwhen suicide
  
        // Goal reached
        if CaptainAtGoal() then
            set goalReached = true
        endif
        exitwhen CaptainAtGoal()
              
        // Entered combat before reaching goal
        if CaptainInCombat(true) then
            set combatEntered = true
        endif
          
        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()
          
        call AdvWaveReachGoalSleep(2)
          
        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds
        endif
      
    endloop
  
    // Goal is reached and fought before
    if goalReached and combatEntered then
        return 1
    // Goal is reached and not fought before
    elseif goalReached and not combatEntered then
        return 2
    // Attack wave is dead (no matter having fought)
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif
  
    // unreachable, but my syntax checker wants it -P
    return 0
endfunction

function AdvSuicideWaveRoutineEnterCombat takes boolean timeout returns integer
    local boolean combatEntered = false
    local boolean allDead = false
    local boolean outOfTime = false

    loop
  
        exitwhen suicide
  
        // Combat
        if CaptainInCombat(true) then
            set combatEntered = true
        endif
        exitwhen CaptainInCombat(true)
          
        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()
          
        call AdvWaveEnterCombatSleep(1)
          
        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_enter_combat_sleep_seconds < -wave_enter_combat_timeout_seconds
        endif
      
    endloop
  
    // Combat entered
    if combatEntered then
        return 1
    // Attack wave is dead
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif
  
    // unreachable, but my syntax checker wants it -P
    return 0
endfunction

function AdvSuicideWaveRoutineClearGoal takes boolean timeout returns integer
    local boolean goalCleared = false
    local boolean allDead = false
    local boolean outOfTime = false

    loop
  
        exitwhen suicide

        // Goal cleared
        if not CaptainInCombat(true) then
            set goalCleared = true
        endif
        exitwhen not CaptainInCombat(true)
  
        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()
      
        call AdvWaveClearGoalSleep(2)
      
        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_clear_goal_sleep_seconds < -wave_clear_goal_timeout_seconds
        endif
      
    endloop
  
    // Goal cleared
    if goalCleared then
        return 1
    // Attack wave is dead
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif
  
    // unreachable, but my syntax checker wants it -P
    return 0
endfunction

//============================================================================

function AdvSuicideWaveRoutine takes player targetPlayer, real startX, real startY, boolean reachGoalTimeout, boolean enterCombatTimeout, boolean clearGoalTimeout, integer continuePercentage returns nothing
    local integer continuePercentageNew = continuePercentage
    local real previousTargetX = last_target_x
    local real previousTargetY = last_target_y
    local integer state = 0
    local boolean combatEnteredBeforeGoal = false
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean goalCleared = false
    local boolean continueAttackReducePercentageIfFarAway = continue_attack_reduce_percentage_if_far_away
    local boolean newTargetFound = false
  
    // Reset all timeout seconds
    set wave_reach_goal_sleep_seconds = 0
    set wave_enter_combat_sleep_seconds = 0
    set wave_clear_goal_sleep_seconds = 0
  
    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to reach goal...")

    // --- WAIT TO REACH GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineReachGoal(reachGoalTimeout)
  
    // Timeout
    if state == 0 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached timeout!")
  
        set goalReached = false
        set combatEnteredBeforeGoal = false
      
    // Dead
    elseif state == -1 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")
  
        set goalReached = false
        set combatEnteredBeforeGoal = false
      
    // Goal reached after figthing
    elseif state == 1 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached after figthing!")
  
        set goalReached = true
        set combatEnteredBeforeGoal = true
      
    // Goal reached before figthing
    elseif state == 2 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached before figthing!")
  
        set goalReached = true
        set combatEnteredBeforeGoal = false
      
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to enter combat...")

    // --- WAIT TO ENTER COMBAT (OR DIE) ---
    set state = AdvSuicideWaveRoutineEnterCombat(enterCombatTimeout)
  
    // Timeout
    if state == 0 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave enter combat reached timeout!")
  
        set combatEntered = false
      
    // Dead
    elseif state == -1 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")
  
        set combatEntered = true
      
    // Combat entered
    elseif state == 1 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave combat entered!")
  
        set combatEntered = true
      
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to clear goal...")

    // --- WAIT TO CLEAR GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineClearGoal(clearGoalTimeout)
  
    // Timeout
    if state == 0 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave clear goal reached timeout!")
  
        set combatEntered = false
      
    // Dead
    elseif state == -1 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")
  
        set combatEntered = true
      
    // Goal cleared
    elseif state == 1 then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave goal cleared!")
  
        set goalCleared = true
      
    endif

    // --- GROUP DEAD ---
    if CaptainIsEmpty() then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave dead, going back home...")
      
        call SetCaptainHome(ATTACK_CAPTAIN, startX,  startY)
        call CaptainAttack(startX, startY)
      
        if not suicide then
            call Sleep(1)
        endif
      
        call TeleportCaptain(startX, startY)
      
        // Wait for the captain to go back home
      
        loop
      
            call Sleep(1)
          
            exitwhen suicide
            exitwhen CaptainAtGoal()
      
        endloop
      
        call AdvDebugDisplayToPlayer("AI Info: returned home!")
      
        call ClearCaptainTargets()
        call ResetCaptainLocs()
        call AdvSetCaptainHome(BOTH_CAPTAINS)
      
        if not suicide then
            call Sleep(2)
        endif
      
        return
      
    endif
  
    // --- ATTACK FINISHED CONTINUE (IF REQUIRED) ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave finished duty, checking if  should continue...")  
  
    // If attack wave has not fought before reaching goal and then never fought again, it must continue
    if goalReached and not combatEnteredBeforeGoal and not combatEntered then
  
        call AdvDebugDisplayToPlayer("AI Info: attack wave never fought at goal nor to reach it, forcing continue...")  
  
        set continuePercentage = 100
        set continueAttackReducePercentageIfFarAway = false
    endif

    if not suicide and not CaptainIsEmpty() and GetRandomInt(0, 100) <= continuePercentage then

        call AdvDebugDisplayToPlayer("AI Info: attack wave continues to another target!")
      
        set newTargetFound = AdvSuicideWaveLaunch(targetPlayer, 0.0, 0.0, false, true, can_search_units, false, true, true)
      
        if newTargetFound then
            // If target is not close to previous one, decrease the continue percentage (if required)
            if continueAttackReducePercentageIfFarAway and AdvDistance(previousTargetX, previousTargetY, last_target_x, last_target_y) > continue_far_away_radius then
                call AdvDebugDisplayToPlayer("AI Info: reducing percentage of continuing again...")
              
                set continuePercentageNew = continuePercentageNew - continue_attack_reduce_percentage
            endif
          
            call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continuePercentageNew)
          
            return  
        endif
      
    endif
  
    // --- ATTACK FINISHED GO HOME ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave does not continue, going back home...")
  
    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)
  
    // Wait for the captain to go back home
    loop
  
        call Sleep(1)
      
        exitwhen suicide
        exitwhen CaptainAtGoal()
  
    endloop
  
    call AdvDebugDisplayToPlayer("AI Info: returned home!")
  
    call ClearCaptainTargets()
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)
  
    if not suicide then
        call Sleep(2)
    endif
  
endfunction

//============================================================================
// ADVANCED SUICIDE ON PLAYER
//============================================================================
function AdvSuicideOnPlayer takes integer seconds, player targetPlayer, real startX, real startY returns nothing
    local integer woodPeons
    local boolean targetFound = false
  
    call AdvDebugDisplayToPlayer("AI Info: waiting to attack...")
      
    // Make sure the captain are at right spots
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)
  
    if not suicide then
        call Sleep(2)
    endif

    // Prepare attack for given seconds
    if not PrepSuicideOnPlayer(seconds) then
        return
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: wait is over!")

    // If necessary (e.g. undead race), save the wood peons before attack group is prepared
    if wood_peons_warriors then
        set woodPeons = campaign_wood_peons
        set campaign_wood_peons = 0
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: forming group...")

    loop
  
        exitwhen suicide
  
        call FormGroup(5, true)  
        exitwhen sleep_seconds <= 0
      
    endloop
  
    call AdvDebugDisplayToPlayer("AI Info: group formed!")
  
    // If necessary (e.g. undead race), reset the wood peons after attack group is ready
    if wood_peons_warriors then
        set campaign_wood_peons = woodPeons
    endif
  
    // Travel to start position
    call AdvDebugDisplayToPlayer("AI Info: traveling to start position...")
  
    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)
  
    loop
  
        call Sleep(1)
  
        exitwhen suicide
        exitwhen CaptainAtGoal()  
        exitwhen CaptainIsEmpty()
  
    endloop
  
    // Stop here if captain is empty
    if CaptainIsEmpty() then
        return
    endif
  
    call AdvDebugDisplayToPlayer("AI Info: start position reached!")
  
    // Launch the wave against a suitable target
    set targetFound = AdvSuicideWaveLaunch(targetPlayer, startX, startY, search_preferred_locations, search_everywhere, can_search_units, prioritize_town_halls, prioritize_nearest, false)
      
    set harass_length = 0

    // Start the wave routine if there is a target
    if targetFound then
        call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continue_attack_percentage)
    endif
  
endfunction

//============================================================================

function AdvSuicideOnPlayerEx takes integer easy, integer med, integer hard, player targetPlayer, real startX, real startY returns nothing

    if difficulty == EASY then
        call AdvSuicideOnPlayer(easy, targetPlayer, startX, startY)  
    elseif difficulty == NORMAL then
        call AdvSuicideOnPlayer(med, targetPlayer, startX, startY)  
    else
        call AdvSuicideOnPlayer(hard, targetPlayer, startX, startY)  
    endif
  
endfunction

function setbase takes nothing returns nothing
    call SetBuildUnitEx( 1,1,1, DPEASANT       )
    call SetBuildUnitEx( 1,1,1, DTOWN_HALL     )
    call SetBuildUnitEx( 5,5,5, DPEASANT       )
    call SetBuildUnitEx( 4,4,4, DBARRACKS      )
    call SetBuildUnitEx( 1,1,1, BLACKSMITH    )
    call SetBuildUnitEx( 1,1,1, LUMBER_MILL    )
    call SetBuildUnitEx( 1,1,1, DKEEP          )
    call SetBuildUnitEx( 10,10,10, HOUSE      )
    call SetBuildUnitEx( 1,1,1, DCHURCH        )
    call SetBuildUnitEx( 1,1,1, DWORKSHOP        )
    call SetBuildUnitEx( 8,8,8, DPEASANT       )
    call SetBuildUnitEx( 4,4,4, WATCH_TOWER       )
    call SetBuildUnitEx( 4,4,4, GUARD_TOWER       )
endfunction

function main takes nothing returns nothing
    call AdvSetDebug(human, true)

    call CampaignAI(HOUSE,null)
    call GroupTimedLife(true)
    call SetSlowChopping(true)

    call setbase()
  
    call CampaignDefenderEx( 12,18,24, DAWNGUARD    )
    call CampaignDefenderEx( 8,10,12, BOWMAN )
    call CampaignDefenderEx( 2,4,6,  DAWNPRIEST )
    call CampaignDefenderEx( 2,2,4,  AUGUR )
    call CampaignDefenderEx( 0,1,3,  DBALLISTA )
  
    // *** WAVE 1 ***
    call InitAssaultGroup()
        call CampaignAttackerEx( 6,8,12, DAWNGUARD)
    call CampaignAttackerEx( 3,4,6, BOWMAN )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 2 ***
    call InitAssaultGroup()
        call CampaignAttackerEx( 8,10,16, DAWNGUARD)
    call CampaignAttackerEx( 2,2,6, BOWMAN )
    call CampaignAttackerEx( 2,2,2, DAWNPRIEST )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 3 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 10,12,18, DAWNGUARD)
    call CampaignAttackerEx( 4,4,8, BOWMAN )
    call CampaignAttackerEx( 2,4,6, DAWNPRIEST )
    call CampaignAttackerEx( 1,1,2, DBALLISTA )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 4 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 10,12,18, DAWNGUARD)
    call CampaignAttackerEx( 6,8,12, BOWMAN )
    call CampaignAttackerEx( 2,2,3, DAWNPRIEST )
    call CampaignAttackerEx( 1,1,2, DBALLISTA )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 5 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 10,12,18, DAWNGUARD)
    call CampaignAttackerEx( 4,4,8, BOWMAN )
    call CampaignAttackerEx( 2,4,6, DAWNPRIEST )
    call CampaignAttackerEx( 1,1,1, AUGUR )
    call CampaignAttackerEx( 1,1,2, DBALLISTA )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 6 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 10,12,18, DAWNGUARD)
    call CampaignAttackerEx( 6,8,12, BOWMAN )
    call CampaignAttackerEx( 2,2,3, DAWNPRIEST )
    call CampaignAttackerEx( 1,1,2, DBALLISTA )
    call CampaignAttackerEx( 1,1,1, AUGUR )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 7 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 10,12,18, DAWNGUARD)
    call CampaignAttackerEx( 4,4,8, BOWMAN )
    call CampaignAttackerEx( 2,4,6, DAWNPRIEST )
    call CampaignAttackerEx( 1,1,2, AUGUR )
    call CampaignAttackerEx( 1,1,2, DBALLISTA )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 8 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 10,12,18, DAWNGUARD)
    call CampaignAttackerEx( 6,8,12, BOWMAN )
    call CampaignAttackerEx( 2,2,3, DAWNPRIEST )
    call CampaignAttackerEx( 1,1,2, DBALLISTA )
    call CampaignAttackerEx( 1,1,2, AUGUR )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    loop
    // *** WAVE 9 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 12,18,24, DAWNGUARD)
    call CampaignAttackerEx( 8,10,12, BOWMAN )
    call CampaignAttackerEx( 2,4,6, DAWNPRIEST )
    call CampaignAttackerEx( 2,3,4, AUGUR )
    call CampaignAttackerEx( 1,1,3, DBALLISTA )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)

    // *** WAVE 10 ***
    call InitAssaultGroup()
    call CampaignAttackerEx( 12,18,24, DAWNGUARD)
    call CampaignAttackerEx( 8,10,12, BOWMAN )
    call CampaignAttackerEx( 2,3,4, DAWNPRIEST )
    call CampaignAttackerEx( 2,3,6, AUGUR )
    call CampaignAttackerEx( 1,1,3, DBALLISTA )
    call AdvSuicideOnPlayerEx(M2, M2, M2, human, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
    endloop
endfunction

JASS:
    player debug_player
    boolean debugTrace                                             = false

    boolean suicide                                            = false
    integer wave_reach_goal_timeout_seconds                 = 300
    integer wave_enter_combat_timeout_seconds                 = 3
    integer wave_clear_goal_timeout_seconds                    = 60

    real continue_far_away_radius                             = 960.0
    real preferred_locations_radius                         = 640.0

    integer wave_reach_goal_sleep_seconds
    integer wave_enter_combat_sleep_seconds
    integer wave_clear_goal_sleep_seconds

    real defense_captain_home_x
    real defense_captain_home_y
    boolean defense_captain_home_set                         = false

    real attack_wave_gather_return_x
    real attack_wave_gather_return_y
    boolean attack_wave_gather_return_set                    = false
    
    location array preferred_locations
    integer preferred_locations_count                         = 0

    location array preferred_locations_current
    integer preferred_locations_count_current                 = 0
    boolean wood_peons_warriors                             = false
    boolean search_preferred_locations                         = true
    boolean search_everywhere                                 = true
    boolean can_search_units                                 = true
    boolean can_search_invulnerable                         = false
    boolean prioritize_town_halls                             = true
    boolean prioritize_nearest                                 = true
    boolean reach_goal_timeout                                 = true
    boolean enter_combat_timeout                             = true
    boolean clear_goal_timeout                                 = true
    integer continue_attack_percentage                         = 100
    boolean continue_attack_reduce_percentage_if_far_away     = true
    integer continue_attack_reduce_percentage                 = 25

    unit array targetable_town_halls
    integer targetable_town_halls_count                     = 0
    unit array targetable_buildings
    integer targetable_buildings_count                         = 0

    unit array targetable_units
    integer targetable_units_count                             = 0
    real last_target_x
    real last_target_y
    integer search_target_max_cycles_default                = 100
    integer search_target_max_cycles_last_target            = 10
JASS:
//============================================================================
//  ADVANCED UTILITY
//============================================================================
function AdvDistance takes real x1, real y1, real x2, real y2 returns real
    return SquareRoot(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)))
endfunction
function AdvIsUnitAlive takes unit u returns boolean
    return GetUnitState(u, UNIT_STATE_LIFE) > 0.0
endfunction
function AdvDebugDisplayToPlayer takes string message returns nothing
    if debugTrace then
        call DisplayTextToPlayer(debug_player, 0, 0, message)
    endif
    call Trace(message)
endfunction
// Adds all units in g to h. Substitute for BlzGroupAddGroupFast
function AdvGroupAddGroup takes group g, group h returns nothing
    local unit u
    local group g2
    set g2 = CreateGroup()
    call AdvDebugDisplayToPlayer("AI Info: trying to add group!")
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupAddUnit(h, u)
        call GroupRemoveUnit(g, u)
        call GroupAddUnit(g2, u)
    endloop
    loop
        set u = FirstOfGroup(g2)
        exitwhen u == null
        call GroupAddUnit(g, u)
        call GroupRemoveUnit(g2, u)
    endloop
    call DestroyGroup(g2)
endfunction
// Counts the units in a group. Substitute for BlzGroupGetSize. No I'm not happy that this is O(n)
function AdvGroupGetSize takes group g returns integer
    local integer size
    local unit u
    local group g2
    set g2 = CreateGroup()
    set size = 0
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        set size = size + 1
        call GroupRemoveUnit(g, u)
        call GroupAddUnit(g2, u)
    endloop
    loop
        set u = FirstOfGroup(g2)
        exitwhen u == null
        call GroupAddUnit(g, u)
        call GroupRemoveUnit(g2, u)
    endloop
    call DestroyGroup(g2)
    return size
endfunction
//============================================================================
//  ADVANCED SETTINGS
//============================================================================
function AdvSetDebug takes player debugPlayer, boolean flag returns nothing
    set debug_player = debugPlayer
    set debugTrace = flag
endfunction
//============================================================================
function AdvSetWaveReachGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_reach_goal_timeout_seconds = seconds
endfunction
function AdvSetWaveEnterCombatTimeoutSeconds takes integer seconds returns nothing
    set wave_enter_combat_timeout_seconds = seconds
endfunction
function AdvSetWaveClearGoalTimeoutSeconds takes integer seconds returns nothing
    set wave_clear_goal_timeout_seconds = seconds
endfunction
//============================================================================
function AdvSetContinueFarAwayRadius takes real radius returns nothing
    set continue_far_away_radius = radius
endfunction
function AdvSetPreferredLocationsRadius takes real radius returns nothing
    set preferred_locations_radius = radius
endfunction
//============================================================================
function AdvSetWoodPeonsWarriors takes boolean flag returns nothing
    set wood_peons_warriors = flag
endfunction
//============================================================================
function AdvSetSearchPreferredLocations takes boolean flag returns nothing
    set search_preferred_locations = flag
endfunction
function AdvSetSearchEverywhere takes boolean flag returns nothing
    set search_everywhere = flag
endfunction
function AdvSetCanSearchUnit takes boolean flag returns nothing
    set can_search_units = flag
endfunction
function AdvSetCanSearchInvulnerable takes boolean flag returns nothing
    set can_search_invulnerable = flag
endfunction
//============================================================================
function AdvSetPrioritizeTownHalls takes boolean flag returns nothing
    set prioritize_town_halls = flag
endfunction
function AdvSetPrioritizeNearest takes boolean flag returns nothing
    set prioritize_nearest = flag
endfunction
function AdvSetSearchTargetMaxCyclesDefault takes integer value returns nothing
    set search_target_max_cycles_default = value
endfunction
function AdvSetSearchTargetMaxCyclesLastTarget takes integer value returns nothing
    set search_target_max_cycles_last_target = value
endfunction
//============================================================================
function AdvSetUseReachGoalTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction
function AdvSetUseEnterCombatTimeout takes boolean flag returns nothing
    set reach_goal_timeout = flag
endfunction
function AdvSetUseClearGoalTimeout takes boolean flag returns nothing
    set enter_combat_timeout = flag
endfunction
//============================================================================
function AdvSetContinueAttackPercentage takes integer percentage returns nothing
    set continue_attack_percentage = percentage
endfunction
function AdvSetContinueAttackReducePercentageIfFarAway takes boolean flag returns nothing
    set continue_attack_reduce_percentage_if_far_away = flag
endfunction
function AdvSetContinueAttackReducePercentage takes integer percentage returns nothing
    set continue_attack_reduce_percentage = percentage
endfunction
//============================================================================
function AdvSetAttackWaveGatherReturnXY takes real x, real y returns nothing
    set attack_wave_gather_return_x = x
    set attack_wave_gather_return_y = y
    set attack_wave_gather_return_set = true
endfunction
function AdvSetDefenseCaptainHomeXY takes real x, real y returns nothing
    set defense_captain_home_x = x
    set defense_captain_home_y = y
    set defense_captain_home_set = true
endfunction
// Note: this should never be called in the script with argument different from DEFENSE_CAPTAIN
function AdvSetCaptainHome takes integer whichCaptain returns nothing
    if whichCaptain == BOTH_CAPTAINS then

        if defense_captain_home_set then
            call SetCaptainHome(DEFENSE_CAPTAIN, defense_captain_home_x, defense_captain_home_y)
        endif
    
        if attack_wave_gather_return_set then
            call SetCaptainHome(ATTACK_CAPTAIN, attack_wave_gather_return_x, attack_wave_gather_return_y)
        endif
    
        return
    
    endif
    if defense_captain_home_set and whichCaptain == DEFENSE_CAPTAIN then

        call SetCaptainHome(DEFENSE_CAPTAIN, defense_captain_home_x, defense_captain_home_y)
        return
    
    endif

    if attack_wave_gather_return_set and whichCaptain == ATTACK_CAPTAIN then

        call SetCaptainHome(ATTACK_CAPTAIN, attack_wave_gather_return_x, attack_wave_gather_return_y)
        return
    
    endif
    
endfunction
//============================================================================
function AdvAddPreferredLocation takes real x, real y returns integer
    set preferred_locations[preferred_locations_count] = Location(x, y)
    set preferred_locations_count = preferred_locations_count + 1

    return preferred_locations_count
endfunction
function AdvSetPreferredLocationXY takes integer index, real x, real y returns nothing
    if index < preferred_locations_count then
        set preferred_locations[index] = Location(x, y)
    endif
endfunction
//============================================================================
function AdvSetSuicide takes boolean flag returns nothing
    set suicide = flag
endfunction
//============================================================================
// ADVANCED SUICIDE WAVE LAUNCH
//============================================================================
function AdvGetRandomTargetableTownHall takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_town_halls_count - 1)

    if targetable_town_halls_count <= 0 then
        return null
    endif
    return targetable_town_halls[randomIndex]
endfunction
function AdvGetNearestTargetableTownHall takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance

    if targetable_town_halls_count <= 0 then
        return null
    endif

    set minDistance = AdvDistance(GetUnitX(targetable_town_halls[minDistanceIndex]), GetUnitY(targetable_town_halls[minDistanceIndex]), x, y)
    
    loop

        set index = index + 1        
        exitwhen index >= targetable_town_halls_count
    
        if AdvDistance(GetUnitX(targetable_town_halls[index]), GetUnitY(targetable_town_halls[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_town_halls[minDistanceIndex]), GetUnitY(targetable_town_halls[minDistanceIndex]), x, y)
        endif
    
    endloop

    return targetable_town_halls[minDistanceIndex]

endfunction
//============================================================================
function AdvGetRandomTargetableBuilding takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_buildings_count - 1)

    if targetable_buildings_count <= 0 then
        return null
    endif
    return targetable_buildings[randomIndex]
endfunction
function AdvGetNearestTargetableBuilding takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance

    if targetable_buildings_count <= 0 then
        return null
    endif

    set minDistance = AdvDistance(GetUnitX(targetable_buildings[minDistanceIndex]), GetUnitY(targetable_buildings[minDistanceIndex]), x, y)
    
    loop

        set index = index + 1
        exitwhen index >= targetable_buildings_count
            
        if AdvDistance(GetUnitX(targetable_buildings[index]), GetUnitY(targetable_buildings[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_buildings[minDistanceIndex]), GetUnitY(targetable_buildings[minDistanceIndex]), x, y)
        endif
    
    endloop

    return targetable_buildings[minDistanceIndex]

endfunction
//============================================================================
function AdvGetRandomTargetableUnit takes nothing returns unit
    local integer randomIndex = GetRandomInt(0, targetable_units_count - 1)

    if targetable_units_count <= 0 then
        return null
    endif
    return targetable_units[randomIndex]
endfunction
function AdvGetNearestTargetableUnit takes real x, real y returns unit
    local integer index = 0
    local integer minDistanceIndex = 0
    local real minDistance

    if targetable_units_count <= 0 then
        return null
    endif

    set minDistance = AdvDistance(GetUnitX(targetable_units[minDistanceIndex]), GetUnitY(targetable_units[minDistanceIndex]), x, y)
    
    loop

        set index = index + 1
        exitwhen index >= targetable_units_count
            
        if AdvDistance(GetUnitX(targetable_units[index]), GetUnitY(targetable_units[index]), x, y) < minDistance then
            set minDistanceIndex = index
            set minDistance = AdvDistance(GetUnitX(targetable_units[minDistanceIndex]), GetUnitY(targetable_units[minDistanceIndex]), x, y)
        endif
    
    endloop

    return targetable_units[minDistanceIndex]

endfunction
//============================================================================
function AdvAssignTargetableTownHalls takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0

    set targetable_town_halls_count = 0
    
    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)     
            call AdvGroupAddGroup(tempGroup, targetsGroup)
        
            set index = index + 1    
            call GroupClear(tempGroup)
        
            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif

    if AdvGroupGetSize(targetsGroup) <= 0 then
        return false
    endif

    loop

        set currentUnit = FirstOfGroup(targetsGroup)
    
        exitwhen currentUnit == null
    
        if GetOwningPlayer(currentUnit) == targetPlayer and IsUnitType(currentUnit, UNIT_TYPE_TOWNHALL) and IsUnitType(currentUnit, UNIT_TYPE_STRUCTURE) and AdvIsUnitAlive(currentUnit) then        
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_town_halls[targetable_town_halls_count] = currentUnit
                    set targetable_town_halls_count = targetable_town_halls_count + 1
                endif
            else
                set targetable_town_halls[targetable_town_halls_count] = currentUnit
                set targetable_town_halls_count = targetable_town_halls_count + 1
            endif
        endif
    
        call GroupRemoveUnit(targetsGroup, currentUnit)

    endloop

    if targetable_town_halls_count > 0 then
        return true
    endif

    return false

endfunction
//============================================================================
function AdvAssignTargetableBuildings takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0

    set targetable_buildings_count = 0

    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)     
            call AdvGroupAddGroup(tempGroup, targetsGroup)
        
            set index = index + 1    
            call GroupClear(tempGroup)
        
            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif

    if AdvGroupGetSize(targetsGroup) <= 0 then
        return false
    endif

    loop

        set currentUnit = FirstOfGroup(targetsGroup)
    
        exitwhen currentUnit == null
    
        if GetOwningPlayer(currentUnit) == targetPlayer and IsUnitType(currentUnit, UNIT_TYPE_STRUCTURE) and AdvIsUnitAlive(currentUnit) then    
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_buildings[targetable_buildings_count] = currentUnit
                    set targetable_buildings_count = targetable_buildings_count + 1
                endif
            else
                set targetable_buildings[targetable_buildings_count] = currentUnit
                set targetable_buildings_count = targetable_buildings_count + 1
            endif
        endif
    
        call GroupRemoveUnit(targetsGroup, currentUnit)

    endloop

    if targetable_buildings_count > 0 then
        return true
    endif

    return false

endfunction
//============================================================================
function AdvAssignTargetableUnits takes player targetPlayer, boolean preferredLocations returns boolean
    local unit currentUnit = null
    local group targetsGroup = CreateGroup()
    local group tempGroup = CreateGroup()
    local integer index = 0

    set targetable_units_count = 0

    if preferredLocations then
        loop
            call GroupEnumUnitsInRangeOfLoc(tempGroup, preferred_locations_current[index], preferred_locations_radius, null)     
            call AdvGroupAddGroup(tempGroup, targetsGroup)
        
            set index = index + 1    
            call GroupClear(tempGroup)
        
            exitwhen index >= preferred_locations_count_current
        endloop
    else
        call GroupEnumUnitsOfPlayer(targetsGroup, targetPlayer, null)
    endif

    if AdvGroupGetSize(targetsGroup) <= 0 then
        return false
    endif

    loop

        set currentUnit = FirstOfGroup(targetsGroup)
    
        exitwhen currentUnit == null
    
        if GetOwningPlayer(currentUnit) == targetPlayer and AdvIsUnitAlive(currentUnit) then
            if not can_search_invulnerable then
                if not BlzIsUnitInvulnerable(currentUnit) then
                    set targetable_units[targetable_units_count] = currentUnit
                    set targetable_units_count = targetable_units_count + 1
                endif
            else
                set targetable_units[targetable_units_count] = currentUnit
                set targetable_units_count = targetable_units_count + 1
            endif
        endif
    
        call GroupRemoveUnit(targetsGroup, currentUnit)

    endloop

    if targetable_units_count > 0 then
        return true
    endif

    return false

endfunction
//============================================================================
function AdvSuicideWaveLaunch takes player targetPlayer, real startX, real startY, boolean searchPreferredLocations, boolean searchEverywhere, boolean canSearchUnits, boolean prioritizeTownHalls, boolean prioritizeNearest, boolean useLastTarget returns boolean
    local unit target = null
    local integer index = 0
    local real targetX
    local real targetY
    local integer searchTargetCycle = 0

    // Set the start coordinates
    if useLastTarget then
        set startX = last_target_x
        set startY = last_target_y
    endif

    call AdvDebugDisplayToPlayer("AI Info: searching for a target...")

    // --- LOOP UNTIL TARGET IS FOUND OR TIMEOUT IS REACHED ---
    loop

        exitwhen suicide

        // --- CACHE PREFERRED LOCATIONS ---
        set preferred_locations_count_current = 0
        if searchPreferredLocations and preferred_locations_count > 0 then
    
            set preferred_locations_count_current = preferred_locations_count
            loop
        
                set preferred_locations_current[index] = preferred_locations[index]
        
                set index = index + 1
                exitwhen index >= preferred_locations_count_current
            endloop
        
        endif
    
        // --- TOWN HALLS PREFERRED LOCATIONS ---
        if searchPreferredLocations and prioritizeTownHalls and preferred_locations_count_current > 0 then
    
            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls at preferred locations...")
        
            if AdvAssignTargetableTownHalls(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")
            
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")
                
                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)
            
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")
                        
                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif
        
            endif
    
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif
    
        endif
    
        exitwhen target != null
    
        // --- BUILDINGS PREFERRED LOCATIONS ---
        if searchPreferredLocations and preferred_locations_count_current > 0 then
    
            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings at preferred locations...")
        
            if AdvAssignTargetableBuildings(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")
        
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")
                
                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)
            
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")
                        
                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif
            
            endif
            
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: building target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: building target found!")
            endif
    
        endif
    
        exitwhen target != null

        // --- TOWN HALLS EVERYWHERE ----
        if searchEverywhere and prioritizeTownHalls then
    
            call AdvDebugDisplayToPlayer("AI Info: looking for target town halls anywhere on the map...")
        
            if AdvAssignTargetableTownHalls(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable town hall found!")
            
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest town hall...")
                
                    // Nearest town hall
                    set target = AdvGetNearestTargetableTownHall(startX, startY)
            
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random town hall target...")
                        
                    // Random town hall
                    set target = AdvGetRandomTargetableTownHall()
                endif
        
            endif
    
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: town hall target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: town hall target found!")
            endif
    
        
        endif
    
        exitwhen target != null
    
        // --- BUILDINGS EVERYWHERE ---
        if searchEverywhere then
    
            call AdvDebugDisplayToPlayer("AI Info: looking for target buildings anywhere on the map...")
        
            if AdvAssignTargetableBuildings(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable building found!")
        
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest building...")
                
                    // Nearest building
                    set target = AdvGetNearestTargetableBuilding(startX, startY)
            
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random building...")
                        
                    // Random building
                    set target = AdvGetRandomTargetableBuilding()
                endif
            
                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: building target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: building target found!")
                endif
            
            endif
        
        endif
    
        exitwhen target != null
    
        // --- UNITS PREFERRED LOCATIONS ---
        if canSearchUnits and searchPreferredLocations and preferred_locations_count_current > 0 then
    
            call AdvDebugDisplayToPlayer("AI Info: looking for target units at preferred locations...")
        
            if AdvAssignTargetableUnits(targetPlayer, true) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")
        
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")
                
                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)
            
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")
                        
                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif
            
            endif
            
            if target == null then
                call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
            else
                call AdvDebugDisplayToPlayer("AI Info: unit target found!")
            endif
    
        endif
    
        exitwhen target != null
    
        // --- UNITS EVERYWHERE ---
        if canSearchUnits and searchEverywhere then
    
            call AdvDebugDisplayToPlayer("AI Info: looking for target units anywhere on the map...")
        
            if AdvAssignTargetableUnits(targetPlayer, false) then
                call AdvDebugDisplayToPlayer("AI Info: at least one targetable unit found!")
        
                if prioritizeNearest then
                    call AdvDebugDisplayToPlayer("AI Info: looking for the nearest unit...")
                
                    // Nearest unit
                    set target = AdvGetNearestTargetableUnit(startX, startY)
            
                else
                    call AdvDebugDisplayToPlayer("AI Info: looking for a random unit...")
                        
                    // Random unit
                    set target = AdvGetRandomTargetableUnit()
                endif
            
                if target == null then
                    call AdvDebugDisplayToPlayer("AI Info: unit target not found!")
                else
                    call AdvDebugDisplayToPlayer("AI Info: unit target found!")
                endif
            
            endif
        
        endif
    
        exitwhen target != null
    
        call Sleep(5)
        set searchTargetCycle = searchTargetCycle + 1
    
        // When using the last target as start point, we are continuing the attack, so cycle less times (a negative value means to cycle forever)
        if search_target_max_cycles_last_target > -1 then
            exitwhen useLastTarget and searchTargetCycle > search_target_max_cycles_last_target
        endif
    
        // Cycles timeout (a negative value means to cycle forever)
        if search_target_max_cycles_default > -1 then
            exitwhen searchTargetCycle > search_target_max_cycles_default
        endif
    
        call AdvDebugDisplayToPlayer("AI Info: no target found, trying again...")
    
    endloop

    // --- LAUNCH ATTACK IF THERE IS A TARGET---

    if target != null then

        call AdvDebugDisplayToPlayer("AI Info: assigning target coordinates and launching attack!")
    
        set targetX = GetUnitX(target)
        set targetY = GetUnitY(target)
    
        // Send the captain to attack and store last targets
        set last_target_x = targetX
        set last_target_y = targetY
        call SetCaptainHome(ATTACK_CAPTAIN, targetX,  targetY)
        call CaptainAttack(targetX, targetY)
    
        if not suicide then
            call Sleep(5)
        endif
    
        // Target found
        return true
    
    endif

    call AdvDebugDisplayToPlayer("AI Info: no target found, done trying!")

    // No target found
    return false
endfunction
//============================================================================
// ADVANCED SUICIDE WAVE ROUTINE
//============================================================================
function AdvWaveReachGoalSleep takes integer seconds returns nothing
    // Decrease counter seconds to reach the goal
    set wave_reach_goal_sleep_seconds = wave_reach_goal_sleep_seconds - seconds

    // Wait with precision
    loop

        exitwhen seconds <= 0
        exitwhen suicide
        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
    
    endloop

endfunction
function AdvWaveEnterCombatSleep takes integer seconds returns nothing
    // Decrease counter seconds to enter combat
    set wave_enter_combat_sleep_seconds = wave_enter_combat_sleep_seconds - seconds

    // Wait with precision
    loop

        exitwhen seconds <= 0
        exitwhen suicide
        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
    
    endloop

endfunction
function AdvWaveClearGoalSleep takes integer seconds returns nothing
    // Decrease counter seconds to die
    set wave_clear_goal_sleep_seconds = wave_clear_goal_sleep_seconds - seconds

    // Wait with precision
    loop

        exitwhen seconds <= 0
        exitwhen suicide
        if seconds >= 5 then
            call Sleep(5)
            set seconds = seconds - 5
        else
            call Sleep(seconds)
            set seconds = 0
        endif
    
    endloop

endfunction
//============================================================================
function AdvSuicideWaveRoutineReachGoal takes boolean timeout returns integer
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean allDead = false
    local boolean outOfTime = false
    loop

        exitwhen suicide

        // Goal reached
        if CaptainAtGoal() then
            set goalReached = true
        endif
        exitwhen CaptainAtGoal()
            
        // Entered combat before reaching goal
        if CaptainInCombat(true) then
            set combatEntered = true
        endif
        
        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()
        
        call AdvWaveReachGoalSleep(2)
        
        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds
        endif
    
    endloop

    // Goal is reached and fought before
    if goalReached and combatEntered then
        return 1
    // Goal is reached and not fought before
    elseif goalReached and not combatEntered then
        return 2
    // Attack wave is dead (no matter having fought)
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif

    // unreachable, but my syntax checker wants it -P
    return 0
endfunction
function AdvSuicideWaveRoutineEnterCombat takes boolean timeout returns integer
    local boolean combatEntered = false
    local boolean allDead = false
    local boolean outOfTime = false
    loop

        exitwhen suicide

        // Combat
        if CaptainInCombat(true) then
            set combatEntered = true
        endif
        exitwhen CaptainInCombat(true)
        
        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()
        
        call AdvWaveEnterCombatSleep(1)
        
        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_enter_combat_sleep_seconds < -wave_enter_combat_timeout_seconds
        endif
    
    endloop

    // Combat entered
    if combatEntered then
        return 1
    // Attack wave is dead
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif

    // unreachable, but my syntax checker wants it -P
    return 0
endfunction
function AdvSuicideWaveRoutineClearGoal takes boolean timeout returns integer
    local boolean goalCleared = false
    local boolean allDead = false
    local boolean outOfTime = false
    loop

        exitwhen suicide
        // Goal cleared
        if not CaptainInCombat(true) then
            set goalCleared = true
        endif
        exitwhen not CaptainInCombat(true)

        // All dead
        if CaptainIsEmpty() then
            set allDead = true
        endif
        exitwhen CaptainIsEmpty()
    
        call AdvWaveClearGoalSleep(2)
    
        // Out of time
        if timeout then
            if wave_reach_goal_sleep_seconds < -wave_reach_goal_timeout_seconds then
                set outOfTime = true
            endif
            exitwhen wave_clear_goal_sleep_seconds < -wave_clear_goal_timeout_seconds
        endif
    
    endloop

    // Goal cleared
    if goalCleared then
        return 1
    // Attack wave is dead
    elseif allDead then
        return -1
    // Timeout expired
    elseif outOfTime then
        return 0
    endif

    // unreachable, but my syntax checker wants it -P
    return 0
endfunction
//============================================================================
function AdvSuicideWaveRoutine takes player targetPlayer, real startX, real startY, boolean reachGoalTimeout, boolean enterCombatTimeout, boolean clearGoalTimeout, integer continuePercentage returns nothing
    local integer continuePercentageNew = continuePercentage
    local real previousTargetX = last_target_x
    local real previousTargetY = last_target_y
    local integer state = 0
    local boolean combatEnteredBeforeGoal = false
    local boolean goalReached = false
    local boolean combatEntered = false
    local boolean goalCleared = false
    local boolean continueAttackReducePercentageIfFarAway = continue_attack_reduce_percentage_if_far_away
    local boolean newTargetFound = false

    // Reset all timeout seconds
    set wave_reach_goal_sleep_seconds = 0
    set wave_enter_combat_sleep_seconds = 0
    set wave_clear_goal_sleep_seconds = 0

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to reach goal...")
    // --- WAIT TO REACH GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineReachGoal(reachGoalTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached timeout!")

        set goalReached = false
        set combatEnteredBeforeGoal = false
    
    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set goalReached = false
        set combatEnteredBeforeGoal = false
    
    // Goal reached after figthing
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached after figthing!")

        set goalReached = true
        set combatEnteredBeforeGoal = true
    
    // Goal reached before figthing
    elseif state == 2 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal reached before figthing!")

        set goalReached = true
        set combatEnteredBeforeGoal = false
    
    endif

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to enter combat...")
    // --- WAIT TO ENTER COMBAT (OR DIE) ---
    set state = AdvSuicideWaveRoutineEnterCombat(enterCombatTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave enter combat reached timeout!")

        set combatEntered = false
    
    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set combatEntered = true
    
    // Combat entered
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave combat entered!")

        set combatEntered = true
    
    endif

    call AdvDebugDisplayToPlayer("AI Info: waiting for attack wave to clear goal...")
    // --- WAIT TO CLEAR GOAL (OR DIE) ---
    set state = AdvSuicideWaveRoutineClearGoal(clearGoalTimeout)

    // Timeout
    if state == 0 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave clear goal reached timeout!")

        set combatEntered = false
    
    // Dead
    elseif state == -1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead!")

        set combatEntered = true
    
    // Goal cleared
    elseif state == 1 then

        call AdvDebugDisplayToPlayer("AI Info: attack wave goal cleared!")

        set goalCleared = true
    
    endif
    // --- GROUP DEAD ---
    if CaptainIsEmpty() then

        call AdvDebugDisplayToPlayer("AI Info: attack wave dead, going back home...")
    
        call SetCaptainHome(ATTACK_CAPTAIN, startX,  startY)
        call CaptainAttack(startX, startY)
    
        if not suicide then
            call Sleep(1)
        endif
    
        call TeleportCaptain(startX, startY)
    
        // Wait for the captain to go back home
    
        loop
    
            call Sleep(1)
        
            exitwhen suicide
            exitwhen CaptainAtGoal()
    
        endloop
    
        call AdvDebugDisplayToPlayer("AI Info: returned home!")
    
        call ClearCaptainTargets()
        call ResetCaptainLocs()
        call AdvSetCaptainHome(BOTH_CAPTAINS)
    
        if not suicide then
            call Sleep(2)
        endif
    
        return
    
    endif

    // --- ATTACK FINISHED CONTINUE (IF REQUIRED) ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave finished duty, checking if  should continue...")

    // If attack wave has not fought before reaching goal and then never fought again, it must continue
    if goalReached and not combatEnteredBeforeGoal and not combatEntered then

        call AdvDebugDisplayToPlayer("AI Info: attack wave never fought at goal nor to reach it, forcing continue...")

        set continuePercentage = 100
        set continueAttackReducePercentageIfFarAway = false
    endif
    if not suicide and not CaptainIsEmpty() and GetRandomInt(0, 100) <= continuePercentage then
        call AdvDebugDisplayToPlayer("AI Info: attack wave continues to another target!")
    
        set newTargetFound = AdvSuicideWaveLaunch(targetPlayer, 0.0, 0.0, false, true, can_search_units, false, true, true)
    
        if newTargetFound then
            // If target is not close to previous one, decrease the continue percentage (if required)
            if continueAttackReducePercentageIfFarAway and AdvDistance(previousTargetX, previousTargetY, last_target_x, last_target_y) > continue_far_away_radius then
                call AdvDebugDisplayToPlayer("AI Info: reducing percentage of continuing again...")
            
                set continuePercentageNew = continuePercentageNew - continue_attack_reduce_percentage
            endif
        
            call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continuePercentageNew)
        
            return
        endif
    
    endif

    // --- ATTACK FINISHED GO HOME ---
    call AdvDebugDisplayToPlayer("AI Info: attack wave does not continue, going back home...")

    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)

    // Wait for the captain to go back home
    loop

        call Sleep(1)
    
        exitwhen suicide
        exitwhen CaptainAtGoal()

    endloop

    call AdvDebugDisplayToPlayer("AI Info: returned home!")

    call ClearCaptainTargets()
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)

    if not suicide then
        call Sleep(2)
    endif

endfunction
//============================================================================
// ADVANCED SUICIDE ON PLAYER
//============================================================================
function AdvSuicideOnPlayer takes integer seconds, player targetPlayer, real startX, real startY returns nothing
    local integer woodPeons
    local boolean targetFound = false

    call AdvDebugDisplayToPlayer("AI Info: waiting to attack...")
    
    // Make sure the captain are at right spots
    call ResetCaptainLocs()
    call AdvSetCaptainHome(BOTH_CAPTAINS)

    if not suicide then
        call Sleep(2)
    endif
    // Prepare attack for given seconds
    if not PrepSuicideOnPlayer(seconds) then
        return
    endif

    call AdvDebugDisplayToPlayer("AI Info: wait is over!")
    // If necessary (e.g. undead race), save the wood peons before attack group is prepared
    if wood_peons_warriors then
        set woodPeons = campaign_wood_peons
        set campaign_wood_peons = 0
    endif

    call AdvDebugDisplayToPlayer("AI Info: forming group...")
    loop

        exitwhen suicide

        call FormGroup(5, true)
        exitwhen sleep_seconds <= 0
    
    endloop

    call AdvDebugDisplayToPlayer("AI Info: group formed!")

    // If necessary (e.g. undead race), reset the wood peons after attack group is ready
    if wood_peons_warriors then
        set campaign_wood_peons = woodPeons
    endif

    // Travel to start position
    call AdvDebugDisplayToPlayer("AI Info: traveling to start position...")

    call SetCaptainHome(ATTACK_CAPTAIN, startX, startY)
    call CaptainAttack(startX, startY)

    loop

        call Sleep(1)

        exitwhen suicide
        exitwhen CaptainAtGoal()
        exitwhen CaptainIsEmpty()

    endloop

    // Stop here if captain is empty
    if CaptainIsEmpty() then
        return
    endif

    call AdvDebugDisplayToPlayer("AI Info: start position reached!")

    // Launch the wave against a suitable target
    set targetFound = AdvSuicideWaveLaunch(targetPlayer, startX, startY, search_preferred_locations, search_everywhere, can_search_units, prioritize_town_halls, prioritize_nearest, false)
    
    set harass_length = 0
    // Start the wave routine if there is a target
    if targetFound then
        call AdvSuicideWaveRoutine(targetPlayer, startX, startY, reach_goal_timeout, enter_combat_timeout, clear_goal_timeout, continue_attack_percentage)
    endif

endfunction
//============================================================================
function AdvSuicideOnPlayerEx takes integer easy, integer med, integer hard, player targetPlayer, real startX, real startY returns nothing
    if difficulty == EASY then
        call AdvSuicideOnPlayer(easy, targetPlayer, startX, startY)
    elseif difficulty == NORMAL then
        call AdvSuicideOnPlayer(med, targetPlayer, startX, startY)
    else
        call AdvSuicideOnPlayer(hard, targetPlayer, startX, startY)
    endif

endfunction

The key here is the addition of these functions, which I hate. There has to be a better way to write these:
JASS:
// Adds all units in g to h. Substitute for BlzGroupAddGroupFast
function AdvGroupAddGroup takes group g, group h returns nothing
    local unit u
    local group g2
    set g2 = CreateGroup()
    call AdvDebugDisplayToPlayer("AI Info: trying to add group!")
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupAddUnit(h, u)
        call GroupRemoveUnit(g, u)
        call GroupAddUnit(g2, u)
    endloop
    loop
        set u = FirstOfGroup(g2)
        exitwhen u == null
        call GroupAddUnit(g, u)
        call GroupRemoveUnit(g2, u)
    endloop
    call DestroyGroup(g2)
endfunction

// Counts the units in a group. Substitute for BlzGroupGetSize. No I'm not happy that this is O(n)
function AdvGroupGetSize takes group g returns integer
    local integer size
    local unit u
    local group g2
    set g2 = CreateGroup()
    set size = 0
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        set size = size + 1
        call GroupRemoveUnit(g, u)
        call GroupAddUnit(g2, u)
    endloop
    loop
        set u = FirstOfGroup(g2)
        exitwhen u == null
        call GroupAddUnit(g, u)
        call GroupRemoveUnit(g2, u)
    endloop
    call DestroyGroup(g2)
    return size
endfunction

The problem with BlzIsUnitInvulnerable is that in 1.29 its output is reversed. It returns true for vulnerable units and false for invulnerable units. At some point in the 1.30.x patch cycle it got changed to return true for invulnerable and false for vulnerable. I have empirically verified this result. It's possible to detect game version, so in principle this can be accounted for.
EDIT: Yes, it can.
JASS:
GetLocalizedString("DOWNLOADING_MAP") == "DOWNLOADING_MAP")
returns true if you're on 1.29.x or earlier and false if you're on 1.30.x or higher. We can use that to make a modified BlzIsUnitInvulnerable that's version-resistant.
 
Last edited:
Good afternoon! I created a new thread and added improvements to it, and I can't understand why it doesn't work! There is also a feeling that the condition variables are not updated. Please tell me what I'm doing wrong? Thank you in advance for your help.

Are you using it on Reforged (1.32 or superior)?
 
Level 25
Joined
Feb 2, 2006
Messages
1,687
I am interested in using this although I did not really experience any major issues with campaign AIs in my map except for expansions and custom gold mines. Could you maybe provide functions which allow you to build n buildings in expansion k?

Currently, my AI script has something like this:

JASS:
    call SetBuildExpa(expansions, DALARAN_TIER_3)
    call SetBuildExpa(expansions, DALARAN_BLACKSMITH)
    call SetBuildExpa(expansions, DALARAN_GUARD_TOWER_2)

where expansions is the number of expansions. From my understanding, I can only specify how many expansions are created but I cannot specify where nor how many buildings/units per expansion, so I have to copy the line multiple times?

JASS:
// 3 towers per expansion:
call SetBuildExpa(expansions, DALARAN_GUARD_TOWER_2)
call SetBuildExpa(expansions, DALARAN_GUARD_TOWER_2)
call SetBuildExpa(expansions, DALARAN_GUARD_TOWER_2)

I haven't really tested this but from reading the code the first parameter only defines the index of the expansion and in this case I would prefer something like this:

JASS:
call AdvSetBuildExpa(expansions, 3, DALARAN_GUARD_TOWER_2)

It would also be nice to set the mine where the expansion takes place but I am not sure if this is actually possible.
Is there any campaign AI from Blizzard which features at least one expansion as reference?

For all of my custom races I had to heavily modify TownCountEx for all the different new tier 1-3 and tower buildings to prevent the AI from building too many town halls and towers:

JASS:
    elseif unitid == DALARAN_TIER_1 then
        set have_qty = have_qty + GetUnitCountEx(DALARAN_TIER_2,false,townid) + GetUnitCountEx(DALARAN_TIER_3,false,townid)
    elseif unitid == DALARAN_TIER_2 then
        set have_qty = have_qty  + GetUnitCountEx(DALARAN_TIER_3,false,townid)
    elseif unitid == DALARAN_GUARD_TOWER_1 then
        set have_qty = have_qty  + GetUnitCountEx(DALARAN_GUARD_TOWER_2,false,townid)

You could provide a function like:

JASS:
function AdvBuildingTiers takes integer tier1, integer tier2, integer tier3

which then can be used somewhere in the beginning of the AI script instead of the common.ai like:

JASS:
call AdvBuildingTiers(DALARAN_TIER_1, DALARAN_TIER_2, DALARAN_TIER_3)
call AdvBuildingTiers(DALARAN_GUARD_TOWER_1, DALARAN_GUARD_TOWER_2, 0)

and will do the magic automatically. Maybe with more parameters to support more tiers.

Another big issue is with custom Haunted and Entangled Goldmines. AI does not always use them properly, so I have to load them manually with triggers every x seconds. Maybe there could be some useful solution here but I have no idea if it is hardcoded or not.
 
Level 24
Joined
Feb 19, 2011
Messages
656
Maybe I'm way out of my field trying to understand all this. I just want a simple attack wave to not retreat back after destroying a single building in its way to attack a player base. Read through the tutorial and used your other AIs as examples. But just not understanding.


JASS:
globals
    player MyVictim = PlayerEx(1)
    
    
    constant real ATTACK_WAVE_START_X = -4740
    constant real ATTACK_WAVE_START_Y = 5017
    
    
endglobals

//--------------------------------------------------------------------------------------------------
//  main
//--------------------------------------------------------------------------------------------------
function main takes nothing returns nothing
    call CampaignAI('uaco',null)
    call SetReplacements(3,3,5)
    call DoCampaignFarms(false)
    call SetPeonsRepair(true)
    
    local boolean AttackWaveAttackUser = AttackUser
    
    call SetCaptainHome(ATTACK_CAPTAIN, -4740, 5017)
    call SetCaptainHome(DEFENSE_CAPTAIN, -4169, 3374)

         // Tier 1 Buildings
        call SetBuildUnit( 1, 'unpl' )          //Necropolis
        call SetBuildUnit( 5, 'uaco' )         // Acolye
        call SetBuildUnit( 1, 'ugol' )         // Haunted Gold Mine
        call SetBuildUnitEx( 2, 2, 2, 'usep' ) // Crypt  
        call SetBuildUnit( 4, 'ugho' )         // Ghoul
        call SetBuildUnit( 1, 'uaod' )         // Altar of Darkness
        call CampaignDefenderEx( 1, 1, 1, 'U01F' )     //Celedaen
        call CampaignDefenderEx( 2, 3, 4, 'unec' )     //Necro
        call CampaignDefenderEx( 2, 2, 3, 'uabo' )     //Abomination
        call CampaignDefenderEx( 1, 2, 3, 'umtw' )     //Meat Wagons 
        call CampaignDefenderEx( 1, 2, 4, 'ugar' )     //Gargoyle
        call SetBuildUnit( 1, 'ugrv' )         // Graveyard
        call SetBuildUnit( 1, 'utom' )         // Tomb of Relics
        call SetBuildUnitEx( 6, 6, 6, 'uzig' )  // Ziggurat
        call SetBuildUnitEx( 6, 6, 6, 'uzg1' )  // Spirit Tower
        call SetBuildUnit( 1, 'unp1' )          //Halls of the Dead
        call SetBuildUnit( 1, 'usap' )         // Sacrificial Pit
        call SetBuildUnit( 1, 'uslh' )         // Slaughterhouse
        call SetBuildUnit( 2, 'utod' )         // Temple of the Damned 
        call SetBuildUnit( 1, 'unp2' )          //Black Citadel
        call SetBuildUnit( 1, 'ubon' )         // Boneyard
        // Tier 2 buildings
 // **********************************
        // *    End Building Strategy       *
        // **********************************
        // **********************************
        // *       Attack Strategy          *
        // **********************************
        //*** WAVE 1 *** AI will begin to attack in 10 minutes
        loop
        call InitAssaultGroup()
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen      
        call CampaignAttackerEx( 2, 3, 4, 'unec' ) //Necro
        call AdvSuicideOnPlayerEx( M7, M6, M5, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide
        //*** WAVE 1 *** AI will begin to attack in 10 minutes
        call InitAssaultGroup()                                                    
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen
        call CampaignAttackerEx( 2, 2, 3, 'unec' ) //Necro
        call CampaignAttackerEx( 2, 3, 5, 'ugho' ) //Ghoul
        call SAdvSuicideOnPlayerEx( M6, M5, M4, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide
        //*** WAVE 2 *** Between 2 or 3 minutes after Wave 1
        call InitAssaultGroup()
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen
        call CampaignAttackerEx( 2, 3, 4, 'ugho' ) //Ghoul
        call CampaignAttackerEx( 1, 1, 2, 'uabo' ) //Abomination
        call AdvSuicideOnPlayerEx( M7, M6, M5, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y) 
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide         //12 mins ingame
        //*** Upgrades 1 ***                                                                        
        call SetBuildUpgrEx( 1, 1, 1, 'Rune' )  // Necro Training
        call SetBuildUpgrEx( 1, 1, 1, 'Ruba' )  // Banshee Training
        //*** WAVE 3 *** Between 2 or 3 minutes after Wave 2
        call InitAssaultGroup()
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen
        call CampaignAttackerEx( 1, 2, 3, 'uabo' ) //Abomination
        call CampaignAttackerEx( 2, 2, 3, 'unec' ) //Necro
        call AdvSuicideOnPlayerEx( M6, M5, M4, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)  
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide        //12 mins ingame
        //*** WAVE 1 *** AI will begin to attack in 10 minutes
        call SetBuildUpgrEx( 2, 2, 2, 'Rura' )  // Creature Attack Upg
        call SetBuildUpgrEx( 2, 2, 2, 'Rume' )  // Unholy Strength Upg
        call SetBuildUpgrEx( 2, 2, 2, 'Rucr' )  // Creature Armor
        call SetBuildUpgrEx( 2, 2, 2, 'Ruar' )  // Unholy Armor
        call SetBuildUpgrEx( 1, 1, 1, 'Rugf' )  // Ghoul Frenzy
        //*** WAVE 4 *** Between 2 or 3 minutes after Wave 3
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen
        call CampaignAttackerEx( 1, 2, 3, 'uabo' ) //Abomination
        call CampaignAttackerEx( 2, 2, 3, 'umtw' ) //Meatwagon
        call AdvSuicideOnPlayerEx( M7, M6, M5, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)  
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide       
        //*** WAVE 4 *** Between 2 or 3 minutes after Wave 3
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen
        call CampaignAttackerEx( 1, 2, 3, 'uabo' ) //Abomination
        call CampaignAttackerEx( 2, 2, 4, 'unec' ) //Necro
        call CampaignAttackerEx( 2, 3, 4, 'ugho' ) //Ghoul
        call AdvSuicideOnPlayerEx( M7, M6, M5, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide
        //*** Upgrades 1 ***
        call SetBuildUpgrEx( 1, 2, 2, 'Rune' )  // Necro Training
        call SetBuildUpgrEx( 1, 2, 2, 'Ruba' )  // Banshee Training
        //*** WAVE 4 *** Between 2 or 3 minutes after Wave 3
        call CampaignAttackerEx( 1, 1, 1, 'U01F' ) //Celedaen
        call CampaignAttackerEx( 1, 2, 3, 'uabo' ) //Abomination
        call CampaignAttackerEx( 2, 2, 4, 'unec' ) //Necro
        call CampaignAttackerEx( 0, 1, 2, 'ufro' ) //Frost Wyrm
        call AdvSuicideOnPlayerEx( M7, M6, M5, MyVictim, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)   
        exitwhen AttackWaveAttackUser != AttackUser or AttackSuicide != AttackWaveAttackSuicide
              
        endloop
        

endfunction
 
Level 24
Joined
Feb 19, 2011
Messages
656
Ok. Let me try something different.

I used your code from an undead base. I tried to make it as simple as I can for me. I have your script imported and properly named, the player using the ai has plenty of resources and space.

Do you happen to have a very simple template that someone can use to learn from? For the most part I just want to make my ai attack so that they wont retreat back to base after destroying a single structure. At least for now.

I try learning about JassAi, but the pool of people that actually know it and are willing to teach it is very low.

JASS:
globals
    
    // Players

    player User = PlayerEx(1)
    
    // Behaviour
    
    constant real ATTACK_WAVE_START_X = -3600.0
    constant real ATTACK_WAVE_START_Y = -14400.0
        
    boolean AttackUser = false
    

    
endglobals

// BUILD ORDER
//------------------------------------------------
function SetBuildOrder takes nothing returns nothing

        call SetBuildUnit( 1, 'unpl' )          //Necropolis
        call SetBuildUnit( 5, 'uaco' )         // Acolye
        call SetBuildUnit( 1, 'ugol' )         // Haunted Gold Mine
        call SetBuildUnitEx( 2, 2, 2, 'usep' ) // Crypt  
        call SetBuildUnit( 4, 'ugho' )         // Ghoul
        call SetBuildUnitEx( 6, 6, 6, 'uzig' )  // Ziggurat


endfunction

// DEFENDERS
//------------------------------------------------
function SetDefenders takes nothing returns nothing

        call CampaignDefenderEx( 2, 3, 5, 'ugho' )     // Ghoul
        
endfunction

// RESEARCH UPGRADES
//------------------------------------------------
function ResearchUpgrades takes nothing returns nothing

    call Sleep(M12)
    
    call SetBuildUpgrEx(1, 1, 1, UPG_CR_ARMOR)
        
        
endfunction

// ATTACK LOOP
//------------------------------------------------
function AttackWavesAttackUser takes nothing returns nothing
    local boolean AttackWaveAttackUser = AttackUser
    
    call AdvDebugDisplayToPlayer("AI Info: attacking User...")
    
    loop 
        
        // *** WAVE 1 ***
        call InitAssaultGroup()
        call CampaignAttackerEx(2, 3, 4, 'ugho')
        call AdvSuicideOnPlayerEx(M2, M2, M1, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackUser != AttackUser
        
        
        loop
            
            // *** WAVE 4 ***
            call InitAssaultGroup()
        call CampaignAttackerEx(2, 3, 4, 'ugho')
        call AdvSuicideOnPlayerEx(M2, M2, M1, User, ATTACK_WAVE_START_X, ATTACK_WAVE_START_Y)
        exitwhen AttackWaveAttackUser != AttackUser
                

        endloop
        
        // Make sure to leave both loops
        exitwhen AttackWaveAttackUser != AttackUser
        
    endloop
    
    call AdvDebugDisplayToPlayer("AI Info: finished attacking User!")
    
endfunction

function AttackLoop takes nothing returns nothing

    loop
        
        // If attack are set, execute the requested attack loop    
        if AttackUser then
            call AttackWavesAttackUser()    
        endif
           
        // Wait a little time before looping again
        call Sleep(10)
    
    endloop

endfunction



// MAIN 
//------------------------------------------------
function main takes nothing returns nothing

    call CampaignAI(ZIGGURAT_1, null)
    
    // Define AI properties    
    call DoCampaignFarms(false)
    call GroupTimedLife(true)
    call SetReplacements(1, 1, 2)
    call AdvSetWoodPeonsWarriors(true)
        
    call AdvSetContinueAttackReducePercentageIfFarAway(false)
    call AdvSetContinueAttackPercentage(100)
    call AdvSetPrioritizeTownHalls(true)
    call AdvSetPrioritizeNearest(true)
    
    // Define build order
    call SetBuildOrder()
    
    // Start to research upgrades at timed intervals
    call StartThread(function ResearchUpgrades)
    
    // Define defenders
    call SetDefenders()

    
    // Launch attack waves as soon as possible (it depends on the commands received)
    call AttackLoop()
        
endfunction
 
Level 8
Joined
May 12, 2018
Messages
106
Hi IM! I'm using this code tool you provided very well. But I think there are some problems.

Functions such as AdvAssignTargetableTownHalls use local group variables, but DestroyGroup was not used at the end. Does AI script not have this leakage problem even if without using DestroyGroup?

Also, if CPU that running AdvSuicideOnPlayer function is eliminated, it seems to crash unconditionally.
As soon as the AI player is eliminated, it must receive the AI Command/Data signals that the AdvSuicideOnPlayer function immediately stops running. Additional explanations regarding this seem to be needed.
 
Level 39
Joined
Feb 27, 2007
Messages
4,994
Functions such as AdvAssignTargetableTownHalls use local group variables, but DestroyGroup was not used at the end. Does AI script not have this leakage problem even if without using DestroyGroup?
There appear to be many such functions that leak objects. As far as I understand these are mistakes within the script, not something that JASS AI gets to ignore.
 
Top