How to Extends / Alias / Overwrite existing class in MZ/ ES6 [Tutorial]

Galenmereth

Retired
Veteran
Joined
May 15, 2013
Messages
2,248
Reaction score
2,142
First Language
English
Primarily Uses
N/A
@TheoAllen Sure thing, although now it looks so much worse in light theme ;)

@Tsukihime You would have to ensure any plugin that changes `Pawn` to be loaded before the plugin that adds `Tank`, as you would have to do the way most plugins are written in MV as well. You could call some prototype methods during runtime, like this, but it adds a whole other can of worms if the methods don't return static values and rely on context and/or instance properties:

Code:
// our base soldier class
class Pawn {
  get hp() {
    return 100;
  }
}

// some special unit with twice the HP
class Tank extends Pawn {
    constructor() {
      super();
  }
  get hp() {
    return Pawn.prototype.hp * 2; // Calls the prototype method hp of Pawn during runtime, resolving it on each call
  }
}

console.log(new Pawn().hp) // 100
console.log(new Tank().hp) // 200. As expected

// NEW PLUGIN that gives all of our units 1000 HP bonus!
Pawn = class extends Pawn {
  get hp() {
    return super.hp + 1000
  }
}

console.log(new Pawn().hp) // 1100. Works well.
console.log(new Tank().hp) // 2200. As expected, as it now points to the current prototype method `hp` of `Pawn` during runtime
This would of course break if `Pawn` defined hp something like this:

Code:
class Pawn {
  constructor() {
    this._hp = 100;
  }

  get hp() {
    return this._hp;
  }
}
We can't call `Pawn.prototype.hp` now because it doesn't have the context object `this` defined, and thus would return undefined.

This problem is one you will always have when you overwrite and extend classes in this manner, and compatibility will always be a challenge no matter which option you opt to go for. I'm curious to see if MZ is using modules (export/import), however; that could change things a lot: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
 
Last edited:

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,573
Reaction score
6,497
First Language
Indonesian
Primarily Uses
RMVXA
@Tsukihime You could ensure any plugin that changes `Pawn` to be loaded before the plugin that adds `Tank`, as you would have to do today. Or you could have it reference Pawn´s prototype method during runtime, eg:
Or the tank would be a wrapper class in which it isn't exactly extending pawn, but hold a reference to an instance of Pawns object.
However, that left a question on how the default MZ code does the class extension. Would they use ES6 syntax or still a prototype syntax like MV. Because if it's using ES6 Syntax, Hime's question would still a valid question. If you want to change Game_Battler, it won't take effect in Game_Actor.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,556
Reaction score
3,811
First Language
English
Or you could have it reference Pawn´s prototype method during runtime, eg:
Actually that doesn't look too bad.

Code:
// our base soldier class
class Pawn {
  get hp() {
    return 100;
  }
}

// some special unit with twice the HP
class Tank extends Pawn {
  get hp() {
    return Pawn.prototype.hp * 2
  }
}

console.log(new Pawn().hp) // 100. Works well.
console.log(new Tank().hp) // 200. Looks good

// actually let's increase our soldiers' HP
Pawn = class extends Pawn {
  get hp() {
    return super.hp + 1000
  }
}

console.log(new Pawn().hp) // 1100. Works well.
console.log(new Tank().hp) // 2200. Works with the prototype

// Let's change tank HP multiplier, given 5x MORE ON TOP OF THE 2x!!!!
Tank = class extends Tank {
  get hp() {
    return super.hp * 5
  }
}

console.log(new Pawn().hp) // 1100. Works well.
console.log(new Tank().hp) // 11000. Works with the prototype
So now I can have a new plugin change the parent class (doing alias) and it works fine.
I can also have a new plugin change the child class (doing alias), and it works fine.

It's also quite clean.

Code:
super.hp

vs

PARENT.prototype.hp
Could be an issue if you don't actually know the PARENT beforehand...but I can't think of an issue.
 

Ossra

Formerly Exhydra
Veteran
Joined
Aug 21, 2013
Messages
1,076
Reaction score
845
First Language
English
Primarily Uses
RMMV
Hmm, that does look alright. Kind of depends on how RPG Maker MZ's base code is written.
 

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,573
Reaction score
6,497
First Language
Indonesian
Primarily Uses
RMVXA
@Tsukihime what about this. Instead of hardcoded to return 100, what if it's a class variable instead that you could change the HP on the fly? Because you know, they could get damaged.
 

