Return previous instruction js

Viewed 385

I'm building a video game where a spaceship moves with controllers and it must avoid the fireball in order to continue to play. If it collides into the fireball the game must display "Game Over" and restart.

At the beginning of the game, there is an input where the user puts his name. Then there is a countdown and then the game starts. I would like that the user gets back to the countdown instead of the point where he must input his name. Does someone knows how to do this?

Input

Game over

Countdown

Code for input:

<form id="askName" title="Write your name">
  <label> Enter your username: </label>
  <input id="input" type="text" maxlength="10" autofocus>
  <button type="button" onclick="countDown(); return Username()" id="begin-timer">
    Submit
  </button>

let icon = document.getElementById("icon")
let fireballElement = document.querySelector("#fireball")
var input = document.getElementById("input")
function Username(field) {
  field = input.value
  if (field == "") { alert("Complete blanks"); return false }
  document.getElementById("askName").style.display = "none"
  setTimeout(function() {
    document.getElementById("name").innerHTML = "Player: " + field
    icon.style.display = 'block'
    fireballElement.style.display = "block"
    checkCollision()
  }, 4000)
}

CountDown

var count = 3
function countDown() {
  function preventCountFast() {
    document.getElementById("count").innerHTML = count
    if (count > 0) { count-- }
    else {
      clearInterval(ncount);
      document.getElementById("count").style.display = "none"
    }
  }
  var ncount = setInterval(preventCountFast, 1000)
}

Detect collision when fireball touches spaceship:

function checkCollision() {
  var elem = document.getElementById("icon")
  var elem2 = document.getElementById("fireball")
  if (detectOverlap(elem, elem2) && elem2.getAttribute('hit') == 'false') {
    hits++
    elem2.setAttribute('hit', true)

    //THIS IS WHERE YOU SHOULD LOOK AT
    document.querySelector("#stopGame").style.display = "inline"
  }

  setTimeout(checkCollision, 20)
}

var detectOverlap = (function() {
  function getPositions(elem) {
    var pos = elem.getBoundingClientRect()
    return [[pos.left, pos.right], [pos.top, pos.bottom]]
  }
  function comparePositions(p1, p2) {
    var r1, r2
    r1 = p1[0] < p2[0] ? p1 : p2
    r2 = p1[0] < p2[0] ? p2 : p1
    return r1[1] > r2[0] || r1[0] === r2[0]
  }
  return function(a, b) {
    var pos1 = getPositions(a), pos2 = getPositions(b)
    return comparePositions(pos1[0], pos2[0]) && comparePositions(pos1[1], pos2[1])
  }
})() 

<img src="Photo/fireball.png" id="fireball" style="display:none>
<img src="Photo/Spaceship1.png" id="icon" style="display:none">
<h2 id="stopGame"> Game Over! </h2>

Fireball movement:

function fFireball(offset) {
  return Math.floor(Math.random() * (window.innerWidth - offset))
}
let fireball = { x: fFireball(fireballElement.offsetWidth), y: 0 }
const fireLoop = function() {
  fireball.y += 1
  fireballElement.style.top = fireball.y + 'px'
  if (fireball.y > window.innerHeight) {
    fireball.x = fFireball(fireballElement.offsetWidth)
    fireballElement.style.left = fireball.x + 'px'
    fireball.y = 0
    fireballElement.setAttribute('hit', false )
  }
}
fireballElement.style.left = fireball.x + 'px'
let fireInterval = setInterval(fireLoop, 1000 / 200)

Spaceship movement:

//Spaceship moves into space + prevent going out borders
let hits = 0
let display = document.getElementById("body")
let rect = icon
let pos = { top: 1000, left: 570 }
const keys = {}
window.addEventListener("keydown", function(e) { keys[e.keyCode] = true  })
window.addEventListener("keyup"  , function(e) { keys[e.keyCode] = false })
const loop = function() {
  if (keys[37] || keys[81]) { pos.left -= 10 }
  if (keys[39] || keys[68]) { pos.left += 10 }
  if (keys[38] || keys[90]) { pos.top  -= 10 }
  if (keys[40] || keys[83]) { pos.top  += 10 }
  var owidth = display.offsetWidth
  var oheight = display.offsetHeight
  var iwidth = rect.offsetWidth
  var iheight = rect.offsetHeight
  if (pos.left < 0) pos.left = -10
  if (pos.top  < 0) pos.top = -10
  if (pos.left + iwidth  >= owidth ) pos.left = owidth - iwidth
  if (pos.top  + iheight >= oheight) pos.top  = oheight- iheight
  rect.setAttribute("data", owidth + ":" + oheight)
  rect.style.left = pos.left + "px"; rect.style.top = pos.top + "px"
}
let sens = setInterval(loop, 1000 / 60)
3 Answers

