Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Most Efficient Way To Clamp Number In An Advanced Javascript Object (Sugarcube)

Okay, so I'm going to try to describe this but please forgive me if I don't make sense, I'm just now learning a little bit more advanced Javascript. So, I'm using SugarCube, and if anyone is curious about how to "clamp" a number in a more basic way, I made a post that was answered quite clearly by TheMadExile here http://twinery.org/forum/index.php/topic,1781.msg4596.html#msg4596.

Okay, but what I'd like to accomplish now is (I think) a little more complex. So say I created an "NpC" object in a script passage. The NpC object would have a couple different properties, lets just say something like Health, Hunger, and Thirst. Now ideally, these values would range from 0 to 100. One way of doing this, and this is just my own way, is to add this little bit of code into the PassageReady passage -
<<set $NpC.SomeStat = $NpC.SomeStat.clamp(0, 100)>>
    But, while this works, it's very inefficient because I would have to add that line to every stat of every instance of the NpC object. So the logical thing to do, I would assume is to somehow add that rule to the NpC object's prototype, so that every new NpC's stats follow that "rule".

    The problem is, I cant seem to find a method in Javascript that does that. And math.min and math.max, to the best of my knowledge do not do that. I'm sorry if this is muddled but I've searched everywhere about how to achieve this effect in Javascript and I'm completely at a loss.   
Any help or guidance would be greatly appreciated. And I think a lot of JS and Twine newbies would really benefit from this.

