Regarding not caching which level you are. The problem within the industry is that you can’t listen to when you ‘level up’ for it is fully dynamic (no turning point next to the formula saying level x or y).
This is why we would want to ‘cache’ the level. So we are forced to set the level at a state and make it an explicit state instead of dynamic. Allowing for events to be triggered.
You solved this by doing the check within the setter and firing the event after.
The problem with that is that the level can already return > x before the event was fired (which can happen in a different frame). This is why its many times advised to have all the state change simultaneously along with firing the event directly afterwards.
So different observers update at a different time, one looking at the level, the other listening to the event. The UI already shows the next level while the event is yet to be triggered (very non-destructive example). Or a system triggers before another does which should run in sequence (destructive example)