Andy Breakdown

Revision as of 18:26, 19 December 2023 by MrDuck557 (talk | contribs) (Power stuff finished. activatePower and activateSuperpower explained)

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.

THIS IS UNFINISHED DO NOT COMPLAIN ABOUT MISSING SECTIONS

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.