Convert your countDown() to this:

function countDown(count) {
    function preventCountFast() {
        document.getElementById("count").innerHTML = count
        document.getElementById("count").style.display = "block"
        if (count > 0) { count-- }
        else { clearInterval(ncount); document.getElementById("count").style.display = "none" }
    }
    var ncount = setInterval(preventCountFast, 1000)
}

So, while calling this, always call it as countDown(3) it will be easier. Next, modify your checkCollision() just a bit:

function checkCollision() {
    var elem = document.getElementById("icon")
    var elem2 = document.getElementById("fireball")
    if (detectOverlap(elem, elem2) && elem2.getAttribute('hit') == 'false') {
        hits++
        elem2.setAttribute('hit', true)
        document.querySelector("#stopGame").style.display = "block"
        document.querySelector("#stopGame").style.animation = "seconds 5s forwards"
        /* Added these three lines so that countDown() is called again if collision 
           occurs*/
        setTimeout(function () {
            countDown(5);
        }, 2000);
        
        /* End of edit */
    }
    setTimeout(checkCollision, 1)
}

Create a restart function and call it at the end of checkCollision like below.

function checkCollision() {
    var elem = document.getElementById("icon")
    var elem2 = document.getElementById("fireball")
    if (detectOverlap(elem, elem2) && elem2.getAttribute('hit') == 'false') {
        hits++
        elem2.setAttribute('hit', true)
        document.querySelector("#stopGame").style.display = "block"
        document.querySelector("#stopGame").style.animation = "seconds 5s forwards"
        // call restart at end of game
        restart();
        return;
    }
    setTimeout(checkCollision, 1)
}

function restart() {
    // clear fireball animation
    clearInterval(fireInterval);
    count = 3;
    let countElement = document.getElementById("count");
    let stopGame = document.querySelector("#stopGame");
    // show game over for 3 seconds to user
    setTimeout(function () {
      stopGame.style.animation = "";
      stopGame.style.display = icon.style.display = fireballElement.style.display =
        "none";
      countElement.innerHTML = "";
      countElement.style.display = "block";
      countElement.style.transform = "scale(1)";
      // allow count rerender
      setTimeout(function () {
        fireballElement.setAttribute("hit", false);
        countDown();
        // wait for count down to be over
        setTimeout(function () {
          // reset player position
          pos.left = display.offsetWidth / 2;
          pos.top = display.offsetHeight;
          icon.style.display = "block";
          // reset fireball position
          fireball = { x: fFireball(display.offsetWidth / 2), y: 0 };
          fireballElement.style.top = fireball.y + "px";
          fireballElement.style.left = fireball.x + "px";
          fireballElement.style.display = "block";
          clearInterval(sens);
          // restart loop list
          sens = setInterval(loop, 1000 / 60);
          fireInterval = setInterval(fireLoop, 1000 / 200);
          checkCollision();
        }, 4000);
      }, 1000);
    }, 3000);
  }

Separate logic from presentation

The reason why you encountered this problem is that your code is tightly coupled to the presentation (DOM), and makes use of the global scope.

Your game logic should be separated from the presentation - DOM is basically an I/O device. Otherwise you tie yourself to a particular implementation (imagine refactoring this to a React application or using Material design, etc).

The principle is called "separation of concerns" (or SoC), and is a well-known principle of software design that will serve you well in the future.


Part 1. Counter

If you rely on a global variable like count to launch the timer, you will inevitably encounter issues with resetting it - make the state internal and only pass in configuration (start, end, and what to do on each step).

function getCounter({
  init = 3,
  onEnd = () => {},
  onStep = () => {},
  until = 0
} = {}) {

  let curr = init;

  return {
    interval : null,
    start() {
      this.interval = setInterval(() => {
        onStep(this, curr--);
        const shouldStop = until === curr;
        shouldStop && this.stop();
      }, 1e3);
   
    },
    stop() {
      const { interval } = this;
      clearInterval(interval);
      onEnd();
    }
  };
}

const counter = getCounter({ 
  onStep : (counter, curr) => console.log(curr),
  onEnd : () => console.log("ended")
});

counter.start();


Part 2. Game Over

Instead of controlling the UI upon collision, control what should happen, and defer coupling to a particular API:

const detectOverlap = () => !!Math.floor( Math.random() * 8 ); //mock for testing

