Custom Unit: Difference between revisions
No edit summary |
No edit summary |
||
(27 intermediate revisions by the same user not shown) | |||
Line 2: | Line 2: | ||
In case you want to learn more about Javascript, you can check the tutorial here: [https://www.youtube.com/watch?v=fju9ii8YsGs Derek Banas' Javascript tutorial] , purchase O'Reilly's excellent book on the subject [https://www.amazon.com/JavaScript-Definitive-Most-Used-Programming-Language/dp/1491952024/ JavaScript: The Definitive Guide], or check out [https://www.w3schools.com/js/ W3 Schools Interactive Tutorials] | In case you want to learn more about Javascript, you can check the tutorial here: [https://www.youtube.com/watch?v=fju9ii8YsGs Derek Banas' Javascript tutorial] , purchase O'Reilly's excellent book on the subject [https://www.amazon.com/JavaScript-Definitive-Most-Used-Programming-Language/dp/1491952024/ JavaScript: The Definitive Guide], or check out [https://www.w3schools.com/js/ W3 Schools Interactive Tutorials] | ||
'''You may download the files of this tutorial on [https://discord.com/channels/615556870064832533/688802481504911372/1151297536846463016 Discord]''' | |||
==Setup== | ==Setup== | ||
In this tutorial, you will learn how to create a custom | 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: | 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: | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="apacheconf"> | ||
~/mods/nightstalker | |||
├── mod.txt | ├── mod.txt | ||
└── scripts | └── scripts | ||
Line 18: | Line 19: | ||
└── night_stalker.js | └── night_stalker.js | ||
</syntaxhighlight> | </syntaxhighlight> | ||
This file structure is important as the game detects the folders to make changes to the game. | |||
Inside the '''mod.txt''' add the following: | |||
<syntaxhighlight lang="apacheconf"> | |||
name=Night Stalkers | |||
description=Custom Mech Units, created for the wiki tutorial. | |||
version=1.0.0 | |||
compatible_mods= | |||
incompatible_mods= | |||
required_mods= | |||
cosmetic=false | |||
</syntaxhighlight> | |||
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''': | |||
<syntaxhighlight lang="javascript"> | |||
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(); | |||
</syntaxhighlight> | |||
The '''setWeapon1ID()''' and '''setWeapon2ID()''' can take a string of any of the [https://github.com/Robosturm/Commander_Wars/tree/98fb5833b1a0ab6378589e251a43d276462c66b2/resources/scripts/weapons 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 | |||
<syntaxhighlight lang="javascript"> | |||
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]],]; | |||
</syntaxhighlight> | |||
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. | |||
<syntaxhighlight lang="javascript"> | |||
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; | |||
}; | |||
</syntaxhighlight> | |||
You may also choose to set a different sound for the walking animation. These are the available [https://github.com/Robosturm/Commander_Wars/tree/master/resources/sounds 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 | |||
<syntaxhighlight lang="javascript"> | |||
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>"); | |||
}; | |||
</syntaxhighlight> | |||
==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 | |||
<syntaxhighlight lang="javascript"> | |||
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"; | |||
}; | |||
</syntaxhighlight> | |||
Like with the animation sound, you may use the available [https://github.com/Robosturm/Commander_Wars/tree/master/resources/sounds 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: | |||
<syntaxhighlight lang="javascript"> | |||
this.getUnitDamageID = function(unit, map) | |||
{ | |||
return "MECH"; | |||
}; | |||
</syntaxhighlight> | |||
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: | |||
<syntaxhighlight lang="javascript"> | |||
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(); | |||
</syntaxhighlight> | |||
==Adding the unit to a building== | |||
To add the unit to the factory we will do so by adding the Night Stalker to the construction list inside the '''factory.js''', like this: | |||
<syntaxhighlight lang="javascript"> | |||
FACTORY.constructionList.push("NIGHT_STALKER"); | |||
</syntaxhighlight> | |||
In case you wanted to add it to a different type of building, you'd need to create the corresponding Javascript file. The following is a list of the buildings available in the game, from which you can build units: | |||
{| class="wikitable" style="margin:auto" | |||
|+ Available buildings | |||
|- | |||
! Building name !! Building constructor name !! Javascript file | |||
|- | |||
| Airport || AIRPORT || airport.js | |||
|- | |||
| Amphibious factory<ref>Used to deploy [[Hovercraft Units]]</ref>|| AMPHIBIOUSFACTORY || amphibiousfactory.js | |||
|- | |||
| Factory || FACTORY || factory.js | |||
|- | |||
| Harbour || HARBOUR || harbour.js | |||
|} | |||
==Activating the mod== | |||
On the main menu of the game, go to '''Options>Mods''' then check the box that reads '''Night Stalkers''' the game will restart. | |||
==Notes== | |||
<references/> | |||
[[Category:Units]] | |||
[[Category:Modding tutorials]] |
Latest revision as of 22:16, 26 November 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
You may download the files of this tutorial on Discord
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:
~/mods/nightstalker
├── 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);
};
Movement and Actions
This code sets the movement type of the unit. You may also choose among other movement types
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"];
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
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;
};
Setting the unit type
This sets the group of units it belongs to, for a list of possible units type check Unit Types
this.getUnitType = function()
{
return GameEnums.UnitType_Infantry;
};
Editor placement sounds
This sets the sound a unit emits when it's put in a map, while using the map editor
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 a building
To add the unit to the factory we will do so by adding the Night Stalker to the construction list inside the factory.js, like this:
FACTORY.constructionList.push("NIGHT_STALKER");
In case you wanted to add it to a different type of building, you'd need to create the corresponding Javascript file. The following is a list of the buildings available in the game, from which you can build units:
Building name | Building constructor name | Javascript file |
---|---|---|
Airport | AIRPORT | airport.js |
Amphibious factory[1] | AMPHIBIOUSFACTORY | amphibiousfactory.js |
Factory | FACTORY | factory.js |
Harbour | HARBOUR | harbour.js |
Activating the mod
On the main menu of the game, go to Options>Mods then check the box that reads Night Stalkers the game will restart.
Notes
- ↑ Used to deploy Hovercraft Units