Classes for common objects

Background

Two new optional properties were just added to droids: .cargoSize and .cargoUsed

On non-transporter droids, those properties will not be defined (as in the property itself will not exist).

In Javascript, when you try to access the value of a property that doesn't exist, you get an error. So you have to do some extra code for such properties to first check that the property is defined, and only then try to access its value.

I was going to suggest to Per that the properties always exist, but with a value of null for non-transport objects.

But...

Droid objects are already large, closely followed by structure and feature objects. And the new template object is also pretty big.

When a script uses an enum*() function, large lists of these objects have to be composed by the C++ code and delivered to the JS environment. And, in many cases, the results of that work is used just once and then the objects are discarded. It's like capitalist consumerism applied to Warzone scripting – use once, then throw away. It's not good for the environment (tongue)

Example properties

When we look at the Droid object, we can find a whole bunch of properties that can be considered "optional". That is to say, if they don't get added to the object, we can safely assume their value to be false or null. The list of such properties on droid objects is as follows:

  • group
  • armed
  • isVTOL
  • isRadarDetector
  • isCB
  • isSensor
  • canHitAir
  • canHitGround
  • hasIndirect
  • range
  • cargoSlots
  • cargoUsed

That's quite a lot of properties, eh?

For each of those properties, there is already an 'if' statement, and in most cases accompanied by an 'else' to set the default value.

Defaulting the values

We can provide defaults to all these values using prototypal inheritance – a standard feature of Javascript. It's very fast and easy to work with, and it would offer some other compelling benefits to scripters that I'll discuss later.

First, we need to define a class in the JS environment. It's important that this class is exposed to scripts, they will want to access it!

Creating the superlcass (I'm using JS here because I don't know C++)...
_global.WZDroid = function WZDroid() {}; // *must* be a function, not an object
 
WZDroid.prototype.type = DROID; // no need to specify .type on droid instances again!
 
// Then repeat for each of the other properties listed earlier:
WZDroid.prototype.<property> = <defaultValue>; // where defaultValue is either true or null (as applicable)

We do this at the very start of the game when the scripting environment is created, before loading any .js files in to that environment.

Notes:

  • It will be a massive help to scripters if the class function is named (ie. the function should have an immutable '.name' property) – this is so that the typeof operator works properly.
  • The class must be accessible to scripts running in the environment – this is so that the instanceof operator works properly.

So, we now have a function called WZDroid (the class) and we've defined defaults for the various "optional" droid properties on WZDroid.prototype. Now we need to make all the droid objects use this superclass.

In the JS API, there is a convDroid() function – this is where the inheritance of the WZDroid superclass will take place.

First, for each of the "optional" properties processed in convDroid(), remove the 'else' blocks – we no longer create the property if it will be using it's default value.

For example, the .range property gets changed so it looks like this:

Example of removing falsey properties...
	if (range >= 0)
	{
		value.setProperty("range", range, QScriptValue::ReadOnly);
	}
//	else
//	{
//		value.setProperty("range", QScriptValue::NullValue);
//	}

Before returning the droid object, the following tweaks need making (and yes, this is my first ever attempt at C++ so excuse any bugs!):

  • value.setProperty( "__proto__", engine->globalObject().property("WZDroid").property("prototype") );
  • value.property("prototype").setProperty( "constructor", engine->globalObject().property("WZDroid") );

As far as Javascript is concerned, the droid object is now an instance of the WZDroid class. (smile)

This should reduce the number of properties on each droid instance, and significantly reduce the amount of data returned from enum*() functions.

But we can probably now make some additional optimisations, for example:

Pseudo-code...
if (vtol droid) {
	set armed (only if droid has a weapon)
	set isVTOL
	if (transporter) { // I assume transports are a subclass of vtol?
		set cargoSlots
		set cargoUsed
	}
}
if (isWeapon) {
	set canHitAir (only if true)
	set canHitGround (only if true)
	set hasIndirect (only if true)
    set range
} else {
	set isRadarDetector (only if true)
	set isCB property (only if true)
	set isSensor (only if true)
    set range? // would be great if there were separate .sensorRange / .weaponRange properties ;)
}

So, if the droid isn't a weapon, no need to even check what it's weapon is capable of because we don't need to bother about creating those falsey properties any more (they will default to false or null due to droid inheriting WZDroid over on the JS side of things).

Rinse, wash, repeat

We can apply the same technique to the following objects:

  • Structure object – WZStructure class defaults:
    • .type = STRUCTURE
    • .status = BUILT
    • .stattype = DEFENSE
    • .modules = null
    • .group = null
    • .isSensor = false
    • .isCB = false
    • .isRadarDetector = false
    • .isECM = false (not sure if this prop even exists yet?)
    • .canHitAir = flase
    • .canHitGround = false
    • .hasIndirect = false
  • Feature object – WZFeature class defaults:
    • .type = FEATURE
    • .player = 99 (only oil drums, artifacts and oil resources will need to specify a player at instance level)
    • .damageable = true
    • .group = null
    • .stattype = null?
  • Template object – I've only just noticed this was added, but it too should be able to set some defaults:
    • .type = TEMPLATE_DATA?  (looks like there is no .type for this object yet!)
    • .brain = null?
    • .repair = null?
    • .ecm = null?
    • .sensor = null?
    • .construct = null?