Galenmereth

Retired
Veteran
Joined
May 15, 2013
Messages
2,248
Reaction score
2,142
First Language
English
Primarily Uses
N/A
@TheoAllen I edited my post in the meantime and took that into account, as it's an added challenge with this type of approach in general.

And wrappers like you're describing is something I've used a fair bit in my own plugins. The only problem with that is interoperability with random stranger's plugins, but there really is no silver bullet to that challenge unless one implements a specific plugin API to resolve it.
 

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,573
Reaction score
6,497
First Language
Indonesian
Primarily Uses
RMVXA
And wrappers like you're describing is something I've used a fair bit in my own plugins. The only problem with that is interoperability with random stranger's plugins, but there really is no silver bullet to that challenge unless one implements a specific plugin API to resolve it.
It is not exactly a problem. Sure, there would be compatibility issues, however, if patch can be made, like, simply put this plugin file below those two plugins to solve the issue, all is well. The problem is when the stranger's plugins use encapsulation like IIFE (who think IIFE is good for MV plugins, honestly), you couldn't patch it. This is slightly out of topic, but I still think it's kinda relevant.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,556
Reaction score
3,811
First Language
English
Looks like it was too early to celebrate lol if it can't operate on "this" then it's basically useless for anything that deals with instance variables.

Guess I'll just go back to typing things out the long way...I was just starting to enjoy not having to type "prototype" everywhere.
 

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,573
Reaction score
6,497
First Language
Indonesian
Primarily Uses
RMVXA
Looks like it was too early to celebrate lol if it can't operate on "this" then it's basically useless for anything that deals with instance variables.

Guess I'll just go back to typing things out the long way...I was just starting to enjoy not having to type "prototype" everywhere.
Or you could just alias the prototype itself, like ...
Code:
var a = Game_Actor.prototype
var alias = a.OldFunction
a.OldFunction  = () {
  alias.call(this)
  // Add more statement here
}
Mind the global namespace though.
 

Galenmereth

Retired
Veteran
Joined
May 15, 2013
Messages
2,248
Reaction score
2,142
First Language
English
Primarily Uses
N/A
@Tsukihime
Code:
class Tank extends Pawn {
    hp() {
        return Pawn.prototype.hp.call(this);
    }
}

// This works, but it won't work with getters
Also, prototype won't solve this specific problem anymore than class extending will in terms of changes to Pawn being made in a plugin loaded after your Tank plugin. In either case, Tank won't know about the changes to Pawn's methods on initial parse, only during runtime. You'd have to find a solution in either case.
 

nio kasgami

VampCat
Veteran
Joined
May 21, 2013
Messages
8,949
Reaction score
3,038
First Language
French
Primarily Uses
RMMV
Okay so if I understand...
@Galenmereth

it's mean I can't do that anymore? and it will not work?

Code:
// Plugin A

class Something extends Scene_Battle {
   constructor(){
    super();
   }
  
   someExistingFunction(){
    super.someExistingFunction();
   }
}

Scene_Battle = Something;


// Plugin B

class Something2 extends Scene_Battle {
   constructor(){
    super();
   }
  
   someExistingFunction(){
    super.someExistingFunction();
   }
}

Scene_Battle = Something2;
Since it will not work properly?
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,556
Reaction score
3,811
First Language
English
Ok I think I got some progress. This is my revised pawn/tank example.

- Pawns start with 100 HP. Tanks also start with 100 HP.
- New method "damage" defined which decreases HP.
- Pawns take "Full damage", Tanks have 20 damage reduction cause "armor"

Code snippets can be copy and pasted and run one by one as you go.

Code:
// our base soldier class
class Pawn {
  constructor() {
    this.initParams();
  }

  initParams() {
    this._hp = 100;
  }

  hp() {
    return this._hp;
  }

  damage(value) {
    this._hp -= value;
  }
}

// tank takes half the damage
class Tank extends Pawn {
  damage(value) {  
    Pawn.prototype.damage.call(this, value - 20)
  }
}

pawn = new Pawn(); // starts with 100 HP
tank = new Tank() // starts with 100 HP
pawn.damage(50)
tank.damage(50)
console.log(pawn.hp()) // 50 left, took 50 damage
console.log(tank.hp()) // 70 left, took 50 - 20 = 30 damage
So that works fine

Now, let's introduce an alias that will give all units a -10 damage in their formulas, for "natural armor"

