How to go to next passage using JavaScript?

Twine Version: 2.5.1
Story Format: sugarcube 2.36.1

Hello,

My example code is below. It works perfectly except it does not go to a passage when conditions are met. It throws an error saying “Engine is not defined”. I’m pretty new to JavaScript and cannot figure out how to get to a new passage using just JavaScript code. Using macros outside of the script just sends the player immediately to a new passage.

The premise is the player is in a dark cave and there are bats flying around. The small bat images will fade in and out pretty quickly in random spots around the screen and the player must click on them before they fade or a giant bat image will briefly appear on screen and the player will take damage. If the player clicks enough bats they should go to one passage, and if they take too much damage they should go to another.

Any help is appreciated, I’ve been combing old forum posts for a few days now and I’ve reached my limit. Cheers.

<div id="image-container">
  <img id="image1" src="images/smallBat.png" style="display:none; position: absolute;">
  <img id="image2" src="images/smallBat.png" style="display:none; position: absolute;">
  <img id="image3" src="images/smallBat.png" style="display:none; position: absolute;">
  <img id="damage-image" src="images/bat.png" style="display:none; position: absolute;">
</div>

<<run UIBar.stow()>>

<script>
  var image1 = document.getElementById("image1");
  var image2 = document.getElementById("image2");
  var image3 = document.getElementById("image3");
  var damageImage = document.getElementById("damage-image");
  var audio = new Audio("music/umph.mp3");

  var clickCount = 0;
  var damageCount = 0;
  
  function fadeInOut(image) {
    var x = Math.random() * (window.innerWidth - image.width) + window.scrollX;
    var y = Math.random() * (window.innerHeight - image.height) + window.scrollY;

    image.style.left = x + "px";
    image.style.top = y + "px";
    image.style.display = "block";

    image.addEventListener("click", function(){
    	clickCount++;
        image.style.display = "none";
        if (clickCount >= 20) {
        Engine.play("killedBats");
        }
    });

    setTimeout(function() {
      if(image.style.display != "none"){
          image.style.display = "none";
          damageCount++;
          damageImage.style.left = (window.innerWidth - damageImage.width)/2 + "px";
          damageImage.style.top = (window.innerHeight - damageImage.height)/2 + "px";
          damageImage.style.display = "block";
          audio.play();
          if (damageCount >= 10) {
          Engine.play("killedByBats");
          }
          setTimeout(function(){
            damageImage.style.display = "none";
          },100);
      }
      setTimeout(fadeInOut, 1000, image);
    }, 1000);
  }
  setTimeout(fadeInOut,500, image1);
  setTimeout(fadeInOut,1000, image2);
  setTimeout(fadeInOut,18000, image3);
</script>

<<audio caveBats loop play>>

There are a number of issues with your example code, some of them being:

1: You are using a Standard HTML <script> element to execute the JavaScript code.

Which means SugarCube specific features (like its Engine API) will not be available in the Scope that that code is being executed in. You need to use the <<script>> macro instead.

2: You are referencing HTML elements in the JavaScript code that were created earlier in the same Passage.

The HTML generated by a Passage is first stored in a buffer, and after all of the Passage’s content has been processed that buffer is then added to the web-page’s Document Object Model (DOM). So at the time your document.getElementById() functions are getting executed the target <img> elements won’t exist in the DOM yet.

The <<done>> macro can used to delay the execution of the JavaScript until after the <img> elements have been a added to the page.

Your example would look something like the following after applying the solutions to the first to issues…

<div id="image-container">
  <img id="image1" src="images/smallBat.png" style="display:none; position: absolute;">
  <img id="image2" src="images/smallBat.png" style="display:none; position: absolute;">
  <img id="image3" src="images/smallBat.png" style="display:none; position: absolute;">
  <img id="damage-image" src="images/bat.png" style="display:none; position: absolute;">
</div>

<<run UIBar.stow()>>

<<done>>
	<<script>>
		var image1 = document.getElementById("image1");
		var image2 = document.getElementById("image2");
		var image3 = document.getElementById("image3");
		var damageImage = document.getElementById("damage-image");
		var audio = new Audio("music/umph.mp3");

		var clickCount = 0;
		var damageCount = 0;

		function fadeInOut(image) {
			console.log('image: '+ image);

			var x = Math.random() * (window.innerWidth - image.width) + window.scrollX;
			var y = Math.random() * (window.innerHeight - image.height) + window.scrollY;

			image.style.left = x + "px";
			image.style.top = y + "px";
			image.style.display = "block";

			image.addEventListener("click", function() {
				clickCount++;
				image.style.display = "none";

				if (clickCount >= 20) {
					Engine.play("killedBats");
				}
			});

			setTimeout(function() {
				if (image.style.display != "none") {
					image.style.display = "none";
					damageCount++;
					damageImage.style.left = (window.innerWidth - damageImage.width)/2 + "px";
					damageImage.style.top = (window.innerHeight - damageImage.height)/2 + "px";
					damageImage.style.display = "block";

					audio.play();

					if (damageCount >= 10) {
						Engine.play("killedByBats");
					}

					setTimeout(function() {
						damageImage.style.display = "none";
					}, 100);
				}

				setTimeout(fadeInOut, 1000, image);

			}, 1000);
		}

		setTimeout(fadeInOut, 500, image1);
		setTimeout(fadeInOut, 1000, image2);
		setTimeout(fadeInOut, 18000, image3);
	<</script>>
<</done>>

<<audio caveBats loop play>>

3: You have multiple timers active simultaneously, you are nesting timers within timers, and none of those timers are associated with the “current” Passage so they will keep running even after a transition to either killedBats or killedByBats has occurred.

Without knowing exactly what you are trying to achieve with that JavaScript code I don’t really have a suggestion on how to do it without such a mess of timers.

First of all, thank you for the reply and help on the first issues. Your explanation was helpful and the revised code does what’s intended. It does, however, do exactly as you describe in the third issue which is it keeps running even after a transition to the next passage. I tried experimenting by adding some clearTimeout() functions but could not get it to work. Unfortunately since I don’t fully understand what I’m doing my code is like a shanty town with new code built on top of old code as I cherry pick code from forums and experiment. It’s technically mostly functional but often doesn’t make any sense. Anyway, do you have any suggestions for stopping the code once transitioned to a new passage?

It’s possible some of the timers are redundant. The goal is just to have two or three images of small bats appear in random places throughout the screen. They fade in and out in the span of between maybe a half second and 2 seconds. The player must click on them before they fully fade or they take damage. If they take damage, a large bat image appears in the middle of the screen for a brief moment, and if enough damage is taken the player transitions to the killedByBats passage. If the player clicks enough of the small bat images before taking too much damage then they will transition to the killedBats passage. Regardless of which passage they end up in, I’d like it to be a blank slate so to speak, without any of the previous code still running.

Thank you again, cheers.

Jumping in the conversation: I grasp your idea, that’s probably 15 to 45 minutes of coding and testing for someone who knows JavaScript and HTML/DOM. You might consider offering to pay someone if you don’t get someone offering code for free. I haven’t run across anything like what you describe in shared code, so far.

Thanks, it’s not a crucial element to the game so I’ve decided to put it on the back burner for now. I realize that kind of interactivity kind of defeats the purpose of the Twine platform so I’m trying to come up with more narrative or turn based solutions. I may revisit when my coding chops are a little better though. Appreciate the reply, cheers!