- Joined
- Jun 15, 2016
- Messages
- 472
Or How I Learned to Stop Worrying and Read AI
This tutorial serves one primary purpose: teaching you how to learn ai by yourself.
In order to achieve that we'll go through two steps:
First, we'll read through an ai script from one of blizzard's campaign maps and use it to understand how ai works. We will then use our lessons in order to create an ai script which upgrades a tier when prompted.
What you need for this tutorial:
- A basic understanding of JASS/other programming language. Nothing too major, but you should know what a function is, global and local variables, looping, etcetera...
- A JASS syntax highlighter able to highlight and show native ai functions. I use jasscraft, but whatever floats your boat is fine. You can also keep this page, which has a list of all ai functions, open for reference.
- Some ai files to read (see attached files at the bottom of the post).
- Reading moyack's tutorial about making a campaign ai is not a must, but it is a good run through the basic functions, and will definitely help you here.
- Patience: this tutorial will eventually give you a functioning human ai, but it's worthless without understanding how it works.
A short introduction to AI:
In warcraft 3, ai is a piece of code which manages the aspect of player actions. The ai is in fact an external script that can be created in any normal text editor (I suggested jasscraft because it has highlights and it shows the inner workings of some of blizzard's functions, but any notepad will do). The script is then imported to the map and assigned to a computer player with triggers.
Why is ai good:
- It doesn't rely on the map to provide it with information (at least not for basic actions). If you want to take an existing ai for your map, you just need to make sure it's an ai of the same race, specify which player to attack, and you're good to go.
- It has a lot of built-in functions that really simplify the process of player simulation.
- It does not use a lot of leaking variables. The ai works primarily with integers, such as unit type representation instead of units, X/Y coordinates instead of locations etc.
Why is ai bad:
- The ai is not specific. If you want the ai to order a specific unit, or build something at a specific location, doing it will be very clumsy.
- AI has very little documentation, meaning if you want to create something special, you'll probably need to make it yourself via testing, trial & error, and (god forbid) some creative thinking.
- The ai is difficult to debug. So far I've encountered 4 kinds of "crashes":
- A real crash when loading the map (i.e. the game quits when loading and displays an error message). This usually happens because of a glaring syntax error, like starting an
if
clause without anendif
, or something else. The other possibility is creating a loop which runs without stopping at all (we will get to that later).
- The ai does nothing: can be due to one of many errors such as referencing to a non-existent variable or writing a function without
call
before it.
- The ai doesn't do a specific task: could be a problem with the order of called functions. Check what the ai was supposed to do right after the last thing it did.
- Miscellaneous other bugs and quirks, one of which we will see during this tutorial.
- A real crash when loading the map (i.e. the game quits when loading and displays an error message). This usually happens because of a glaring syntax error, like starting an
Unfortunately for us, we will mostly encounter the second "crash" (at least I did so far), which will require going over the entire script and search for tiny flaws.
All of the problems listed above now bring me to the golden rule of ai: Tread carefully! when writing ai, start small. Write a little piece of code, see if it works, rinse, repeat. When writing an ai with a lot of functionalities, it is even preferred to work with 2 versions just in case you mess something up. Every time you reach a point where all of your code works, save in a different file and keep going.
So, after a long introduction, let's read some ai!
Campaign ai basics
For our example we will use the ai for both yellow and purple chaos orc players from the fifth blood-elf mission “Gates of the abyss”. Grab it from the attachment below and take a look.
So that's a campaign ai. It's very long, but don't worry, we'll break it up into three parts: Initialization, Base building and Attack strategy.
JASS:
call CampaignAI(CHAOS_BURROW,null)
call SetReplacements(3,3,5)
call DoCampaignFarms(false)
JASS:
call SetBuildUnitEx ( 0,0,1, GREAT_HALL )
call SetBuildUnit ( 1, CHAOS_PEON )
call SetBuildUnitEx ( 0,0,2, ORC_BARRACKS )
call SetBuildUnitEx ( 0,0,5, CHAOS_BURROW )
call SetBuildUnitEx ( 0,0,1, FORGE )
call SetBuildUnit ( 8, CHAOS_PEON )
call SetBuildUnitEx ( 0,0,1, STRONGHOLD )
call SetBuildUnitEx ( 0,0,2, BESTIARY )
call SetBuildUnitEx ( 0,0,2, LODGE )
call CampaignDefenderEx( 0,0,2, CHAOS_GRUNT )
call CampaignDefenderEx( 0,0,2, CHAOS_WARLOCK )
call CampaignDefenderEx( 1,1,1, CHAOS_RAIDER )
call CampaignDefenderEx( 1,1,1, CHAOS_GROM )
call CampaignDefenderEx( 1,1,3, ORC_DRAGON )
call SetBuildUpgrEx( 1,1,1, UPG_ORC_BURROWS )
call SetBuildUpgrEx( 0,0,1, UPG_ORC_ARMOR )
call SetBuildUpgrEx( 0,0,1, UPG_ORC_MELEE )
call SetBuildUpgrEx( 1,1,1, UPG_ORC_ENSNARE )
call SetBuildUpgrEx( 3,3,3, UPG_ORC_SPIKES )
JASS:
call WaitForSignal()
//*** WAVE 1 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,1, ORC_DRAGON )
call SuicideOnPlayer(M5,user)
//*** WAVE 2 ***
call InitAssaultGroup()
call CampaignAttackerEx( 3,3,5, CHAOS_GRUNT )
call CampaignAttackerEx( 1,1,1, CHAOS_GROM )
call SuicideOnPlayerEx(M6,M6,M5,user)
//*** WAVE 3 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,2, ORC_DRAGON )
call SuicideOnPlayerEx(M7,M7,M6,user)
call SetBuildUpgrEx( 0,0,1, UPG_ORC_BERSERK )
//*** WAVE 4 ***
call InitAssaultGroup()
call CampaignAttackerEx( 7,7,9, CHAOS_RAIDER )
call SuicideOnPlayerEx(M6,M6,M5,user)
call SetBuildUpgrEx( 0,0,1, UPG_ORC_WAR_DRUMS )
//*** WAVE 5 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,2, ORC_DRAGON )
call SuicideOnPlayerEx(M7,M7,M6,user)
call SetBuildUpgrEx( 1,1,2, UPG_ORC_ARMOR )
call SetBuildUpgrEx( 1,1,2, UPG_ORC_MELEE )
//*** WAVE 6 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,1, CHAOS_GROM )
call CampaignAttackerEx( 3,3,4, CHAOS_WARLOCK )
call SuicideOnPlayerEx(M6,M6,M5,user)
//*** WAVE 7 ***
call InitAssaultGroup()
call CampaignAttackerEx( 5,5,7, CHAOS_RAIDER )
call CampaignAttackerEx( 1,1,1, CHAOS_KODO )
call SuicideOnPlayerEx(M7,M7,M6,user)
call SetBuildUpgrEx( 1,1,1, UPG_ORC_WAR_DRUMS )
//*** WAVE 8 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,2, ORC_DRAGON )
call CampaignAttackerEx( 2,2,3, CHAOS_GRUNT )
call SuicideOnPlayerEx(M6,M6,M5,user)
//*** WAVE 9 ***
call InitAssaultGroup()
call CampaignAttackerEx( 3,3,5, CHAOS_GRUNT )
call CampaignAttackerEx( 1,1,2, CHAOS_KODO )
call SuicideOnPlayerEx(M7,M7,M6,user)
call SetBuildUpgrEx( 1,1,3, UPG_ORC_ARMOR )
call SetBuildUpgrEx( 1,1,3, UPG_ORC_MELEE )
//*** WAVE 10 ***
call InitAssaultGroup()
call CampaignAttackerEx( 5,5,7, CHAOS_RAIDER )
call CampaignAttackerEx( 1,1,1, CHAOS_GROM )
call SuicideOnPlayerEx(M6,M6,M5,user)
loop
//*** WAVE 11 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,2, ORC_DRAGON )
call CampaignAttackerEx( 2,2,3, CHAOS_GRUNT )
call SuicideOnPlayerEx(M7,M7,M6,user)
//*** WAVE 12 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,1, CHAOS_GROM )
call CampaignAttackerEx( 3,3,4, CHAOS_WARLOCK )
call SuicideOnPlayerEx(M6,M6,M5,user)
//*** WAVE 13 ***
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,2, ORC_DRAGON )
call CampaignAttackerEx( 2,2,3, CHAOS_RAIDER )
call SuicideOnPlayerEx(M7,M7,M6,user)
//*** WAVE 14+ ***
call InitAssaultGroup()
call CampaignAttackerEx( 5,5,7, CHAOS_RAIDER )
call CampaignAttackerEx( 1,1,1, CHAOS_GROM )
call SuicideOnPlayerEx(M6,M6,M5,user)
endloop
Ignore the attack strategy and defenders, we'll deal with those in time. At the moment focus yourself on the two functions
SetBuildUnitEx
(in Base building) and CampaignAI
(in Initialization).Look at the structure building function
SetBuildUnitEx(e, m, h, unitid)
. As you'll see, this function actually calls the function SetBuildAll(BUILD_UNIT, level, unitid, TownNumber)
with different parameters for every difficulty:
JASS:
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 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
Well... That's it? Is that the building part? Doesn't look like it, all I see is variables being assigned. Also, you'll notice that only in hard difficulty the ai orders to build structures with more then zero quantity. Now, if you'll look closely at
SetBuildAll()
, you'll see that if the quantity given is less than 1, the ai doesn't even assign any variable.So, where is the building part?
Here we finally reach our first lesson: the ai has a composite workflow. And what that means is that the ai does more than one thing at a time. If you know JASS, or even if you have a solid understanding of triggers, you'll know that functions (or actions) are done one at a time, only really really fast. If you want to make two things happen simultaneously, you'll call two triggers at the same time. But in ai you can't do that cause everything is just one big trigger. So what can you do?
For the answer, we'll look again at the starting point of the ai:
CampaignAI()
, and see what it does.
JASS:
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
You'll notice that at the end of the function, there are two very interesting lines:
JASS:
call StartThread(function CampaignBasics)
call StartBuildLoop()
And inside StartBuildLoop() there's this:
JASS:
function StartBuildLoop takes nothing returns nothing
call StartThread(function BuildLoop)
endfunction
This is where the magic happens. StartThread takes a function as an argument, and starts it simultaneously with the other functions. This might not be immediately clear, but bear with me. Let's look at the functions called by StartThread:
BuildLoop()
& CampaignBasics()
.
JASS:
function BuildLoop takes nothing returns nothing
call OneBuildLoop()
call StaggerSleep(1,2)
loop
call OneBuildLoop()
call Sleep(2)
endloop
endfunction
JASS:
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
Both functions are loops which call the same function every couple of seconds (With the Sleep function being the waiting time). Nevermind what they do now, it's how they do it: by calling a function constantly, checking and updating its parameters.
Imagine if all of those functions were in the same thread. The first one will be called, and that's it. It's an infinite loop! the ai will stay there and not move to the other functions. The ai enables a sort of branching pattern which constantly checks and updates conditions.
Consider the following chart (each square is the start of a thread, and each circle is an infinite loop):
“But what does that help?” you may ask. Let's wait with that question for a bit, because with our newfound knowledge, we can answer how the ai builds things. For that, let's look at the function called by BuildLoop:
OneBuildLoop()
.
JASS:
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
Now we're getting somewhere! This function takes the parameters you've set in
SetBuildUnitEx()
, and checks if they can be built, be it unit upgrade or expension. But this isn't it yet, let's go just one level deeper – into StartUnit()
:
JASS:
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
And there you have it! A detailed, well explained function by Blizzard itself, describing the internal process done in order to determine if and when to build a unit. And if you're still skeptical, let's try seeing what the function
SetProduce()
does: native SetProduce takes integer qty, integer id, integer town returns boolean
.Nothing. No detailed function, no checked conditions, only an explicit order for the game to build your unit. It was a long road, but damn worth it.
Now we can answer what good are the different threads: if all building functions were on the same thread, the ai would pause entirely every time it will build something. With multiple threads the ai can store your building priorities for later, and goes over it “somewhere else” so it does not interrupt the rest of the ai.
Recap: The ai takes a set of integer arrays which store the building order (indexed by the variable Build_Length), goes over it periodically, and builds whichever unit is not yet built. That is why the ai only rebuilds in hard mode - in hard mode the ai loads the building priorities to its internal build array, then builds everything which is not already built, i.e. everything Keal and Lady Vashj destroy in the orc base. In easy/normal mode, the building priorities are all 0, and the ai rebuilds nothing.
Building your own AI
Now that we're done with theory, let's move on to the practice. Before you start hacking away at your ai, you need somewhere to test it. a sandbox - if you will.For that, take some melee map of your choosing, copy it and do the following:
- Make sure your ai player is assigned to the ai race.
- Give your test subject a lot of resources (over 50,000 of both, don't be cheap).
- Make the whole map visible.
- Give yourself some OP units and be prepared to use cheat-codes, because testing the ai by playing a normal game will take you a about three times as long.
- Delete the melee initialization trigger (or just the "Melee Game - Run melee AI scripts (for computer players)" action). Instead you'll import your own ai file via the import manager (remember, for that to work you'll need your script to have .ai extension) and set it up to the computer player with this action: "AI - Start campaign AI script for someplayer: yourscript.ai".
-
Melee Initialization
-
Events
- Map initialization
- Conditions
-
Actions
- Melee Game - Use melee time of day (for all players)
- Melee Game - Limit Heroes to 1 per Hero-type (for all players)
- Melee Game - Set starting resources (for all players)
- Melee Game - Remove creeps and critters from used start locations (for all players)
- Melee Game - Create starting units (for all players)
- AI - Start campaign AI script for Player 2 (Blue): war3mapImported\Gold Digger.ai
- Player - Set Player 2 (Blue) Current gold to 75000
- Player - Set Player 2 (Blue) Current lumber to 75000
- Visibility - Create an initially Enabled visibility modifier for Player 1 (Red) emitting Visibility across (Playable map area)
-
Events
What's Init
We'll start the our AI script the same as the example we were reading: a global declaration and and the initialization part. The global declaration simple enough - you set the target player, as well as a variable integer named "Tier", which will be initially set to 1.The initialization is composed of 3 functions: CampaignAI(), SetReplacements(), DoCampaignFarms(). We're already familiar with the CampaignAI() function, which starts our building and campaign basic functions. But before it does that, CampaignAI() determines the AI difficulty, then sets its attributes accordingly (peons repair buildings and injured units flee from damage only on hard difficulty for instance), other characteristics include:
- SetSlowChopping(true) - and causes the ai to harvest 1 gold and 1 lumber each time.
- SetHeroesFlee(false) - if true heroes will flee.
- SetGroupsFlee(false) - if true unit groups will flee.
- GroupTimedLife(false) - if this function is set to true summoned units will join the summoners group. In the blood elf ai script of the last undead mission, this function is set to true - meaning that if Keal summons a phoenix during an attack, it will stay with him instead of just standing there attacking anyone in its aggro-range.
- InitAI() and SetCampaignAI() - these two functions state the start of ai actions and declere that it is a campaign ai. These are just init natives that must be called at the beginning of the ai, not much more to say about them.
- CreateCaptains() -This is another init function which must be called at the start of the ai, and we will discuss what it does later in this tutorial.
- If a hero was assigned to the ai, it will save its leveling function in SetHeroLevels(heroes). We will not discuss that function in this tutorial, but you can go to any melee ai and see how it is done.
- Setting the ai's farm building according to the argument assigned to CampaignAI (line set racial_farm = farms). We'll see why that's important in a second.
JASS:
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
the first condition "do_campaign_farms" is actually determined in the third init function - DoCampaignFarms(false). In other words, the ai does not allow building extra farms in this case.
* It is worth noting that apparently CampaignBasicsA() does not consider the max food cap when its building farms, making the ai build another farm before stopping (i.e. according to the ai the food cap is 110, so if you're changing the gameplay constants of food cap the ai has got you covered).
The last part of initializing the ai is SetReplacements() - which determines how many times preplaced units will be produced after being killed. We will not use that in our ai, but this can be very helpful when creating ai for missions, so keep that in mind.
JASS:
globals
player user = Player(0)
integer Tier = 1
endglobals
function main takes nothing returns nothing
call CampaignAI('hhou',null)
call DoCampaignFarms(false)
endfunction
Moving along, we'll write our building priorities. For that, we'll use the function SetBuildUnitEx(e,m,h,u) that we already looked into earlier. The function takes the number of built units for each level - easy, medium, hard, and the built unit.
The units types in the editor are written with 4 letter representations like this - peasant = 'hpea', footman = 'hfoo' and so on, to see all unit type representations go to the object editor and press Ctrl+d. Another option is to set your unit representation to a variable at the beginning of the script. Blizzard did that in the common ai file, so peasent = PEASENT, footman =FOOTMAN/FOOTMEN and a red chaos grunt = CHAOS_GRUNT (as you can see in the script of "Gates of the Abyss").
Last thing, the amount arguments of this function account for the total amount of units which should be built.
So, if you want to build a peasant but you already have 5 from the start of the game you'll need to write
SetBuildUnitEx(6,6,6, 'hpea')
or SetBuildUnitEx(6,6,6, PEASENT)
.I trust you to create your own build order so that it will suit the human techtree dependencies (if you won't the ai will just stop where it can't build the next structure), just put it in a function of its own and condition whatever is in a higher tier to concur with our Tier variable, as in upgrading to a keep and all tier 2 structures will be inside an
if Tier > 1 then
clause. This is what my function looks like at this point:
JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
player user = Player(0)
integer Tier = 1
endglobals
//BUILD PRIORITIES
//------------------------------------------------
function BuildOrder takes nothing returns nothing
call SetBuildUnitEx( 1,1,1, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'htow') // town hall
call SetBuildUnitEx( 5,5,5, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'hbar') // barracks
call SetBuildUnitEx( 2,2,2, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'halt') // hero altar
call SetBuildUnitEx( 8,8,8, 'hpea') // peasent
call SetBuildUnitEx( 5,5,5, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hlum') // lumber mill
call SetBuildUnitEx( 1,1,1, 'hbla') // blacksmith
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'hkee') // Keep upgrade
call SetBuildUnitEx( 1,1,1, 'hars') // arcane sanctum
endif
call SetBuildUnitEx( 1,1,1, 'hvlt') // item shop
call SetBuildUnitEx( 6,6,6, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hwtw') // watch tower
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'harm') // workshop
call SetBuildUnitEx( 11,11,11, 'hhou') // farm
call SetBuildUnitEx( 3,3,3, 'hwtw') // watch tower
endif
if Tier > 2 then
call SetBuildUnitEx( 1,1,1, 'hcas') // Castle upgrade
call SetBuildUnitEx( 1,1,1, 'hatw') // arcane tower
call SetBuildUnitEx( 1,1,1, 'hgra') // gryphon aviery
call SetBuildUnitEx( 1,1,1, 'hgtw') // guard tower
call SetBuildUnitEx( 1,1,1, 'hctw') // canon tower
call SetBuildUnitEx( 14,14,14, 'hhou') // farm
endif
endfunction
//MAIN FUNCTION
//------------------------------------------------
function main takes nothing returns nothing
call CampaignAI('hhou',null)
call DoCampaignFarms(false)
call BuildOrder()
endfunction
Commands
AI commands are triggers which send info to the ai, and are used to make the ai more reactive. Each ai command can send two integers to the ai script: command integer and data integer. Go to your test map's trigger editor and add the following trigger:-
Command
-
Events
- Player - Player 1 (Red) types a chat message containing -COM as An exact match
- Conditions
-
Actions
- AI - Send Player 2 (Blue) the AI Command (CommandInteger, DataInteger)
-
Events
You've already seen one of the ways the ai receives a command like that:
WaitForSignal()
in the gates of the abyss ai we're using in this tutorial, it's called right before the attack waves start. Opening up WaitForSignal() will reveal that it is in fact an infinite loop, which exits only what a different function called CommandsWaiting()
returns a value greater than 0.
JASS:
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
Don't trouble yourself with the display variable and Trace command, these are probably remnants of the game developers' version which displayed a lot of the game's internal works. what you should care about is what this function actually does: it jams the ai script in an infinite loop until it receives an order. A lot of blizzard's campaign ais use
WaitForSignal()
in order to wait with the attacking part of the ai until they send it a command. Most of the times the command comes when the starting cinematic ends, in "gates of the abyss" the command waits until Illidan closes the first of four portals.However that's not the end of WaitForSignal(). It does two very important things:
- At the start of the function it sets a local integer cmd. Then, when a command is received and breaks the loop, it will assign the commandInteger (that's the first integer you can specify in the command trigger, remember?) to the cmd local variable and returns it.
- At the end of the function it calls the
PopLastCommand()
function, which deletes the last command. That part is mandatory if you want to use more than one command (which you need for this tutorial).
-
Tier 2
-
Events
- Player - Player 1 (Red) types a chat message containing -Tier2 as An exact match
- Conditions
-
Actions
- AI - Send Player 2 (Blue) the AI Command (2, 0)
-
Events
-
Tier 2
-
Events
- Player - Player 1 (Red) types a chat message containing -Tier3 as An exact match
- Conditions
-
Actions
- AI - Send Player 2 (Blue) the AI Command (3, 0)
-
Events
InitBuildArray()
and start it again. like this:
JASS:
function TierCondition takes nothing returns nothing
if CommandsWaiting() > 0 then
set Tier = GetLastCommand()
call PopLastCommand()
call InitBuildArray()
call BuildOrder()
endif
endfunction
JASS:
function ConditionLoop takes nothing returns nothing
call TierCondition()
call StaggerSleep(1,5)
loop
call TierCondition()
call Sleep(2)
endloop
endfunction
JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
player user = Player(0)
integer Tier = 1
endglobals
//BUILD PRIORITIES
//------------------------------------------------
function BuildOrder takes nothing returns nothing
call SetBuildUnitEx( 1,1,1, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'htow') // town hall
call SetBuildUnitEx( 5,5,5, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'hbar') // barracks
call SetBuildUnitEx( 2,2,2, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'halt') // hero altar
call SetBuildUnitEx( 8,8,8, 'hpea') // peasent
call SetBuildUnitEx( 5,5,5, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hlum') // lumber mill
call SetBuildUnitEx( 1,1,1, 'hbla') // blacksmith
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'hkee') // Keep upgrade
call SetBuildUnitEx( 1,1,1, 'hars') // arcane sanctum
endif
call SetBuildUnitEx( 1,1,1, 'hvlt') // item shop
call SetBuildUnitEx( 6,6,6, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hwtw') // watch tower
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'harm') // workshop
call SetBuildUnitEx( 11,11,11, 'hhou') // farm
call SetBuildUnitEx( 3,3,3, 'hwtw') // watch tower
endif
if Tier > 2 then
call SetBuildUnitEx( 1,1,1, 'hcas') // Castle upgrade
call SetBuildUnitEx( 1,1,1, 'hatw') // arcane tower
call SetBuildUnitEx( 1,1,1, 'hgra') // gryphon aviery
call SetBuildUnitEx( 1,1,1, 'hgtw') // guard tower
call SetBuildUnitEx( 1,1,1, 'hctw') // canon tower
call SetBuildUnitEx( 14,14,14, 'hhou') // farm
endif
endfunction
//COMMAND FUNCTIONS
//------------------------------------------------
function TierCondition takes nothing returns nothing
if CommandsWaiting() > 0 then
set Tier = GetLastCommand()
call PopLastCommand()
call InitBuildArray()
call BuildOrder()
endif
endfunction
function ConditionLoop takes nothing returns nothing
call TierCondition()
call StaggerSleep(1,5)
loop
call TierCondition()
call Sleep(5)
endloop
endfunction
//MAIN FUNCTION
//------------------------------------------------
function main takes nothing returns nothing
call CampaignAI('hhou',null)
call DoCampaignFarms(false)
call BuildOrder()
call StartThread(function ConditionLoop)
endfunction
Now this is a nice base we've got going there, it'd be a shame if something (like your group of preplaced OP units) will come over and attack this base, wouldn't it? So let's go on to build some defenders for the base.
Base defenders
There are only 2 functions you should know about when creating base defenders:CampaignDefenderEx
and InitDefenseGroup
. The first function stores your defending units in a variable array (like SetBuildUnitEx for buildings), and the second one empties said array (like InitBuildArray for buildings). Pretty simple right? Well there is one catch: defending units are saved in 2 arrays. Observe:
JASS:
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) //<==AN ILLUSION! WHAT ARE YOU HIDING?!
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
SetBuildUnit
hides is that it stores the defending units in the building array, the same array where the structures of your base are stored. In this particular script, we shouldn't care about that, but if you want to change your defenders mid-game without changing your building strategy remember to empty your building list with InitBuildArray as well, otherwise the ai will build the old defending units which will just stand there doing nothing. So we will create a function which will set the defenders according to the tier, and we'll update the function TierCondition which already calls the building function again after changing the Tier variable. Something like this:
JASS:
function CampaignDefenses takes nothing returns nothing
if Tier == 1 then
call CampaignDefenderEx( 2,2,2, 'hfoo' ) // footman
call CampaignDefenderEx( 2,2,2, 'hrif' ) // rifleman
elseif Tier == 2 then
call CampaignDefenderEx( 2,2,2, 'hmpr' ) // priest
call CampaignDefenderEx( 1,1,1, 'hrif' ) // rifleman
call CampaignDefenderEx( 4,4,4, 'hfoo' ) // footman
elseif Tier == 3 then
call CampaignDefenderEx( 2,2,2, 'hgyr' ) // gyrocopter
call CampaignDefenderEx( 3,3,3, 'hkni' ) // knight
call CampaignDefenderEx( 2,2,2, 'hmpr' ) // priest
call CampaignDefenderEx( 1,1,1, 'hsor' ) // sorceress
call CampaignDefenderEx( 1,1,1, 'hgry' ) // gryphon rider
endif
endfunction
//================================================
function TierCondition takes nothing returns nothing
if CommandsWaiting() > 0 then
set Tier = GetLastCommand()
call PopLastCommand()
call InitBuildArray()
call InitDefenseGroup()
call BuildOrder()
call CampaignDefenses()
endif
endfunction
Now, your base will build defending units, and will change them when changing a tier. You'll be able to see the changes when you try to attack your ai base (which I strongly suggest, it's very fun).
JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
player user = Player(0)
integer Tier = 1
endglobals
//BUILD PRIORITIES
//------------------------------------------------
function BuildOrder takes nothing returns nothing
call SetBuildUnitEx( 1,1,1, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'htow') // town hall
call SetBuildUnitEx( 5,5,5, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'hbar') // barracks
call SetBuildUnitEx( 2,2,2, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'halt') // hero altar
call SetBuildUnitEx( 8,8,8, 'hpea') // peasent
call SetBuildUnitEx( 5,5,5, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hlum') // lumber mill
call SetBuildUnitEx( 1,1,1, 'hbla') // blacksmith
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'hkee') // Keep upgrade
call SetBuildUnitEx( 1,1,1, 'hars') // arcane sanctum
endif
call SetBuildUnitEx( 1,1,1, 'hvlt') // item shop
call SetBuildUnitEx( 6,6,6, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hwtw') // watch tower
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'harm') // workshop
call SetBuildUnitEx( 11,11,11, 'hhou') // farm
call SetBuildUnitEx( 3,3,3, 'hwtw') // watch tower
endif
if Tier > 2 then
call SetBuildUnitEx( 1,1,1, 'hcas') // Castle upgrade
call SetBuildUnitEx( 1,1,1, 'hatw') // arcane tower
call SetBuildUnitEx( 1,1,1, 'hgra') // gryphon aviery
call SetBuildUnitEx( 1,1,1, 'hgtw') // guard tower
call SetBuildUnitEx( 1,1,1, 'hctw') // canon tower
call SetBuildUnitEx( 14,14,14, 'hhou') // farm
endif
endfunction
//BASE DEFENSE
//------------------------------------------------
function CampaignDefenses takes nothing returns nothing
if Tier == 1 then
call CampaignDefenderEx( 2,2,2, 'hfoo') // footman
call CampaignDefenderEx( 2,2,2, 'hrif') // rifleman
elseif Tier == 2 then
call CampaignDefenderEx( 2,2,2, 'hmpr') // priest
call CampaignDefenderEx( 1,1,1, 'hrif') // rifleman
call CampaignDefenderEx( 4,4,4, 'hfoo') // footman
elseif Tier == 3 then
call CampaignDefenderEx( 2,2,2, 'hgyr') // gyrocopter
call CampaignDefenderEx( 3,3,3, 'hkni') // knight
call CampaignDefenderEx( 2,2,2, 'hmpr') // priest
call CampaignDefenderEx( 1,1,1, 'hsor') // sorceress
call CampaignDefenderEx( 1,1,1, 'hgry') // gryphon rider
endif
endfunction
//COMMAND FUNCTIONS
//------------------------------------------------
function TierCondition takes nothing returns nothing
if CommandsWaiting() > 0 then
set Tier = GetLastCommand()
call PopLastCommand()
call InitBuildArray()
call InitDefenseGroup()
call BuildOrder()
call CampaignDefenses()
endif
endfunction
function ConditionLoop takes nothing returns nothing
call TierCondition()
call StaggerSleep(1,5)
loop
call TierCondition()
call Sleep(5)
endloop
endfunction
//MAIN FUNCTION
//------------------------------------------------
function main takes nothing returns nothing
call CampaignAI('hhou',null)
call DoCampaignFarms(false)
call BuildOrder()
call CampaignDefenses()
call StartThread(function ConditionLoop)
endfunction
We're done with all the necessary fluff, and the base is ready to crank out attack waves at any foe you wish.
Attack strategy
To start building the attack wave strategy, we will need another thread. But before you go spamming
StartThread
away, remember that we already have a perfectly good thread free to use: the main function. So you'll use that, and start creating the attack wave functions.This is how one attack wave by Blizzard looks like:
JASS:
call InitAssaultGroup()
call CampaignAttackerEx( 1,1,1, DEATH_KNIGHT)
call CampaignAttackerEx( 4,4,6, GHOUL )
call CampaignAttackerEx( 2,2,4, ABOMINATION )
call CampaignAttackerEx( 0,0,1, MEAT_WAGON )
call SuicideOnPlayerEx(M7,M7,M5,user)
Yes, this is from another campaign mission, but all waves follow the same template: empty the attacking units array (like we did with the defenders and the build priorities), then set new attacking units, then start the attack.
The attacking function looks like this:
SuicideOnPlayerEx(e,n,h,target)
, with e=time the script waits before attacking in easy difficulty, n=time the script waits before attacking in normal difficulty, h=time the script waits before attacking in hard difficulty, and target=attacked player.So we make a couple of attack waves and then what? Even if you create 50 attack waves, the ai will go over all of them in a really long game, and you'll run out of ideas for attack groups by wave 25 tops. So we start looping after a certain wave, just like the example "Gates of the abyss".
We don't need just one wave strategy though, we need one for every tier, and a way to change between them. We'll do that by creating 3 wave strategy functions. Each of those functions will start with setting a local integer equal to the Tier global variable, lets call it AttackTier. We'll use that to check if Tier changed every now and then.
To implement this exit strategy, we'll put our entire attack wave strategy inside a loop and write after every wave this line
exitwhen AttackTier != Tier
. This way, whenever the tier changes the attack strategy will change when the current attack wave is done. Inside that, we will nest another function which will act as the actual loop of the attack waves. The exit condition to this loop will also be exitwhen AttackTier != Tier
.Alright, this was a bit much, so let's recap with an example:
JASS:
function Tier2Waves takes nothing returns nothing
local integer AttackTier = Tier
loop
//This is the "outer loop". It has no purpose except providing us with comfortable exit points from the function.
//As such, it will not run more then once.
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN) //Writing units like this is also fine.
call CampaignAttackerEx(1,2,2,PRIEST)
call SuicideOnPlayerEx(M4,M4,M3,user)
exitwhen AttackTier != Tier
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(5,5,8,RIFLEMAN)
call SuicideOnPlayerEx(M4,M4,M4,user)
exitwhen AttackTier != Tier
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,4,4,FOOTMAN)
call CampaignAttackerEx(1,1,2,PRIEST)
call CampaignAttackerEx(3,3,3,SORCERESS)
call SuicideOnPlayerEx(M5,M5,M4,user)
exitwhen AttackTier != Tier
//This is the inner loop, which will run as much as necessary.
//Note it has the same "exitwhen" as the outer loop.
//This way when the condition is fulfilled both layers of the loop will break.
loop
//Wave 4+
call InitAssaultGroup()
call CampaignAttackerEx(6,9,8,FOOTMAN)
call CampaignAttackerEx(1,2,3,MORTAR)
call SuicideOnPlayerEx(M7,M7,M5,user)
exitwhen AttackTier != Tier
//Wave 5+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,GYRO)
call SuicideOnPlayerEx(M6,M6,M5,user)
exitwhen AttackTier != Tier
//Wave 6+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,3,RIFLEMAN)
call CampaignAttackerEx(0,2,4,SORCERESS)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(M6,M6,M6,user)
exitwhen AttackTier != Tier
//Wave 7+
call InitAssaultGroup()
call CampaignAttackerEx(6,6,8,FOOTMAN)
call CampaignAttackerEx(1,2,2,MORTAR)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(M7,M7,M7,user)
exitwhen AttackTier != Tier
endloop
//Don't forget to add another line of exitwhen after the inner loop,
//otherwise the ai will send the first wave again before exiting the function.
exitwhen AttackTier != Tier
endloop
endfunction
Now the last thing we need to put here are upgrades - those will go in between waves using the function
SetBuildUpgrEx(e,m,h, upgrade)
. This function works just like any other build unit function, except that instead of representing the amount of units built, e
,m
and h
represent the level of the upgrade. The upgrade function is also like a normal build function because once you're done researching the upgrade, you can call the function as much as you want, it won't do anything - so there's no problem to put it in a loop. So after adding the upgrades, the attack wave functions should look like this:
JASS:
//ATTACK WAVE STRATEGY
//------------------------------------------------
function Tier1Waves takes nothing returns nothing
local integer AttackTier = Tier
loop
call SetBuildUpgrEx(1,1,1, 'Rhde')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN)
call SuicideOnPlayerEx(15,15,15,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhra')
call SetBuildUpgrEx(1,1,1, 'Rhla')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(2,3,5,RIFLEMAN)
call SuicideOnPlayerEx(15,15,15,user)
exitwhen AttackTier != Tier
loop
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,3,8,FOOTMAN)
call CampaignAttackerEx(2,4,2,RIFLEMAN)
call SuicideOnPlayerEx(20,20,20,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhme')
call SetBuildUpgrEx(1,1,1, 'Rhar')
//Wave 4
call InitAssaultGroup()
call CampaignAttackerEx(6,7,4,FOOTMAN)
call CampaignAttackerEx(2,3,6,RIFLEMAN)
call SuicideOnPlayerEx(25,25,25,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function Tier2Waves takes nothing returns nothing
local integer AttackTier = Tier
//Research all upgrades of earlier tier.
call SetBuildUpgrEx(1,1,1, 'Rhra')
call SetBuildUpgrEx(1,1,1, 'Rhla')
call SetBuildUpgrEx(1,1,1, 'Rhme')
call SetBuildUpgrEx(1,1,1, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhde')
loop
call SetBuildUpgrEx(1,1,1, 'Rhpt')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN)
call CampaignAttackerEx(1,2,2,PRIEST)
call SuicideOnPlayerEx(20,20,20,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhri')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(5,5,8,RIFLEMAN)
call SuicideOnPlayerEx(25,25,25,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhme')
call SetBuildUpgrEx(2,2,2, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhst')
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,4,4,FOOTMAN)
call CampaignAttackerEx(1,1,2,PRIEST)
call CampaignAttackerEx(3,3,3,SORCERESS)
call SuicideOnPlayerEx(30,35,35,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhra')
call SetBuildUpgrEx(2,2,2, 'Rhla')
loop
//Wave 4+
call InitAssaultGroup()
call CampaignAttackerEx(6,9,8,FOOTMAN)
call CampaignAttackerEx(1,2,3,MORTAR)
call SuicideOnPlayerEx(40,40,40,user)
exitwhen AttackTier != Tier
//Wave 5+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,GYRO)
call SuicideOnPlayerEx(30,30,30,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhcd')
//Wave 6+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,3,RIFLEMAN)
call CampaignAttackerEx(0,2,4,SORCERESS)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(35,35,35,user)
exitwhen AttackTier != Tier
//Wave 7+
call InitAssaultGroup()
call CampaignAttackerEx(6,6,8,FOOTMAN)
call CampaignAttackerEx(1,2,2,MORTAR)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(45,45,45,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function Tier3Waves takes nothing returns nothing
local integer AttackTier = Tier
//Research all upgrades of earlier tier.
call SetBuildUpgrEx(1,1,1, 'Rhpt')
call SetBuildUpgrEx(1,1,1, 'Rhri')
call SetBuildUpgrEx(2,2,2, 'Rhme')
call SetBuildUpgrEx(2,2,2, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhst')
call SetBuildUpgrEx(2,2,2, 'Rhra')
call SetBuildUpgrEx(2,2,2, 'Rhla')
call SetBuildUpgrEx(1,1,1, 'Rhcd')
loop
call SetBuildUpgrEx(2,2,2, 'Rhpt')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,FOOTMAN)
call CampaignAttackerEx(1,2,2,PRIEST)
call CampaignAttackerEx(0,0,2,TANK)
call SuicideOnPlayerEx(40,40,40,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhan')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(4,5,8,KNIGHT)
call SuicideOnPlayerEx(45,45,45,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhst')
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(8,8,8, SORCERESS)
call CampaignAttackerEx(0,0,3, HUMAN_DRAGON_HAWK)
call SuicideOnPlayerEx(35,35,35,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhme')
call SetBuildUpgrEx(3,3,3, 'Rhar')
//Wave 4
call InitAssaultGroup()
call CampaignAttackerEx(6,8,12, SORCERESS)
call SuicideOnPlayerEx(30,30,30,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhla')
loop
//Wave 5
call InitAssaultGroup()
call CampaignAttackerEx(4,6,5,KNIGHT)
call CampaignAttackerEx(1,3,6,PRIEST)
call SuicideOnPlayerEx(50,50,50,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhra')
//Wave 6
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,GRYPHON)
call SuicideOnPlayerEx(55,55,55,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhrt')
//Wave 7
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,FOOTMAN)
call CampaignAttackerEx(4,4,4,RIFLEMAN)
call CampaignAttackerEx(1,2,4,PRIEST)
call CampaignAttackerEx(0,2,2,MORTAR)
call SuicideOnPlayerEx(60,60,60,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhhb')
//Wave 8
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,GRYPHON)
call CampaignAttackerEx(4,4,7,KNIGHT)
call SuicideOnPlayerEx(70,70,70,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
So we've added an attack wave function for every tier, and now we have just one thing left to do: choose the attack wave function according to the current tier. To do that we'll need, you guessed it, another infinite loop function. This one will call an attack wave function according to the value of the variable
Tier
, so it'll look like that:
JASS:
Function AttackSwitch takes nothing returns nothing
loop
if Tier == 1 then
call Tier1Waves()
elseif Tier == 2 then
call Tier2Waves()
elseif Tier == 3 then
call Tier3Waves()
else
call DisplayTextToPlayer(user,0,0,"Something's not right")
endif
//We don't need to call Sleep() here because every time one of
//the attack wave functions end this function will immediately call another one.
endloop
endfunction
That last bit over there is your debugging function in ai, and it does what the name suggests: it displays to a player (first argument) some text (last argument). The 2nd and 3rd argument can always stay at 0, as they don't affect the displayed text (AFAIK). DisplayTextToPlayer is especially useful to see where your script fails: put it right after
CampaignAI()
to see if the game actually received the script, spam it in new functions to see where they fail. Go nuts.Anyway, now that the attack strategy is done, your script is ready to go. So just for reference:
JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
player user = Player(0)
integer Tier = 1
endglobals
//BUILD PRIORITIES
//------------------------------------------------
function BuildOrder takes nothing returns nothing
call SetBuildUnitEx( 1,1,1, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'htow') // town hall
call SetBuildUnitEx( 5,5,5, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'hbar') // barracks
call SetBuildUnitEx( 2,2,2, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'halt') // hero altar
call SetBuildUnitEx( 8,8,8, 'hpea') // peasent
call SetBuildUnitEx( 5,5,5, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hlum') // lumber mill
call SetBuildUnitEx( 1,1,1, 'hbla') // blacksmith
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'hkee') // Keep upgrade
call SetBuildUnitEx( 1,1,1, 'hars') // arcane sanctum
endif
call SetBuildUnitEx( 1,1,1, 'hvlt') // item shop
call SetBuildUnitEx( 6,6,6, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hwtw') // watch tower
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'harm') // workshop
call SetBuildUnitEx( 11,11,11, 'hhou') // farm
call SetBuildUnitEx( 3,3,3, 'hwtw') // watch tower
endif
if Tier > 2 then
call SetBuildUnitEx( 1,1,1, 'hcas') // Castle upgrade
call SetBuildUnitEx( 1,1,1, 'hatw') // arcane tower
call SetBuildUnitEx( 1,1,1, 'hgra') // gryphon aviery
call SetBuildUnitEx( 1,1,1, 'hgtw') // guard tower
call SetBuildUnitEx( 1,1,1, 'hctw') // canon tower
call SetBuildUnitEx( 14,14,14, 'hhou') // farm
endif
endfunction
//BASE DEFENSE
//------------------------------------------------
function CampaignDefenses takes nothing returns nothing
if Tier == 1 then
call CampaignDefenderEx( 2,3,4, 'hfoo') // footman
call CampaignDefenderEx( 1,2,2, 'hrif') // rifleman
elseif Tier == 2 then
call CampaignDefenderEx( 1,2,3, 'hmpr') // priest
call CampaignDefenderEx( 1,1,2, 'hrif') // rifleman
call CampaignDefenderEx( 3,4,6, 'hfoo') // footman
elseif Tier == 3 then
call CampaignDefenderEx( 0,2,2, 'hgyr') // gyrocopter
call CampaignDefenderEx( 2,3,4, 'hkni') // knight
call CampaignDefenderEx( 2,3,2, 'hmpr') // priest
call CampaignDefenderEx( 1,1,2, 'hsor') // sorceress
call CampaignDefenderEx( 1,0,1, 'hgry') // gryphon rider
endif
endfunction
//ATTACK WAVE STRATEGY
//------------------------------------------------
function Tier1Waves takes nothing returns nothing
local integer AttackTier = Tier
loop
call SetBuildUpgrEx(1,1,1, 'Rhde')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN)
call SuicideOnPlayerEx(15,15,15,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhra')
call SetBuildUpgrEx(1,1,1, 'Rhla')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(2,3,5,RIFLEMAN)
call SuicideOnPlayerEx(15,15,15,user)
exitwhen AttackTier != Tier
loop
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,3,8,FOOTMAN)
call CampaignAttackerEx(2,4,2,RIFLEMAN)
call SuicideOnPlayerEx(20,20,20,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhme')
call SetBuildUpgrEx(1,1,1, 'Rhar')
//Wave 4
call InitAssaultGroup()
call CampaignAttackerEx(6,7,4,FOOTMAN)
call CampaignAttackerEx(2,3,6,RIFLEMAN)
call SuicideOnPlayerEx(25,25,25,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function Tier2Waves takes nothing returns nothing
local integer AttackTier = Tier
//Research all upgrades of earlier tier.
call SetBuildUpgrEx(1,1,1, 'Rhra')
call SetBuildUpgrEx(1,1,1, 'Rhla')
call SetBuildUpgrEx(1,1,1, 'Rhme')
call SetBuildUpgrEx(1,1,1, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhde')
loop
call SetBuildUpgrEx(1,1,1, 'Rhpt')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN)
call CampaignAttackerEx(1,2,2,PRIEST)
call SuicideOnPlayerEx(20,20,20,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhri')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(5,5,8,RIFLEMAN)
call SuicideOnPlayerEx(25,25,25,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhme')
call SetBuildUpgrEx(2,2,2, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhst')
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,4,4,FOOTMAN)
call CampaignAttackerEx(1,1,2,PRIEST)
call CampaignAttackerEx(3,3,3,SORCERESS)
call SuicideOnPlayerEx(30,35,35,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhra')
call SetBuildUpgrEx(2,2,2, 'Rhla')
loop
//Wave 4+
call InitAssaultGroup()
call CampaignAttackerEx(6,9,8,FOOTMAN)
call CampaignAttackerEx(1,2,3,MORTAR)
call SuicideOnPlayerEx(40,40,40,user)
exitwhen AttackTier != Tier
//Wave 5+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,GYRO)
call SuicideOnPlayerEx(30,30,30,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhcd')
//Wave 6+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,3,RIFLEMAN)
call CampaignAttackerEx(0,2,4,SORCERESS)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(35,35,35,user)
exitwhen AttackTier != Tier
//Wave 7+
call InitAssaultGroup()
call CampaignAttackerEx(6,6,8,FOOTMAN)
call CampaignAttackerEx(1,2,2,MORTAR)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(45,45,45,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function Tier3Waves takes nothing returns nothing
local integer AttackTier = Tier
//Research all upgrades of earlier tier.
call SetBuildUpgrEx(1,1,1, 'Rhpt')
call SetBuildUpgrEx(1,1,1, 'Rhri')
call SetBuildUpgrEx(2,2,2, 'Rhme')
call SetBuildUpgrEx(2,2,2, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhst')
call SetBuildUpgrEx(2,2,2, 'Rhra')
call SetBuildUpgrEx(2,2,2, 'Rhla')
call SetBuildUpgrEx(1,1,1, 'Rhcd')
loop
call SetBuildUpgrEx(2,2,2, 'Rhpt')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,FOOTMAN)
call CampaignAttackerEx(1,2,2,PRIEST)
call CampaignAttackerEx(0,0,2,TANK)
call SuicideOnPlayerEx(40,40,40,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhan')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(4,5,8,KNIGHT)
call SuicideOnPlayerEx(45,45,45,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhst')
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(8,8,8, SORCERESS)
call CampaignAttackerEx(0,0,3, HUMAN_DRAGON_HAWK)
call SuicideOnPlayerEx(35,35,35,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhme')
call SetBuildUpgrEx(3,3,3, 'Rhar')
//Wave 4
call InitAssaultGroup()
call CampaignAttackerEx(6,8,12, SORCERESS)
call SuicideOnPlayerEx(30,30,30,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhla')
loop
//Wave 5
call InitAssaultGroup()
call CampaignAttackerEx(4,6,5,KNIGHT)
call CampaignAttackerEx(1,3,6,PRIEST)
call SuicideOnPlayerEx(50,50,50,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhra')
//Wave 6
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,GRYPHON)
call SuicideOnPlayerEx(55,55,55,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhrt')
//Wave 7
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,FOOTMAN)
call CampaignAttackerEx(4,4,4,RIFLEMAN)
call CampaignAttackerEx(1,2,4,PRIEST)
call CampaignAttackerEx(0,2,2,MORTAR)
call SuicideOnPlayerEx(60,60,60,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhhb')
//Wave 8
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,GRYPHON)
call CampaignAttackerEx(4,4,7,KNIGHT)
call SuicideOnPlayerEx(70,70,70,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function AttackLoop takes nothing returns nothing
loop
if Tier == 1 then
call Tier1Waves()
elseif Tier == 2 then
call Tier2Waves()
elseif Tier == 3 then
call Tier3Waves()
else
call DisplayTextToPlayer(user,0,0,"Something's not right")
endif
endloop
endfunction
//COMMAND FUNCTIONS
//------------------------------------------------
function TierCondition takes nothing returns nothing
if CommandsWaiting() > 0 then
set Tier = GetLastCommand()
call PopLastCommand()
call InitBuildArray()
call InitDefenseGroup()
call BuildOrder()
call CampaignDefenses()
endif
endfunction
function ConditionLoop takes nothing returns nothing
call TierCondition()
call StaggerSleep(1,5)
loop
call TierCondition()
call Sleep(5)
endloop
endfunction
//MAIN FUNCTION
//------------------------------------------------
function main takes nothing returns nothing
call CampaignAI('hhou',null)
call DoCampaignFarms(false)
call BuildOrder()
call CampaignDefenses()
call StartThread(function ConditionLoop)
call AttackLoop()
endfunction
Now that we're done, import the script to your map, open it, punch "warpten" and "whosyourdaddy" on the keypad (that's right, I endorse cheating), and enjoy yourself!
What's that? The attack waves are too small? I know [evilgrin].
You might vaguely recall I talked about a bug which is not a syntax error at the beginning of the tutorial. Well, this is it:
The problem is that the attack waves are cut short, but what causes that? It can't be
CampaignAttackerEx
, because that function just assigns values to variables to be used in a thread opened by CampaignAI ()
at the start of the script, and we'll assume that Blizzard didn't mess up something so basic in its script, so what's left is the attack function itself - SuicideOnPlayerEx
. Let's open it up. After opening a few wrapper functions we reach this:
JASS:
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
Yep, it's complicated... But you don't need to understand all of it right now, so I'll cut to the chase: what happened can be described as a sort of thread desync. The ai builds the attacking units in one thread, and gets ready for the attack in a different thread. Because we specified such a small waiting time in
SuicideOnPlayerEx
, it just started the attack wave without waiting for all of the units. This is a part of what it does, and it's a good thing, too. Otherwise once you've destroyed one unit factory, the ai will stop attacking, because it wouldn't be able to build some of the units.Fortunately for us, there is a very simple solution - making the waiting time longer, but as you move on to more complicated workflows, you'll find yourself in a need to maintain a lot of checks and balances in order to keep your ai in line. otherwise, you might find the ai stuck in some infinite loop (and you know you'll have plenty of those) where it does nothing. This brings me to the second golden rule of ai: thread carefully! Don't spam
StartThread
and use each thread wisely. In this ai we've created there's an entire thread designed to catch 2 commands, meaning it can run twice at bast in what can be an hour long game. This is fine in our simple example, but if you had another conditions you'd like to check periodically, that would be the place to put them. Also, make sure to make your threads work in harmony with one another.This is the ai script with working waiting times between waves, and this time I assure you this really is a working version:
JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
player user = Player(0)
integer Tier = 1
endglobals
//BUILD PRIORITIES
//------------------------------------------------
function BuildOrder takes nothing returns nothing
call SetBuildUnitEx( 1,1,1, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'htow') // town hall
call SetBuildUnitEx( 5,5,5, 'hpea') // peasent
call SetBuildUnitEx( 1,1,1, 'hbar') // barracks
call SetBuildUnitEx( 2,2,2, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'halt') // hero altar
call SetBuildUnitEx( 8,8,8, 'hpea') // peasent
call SetBuildUnitEx( 5,5,5, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hlum') // lumber mill
call SetBuildUnitEx( 1,1,1, 'hbla') // blacksmith
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'hkee') // Keep upgrade
call SetBuildUnitEx( 1,1,1, 'hars') // arcane sanctum
endif
call SetBuildUnitEx( 1,1,1, 'hvlt') // item shop
call SetBuildUnitEx( 6,6,6, 'hhou') // farm
call SetBuildUnitEx( 1,1,1, 'hwtw') // watch tower
if Tier > 1 then
call SetBuildUnitEx( 1,1,1, 'harm') // workshop
call SetBuildUnitEx( 11,11,11, 'hhou') // farm
call SetBuildUnitEx( 3,3,3, 'hwtw') // watch tower
endif
if Tier > 2 then
call SetBuildUnitEx( 1,1,1, 'hcas') // Castle upgrade
call SetBuildUnitEx( 1,1,1, 'hatw') // arcane tower
call SetBuildUnitEx( 1,1,1, 'hgra') // gryphon aviery
call SetBuildUnitEx( 1,1,1, 'hgtw') // guard tower
call SetBuildUnitEx( 1,1,1, 'hctw') // canon tower
call SetBuildUnitEx( 14,14,14, 'hhou') // farm
endif
endfunction
//BASE DEFENSE
//------------------------------------------------
function CampaignDefenses takes nothing returns nothing
if Tier == 1 then
call CampaignDefenderEx( 2,3,4, 'hfoo') // footman
call CampaignDefenderEx( 1,2,2, 'hrif') // rifleman
elseif Tier == 2 then
call CampaignDefenderEx( 1,2,3, 'hmpr') // priest
call CampaignDefenderEx( 1,1,2, 'hrif') // rifleman
call CampaignDefenderEx( 3,4,6, 'hfoo') // footman
elseif Tier == 3 then
call CampaignDefenderEx( 0,2,2, 'hgyr') // gyrocopter
call CampaignDefenderEx( 2,3,4, 'hkni') // knight
call CampaignDefenderEx( 2,3,2, 'hmpr') // priest
call CampaignDefenderEx( 1,1,2, 'hsor') // sorceress
call CampaignDefenderEx( 1,0,1, 'hgry') // gryphon rider
endif
endfunction
//ATTACK WAVE STRATEGY
//------------------------------------------------
function Tier1Waves takes nothing returns nothing
local integer AttackTier = Tier
loop
call SetBuildUpgrEx(1,1,1, 'Rhde')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN)
call SuicideOnPlayerEx(M3,M3,M2,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhra')
call SetBuildUpgrEx(1,1,1, 'Rhla')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(2,3,5,RIFLEMAN)
call SuicideOnPlayerEx(M4,M3,M3,user)
exitwhen AttackTier != Tier
loop
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,3,8,FOOTMAN)
call CampaignAttackerEx(2,4,2,RIFLEMAN)
call SuicideOnPlayerEx(M4,M3,M4,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhme')
call SetBuildUpgrEx(1,1,1, 'Rhar')
//Wave 4
call InitAssaultGroup()
call CampaignAttackerEx(6,7,4,FOOTMAN)
call CampaignAttackerEx(2,3,6,RIFLEMAN)
call SuicideOnPlayerEx(M5,M5,M4,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function Tier2Waves takes nothing returns nothing
local integer AttackTier = Tier
//Research all upgrades of earlier tier.
call SetBuildUpgrEx(1,1,1, 'Rhra')
call SetBuildUpgrEx(1,1,1, 'Rhla')
call SetBuildUpgrEx(1,1,1, 'Rhme')
call SetBuildUpgrEx(1,1,1, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhde')
loop
call SetBuildUpgrEx(1,1,1, 'Rhpt')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(3,4,6,FOOTMAN)
call CampaignAttackerEx(1,2,2,PRIEST)
call SuicideOnPlayerEx(M3,M3,M3,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhri')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(5,5,8,RIFLEMAN)
call SuicideOnPlayerEx(M4,M4,M4,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhme')
call SetBuildUpgrEx(2,2,2, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhst')
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(3,4,4,FOOTMAN)
call CampaignAttackerEx(1,1,2,PRIEST)
call CampaignAttackerEx(3,3,3,SORCERESS)
call SuicideOnPlayerEx(M5,M5,M4,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhra')
call SetBuildUpgrEx(2,2,2, 'Rhla')
loop
//Wave 4+
call InitAssaultGroup()
call CampaignAttackerEx(6,9,8,FOOTMAN)
call CampaignAttackerEx(1,2,3,MORTAR)
call SuicideOnPlayerEx(M7,M7,M5,user)
exitwhen AttackTier != Tier
//Wave 5+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,GYRO)
call SuicideOnPlayerEx(M6,M6,M5,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhcd')
//Wave 6+
call InitAssaultGroup()
call CampaignAttackerEx(4,4,3,RIFLEMAN)
call CampaignAttackerEx(0,2,4,SORCERESS)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(M6,M6,M6,user)
exitwhen AttackTier != Tier
//Wave 7+
call InitAssaultGroup()
call CampaignAttackerEx(6,6,8,FOOTMAN)
call CampaignAttackerEx(1,2,2,MORTAR)
call CampaignAttackerEx(1,1,3,PRIEST)
call SuicideOnPlayerEx(M7,M7,M7,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function Tier3Waves takes nothing returns nothing
local integer AttackTier = Tier
//Research all upgrades of earlier tier.
call SetBuildUpgrEx(1,1,1, 'Rhpt')
call SetBuildUpgrEx(1,1,1, 'Rhri')
call SetBuildUpgrEx(2,2,2, 'Rhme')
call SetBuildUpgrEx(2,2,2, 'Rhar')
call SetBuildUpgrEx(1,1,1, 'Rhst')
call SetBuildUpgrEx(2,2,2, 'Rhra')
call SetBuildUpgrEx(2,2,2, 'Rhla')
call SetBuildUpgrEx(1,1,1, 'Rhcd')
loop
call SetBuildUpgrEx(2,2,2, 'Rhpt')
//Wave 1
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,FOOTMAN)
call CampaignAttackerEx(1,2,2,PRIEST)
call CampaignAttackerEx(0,0,2,TANK)
call SuicideOnPlayerEx(M4,M4,M5,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhan')
//Wave 2
call InitAssaultGroup()
call CampaignAttackerEx(4,5,8,KNIGHT)
call SuicideOnPlayerEx(M3,M4,M6,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(2,2,2, 'Rhst')
//Wave 3
call InitAssaultGroup()
call CampaignAttackerEx(8,8,8, SORCERESS)
call CampaignAttackerEx(0,0,3, HUMAN_DRAGON_HAWK)
call SuicideOnPlayerEx(M5,M5,M5,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhme')
call SetBuildUpgrEx(3,3,3, 'Rhar')
//Wave 4
call InitAssaultGroup()
call CampaignAttackerEx(6,8,12, SORCERESS)
call SuicideOnPlayerEx(M3,M4,M4,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhla')
loop
//Wave 5
call InitAssaultGroup()
call CampaignAttackerEx(4,6,5,KNIGHT)
call CampaignAttackerEx(1,3,6,PRIEST)
call SuicideOnPlayerEx(M4,M5,M5,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(3,3,3, 'Rhra')
//Wave 6
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,GRYPHON)
call SuicideOnPlayerEx(M4,M3,M5,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhrt')
//Wave 7
call InitAssaultGroup()
call CampaignAttackerEx(4,4,6,FOOTMAN)
call CampaignAttackerEx(4,4,4,RIFLEMAN)
call CampaignAttackerEx(1,2,4,PRIEST)
call CampaignAttackerEx(0,2,2,MORTAR)
call SuicideOnPlayerEx(M5,M5,M6,user)
exitwhen AttackTier != Tier
call SetBuildUpgrEx(1,1,1, 'Rhhb')
//Wave 8
call InitAssaultGroup()
call CampaignAttackerEx(4,4,7,GRYPHON)
call CampaignAttackerEx(4,4,7,KNIGHT)
call SuicideOnPlayerEx(M6,M6,M7,user)
exitwhen AttackTier != Tier
endloop
exitwhen AttackTier != Tier
endloop
endfunction
function AttackLoop takes nothing returns nothing
loop
if Tier == 1 then
call Tier1Waves()
elseif Tier == 2 then
call Tier2Waves()
elseif Tier == 3 then
call Tier3Waves()
else
call DisplayTextToPlayer(user,0,0,"Something's not right")
endif
endloop
endfunction
//COMMAND FUNCTIONS
//------------------------------------------------
function TierCondition takes nothing returns nothing
if CommandsWaiting() > 0 then
set Tier = GetLastCommand()
call PopLastCommand()
call InitBuildArray()
call InitDefenseGroup()
call BuildOrder()
call CampaignDefenses()
endif
endfunction
function ConditionLoop takes nothing returns nothing
call TierCondition()
call StaggerSleep(1,5)
loop
call TierCondition()
call Sleep(5)
endloop
endfunction
//MAIN FUNCTION
//------------------------------------------------
function main takes nothing returns nothing
call CampaignAI('hhou',null)
call DoCampaignFarms(false)
call BuildOrder()
call CampaignDefenses()
call StartThread(function ConditionLoop)
call AttackLoop()
endfunction
This is it. I wanted to write a lot more concerning how ai fights and how to test your work, but the tutorial is long enough already.
What's next?
There's much more to learn about ai. For starters, I kept 2 minor faults in the ai for you to exercise on:- When upgrading a tier the ai will build an entirely different group of defenders, but the defenders of the old tier will stay in the base until killed or added to an attack wave. This is bad, and can potentially make the ai player reach the food cap. Find a way to dispose of those excess defenders.
- Right now, the ai will research upgrades before building Attack waves units. Instead of that, try making the ai research after building the attack wave.
Attachments
Last edited: