Using drag and drop to set variables

+3 votes
asked Aug 11 by litrouke (1,590 points)
edited Aug 12 by litrouke

Technically two questions (and a verbose explanation, for which I apologize). Using Twine 1 with Sugarcube 2.

The setup: Instead of the reader choosing from hypertext links, the reader drags an icon into a droppable box. Their choice of icon (let's say, one of four options) sets a variable upon that drop (ie, $firstIcon = true). The reader pushes a button to confirm their choice, and they move on to the next passage as determined by that variable.

Question one: I have the general drag/drop mechanics worked out, but I don't know how to actually set the variable per icon... Because jQuery's draggable doesn't have an event for "got dropped into somewhere," as far as I can tell. I have to set the variable on the droppable side -- but then how do I get Javascript to recognize which of four icons was dropped into the container? Something with $('this')?? My JS knowledge does not extend that far. edit: solved!

Question two: I have a 'redo' button that negates the reader's initial choice of icon and allows everything to be draggable/droppable again so they can pick a new choice. I want to send all icons back to their original positions prior to being dragged.... Is that possible?

Here's the code I've been working on so far. Please be kind with baby's first JS, but feel free to point out where I'm being redundant or silly. Right now I have two icons that match two different droppable containers for easy testing, and then two buttons -- one to move the reader to the next passage, one to start over. The buttons are initially hidden; once the reader successfully drags an icon into a box, the buttons appear.

HTML:

<div id="icon1"></div>
<div id="icon2"></div>

<div id="firstBox"></div>
<div id="secondBox"></div>

<span class="btn1"><<button "next passage">><<goto "wherever">><</button>></span>
<span class="btn2"><<button "redo">><</button>></span>

CSS:

#icon1 {
float: left;
height: 100px;
width: 100px;
background-color: white;
}

#icon2 {
float: left;
height: 100px;
width: 100px;
background-color: grey;
}

#firstBox {
float: right;
height: 200px;
width: 200px;
background-color: blue;
}

#secondBox {
float: right;
height: 200px;
width: 200px;
background-color: yellow;
}

JS:


//hide the buttons at first
jQuery(document).ready( 
	function($) {
		$('button').hide();
});


//set up the icons and containers
$( function() {
    $( "#icon1" ).draggable({
	revert: true,
	containment: "document",
	scope: "first",
	snap: true
	});

    $( "#icon2" ).draggable({
	revert: true,
	containment: "document",
	scope: "second",
	snap: true
	});

	$( "#firstBox" ).droppable({
	scope: "first",
	drop: handleFirstDrop
	});

	$( "#secondBox" ).droppable({
	scope: "second",
	drop: handleSecondDrop
	});
});


//when the right icon gets dropped into the right box, set a variable accordingly. but i want to have multiple icons within the scope of each container... and i dont know how to differentiate them.

function handleFirstDrop( event, ui ) {
    ui.draggable.draggable( 'disable' );
    $(this).droppable( 'disable' );
    ui.draggable.position( { of: $(this), my: 'left top', at: 'left top' } );
    ui.draggable.draggable( 'option', 'revert', false );
    state.active.variables["correctCards"]++;
	state.active.variables["firstChoice"] = true;

  if ( state.active.variables["correctCards"] == 1 ) {
    $('button').show();
    $('.ui-draggable').draggable('disable');
    }
} 


function handleSecondDrop( event, ui ) {
    ui.draggable.draggable( 'disable' );
    $(this).droppable( 'disable' );
    ui.draggable.position( { of: $(this), my: 'left top', at: 'left top' } );
    ui.draggable.draggable( 'option', 'revert', false );
    state.active.variables["correctCards"]++;
	state.active.variables["secondChoice"] = true;

//once an icon has been dropped, show the buttons and freeze the other icons

  if ( state.active.variables["correctCards"] == 1 ) {
    $('button').show();
    $('.ui-draggable').draggable('disable');
    }
} 

