Howdy, Stranger!

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

Need help getting rid of my OOP. [SugarCube]

I recently got motivation to work on my Twine (rather, Tweego, which SugarCube is disappointingly losing support for) game again, and now I have completely lost it. I just get so mad and so demotivated when I run into these roadblocks that I'm starting to think I'm just not cut out to make games of any kind whatsoever.

I'm trying to make a text-based RPG, basically. So I figure I'll write a support library in Javascript. I use ES6 with traceur because the features it offers are pretty nice. OOP is always good, right? So I make a few classes with a little bit of inheritance and they're working great!

And then I save my game.

While trying to get functionality restored post-load, I came upon the realization that all of my class information is being discarded when the game is saved. Of course it is. It's impractical to expect that functions and classes and the like would be saved. Dumb objects with lots of nesting will work, but they can't have functions.

The entire way I have structured my code has been destroyed. I have absolutely no motivation to think about this problem right now, so I would like to ask for help with ideas restructuring everything.

Oh, by the way... there's no way to use userlib.js with Tweego. I had to modify the header. Also, I don't really appreciate that the "made with tweego" message got removed.

Anyways, the source for my project is here: http://tiz.qc.to/pub/shine-0.0.4.zip

Comments

  • I cant download your source to check because the connection to the server times out but I am guessing that your problem is that the javascript objects you are creating contain methods/functions and when you load one of these object within a save it is these methods/functions that are gone.

    You maybe able to get around this problem by using SugarCubes "config.saves.onLoad" feature to re-add these methods/function to the object when you load a save. The TheMadExile posted about how to solve this issue as a reply to a question on a different forum, which I wont link to here due to the other forum's adult related content.

    If what I described above sounds like your problem then PM me and I will supply you a link to the post.
  • The dynamic DNS was pointing to the wrong place. The fix should be propagated to most DNS servers by now.

    [quote]I am guessing that your problem is that the javascript objects you are creating contain methods/functions and when you load one of these object within a save it is these methods/functions that are gone.
    ...I already said that.

    [quote]You maybe able to get around this problem by using SugarCubes "config.saves.onLoad" feature to re-add these methods/function to the object when you load a save.
    I don't know if it's quite that simple. I really don't know what traceur is doing under the hood to make classes work the way they do, let alone what I would need to do to make those objects instances of the various classes once again.

    It would probably be a lot easier to just not have functions in my objects but that requires rethinking a lot of stuff.
  • I now get a "The requested URL /pub/shine-0.0.4.zip was not found on this server." error.
  • Awesome, my sync software has gone belly up. Another thing I needed to break on this terrible weekend.

    I copied it there manually, you should be able to get it now. Apologies for all the problems.
  • TiZ wrote:

    (rather, Tweego, which SugarCube is disappointingly losing support for)



    Oh, by the way... there's no way to use userlib.js with Tweego. I had to modify the header. Also, I don't really appreciate that the "made with tweego" message got removed.


    Calm.  As the author of both, I can assure you that SugarCube is not losing support for TweeGo (or vice versa).  I simply haven't pushed a TweeGo update in a while.  Both of the above issues have now been addressed.


    TiZ wrote:

    I'm trying to make a text-based RPG, basically. So I figure I'll write a support library in Javascript. I use ES6 with traceur because the features it offers are pretty nice. OOP is always good, right? So I make a few classes with a little bit of inheritance and they're working great!

    And then I save my game.

    While trying to get functionality restored post-load, I came upon the realization that all of my class information is being discarded when the game is saved. Of course it is. It's impractical to expect that functions and classes and the like would be saved. Dumb objects with lots of nesting will work, but they can't have functions.

    The entire way I have structured my code has been destroyed. I have absolutely no motivation to think about this problem right now, so I would like to ask for help with ideas restructuring everything.


    Without trying to dig through your entire codebase to find out exactly what you're having issues with and assuming that you're saving your objects in $variables.

    You can probably get around whatever your object revival problems are by writing toJSON() methods which save the objects' data and leverage SugarCube's JSON.reviveWrapper() to ensure their revival.  Basically, something like the following examples from previous threads: A basic example reusing the constructor:

    class Character extends Combatant {
    /* original code */

    toJSON() {
    return JSON.reviveWrapper('new Character(' + JSON.stringify({
    fullName : this.fullName,
    gender : this.gender,
    invItem : this.invItem,
    invIn : this.invIn,
    invGet : this.invIn,
    invFirstGet : this.invFirstGet,
    baseMaxHP : this.baseMaxHP,
    baseStr : this.baseStr,
    baseSpi : this.baseSpi,
    baseAtk : this.baseAtk,
    baseDef : this.baseDef,
    baseAgi : this.baseAgi,
    pul : this.pul,
    asp : this.asp,
    sop : this.sop,
    pro : this.pro,
    her : this.her,
    spi : this.spi,
    cun : this.cun
    }) + ')');
    }
    }
  • TheMadExile wrote:
    Calm.  As the author of both, I can assure you that SugarCube is not losing support for TweeGo (or vice versa).  I simply haven't pushed a TweeGo update in a while.  Both of the above issues have now been addressed.

    Sorry for coming off all pissy about that. I'm happy to hear that those issues have been fixed; I'll be able to get rid of the external .js this way.

    TheMadExile wrote:
    Without trying to dig through your entire codebase to find out exactly what you're having issues with and assuming that you're saving your objects in $variables.

    That's correct.

    TheMadExile wrote:
    probably[/i] get around whatever your object revival problems are by writing toJSON() methods which save the objects' data and leverage SugarCube's JSON.reviveWrapper() to ensure their revival.  Basically, something like the following examples from previous threads:

    Re: Keeping complex JS objects in variables?
    Re: Javascript Object Prototype Syntax (SugarCube)

    That's interesting... so it's a string containing code that gets executed, that I can use to turn the dumb object back into a smart object, then? I wonder if I can just stringify the object itself and avoid all of that cruft. Or maybe Sugar can help me shortcut it somehow.

    EDIT: Your example code seems to work! Now to figure out some way to shortcut it. I can't just use "this" because stringify calls toJSON and it'll get stuck in a loop.
  • Sorry for double posting.

    Every attempt I make to shortcut the tedium of specifying every last property results in this:
    "@@revive@@(new Character({"fullName":"Ellie Luisante","gender":"f","invItem":"Purse of Holding","invIn":"in your Purse of Holding","invGet":"You snatch %loot% up into your Purse of Holding.","invFirstGet":"You snatch %loot% up into your Purse of Holding. You figure that if the purse and clothing conspiracy is going to keep you from having practical pockets, then you might as well have a purse with infinite space. The item dimension it links to gives you just that. This is better than pockets anyway.","baseMaxHP":1050,"baseStr":90,"baseSpi":75,"baseAtk":90,"baseDef":110,"baseAgi":125,"pul":200,"asp":25,"sop":120,"pro":110,"her":100,"spi":100,"cun":80,"name":"Ellie","id":"ellie","equipment":[],"_hp":1050,"_tp":50,"lastName":"Ellie","weapon":{"name":"Flower Blade","desc":"A quaint jian with a handguard styled like a flower."}}))"
    My last attempt was this. I don't expect you to know anything about Sugar.js, but I feel like there's got to be a better way.
    toJSON () {
    return JSON.reviveWrapper("new Character(" + JSON.stringify(
    Object.reject(this, "__proto__")) + ")");
    }
  • TiZ wrote:

    That's interesting... so it's a string containing code that gets executed, that I can use to turn the dumb object back into a smart object, then?


    Basically.  SugarCube serializes its $variable store with JSON, which doesn't handle functions and methods, so to restore objects with attached methods magic must ensure.  I'll assume you know about the toJSON() method.  The JSON.reviveWrapper() method wraps the given code string in the boilerplate necessary to have it executed when deserializing it later, thereby reviving an equivalent copy of the original object.

    If you want to see exactly what's going on, check the bottom of intrinsics.js for SugarCube's JSON extensions.


    TiZ wrote:

    I wonder if I can just stringify the object itself and avoid all of that cruft.


    Not via JSON.  Oh, you can restore the object and its methods, SugarCube will do that by default, however, that will not properly restore two types of relationships: references and scopes.  Only by creating a new object with its constructor or a revival method and the requisite data can you ensure that all references and scopes are recreated as they need to be.


    TiZ wrote:

    Or maybe Sugar can help me shortcut it somehow.


    Maybe.  I haven't really looked at Sugar enough to be able to say.


    Beyond the above, my example was simply meant to give you a idea about what would be required.  I wouldn't necessarily suggest doing it exactly like that.

    For example, if you were only keeping serializable data in this (not counting methods, they can be excluded by type), then you could do something like this:

    toJSON() {
    var obj = {};
    Object.keys(this).forEach(function (p) {
    if (typeof this[p] !== "function") { // data only, no methods
    obj[p] = this[p];
    }
    }, this);
    return JSON.reviveWrapper('new Character(' + JSON.stringify(obj) + ')');
    }
    As another example, if you were keeping your serializable data in a private-ish member like _data, then you could do something like this:

    toJSON() {
    return JSON.reviveWrapper('new Character(' + JSON.stringify(this._data) + ')');
    }
  • TheMadExile wrote:

    For example, if you were only keeping serializable data in this (not counting methods, they can be excluded by type), then you could do something like this:

    toJSON() {
    var obj = {};
    Object.keys(this).forEach(function (p) {
    if (typeof this[p] !== "function") { // data only, no methods
    obj[p] = this[p];
    }
    }, this);
    return JSON.reviveWrapper('new Character(' + JSON.stringify(obj) + ')');
    }


    This approach results in $chars.ellie being the same garbage string. I'm going to presume I made my post while you were elaborating, so I'll repost the garbage string here:
    "@@revive@@(new Character({"fullName":"Ellie Luisante","gender":"f","invItem":"Purse of Holding","invIn":"in your Purse of Holding","invGet":"You snatch %loot% up into your Purse of Holding.","invFirstGet":"You snatch %loot% up into your Purse of Holding. You figure that if the purse and clothing conspiracy is going to keep you from having practical pockets, then you might as well have a purse with infinite space. The item dimension it links to gives you just that. This is better than pockets anyway.","baseMaxHP":1050,"baseStr":90,"baseSpi":75,"baseAtk":90,"baseDef":110,"baseAgi":125,"pul":200,"asp":25,"sop":120,"pro":110,"her":100,"spi":100,"cun":80,"name":"Ellie","id":"ellie","equipment":[],"_hp":1050,"_tp":50,"lastName":"Ellie","weapon":{"name":"Flower Blade","desc":"A quaint jian with a handguard styled like a flower."}}))"
    If I use Object.select with the keys of Character's prototype, it doesn't give me the garbage string, but the properties of the inherited class are missing, so I would need to go up the chain and grab all those keys.

    Can you tell me what causes this kind of thing to happen so I can try to come up with a better fix?
  • TiZ wrote:

    This approach results in $chars.ellie being the same garbage string. I'm going to presume I made my post while you were elaborating, so I'll repost the garbage string here:


    What, exactly, is wrong with that string?
  • Ah, my bad. I should have given way more context. Apologies.

    When I load the game, I expect $chars.ellie to be an object, but it is that string instead.
  • TiZ wrote:

    When I load the game, I expect $chars.ellie to be an object, but it is that string instead.


    Ah, okay.  That would have to be because the code is throwing an exception.  There's nothing I see off-hand which would cause an exception, so I'm guessing that it's coming from the Character() constructor.  Probably the result of grabbing properties you shouldn't be, so you might have to end up culling some properties.

    You could try the code manually to see what exception is thrown.  For example, putting this in a script tagged passage:

    eval('new Character({"fullName":"Ellie Luisante","gender":"f","invItem":"Purse of Holding","invIn":"in your Purse of Holding","invGet":"You snatch %loot% up into your Purse of Holding.","invFirstGet":"You snatch %loot% up into your Purse of Holding. You figure that if the purse and clothing conspiracy is going to keep you from having practical pockets, then you might as well have a purse with infinite space. The item dimension it links to gives you just that. This is better than pockets anyway.","baseMaxHP":1050,"baseStr":90,"baseSpi":75,"baseAtk":90,"baseDef":110,"baseAgi":125,"pul":200,"asp":25,"sop":120,"pro":110,"her":100,"spi":100,"cun":80,"name":"Ellie","id":"ellie","equipment":[],"_hp":1050,"_tp":50,"lastName":"Ellie","weapon":{"name":"Flower Blade","desc":"A quaint jian with a handguard styled like a flower."}})');

    If it is because you're getting properties that you shouldn't be passing to the constructor, then you'll either have to segregate your serializable data from the rest or filter the non-serializable properties out.

    For example, using one the examples I gave in a previous reply, you could do something like this to filter:

    toJSON() {
    var obj = {};
    Object.keys(this).forEach(function (p) {
    // exclusions
    if (typeof this[p] === "function") return; // no methods
    if (p[0] === "_") return; // no properties starting with an underscore
    if (p === "weapon") return; // no properties named "weapon"
    // Et cetera, you get the idea....

    // safe to include
    obj[p] = this[p];
    }, this);
    return JSON.reviveWrapper('new Character(' + JSON.stringify(obj) + ')');
    }
  • I see. Thank you for all of your help. I will investigate and return with findings. :)
  • So the weapon property was what was breaking it... apparently it can't handle nested objects? So what I decided to do was add id properties to all the weapons, and make a special setter for the weapon property on Character. The weapons never change, so they're just stored globally. So if the property is set with a string, it looks up the weapon and sets it that way. I also have to preserve the _ values... but I have to transform them into the public facing properties. So basically:
    toJSON () {
    var obj = {};
    Object.each(this, (key, value) => {
    if (key === "_weapon") obj.weapon = this._weapon.id;
    else if (typeof this[key] === "function") return;
    else if (key[0] === "_") obj[key.from(1)] = value;
    else obj[key] = value;
    });
    return JSON.reviveWrapper("new Character(" + JSON.stringify(obj) + ")");
    }
    I'll keep this limitation in mind as I flesh out the game's data structures.
  • As a suggestion, you're probably better off putting any exclusionary/terminating conditionals ahead of the output selection conditionals chain, like so:

    if (typeof this[key] === "function") return;

    if (key === "_weapon") obj.weapon = this._weapon.id;
    else if (key[0] === "_") obj[key.from(1)] = value;
    else obj[key] = value;
    Unless this._weapon can be a function, there's little reason to have the return in the middle of the pile (so to speak).
  • You're absolutely right, that's stylistically much better. Thanks. :)
  • I know another double-post is sin, but editing won't get your attention and summon you back here. :P

    So I just realized I'm doing a potentially terrible thing where I store character information in multiple places; there is the $chars variable which should be the one that actually holds all the data, the $player variable which holds the character you are currently playing as (I intend to have the ability to change perspectives), and the $party variable with is an array representing your current battle party.

    So... I was under the impression that JavaScript passed objects by reference. if you do obj2 = obj1 and change something on obj2, it changes obj1 as well. If I do something like that in the browser console, that's how it works! But it seems like when actually doing stuff in the story, it doesn't work that way. For example, if you grab any item in the game, it will show a special message for the first time you get something, and set a flag on the player character saying that the message has been shown. That flag is set on $player, but it's not set on $chars.ellie.

    I've already got workarounds in mind, namely more getter functions and replacing object references with ids... but as a bit of curiosity, can you explain to me why this happens?
  • Each time you move between passages the History sub-system clones/copies all the variables, so each passage visited ends up with its own set of the variables.

    I believe that this would result in the situation where two variables that used to point to the same instance of an object now point to two different instances of a similar object both with the same attribute values. In this case changing the value of an attribute in one instance would not effect the other.
  • Pretty much what greyelf said.  My current standing recommendation is to store all similar objects in a master object/array and then to pass around keys/indices to the master, rather than using references for this reason.
Sign In or Register to comment.