Going even further - base object

Droids, Structures and Features all inherit from the 'base obect', or what I tend to refer to as a Game object, so it makes sense to have the WZDroid, WZStructure and WZFeature classes all have a WZGameObject superclass.

First, we need to define WZGameObject within the scripting environment:

WZGameObject (again, using JS as I don't know C++)...
_global.WZGameObject = function() {};
 
WZGameObject.prototype.selected = false;

While this might seem a lot of extra work to default just one property, there are huge benefits to us scripters which I'll explain later.

Then we can apply the superclass to our classes...

Putting it all together...
// remember, this only happens once, when scripting engine initialised
 
_global.WZGameObject = function() {};
WZGameObject.prototype.selected = false;
 
_global.wzProto = new WZGameObject;
 
_global.WZDroid = function WZDroid() {};
WZDroid.prototype.__proto__ = wzProto;
WZDroid.prototype.type = DROID;
WZDroid.prototype.<property> = <defaultValue>; // for each default property
 
_global.WZStructure = function WZStructure() {};
WZStructure.prototype.__proto__ = wzProto;
WZStructure.prototype.type = STRUCTURE;
WZStructure.prototype.<property> = <defaultValue>; // for each default property
 
_global.WZFeature = function WZFeature() {};
WZFeature.prototype.__proto__ = wzProto;
WZFeature.prototype.type = FEATURE;
WZFeature.prototype.<property> = <defaultValue>; // for each default property

From the scirpters' perspective...

Note: These are just super-basic examples for illustration purposes. Things get much more exciting with accessor properties (getter/setters). Anyway...

Scripting paradise...
// typeof and instanceof operators suddenly become useful!
someDroid instanceof WZGameObject // true (fast way to check if object is a game object)
someDroid instanceof WZDroid // true (faster than someDroid.type == DROID)
typeof someDroid // "WZDroid"
 
// we can now create our own methods for game objects!!
WZDroid.attackObj = function(obj) {
	if (this.droidType != DROID_WEAPON) return; // only weapons can attack
	// ...could also check health level, not going for repairs, etc.
	orderDroidObj(this, DORDER_ATTACK, obj);
}
 
someDroidObj.attack(someEnemyDroid); // :)
 
// which in turn let's us do stuff like this...
 
var goAttack = function attackObjWith(myDroid) {
	myDroid.attackObj(this);
}
 
function eventAttacked(victim, enemyTarget) {
	enumRange(victim.x, victim.y, 10, ALLIES).forEach( goAttack, enemyTarget );
}
 
// or imagine stuff like this...
 
WZDroid.huntForOil = function() { .... };
WZDroid.scoutArea = function(areaObj) { .... };
 
// starting to make more sense now? :)
 
// we can add our own methods to all three types of object:
wzProto.transferTo = function(player) {
	if (this instanceof WZFeature) return; // see, using it already!
	donateObject(this, player); // assuming this will soon handle structure objects as well
}
 
someFeature.transferTo(2); // does nothing
someStructure.transferTo(2); // transfers to player 2
someDroid.transferTo(2); // transfers to player 2

Thinking to the future...

The game objects are likely to grow some more as time goes on. Most of the time, scripts are only looking at a set of specific properties per enum*(), a lot of data is being put in to those objects and then not getting used.

Once we have the objects inheriting classes, we could actually define getter/setter properties on the class.

For example:

Yes, I really must learn C++, but until then, here it is in JS...
// this is how to define a getter/setter in JS, no idea how it's done in C++
 
Object.defineProperty(
	WZDroid.prototype,
	"isVTOL",
	{
		get: function() {
			try {
				return isVTOL(this);
			} catch(e) { /* ignore errors */ }
		},
		enumerable: true, 
		configurable: false
	}
);

This means that the C++ convDroid() function no longer creates a .isVTOL property. If the calling script access the <someDroid>.isVTOL property, it runs the get() function and returns its value.

Now, I'm not suggesting we do this any time soon. Much thought is required before taking this extra step. For example, if in eventDestroyed(victim) the script did "if (victim.isVTOL) ..." the JS APIs isVTOL function will fail because the droid is no longer on the map (I learnt this the hard way while working on the Enhanced SitRep mod).

But there might be some properties that could be pushed on to the WZDroid class in this manner, for example .action (never used in eventDestroyed, and rarely used anywhere else – although nutters like me do use it so don't remove it please!!). Another one is .armed – you don't care if a destroyed droid was armed or not heh. A final one is .range – again, nobody cares about this on a dead droid.

With just those three properties moved from instances to the WZDroid prototype, as a getter/setter properties (but with no setter, they're read-only), that could amount to tens of thousands of fewer property definitions in the space of a game (eg. 1500 droids on map, enumDroid() being called on a regular basis over a 10+ minute period...). 

Anyway, I'll leave it there. The main desire is to get default static values put on to classes, this last section is just a ponder of where things could go in the distant future.