First campaign javascript dissected

For AI and campaign script related discussions and questions

First campaign javascript dissected

Postby Per » 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.

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;

Any code needs some globals. Nothing very special here.

Code: Select all
function gameLost()
{
   gameOverMessage(false);
}

The special gameOverMessage() function launches the familiar end-of-game dialog in the UI. If true, you won.

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
   }
}

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.

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);
}

This is the second active area. All it does is remove the blinking red dot on the map.

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
   }
}

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.

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");
}

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.

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);
   }
}

We listen to chat messages to see if we receive any special cheat messages meant for debugging.

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
   }
}

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.

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
      }
   }
}

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.

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);
}

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.

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);
   }
}

This is the code that sets up the mission time when we get power income.

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!");
   }
}

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.

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);
      }
   }
}

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?
Per
Warzone 2100 Team Member
Warzone 2100 Team Member
 
Posts: 3609
Joined: 03 Aug 2006, 19:39

Re: First campaign javascript dissected

Postby aubergine » 15 Dec 2012, 15:09

eventAttacked situation reporting definitely needs to live in rules.js IMHO
"Dedicated to discovering Warzone artefacts, and sharing them freely for the benefit of the community."
-- https://warzone.atlassian.net/wiki/display/GO
User avatar
aubergine
Professional
Professional
 
Posts: 3459
Joined: 10 Oct 2010, 00:58

Re: First campaign javascript dissected

Postby Lord Apocalypse » 15 Dec 2012, 21:48

looks good to me so far. See my last post in the cam1 improvement thread for a few ideas possibly related to improving all the campaigns.
Lord Apocalypse
Regular
Regular
 
Posts: 678
Joined: 29 Jul 2009, 18:01

Re: First campaign javascript dissected

Postby Emdek » 15 Dec 2012, 23:06

I don't like two things about above code, lack of consistent use of camel case naming for variables and post-incrementation instead of pre-incrementation for variables used as counters. ;-)
Nadszedł już czas, najwyższy czas, nienawiść zniszczyć w sobie.
The time has come, the high time, to destroy hatred in oneself.


Beware! Mad Qt Evangelist.
User avatar
Emdek
Regular
Regular
 
Posts: 1329
Joined: 24 Jan 2010, 13:14
Location: Poland

Re: First campaign javascript dissected

Postby NoQ » 12 Oct 2014, 10:57

Managed to squash cam1a.slo, cam1a.vlo, cam1a-ai.slo, cam1a-ai.vlo into under 240 lines of code, splitting all re-usable stuff into a re-usable lib ("libcampaign.js"). Discuss?


