Howdy, Stranger!

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

Printing and Comparing Objects in SugarCube 2

I'm a beginner to Twine and SugarCube 2 and need some advice in interacting with objects.
I defined a Weapon prototype in the Story Javascript as so:
window.Weapon = function(name, cost, damage) {
	this.name = name;
	this.cost = cost;
	this.damage = damage;
}

window.Weapon.prototype.hit = function() {
	return random(0,this.damage);
};

window.Weapon.prototype.prettyName = function() {
	return name;
};

In an init passage I created some weapons and set the current weapon to be dagger:
<<set $dagger = new Weapon("dagger",30,3); 
	  $smallsword = new Weapon("small sword",50,3)>>
<<set $weapon = $dagger>>

Inside a shop passage, I would like to first print out the current weapon's name and based on current weapon show options for buying other weapons:
Your current weapon is a <<$weapon.prettyName()>>.
<<if $weapon == $dagger>>\
	[[buy small sword]]
	(30 coins, does 3 damage)\
<<elseif $weapon == $smallsword>>\
	[[buy big sword]]
	(50 coins, does 5 damage)\
<<endif>>

The first line doesn't work and literally displays "<<$weapon.prettyName()>>".
This is also the case if I replace it with
$weapon.prettyName()
(without the double braces).
What works is
$weapon.name
correctly displaying "dagger".

My first question: is there a way to overload the way a variable is printed?
If I replace the first line with just
Your current weapon is a $weapon.
It will display [object Object] as expected. But I would like to define a prototype function, such as
toString()
that returns the correct string version of the object to print.
If not, how can I fix the call to weapon.prettyName() so that it can be executed rather than printed literally?

My second question: how can I do object comparisons inside Twine 2 and SugarCube 2?
My if statements are never true; the weapon variable is not pointing to the same dagger object it was assigned to.
Why is this and how can I actually keep track of things like this?

