Howdy, Stranger!

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

Setter Link broken inside for loop for SugarCube 2

While building an inventory system I ran into an interesting problem:
''Wearing''
* top: //<<= clothes[PC.top].name>>// [[inspect][$inspected_item = PC.top]]
* bottom: //<<= clothes[PC.bot].name>>// [[inspect][$inspected_item = PC.bot]]
* shoes: //<<= clothes[PC.shoes].name>>// [[inspect][$inspected_item = PC.shoes]]
<<for $i = 0; $i < PC.acc.length; ++$i>>\
* accessory: //<<= clothes[PC.acc[$i]].name>>// [[inspect][$inspected_item = PC.acc[$i]]]
<</for>>\

''Backpack (aka your servant)''
<<for $i = 0; $i < PC.back.length; ++$i>>\
<<if clothes.hasOwnProperty(PC.back[$i])>>\
* (<<= clothes[PC.back[$i]].loc>>) //<<= clothes[PC.back[$i]].name>>// <<= $i>> <<= PC.back[$i]>> [[inspect][$inspected_item = PC.back[$i]]]
<</if>>\
<</for>>\

Here I'm using a "temporary" variable $inspected_item as a way to pass a parameter into the "inspect" passage.
The inspect passage is:
$inspected_item
<<if clothes.hasOwnProperty($inspected_item)>>\
<<= clothes[$inspected_item].longdesc.capitalizeFirstLetter()>>
<<endif>>\
clothes is a global lookup table mapping strings to Clothing objects, which have longdesc as a property.

The setter links work as intended for the non-loop portions, but when it gets to the backpack - tested with
PC.back = ["test_top", "test_bot"]
the inventory page works normally:
// ...proper non-loop stuff
Backpack (aka your servant)
(top) test top 0 test_top inspect
(bot) test bot 1 test_bot inspect
but when I click on inspect, it seems the $inspected_item has not been set:
$inspected_item
Both the loop counter and the value I want to set to have the proper value before the link setter, so it points to the link setter as the problem here. Any ideas on what might be wrong or a better way of doing what I'm trying to do?

