Class's Object data member not playing nicely with history

Twine Version: 2.9.2

Hi all, sorry if this is a n00b question. Just trying to get my feet wet with Sugarcube after having used Harlowe for a while. One thing that appeals to me about the format is built-in file-based load / save, and in playing with that I realized there must be something about classes that I’m missing.

The behavior is, briefly, that I have a data member of a Class that is itself a generic Object. The class (code below) saves and restores without giving an error, but when I increment, say, the attribute “strength” of the “skills” Object in an instance of the class on one page and then go back and forth in the history, the “skills” Object doesn’t go backward and forward in time correctly. If I go back and forth over a passage that increments the attribute, it just keeps increasing.

I don’t observe this with a generic Object having a “skills” attribute that is itself a generic Object, so I’m thinking there must be some method I’ve failed to implement or have implemented incorrectly.

Any tips are welcome, as I’m still trying to flail my way around learning Sugarcube and JavaScript at the same time.

setup.player = class player {
  constructor(name, skillObj) {
    this.name = name;
    this.skills = skillObj;
  }
  
  clone() {
    return new this.constructor(this.name, this.skills);
  }
  
  toJSON() {
    return Serial.createReviver(String.format(
      'new setup.{0}({1}, {2})', 
      this.constructor.name, 
      JSON.stringify(this.name), 
      JSON.stringify(this.skills)
     ));
  }
  
  getSkill(whichSkill) {
    if (!whichSkill in Object.keys(this.skills)) {
      return 0;
    }
    return this.skills[whichSkill];
  }
  
  incrementSkill(whichSkill, howMuch) {
    if (!whichSkill in Object.keys(this.skills)) {
      Object.defineProperty(this.skills, whichSkill, {value: 0});
    }
    Object.defineProperty(this.skills, whichSkill, {value: this.skills[whichSkill] + howMuch});
  }
  
  getName() {
    return this.name;
  }
  
  setName(newName) {
   	this.name = newName; 
  }
};

Can you supply an example of the Generic Object that is being assigned to the skills property of the player class.

The JSON.stringify() method is being used to create a String representation of each of the class’s properties values, so knowing the data-type of those values or any child properties those values might have is important, Because as mentioned in the Description section of that method’s documentation, there are specific data-types that that method will exclude or replace with another value.

Thanks for the reply! Sure:

<<set $you = new setup.player("You", {"strength": 10})>>\

Very simple. I’m still trying to find the bathroom here.

The specific bit that I first noticed it with: If I execute this and then save, then reload, it increments not once but twice. Then I realized the whole history bit wasn’t working either. If you just execute this and go back and forth across the passage, it just keeps incrementing.

Before change:
name: <<print $you.getName()>>
strength: <<print $you.getSkill("strength")>>
intelligence: <<print $testWrapper["skills"]["intelligence"]>>

<<run $you.incrementSkill("strength", 1)>>

After change:
name: <<print $you.getName()>>
strength: <<print $you.getSkill("strength")>>
intelligence: <<print $testWrapper["skills"]["intelligence"]>>

Totally different behavior with:

<<set $testObj = {"intelligence": 12}>>\
<<set $testWrapper = {"skills": $testObj}>>\

and the passage

Before change:
name: <<print $you.getName()>>
strength: <<print $you.getSkill("strength")>>
intelligence: <<print $testWrapper["skills"]["intelligence"]>>

<<set _newSkills = setup.smartIncrement($testWrapper["skills"], "intelligence", 3)>>
<<run Object.defineProperty($testWrapper, "skills", {value: _newSkills})>>

After change:
name: <<print $you.getName()>>
strength: <<print $you.getSkill("strength")>>
intelligence: <<print $testWrapper["skills"]["intelligence"]>>

with…

setup.smartIncrement = function(whichObject, whichAttribute, howMuch) {
  let result = structuredClone(whichObject);
  if (!whichAttribute in Object.keys(result)) {
    Object.defineProperty(result, whichAttribute, {value: 0});
  }
  Object.defineProperty(result, whichAttribute, {value: whichObject[whichAttribute] + howMuch});
  return result;
};
1 Like

Unless you specifically want to property related features like listed in the Object.defineProperty() method’s Description, then you don’t need to use that method to add or update properties to/of an Object instance.

  incrementSkill(whichSkill, howMuch) {
    if (! this.skills.hasOwnProperty(whichSkill)) {
      this.skills[whichSkill] = 0;
    }
    this.skills[whichSkill] += howMuch;
  }

notes:

  • Each time a Passage Transition occurs the current state of all “set” Story Variables is cloned.
  • The “original” copy of those variables is added to a Moment representing the Passage being left, and that Moment is added to Progress History.
  • The “clone” copy of those variables is made available to the Passage now being visited.
  • This cloning process breaks Object Referential Integrity, which is a fancy way of saying that if two (or more) variables/properties/array-elements were refencing the same Object instance before the Passage Transition, then each of those variables/properties/array-elements will be referencing their own unique copy of the original Object instance after the Transition.

The following example was written using Twee Notation.

:: Before
Initialise multiple references to same Object instance.
<<set $person to {name: 'Jane'}>>
<<set $people to [$person]>>
<<set $group to {'leader': $person}>>

Will all show the same value, because the all reference the same Object instance.
Person Name: $person.name
Name of 1st in People: <<= $people[0].name>>
Name of leader in Group: <<= $group['leader'].name>>

Change the Name (1)
<<set $person.name to 'Mary'>>

All will show the new name, because the all reference the same Object instance.
Person Name: $person.name
Name of 1st in People: <<= $people[0].name>>
Name of leader in Group: <<= $group['leader'].name>>

[[After]]

:: After
Still all showing the same name, but the now all reference with own copy of the Object instance.
Person Name: $person.name
Name of 1st in People: <<= $people[0].name>>
Name of leader in Group: <<= $group['leader'].name>>

Change the Name again (2)
<<set $person.name to 'Jane'>>

Now only the actual Object referenced in the above change code is changed.
Person Name: $person.name
Name of 1st in People: <<= $people[0].name>>
Name of leader in Group: <<= $group['leader'].name>>

(1) this same change could also be done using either $people[0].name to 'Mary' or $group['leader'].name to 'Mary' and the same outcome would occur.
(2) which reference ($person.name, $people[0].name, $group['leader'].name) is used to make the change does matter here, because each references a different unique copy of the Object instance.

2 Likes

Thanks for taking the time to reply, and I appreciate the tips.

I had certainly seen that passing around references doesn’t really work. (In my simple example above, I defined $testWrapper and $testObj the way I did so I could verify that increments to the wrapped object didn’t touch the original one.)

What I’m getting from your description of the history’s mechanics is that, in this case, it seems like clone() is somehow leaving the reference intact. That is, when a page increments an attribute within the instance, and then I go back one step in time, the “original” copy from your play-by-play is still referencing the incremented object, not that object’s state as it existed when the passage was left.

My conclusion, then, is that if I write my constructor to explicitly copy the skills object, this issue should go away. Let me try it…

And it worked! All it took was changing the constructor to:

  constructor(name, skillObj) {
    this.name = name;
    this.skills = structuredClone(skillObj);

Thanks again for the tips to clean up my syntax and the detailed mechanics of passage transitions.