Andy Breakdown
Notice: Modding tutorials assume you know at least the basics of coding or scripting. Commander Wars uses Javascript as the Scripting Language. In case you want to learn more about Javascript, you can check the tutorial here: Derek Banas' Javascript tutorial , purchase O'Reilly's excellent book on the subject JavaScript: The Definitive Guide, or check out W3 Schools Interactive Tutorials
This tutorial references https://github.com/Robosturm/Commander_Wars/blob/master/resources/scripts/cos/co_andy.js, the source code of the actual vanilla Andy in COW.
Small Disclaimer
This will only go through vanilla Andy's js code, and not any other files. This is intended for newer players to understand the workings of what the code they're copy-pasting actually does. If you want to make a brand new CO in a mod, a good starting point is Custom CO. Alternatively, download a mod and edit to suit your needs.
Also I refer to normal CO powers as COP and super CO powers as SCOP, just clearing that up
Some stuff in the header
var Constructor = function()
{
The very first line (or two) of code. This defines a variable called Constructor to be a function. What does the function do? It's defined in the curly brackets {} following it. Notice how only the opening bracket is present. This is a long function (the whole CO).
this.getCOStyles = function()
{
return ["+alt", "+alt2", "+alt3"];
};
Due to some js... weirdness, we treat functions as objects that you can define properties of. If we called Constructor.getCOStyles() after finishing the Constructor function, we would get this. Specifically it would return ["+alt", "+alt2", "+alt3"]. This is telling the game to look for alternate CO images labeled "andy+alt", "andy+alt2", and "andy+alt3" as well as simply "andy". If you look in the corresponding images folder ([1]), we do actually find said images. This is Commander Wars's way to get CO styles, so if you want to have extra costumes for your CO, this is how you do it.
this.getAiUsePower = function(co, powerSurplus, unitCount, repairUnits, indirectUnits, directUnits, enemyUnits, turnMode)
{
if (turnMode === GameEnums.AiTurnMode_StartOfDay)
{
if (co.canUseSuperpower())
{
return GameEnums.PowerMode_Superpower;
}
else if (powerSurplus <= 0.5 &&
co.canUsePower())
{
return CO.getAiUsePowerAtUnitCount(co, powerSurplus, turnMode, repairUnits);
}
}
};
This is a custom function for the AI behavior of Andy. If it looks scary, that's because it kinda is there's just a bunch of variables and if statements flying around. Let's break this down further
this.getAiUsePower = function(co, powerSurplus, unitCount, repairUnits, indirectUnits, directUnits, enemyUnits, turnMode)
{
Again, a function definition. But this time, with words inside the function() brackets. Those are called parameters, and how you can pass data into a function. Here, the game is passing in:
- The CO (co) (specifically the player's instance of a CO, in an andy mirror match both andys are actually considered different COs)
- The power charge more than what the COP needs (powerSurplus)
- The count of all owned units on the field (unitCount)
- Count of all units less than 10hp (repairUnits)
- Count of all units that can shoot at more than 1 range (indirectUnits)
- Count of all units not an indirect unit (directUnits)
- The number of enemy units
- The turn mode of the AI, set to start of day, during the day, and end of day.
The vast majority of the time, a significant portion of these will not be used (for example, Andy doesn't really care if his units are direct or indirect, so long as they're repaired)
if (turnMode === GameEnums.AiTurnMode_StartOfDay)
{
This checks that the turn mode is start of day. This makes sure that Andy only ever uses his power at the start of each day.
if (co.canUseSuperpower())
{
return GameEnums.PowerMode_Superpower;
}
If Andy can use his SCOP of course he uses his SCOP
else if (powerSurplus <= 0.5 &&
co.canUsePower())
{
return CO.getAiUsePowerAtUnitCount(co, powerSurplus, turnMode, repairUnits);
}
If Andy has at most 0.5 stars more than his COP (and if he can use his COP in the first place), return CO.getAiUsePowerAtUnitCount(co, powerSurplus, turnMode, repairUnits);?
Referencing CO.getAiUsePowerAtUnitCount, it first checks if turnMode is equal to start of day, and also if repairUnits (in Andy's getAiUsePower at least, the name is changed during the function call to unitCount) is at least 5.
If either of these conditions are not fulfilled, don't use any power.
If both of these conditions are fulfilled, use SCOP is possible, and use COP if powerSurplus <= 0.5 (and if COP is usable in the first place)
So essentially, Andy doesn't want to Hyper Repair unless he has just enough power charge and if he can repair at least 5 units.
}
};
Closing brackets you always need those
You may be put off at the lack of a return statement here. Won't the code break if the function doesn't return anything?
I'm put off too.
Also the semicolon at the end? This is because we're actually defining a variable (getAiUsePower) as this function. Defining a variable is a code statement. We need semicolons after each code statement so the computer knows where to end each block of code. This is a property of c++ and java as well as javascript.
Next segment!
Power stuff
this.init = function(co, map)
{
co.setPowerStars(3);
co.setSuperpowerStars(3);
};
init() is generally a function called to make sure that something is ready and to set up any needed variables. In this case, the only things that are done are setting COP stars to 3 and setting SCOP stars to 3. Note how the SCOP does not cost 3 stars, rather this is 3 stars on top of the COP cost of 3 stars, for a total of 6 stars. Since the next function probably won't fit on the screen all at once, I'll just break it down now.
this.activatePower = function(co, map)
{
var dialogAnimation = co.createPowerSentence();
var powerNameAnimation = co.createPowerScreen(GameEnums.PowerMode_Power);
dialogAnimation.queueAnimation(powerNameAnimation);
The function is called activatePower, which is used when you activate power (COP specifically)
dialogAnimation and powerNameAnimation are used for the flashy effects whenever you use a power, the ones that appear, fill the screen with the power name, and disappear before your units get sparkles on them.
.queueAnimation is used to make sure that the whole screen is filled after the dialog finishes.
var units = co.getOwner().getUnits();
var animations = [];
var counter = 0;
units.randomize();
To go through all the units and make animations, we'll need to get all the units and have a place to store all the animations. counter is set to 0 and will correspond to animations's size (incidentally both are 0 right now) units.randomize() makes sure that the units will be considered in a random order. Since animations happen in the same order, the animations are randomized as well.
for (var i = 0; i < units.size(); i++)
{
var unit = units.at(i);
The for loop will iterate through the units, with i being the index and unit being the current unit. This is randomized because units was randomized beforehand.
var animation = GameAnimationFactory.createAnimation(map, unit.getX(), unit.getY());
animation.writeDataInt32(unit.getX());
animation.writeDataInt32(unit.getY());
animation.writeDataInt32(CO_ANDY.powerHeal);
animation.setEndOfAnimationCall("ANIMATION", "postAnimationHeal");
Here the animation is set up. The animation is created on the map at the unit's position in createAnimation. Then the unit's position is written to the animation and CO_ANDY.powerHeal. powerHeal is an integer specifying the COP healing, in this case 2. The variable is useful to avoid "magic numbers", constant values that are typed in directly. If you want to change a magic number, you have to manually go into the code and edit every instance of that magic number. Keeping your constants in variables like CO_ANDY.powerHeal allows you to change the value quickly and easily from one place. You'll see variables like this later in the code.
Note: CO_<co name> is standard convention for CO variables. Often, you'll store important variables for your CO on CO_<co name> like CO_ANDY.powerHeal. CO_<co name> is how other scripts are going to access your CO, and how your own script accesses your CO.
At the end of the animation, the units will be healed for 2 hp. postAnimationHeal uses the prior three values to know what position to heal as well as how much to heal. setEndOfAnimationCall tells the animation engine to call a function (in this case postAnimationHeal) from an object (ANIMATION) at the end of the animation. Here's the actual postAnimationHeal if you're curious. [2]
Before going any further into the animation code, I want to go into the big picture of what is actually happening.
So first of all, when an animation is created, it's going to be handled by the Commander Wars engine no matter what. It doesn't matter if no variable in your script points to it, it's going to be saved in the core game and the core game will handle it.
The power animation for the units goes something like this: make an animation for every unit in a random order, but don't have more than 5 animations going on at the same time.
With that out of the way, here's the rest of the code
var delay = globals.randInt(135, 265);
if (animations.length < 5)
{
delay *= i;
}
animation.setSound("power0.wav", 1, delay);
delay is set to some random number between 135 and 265. Then, if the animation queue isn't filled (length is 5), multiply the delay by i.
What delay *= i will do is approximately make sure that the animations are in order. Say we happen to roll 200 3 times, the delays would be 200 (start at 200), 400, and 600. This will make the animations appear one after the other, kind of like how it works in the original advance wars. I'll explain what happens if the delay isn't multiplied later.
animation.setSound("power0.wav", 1, delay) sets the sound of the animation. power0.wav is the filename of the sound, 1 is the number of times to repeat (1 means 1 play) and delay is how delayed it is. If you want, I believe you can add another number for volume and also set if the oldest sound should be stopped. (animation.setSound("filename", repeats, delay, volume, stopOldestSound))
if (animations.length < 5)
{
animation.addSprite("power0", -map.getImageSize() * 1.27, -map.getImageSize() * 1.27, 0, 2, delay);
powerNameAnimation.queueAnimation(animation);
animations.push(animation);
}
animations.length < 5 means the animation queue isn't full. It then adds a sprite (power0) and positions it accordingly.
Looking at the source of addSprite [3], it offsets the image so that the unit is in the center (-map.getImageSize()*1.27) (both of them), sets sleepAfterFinish (0 is no I think), sets the scale (2) and adds delay (delay). There's another parameter for loops, which would probably set repeats if you want the image to show up more than once.
powerNameAnimation.queueAnimation(animation) sets the current animation to fire right after powerNameAnimation (the screen fill with your power name). But while the animation starts right away internally, the sound and image are delayed (thanks to delay) so what you see and hear on screen actually starts a bit later (which is probably what you want). queueAnimation() is a function attached to the animation class in general, so don't be confused if you see a different animation object using this function.
animations.push(animation) sticks the current animation at the end of the animations list. This will become very relevant shortly.
else
{
animation.addSprite("power0", -map.getImageSize() * 1.27, -map.getImageSize() * 1.27, 0, 2, delay);
animations[counter].queueAnimation(animation);
animations[counter] = animation;
counter++;
if (counter >= animations.length)
{
counter = 0;
}
}
else is for if the statement in the if block isn't true (in this case animations.length < 5). This would mean that the animation queue is full.
Just to recap, delay has not been multiplied by i, counter is the index of some animation in animation, and animations is full of animations.
We've seen the addSprite method before. What's different is animations[counter].queueAnimation(animation). Here, the current animation is set to fire right after animations[counter] finishes. Then the current animation replaces the old animation, and counter increases by 1. (++ does that) If counter >= animations.length (animations[counter] would throw an error), just set it back to 0.
Was that confusing? Here's a step by step diagram.
Right before the first instance of this, animations is filled with [0,1,2,3,4]. (not actually those numbers, just animations 0, 1, 2, 3, and 4)
On the first iteration of this else statement, counter is 0, so animation 0 will set 5 to fire right after it, then 5 takes 0's place. counter increases by 1.
The list looks like this: [5,1,2,3,4], and counter is 1 now.
Next iteration: [5,6,2,3,4] (6 is queued after 1, counter is 2)
Skip forwards a bit: [5,6,7,8,4] (7 after 2, 8 after 3, counter is 4)
Now the animations will do their dance [5,6,7,8,9], and counter increases by 1 to 5.
But animations[5] is not valid (remember 0 indexing), so counter is set back to 0, and the cycle repeats.
The delay works here, because we have 0,5,10,15 going "1 delay" after each other, 1,6,11,16 going "1 delay" after each other (but 1 is delayed by "1 delay" relative to 0) and the rest is similar. If you don't think this is good, feel free to mess around with the code and see what happens.
}
};
And that's that function done.
activateSuperpower is actually pretty similar, using much of the same mechanics to do basically the same thing.
Notable changes:
animation.writeDataInt32(CO_ANDY.superPowerHeal);
When using the SCOP, we use the superPowerHeal rather than the regular power heal.
if (animations.length < 7)
bigger queue, more animations at once
if (i % 2 === 0)
{
animation.setSound("power12_1.wav", 1, delay);
}
else
{
animation.setSound("power12_2.wav", 1, delay);
}
We have two sounds! i%2===0 will be true every other iteration, so we alternate between the two sounds.
animation.addSprite("power12", -map.getImageSize() * 2, -map.getImageSize() * 2, 0, 2, delay);
Different image, and the offset is bigger (because the image is bigger)
Again, it was basically the same thing, but with SCOP stuff.
You're asking how long the next functions are going to be?
Not very long actually, they're far simpler and mostly just for returning numbers.
Music and Modifiers
Most of these functions will return a value. Not much logic in these.
this.loadCOMusic = function(co, map)
{
if (CO.isActive(co))
{
switch (co.getPowerMode())
{
case GameEnums.PowerMode_Power:
audio.addMusic("resources/music/cos/power.mp3", 992, 45321);
break;
case GameEnums.PowerMode_Superpower:
audio.addMusic("resources/music/cos/superpower.mp3", 1505, 49515);
break;
case GameEnums.PowerMode_Tagpower:
audio.addMusic("resources/music/cos/tagpower.mp3", 14611, 65538);
break;
default:
audio.addMusic("resources/music/cos/andy.mp3", 4466, 74972);
break;
}
}
};
co.getPowerMode() returns something based on what power is activated.
switch statements execute different code based on the input value. In this case that would be the power mode.
This format (a switch statement using the power mode) is actually the standard way of doing different things if your power is active or not. You don't have to memorize it, just copy-paste it a bunch.
audio.addMusic plays the music (in a loop presumably) from startTime (in milliseconds) to endTime (in milliseconds).
The break statements prevent the code from considering other cases once one case has been run.
this.getCOUnitRange = function(co, map)
{
return 3;
};
this.getCOArmy = function()
{
return "OS";
};
getCOUnitRange should return the CO zone range, in this case 3. getCOArmy should return a string corresponding to the co's country. In this case Andy is from Orange Star, so this function returns "OS". You can check other COs for their country's strings.
this.superPowerHeal = 5;
this.superPowerOffBonus = 30;
this.superPowerMovementBonus = 1;
this.powerDefBonus = 20;
this.powerOffBonus = 20;
this.powerHeal = 2;
this.d2dCoZoneOffBonus = 20;
this.d2dCoZoneDefBonus = 20;
These are variables for easy modification of Andy's numbers. If you check the in-game wiki, these numbers should line up with what you see there. As stated above, these variables make sure you don't accidentally change the numbers in one place but not another.
this.getOffensiveBonus = function(co, attacker, atkPosX, atkPosY,
defender, defPosX, defPosY, isDefender, action, luckmode, map)
{
if (CO.isActive(co))
{
switch (co.getPowerMode())
{
case GameEnums.PowerMode_Tagpower:
case GameEnums.PowerMode_Superpower:
return CO_ANDY.superPowerOffBonus;
case GameEnums.PowerMode_Power:
return CO_ANDY.powerOffBonus;
default:
if (co.inCORange(Qt.point(atkPosX, atkPosY), attacker))
{
return CO_ANDY.d2dCoZoneOffBonus;
}
break;
}
}
return 0;
};
getOffensiveBonus is called when you want to find the offensive bonus of a specific unit. There are a lot of parameters for this function, but honestly most of them are pretty self-explanatory (what do you expect defender to be).
if (CO.isActive(co)) just makes sure your CO is actually active and should be used.
The switch statement is explained above. In this case, it changes the offensive bonus depending on if the power is active or not.
Note the co.inCORange() function. This is a very easy way to check if a unit is in the CO zone or not. You may be curious why this check isn't in any other case. In the original AW DoR, where CO zones were first introduced, CO zones became global while using powers. This is carried over to COW, so not only would the check be redundant (you can be sure your unit is in the map-wide zone), it may not even work (sometimes when a CO unit is moved during powers, the zone shows up as range 0).
Also the break statement at the end. This is best practice in switch statements, as it ensures you get out of the switch right away and not accidentally fall into other cases. You don't need to do this after a return statement, because return sends you out of the function as well as the switch.
this.getDeffensiveBonus = function(co, attacker, atkPosX, atkPosY,
defender, defPosX, defPosY, isAttacker, action, luckmode, map)
{
if (CO.isActive(co))
{
if (co.getPowerMode() > GameEnums.PowerMode_Off)
{
return CO_ANDY.powerDefBonus;
}
else if (co.inCORange(Qt.point(defPosX, defPosY), defender))
{
return CO_ANDY.d2dCoZoneDefBonus;
}
}
return 0;
};
Very similar, except for one thing.
A funny thing about enums, they're actually numbers.
This allows for funky stuff like co.getPowerMode() > GameEnums.PowerMode_Off. This will be true if either CO power is active.
I don't personally use this, but this is an alternative to a switch statement.
this.getMovementpointModifier = function(co, unit, posX, posY, map)
{
if (CO.isActive(co))
{
if (co.getPowerMode() === GameEnums.PowerMode_Superpower ||
co.getPowerMode() === GameEnums.PowerMode_Tagpower)
{
return CO_ANDY.superPowerMovementBonus;
}
}
return 0;
};
Very similar, again Since movement takes place outside of combat, you don't have attacker, attackerX, defender, and defenderY (some variables skipped for brevity). Instead you just have the unit you're moving. This shouldn't be too relevant, but beware accidentally copying attacker and not replacing it with unit.
this.getCOUnits = function(co, building, map)
{
if (CO.isActive(co))
{
var buildingId = building.getBuildingID();
if (buildingId === "FACTORY" ||
buildingId === "TOWN" ||
BUILDING.isHq(building))
{
return ["ZCOUNIT_REPAIR_TANK"];
}
}
return [];
};
This function gets your exclusive CO units. In Andy's case, we have repair tanks (internal ID ZCOUNIT_REPAIR_TANK). But since it's a land unit, we don't want to build it out of any building (as much as I think Andy forgetting what an airport is supposed to do is funny). So we check the buildingId. For compatibility with Hachi and Hachi's soul, we also add the repair tank to the build lists of towns and hqs, even though they can't build anything.
this.getAiCoUnitBonus = function(co, unit, map)
{
return 1;
};
I actually had to look up what this does. If we step away from Andy for a bit and go to Sami's code [4]:
this.getAiCoUnitBonus = function(co, unit, map)
{
if (unit.getUnitID !== "INFANTRY")
{
if (unit.getUnitType() === GameEnums.UnitType_Infantry)
{
return 8;
}
else if (unit.getBaseMaxRange() === 1)
{
return -1;
}
}
return 2;
};
It quickly becomes apparent what this method is used for. If the unit is an infantry, skip the if statement and return 2. If the unit is of type infantry (but not the 1k infantry, something like a commando), return 8. If it's a direct unit, return -1. And if it's an indirect, you go outside of the if statement and return 2.
In short, Sami prefers infantry type over infantry and indirects, and those over direct units (for CO units) (Thanks Sami!)
Andy returns 1 no matter what, so no preference.
What a long winded way of explaining 4 lines of code.
Bio, Description, and Details
// CO - Intel
this.getBio = function(co)
{
return qsTr("A whiz with a wrench, this mechanical boy wonder earned fame as the hero who saved Macro Land in the last great war.");
};
Now we get to the CO information. This just returns a string. What's interesting is the use of the function qsTr. This converts the string into a format that's easier to display, so just put it around the test you're displaying and you're good to go.
this.getHits = function(co)
{
return qsTr("Mechanics");
};
this.getMiss = function(co)
{
return qsTr("Waking up too early");
};
this.getCODescription = function(co)
{
return qsTr("No real weaknesses or strengths. Ready to fight wherever and whenever.");
};
More bio. getCODescription is where you'll put a short description of your CO. Nothing long and no numbers, just tell the user the basics. Also useful for more flavor text.
this.getLongCODescription = function()
{
var text = qsTr("\nSpecial Unit:\nRepair Tanks\n") +
qsTr("\nGlobal Effect: \nNone.") +
qsTr("\n\nCO Zone Effect: \nAndy's units gain +%0% firepower and +%1% defence.");
text = replaceTextArgs(text, [CO_ANDY.d2dCoZoneOffBonus, CO_ANDY.d2dCoZoneDefBonus]);
return text;
};
Predictably, the long CO description, but this time with numbers!
var text is just a temporary placeholder, the name isn't important
You can join strings to each other with the + operator. You don't have to do this, but here it makes the text easier to read (code-side, there should be no difference on screen). \n is the newline symbol, which makes a newline. You can get more info on strings on google.
What's the %0% doing in the string? It's used by the replaceTextArgs to substitute the proper bonus numbers into the description. Specifically, %0% says "insert the thing in index 0 of the list of objects here". In this case, it's easy to see that CO_ANDY.d2dCoZoneOffBonus is the first object in the list. Similar idea with %1%.
Doing this rather than hardcoding the numbers into the string makes it easier to change the stats of the CO without messing up the description. You can hardcode the numbers if you like, just remember they're there.
this.getPowerDescription = function(co)
{
var text = qsTr("Restores +%0 HP to all of Andy's units. His units gain +%1% firepower and +%2% defence.");
text = replaceTextArgs(text, [CO_ANDY.powerHeal, CO_ANDY.powerOffBonus, CO_ANDY.powerDefBonus]);
return text;
};
this.getPowerName = function(co)
{
return qsTr("Hyper Repair");
};
Power description and power name. What else is there to say.
this.getSuperPowerDescription = function(co)
{
var text = qsTr("Restores +%0 HP to all of Andy's units. His units gain +%2 movement, +%1% firepower, and +%3% defence.");
text = replaceTextArgs(text, [CO_ANDY.superPowerHeal, CO_ANDY.superPowerOffBonus, CO_ANDY.superPowerMovementBonus, CO_ANDY.powerDefBonus]);
return text;
};
this.getSuperPowerName = function(co)
{
return qsTr("Hyper Upgrade");
};
The super power. Spicy I know
this.getPowerSentences = function(co)
{
return [qsTr("I've got parts to spare!"),
qsTr("I'm not giving up!"),
qsTr("Time to roll up my sleeves!"),
qsTr("I haven't even cranked the engine yet!"),
qsTr("Pass me my wrench!"),
qsTr("It's time for a tune-up!"),
qsTr("Never give up, and never lose! I'm on my way!"),
qsTr("I'm not worried! I can fix anything!")];
};
When you use a power, the CO says something before actually using it. What that something is comes from here. Just return a list of strings (using []), the hard part is thinking of what to say.
this.getVictorySentences = function(co)
{
return [qsTr("We won! Woohoo!"),
qsTr("I can fix anything!"),
qsTr("I did it! Did you see that!?")];
};
this.getDefeatSentences = function(co)
{
return [qsTr("Oh, come on!"),
qsTr("Next time I see you, you're in trouble!")];
};
There are a surprising number of words that go into making a CO aren't there?
this.getName = function()
{
return qsTr("Andy");
};
}
Just to confirm, we are in fact looking at Andy's code.
Also the closing bracket of the original Constructor function. How long ago was that?
Now for two vital lines of code:
Constructor.prototype = CO;
var CO_ANDY = new Constructor();
We set the type of constructor to CO. This tells COW that this is a CO and not some weirdly shaped unit.
Then we set the var CO_ANDY to a new instance of Constructor. This makes CO_ANDY and actual CO object, and not just the blueprints of how to make Andy. CO_ANDY is where COW (and other scripts) will reference your CO from now on.
And with those two lines, we are finished!
Endnote
If you made it to the end, congrats! You now know the details of how Andy's code works and (hopefully) how to make your own.
What's that? You don't know how to make stuff affect different kinds of units?
Uh...
Maybe I should do this for Max as well.
Anyways, that's it for this guide. Have a nice day!