Line 126: Line 126:


==Movement and Actions==
==Movement and Actions==
This code sets the movement type of the unit. You may also choose among other [https://github.com/Robosturm/Commander_Wars/tree/98fb5833b1a0ab6378589e251a43d276462c66b2/resources/scripts/movementtables movement types].
This code sets the movement type of the unit. You may also choose among other [https://github.com/Robosturm/Commander_Wars/tree/98fb5833b1a0ab6378589e251a43d276462c66b2/resources/scripts/movementtables movement types]
 
<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
  this.getMovementType = function()
  this.getMovementType = function()
Line 159: Line 160:
If these are in the game, one only needs to pass the name of the wav file as a parameter.  
If these are in the game, one only needs to pass the name of the wav file as a parameter.  


alternatively, you may create a '''sounds''' folder in the root of the mod, and place a wav file there. If you do, you only pass the name of the wav file as well.  
alternatively, you may create a '''sounds''' folder in the root of the mod, and place a wav file there. If you do, you only pass the name of the wav file as well.


==Name and unit description==
==Name and unit description==

Revision as of 22:06, 12 September 2023

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

Setup

In this tutorial, you will learn how to create a custom Mech Unit with the movement range of Infantry and 6000g cost, with the ability to hide, which we will call 'Night Stalker'. The Night Stalker unit can only be attacked by other Infantry Units while hidden.

To start off, create a folder named nightstalker inside the mods folder of the game, and inside of it the following folder and file structure:

.
├── mod.txt
└── scripts
    ├── battleanimations
    │   └── BATTLEANIMATION_NIGHT_STALKER.js
    ├── building
    │   └── factory.js
    └── units
        └── night_stalker.js

This file structure is important as the game detects the folders to make changes to the game.

Inside the mod.txt add the following:

name=Night Stalkers
description=Custom Mech Units, created for the wiki tutorial.
version=1.0.0
compatible_mods=
incompatible_mods=
required_mods=
cosmetic=false

This makes the mod visible to the game under Options>Mods

Initializing the unit

Initialize the code of the Night Stalker by putting this inside the night_stalker.js:

  var Constructor = function()
{
    this.init = function(unit)
    {
        unit.setAmmo1(10);
        unit.setMaxAmmo1(10);
        unit.setWeapon1ID("WEAPON_MECH_MG");

        unit.setAmmo2(3);
        unit.setMaxAmmo2(3);
        unit.setWeapon2ID("WEAPON_BAZOOKA");

        unit.setFuel(70);
        unit.setMaxFuel(70);
        unit.setBaseMovementPoints(3);
        unit.setMinRange(1);
        unit.setMaxRange(1);
		unit.setVision(2);
    };
}

Constructor.prototype = UNIT;
var NIGHT_STALKER = new Constructor();

The setWeapon1ID() and setWeapon2ID() can take a string of any of the available weapons in the game. One can also create a custom weapon, but that will not be covered in this tutorial.

Adding the Army Data and Unit Costs

Up next we add the base costs as well as the Army data of the unit

this.getBaseCost = function()
    {
        return 6000;
    };

    this.armyData = [["os", "os"],
                     ["bm", "bm"],
                     ["ge", "ge"],
                     ["yc", "yc"],
                     ["bh", "bh"],
                     ["bg", "bg"],
                     ["ma", "ma"],
                     ["ac", "ac"],
                     ["bd", "bd"],
                     ["dm", "dm"],
                     ["gs", "gs"],
                     ["pf", "pf"],
                     ["ti", "ti"],];

    this.animationData = [["os", [1]],
                          ["bm", [1]],
                          ["ge", [1]],
                          ["yc", [1]],
                          ["bh", [1]],
                          ["bg", [2]],
                          ["ma", [2]],
                          ["ac", [2]],
                          ["bd", [2]],
                          ["dm", [2]],
                          ["gs", [2]],
                          ["pf", [2]],
                          ["ti", [2]],];

The array entries correspond to the names of the factions of the game. These can be customized as well, but won't be covered in this tutorial.

Loading Sprites

This part of the code loads the sprites of the unit. We will be using the same parameters as the sprites of the Mech unit.

this.loadSprites = function(unit)
    {
        // none neutral player
        var player = unit.getOwner();
        // get army name
        var armyName = Global.getArmyNameFromPlayerTable(player, MECH.armyData);
        // load sprites
        unit.loadSpriteV2("mech+" + armyName +"+mask", GameEnums.Recoloring_Matrix);
        unit.loadSpriteV2("mech+" + armyName, GameEnums.Recoloring_None);
    };
<syntaxhighlight>

==Movement and Actions==
This code sets the movement type of the unit. You may also choose among other [https://github.com/Robosturm/Commander_Wars/tree/98fb5833b1a0ab6378589e251a43d276462c66b2/resources/scripts/movementtables movement types]

<syntaxhighlight lang="javascript">
 this.getMovementType = function()
    {
        return "MOVE_FEET";
    };
    this.actionList = ["ACTION_STEALTH", "ACTION_UNSTEALTH","ACTION_FIRE", "ACTION_MISSILE", "ACTION_CAPTURE", "ACTION_JOIN", "ACTION_LOAD", "ACTION_WAIT", "ACTION_CO_UNIT_0", "ACTION_CO_UNIT_1"];

<syntaxhighlight>

The action array is the same as the original Mech unit, however by adding the '''ACTION_STEALTH''' and '''ACTION_UNSTEALTH''' these actions will be added as well to the unit in game.

==Walking animation==
We will also copy the Mech's walking animation, this shows when the unit is ordered to moved in game

<syntaxhighlight lang="javascript">
 this.doWalkingAnimation = function(action, map)
    {
        var unit = action.getTargetUnit();
        var animation = GameAnimationFactory.createWalkingAnimation(map, unit, action);
        // none neutral player
        var player = unit.getOwner();
        // get army name
        var armyName = Global.getArmyNameFromPlayerTable(player, MECH.armyData);
        var data = Global.getDataFromTable(armyName, MECH.animationData);
        animation.loadSpriteV2("mech+" + armyName + "+walk+mask", GameEnums.Recoloring_Matrix, data[0]);
        animation.setSound("moveboots.wav", -2);
        return animation;
    };

You may also choose to set a different sound for the walking animation. These are the available sounds in the game If these are in the game, one only needs to pass the name of the wav file as a parameter.

alternatively, you may create a sounds folder in the root of the mod, and place a wav file there. If you do, you only pass the name of the wav file as well.

Name and unit description

These functions set the name and unit description. The Description uses HTML syntax for highlighting

this.getName = function()
    {
        return qsTr("Night Stalker");
    };

    this.getDescription = function()
    {
        return qsTr("<r>Attack power high. Can </r><div c='#00ff00'>capture </div><r> bases and hide. </r><div c='#00ff00'>Vision +3 </div><r> when on mountains.</r>");
    };

Can move and fire

The following function defines if a unit can attack and move on the same turn. It's set to false for indirect units

this.canMoveAndFire = function()
    {
        return true;
    };
<syntaxhighlight>

==Setting the unit type==
This sets the group of units it belongs to, for a list of possible units type check [[:Category:Units|Unit Types]]

<syntaxhighlight lang="javascript">
this.getUnitType = function()
    {
        return GameEnums.UnitType_Infantry;
    };
<syntaxhighlight>

==Editor placement sounds==
This sets the sound a unit emits when it's put in a map, while using the map editor

<syntaxhighlight lang="javascript">
this.getEditorPlacementSound = function()
    {
        return "moveboots.wav";
    };

Like with the animation sound, you may use the available sounds in the game or place a sound in a new sounds folder, at the root of the mod.

Setting up the damage the unit receives

In this tutorial, we will make the unit receive the same damage as the vanilla Mech units, to do so we add:

 this.getUnitDamageID = function(unit, map)
    {
        return "MECH";
    };

However it is possible to set the damage the unit receives by creating a csv file. Won't be covered in this tutorial.

Creating the battle animation

The file BATTLEANIMATION_NIGHT_STALKER.js defines the behavior of the battle animations. The definition and setup of a custom battle animation, is beyond the scope of this tutorial. For simplicity, we will copy the contents of the Mech unit and modify it to match the constructor of the custom unit. The code of BATTLEANIMATION_NIGHT_STALKER.js is copied verbatim for reference:

var Constructor = function()
{
    this.getMaxUnitCount = function()
    {
        return 5;
    };
    this.armyData = [["ac", "ac"],
                     ["bd", "bd"],
                     ["bh", "bh"],
                     ["bg", "bg"],
                     ["bm", "bm"],
                     ["dm", "dm"],
                     ["ge", "ge"],
                     ["gs", "gs"],
                     ["ma", "ma"],
                     ["os", "os"],
                     ["pf", "pf"],
                     ["ti", "ti"],
                     ["yc", "yc"],];

    this.animationData = [["ac", ["bazooka_bm", Qt.point(20, 16), Qt.point(-50, 20), -90]],
                          ["bd", ["bazooka_bm", Qt.point(17, 16), Qt.point(-50, 20), -90]],
                          ["bh", ["bazooka_bh", Qt.point(15, 12), Qt.point(-50, 20), -90]],
                          ["bg", ["bazooka_bh", Qt.point(15, 12), Qt.point(-50, 20), -90]],
                          ["bm", ["bazooka_bm", Qt.point(17, 16), Qt.point(-50, 20), -90]],
                          ["dm", ["bazooka_ge", Qt.point(14, 9),  Qt.point(-50, 20), -90]],
                          ["ge", ["bazooka_ge", Qt.point(15, 14), Qt.point(-50, 20), -90]],
                          ["gs", ["bazooka_ge", Qt.point(14, 9),  Qt.point(-50, 20), -90]],
                          ["ma", ["bazooka_os", Qt.point(20, 10), Qt.point(-50, 20), -90]],
                          ["os", ["bazooka_os", Qt.point(18, 17), Qt.point(-50, 20), -90]],
                          ["pf", ["bazooka_os", Qt.point(17, 16), Qt.point(-50, 20), -90]],
                          ["ti", ["bazooka_yc", Qt.point(18, 17), Qt.point(-50, 20), -90]],
                          ["yc", ["bazooka_yc", Qt.point(19, 17), Qt.point(-50, 20), -90]],];

    this.getRiverString = function(unit)
    {
        var terrainId = "PLAINS";
        var terrain = unit.getTerrain();
        if (terrain !== null)
        {
            terrainId = unit.getTerrain().getTerrainID();
        }
        if (terrainId === "RIVER" ||
                terrainId === "DESERT_TRY_RIVER")
        {
            return "+river";
        }
        return "";
    };

    this.isMountain = function(terrainId)
    {
        if (terrainId === "MOUNTAIN" ||
            terrainId === "SNOW_MOUNTAIN" ||
            terrainId === "DESERT_ROCK" ||
            terrainId === "WASTE_MOUNTAIN")
        {
            return true
        }
        return false;
    };

    this.loadMoveInAnimation = function(sprite, unit, defender, weapon)
    {
        if (weapon === 1 || defender === null)
        {
            var count = sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
            var armyName = Global.getArmyNameFromPlayerTable(unit.getOwner(), BATTLEANIMATION_NIGHT_STALKER.armyData);
            var riverName = BATTLEANIMATION_NIGHT_STALKER.getRiverString(unit);            
            sprite.loadMovingSpriteV2("mech+" + armyName + riverName + "+walk+mask", GameEnums.Recoloring_Matrix, sprite.getMaxUnitCount(), Qt.point(-75, 5),
                                      Qt.point(65, 0), 600, false,
                                      1, 1);
            var spriteId = "mech+" + armyName + riverName + "+walk";
            if (sprite.existResAnim(spriteId))
            {
                sprite.loadMovingSprite(spriteId, false, sprite.getMaxUnitCount(), Qt.point(-75, 5),
                                        Qt.point(65, 0), 600, false,
                                        1, 1);
            }
            for (var i = 0; i < count; i++)
            {
                sprite.loadSound("infantry_move.wav", 5, i * BATTLEANIMATION.defaultFrameDelay);
            }
            BATTLEANIMATION.loadSpotterOrCoMini(sprite, unit, false);
        }
        else
        {
            BATTLEANIMATION_INFANTRY.loadMoveInAnimation(sprite, unit, defender, weapon);
        }
    };

    this.loadStopAnimation = function(sprite, unit, defender, weapon)
    {
        if (weapon === 1 || defender === null)
        {
            var terrainId = unit.getTerrain().getTerrainID();
            if (BATTLEANIMATION_NIGHT_STALKER.isMountain(terrainId))
            {
                BATTLEANIMATION_NIGHT_STALKER.loadStandingAnimation(sprite, unit, defender, weapon);
            }
            else
            {
                BATTLEANIMATION_NIGHT_STALKER.loadSprite(sprite, unit, defender, weapon, "+stop", 1, 0, 1);
            }
        }
        else
        {
            BATTLEANIMATION_INFANTRY.loadStopAnimation(sprite, unit, defender, weapon);
        }
    };

    this.loadSprite = function(sprite, unit, defender, weapon, ending, count, startFrame = 0, endFrame = 0)
    {
        var armyName = Global.getArmyNameFromPlayerTable(unit.getOwner(), BATTLEANIMATION_NIGHT_STALKER.armyData);
        var riverName = BATTLEANIMATION_NIGHT_STALKER.getRiverString(unit);
        var offset = Qt.point(-10, 5);
        sprite.loadSpriteV2("mech+" + armyName + riverName + ending + "+mask", GameEnums.Recoloring_Matrix,
                            BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount(), offset, count, 1, 0, 0,
                            false, false, 100, endFrame, startFrame);
        var spriteId = "mech+" + armyName + riverName + ending;
        if (sprite.existResAnim(spriteId))
        {
            sprite.loadSpriteV2(spriteId, GameEnums.Recoloring_None,
                                BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount(), offset, count, 1, 0, 0,
                                false, false, 100, endFrame, startFrame);
        }
        BATTLEANIMATION.loadSpotterOrCoMini(sprite, unit, false);
    };

    this.loadStandingAnimation = function(sprite, unit, defender, weapon)
    {
        if (weapon === 1 || defender === null)
        {
            BATTLEANIMATION_NIGHT_STALKER.loadSprite(sprite, unit, defender, weapon, "", 1);
        }
        else
        {
            BATTLEANIMATION_INFANTRY.loadStandingAnimation(sprite, unit, defender, weapon);
        }
    };

    this.loadFireAnimation = function(sprite, unit, defender, weapon)
    {
        var count = sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
        if (weapon === 1)
        {
            BATTLEANIMATION_NIGHT_STALKER.loadSprite(sprite, unit, defender, weapon, "+fire", 1, 0, 2);
            var player = unit.getOwner();
            var armyName = Global.getArmyNameFromPlayerTable(player, BATTLEANIMATION_NIGHT_STALKER.armyData);
            var data = Global.getDataFromTable(armyName, BATTLEANIMATION_NIGHT_STALKER.animationData);
            var weaponRes = data[0];
            var offset = data[1];
            sprite.loadMovingSprite(weaponRes, false, sprite.getMaxUnitCount(), offset,
                                    Qt.point(160, 0), 500, false,
                                    1, 1, -1);
            sprite.loadSprite("rocket_trailing_smoke",  false, BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount(), offset);
            offset = Qt.point(data[1].x - 55, data[1].y - 5);
            sprite.loadSprite("bazooka_launch",  false, BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount(), offset);
            for (var i = 0; i < count; i++)
            {
                sprite.loadSound("baazoka_fire.wav", 1, i * BATTLEANIMATION.defaultFrameDelay);
            }
        }
        else
        {
            BATTLEANIMATION_INFANTRY.loadFireAnimation(sprite, unit, defender, weapon);
        }
    };

    this.getFireDurationMS = function(sprite, unit, defender, weapon)
    {
        if (weapon === 1)
        {
            return 700 + BATTLEANIMATION.defaultFrameDelay * sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
        }
        else
        {
            return 600 - BATTLEANIMATION.defaultFrameDelay * sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
        }
    };

    this.loadStandingFiredAnimation = function(sprite, unit, defender, weapon)
    {
        if (weapon === 1)
        {
            BATTLEANIMATION_NIGHT_STALKER.loadSprite(sprite, unit, defender, weapon, "+fire", 1, 2, 2);
        }
        else
        {
            BATTLEANIMATION_INFANTRY.loadStandingAnimation(sprite, unit, defender, weapon);
        }
    };

    this.loadImpactUnitOverlayAnimation = function(sprite, unit, defender, weapon)
    {
        if (weapon === 0)
        {
            sprite.loadColorOverlayForLastLoadedFrame("#969696", 1000, 1, 300);
        }
        else
        {
            sprite.loadColorOverlayForLastLoadedFrame("#969696", 300, 2, 0);
        }
    };

    this.loadImpactAnimation = function(sprite, unit, defender, weapon)
    {
        var count = sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
        var i = 0;
        if (weapon === 1)
        {
            sprite.loadSprite("cannon_hit",  false, sprite.getMaxUnitCount(), Qt.point(0, 20),
                              1, 1.0, 0, 400, true);
            sprite.addSpriteScreenshake(8, 0.95, 800, 500);
            var player = unit.getOwner();
            var armyName = Global.getArmyNameFromPlayerTable(player, BATTLEANIMATION_NIGHT_STALKER.armyData);
            var data = Global.getDataFromTable(armyName, BATTLEANIMATION_NIGHT_STALKER.animationData);
            var weaponRes = data[0];
            sprite.loadMovingSprite(weaponRes, false, sprite.getMaxUnitCount(), Qt.point(127, 24),
                                    Qt.point(-127, 0), 400, true,
                                    1, 1, 0, 0, true);
            for (i = 0; i < count; i++)
            {
                sprite.loadSound("rocket_flying.wav", 1, 0);
                sprite.loadSound("impact_explosion.wav", 1, 200 + i * BATTLEANIMATION.defaultFrameDelay);
            }
        }
        else
        {
            var yOffset = 22;
            if (unit.getUnitType()  === GameEnums.UnitType_Air)
            {
                yOffset = 40
            }
            sprite.loadSprite("mg_hit",  false, sprite.getMaxUnitCount(), Qt.point(0, yOffset),
                              1, 1.0, 0, 0, true);
            BATTLEANIMATION.playMgImpactSound(sprite, unit, defender, weapon, count);
        }
    };

    this.getImpactDurationMS = function(sprite, unit, defender, weapon)
    {
        if (weapon === 0)
        {
            return 400 - BATTLEANIMATION.defaultFrameDelay + BATTLEANIMATION.defaultFrameDelay * sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
        }
        else
        {
            return 400 - BATTLEANIMATION.defaultFrameDelay + BATTLEANIMATION.defaultFrameDelay * sprite.getUnitCount(BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount());
        }
    };

    this.getPositionOffset = function(sprite, unit, terrain, unitIdx)
    {
        if (terrain !== null)
        {
            if (BATTLEANIMATION_NIGHT_STALKER.isMountain(terrain.getID()))
            {
                if (unitIdx >= 4)
                {
                    return Qt.point(-20 * (6 - unitIdx), 0);
                }
            }
        }
        return Qt.point(0, 0);
    };

    this.hasMoveInAnimation = function(sprite, unit, defender, weapon)
    {
        var terrainId = unit.getTerrain().getTerrainID();
        if (BATTLEANIMATION_NIGHT_STALKER.isMountain(terrainId))
        {
            return false;
        }
        else
        {
            // return true if the unit has an implementation for loadMoveInAnimation
            return true;
        }
    };
    this.getMoveInDurationMS = function()
    {
        return 610;
    };

    this.getStopDurationMS = function(sprite, unit, defender, weapon)
    {
        return 300 + BATTLEANIMATION.defaultFrameDelay * BATTLEANIMATION_NIGHT_STALKER.getMaxUnitCount();
    };

    this.getDyingDurationMS = function(sprite, unit, defender, weapon)
    {
        return 600;
    };

    this.hasDyingAnimation = function()
    {
        return true;
    };

    this.loadDyingAnimation = function(sprite, unit, defender, weapon)
    {
        if (weapon === 1)
        {
            var armyName = Global.getArmyNameFromPlayerTable(unit.getOwner(), BATTLEANIMATION_NIGHT_STALKER.armyData);
            var riverName = BATTLEANIMATION_NIGHT_STALKER.getRiverString(unit);
            var data = Global.getDataFromTable(armyName, BATTLEANIMATION_NIGHT_STALKER.animationData);
            var offset = Qt.point(-10, 5);
            var rotation = data[3];
            var movement = data[2];
            var ending = "";
            if (sprite.getHasFired())
            {
                ending = "+fire";
            }
            sprite.loadDyingMovingSprite("mech+" + armyName + riverName + ending + "+mask",
                                         "mech+" + armyName + "+dying+mask",
                                         GameEnums.Recoloring_Matrix,
                                         offset, movement, rotation, 400, 0, 2);
            var spriteId = "mech+" + armyName + riverName + ending;
            if (sprite.existResAnim(spriteId))
            {
                sprite.loadDyingMovingSprite(spriteId,
                                             "mech+" + armyName + "+dying",
                                             GameEnums.Recoloring_None,
                                             offset, movement, rotation, 400, 0, 2);
            }
            BATTLEANIMATION.loadSpotterOrCoMini(sprite, unit, false);
        }
        else
        {
            BATTLEANIMATION_INFANTRY.loadDyingAnimation(sprite, unit, defender, weapon);
        }
    };
};

Constructor.prototype = BATTLEANIMATION;
var BATTLEANIMATION_NIGHT_STALKER = new Constructor();

Adding the unit to the factories

To add the unit to the factory we will do so by adding the Night Stalker to the construction list, like this:

this.constructionList = ["INFANTRY", "MOTORBIKE", "SNIPER", "MECH", "RECON", "APC", "FLARE",
                             "ARTILLERY", "LIGHT_TANK", "FLAK", "ANTITANKCANNON", "HEAVY_TANK",
                             "NEOTANK", "MISSILE", "ROCKETTHROWER", "MEGATANK", "PIPERUNNER", "NIGHT_STALKER"];

The full code of the factory.js added to the mod is the following:

var Constructor = function()
{
    this.getBuildingGroup = function()
    {
        return 0;
    };
    this.loadSprites = function(building, neutral, map)
    {
        building.loadSpriteV2("factory+mask", GameEnums.Recoloring_Matrix, 400, Qt.point(0,0), "", neutral);
        if (building.getOwnerID() >= 0 && !neutral)
        {
            building.loadSpriteV2("factory", GameEnums.Recoloring_Matrix, 200, Qt.point(0,0), TERRAIN.getBuildingTerrainPalette(building));
        }
        else
        {
            building.loadSpriteV2("factory+neutral", GameEnums.Recoloring_Matrix, 400, Qt.point(0,0), TERRAIN.getBuildingTerrainPalette(building));
        }
    };
    this.getName = function()
    {
        return qsTr("Base");
    };
    this.addCaptureAnimationBuilding = function(animation, building, startPlayer, capturedPlayer)
    {
        animation.addBuildingSprite("factory+mask", startPlayer , capturedPlayer, GameEnums.Recoloring_Matrix);
    };

    this.actionList = ["ACTION_BUILD_UNITS"];

    this.constructionList = ["INFANTRY", "MOTORBIKE", "SNIPER", "MECH", "RECON", "APC", "FLARE",
                             "ARTILLERY", "LIGHT_TANK", "FLAK", "ANTITANKCANNON", "HEAVY_TANK",
                             "NEOTANK", "MISSILE", "ROCKETTHROWER", "MEGATANK", "PIPERUNNER", "NIGHT_STALKER"];
    this.getConstructionList = function(building)
    {
        return FACTORY.constructionList;
    };
    this.getTerrainAnimationForeground = function(unit, terrain, defender, map)
    {
        return "";
    };

    this.getTerrainAnimationBackground = function(unit, terrain, defender, map)
    {
        var weatherModifier = TERRAIN.getWeatherModifier(map);
        if (weatherModifier === "")
        {
            weatherModifier = TERRAIN.getTerrainWeatherModifier(terrain);
        }
        return "back_" + weatherModifier + "factory";
    };

    this.getDescription = function()
    {
        return qsTr("<r>Once captured can be used for </r><div c='#00ff00'>production and resupplying </div><r>of </r><div c='#00ff00'>ground </div><r>units.</r>");
    };

    this.getVisionHide = function(building)
    {
        return true;
    };

    this.getRepairTypes = function(building)
    {
        return [GameEnums.UnitType_Ground, GameEnums.UnitType_Infantry];
    };
	this.onWeatherChanged = function(building, weather, map)
	{	
		var weatherId = weather.getWeatherId();
		if (weatherId === "WEATHER_SNOW")
		{
			building.loadWeatherOverlaySpriteV2("factory+snow", false);
		};
    };
}

Constructor.prototype = BUILDING;
var FACTORY = new Constructor();

Activating the mod

On the main many of the game, go to Options>Mods then check the box that reads Night Stalkers the game will restart.