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

● ARCHIVED · READ-ONLY
Started by nio kasgami 113 posts Page 2 of 6 View original ↗
  1. @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
  2. Galenmereth said:
    @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.
  3. Galenmereth said:
    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.
  4. Hmm, that does look alright. Kind of depends on how RPG Maker MZ's base code is written.
  5. @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.
  6. @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.
  7. Galenmereth said:
    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.
  8. 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.
  9. Tsukihime said:
    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.
  10. @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.
  11. 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?
  12. 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.
  13. 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. :(
  14. 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.
  15. ?????? said:
    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.
  16. 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.
  17. @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.

    ?????? said:
    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.
  18. Galenmereth said:
    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.
  19. 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.
  20. Galenmereth said:
    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.