Code:
// all units have base 10 damage resistance
Pawn = class extends Pawn {
  damage(value) {  
    super.damage.call(this, value - 10);
  }
}

pawn = new Pawn(); // start with 100 HP
tank = new Tank(); // start with 100 HP
pawn.damage(50)
tank.damage(50)
console.log(pawn.hp()) // 60 left, (50 raw damage - 10 due to base resistance = 40 damage received)
console.log(tank.hp()) // 80 left, (50 raw damage - 20 tank resist - 10 base resist = 20 damage received
So this is nice, the tank inherited that extra resistance as well.

Finally, let's say I wanted to give the tank a further damage reduction. ANOTHER flat 20 damage reduction

Code:
Tank = class extends Tank {
  damage(value) {
    super.damage.call(this, value - 20)
  }
}

pawn = new Pawn(); // start with 100 HP
tank = new Tank()
pawn.damage(50)
tank.damage(50)
console.log(pawn.hp()) // 60 left (50 damage - 10 base resist)
console.log(tank.hp()) // 100 left (50 damage - 20 NEW resist - 20 OLD resist - 10 BASE resist = 0 damage received)

So now changes to the parent will be reflected in the child, because the child class originally referenced the prototype already.

But then I tried something else. I decided I wanted to increase all units initial HP

Code:
Pawn = class extends Pawn {
  initParams() {
    super.initParams()
    this._hp = 200
  }
}
pawn = new Pawn(); // start with 200 HP now
tank = new Tank() // start with 200 HP ???
pawn.damage(50)
tank.damage(50)
console.log(pawn.hp()) // 160 left (50 damage - 10 base resist = 40 damage received on 200 HP)
console.log(tank.hp()) // 100 left (50 damage - 20 NEW resist - 20 OLD resist - 10 BASE resist = 0 damage received on 200 HP)
And that doesn't work. However, if I explicitly had the tank method call the parent prototype, then it works!

Code:
Tank = class extends Tank {
  initParams() {
    Pawn.prototype.initParams.call(this);
  }
}
pawn = new Pawn();
tank = new Tank()
pawn.damage(50)  // start with 200 HP
tank.damage(50)  // start with 200 HP!!!
console.log(pawn.hp()) // 160 left (50 damage - 10 base resist = 40 damage received on 200 HP)
console.log(tank.hp()) // 200 left (50 damage - 20 NEW resist - 20 OLD resist - 10 BASE resist = 0 damage received on 200 HP)
So to generalize, I would need to explicitly point all child class methods to the parent's prototypes manually when I first set it up

Code:
class Parent {
   method() {
   }
}

class Child extends Parent {
   method() {
      Parent.prototype.method.call(this);
   }
}
And then afterwards, I can do the class extend alias, and use "call" to bind "this" when calling functions.

Code:
Child = class extends Child {
  method() {
    super.method.call(this);
  }
}
Not sure why explicitly setting prototype works though. I would've assume the "extend" keyword would basically do that.
 

??????

Diabolical Codemaster
Veteran
Joined
May 11, 2012
Messages
6,467
Reaction score
3,033
First Language
Binary
Primarily Uses
RMMZ
Interesting.. will have to run a few tests to check this out. from the looks of it, and considering es6 classes are just some sugar on the syntax, I wonder how difficult it would actually be to port a plugin from mv to mz.