Comments

  • mysticmuse wrote: »
    I defined a Weapon prototype in the Story Javascript as so:
    window.Weapon = function(name, cost, damage) {
    	this.name = name;
    	this.cost = cost;
    	this.damage = damage;
    }
    
    window.Weapon.prototype.hit = function() {
    	return random(0,this.damage);
    };
    
    window.Weapon.prototype.prettyName = function() {
    	return name;
    };
    
    A little old school, but it works.

    mysticmuse wrote: »
    In an init passage I created some weapons and set the current weapon to be dagger:
    <<set $dagger = new Weapon("dagger",30,3); 
    	  $smallsword = new Weapon("small sword",50,3)>>
    <<set $weapon = $dagger>>
    
    First. By "an init passage", may I assume you meant the StoryInit special passage?

    Second. Because of the way variables in most story formats are cloned (or something similar) between passages (required for the history to work), assigning $dagger to $weapon isn't going maintain the reference beyond the initial passage where the assignment was made (because of the cloning). Basically, do not depend on reference equality from one passage to the next.

    You'll probably want to do something like the following instead:
    <<set $Weapons to {
    	"dagger"     : new Weapon("dagger", 30, 3),
    	"smallsword" : new Weapon("small sword", 50, 3),
    }>>
    <<set $weapon to "dagger">>
    
    To access the specific Weapon object, you'd do something like:
    $Weapons[$weapon]
    
    To check if $weapon is a specific Weapon could be done in a couple of different ways:
    /* Check the property name. */
    <<if $weapon is "dagger">> … <</if>>
    
    /* Check the objects within $Weapons. */
    <<if $Weapons[$weapon] is $Weapons["dagger"]>> … <</if>>
    

    mysticmuse wrote: »
    Inside a shop passage, I would like to first print out the current weapon's name and based on current weapon show options for buying other weapons:
    Your current weapon is a <<$weapon.prettyName()>>.
    <<if $weapon == $dagger>>\
    	[[buy small sword]]
    	(30 coins, does 3 damage)\
    <<elseif $weapon == $smallsword>>\
    	[[buy big sword]]
    	(50 coins, does 5 damage)\
    <<endif>>
    

    The first line doesn't work and literally displays "<<$weapon.prettyName()>>".
    This is also the case if I replace it with
    $weapon.prettyName()
    
    (without the double braces).
    What works is
    $weapon.name
    
    correctly displaying "dagger".
    If you're going to use SugarCube 2, I'd suggest reading its documentation (specifically on: naked $variables, the <<print>> macro and its aliases <<=>> & <<->>). To print an expression, which includes method calls like $weapon.prettyName(), you should probably use one of the following:
    /* The <<print …>> macro. */
    <<print $weapon.prettyName()>>
    
    /* Its 100% equivalent alias, <<= …>>. */
    <<= $weapon.prettyName()>>
    

    Beyond that, and bearing in mind the reference issue suggestion I made above, I'd suggest the following:
    Your current weapon is a <<= $weapon.prettyName()>>.
    <<if $weapon is "dagger">>\
    	[[buy small sword]]
    	(30 coins, does 3 damage)\
    <<elseif $weapon is "smallsword">>\
    	[[buy big sword]]
    	(50 coins, does 5 damage)\
    <</if>>
    

    mysticmuse wrote: »
    My first question: is there a way to overload the way a variable is printed?
    If I replace the first line with just
    Your current weapon is a $weapon.
    
    It will display [object Object] as expected. But I would like to define a prototype function, such as
    toString()
    
    that returns the correct string version of the object to print.
    If not, how can I fix the call to weapon.prettyName() so that it can be executed rather than printed literally?
    I've already noted, above, what you were doing wrong with how you were trying to print prettyName(). A custom toString() method for Weapon, while easy enough to create, wouldn't really help you out due to the way printing currently works. So, I'd recommend simply calling prettyName() for now.

    mysticmuse wrote: »
    My second question: how can I do object comparisons inside Twine 2 and SugarCube 2?
    My if statements are never true; the weapon variable is not pointing to the same dagger object it was assigned to.
    Why is this and how can I actually keep track of things like this?
    Answered above.
  • edited December 2015
    Thanks a lot for the prompt response!
    I did attempt
    <<print $weapon.prettyName()>>
    
    but it gave a garbage answer. The culprit was that I was returning name instead of this.name.

    Are the environment data all cloned between passages? If you have a lot of lookup tables, would that be a performance concern, particularly since those are constant (unnecessary copy)?

    You mentioned that having the weapons based on a prototype is old school, do you recommend any better way of doing it?

    I've also read this discussion on keeping JS objects in variables and it seems that saving game state where the state is comprised by many complex objects (ex. if the player is a PC object, and every NPC is an NPC object, all with properties that are references to other objects) would be very cumbersome. Would you recommend having as flat a system as possible using Maps instead of objects as much as possible?

    This was a test RPG story so feel free to give as much "overhaul" feedback as possible, as this was just testing out different ways of setting up a RPG framework.
  • mysticmuse wrote: »
    Are the environment data all cloned between passages? If you have a lot of lookup tables, would that be a performance concern, particularly since those are constant (unnecessary copy)?
    I'm unsure what you mean by "environment data". If you mean story variables (e.g. $variable), then yes. The story variable store is cloned for each new moment/state within the history. If you meant anything else, then no.

    If you're creating objects which are essentially static (i.e. unchanging), then you would probably be better served by not placing them within story variables. I'd probably suggest creating properties on either the setup object (provided by SugarCube) or the global window object. As long as you don't trample existing properties, window is a popular choice since its properties become auto-globals.

    mysticmuse wrote: »
    You mentioned that having the weapons based on a prototype is old school, do you recommend any better way of doing it?
    That's not what I meant. Also, all objects in JavaScript are prototypal regardless of the syntactical sugar involved (i.e. whether you're using the original prototypal syntax, the classical syntax, or the newer class syntax).

    I meant that the way you're doing it is old school. By simply assigning methods onto the prototype property, you're not setting the property descriptors (e.g. whether they're enumerable). It's not a big deal. As I noted, it works.

    mysticmuse wrote: »
    Would you recommend having as flat a system as possible using Maps instead of objects as much as possible?
    It would be simpler, cognitively speaking, to use property bags (whether done via generic or Map objects), rather than more complex objects. The difference between the two, for the purpose of this discussion, being that the former are essentially collections of key/value pairs, while the latter may be instantiated and, generally, include behaviors.

    The essential idea from the thread you linked is that to properly re-instantiate any complex object, the deserialization code needs to know how to do so. The most common basic types (value types, generic objects, arrays, Date, RegExp, Map, Set) are covered. Custom complex object types need to define a toJSON method which does the requisite setup during serialization. While not really complex to do, it does require some knowledge and effort.

    So, as you're a beginner, I suppose that I'd say yes. If you can get away with it, then I would recommend sticking with property bags (i.e. data only objects), whether you're using generic or Map objects. Quite often, that's all you really need anyway.
  • Looking at Twine guides and SugarCube's documentation, I can't find clarification for where should certain scripts go. What is the purpose of the story JavaScript? Where should the definition of those property bags be and how should their definition look?
    (inside Story JavaScript or in the StoryInit passage (wouldn't that make them story variables))

    You mentioned that window's properties become auto globals, so if I define
    window.PC = { // global player character object
    	name : "Puss in Boots",
            occupation: "drug dealer"
    };
    
    Inside the Story JavaScript,
    I should be able to in a passage ask for
    Your name is <<= $PC.name>>
    
    It throws the error of not being able to read property 'name' of undefined; meaning it can't find the PC global object...

    I'm a beginner in Twine and SugarCube, but somewhat experienced in programming: so feel free to explain in as much technical depth as you need :P.
    Thanks again for the help!
  • edited December 2015
    mysticmuse wrote: »
    What is the purpose of the Story JavaScript
    It is where you place the Javscript code you want to be part of your story, it is the equivalent of a Twine 1 script tagged passage.

    The StoryInit passage is for any macro based code that you want run before the first passage is shown, you can use it to setup default values for your story variables like so.
    <<set $PC to {
    	name : "Puss in Boots",
    	occupation: "drug dealer"
    }>>
    

    The reason your <<= $PC.name>> code did not work is because you only use a $ sign when referencing a SugarCube story variable. The window.PC property/variable you created in your example is a Javascript property/variable so you would reference is like so.
    Your name is <<= PC.name>>
    
  • Ah I see. So the question still remains whether the initialization for static lookup should be done inside Story JS or the StoryInit passage. Wouldn't it be better to put it inside Story JS so that it isn't reloaded between passages?
  • greyelf answered most of your previous post's questions I believe, so I'll just touch on few things that might warrant further explanation.

    A story variable is, usually, one created/accessed within macros and is prepended by the $ (dollar sign) sigil. For example:
    <<set $hasGoldenIdol to false>>
    <<set $gold to 10>>
    <<set $PC to {
    	name       : "Puss in Boots",
    	occupation : "drug dealer"
    }>>
    
    Note: The $ sigil (i.e. when prepended to the front of an identifier) is only special, denoting a story variable, within macros (and the naked $variable formatter). Within pure JavaScript code it has no special meaning (though SugarCube does include jQuery, so the $ by itself is an alias for it).

    You may also create/access story variables within purely JavaScript code by accessing the State.variables object (which is where the story variables actually live). For example, the following JavaScript is equivalent to the TwineScript above:
    State.variables.hasGoldenIdol = false;
    State.variables.gold = 10;
    State.variables.PC = {
    	name       : "Puss in Boots",
    	occupation : "drug dealer"
    };
    

    mysticmuse wrote: »
    Where should the definition of those property bags be and how should their definition look?
    A property bag is a generic term for a simple object which contains key/value data pairs (i.e. a container of data properties). You've already seen and used examples of them. For example, using a generic object which only consists of data properties:
    /* Via a story variable (in TwineScript). */
    <<set $PC to {
    	name       : "Puss in Boots",
    	occupation : "drug dealer"
    }>>
    
    /* Via a story variable (in JavaScript). */
    State.variables.PC = {
    	name       : "Puss in Boots",
    	occupation : "drug dealer"
    };
    
    /* Via a window property (in JavaScript). */
    window.PC = {
    	name       : "Puss in Boots",
    	occupation : "drug dealer"
    };
    

    If the Map object is more your style, you can do essentially the same thing with it, though accessing the values will be different:
    /* Via a story variable (in TwineScript). */
    <<set $PC to new Map([
    	[ "name", "Puss in Boots" ],
    	[ "occupation",  "drug dealer" ]
    ])>>
    
    /* Via a story variable (in JavaScript). */
    State.variables.PC = new Map([
    	[ "name", "Puss in Boots" ],
    	[ "occupation",  "drug dealer" ]
    ]);
    
    /* Via a window property (in JavaScript). */
    window.PC = new Map([
    	[ "name", "Puss in Boots" ],
    	[ "occupation",  "drug dealer" ]
    ]);
    

    mysticmuse wrote: »
    So the question still remains whether the initialization for static lookup should be done inside Story JS or the StoryInit passage. Wouldn't it be better to put it inside Story JS so that it isn't reloaded between passages?
    I recommend that all story initialization be done either within a JavaScript section (Story JavaScript in Twine 2, a script tagged passage in Twine 1) or the StoryInit special passage. Both are executed exactly once at story startup/initialization (to be clear, unlike most of the Story… and Passage… special passages, the StoryInit special passage is not reprocessed upon passage navigation).

    As to which one to use for what, apples to apples and all that. I generally recommend putting any purely JavaScript setup code within your story's Story JavaScript section and any macro/TwineScript setup code within your story's StoryInit special passage.
  • Thanks, all this has been quite helpful!
Sign In or Register to comment.