Tiny Tight Guide to Classes in SugarCube V0.5.1

Sidenote:   While this is a Tiny Tight Guide to use JavaScript classes in SugarCube it might not explain everything. The goal of this Guide is to show how to create/define classes written in JavaScript and how those then can be used in SugarCube, it's not to teach programming or all of it's basics.


Chapter[Select] — Indexes start at 0

Chapter Select Chapter — What even is a Class? Chapter — What does it mean to be a Character? Chapter — Babysteps Chapter — Is it "Secret sauce" or just "White gooey Slime"?



Chapter — Babysteps

Okay so lets make a Character class.
First we need to define the class itself which we also attach-on/assign-to the window object because that's globally available. We need to attach it to something (either window object or on the setup object) otherwise SugarCube would simply forget that we created it, once SugarCube is done running the JavaScript section, because that section is scoped and once a scope ends things that aren't global will simply disappear.

window.Character = class Character {};
A class is like a blueprint to a house or a car, it describes the object that eventually gets build using the blueprint as a template/guidance. So a class describes an object, and the object also commonly referred to as "instance" is the finished build house/car that can be used and modified.

Every object has a constructor, even if you don't define it it will just have a default one. A constructor is a special method, that gets called when ever the new keyword is invoked along with a class. a method is a function that belongs to a class/instance, you might have come across <string>.toLowerCase() or <array>.random() in SugarCube, those are methods which simply are functions running based on the instances current state.

The constructor in SugarCube is used for 2 things, to provide default values to the resulting instance, and to be able to overwrite those default values by passing in an object. SugarCube does use the constructor to pass in an object that holds the saved data when loading a save file.

window.Character = class Character {
  constructor(config = null) {

  }
};

Is how we create a constructor that accepts an argument, the argument being called config which will contain the data we want to overwrite. we do give it a default value of null tho, because if we don't and if we don't pass in a value into the constructor then it will be undefined, so it's better to tell it that there is no value. Lets first create the properties name and money and give them some placeholder value:

window.Character = class Character {
  constructor(config = null) {
    this.name = "NA";
    this.money = 0;
  }
};

The this keyword is a self reference, it refers to itself and can be used to run methods or access properties, similar how you can refer to your self while providing information about yourself "i have blue eyes" or "my eye color" is simply this.eyeColor. so in the example above, we technically attach a name keyword to the self reference this and giving it a default value which is an empty string. Do note that the spelling is always case sensitive and must be consistent throughout.

Now if we want SugarCube to restore values, or want to be able to pass in values ourselves to overwrite the default value we created we can simply copy and paste this code at the end of the constructor:

window.Character = class Character {
  constructor(config = null) {
    this.name = "NA";
    this.money = 0;
  
    if(config != null) {
      Object.keys(config).forEach(function(pn) {
        this[pn] = clone(config[pn]);
      }, this);
    }
  }
};

This part in particular:

if(config != null) {
  Object.keys(config).forEach(function(pn) {
    this[pn] = clone(config[pn]);
  }, this);
}

This does check if config has a value, and if so will overwrite all default values that we defined above via the this.property = value; with the properties that are defined within that config object. (note: pn is simply short for propertyname and is a temporary variable, the name does not matter and you can rename it if you like as long as you rename all occurences within the .forEach() method)

Now SugarCube does require non-generic objects (which a custom class is) to also define 2 methods, the .toJSON() and .clone() methods.

Methods in JavaScript follow the signature of the method name, then the argument list inside parentheses, followed by the code block which are indicated by the area between the squiggly brackets:

methodname(args){
  /* code block*/
}

The args can be left empty if you don't intend it to receive values from the "outside" eg.:

methodname(){
  /* code block*/
}

We technically already created a method on our class, the constructor being a special method that runs when ever we create a new instance of a class.

However the .toJSON and .clone methods are probably code snippets that you'll simply copy and paste between classes. Here's what i usually use:

window.Character = class Character {
  constructor(config = null) {
    this.name = "NA";
    this.money = 0;
  
    if(config != null) {
      Object.keys(config).forEach(function(pn) {
        this[pn] = clone(config[pn]);
      }, this);
    }
  }
  clone() {
    return new this.constructor(this);
  }

  toJSON() {
    const ownData = {};
    Object.keys(this).forEach(function(pn) {
      ownData[pn] = clone(this[pn]);
    }, this);
    return JSON.reviveWrapper(`new ${this.constructor.name}($ReviveData$)`, ownData);
  }
};

