I'm making an idle game in the vein of Melvor Idle and I've run into a problem calculating the yield and consumption of resources while the user is away. For those who don't know, many idle games have a feature where they'll still "run" while the user is away. I say "run," because they most likely just save the user's log off and login time and then determine any offline yield based on that time delta, e.g. if I quit when I'm cutting wood @ 1 log per minute, when I get back an hour later, the game will calculate that I gained 60 logs in the interim by multiplying the time I was gone by task completion rate.

However, tasks can be more complex than simply getting a resource every minute. Tasks can consume some resources to generate another, e.g. turning ore into ingots, if there is no resource to consume, the task is cancelled. Tasks can also be exhausted and need to recharge, e.g. after doing a task 10 times, it needs to cool down for 2 minutes (look at mining in Melvor for example). I would also like players to be able to perform multiple tasks at once.

So, each task has the following features:

  • time: integer; how long it takes to complete one task in milliseconds
  • cost: a list of tuples of the form (resource, count), e.g. (ore, 5)
  • yield: a list of tuples of the form (resource, count), e.g. (ingot, 1)
  • charges: integer; the number of times this task can be completed before needing to cooldown
  • cooldown: integer; cooldown time in milliseconds

And I maintain a list of active tasks in the game's overall state.

You can see how this complicates the delta calculation. I can very easily deal with this if I ignore cost. The total yield can be modeled by this function multiplied by the task's yield:

function tasksCompleted(time: number, taskTime: number, taskCharges: number, taskCooldown: number) {
  if (!taskCooldown)
    return time / taskTime
  if (taskCharges <= 1)
    return Math.floor(time / taskCooldown)
  let upTime = taskTime * taskCharges;
  let totalTime = upTime + taskCooldown
  let tasksCompleted = Math.floor(time / totalTime)
  let tasksRemainder = time % totalTime
  return Math.min(charges, Math.floor(taskRemainder / taskTime)) + charges * tasksCompleted

I just don't know how I'd easily find the x intercept of this function so I can figure out when the cost of the task outpaces the player's resource pool. Further, imagine the player is running 2 tasks that both exhaust the same resource but have different completion times, charges, and cooldowns, how do I combine these functions to find the intercept?

My question is: is there an optimal way to represent this problem, and if not, how might I simplify it to achieve as close to the desired results as possible?

Potential solutions I have come up with so far, but don't particularly like:

  • Calculate yields iteratively rather than with a function and a delta. This avoids the complex math because if I ever run out of a resource, I'll know mid-loop. I don't like this because it means that the game has to essentially run X hours of game the instant the user logs back in.

  • Do what Melvor does and only let users perform one task and don't let tasks with cooldowns have costs. This simplifies things, but performing multiple tasks is a core mechanic to the game design, and I'd rather not lose that.

  • Give each task an "offline function" which would be a simple linear function that I could more easily compute. I'd still have to figure out how to combine linear functions in the code and I don't like losing the fidelity, but it could work.

I'd appreciate any advice you could give me!

  • \$\begingroup\$ Another potential solution is to approximate iterative calculations by checking the result of the time delta, see if the game state becomes invalid, and if it is, do a binary sort through the delta at increments of the length of the shortest task to find the earliest invalid state, cancel the offending task, and do it again for all remaining tasks. That seems semi-elegant to me, but not very mathematically satisfying. \$\endgroup\$ – RoboticWater May 8 '20 at 0:25
  • \$\begingroup\$ I actually just realized that this only works if we assume that the net yield is strictly decreasing for any resource. There could be an oscillating effect created by two tasks that this doesn't account for. A stochastic search could work, but might miss invalid points. \$\endgroup\$ – RoboticWater May 30 '20 at 20:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.