Status bar face hysteresis

When the player's health moves between ranges corresponding to different status bar face animations, the red HEALTH percentage changes immediately, but the face itself is not updated until the end of the current animation sequence.

For example, if a severely injured player (below 20% health) gains a new weapon, followed closely by a large health pickup such as a megasphere, the status bar briefly displays a very high health percentage alongside an extremely bloodied face, as shown in the figure.

Technical
The phenomenon occurs because the engine chooses the next face graphic according to a strict hierarchy, and then tries to avoid updating the face again until a certain number of tics have passed. From st_stuff.c:

#define ST_EVILGRINCOUNT               (2*TICRATE) #define ST_STRAIGHTFACECOUNT           (TICRATE/2) #define ST_TURNCOUNT                   (1*TICRATE) #define ST_OUCHCOUNT                   (1*TICRATE) #define ST_RAMPAGEDELAY                (2*TICRATE)

void ST_updateFaceWidget(void) {               int                i;                angle_t            badguyangle; angle_t           diffang; static int        lastattackdown = -1; static int        priority = 0; boolean           doevilgrin; if (priority < 10) {                   // dead if (!plyr->health) {                       priority = 9; st_faceindex = ST_DEADFACE; st_facecount = 1; }               }                if (priority < 9) {                   if (plyr->bonuscount) {                       // picking up bonus doevilgrin = false; for (i=0;iweaponowned[i]) {                               doevilgrin = true; oldweaponsowned[i] = plyr->weaponowned[i]; }                       }                        if (doevilgrin) {                           // evil grin if just picked up weapon priority = 8; st_facecount = ST_EVILGRINCOUNT; st_faceindex = ST_calcPainOffset + ST_EVILGRINOFFSET; }                   }                }                if (priority < 8) {                   if (plyr->damagecount                        && plyr->attacker                        && plyr->attacker != plyr->mo) {                       // being attacked priority = 7; if (plyr->health - st_oldhealth > ST_MUCHPAIN) {                           st_facecount = ST_TURNCOUNT; st_faceindex = ST_calcPainOffset + ST_OUCHOFFSET; }                       else {                           badguyangle = R_PointToAngle2(plyr->mo->x,                                                          plyr->mo->y,                                                          plyr->attacker->x,                                                          plyr->attacker->y); if (badguyangle > plyr->mo->angle) {                               // whether right or left diffang = badguyangle - plyr->mo->angle; i = diffang > ANG180; }                           else {                               // whether left or right diffang = plyr->mo->angle - badguyangle; i = diffang <= ANG180; } // confusing, aint it? st_facecount = ST_TURNCOUNT; st_faceindex = ST_calcPainOffset; if (diffang < ANG45) {                               // head-on st_faceindex += ST_RAMPAGEOFFSET; }                           else if (i) {                               // turn face right st_faceindex += ST_TURNOFFSET; }                           else {                               // turn face left st_faceindex += ST_TURNOFFSET+1; }                       }                    }                }                if (priority < 7) {                   // getting hurt because of your own damn stupidity if (plyr->damagecount) {                       if (plyr->health - st_oldhealth > ST_MUCHPAIN) {                           priority = 7; st_facecount = ST_TURNCOUNT; st_faceindex = ST_calcPainOffset + ST_OUCHOFFSET; }                       else {                           priority = 6; st_facecount = ST_TURNCOUNT; st_faceindex = ST_calcPainOffset + ST_RAMPAGEOFFSET; }                   }                }                if (priority < 6) {                   // rapid firing if (plyr->attackdown) {                       if (lastattackdown==-1) lastattackdown = ST_RAMPAGEDELAY; else if (!--lastattackdown) {                           priority = 5; st_faceindex = ST_calcPainOffset + ST_RAMPAGEOFFSET; st_facecount = 1; lastattackdown = 1; }                   }                    else lastattackdown = -1; }               if (priority < 5) {                   // invulnerability if ((plyr->cheats & CF_GODMODE)                       || plyr->powers[pw_invulnerability]) {                       priority = 4; st_faceindex = ST_GODFACE; st_facecount = 1; }               }                // look left or look right if the facecount has timed out if (!st_facecount) {                   st_faceindex = ST_calcPainOffset + (st_randomnumber % 3); st_facecount = ST_STRAIGHTFACECOUNT; priority = 0; }               st_facecount--; }

Each change in the face graphic increases the counter variable st_facecount (bolded for emphasis), which decreases by 1 each tic. In order for the face to be refreshed, st_facecount must be zero or an in-game event with higher precedence must occur; for example, player death replaces a grinning face with the "death" face immediately.

Some face types, such as the default "looking ahead" face or the invulnerability face, can be overridden by damage or a weapon pickup on the very next call (st_facecount = 1), but others linger for half a second or so (TICRATE is 35); subsequent health pickups therefore cannot be represented in the face during that interval.