//the redo button. don't know how to send the icons back to their original positions tho.
$( function() {
	$('.btn2').click(function() {

		state.active.variables["correctCards"] = 0;
		state.active.variables["firstChoice"] = false;
		state.active.variables["secondChoice"] = false;
		$('button').hide();
		$('.ui-draggable').draggable('enable');
		$('.ui-droppable').droppable('enable');

	});
});

Oh, and if the reader presses the 'I'm good to go ahead with my choice' button, they're sent through a dummy passage that invisibly executes this code to determine where to send them:

<<if $firstChoice is true>><<goto "first choice">>
<<elseif $secondChoice is true>><<goto "second choice">>
<</if>>

I'm so sorry for the length of this question, but I wanted to be as thorough as possible haha.

3 Answers

+1 vote
answered Aug 13 by Chapel (27,390 points)
selected Aug 13 by litrouke
 
Best answer

It seems you're using jQuery UI, right?  My knowledge of it is extremely limited, but I'm pretty sure that the way it works is by changing the element's CSS position value to be relative, and then assigning it a location on the screen in pixels, based on where you drag the element to.  So the easiest way to 'snap' elements back is to just make their positioning no longer relative.  Adding this code to your redo button should, theoretically anyway, do that:

[...]
$('#icon1').css('position', '');
$('#icon2').css('position', '');
[...]

That said, there are a couple of smaller issues with your code:

  1.  You're using the $(function ... ) and $(document).ready(...) calls.  Generally speaking, these calls are not useful in Twine.  You're still forced to use them in Harlowe because there aren't really any alternatives, but in SugarCube, you should generally use task objects instead and tie your code to the passage rendering process instead of the document's loading.
  2. This event handler could be better. 
    $( function() {
    	$('.btn2').click(function() {
    
    ...
    
    	});
    });
    
    Instead, try:
    
    $(document).on('click', '.btn2 button', function() {
    
    ...
    
    });

    The second code chunk is generally safer; it works for elements that are dynamically added (i.e. most Twine elements) and is leaner.  You also don't generally need to wrap listeners like this in ready() calls [ $(function ....) is a shortcut for $(document).ready(...) ].

  3. It's 'State', not 'state'.  The lower-cased version is an alias to make migrating scripts from SugarCube 1 and Twine 1 formats easier.  It's not a huge deal, but that alias might not exist forever, and is probably best considered deprecated.  Also, you don't need to specify the active moment for the vast majority of State properties; i.e. 'State.active.variables' is functionally the same as the shorter 'State.variables'.

commented Aug 13 by litrouke (1,590 points)

Fixed #2 and #3, thank you so much! As for #1, it technically works, but for some reason it brutalized my layout... before vs. after. Seems like it kicked the icons out of their container, but I have no idea why. I tried both predisplay and postdisplay to see if that helped, but both affect the layout. For reference, the relevant CSS...

body {
background-color: white;
margin-left: -26em;
color: black;
}

#bigContainer {
width: 1300px;
height: 650px;
position: relative;
background-color: #221f27;
}

#iconContainer {
position: absolute;
left: 21px;
bottom: 0;
width: 97%;
height: 130px;
background-color: /*#221f27*/ red;
border-bottom: 14px solid #221f27;
}

#icon1, #icon2, #icon3, #icon4 {
position: absolute;
bottom: 0;
padding: 5px;
height: 105px;
width: 105px;
}

#icon1 {
left: 0;
background-color: rgb(239,239,214);
}

#icon2 {
left: 130px;
background-color: rgb(215,227,180);
}

#icon3 {
left: 260px;
background-color: rgb(155,206,170);
}

#icon4 {
left: 390px;
background-color: rgb(82,178,157);
}

As for the positioning code, sadly it doesn't seem to have any effect. It doesn't produce an error, but nor does it move the icons. You said, "I'm pretty sure that the way it works is by changing the element's CSS position value to be relative" -- is there an issue with the fact that the icons are initially absolute?

Thanks for all you time and patience as always.

commented Aug 13 by Chapel (27,390 points)

