Status bar face hysteresis

From DoomWiki.org

Status bar face hysteresis demonstrated in MAP30: Icon of Sin

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[edit]

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;i<NUMWEAPONS;i++)        
               {        
                   if (oldweaponsowned[i] != plyr->weaponowned[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.