const checkCollision = ({
  obj1,
  obj2,
  interval = 20,
  curHits = 0,
  maxHits = 0,
  onMiss,
  onHit,
  onGameOver
}) => {

  const isHit = detectOverlap(obj1, obj2);
  
  isHit && curHits++;
  
  const hitOrMissConfig = {
    curHits,
    maxHits,
    obj1,
    obj2
  };
  
  isHit ? onHit(hitOrMissConfig) : onMiss(hitOrMissConfig);

  const timeout = setTimeout(
    () => checkCollision({
      obj1, obj2, interval,
      curHits, maxHits,
      onHit, onMiss, onGameOver
    }),
    interval
  );

  if (curHits >= maxHits) {
    clearTimeout(timeout);
    return onGameOver();
  }
};

const obj1 = { id : 1 };
const obj2 = { id : 2 };

const onHit = () => console.log("hit!");
const onMiss = () => console.log("miss!");
const onGameOver = () => console.log("game over!");

checkCollision({ obj1, obj2, onHit, onMiss, onGameOver, maxHits : 8 });


Part 3. Start logic

Instead of starting the game from the name form, you should encapsulate your logic and call it as a callback - this way you will have control over when to initiate the name form, countdown or anything else:

const loadForm = ({ parent = document.body, defaultUname, onSubmit } = {}) => {
  const form = document.createElement("form");
  
  const input = document.createElement("input");
  input.name = "name";
  input.type = "text";
  input.value = defaultUname;
  
  const start = document.createElement("button");
  start.type = "button";
  start.innerText = "Start";
  
  form.append(input, start);
  parent.append(form);
  
  start.addEventListener("click", (event) => {
    onSubmit({ name : input.value, firstTime : false });
    form.remove();
  });
};

//mocks for testing
const checkCollision = () => console.log("checking collision");
const countDown = (init) => {
  if(init) {
    console.log(init);
    setTimeout(() => countDown(--init), 1e3);
  }
};

const startGame = ({ name = "Player 1", firstTime = true } = {}) => {

  let restarted = false;
  
  if(!restarted && firstTime) { 
    return loadForm({ 
      defaultUname : name, 
      onSubmit : startGame 
    }); 
  }
  
  countDown(3);  
};

startGame();


All Steps combined

You will have to implement the UI handling, guards against no name, and connect collision detection, but this should take care of all the core logic. You might also want to make your fireball and spaceship proper JavaScript objects, and not impose logic on DOM elements for the reasons stated above.

function getCounter({
  init = 3,
  onEnd = () => {},
  onStep = () => {},
  until = 0
} = {}) {

  let curr = init;

  return {
    interval : null,
    start() {
      this.interval = setInterval(() => {
        onStep(this, curr--);
        const shouldStop = until === curr;
        shouldStop && this.stop();
      }, 1e3);
   
    },
    stop() {
      const { interval } = this;
      clearInterval(interval);
      onEnd();
    }
  };
}

const detectOverlap = () => !!Math.floor(Math.random() * 4); //mock for testing

const checkCollision = ({
  obj1,
  obj2,
  interval = 20,
  curHits = 0,
  maxHits = 0,
  onMiss,
  onHit,
  onGameOver
}) => {

  const isHit = detectOverlap(obj1, obj2);

  isHit && curHits++;
  
  const hitOrMissConfig = {
    curHits,
    maxHits,
    obj1,
    obj2
  };
  
  isHit ? onHit(hitOrMissConfig) : onMiss(hitOrMissConfig);

  const timeout = setTimeout(
    () => checkCollision({
      obj1, obj2, interval,
      curHits, maxHits,
      onHit, onMiss, onGameOver
    }),
    interval
  );

  if (curHits >= maxHits) {
    clearTimeout(timeout);
    return onGameOver();
  }
};

const loadForm = ({
  parent = document.body,
  defaultUname,
  onSubmit
} = {}) => {
  const form = document.createElement("form");

  const input = document.createElement("input");
  input.name = "name";
  input.type = "text";
  input.value = defaultUname;

  const start = document.createElement("button");
  start.type = "button";
  start.innerText = "Start";

  form.append(input, start);
  parent.append(form);

  start.addEventListener("click", (event) => {
    onSubmit({
      name: input.value,
      firstTime: false
    });
    form.remove();
  });
};

const startGame = ({
  name = "Player 1",
  firstTime = true
} = {}) => {

  if (firstTime) {
    return loadForm({
      defaultUname: name,
      onSubmit: startGame
    });
  }
  
  console.log(`Get ready, ${name}`);
  
  const counter = getCounter({ 
    onStep : (_,count) => console.log(count),
    onEnd : () => checkCollision({
      interval : 1e2,
      maxHits : 8,
      obj1 : { id : 1 },
      obj2 : { id : 2 },
      onHit : () => console.log("hit!"),
      onMiss : () => console.log("miss!"),
      onGameOver : () => {
        console.log("game over!");
        startGame({ name, firstTime : false });
      }
    })
  });
  
  counter.start();
  
};

startGame();


Useful Resources

  1. How I separate logic from presentation?
Related