Comments

  • One method that works with SugarCube (but not Sugarcane) is to use the defineProperty functional of modern java-script.

    Using the following passages: (note: 'Define Actor' is a script passage as denoted by the [script])

    :: Define Actor [script]
    /* Define a Constructor for the Actor object */
    window.Actor = function () {
    this.name = "Generic";
    this.age = 20;
    this._health = 100;
    };

    /* Add a health property to the Actor object */
    Object.defineProperty(Actor.prototype, "health", {
    get: function() {
    return this._health;
    },
    set: function(value) {
    this._health = Math.clamp(value, 0, 100);
    }
    });


    :: StoryInit
    /% create an instance of the Actor object %/
    <<set $me to new Actor()>>


    :: Start
    Health should only allow values between 0 and 100.
    My current health is: <<print $me.health>>
    Try changing health to fifty: <<set $me.health = 50>><<print $me.health>>
    Try changing health to two hundred: <<set $me.health = 200>><<print $me.health>>
    Try changing health to negitive one <<set $me.health = -1>><<print $me.health>>

    Set the health back to fifty and check it again in the next passage.
    [[Next Passage][$me.health = 50]]


    :: Next Passage
    My health should still be fifty: <<print $me.health>>

    Try changing it to one hundred to check object coping did not break things: <<set $me.health = 100>><<print $me.health>>
    The 'setter' (set) method gets called when you assign a value to the health property which allows the clamping of the value.
  • Well, you could simply clamp whenever you modify the value, rather than doing all your clamping in a big blob in PassageReady.  For example:

    /% Subtract 10 from $NpC.SomeStat and clamp. %/
    <<set $NpC.SomeStat = Math.clamp($NpC.SomeStat - 10, 0, 100)>>

    /% Add 20 to $NpC.SomeStat and clamp. %/
    <<set $NpC.SomeStat = Math.clamp($NpC.SomeStat + 20, 0, 100)>>

    If you don't like that idea, it is possible to do so automatically in a couple of ways.

    1. Create a method to do so for each stat.  For example:

    window.Actor.prototype.SetSomeStat = function (val) {
    this.SomeStat = Math.clamp(this.SomeStat + val, 0, 100);
    };
    Usage:

    /% Subtract 10 from $NpC.SomeStat and clamp. %/
    <<set $NpC.SetSomeStat(-10)>>

    /% Add 20 to $NpC.SomeStat and clamp. %/
    <<set $NpC.SetSomeStat(20)>>

    2. Create a getter and setter for each stat.  This will require that you change the internal name for each stat (or rehome them), the external interface would remain exactly the same, however.  For example:

    /* I'll use the underscore prefix convention for the internal name in this example (i.e. _SomeStat). */
    Object.defineProperty(window.Actor.prototype, "SomeStat", {
    get : function() { return this._SomeStat; },
    set : function(val) { this._SomeStat = Math.clamp(this._SomeStat + val, 0, 100); }
    });
    Usage:

    /% Subtract 10 from $NpC.SomeStat and clamp. %/
    <<set $NpC.SomeStat -= 10>>

    /% Add 20 to $NpC.SomeStat and clamp. %/
    <<set $NpC.SomeStat += 20>>
    The example setup, reusing the example from last time, would look like:

    // Actor constructor
    window.Actor = function (obj) {
    // Setup your data members here
    this.FirstName = "";
    this.LastName = "";
    this.Description = "";
    this._SomeStat = 0;

    // Merge properties from obj, if any
    if (typeof obj === "object") {
    Object.keys(obj).forEach(function (p) {
    this[p] = obj[p];
    }, this);
    }
    };

    // Actor serialization method
    window.Actor.prototype.toJSON = function () {
    return JSON.reviveWrapper('new Actor(' + JSON.stringify({
    /* Replicate all data members here. */
    FirstName : this.FirstName,
    LastName : this.LastName,
    Description : this.Description,
    SomeStat : this._SomeStat
    }) + ')');
    };

    // Actor getter/setter methods
    Object.defineProperty(window.Actor.prototype, "SomeStat", {
    get : function() { return this._SomeStat; },
    set : function(val) { this._SomeStat = Math.clamp(val, 0, 100); }
    });

    // Actor miscellaneous methods
    window.Actor.prototype.FullName = function () {
    return this.FirstName + " " + this.LastName;
    };

    // Actor static (non-instance) methods
    window.Actor.Debug = function (Who) {
    return "Hello, my name is " + Who.FullName() + ". You killed my father. Prepare to die.";
    };

    If you're going that far, however, you could always go all the way with something like this:

    // Actor constructor
    window.Actor = function (obj) {
    // Setup your data members here
    this._data = {
    FirstName : "",
    LastName : "",
    Description : "",
    SomeStat : ""
    };

    // Merge properties from obj, if any
    if (typeof obj === "object") {
    Object.keys(obj).forEach(function (p) {
    this[p] = obj[p];
    }, this);
    }
    };

    // Actor prototype
    Object.defineProperties(window.Actor.prototype, {
    // Serialization method
    "toJSON" : {
    value : function () { return JSON.reviveWrapper('new Actor(' + JSON.stringify(this._data) + ')'); }
    },

    // Getter/Setter methods
    "FirstName" : {
    get : function() { return this._data.FirstName; },
    set : function(val) { this._data.FirstName = val; }
    },
    "LastName" : {
    get : function() { return this._data.LastName; },
    set : function(val) { this._data.LastName = val; }
    },
    "FullName" : {
    get : function() { return this._data.FirstName + " " + this._data.LastName; }
    },
    "Description" : {
    get : function() { return this._data.Description; },
    set : function(val) { this._data.Description = val; }
    },
    "SomeStat" : {
    get : function() { return this._data.SomeStat; },
    set : function(val) { this._data.SomeStat = Math.clamp(val, 0, 100); }
    }
    });

    // Actor static (non-instance) methods
    window.Actor.Debug = function (Who) {
    return "Hello, my name is " + Who.FullName + ". You killed my father. Prepare to die.";
    };
    One nice benefit to the above is that you don't have to keep mucking with the serialization method.  You'll also note that with that setup, FullName is setup as a getter rather than a normal method, so no parens are used when accessing it (it looks like any other data member or getter).
  • Scooped by greyelf on getters/setters! :)


    Let me address this, however:
    greyelf wrote:

    One method that works with SugarCube (but not Sugarcane) is to use the defineProperty functional of modern java-script.


    Rather than saying they don't work in the vanilla story formats, it would be more correct to say that using Object.defineProperty() and/or Object.defineProperties() in the vanilla story formats will limit their Internet Explorer support, which should normally be IE8+, to IE9+ (AFAIK, that's the only real browser concession when using them).  Other that that one caveat, they should work without issue in the vanilla story formats.
  • TheMadExile wrote:

    Rather than saying they don't work in the vanilla story formats, it would be more correct to say that using Object.defineProperty() and/or Object.defineProperties() in the vanilla story formats will limit their Internet Explorer support, which should normally be IE8+, to IE9+ (AFAIK, that's the only real browser concession when using them).  Other that that one caveat, they should work without issue in the vanilla story formats.


    The vanilla story formats with 1.4.2 all display a "bad expression: $me.health = 50" error in both Firefox 31 and Chrome 37 for each of the three <<set>> macro calls in the Start passage of my example. (the integer part of message obviously changes for each of the three calls)

    So unless I'm doing something silly it definitely does not work using the vanilla story formats.
  • Without checking, I'm going to go out on a limb here and say that this has nothing to do with using Object.defineProperty() and/or Object.defineProperties() in the vanilla story formats and everything to do with the fact that the vanilla story formats do not have Math.clamp().  If you try what you're doing in the console, I'd be willing to bet that you'll see an error from your setter along the lines of "Math.clamp is not a function".  Why the vanilla story formats are not propagating that error into the displayed error message, I couldn't say.
  • LOL, so it was me doing something silly like assuming a method existed in one header because it was in another.

    Though there is no error in the Console which is what confused me in the first place.
  • greyelf wrote:

    Though there is no error in the Console which is what confused me in the first place.


    There won't be, since the exception is caught by the story format.  You'd actually have to drop to the console and run though it by hand to see it.
Sign In or Register to comment.