Reaction delay
A wait an enemy or computer opponent must serve before it may attack, so the player gets a moment after being seen.
On this page
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
| Game | Year | How | Note |
|---|---|---|---|
| Doom | 1993 | Countdown from spawn | 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 |
| Perfect Dark | 2000 | Sight timer | 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 |
Every claim about this mechanic, by game (2)
References
- ^ abA Doom monster spawns with its type's
reactiontime(8 for the zombieman) except on Nightmare, where it stays 0; each call ofA_Chaselowers 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 setsMF_JUSTHIT) since the check last ran. Melee attacks don't check it.Codeid-Software/DOOMHow 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.
- ^ abA Perfect Dark combat simulant keeps a
shootdelaytimer60per 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 (burstsdoneabove 0 orcyclonedischarging), which keeps it firing without the test, the simulant opens fire only once the timer reaches its difficulty'sshootdelay60: 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 areactiontimeat spawn (8 for the zombieman, 0 on Nightmare), counted down once perA_Chasecall, 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/DOOMrevisedHow 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 secondsrc/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.
Sources
Code
- Software, id. 2024. “DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10).” Commit a77dfb9. id-Software/DOOM @ a77dfb9 · licence, all uses
- Dwyer, Ryan and perfect-dark contributors. 2026. “perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer).” Commit 46d3393. ryandwyer/perfect-dark @ 46d3393 · licence, all uses