Breaking a <<for>> loop (Sugarcube 2.30)

Good evening!

I am having again a little problem which I cannot find solution for.
Situation: At the end of each level, I have a passage that informs about level score and adds it to total score. For higher drama, points are transfered in steps of ten using a < < for > > loop.
However, players might not want to watch point transfer to the end and just click the “continue” button to continue to next level. What I wanted to achieve is that if “continue” button is clicked, is that the < < for > > loop, which transfers points, < < breaks > >. So, my “continue” button sets flag $stopPointsTransfer to true, which I wanted to use to < < break > > the loop. But the loop just continues and messes up the calculation. Could somebody tell me where my logic is wrong?

<<timed 1s>>
	<<for _i to 0; _i lt $levelScore/10; _i++>>
		<<if $stopPointsTransfer>>
			<<break>>
		<<else>>
			<<timed 10ms>>
			<<set $allOverScore +=10>>
			<<set $levelScore -=10>>
			<<replace "#score">>
				$allOverScore	
			<</replace>>
			<<replace "#levelScore">>
				$levelScore	
			<</replace>>
			<</timed>>
		<</if>>
	<</for>>
<</timed>>

@@.startButton;<<button "Continue">>
<<set $stopPointsTransfer to true>>
<<if $levelScore neq 0>>
	<<set $allOverScore += $levelScore>>
	<<set $levelScore to 0>>
	<<replace "#levelScore">>$levelScore<</replace>>
<</if>>
<<if $discipline.length gt 0>>
	<<set _passage to $discipline.pluck()>>
	<<goto _passage>>
<<else>>
	<<goto "end">>
<</if>>
<</button>>@@

(EDIT: Totally rewrote this.)

After reading tayruh’s comment and trying to explain why it wouldn’t work, I noticed a major problem in my own suggestion, so here’s a corrected solution.

The problem is that your whole <<for>> loop gets run all at once, which stacks up <<timed>> events that you have no way of stopping. A better solution involves using a widget which calls itself after each step, if necessary. So, make a new passage with “widget” and “nobr” tags and put this in it:

<<widget "LevelUpdate">>
	<<timed 10ms>>
		<<if $levelScore > 0>>
			<<set $allOverScore += 10>>
			<<set $levelScore -= 10>>
			<<replace "#score">>
				$allOverScore	
			<</replace>>
			<<replace "#levelScore">>
				$levelScore	
			<</replace>>
			<<if $levelScore > 0>>
				<<LevelUpdate>>
			<</if>>
		<</if>>
	<</timed>>
<</widget>>

That will create a <<LevelUpdate>> widget which calls itself as many times as necessary to update the scores, stopping any updates as soon as $levelScore is no longer greater than zero.

Thus, the code in your passage would actually look like this:

<<timed 1s>>
	<<LevelUpdate>>
<</timed>>
@@.startButton;<<button "Continue">>
	<<if $levelScore > 0>>
		<<set $allOverScore += $levelScore>>
		<<set $levelScore = 0>>
		<<replace "#score">>$allOverScore<</replace>>
		<<replace "#levelScore">>$levelScore<</replace>>
	<</if>>
	<<if $discipline.length > 0>>
		<<set _passage to $discipline.pluck()>>
		<<goto _passage>>
	<<else>>
		<<goto "end">>
	<</if>>
<</button>>@@

And that should do the trick! :slight_smile:

Please use the New Topic form’s “optional tags” field to state the name and version/series number of the Story Format you are using. Placing such information within the Topic’s Title just makes that title longer than it needs to be.

I will assume that the variables referenced in your example contain values something like the following…

<<set $levelScore to 1000>>
<<set $stopPointsTransfer to false>>
<<set $allOverScore to 0>>
<<set $discipline to ["passage 1", "passage 2"]>>

Simply put your issue is one of timing, and I believe the following variation of your example demonstrate what is happening. It has been changed to output debug messages to the Console that is included with your web-browser’s Web Developer Tools.

<<silently>>
<<set $levelScore to 1000>>
<<set $stopPointsTransfer to false>>
<<set $allOverScore to 0>>
<<set $discipline to ["passage 1", "passage 2"]>>
<</silently>>

@@#score;$allOverScore@@
@@#levelScore;$levelScore@@

<<silently>>
<<timed 1s>>
	<<run console.log('loop: start: levelScore: ' + $levelScore) >>
	<<for _i to 0; _i lt $levelScore / 10; _i++>>
		<<run console.log('loop: levelScore: ' + $levelScore + ' i: ' + _i) >>
		<<if $stopPointsTransfer>>
			<<run console.log('loop: break') >>
			<<break>>
		<<else>>
			<<timed 10ms>>
				<<set $allOverScore += 10>>
				<<set $levelScore -= 10>>
				<<run console.log('loop: page update: allOverScore: ' + $allOverScore + ' levelScore: ' + $levelScore) >>
				<<replace "#score">>\
					$allOverScore\
				<</replace>>
				<<replace "#levelScore">>\
					$levelScore\
				<</replace>>
			<</timed>>
		<</if>>
	<</for>>
	<<run console.log('loop: finish levelScore: ' + $levelScore) >>