Until we can see some of the default code for mz though, we wont really know for sure how were going to have to implement things. :(
 

Ossra

Formerly Exhydra
Veteran
Joined
Aug 21, 2013
Messages
1,076
Reaction score
845
First Language
English
Primarily Uses
RMMV
Yeah, everything kind of depends on how the base RPG Maker code is written and how plugins are loaded. Unless there is going to be a developer update where the code is made available, we will simply have to wait. I have to imagine that the developer team gave plugins a lot of thought. It is nice to tinker around with ES6 aliasing, though.

I believe that porting over plugins will not be too rough.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,556
Reaction score
3,811
First Language
English
Interesting.. will have to run a few tests to check this out. from the looks of it, and considering es6 classes are just some sugar on the syntax, I wonder how difficult it would actually be to port a plugin from mv to mz.

Until we can see some of the default code for mz though, we wont really know for sure how were going to have to implement things. :(
They released MZ API https://www.rpgmakerweb.com/support/api/index
You can click on the code line to see the source. It's only stuff in rpg_core.js though

Honestly it looks about the same lol. Prototype everywhere.
They probably just say MV plugins don't work so people don't have an expectation that they can just pop their plugins into MZ and continue.

I'd expect my 99% plugins to be able to work assuming they didn't change the underlying classes like interpreter, battlers, etc.
I don't think they would change it either, given that there's no real need to. It's essentially the same code base from VX and Ace. XP was a nightmare and they then refactored it when VX came out, did some more refactoring for Ace, and then the structure was more-or-less the same for MV.

Actually there isn't anything special about ES6 in MV. I've been using the class/extend syntax on all of my code in the last couple days and it works fine on browser and desktop. Maybe mobile might not work? Not sure never tested.
 

??????

Diabolical Codemaster
Veteran
Joined
May 11, 2012
Messages
6,467
Reaction score
3,033
First Language
Binary
Primarily Uses
RMMZ
already got a local copy of the doc :D

edit:
from the looks of things, we have all been overthinking how much change they have actually done to the core foundation.
 

Galenmereth

Retired
Veteran
Joined
May 15, 2013
Messages
2,248
Reaction score
2,142
First Language
English
Primarily Uses
N/A
@Tsukihime It doesn't work with extends because it extends the class at that point in time. Calling the prototype method calls the method during runtime.

This is an important difference to understand, and it has nothing to do with the extend keyword itself. It would be exactly the same if you had defined them using the prototype keyword. But when you call `Pawn.prototype.initParams.call(this)` within the `initParam` method of Tank, it calls that during runtime, hence it will "get" any changes done to Pawn by code later on.

To reiterate:

Code:
class Tank extends Pawn {}

// is functionally identical to

function Tank() {}

Tank.prototype = Object.create(Pawn.prototype);
Tank.prototype.constructor = Pawn;
And when you redefine pawn later on, then

Code:
Pawn = class extends Pawn {
    initParams() {
        super.initParams();
    }
}

// is functionally the same as doing

var originalInitParams = Pawn.prototype.initParams;
Pawn.prototype.initParams = function() {
    originalInitParams();
}
and in both cases, this would not be picked up by `Tank` unless you call the prototype method during runtime. The major difference is in readability, not having to manually create references to the original method (super takes care of it), and in general getting cleaner code.

already got a local copy of the doc :D

edit:
from the looks of things, we have all been overthinking how much change they have actually done to the core foundation.
This discussion wasn't started because of that, it was because people were confused about the class syntax in es6 vs. prototype syntax.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,556
Reaction score
3,811
First Language
English
and in both cases, this would not be picked up by `Tank` unless you call the prototype method during runtime. The major difference is in readability, not having to manually create references to the original method (super takes care of it), and in general getting cleaner code.
I think I understand how it works. So I'm mostly wondering what I'm missing that works in prototype syntax, that isn't working in class syntax.

Ok so let's say I do this. Classic prototype definition

Code:
function Pawn() {
  this.initialize.apply(this, arguments);
}

Pawn.prototype.initialize = function() {
  this.initMembers();
}

Pawn.prototype.initMembers = function() {
  this._hp = 100
}

Pawn.prototype.hp = function() {
  return this._hp
}

function Tank() {
  this.initialize.apply(this, arguments);
}
Tank.prototype = Object.create(Pawn.prototype);
Tank.prototype.constructor = Pawn;

var pawn = new Pawn();
var tank = new Tank();

console.log(pawn.hp(), tank.hp()) // 100, 100

// overwrite prototype
Pawn.prototype.initMembers = function() {
  this._hp = 200
}

var pawn = new Pawn();
var tank = new Tank();
console.log(pawn.hp(), tank.hp()) // 200, 200
So I define a Pawn with some methods, then I create subclass Tank that simply inherits everything.
Now when I change the pawn's prototype, suddenly Tank also picks up the change. Tank didn't have to know anything about the Pawn.

But when I rewrite it in class syntax

Code:
class Pawn {
  constructor() {
    this.initMembers();
  }
  initMembers() {
    this._hp = 100
  }
  hp() {
    return this._hp
  }
}

class Tank extends Pawn {
}

var pawn = new Pawn();
var tank = new Tank();

console.log(pawn.hp(), tank.hp()) // 100, 100

Pawn = class extends Pawn {
  initMembers() {
    this._hp = 200;
  }
}

var pawn = new Pawn();
var tank = new Tank();
console.log(pawn.hp(), tank.hp()) // 200, 100
Tank now no longer knows about the change in Pawn's prototype.

Are these two pieces of code the same? To me it seems like it, but maybe there's something missing.
 

Galenmereth

Retired
Veteran
Joined
May 15, 2013
Messages
2,248
Reaction score
2,142
First Language
English
Primarily Uses
N/A
The difference is this line:

Code:
// overwrite prototype
Pawn.prototype.initMembers = function() {
  this._hp = 200
}
So if you'd change the class extends example to this, it works as expected:

Code:
class Pawn {
  constructor() {
    this.initMembers();
  }
  initMembers() {
    this._hp = 100
  }
  hp() {
    return this._hp
  }
}

class Tank extends Pawn {
}

var pawn = new Pawn();
var tank = new Tank();

console.log(pawn.hp(), tank.hp()) // 100, 100

// overwrite prototype
Pawn.prototype.initMembers = function() {
  this._hp = 200
}


var pawn = new Pawn();
var tank = new Tank();
console.log(pawn.hp(), tank.hp()) // 200, 200
What happens is that when you overwrite Pawn.prototype.initMembers you're no longer extending Pawn, you're overwriting the original method in memory, which Tank actually does still reference. This is... a bit of a quirk of JS prototype inheritance and has caused me a lot of headaches over the years
:rtear:

So if you mix and match like this, when applicable, you can overwrite getters using this method as well. I realize I should've mentioned this a few posts ago now... I just didn't think of it in the right context.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,556
Reaction score
3,811
First Language
English
The difference is this line:

Code:
// overwrite prototype
Pawn.prototype.initMembers = function() {
  this._hp = 200
}
So if you'd change the class extends example to this, it works as expected:

Code:
class Pawn {
  constructor() {
    this.initMembers();
  }
  initMembers() {
    this._hp = 100
  }
  hp() {
    return this._hp
  }
}

class Tank extends Pawn {
}

var pawn = new Pawn();
var tank = new Tank();

console.log(pawn.hp(), tank.hp()) // 100, 100

// overwrite prototype
Pawn.prototype.initMembers = function() {
  this._hp = 200
}


var pawn = new Pawn();
var tank = new Tank();
console.log(pawn.hp(), tank.hp()) // 200, 200
What happens is that when you overwrite Pawn.prototype.initMembers you're no longer extending Pawn, you're overwriting the original method in memory, which Tank actually does still reference. This is... a bit of a quirk of JS prototype inheritance and has caused me a lot of headaches over the years
:rtear:

So if you mix and match like this, when applicable, you can overwrite getters using this method as well. I realize I should've mentioned this a few posts ago now... I just didn't think of it in the right context.
ah, so it's an issue with references.

Code:
Pawn = class extends Pawn {
  initMembers() {
    super.initMembers();
    this._hp = 200;
  }
}
This actually creates a new prototype, which Tank doesn't know about. It's still looking at whatever the old one is.
So I guess maybe I can just say

Code:
class Tank extends Pawn {
  initMembers() {
    Pawn.prototype.initMembers.call(this);
  }
}
And then I don't need to worry anymore.

The only issue here is if I have like a bunch of methods, I'd have to say

Code:
class Tank extends Pawn {
  method1() {
    Pawn.prototype.method1.call(this);
  }
  method2() {
    Pawn.prototype.method2.call(this);
  }
  method3() {
    Pawn.prototype.method3.call(this);
  }
  method4() {
    Pawn.prototype.method4.call(this);
  }
  method5() {
    Pawn.prototype.method5.call(this);
  }
}
I just need to make sure that if I ever introduce a new method in the parent, all subclasses are referencing that method's prototype, which can be extended or overwritten at anytime.
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Profile Posts

Ami
--- Dumped ---

Hero: Please go out with me!
F.Healer: (Silent In 10 Seconds) I refuse.
Hero: ???
F.Healer: I don't want my friendship with F.Mage is ruined because i'm in your side. so...i'm sorry. (Running)
Hero: (Glass Break SFX)
I really wish to have listened someone's advice regarding writing, because I'm a little stuck.

Guys, if you have a whole story in mind, and you strongly believe it's going to work, make the effort of writing it. Because if you get stuck in a scene, you might have to re-assess it.
1 million messages is approaching. Some say the true identity of the legendary Maker is gonna be revealed. I mean come on, the legendary Maker is just a myth.....
Yes it took me this long to make a basic cape. :p Here is a shady necromancer elf to model it.



I dub him Nyxiel.

Forum statistics

Threads
103,348
Messages
998,941
Members
134,858
Latest member
Zamazawarma
Top