Seems high-level enough for me to plan the next level for the next weekend.
So far, a few things are bothering me, before i forget:

  1. `eventChat` doesn't work, implementation of "let me win" is hence broken (and it wasn't me).
  2. I have to rely on the undocumented `isReceivingAllEvents` feature to make `eventDroidBuilt` work with AI factories. As it seems convenient to have one a single script manage game rules and campaign AIs.
  3. Savegame-compatibility kept in mind, but untested (didn't manage to load a campaign game on current master).
  4. While templates.ini for AI units is pointless for a skirmish AI, it is quite useful in campaign. Many templates would be re-used in many levels. A single json of all templates could be useful, and i can fill it in level by level, or rely on the old list somehow.
  5. Human templates are working weirdly. Instead of designing units, i just see them appear, as long as CC is up. Not sure what controls it (probably just auto-loaded from templates.ini when available?) and how it'd work on further levels.
  6. Timer is set to 4 hours, even though argument says 3600, no idea why, wasntme.
User avatar
NoQ
Special
Special
 
Posts: 6068
Joined: 24 Dec 2009, 11:35
Location: /var/zone

Re: First campaign javascript dissected

Postby Per » 12 Oct 2014, 12:15

Really nice!

I looked at the 'eventChat' problem. It is caused by a 'break' statement inside triggerEventChat() in src/qtscript.cpp ... I am not entirely sure why I added that. You can try removing it.

The template stuff is really weird, I agree. We really need to de-complicate it.
Per
Warzone 2100 Team Member
Warzone 2100 Team Member
 
Posts: 3609
Joined: 03 Aug 2006, 19:39

Re: First campaign javascript dissected

Postby Goth Zagog-Thou » 13 Oct 2014, 00:26

Oh good, it wasn't just me reading it incorrectly then. ^_^
User avatar
Goth Zagog-Thou
Regular
Regular
 
Posts: 1582
Joined: 06 Jan 2007, 08:08
Location: Delta Base

Re: First campaign javascript dissected

Postby NoQ » 14 Oct 2014, 07:41

Goth Zagog-Thou wrote:Once the dataset is ported, the next step is moving Campaign 4 over to the JS API. Now that I have a very solid base with the VLO/SLO Campaign, moving it to JS will be both a challenge and a joy. Not impossible by any means -- in fact, the hard part was building it on vlo/slo/wrf .. the "old way" -- and it will take some time, certainly. But the advantages of JS are so myriad .. I honestly find JS a slice of heaven to work with in comparison.

Yay, we seem to be in the same boat. Maybe we could think on sharing the libcampaign thing, with two users it may become more handy - just share the code patterns you often see in your code, and we'd try to find a way of re-using them in a D.R.Y. way.

It already supports stuff like "produce a fixed repeating list of stuff in a factory and send fixed waves to attack the player anywhere, but preferably near a given spot", or "make a group protect a certain location, pursuing enemies within radius but quickly falling back otherwise", with a single function call each; also convenient macros for debugging. If anything like that is useful and avoids double work, let me know.

If i ever advance beyond the first couple of levels, maybe i'd even get to commit it anywhere, so that the lib were trivially available in -master, and probably even documented together with the native API (Per, should i do that? I already have docs in comments, so i can just turn those into docstrings and fix the makefile).
User avatar
NoQ
Special
Special
 
Posts: 6068
Joined: 24 Dec 2009, 11:35
Location: /var/zone

Re: First campaign javascript dissected

Postby Goth Zagog-Thou » 14 Oct 2014, 08:38

I'm more than happy to participate in that. If it furthers the cause, I'm all for it.
User avatar
Goth Zagog-Thou
Regular
Regular
 
Posts: 1582
Joined: 06 Jan 2007, 08:08
Location: Delta Base

Re: First campaign javascript dissected

Postby Per » 14 Oct 2014, 20:16

NoQ wrote:(Per, should i do that? I already have docs in comments, so i can just turn those into docstrings and fix the makefile).

Sounds like a good idea to me.
Per
Warzone 2100 Team Member
Warzone 2100 Team Member
 
Posts: 3609
Joined: 03 Aug 2006, 19:39

Re: First campaign javascript dissected

Postby Goth Zagog-Thou » 15 Oct 2014, 02:51

Assign me a task, someone. Cam 1 Mission 2, perhaps? And yes, I know, no tainting the dataset. Original only. ;)

[EDIT] How about I go ahead and do a port of Mission 2 to JS, following your example of Mission 1 so that the coding style is similiar (using the latest Master from the Buildbot), and I submit it for "peer review" before it gets a commit? That way if anything looks amiss or funky it can be addressed and corrected openly. :)
User avatar
Goth Zagog-Thou
Regular
Regular
 
Posts: 1582
Joined: 06 Jan 2007, 08:08
Location: Delta Base

Re: First campaign javascript dissected

Postby Goth Zagog-Thou » 15 Oct 2014, 04:59

I'm reading the refactored cam1a.js and I like what I see. Lots of potential for a univeral libcampaign.js LOTS of potential indeed. I'm certainly game for experimenting with code snippets that can be standardized and reused. It would make the entire process of porting the Classic Campaign over to JS much easier and rapid.

The most recent version of 3.2 compiled for Windows is 1-Oct-2014 (the version I currently have installed), so I can get to work immediately trying out bits of code.