To at least somewhat cover those methods, starting with the easy one what the clone method does is using the new keyword while using the self reference to fetch its own constructor and then passing in itself. this has the affect that all values are copied into a new object/instance and then returned back "outside" using the return keyword.

Another way to write this would be: Or:
clone() {
  return new Character(this);
}
clone() {
  return JSON.parse(JSON.stringify(this));
}

Altho the first variant does not offer much flexibility and needs to be renamed for each and every new class you copy this snippet into, while the second one is a little more heavy on performance because it first needs to turn it into a string value then back into an object which when turned into a string strips all functions off of it.

All of which are quite strong downsides that are easily prevented by doing this instead:

clone() {
  return new this.constructor(this);
}

Another benefit of writing it like this, is that it also can be inherited by child classes (but we aren't covering inheritance just yet)

The other toJSON method is first creating a new variable called ownData which will gather a copy (not a reference) to the objects own data, which it does in a loop for all property names and simply cloning it, then once it has all values it uses SugarCube's extension method from the JSON class to create a reviveWapper which is needed for saving and loading the object. basically it creates a command how to create the current object.

Before we continue with more class related stuff, I'm gonna explain how we create a new instance of a class, inside a passage we can simply write:

<<set $player = new Character({name: "Maxine"})>>
$player.name
Money: $player.money

The new Character() creates a new instance of the object, the {name: "Maxine"} is how we overwrite the default value of the name property of our object, basically we passed in what value it should be. last part $player.name is SugarCube's naked variable markup and simply prints it onto the passage. If everything worked out it should simply say "Maxine" if we don't provide a name property overwrite it should say "NA" since that's the default value, just like the money part should be displayed as Money: 0 since we aren't overwriting it.

Now back on the topic of methods, there are also getter and setter methods that we can define, those are special methods which on the "outside" don't require the parentheses but rather act like you're directly assigning to a property directly, like we did with $player.name for example in the above class we have the money property that we can directly access via $player.money and we could change the value just like so <<set $player.money -= 9999999999>> which will set the property money of the $player variable to 0 - 9999999999 which is -9999999999 but lets say we don't want to allow it to go past 0 because you cannot take negative money with you (you sure can owe people money, but they cannot take money away from you that you don't have on hand) to do this we change the money property in our class to _money and add a getter and setter method with the original money name so we can still use it like we did prior:

window.Character = class Character {
  constructor(config = null) {
    this.name = "NA";
    this._money = 0;
  
    if(config != null) {
      Object.keys(config).forEach(function(pn) {
        this[pn] = clone(config[pn]);
      }, this);
    }
  }

  get money() {
    return this._money;
  }

  set money(value) {
    this._money = value;
    if(this._money < 0)
      this._money = 0; 
  }

  clone() {
    return new this.constructor(this);
  }

  toJSON() {
    const ownData = {};
    Object.keys(this).forEach(function(pn) {
      ownData[pn] = clone(this[pn]);
    }, this);
    return JSON.reviveWrapper(`new ${this.constructor.name}($ReviveData$)`, ownData);
  }
};

To mark a getter method we just need to prefix it with the keyword get and to mark something as a setter method we only need to prefix it with the keyword set as seen above. now if we were to try <<set $player.money -= 9999999999>> we'd only end up setting $player.money to 0 but if we tried <<set $player.money += 50>> then it would add 50 to whatever $player.money currently is (well technically we are modifying $player._money but we are using the getter and setter methods $player.money to act as a property while it allows us to control what happens when we set/get values to/from it)

I guess last thing to cover, since I mentioned it above, is inheritance. inheritance is meant to expand upon a class with new behavior. a "child" class inherits all public/protected properties and methods of a parent class that it extends. public, protected, and private are the 3 common accessibility levels. to explain it, the $player.name property is marked public and can be accessed anywhere and be inherited to any class that derives from the Character class, it's how we're able to display the current value from a passage context or change it without needing to trigger another class method. private on the other hand is everything that can only be accessed within the classes/objects/instances own scope, it's not available to the outside but only the classes inner workings. like the steering wheel of a car is only accessible inside the car, where the car's door is accessible from the outside and inside of the car. But private marked properties/methods are only available inside the class that it was defined in. lastly protected is more kin to private with the only difference that properties and methods are available to all inherited classes, they still aren't accessible to the "outside" but child classes can inherit and use them as if they are defined on them.

