On this page
  1. Related
  2. Games
  3. References
  4. Sources

A reaction delay holds an enemy’s attack back after it has a target. Doom gives each monster a count at spawn that it spends while chasing, and it holds off ranged attacks until the count is spent, unless a hit has made it flinch1. Perfect Dark’s simulants run a timer while their target is in sight and fire once it passes a delay that shrinks with their difficulty, from 1.5 s for Meat to none for Perfect and Dark2.

  • Countdown from spawnDoom (1993). A monster spawns with a count, 8 for the zombieman and 0 on Nightmare, and makes no ranged attack until its chase frames spend it, unless a hit has made it flinch.1
  • Sight timerPerfect Dark (2000). A simulant fires once its target has been in sight for its difficulty's delay, 1.5 s for Meat down to none for Perfect and Dark.2

Games

GameYearHowNote
Doom1993Countdown from spawna monster spawns with a count, 8 for the zombieman and 0 on Nightmare, and makes no ranged attack until its chase frames spend it, unless a hit has made it flinch
Perfect Dark2000Sight timera simulant fires once its target has been in sight for its difficulty's delay, 1.5 s for Meat down to none for Perfect and Dark
Every claim about this mechanic, by game (2)

References

  1. ^ abA Doom monster spawns with its type's reactiontime (8 for the zombieman) except on Nightmare, where it stays 0; each call of A_Chase lowers it by 1, and while it is above 0 the missile-range check refuses a ranged attack, unless the monster has flinched from a hit (its pain-chance roll, which sets MF_JUSTHIT) since the check last ran. Melee attacks don't check it.Codeid-Software/DOOM
    How we know
    491    memset (mobj, 0, sizeof (*mobj));
    ⋮
    503    if (gameskill != sk_nightmare)
    504	mobj->reactiontime = info->reactiontime;
    
    linuxdoom-1.10/p_mobj.c lines 491–504 at a77dfb9 · upstream ↗
    1134    {		// MT_POSSESSED
    ⋮
    1140	8,		// reactiontime
    
    linuxdoom-1.10/info.c lines 1134–1140 at a77dfb9 · upstream ↗
    676    if (actor->reactiontime)
    677	actor->reactiontime--;
    
    linuxdoom-1.10/p_enemy.c lines 676–677 at a77dfb9 · upstream ↗
    204    if ( actor->flags & MF_JUSTHIT )
    ⋮
    209	return true;
    ⋮
    212    if (actor->reactiontime)
    213	return false;	// do not attack yet
    
    linuxdoom-1.10/p_enemy.c lines 203–213 at a77dfb9 · upstream ↗
    894    if ( (P_Random () < target->info->painchance)
    ⋮
    897	target->flags |= MF_JUSTHIT;	// fight back!
    
    linuxdoom-1.10/p_inter.c lines 894–897 at a77dfb9 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-27.

    In the claim ledger

  2. ^ abA Perfect Dark combat simulant keeps a shootdelaytimer60 per target: it resets to 0 when the simulant picks a new target, counts up in 60ths of a second while the target is in sight, and counts back down to 0 while it isn't. Unless a burst or the Cyclone's secondary discharge is already under way (burstsdone above 0 or cyclonedischarging), which keeps it firing without the test, the simulant opens fire only once the timer reaches its difficulty's shootdelay60: 90 (1.5 s) for Meat, 60 for Easy, 30 for Normal, 15 for Hard, and 0 for Perfect and Dark, on NTSC; the target must also be within 45/256 of a turn (63°, computed) of its facing, unless the simulant is dizzy. Doom gives each monster a reactiontime at spawn (8 for the zombieman, 0 on Nightmare), counted down once per A_Chase call, and refuses a ranged attack until it reaches 0, unless the monster has been hit since it last checked (MF_JUSTHIT).Coderyandwyer/perfect-dark · id-Software/DOOMrevised
    How we know
    87struct botdifficulty g_BotDifficulties[] = {
    88	//           shootdelay60                                       turnunzeromult      forcezerominspeed
    ⋮
    90	/* meat */ { TICKS(90), BADDTOR2(15),   BADDTOR2(30), TICKS(600), 10, BADDTOR2(40),    BADDTOR2(20), TICKS(1000) },
    91	/* easy */ { TICKS(60), BADDTOR2(7),    BADDTOR2(14), TICKS(360), 10, BADDTOR2(28.5f), BADDTOR2(8),  TICKS(1000) },
    92	/* norm */ { TICKS(30), BADDTOR2(4),    BADDTOR2(8),  TICKS(180), 4,  BADDTOR2(20),    BADDTOR2(5),  TICKS(1500) },
    93	/* hard */ { TICKS(15), BADDTOR2(1.5f), BADDTOR2(4),  TICKS(90),  2,  BADDTOR2(14),    BADDTOR2(2),  TICKS(2500) },
    94	/* perf */ { TICKS(0),  BADDTOR2(0),    BADDTOR2(2),  TICKS(45),  1,  BADDTOR2(10),    BADDTOR2(0),  TICKS(4000) },
    95	/* dark */ { TICKS(0),  BADDTOR2(0),    BADDTOR2(0),  TICKS(0),   0,  BADDTOR2(8),     BADDTOR2(0),  TICKS(4000) },
    
    src/game/bot.c lines 87–95 at 46d3393 · upstream ↗
    45struct botdifficulty {
    46	u8 shootdelay60;
    
    src/game/bot.c lines 45–46 at 46d3393 · upstream ↗
    118#else
    119#define TICKS(val)    (val)
    
    src/include/constants.h lines 118–119 at 46d3393 · upstream ↗
    1365	if (botchr->target != propnum) {
    1366		botchr->target = propnum;
    1367		botchr->aibot->shootdelaytimer60 = 0;
    ⋮
    1373		} else {
    ⋮
    1376	} else {
    1377		if (botchr->aibot->targetinsight) {
    1378			if (g_Vars.lvupdate240 > 0) {
    1379				botchr->aibot->shootdelaytimer60 += g_Vars.diffframe60;
    ⋮
    1381		} else {
    1382			if (g_Vars.lvupdate240 > 0) {
    1383				botchr->aibot->shootdelaytimer60 -= g_Vars.diffframe60;
    ⋮
    1386			if (botchr->aibot->shootdelaytimer60 < 0) {
    ⋮
    
    src/game/bot.c lines 1365–1388 at 46d3393 · upstream ↗
    116	/*0x000*/ s32 diffframe60;         // time between current frame start and previous frame start in 60ths of a second
    
    src/include/types.h line 116 at 46d3393 · upstream ↗
    3600						if (canshoot) {
    3601							if (aibot->cyclonedischarging[i] || aibot->burstsdone[i] > 0) {
    3602								firing = true;
    3603							} else if (chr->target != -1
    3604									&& aibot->targetinsight
    3605									&& aibot->shootdelaytimer60 >= g_BotDifficulties[aibot->config->difficulty].shootdelay60
    3606									&& (bot_is_dizzy(chr) || chr_is_target_in_fov(chr, 45, false))
    3607									&& !chr_is_dead(chr_get_target_prop(chr)->chr)) {
    3608								firing = true;
    
    src/game/bot.c lines 3600–3608 at 46d3393 · upstream ↗
    13931#define D256TOR(degrees256) ((degrees256) * (360.0f * M_BADPI / 180.0f / 256.0f))
    
    src/game/chraction.c line 13931 at 46d3393 · upstream ↗
    13979bool chr_is_target_in_fov(struct chrdata *chr, u8 degrees256, u8 reverse)
    ⋮
    13981	f32 angle = chr_get_angle_to_target2(chr, reverse);
    ⋮
    13983	if ((angle < D256TOR(degrees256) && angle < DTOR(180))
    13984			|| (angle > BADDTOR(360) - D256TOR(degrees256) && angle > DTOR(180))) {
    
    src/game/chraction.c lines 13979–13984 at 46d3393 · upstream ↗
    503    if (gameskill != sk_nightmare)
    504	mobj->reactiontime = info->reactiontime;
    
    linuxdoom-1.10/p_mobj.c lines 503–504 at a77dfb9 · upstream ↗
    1134    {		// MT_POSSESSED
    ⋮
    1140	8,		// reactiontime
    
    linuxdoom-1.10/info.c lines 1134–1140 at a77dfb9 · upstream ↗
    672void A_Chase (mobj_t*	actor)
    ⋮
    676    if (actor->reactiontime)
    677	actor->reactiontime--;
    
    linuxdoom-1.10/p_enemy.c lines 672–677 at a77dfb9 · upstream ↗
    204    if ( actor->flags & MF_JUSTHIT )
    ⋮
    206	// the target just hit the enemy,
    207	// so fight back!
    208	actor->flags &= ~MF_JUSTHIT;
    209	return true;
    ⋮
    212    if (actor->reactiontime)
    213	return false;	// do not attack yet
    
    linuxdoom-1.10/p_enemy.c lines 204–213 at a77dfb9 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Revised after a check by a second AI model, 2026-10-22.

    In the claim ledger

Sources

Code