It's hard to tell why the task objects are doing that from the provided code, but they really shouldn't be as near as I can tell, if that's any consolation. 

I only did some bare bones testing on the initial code I provided, and it did work fine with the CSS you had provided in the question itself, which was only floated, with no real positioning. A more complex solution that uses a similar approach would be to grab the relevant style rules and reapply them later. For example: 

setup.iconStyles = function (el) {
    var styles = {
       pos  : $(el).css('position'),
       left : $(el).css('left')
    };
    return clone(styles); 
};

Then you can grab the initial style rules at some point after the elements are drawn and before they are moved (probably a postdisplay): 

setup.icon1 = setup.iconStyle('#icon1');

Then cause the redo button to reset the position: 

// redo button
...
$('#icon1').css({
    'position' : setup.icon1.pos, 
    'left'     : setup.icon1.left
});

That all being said, it's hard to work on this without seeing all the code. If you want to post your twine 1 .tws file to a file sharing service like Drive or Dropbox, I'd be willing to take a look at it, but I definitely understand not wanting to do that as well. 

Note that I didn't test the above code, so it may include syntax errors, but I believe it's close enough for you to get the idea. 

commented Aug 13 by litrouke (1,590 points)

Didn't have a chance yet to try out what you just posted, but I don't mind sharing my shame with the world. I haven't used Drive links much, but this should work, I think. After I get home, I'll try out what you suggested. Thanks again so much!

commented Aug 13 by Chapel (27,390 points)
edited Aug 13 by Chapel

I got the file and that made things much easier.  Here's a much simpler way than either of my initial suggestions:

$(document).on('click', '.btn2 button', function() {

    State.variables["correctCards"] = 0;

    $('button').hide();

    // you can 'chain' all these, fyi
    $('.ui-draggable') 
        .draggable('enable')
        .addClass("touch")
        .draggable( 'option', 'revert', true); // are two draggable calls necessary? (not being coy; i don't know)
    $('.ui-droppable').droppable('enable');

    // consider an array for these elements
    $('#icon1').removeAttr('style');
    $('#icon2').removeAttr('style');
    $('#icon3').removeAttr('style');
    $('#icon4').removeAttr('style');

});

NOTE: You need to make sure to only add style rules through CSS (like you appear to have been doing).  Adding in-line style rules in the HTML or dynamic styles via jQuery will break this code.  (If you need to dynamically add styles, using classes is generally better anyway.)

I noticed you appear to be using a postdisplay without incident now?

Edit: formatting

commented Aug 13 by litrouke (1,590 points)

And just like that, it is beautiful.

Thank you for the remark on chaining as well. I'm finally remembering to group together CSS elements when I apply common styles... now I need to remember similar shortcuts in JS. I have no idea about the two draggable calls, to be honest.

The removeAttr works flawlessly, as I'm sure you found during testing. To make my life simpler, I have it assigned to a single class that spans the icons, rather than the four individual IDs:

$('.icon').removeAttr('style');

In terms of not adding styles via jQuery -- I am adding/removing the 'touch' class from the icons, but it doesn't seem to interrupt the code, so I guess that's fine. I don't normally use any in-line HTML, so that shouldn't be a problem. Thanks for letting me know!

It's the strangest things about the postdisplay... When I load the test passage directly, the formatting goes kaput. But if I load a different passage and click into the drag-n-drop passage, the formatting appears normal. Maybe a Firefox bug? I don't know. But that's fine with me, because I'll have an intro screen anyway.

Thank you so much again! This is my first time attempting my own JS for a Twine game. I know it's a teeny tiny start, but I'm already learning so many useful things. Fun fun. :D

commented Aug 13 by Chapel (27,390 points)

Glad it's working! 

The removeAttr works flawlessly, as I'm sure you found during testing. To make my life simpler, I have it assigned to a single class that spans the icons, rather than the four individual IDs:

That is a much better idea.

 In terms of not adding styles via jQuery -- I am adding/removing the 'touch' class from the icons...

Adding/removing classes is totally fine, I mean doing things like:

