First campaign javascript dissected
Posted: 15 Dec 2012, 15:00
This is a walk-through of the port of the first campaign mission to javascript to review the API. I've pasted the entire script below, interrupted only with my comments.
Any code needs some globals. Nothing very special here.
The special gameOverMessage() function launches the familiar end-of-game dialog in the UI. If true, you won.
Ok, now we are getting somewhere interesting. This event is called from the scripting environment whenever we enter the first labelled area on the map, which includes the first oil derrick. The area is defined in 'labels.ini', and the function that is called has the name 'eventArea' + the name of the label.
We send all the scavs we find in this area to attack the player's starting position. Notice also the use of hackMarkTiles() to make the next active area pulse red when debugging.
This is the second active area. All it does is remove the blinking red dot on the map.
This event is called if we enter cheat mode, and it will highlight the current active label area. There are only two of them on this map.
This function is called when we detect victory, and is where we enable the bonus tech for level end, and give bonus power depending on how much time was left on our mission timer. Then we load the next level.
We listen to chat messages to see if we receive any special cheat messages meant for debugging.
This is where we decide victory or loss. I decided not to check for every baba, just in case one of them had run and hid in a corner somewhere.
This is the code that is run whenever a predefined group of objects have all been destroyed. The military objects at each base locations are in their own groups, and these are the ones we check above. Notice how I implement a two second delay after base destruction before adding the next radar blip by queueing another function. I'm not sure if this is really noticeable, but it seemed right.
This sets up the whole level. Most of it should be pretty much self-explanatory. Notice the way we add labels to the artifacts we create. These stay with the object, and will be queried later when a crate is picked up. Notice also that we have no coordinates or object IDs hard-coded within the script in any place, as is very frequent in campaign wzscript. All such numbers are now in 'labels.ini', which can be edited with a map editor.
This is the code that sets up the mission time when we get power income.
This is the code that handles crate pickup. Notice that crates are not removed automatically (this is the same way as done in the wzscript code). The way we identify which crate is picked up is by reading off the labels added to these crates above in the level start event.
The above function is quite generic, and is probably present in every script. It notifies the player if his base is under attack.
I think the above API and approach should allow us to port the campaign quite quickly, and the resulting scripts should be both short and readable. Any suggestions for improvements?
Code: Select all
var timercoming = 0; // start mission timer after first power gen and derrick is built
var lastHitTime = 0;
var scav1group, scav2group, scav3group, scav4group; // premade groups
var cheatmode = false;
var stage = 0;
var numArtifact = 0;
var base1destroyed = false;
var base2destroyed = false;
var base3destroyed = false;
var base4destroyed = false;
Code: Select all
function gameLost()
{
gameOverMessage(false);
}
Code: Select all
// player zero's droid enteres this area
function eventAreaLaunchScavAttack(droid)
{
stage++;
var spos = label("scav1soundpos");
playSound("pcv375.ogg", spos.x, spos.y, 0);
playSound("pcv456.ogg");
hackAddMessage("MB1A_MSG", MISS_MSG, 0, true);
hackAddMessage("C1A_OBJ1", PROX_MSG, 0, false);
hackMarkTiles(); // clear any marked tiles from debugging
var droids = enumArea("ScavAttack1", ALL_PLAYERS, false);
// send scavengers on war path if triggered above
var startpos = label("playerBase");
for (var i = 0; i < droids.length; i++)
{
if ((droids[i].player == 7 || droids[i].player == 6) && droids[i].type == DROID)
{
orderDroidLoc(droids[i], DORDER_SCOUT, startpos.x, startpos.y);
}
}
if (cheatmode)
{
hackMarkTiles("ScavAttack1"); // mark next area
}
}
We send all the scavs we find in this area to attack the player's starting position. Notice also the use of hackMarkTiles() to make the next active area pulse red when debugging.
Code: Select all
// player zero's droid enteres this area
function eventAreaScavAttack1(droid)
{
stage++;
hackRemoveMessage("C1A_OBJ1", PROX_MSG, 0);
hackMarkTiles(); // clear marks
hackAddMessage("C1A_BASE0", PROX_MSG, 0, false);
}
Code: Select all
function eventCheatMode(entered)
{
cheatmode = entered; // remember this setting
if (entered)
{
if (stage == 0)
{
hackMarkTiles("LaunchScavAttack");
}
else if (stage == 1)
{
hackMarkTiles("ScavAttack1");
}
else
{
hackMarkTiles(); // clear marks
}
}
else
{
hackMarkTiles(); // clear any marked tiles
}
}
Code: Select all
// proceed to next level
function gameWon()
{
enableResearch("R-Wpn-MG1Mk1"); // bonus research topic on level end
var bonusTime = missionTime();
if (bonusTime > 0)
{
setPowerModifier(125); // 25% bonus to completing fast
extraPowerTime(bonusTime);
setPowerModifier(100);
}
loadLevel("CAM_1B");
}
Code: Select all
// listen to chat messages from player
function eventChat(from, to, message)
{
if (message == "let me win" && cheatmode)
{
enableResearch("R-Wpn-MG-Damage01");
enableResearch("R-Sys-Engineering01");
enableResearch("R-Defense-Tower01");
enableResearch("R-Wpn-Flamer01Mk1");
var artifact = label("artifact");
if (artifact)
{
removeObject(artifact);
}
queue("gameWon");
}
else if (message == "status" && cheatmode)
{
console("numArtifact = " + numArtifact);
console("stage = " + stage);
}
}
Code: Select all
// things that need checking every second
function tick()
{
// check if game is lost
var factories = countStruct("A0LightFactory") + countStruct("A0CyborgFactory");
var droids = countDroid(DROID_CONSTRUCT);
if (droids == 0 && factories == 0)
{
queue("gameLost", 4000); // wait 4 secs before throwing the game
}
// check if game is won
var hostiles = countStruct("A0BaBaFactory", 6) + countStruct("A0BaBaFactory", 7)
+ countDroid(DROID_ANY, 6) + countDroid(DROID_ANY, 7);
if (!hostiles && numArtifact >= 4 && stage >= 6)
{
queue("gameWon", 6000); // wait 6 secs before giving it
}
}
Code: Select all
function showbase2()
{
if (!base2destroyed)
{
hackAddMessage("C1A_BASE1", PROX_MSG, 0, false);
var spos = label("scav2soundpos");
playSound("pcv374.ogg", spos.x, spos.y, 0);
}
}
function showbase3()
{
if (!base3destroyed)
{
hackAddMessage("C1A_BASE2", PROX_MSG, 0, false);
var spos = label("scav3soundpos");
playSound("pcv374.ogg", spos.x, spos.y, 0);
}
}
function showbase4()
{
if (!base4destroyed)
{
hackAddMessage("C1A_BASE3", PROX_MSG, 0, false);
var spos = label("retreat4");
playSound("pcv374.ogg", spos.x, spos.y, 0);
}
}
function eventGroupLoss(obj, groupid, newsize)
{
var leftovers;
if (groupid == scav1group && newsize == 0)
{
// eliminated scav base 1
leftovers = enumArea("scavbase1area");
hackRemoveMessage("C1A_BASE0", PROX_MSG, 0);
var spos = label("scav1soundpos");
playSound("pcv391.ogg", spos.x, spos.y, 0);
queue("showbase2", 2000);
base1destroyed = true;
stage++;
}
else if (groupid == scav2group && newsize == 0)
{
// eliminated scav base 2
leftovers = enumArea("scavbase2area");
hackRemoveMessage("C1A_BASE1", PROX_MSG, 0);
var spos = label("scav2soundpos");
playSound("pcv392.ogg", spos.x, spos.y, 0);
queue("showbase3", 2000);
base2destroyed = true;
stage++;
}
else if (groupid == scav3group && newsize == 0)
{
// eliminated scav base 3
leftovers = enumArea("scavbase3area");
hackRemoveMessage("C1A_BASE2", PROX_MSG, 0);
var spos = label("scav3soundpos");
playSound("pcv392.ogg", spos.x, spos.y, 0);
queue("showbase4", 2000);
base3destroyed = true;
stage++;
}
else if (groupid == scav4group && newsize == 0)
{
// eliminated scav base 4
leftovers = enumArea("scavbase4area");
hackRemoveMessage("C1A_BASE3", PROX_MSG, 0);
var spos = label("retreat4");
playSound("pcv392.ogg", spos.x, spos.y, 0);
stage++;
base4destroyed = true;
}
// if scav group gone, nuke any leftovers, such as scav walls
for (var i = 0; leftovers && i < leftovers.length; i++)
{
if (((leftovers[i].player == 6 || leftovers[i].player == 7) && leftovers[i].type == STRUCTURE)
|| (leftovers[i].type == FEATURE && leftovers[i].stattype == BUILDING))
{
removeObject(leftovers[i], true); // remove with special effect
}
}
}
Code: Select all
function addartifact(poslabel, artilabel)
{
var artpos = label(poslabel);
var artifact = addFeature("Crate", artpos.x, artpos.y);
addLabel(artifact, artilabel);
}
function eventStartLevel()
{
var startpos = label("startPosition");
var lz = label("landingZone");
scav1group = label("scavgroup1").id;
scav2group = label("scavgroup2").id;
scav3group = label("scavgroup3").id;
scav4group = label("scavgroup4").id;
centreView(startpos.x, startpos.y);
setNoGoArea(lz.x, lz.y, lz.x2, lz.y2, 0);
setPower(1300);
// allow to build stuff
setStructureLimits("A0PowerGenerator", 5, 0);
setStructureLimits("A0ResourceExtractor", 200, 0);
setStructureLimits("A0ResearchFacility", 5, 0);
setStructureLimits("A0LightFactory", 5, 0);
setStructureLimits("A0CommandCentre", 1, 0);
enableStructure("A0CommandCentre", 0);
enableStructure("A0PowerGenerator", 0);
enableStructure("A0ResourceExtractor", 0);
enableStructure("A0ResearchFacility", 0);
enableStructure("A0LightFactory", 0);
makeComponentAvailable("MG1Mk1", me); // needs to be done this way so doesn't enable rest of tree!
completeResearch("R-Vehicle-Body01", me);
completeResearch("R-Sys-Spade1Mk1", me);
completeResearch("R-Vehicle-Prop-Wheels", me);
// give player briefing
hackAddMessage("CMB1_MSG", CAMP_MSG, 0, false);
setReinforcementTime(-1);
setMissionTime(-1);
// Add artifacts
addartifact("artifact4pos", "artifact1");
addartifact("artifact1pos", "artifact2");
addartifact("artifact3pos", "artifact3");
addartifact("artifact2pos", "artifact4");
setTimer("tick", 1000);
}
Code: Select all
function eventStructureBuilt(structure, droid)
{
if (structure.stattype == POWER_GEN)
{
timercoming++;
}
else if (structure.stattype == RESOURCE_EXTRACTOR)
{
timercoming++;
}
if (timercoming == 2)
{
setMissionTime(3600);
}
}
Code: Select all
function eventPickup(feature, droid)
{
if (feature.stattype != ARTIFACT)
{
return; // not interested!
}
playSound("pcv352.ogg", feature.x, feature.y, feature.z);
var lab = getLabel(feature);
removeObject(feature); // artifacts are not self-removing...
if (lab == "artifact1") // first artifact
{
enableResearch("R-Wpn-MG-Damage01");
numArtifact++;
}
else if (lab == "artifact2") // second artifact
{
enableResearch("R-Sys-Engineering01");
numArtifact++;
}
else if (lab == "artifact3") // third artifact
{
enableResearch("R-Defense-Tower01");
numArtifact++;
}
else if (lab == "artifact4") // final artifact
{
enableResearch("R-Wpn-Flamer01Mk1");
numArtifact++;
}
else
{
debug("Bad artifact found in cam1a!");
}
}
Code: Select all
// /////////////////////////////////////////////////////////////////
// WARNING MESSAGES
// Base Under Attack
// FIXME -- if this is present in every script, put it in rules.js instead?
function eventAttacked(victimObj, attackerObj)
{
if (gameTime > lastHitTime + 5000)
{
lastHitTime = gameTime;
if (victimObj.type == STRUCTURE)
{
playSound("pcv337.ogg", victimObj.x, victimObj.y, victimObj.z); // show position if still alive
}
else
{
playSound("pcv399.ogg", victimObj.x, victimObj.y, victimObj.z);
}
}
}
I think the above API and approach should allow us to port the campaign quite quickly, and the resulting scripts should be both short and readable. Any suggestions for improvements?