<</timed>>
<</silently>>

@@.startButton;\
<<button "Continue">>
	<<run console.log('button: clicked') >>
	<<set $stopPointsTransfer to true>>
	<<if $levelScore neq 0>>
		<<set $allOverScore += $levelScore>>
		<<set $levelScore to 0>>
		<<replace "#levelScore">>$levelScore<</replace>>
	<</if>>
	<<if $discipline.length gt 0>>
		<<set _passage to $discipline.pluck()>>
		<<goto _passage>>
	<<else>>
		<<goto "end">>
	<</if>>
<</button>>@@

If you run the above and click the button while the numbers displayed on the page are changing then you will see something like the following within the Console. (For brevity sake I have trimmed entries from the following)

loop: start: levelScore: 1000
loop: levelScore: 1000 i: 0
loop: levelScore: 1000 i: 1
loop: levelScore: 1000 i: 2
...
loop: levelScore: 1000 i: 97
loop: levelScore: 1000 i: 98
loop: levelScore: 1000 i: 99

loop: finish levelScore: 1000

loop: page update: allOverScore: 10 levelScore: 990
loop: page update: allOverScore: 20 levelScore: 980
loop: page update: allOverScore: 30 levelScore: 970
...
loop: page update: allOverScore: 170 levelScore: 830
loop: page update: allOverScore: 180 levelScore: 820
loop: page update: allOverScore: 190 levelScore: 810

button: clicked

loop: page update: allOverScore: 1010 levelScore: -10
loop: page update: allOverScore: 1020 levelScore: -20
loop: page update: allOverScore: 1030 levelScore: -30
...
VM25378:1 loop: page update: allOverScore: 1790 levelScore: -790
VM25383:1 loop: page update: allOverScore: 1800 levelScore: -800
VM25388:1 loop: page update: allOverScore: 1810 levelScore: -810

The important things to note are that the first updating of the page happens after the looping has finished, as does the button clicking if the end-user waits until they see the numbers changing.

You can’t stop the timer events associated with the inner <<timer 10ms>> from firing, however you can use conditional code like the following to control what happens when they do fire.

			<<timed 10ms>>
				<<if not $stopPointsTransfer>>
					<<set $allOverScore += 10>>
					<<set $levelScore -= 10>>
					<<replace "#score">>\
						$allOverScore\
					<</replace>>
					<<replace "#levelScore">>\
						$levelScore\
					<</replace>>
				<</if>>
			<</timed>>

I’m at work and have no way of testing this, but I was wondering if maybe the issue was caused by JS not being asynchronous. I’m assuming that <<timed>> is just a macro for setTimeout. And if I’m right in that assumption, then setting $stopPointsTransfer from a source outside of the timed macro won’t affect what’s inside the macro while it’s stuck on the for loop.

So, my thought is something like this (avoiding the external timed macro and for loop):

		<<timed 10ms>>
			<<set $allOverScore +=10>>
			<<set $levelScore -=10>>
			<<replace "#score">>
				$allOverScore	
			<</replace>>
			<<replace "#levelScore">>
				$levelScore	
			<</replace>>
			<<if !$stopPointsTransfer && $levelScore > 0>>
				<<next>>
			<</if>>
		 <</timed>>

My thought is that if next creates a new setTimeout callback each time, it’ll give the script time to breathe and grab the new value for $stopPointsTransfer. Hopefully I’m not way off base.

Well, thanks to me reading your [tayruh’s] code and attempting to correct it, I noticed a problem in my own code, because I forgot that <<timed>> is asynchronous. I corrected my code above.

Anyways, your solution won’t work because <<next>> just acts as another <<timed>> macro which takes place after the previous timed event finishes triggering. Thus you can’t stick a <<next>> within an <<if>> macro like that, because it would break the <<if>> macro. In other words, with the <<next>> like that, you’re trying to start the <<if>> in one event, and then finish that <<if>> within a different event 10ms later, which just doesn’t work.

I think you may have been trying to do the equivalent of what my corrected code does, but <<next>> doesn’t work the way you tried it.

1 Like

Ohhh. Okay. I misunderstood how <<next>> works. I never used it before so I thought that it re-ran the current timed macro (I obviously didn’t look at the docs close enough), but I see now that the script effectively pauses there and then runs the following code after another timeout. So yeah, my code is obviously broken. Hah.

Sorry about adding confusion and thanks for the correction.