In most games, just run the loop every time. The cost of checking every bloody button you could ever press is way less than the cost of rendering a single button onscreen. GameAlchemist's answer should be more than enough for anything you actually want to do.
However, if you want better than brute force, you will want to take advantage of temporal consistency. On average, the set of enabled/disabled buttons doesn't change very much. All you have to do is prove that it hasn't changed much, and you're set.
Put all of your upgrades into an ordered structure for each resource, along with their prices. I'm going to talk to using an array, but any structure would do. One tiny change: invert all the prices. 500 minerals should become -500 minerals. Why? It could work either way, but the specific named algorithms tend to have specific meanings, and if I do this silly optimization, they correctly handle the case where I have exactly 500 minerals and want the build option to be enabled.
On the first iteration, we have to do some slower prep work. We take the number of minerals we have (let's say 249), and invert it to -249. Now if we treated this -249 as a lower_bound
, and looked at the part of the ordered array that was equal or greater than this negative number, we'd have the list of everything that is enabled at first (its cost is closer to 0 than the amount of resources we have). Likewise, if we take an upper_bound
operation, we'd look at the part of the ordered arraythat was less than this negative number, we'd have the list of everything that is disabled at first (its cost is further from 0 than the amount of resources we have).
The weirdness with negative numbers should be meaningful with an example where we have exactly 500 resources:
-900 -500 -400 -200 -100 prices of each item (inverted)
-500 actual resources (inverted)
|------------------------| values for which -500 is a lower bound
|------| values for which -500 is an upper bound
You could absolutely word the logic differently and avoid all this silly negation. However, by doing it this way, you can use a lot of existing routines for dealing with bounds in sorted arrays. At least as far back as C++, the definitions of lower_bound
and upper_bound
have been defined such that lower_bound
includes the value you're searching for, while upper_bound
excludes it.
That was a lot of work, however, after frame 1, we can start using some temporal consistency to save time. We need to save off the current enabled/disabled state and resource count before we increment resources. Then we can compare the new resources against the old. If the new resources are higher, we are going to try to enable more states. If the new resources are lower, it means we spent resources last tick, and we need to disable more states.
So what states are going to change? It's easy really. Let's handle the increasing resources case first. If we negate the resources, like we have to to work with this structure, this means our resource number is going more negative. If we do a lower_bound
based on this resource number, we'd get a part of the ordered array which should be enabled, just like before. However, this time, we also know that everything above the previous frame's lower_bound
is already enabled, so we don't need to re-notify them. We only have to notify what's in between. Let's look at this on a number line. I'm intentionally making it jump a lot, from 300 resources to 500 so we can show the algorithm at work. Typically this jump would be a lot smaller.
-900 -500 -400 -200 -100 prices of each item (inverted)
-300 old resources: 300 (inverted)
|---------| values for which -300 is a lower bound
EN EN Enabled resources in previous frame
-500 new resources: 500 (inverted)
|-----------------------| values for which -500 is an lower bound
EN EN EN EN Resources which need to be enabled
Notify Notify Buttons that need notification because
they were not enabled before, but are
enabled now.
Likewise, if we're decreasing resources this frame, we need to disable buttons. Everything which was under the upper_bound
of the previous frame were things that we know were properly disabled already. Everything under the upper_bound
of this frame are things we need to disable. Thus, we should notify everything which is under the new upper_bound
but stop when we see the old upper_bound
because that was already handled. As an example, let's do the opposite of the previous example. Let's go from 500 resources to 300.
-900 -500 -400 -200 -100 prices of each item (inverted)
-500 old resources: 500 (inverted)
|------| values for which -500 is an upper bound
EN EN EN EN Enabled resources in previous frame
-300 new resources: 300 (inverted)
|------------------| values for which -300 is a upper bound
EN EN Resources which need to be enabled
Notify Notify Buttons that need notification that
they are no longer enabled
Phew. I told you its easier to just run the loop every time, but this will run faster than brute force. First off, because we're operating on an ordered array, lower_bound
and upper_bound
are O(log n), rather than the O(n) of the brute force approaches. Second, we're only sending notifications to the minimum number of buttons possible.
If you want to have more than one type of resources, keep an ordered array for each resource separately. Have each object which manages your buttons keep track of how many resources it needs which are unsatisfied by the current resources. On notification that you have enough of a resource, decrement this "unsatisfied" count by 1. on notification that you no longer have enough, increment this "unsatisfied" count by 1. Now you only really have to worry about the transition from 0 (fully satisfied, enabled) to 1+ (not satisfied, disabled).
Lots of work, but that'll keep your runtime costs as low as they can possibly go! If you have a few hundred buttons, and a few dozen resources for each, that may be the trick that takes you from "runs too slow to play" to "fun to play!" Maybe if you're going for hard-realtime 60Hz, this might help. The only way to tell is to profile. There's a lot more upkeep involved than with the brute force method, and depending on how well javascript optimizes, that might hurt more than it helps. However, this is scaleable, so when you want to have 100,000 buildings with 50 resources each, updating every millisecond, on the dot, this might be the ticket you need. It's also quite parallelizable in many steps, not that that matters much for javascript.