As far as my progress with getting the Warzone Unified Dataset ported into 3.2, this is a really good time for something like LibCampaign to come up. Here's why:

- The Multiplayer stuff is entirely ported into Campaign mode. Fortresses, Super-Heavy Bodies, the additional weapons introduced in patches, everything. The Campaign-related things necessary to make the Campaigns work should still be there; and if not, can be added back in.
- The Contingency/USM/Mini/Mechs/Cam4 stuff has, aside from the Bodies and Features, two weapons and two structures in. The rest is in Names.txt only, and can be removed easily. Likewise, the Bodies, Weapons, Structures, and related items can be removed quickly.

Why do I bring this up? I am in favor of a single dataset for both Campaign and Multiplayer, going forward. With the new API, we can script the Tech Trees for each flavor and make them consistent experiences. My own prototype implementation in Campaign 4, rough around the edges as it is, proves it can be done .. it just needs fine-tuning.

Of course, that can be saved for another conversation at another time. :)

I'm extremely motivated with the prospect of Libcampaign.js .. and I'm happy to lend a hand with it.
User avatar
Goth Zagog-Thou
Regular
Regular
 
Posts: 1582
Joined: 06 Jan 2007, 08:08
Location: Delta Base

Re: First campaign javascript dissected

Postby NoQ » 15 Oct 2014, 06:42

Hey hey calm down XD

I'm still thinking-through the whole organisation of this ... thing, the campaign refactoring. Of course i'd publish a call for help soon enough, as the amount of work is huge and requires completely different skill sets (setting up map labels might be half of it) But right now, Goth: from the depth of my inability of telling you what to do, my original idea was to provide the necessary stuff for you to refactor your campaign, of all things, and then absorb the feedback. Because (1) it'd double the amount of applications for libcampaign and assure feature-richness, as cam4 calls for a completely different gameplay, with different idioms to encapsulate; (2) it might actually be a much more useful of a project - as any touching of the original campaign scripts is essentially breaking and may encounter heavy opposition from game fans, which i'd plan to take on myself, and it might get never done even without such opposition, but on the other hand your campaign is destined to survive and prosper as a project.

Anyway, the draft i posted is very immature, i may still want to, sorta, rename all functions in it, so i'd request a bit more time (maybe saturday) to clean it up a bit and commit.
________________________

For the unified dataset: if you have one already working (with stuff such as R-Cam-Wpn-MG1Mk1 or Cam-Body1REC), and it's really a good and approved idea to make a single dataset for campaign and multiplayer, even though most of the stuff would still be duplicated in the set, then, yeah, really, now is the best time to merge it, since all data identifiers mentioned in the campaign would be updated manually and tested.
User avatar
NoQ
Special
Special
 
Posts: 6068
Joined: 24 Dec 2009, 11:35
Location: /var/zone

Re: First campaign javascript dissected

Postby Goth Zagog-Thou » 15 Oct 2014, 06:54

Yep, that particular item is there. I'm talking the more obscure stuff like Level 3 Missile Codes and such for Cam 3 Mission 11. Things like that.

Take all the time you need. There's no rush. We all have lives outside of Warzone 2100. ;)
User avatar
Goth Zagog-Thou
Regular
Regular
 
Posts: 1582
Joined: 06 Jan 2007, 08:08
Location: Delta Base

Re: First campaign javascript dissected

Postby Goth Zagog-Thou » 15 Oct 2014, 07:33

Oh, and as I said in the Campaign 4 thread, the efforts going forward (and until that work is completed) will be dedicated to the port to 3.2 -- and however long that takes is however long it takes. Refactoring the Campaign is part of that process, since it requires porting the vlo/slo to the new API.

I appreciate the encouragement with using Cam 4 as the "Test Platform" for libcampaign.js, and I wholly authorize its' use in that regard. I'll keep working on it as I'm able.
User avatar
Goth Zagog-Thou
Regular
Regular
 
Posts: 1582
Joined: 06 Jan 2007, 08:08
Location: Delta Base

Next

Return to Scripting