$('#icon1').css('margin', '5em');

or

$('#icon2').attr('style', 'color:blue;')

Basically anything that defines style rules on the icons outside of CSS, since the removeAttr() method will kill those styles.

0 votes
answered Aug 12 by litrouke (1,590 points)
edited Aug 12 by litrouke

Figured out Question 1! I think... Was able to grab the ID of the draggable element upon dropping and set it as a variable.

function handleFirstDrop( event, ui ) {
    ui.draggable.draggable( 'disable' );
    $(this).droppable( 'disable' );
    ui.draggable.position( { of: $(this), my: 'left top', at: 'left top' } );
    ui.draggable.draggable( 'option', 'revert', false );
    state.active.variables["correctCards"]++;

//new code here! set the ID of the chosen icon as the player's choice
    state.active.variables["choice"] = ui.draggable.attr('id');

  if ( state.active.variables["correctCards"] == 1 ) {
    $('button').show();
	$('.ui-draggable').draggable('disable');
    }
} 

Still no idea on Question 2...

commented Aug 12 by AnoeticDuckling (1,930 points)
I'm not very good at all this yet but perhaps you could simply reload the passage in order to return all the draggables back to their places? Like having a button that would link the player back to this passage, that would work, wouldn't it? I don't know if it would reset the variables that have been added but maybe that can be worked around by only having the choices be confirmed by pressing a confirm button. I don't know if that's helpful but that was my thoughts on the subject.
commented Aug 12 by litrouke (1,590 points)
Yeah, I considered that -- sadly, there's a whole sequence of text prior to making the drag-decision. So the reader would have to sit through a whole bunch of dialogue any time they wanted to change answers, which I'm not keen on for reasons obvious.

Everything works alright for now.... Having the icons return to their original position is more of an aesthetic touch than a functional need, so I'm happy. Thanks for your thoughts!
0 votes
answered Aug 12 by AnoeticDuckling (1,930 points)

I don't know if you saw my comment but because I think this could actually work, I'm adding this.

You can do the button that just reloads the passage and brings you back to the same passage and then! to bypass the text they'd have to re-sit through, you can use the switch function,

<<switch visited()>>
<<case 1>> 
Text only seen on first visit
<<default>>
Text seen after the above case or cases have been cleared
<</switch>>

You probably already know about \ so things can be left without weird chunks of whitespace

So there's that! But if you've given up, I understand. Health bars are my enemies at this point.

commented Aug 12 by litrouke (1,590 points)

Haven't really given up so much as left it aside to work on other things at the moment. As I said, functional needs trump aesthetic ones, but I'll probably circle around to this at the end if I have time.

The switch use certainly could work -- I fondly remember Sugarcane's <<once>> macro, so I'd probably use Chapel's replacement for it in place of switch, but same concept. Alas, my layout is a lot more complicated than just text... I'd have to wrap a boatload of moving things in different parts of the layout -- and at that point, it would probably be easier to send the reader to a dummy passage that looks like the final stage of the passage they were just in. Which, now that I think of it, is quite doable, but a lot of effort for an aesthetic tweak.

So we'll see! Thanks for the suggestion; it sent my brain down a fruitful path. If I can't figure out Javascript for it and it still bugs me at the end of the game, I might end up doing this.

commented Aug 12 by AnoeticDuckling (1,930 points)
I hadn't considered a dummy passage, that's a good idea! Chapel has a lot of good things, doesn't he? I'm pretty sure I'm only continuing my projects just for the chance to use his stuff because they seem cool, lol.

Well, good luck! These things are hard, but you can do it!
commented Aug 13 by Chapel (27,390 points)

I'm pretty sure I'm only continuing my projects just for the chance to use his stuff because they seem cool, lol.

Don't put that evil on me, haha.  Glad you like them, though. 

commented Aug 13 by AnoeticDuckling (1,930 points)
Mwuahaha! feel the pressure! I'm looking forward to the holy land demo, tho. It's the reason I got into this in the first place!
...