0

I am trying to do combat loop for my learning project and I finally realised I would need some async await / promises, because here is the scenario:

There are 4 fighters on each side. Everything goes well in this case, because enemy randomly picks a character to attack and functions work as intended.

However, your character can die and then we have only 3 left, but I'm not removing them from the array of characters, because they could be revived later, so enemy still rolls k4 and in case they got an index of character that already died they need to roll again.

Howeverm in theory I thought I knew what I was doing, but in reality I get undefined because (in my assumption) other functions dont wait for enemy to pick the index of the character to attack, and while it's still being rolled they execute and program crashes.

Here's some of the code from the enemy turn:

 enemies.forEach((enemy, i) => {
            setTimeout( async () => {
                let allyIndex = await dispatch(getAllyIndex());
                let enemyAgility = getEnemyAgility(i, enemies);
                let allyEvasion = await dispatch(getAllyEvasion(allyIndex));
                let wasAttackSuccessful = await dispatch(calculateAttackSuccessChance(enemyAgility, allyEvasion));

                if (wasAttackSuccessful) {
                    let wasCritical = dispatch(wasAttackCritical(i));
                    let enemyDmg = dispatch(calculateEnemyDmg(i));
                    let allyDef = await dispatch(getAllyDefence(allyIndex));
                    let totalDmg = await dispatch(calculateTotalDmg(enemyDmg, allyDef, wasCritical));

                    let info = ``;
                    if (wasCritical) { info += `Critical hit! ` };
                    let allyName = await getState().characters[allyIndex].name;
                    info += `${enemy.name} dealth ${totalDmg} damage to ${allyName}.`;
                    dispatch(addInfoToArray(info))

                    dispatch(allyLoseHp(totalDmg, allyIndex))
                } else {
                    let info = `${enemy.name} missed!`
                    dispatch(addInfoToArray(info))
                }

                noOfEnemiesAttacked += 1;

                if (noOfEnemiesAttacked === enemies.length) {
                    dispatch(changeTurn('ally'))
                }
            }, 2000 + offset);

            offset += 200;
        })

Issue probably lies in getAllyIndex function. Here's how it looks:

const getAllyIndex = () => {
return function (dispatch, getState) {
    let i = Math.floor((Math.random() * getState().characters.length));
    if (getState().characters[i].stats.hp <= 0) {
        dispatch(getAllyIndex());
    } else {
        return i;
    }
}
}

And the program reports errors in those functions that require allyIndex:

        let allyEvasion = await dispatch(getAllyEvasion(allyIndex));

let allyDef = await dispatch(getAllyDefence(allyIndex));
let allyName = await getState().characters[allyIndex].name;

Have i completely misunderstood the concept of async await or the issue lies somewhere else?

MazMat
  • 1,904
  • 3
  • 12
  • 24
  • Just as an aside question, what's the purpose of the setTimeout in this code? Usually a setTimeout is used to simulate async code, but I can't quite see the purpose of it here. – jacobedawson Dec 08 '18 at 16:32
  • Yes. You do. Don't use `async` as `setTimeout` calllback. Don't use it together with `forEach`. It's unclear why you use `await` with `dispatch`. Does it return a promise to chain it? I guess it doesn't. – Estus Flask Dec 08 '18 at 16:33
  • To simulate real game, so enemies dont attack at once, but in 2s interval. – MazMat Dec 08 '18 at 16:33
  • Your problem seems understanding react/redux lifecycle. If I'm not mistaken, dispatch doesn't return anything, it just alters state. What you should do, is on your component, have a `componentWillReceiveProps` check if the prop you are watching for changed, and then move on to your next functions. Your code seems really messy, you are using your own app state, but then you also delegate other state to redux – iagowp Dec 08 '18 at 16:40
  • I dont think I should be doing this in components, with this approach I imagine the code for enemy turn would get extremely messy, all over the place and possibly even longer. Im doing all of this in actions and I never heard that I shouldnt use getState and dispatching in one place – MazMat Dec 08 '18 at 16:45

2 Answers2

-1

There have been issues with async/await in .forEach() e.g.: Using async/await with a forEach loop

Did you try replacing it with a normal for loop?

Cristian G
  • 460
  • 1
  • 4
  • 22
  • I think it's more of an issue with setTimeout, as it's not the forEach callback that is async, but timeout. But I need to use timeout here or everything happens at once and enemy turn would last less than a second. – MazMat Dec 08 '18 at 16:38
  • Maybe it is the problem with how you set the timeout. Have a look at this, hope it helps (note the await for sleep): https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep – Cristian G Dec 08 '18 at 16:45
  • @MazMat It's definitely both. You must not use `forEach`, *and* you need to promisify and `await` the timeout. – Bergi Dec 08 '18 at 17:24
  • @Bergi yea, I just read on that and modified my code, for now it works in a small scale, now to implement it to the rest of the code and hoping for the best – MazMat Dec 08 '18 at 18:14
  • What was the problem in the end? @MazMat – Cristian G Dec 08 '18 at 18:21
  • Probably as you mentioned, usage of forEach and setTimeout with async await. I also needed to return a promise with getAllyIndex and for now it appears to be working – MazMat Dec 08 '18 at 18:43
  • Ah okay, glad I could help. Don't forget to accept the answer if it solved you issue. Good luck with your game! – Cristian G Dec 08 '18 at 18:44
-1

It looks like you are recursively dispatching getAllyIndex, however recursively dispatching an async action thru redux might be different than a normal function. Also does this need to be an action creator at all, since its return doesn't contain any update for store?

Have you tried using a normal function instead? If you need access to state, perhaps consider removing the recursion part or simply importing store & use store.getState() (a bit of a bad pattern, but might work for your case)

const getAllyIndex = () => {
  return function (dispatch, getState) {
    const characters = getState().characters;
    const getRandomId = () => {
      const i = Math.floor((Math.random() * characters.length));
      if (characters[i].stats.hp <= 0) {
         getRandomId();
      } else return i;
    }
    return getRandomId();
  }
}
Derek Nguyen
  • 11,294
  • 1
  • 40
  • 64
  • I will test your solution after I test another one of my ideas. I got rid of forEach as it doesnt work properly with async await, I created my own timeout that could be awaited and made getAllyIndex to return a promise and so far this solution seems to work, but I need to add this code to the whole combat loop now and see how it goes. – MazMat Dec 08 '18 at 17:15