In JavaScript and by extension SugarCube there isn't really a protected keyword, the most common way to deal with it is to fake it like we did with this._money the _money property isn't meant to be exposed to the outside, for that we have defined the getter and setter methods, while technically it's public and just as manipulable as the name property but this is the best what JavaScript seems to have without turning it into a private field (field being a term to describe private properties).

To mark a property of method private in JavaScript/SugarCube, we simply prefix the name with a # for example this.#name or #AddAMillionMoney(){this.money += 1000000;} just keep in mind that private fields cannot be accessed from a Passage, nor can it be passed down to child classes.

Everything is by default public so there is no prefix or tricks there.

Now lets say we want to extend the Character class with a dedicated Player so we can add specific things to the player object that is unique to the player instance for example if our story has some fighting mechanics but not all NPCs are fight-able so we want health to be specific to the Player class, for this we add a new class that extends on our Character class like so:

window.Player = class Player extends Character {
  constructor(config = null) {
    super();
    this.health = 20;
    this.maxHealth = 20;

    if(config != null) {
      Object.keys(config).forEach(function(pn) {
        this[pn] = clone(config[pn]);
      }, this);
    }
  }
};

Now to extend a class in JavaScript it's as simple as writing class MyChildClassName extends MyParentClassName but we do need a constructor, and we do need to call super() which is a special method call that calls the Parent classes constructor, so it creates the properties (and maybe methods) of the parent class first before constructing the child class, we could pass in the config object into the super() method call, but since we still defining new properties after the parents constructor call we don't need to overwrite any default values yet, instead we do it at the end of our new child classes constructor, which might seem repetitive but we also don't want to pass down the object to the parent constructor to create the health property to then have the child constructor overwrite it to the defaults, plus it does get skipped if we leave super() empty.

To re-explain super() in different words, what it does is: the child class Player calls super() which in return the the constructor() method of parent class which is Character and can be used to pass in values to the parent constructor eg.: super({name: "Maxine"}); or using the variable super(config); but for our case we don't need to do that and values would be overwritten by the most specific child class, in this case the Player class.

The reason why we don't include the .toJSON and .clone methods in our Player class is because they are generically written enough that when they are inherited from the Character class it still works for our Player child class as long as we provide the class name (anonymous classes, meaning classes without a name for example Character/Player, are possible in JavaScript but i won't touch on those, because all that is to them is literally just skipping the name part when writing the class like this windows.MyClass = class {}; and that doesn't fill the this.constructor.name field which breaks our inheritance of the parent class. So generally speaking they aren't useful for for inheritance unless we overwrite the .toJSON method).

Now we also need to change our passage again from this: To this:
<<set $player = new Character({name: "Maxine"})>>
$player.name
Money: $player.money




<<set $player = new Player({
  name: "Maxine", 
  health: 50, 
  maxHealth: 100
})>>
$player.name ($player.health / $player.maxHealth  HP)
Money: $player.money
To display all the new information in our passage.

Chapter — What even is a Class?

To not use jargon terms, the class is essentially a description of what the result (aka object also often referred as instance) will look and behave like. The easiest comparisons is that the class is basically a blueprint to a floor or house, the blueprint describes how many windows and where they are located, which way doors open and where stairs are located. Now if a class is the same as a blueprint then an instance (or object) of a class is the "physical" representation of it, however with the physical representation we do have some leeway to play around with it, not all walls need to be the same color, not all doors need out of the same material, furniture could be moved around.

Chapter — What does it mean to be a Character?

Well in terms of making a class the truth is that the answer varies from one project/game/story to the next. The answer also changes if you look at it from the perspective that every human or potentially humanoid character gets the same basically template aka class and there being no distinction at all between the Player character nor NPCs or even potential Enemies, this approach would make the most sense if any character could be fought against at any time. Like in Skyrim as an example.


© GwenTastic