Think of line 84 as a rubber wall, adding energy to your ball.
Add this line above line 84:
print(this.pos.y, this.velocity.y);
Now watch what happens.
At a certain point, the y position of the ball is 407.76 – it has overshot the bottom of the frame by 7.76. The collision corrector fixes this by changing it to 400 – in essence, teleporting the ball up by 7.76. This exactly matches the height decay (the instant velocity) so the ball lost 0. The next bounce, the ball decays and falls to 407.46 again – and receives another boost, exactly matching its decay again, and so does the same bounce again.
With very slightly different settings, this behavior will find a different equilibrium. For example, try this:
this.pos.y = height -1;
See what happens? With just a bit more bounce energy from the offset, it finds a higher equilibrium point.
If you want to break equilibrium, there are many ways to do it. One is to soak a bit of velocity whenever your “overshoot” case is triggered, so that your ball slows a bit whenever it touches rubber.
if (this.pos.y > height) {
print(this.pos.y, this.velocity.y);
this.pos.y = height;
this.velocity.y *= -.99;
Now the ball always loses energy on every bounce – and in this case, bounces down, although if it was moving very fast, it might still not be losing as much as it is gaining from being moved up by the bounce wall.
Or, you could just have the ball bounce from however deep it fell into the bottom surface. Rather than teleporting it out of the bottom material, you need to use an extra check to prevent in-wall bounce vibration.
if (this.pos.y > height && this.velocity.y > 0) {
print(this.pos.y, this.velocity.y);
// this.pos.y = height; // Comment out this
this.velocity.y *= -1;
Now a ball will only start moving up if it is in a wall AND moving down, so it will only change direction once. However far it travels into the wall, that distance is lost – no teleporting – and so the bounce height always decays over time, regardless of speed.