Comments

  • The assignment part of a Setter link is processed when the link is clicked and not at the time the link is created, which is why the links within your <<for>> loops are not working as you expected them to. At the time of the click event the $i variable is equal to PC.back.length which results in $inspected_item being assigned an invalid value.

    The standard method used to overcome this issue is to use a <<print>> macro to create the Setter Link.
    note: I am assuming the PC.acc array contains String values, note the usage of both single and double quotes to allow the quoting of the String values.
    * accessory: //<<= clothes[PC.acc[$i]].name>>// <<print '[[inspect][$inspected_item = "' + PC.acc[$i] +'"]]'>>
    
  • edited January 2016
    Thanks a bunch!
    I'm also running into trouble with a "wear" passage. It works as intended, except when I want to undo (using SugarCube menu's backward navigation). I use a similar method to setup the variables the "wear" passage needs:
    ''Backpack (aka your servant)''
    <<for $i = 0; $i < PC.back.length; ++$i>>\
    <<if clothes.hasOwnProperty(PC.back[$i])>>\
    * (<<= clothes[PC.back[$i]].loc>>) //<<= clothes[PC.back[$i]].name>>// <<= '[[inspect][$inspected_item = "' + PC.back[$i] +'"]]'>> | <<= '[[wear][$worn_item = "' + PC.back[$i] +'"; $replaced_slot = "' + $i + '"]]'>>\
    <</if>>\
    <</for>>\
    

    The "wear" passage is:
    <<set $replaced_loc = clothes[$worn_item].loc;
    	$replaced_item = PC[$replaced_loc];
    	PC[$replaced_loc] = $worn_item;
    	PC.back[$replaced_slot] = $replaced_item;>>\
    <<unset $replaced_loc>>\
    <<unset $replaced_slot>>\
    <<display inventory>>
    

    The problem is that PC.back's state is restored to its previous state, but PC.top, PC.bot, PC.shoes, PC.acc are not. So what can happen is that I will have two copies of the "test_top" and "test_bot", with the originally worn items disappearing. Do you know what could be causing this problem?
  • edited January 2016
    As explained to you earlier, SugarCube (all the story formats) only manages the History of story variables.
    eg. the ones declared via the <<set>> macro with names that start with a $ sign or in SugarCube's case the ones declared via the Javascript State.variables associated array.

    Without an example of the declaration of the PC object and variable I have to assume that it is a property of the Javscript window object (because it's name does not contain a $ sign) which means that that object itself is not part of the History system.
    Depending on how you implemented the PC object's properties it may be possible for them to be accessing elements of the State.variables array and thus being effected by History.

    You will need to supple an example of the declaration of the PC object for a better answer.
  • edited January 2016
    Ah, mixing story and non story variables could be quite problematic...
    PC is declared and defined in the Story JavaScript like so:
    window.PC = {
    	// other details...
    	// inventory
    	top  : "default_top",
    	bot  : "default_bot",
    	shoes : "leather_boots",
    	acc  : [],	// accessories
    	back : ["test_top","test_bot"],	// backpack
    }
    
    The reason I have it defined here is that I have a lot of methods that belong to the PC object. Would one way to address this be to create PC under State.variables(). Is this the correct way to inject state variables inside Javascript?
    var PC = State.variables.PC = {
    	// other details...
    	// inventory
    	top  : "default_top",
    	bot  : "default_bot",
    	shoes : "leather_boots",
    	acc  : [],	// accessories
    	back : ["test_top","test_bot"],	// backpack
    }
    // then define methods under the PC reference directly
    
    It seems to work (obviously after replacing all mentions of PC with $PC).

    So I guess the lesson of the day is to always keep non constant variables (things that can be changed by the story) as story variables...

    Since making the change, I encountered another problem. Everything works fine until I save and load the save. Upon loading the save, it seems that the functions defined under $PC is no longer defined (I'm guessing that loading doesn't force a reevaluation of the JS, so the functions are defined on the previous $PC object, and after load the story uses another $PC object...)
  • TheMadExile would be a better person to explain the costs/downsides of adding methods to an object being stored in State, in regards to object cloning and the saving/loading of the story state.

    Personally if I was implementing an object where I wanted both state that was tracked by History and methods that weren't then I would use the M and C parts of the MVC pattern.

    eg. The Control would be a global variable named PC which is an instance of a custom Javascript class, with get/set properties or methods that used State.variables.PC as it's Model.
  • So my interpretation of that is to make a PlayerCharacter class part of window, with prototype methods defined, and for PC to be an instance of that class:
    window.PlayerCharacter = function(...) {
    // set this.whatever = whatever for all properties of PC
    };
    // instance method
    window.PlayerCharacter.prototype.somefunc = function() {
    // definition here
    }
    // more instance methods
    State.variables.PC = new PlayerCharacter(
    // very long list properties
    );
    

    I'm still getting the same error of
    Error: <<=>>: bad expression: undefined is not a function
    when I call the method
    <<= $PC.somefunc()>>
    
  • mysticmuse wrote: »
    So I guess the lesson of the day is to always keep non constant variables (things that can be changed by the story) as story variables...
    As I noted in your previous thread, yes. Data which is essentially immutable should probably not be placed within story variables. Conversely, data that is mutable should be placed within story variables.

    mysticmuse wrote: »
    Since making the change, I encountered another problem. Everything works fine until I save and load the save. Upon loading the save, it seems that the functions defined under $PC is no longer defined (I'm guessing that loading doesn't force a reevaluation of the JS, so the functions are defined on the previous $PC object, and after load the story uses another $PC object...)
    That's odd. If you're defining the methods directly on the object itself, then they should have been restored to the object upon deserialization.

    Looking into this, it seems that when I switched over to the new (de)serialization back end I introduced a bug where function expressions were being evaluated as definitions causing the deserialization of those properties to error out. My apologies. I'll publish a fix for this soon.


    In the meantime. If you're attaching methods to your objects, then they aren't really simple objects anymore (i.e. just bags of data properties). If you're going to use complex objects, then you might as well use the classical syntax (i.e. provide a constructor and place your methods on its prototype). Though, doing so will require you to define a toJSON method as well, which returns a JavaScript code string which recreates the object as it existed when serialized. As I noted in your previous thread, it's not really difficult to do once you know how. For example:
    // `Player` constructor.
    window.Player = function (data) {
    	Object.assign(this, {
    		/*
    			Specify your default values here.  They'll be overridden by
    			any properties passed in via the `data` object parameter.
    		*/
    		// other details...
    		// inventory
    		top   : "default_top",
    		bot   : "default_bot",
    		shoes : "default_shoes",
    		acc   : [], // accessories
    		back  : [], // backpack
    	}, data);
    };
    
    // `toJSON` method.
    window.Player.prototype.toJSON = function () {
    	/*
    		This ensures that all of the object's own enumerable properties
    		are passed to the constructor, thus restoring the state of the
    		original object upon deserialization.
    	*/
    	return JSON.reviveWrapper('new Player('
    		+ JSON.stringify(Object.assign({}, this))
    		+ ')');
    };
    
    // Other `Player.prototype` methods....
    

    How you'd use it to create a default instance of Player:
    /* $PC via TwineScript (in StoryInit or wherever needed). */
    <<set $PC to new Player()>>
    
    /* $PC via JavaScript (script tagged passage or the <<script>> macro). */
    State.variables.PC = new Player();
    
    How you'd use it to create an instance of Player, while overriding some of the defaults:
    /* $PC via TwineScript (in StoryInit or wherever needed). */
    <<set $PC to new Player({
    	top   : "leather_tunic",
    	bot   : "leather_trousers",
    	shoes : "leather_boots",
    	back  : [ "test_top", "test_bot" ],
    })>>
    
    /* $PC via JavaScript (script tagged passage or the <<script>> macro). */
    State.variables.PC = new Player({
    	top   : "leather_tunic",
    	bot   : "leather_trousers",
    	shoes : "leather_boots",
    	back  : [ "test_top", "test_bot" ],
    });
    
  • Looking into this, it seems that when I switched over to the new (de)serialization back end I introduced a bug where function expressions were being evaluated as definitions causing the deserialization of those properties to error out. My apologies. I'll publish a fix for this soon.
    And the fix has been published (SugarCube v2.1.2).

    While I was in there, I also updated the print formatting to use an object's custom toString() method ("custom" here meaning not the Object.prototype.toString method), if it has one. So, that should work for you now as well.
  • Thanks for the tips! The constructor with Object.assign clarifies the semantics greatly and allows for a lot more flexibility in what parameters the object can have :P!

    2.1.2 seems to work fine.
Sign In or Register to comment.