@Rman: Wow - thanks for all the info and links. I'll start reading through that this week depending on RL commitments.
So, an update on EggPlant. For the past few days I've been battling with class hierarchies and in particular proper OOP inheritance model in JS. Here's why...
I have a bunch of "Group" AIs - one that's good with VTOLs, one with construction trucks, one with feeder groups, and so on. Each of these AIs have a herd of similar features - such as "move group to x,y" or "retreat to nearest safe base" and so on.
There's some functions that are the same across all the group AIs, some that are the same across a few of them, and some that are unique or only relate to one or two group AIs. Now, I could just create named references to existing functions like this:
Code: Select all
function GroupAI1() { ... }
GroupAI1.prototype.doFoo = function() { ... }
function GroupAI2() { ... }
GroupAI2.prototype.doFoo = GroupAI1.doFoo;
In fact, that's how my AI script started out. But I kept running in to lots of fringe cases where I just wanted to extend an existing function (eg. wrap it in some other code) or whatever, or where I wanted to use practically everything from an existing Group AI but just override a few functions. As time went on, I was becoming more and more frustrated with growing mess inside my script caused by all these little things - it was like death by a thousand paper cuts.
So, I decided it was time to do some proper OOP where ClassA could inherit ClassB - this would give ClassA everything that ClassB has, but allow bits to be overriden to suit the needs of ClassA. And, because ClassA would still be able to call superclass (ClassB) methods (eg. from within a ClassA method of same name), it would make my code a lot cleaner and more understandable.
Key requirements are:
* Constructors (especially superclass ones) shouldn't get called unnecessarily - eg. during inheritance or instantiation.
** By default only the constructor of the instantiated class should get called, however it might choose to call it's superclass contructor and so on (using .epSuper() method)
* Functions should remain untouched (ie. not wrapped in crufty activation objects) when doing inheritance
* There should be no (or as close as) cruft added to objects, classes, instances, etc.
* Calling supertype methods/constructors should be a doddle, and not break if I change names of classes or functions, etc.
* Inheritance should happen very quickly and easily
* Changing superclass prototype should affect all classes based on it, including their instantiated instances
* Class inheritance != merging objects together! OMFG so many libraries use this approach!
* typeof and instanceof should work as expected
Let's say I have 3 classes - cChimp, cBar, and cFoo. I want cBar to inherit cChimp, and cFoo to inherit cBar (which has inherited cChimp):
Code: Select all
function cChimp(){this.name="Chimp"};
cChimp.prototype.c = function(){console("chimp (cChimp)")};
function cBar(){this.name="Bar"};
cBar.epInherit(cChimp);
cBar.prototype.b = function(){console("bar (cBar)");this.epSuper();};
cBar.prototype.f = function(){console("foo (cBar)")};
function cFoo(){this.name="Foo";this.epSuper();};
cFoo.epInherit(cBar);
cFoo.prototype.f = function(){console("foo (cFoo)");this.epSuper();};
(epInherit and epSuper are functions I've made which I'll describe later)
If I now make a new instance of cFoo, which I'll call 'foobarchimp' to illustrate that it contains those three classes, I want things to work this way:
Code: Select all
var foobarchimp = new cFoo();
console(foobarchimp.name); // = Bar - because cFoo constructor called .epSuper() = cBar constructor
foobarchimp.f(); // = foo (cFoo), foo (cBar)
foobarchimp.b(); // = bar (cBar)
foobarchimp.c(); // = chimp (cChimp)
For JS heads, the prototype chain should ideally look like this:
Code: Select all
foobarchimp // = {}
.name // = "Bar"
.prototype // = cFoo.prototype
.constructor // = function cFoo(){this.name="Foo";this.epSuper();};
.prototype // = Function.prototype
.f // = function(){console("foo (cFoo)");this.epSuper();};
.prototype // = cBar.prototype
.constructor // = function cBar(){this.name="Bar"};
.prototype // = Function.prototype
.b // = function(){console("bar (cBar)")};
.f // = function(){console("foo (cBar)")};
.prototype // = cChimp.prototype
.constructor // = function cChimp(){this.name="Chimp"};
.prototype // = Function.prototype
.c // = function(){console("chimp (cChimp)")};
.prototype // = Object.prototype
I thought "Simple - I'll just look at a bunch of JS libraries and grab one of their inherit/extend approaches to use in my AI script". This problem has been solved a million times by now, right?
OH THE PAIN!
I was utterly shocked to see how crappy most OOP is done in JS land these days. It's been 7 years since I was a JS developer, meaning that I've not written a line of code for 7 years until I started this AI project. I thought there'd be a bunch of awesome ways of dealing with proper OOP inheritance and stuff, but no.
Most of the existing libs out there make all sorts of crufty modifications to classes and their methods (functions) - like wrapping them in activation objects, or requiring hard-coded references to superclass methods and so on. And it seems I'm not the only one that's unimpressed with what's on offer today:
The JavaScript _super Bullshit (that post covers several of the issues I spotted with libraries I was looking at).
So, I started hunting around for more information, particularly from blog posts and websites. I eventually found
Understanding JavaScript Prototypes. which helped refresh my memory on a whole bunch of stuff I'd forgotten about, but it still left me with a sense of impending doom. However, there's a link at the bottom of that page to this masterclass of in-depth JS under-the-bonnet detailed knowledge:
ECMA-262-3 in detail. Chapter 7.2. OOP: ECMAScript implementation. Suddenly the task seemed achievable again, although it didn't give much information on how to call superclass methods from an instantiated class.
Well, after almost 2 days of pulling my hair out, cursing the lack of in-game JS debugger and less than useful exception messages in game logs, I have finally cracked it. What's below won't work well in web browsers, but it works beautifully in WZ's JS environment (which I assume is something derived from SpiderMonkey or something like that?)....
First, the epInherit method:
Code: Select all
// allow functions to inherit a superclass
Function.prototype.epInherit = (function(){
function F() {}
return function (parent,child) {
child = child || this.prototype.constructor;
F.prototype = parent.prototype;
child.prototype = new F;
child.prototype.constructor = child;
return child;
};
})();
if FuncA wants to inherit FuncB, it's as simple as:
Obviously, FuncB has to be defined already, and the epInherit needs to happen before you start adding anything to the prototype chain of FuncA (because the epInherit method replaces FuncA.prototype with a new object (the "new F" line in the code above).
Now the epSuper method:
Code: Select all
// allow classes to call superclass methods/constructors very easily
Object.prototype.epSuper = function() {
var proto = this.__proto__;
var match = epSuper.caller;
var name = epSuper.caller.name;
while (!!proto && !!proto.__proto__) {
if (proto.hasOwnProperty(name) && proto[name]===match) { // found the method
return (!!proto.__proto__[name]) ? proto.__proto__[name].apply(this,arguments) : undefined;
} else if (proto.constructor===match) { // found a constructor
return proto.__proto__.constructor.apply(this,arguments);
}
proto = proto.__proto__;
}
return undefined;
};
That allows you to call the overriden superclass constructor (if used in your class' constructor) or method (if used in a method of your class) simply by calling .epSuper(). You can of course pass in parameters - there are several approaches depending on what you need to do:
Code: Select all
// eg. if you called: .foo(1,2,3,4) on an instance of SomeClass, you could do:
SomeClass.protoype.foo = function(a,b,c) {
...
this.epSuper(); // call the overridden superclass "foo" method with no params
... or ...
this.epSuper(a,c,"bleh"); // send some/custom args to the overriden superclass "foo" method, eg: 1,3,"bleh"
... or ...
this.epSuper.apply(this,arguments); // send all arguments to the overriden superclass "foo" method, eg. 1,2,3,4
...
}
I must now sleep for about 3 days to recover. Hope it is useful to someone

"Dedicated to discovering Warzone artefacts, and sharing them freely for the benefit of the community."
-- https://warzone.atlassian.net/wiki/display/GO