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

The atlas reads Perfect Dark through Ryan Dwyer’s matching decompilation, which builds ntsc-final1 by default1.

Autoaim scores each armed enemy near the middle of the screen and takes the best, and the nearest one under the screen’s center wins outright2. Its margin is scaled by 1.5 on Agent, 0.75 on Special Agent, 0.2 on Perfect Agent, and 1 on the Perfect Dark difficulty3. It moves the floating crosshair, not the view, easing it toward the target, and the shot leaves through the crosshair4. Doom’s autoaim only tilts the shot up or down5, and Duke Nukem 3D’s turns the shot itself6.

Where a bullet lands matters, unlike in Doom: a head hit counts 4 times and a torso hit twice, and in solo a player’s head shot counts 100 times7. The Falcon 2’s shot deals a fixed 1 with no roll, against a guard’s default 48. The difficulty level scales damage both ways, so on Agent a guard’s hit costs the player half as much9.

The multiplayer simulants wait before they shoot. Each keeps a timer that runs while its target is in sight and falls back while it isn’t, and it fires only once the timer passes its difficulty’s reaction delay: 1.5 s (Meat) to 0 (Perfect, Dark)10,10.

Mechanics

MechanicKindHow
AutoaimCrosshair pullthe best-scoring armed enemy in the middle half of the screen, the nearest under the screen's center winning outright, with a margin scaled by difficulty; the crosshair eases toward it and the shot follows the crosshair
Locational damageHit locationa head hit ×4 and a torso hit ×2; in solo a player's gun shot multiplies a head hit by 25 more
Damage formulaFixed tablethe Falcon 2's shot carries a fixed damage of 1 with no roll, scaled by body part and difficulty
Difficulty levelTable of scaleseach solo difficulty sets multipliers for damage taken and dealt, enemy accuracy, autoaim, and ammo, and Agent eases off further at low health
Reaction delaySight 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 game, by concept (8)
generalThe Perfect Dark decompilation builds six ROMs and defaults to ntsc-final, which its README describes as "NTSC 8.7 final" beside ntsc-1.0, "the initial, buggy release"; the Makefile sets VERSION = 2 for it.Code
AutoaimPerfect Dark's standard autoaim, for a gun whose autoaim is on, scores each character on screen and takes the highest score. It skips teammates, the dead and dying, and anyone not holding a gun (unless flagged CHRCFLAG_FORCEAUTOAIM). A candidate needs a clear line from the player, its screen center in the middle half of the screen's width, and its height reaching into the band from 17.5% to 82.5% of the screen's height; the screen's center must lie within 1.5 of its screen widths of its middle, times a solo scale (perfect-dark/autoaim-scale). The score is 1 when the screen's center x lies between the target's left and right screen edges, and otherwise falls off with the gap over that margin. The walk runs from the end of a list props_sort orders by prop->z, largest first, which the decompilers' comment calls "furthest first"; since a score of 1 ends the walk, the nearest such target wins on that reading. Doom traces ahead and 5.625° to each side for a target, and Duke Nukem 3D's aim takes the target within 8.4° of the facing that is nearest along it.Code
In solo Perfect Dark the autoaim margin (perfect-dark/autoaim) is multiplied by g_AutoAimScale, which lv_update_solo_handicaps sets by difficulty: 1.5 on Agent, 0.75 on Special Agent, 0.2 on Perfect Agent, and 1 on the Perfect Dark difficulty; the ntsc-final build takes the non-Japanese values (g_Jpn is false). Co-op and Counter-Operative use their own values.Code
Perfect Dark's autoaim moves the crosshair, not the view: once a target is picked, bmove_update_autoaim_x_prop and _y_prop set the aim point to the target's screen center (clamped to the window), and a new target replaces the old only after a 30/60 s hold; bgun_swivel then eases the crosshair toward the point, keeping a fraction crossdamp of the gap each 1/240 s (a unit the decompilers' comments give). On NTSC that fraction, autoaimdamp, falls by 0.00032 a 60th of a second while autoaim holds a target, from at most 0.979 (it climbs back there while none is held) to 0.963, so the pull strengthens over up to 50/60 s (computed); at 0.963 the crosshair closes 14% of the gap each 60th of a second (1 − 0.963⁴, computed). The shot leaves through the crosshair's position plus a random spread. Doom's bullet keeps the player's facing and takes only a vertical slope, and Duke Nukem 3D turns the shot itself to the target's angle in the call that fires it.Code
Damage formulaThe Falcon 2's single shot in Perfect Dark carries a damage of 1 (invfunc_falcon2_singleshot), read by gset_get_damage. A guard adds each hit to its damage against a maxdamage that chr_allocate sets to 4 (a level's AI script can set another), and chr_react_to_damage starts its death once damage reaches it; so on Perfect Agent a default guard takes 4 Falcon 2 shots to a limb, 2 to the torso, or 1 to the head (computed). The player's health starts at 1, and a hit takes damage × 0.125 from it, so a guard's Falcon 2 bullet takes an eighth on Perfect Agent (computed). Each Doom pistol bullet deals 5, 10, or 15 against a zombieman's 20.Code
Difficulty levelSolo Perfect Dark scales damage by difficulty in lv_update_solo_handicaps, which runs every tick: a guard's hit on the player is multiplied by 0.5 on Agent, 0.6 on Special Agent, and 1 on Perfect Agent (and on the Perfect Dark difficulty, times its own enemy-damage setting); the player's shots are doubled on Agent; and explosions hurt the player 0.25 times on Agent and 0.75 times on Special Agent. On Agent the scales for the damage the player takes, from guards and from explosions, shrink further once health plus shield is at or below 0.6, falling linearly to half at 0.125, so 0.25 for a guard's hit (computed); the player's ×2 on its own shots stays fixed. Doom halves all damage to the player on its easiest skill, sk_baby.Code
Locational damagePerfect Dark's chr_damage scales a hit by the body part (hitpart) it struck: a head hit is multiplied by 4 and a torso hit doubled, the limbs and pelvis keep the damage, and a hit on the held gun or a hat deals none. Outside normal multiplayer a gun shot fired by a player multiplies a head hit by a further 25 (headshotdamagescale), 100 in all, unless a shield took it; a Skedar's head is only doubled, and its tail takes 10 times. A hit with no part (HITPART_GENERAL, as when a guard's bullet hits the player) is halved and then doubled as the torso, so it keeps its damage. Doom's P_LineAttack passes a bullet's damage to the thing it hits with no body part.Code
Reaction delayA 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).Code

References

  1. ^ abThe Perfect Dark decompilation builds six ROMs and defaults to ntsc-final, which its README describes as "NTSC 8.7 final" beside ntsc-1.0, "the initial, buggy release"; the Makefile sets VERSION = 2 for it.Coderyandwyer/perfect-dark
    How we know
    5The project is a matching decompilation. When a matching decompilation is compiled with the same compiler that the original developers used, the output will be exactly the same as the retail game, byte for byte.
    
    README.md line 5 at 46d3393 · upstream ↗
    34| ntsc-1.0   | NTSC 8.7 final (the initial, buggy release) | 7f4171b0c8d17815be37913f535e4e93 |
    35| ntsc-final | NTSC 8.7 final                              | e03b088b6ac9e0080440efed07c1e40f |
    
    README.md lines 34–35 at 46d3393 · upstream ↗
    17ROMID ?= ntsc-final
    
    Makefile line 17 at 46d3393 · upstream ↗
    87ifeq ($(ROMID), ntsc-final)
    88    VERSION = 2
    
    Makefile lines 87–88 at 46d3393 · upstream ↗

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

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

    In the claim ledger

  2. ^Perfect Dark's standard autoaim, for a gun whose autoaim is on, scores each character on screen and takes the highest score. It skips teammates, the dead and dying, and anyone not holding a gun (unless flagged CHRCFLAG_FORCEAUTOAIM). A candidate needs a clear line from the player, its screen center in the middle half of the screen's width, and its height reaching into the band from 17.5% to 82.5% of the screen's height; the screen's center must lie within 1.5 of its screen widths of its middle, times a solo scale (perfect-dark/autoaim-scale). The score is 1 when the screen's center x lies between the target's left and right screen edges, and otherwise falls off with the gap over that margin. The walk runs from the end of a list props_sort orders by prop->z, largest first, which the decompilers' comment calls "furthest first"; since a score of 1 ends the walk, the nearest such target wins on that reading. Doom traces ahead and 5.625° to each side for a target, and Duke Nukem 3D's aim takes the target within 8.4° of the facing that is nearest along it.Coderyandwyer/perfect-dark · id-Software/DOOM · 3D Realms, duke3dsource.ziprevised
    How we know
    60/**
    61 * Populate g_Vars.onscreenprops. This is an array of prop pointers, filtered by
    62 * props that are on screen and sorted by distance descending (furthest first).
    ⋮
    64void props_sort(void)
    
    src/game/prop.c lines 60–64 at 46d3393 · upstream ↗
    94		for (j = i; j < count; j++) {
    95			if (depths[j] > depth) {
    96				depth = depths[j];
    97				swapindex = j;
    
    src/game/prop.c lines 94–98 at 46d3393 · upstream ↗
    2699		struct prop **ptr = g_Vars.endonscreenprops - 1;
    ⋮
    2702		while (ptr >= g_Vars.onscreenprops) {
    ⋮
    2710					if (!chr_compare_teams(g_Vars.currentplayer->prop->chr, chr, COMPARE_FRIENDS)
    2711							&& (chr_get_held_prop(chr, HAND_RIGHT)
    2712								|| chr_get_held_prop(chr, HAND_LEFT)
    2713								|| (chr->chrflags & CHRCFLAG_FORCEAUTOAIM)
    2714								|| chr->gunprop)
    2715							&& chr_calculate_autoaim(prop, &screenpos, xrange, yrange)) {
    2716						f32 score = prop_calculate_autoaim_score(prop, &screenpos, xrange, yrange, thisaimpos, false, cangangsta, 0);
    ⋮
    2718						if (score > bestscore) {
    ⋮
    2724							if (score >= 1) {
    2725								break;
    
    src/game/prop.c lines 2699–2725 at 46d3393 · upstream ↗
    5014bool chr_calculate_autoaim(struct prop *prop, struct coord *screenpos, f32 *xrange, f32 *yrange)
    ⋮
    5020			&& chr->actiontype != ACT_DIE
    ⋮
    5023			&& chr->actiontype != ACT_DEAD
    
    src/game/chr.c lines 5014–5025 at 46d3393 · upstream ↗
    2412	} else {
    2413		top = cam_get_screen_top() + cam_get_screen_height() * 0.175f;
    2414		bottom = cam_get_screen_top() + cam_get_screen_height() * 0.825f;
    2415		left = cam_get_screen_left() + cam_get_screen_width() * 0.25f;
    2416		right = cam_get_screen_left() + cam_get_screen_width() * 0.75f;
    
    src/game/prop.c lines 2412–2417 at 46d3393 · upstream ↗
    2441	if (sp74[1] >= top && bottom >= sp7c[1]) {
    ⋮
    2449				sp48 = (sp84[0] - sp8c[0]) * 1.5f;
    ⋮
    2451				if (!g_Vars.normmplayerisrunning) {
    2452					sp48 = sp48 * g_AutoAimScale;
    ⋮
    2455				sp4c = cam_get_screen_left() + 0.5f * cam_get_screen_width() >= (sp8c[0] + sp84[0]) * 0.5f - sp48
    2456					&& cam_get_screen_left() + 0.5f * cam_get_screen_width() <= (sp8c[0] + sp84[0]) * 0.5f + sp48
    2457					&& left <= spa0[0]
    2458					&& right >= spa0[0];
    
    src/game/prop.c lines 2441–2458 at 46d3393 · upstream ↗
    2474				ok = cd_test_los_oobok(&playerprop->pos, playerprop->rooms, &prop->pos,
    2475						CDTYPE_OBJS | CDTYPE_DOORS | CDTYPE_PATHBLOCKER | CDTYPE_BG,
    ⋮
    2479			if (ok) {
    ⋮
    2502				if (cam_get_screen_left() + 0.5f * cam_get_screen_width() >= sp8c[0]
    2503						&& cam_get_screen_left() + 0.5f * cam_get_screen_width() <= sp84[0]) {
    2504					result = 1;
    2505				} else if (cam_get_screen_left() + 0.5f * cam_get_screen_width() >= sp8c[0]) {
    2506					result = 1 - ((cam_get_screen_left() + 0.5f * cam_get_screen_width()) - sp84[0]) / sp48;
    2507				} else {
    2508					result = 1 - (sp8c[0] - (cam_get_screen_left() + 0.5f * cam_get_screen_width())) / sp48;
    
    src/game/prop.c lines 2474–2508 at 46d3393 · upstream ↗
    601void P_BulletSlope (mobj_t*	mo)
    ⋮
    606    an = mo->angle;
    607    bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    ⋮
    611	an += 1<<26;
    612	bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    ⋮
    615	    an -= 2<<26;
    616	    bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    
    linuxdoom-1.10/p_pspr.c lines 601–619 at a77dfb9 · upstream ↗
    62#define AUTO_AIM_ANGLE          48
    
    source/DUKE3D.H line 62 at e75bb06 · upstream ↗
    273                if( (dy1*xv) <= (dx1*yv) )
    274                    if( ( dy2*xv ) >= (dx2*yv) )
    ⋮
    276                    sdist = mulscale(dx3,xv,14) + mulscale(dy3,yv,14);
    277                    if( sdist > 512 && sdist < smax )
    ⋮
    287                        if( a && cans )
    ⋮
    289                            smax = sdist;
    290                            j = i;
    
    source/PLAYER.C lines 273–291 at e75bb06 · 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

  3. ^In solo Perfect Dark the autoaim margin (perfect-dark/autoaim) is multiplied by g_AutoAimScale, which lv_update_solo_handicaps sets by difficulty: 1.5 on Agent, 0.75 on Special Agent, 0.2 on Perfect Agent, and 1 on the Perfect Dark difficulty; the ntsc-final build takes the non-Japanese values (g_Jpn is false). Co-op and Counter-Operative use their own values.Coderyandwyer/perfect-dark
    How we know
    1840	} else {
    1841		if (g_Difficulty == DIFF_A) {
    ⋮
    1864			g_AutoAimScale = 1.5f;
    
    src/game/lv.c lines 1840–1864 at 46d3393 · upstream ↗
    1867		} else if (g_Difficulty == DIFF_SA) {
    ⋮
    1877#if VERSION >= VERSION_JPN_FINAL
    1878			g_AutoAimScale = 0.75f;
    1879#else
    1880			g_AutoAimScale = g_Jpn ? 1.1f : 0.75f;
    1881#endif
    
    src/game/lv.c lines 1867–1881 at 46d3393 · upstream ↗
    1884		} else if (g_Difficulty == DIFF_PA) {
    ⋮
    1897			g_AutoAimScale = g_Jpn ? 0.75f : 0.2f;
    1898#endif
    
    src/game/lv.c lines 1884–1898 at 46d3393 · upstream ↗
    1901		} else if (g_Difficulty == DIFF_PD) {
    ⋮
    1911			g_AutoAimScale = 1;
    
    src/game/lv.c lines 1901–1911 at 46d3393 · upstream ↗
    53#if VERSION >= VERSION_PAL_BETA
    54bool g_Jpn = VERSION == VERSION_JPN_FINAL ? true : false;
    
    src/game/lang.c lines 53–54 at 46d3393 · upstream ↗
    66bool g_Jpn = false;
    
    src/game/lang.c line 66 at 46d3393 · upstream ↗
    2451				if (!g_Vars.normmplayerisrunning) {
    2452					sp48 = sp48 * g_AutoAimScale;
    
    src/game/prop.c lines 2451–2453 at 46d3393 · upstream ↗

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

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

    In the claim ledger

  4. ^Perfect Dark's autoaim moves the crosshair, not the view: once a target is picked, bmove_update_autoaim_x_prop and _y_prop set the aim point to the target's screen center (clamped to the window), and a new target replaces the old only after a 30/60 s hold; bgun_swivel then eases the crosshair toward the point, keeping a fraction crossdamp of the gap each 1/240 s (a unit the decompilers' comments give). On NTSC that fraction, autoaimdamp, falls by 0.00032 a 60th of a second while autoaim holds a target, from at most 0.979 (it climbs back there while none is held) to 0.963, so the pull strengthens over up to 50/60 s (computed); at 0.963 the crosshair closes 14% of the gap each 60th of a second (1 − 0.963⁴, computed). The shot leaves through the crosshair's position plus a random spread. Doom's bullet keeps the player's facing and takes only a vertical slope, and Duke Nukem 3D turns the shot itself to the target's angle in the call that fires it.Coderyandwyer/perfect-dark · id-Software/DOOM · 3D Realms, duke3dsource.zip
    How we know
    2736	if (bestprop) {
    2737		if (bmove_is_autoaim_y_enabled_for_current_weapon() || iscmpsec) {
    2738			bmove_update_autoaim_y_prop(bestprop, (aimpos[1] - cam_get_screen_top()) / (cam_get_screen_height() * 0.5f) - 1);
    ⋮
    2742			bmove_update_autoaim_x_prop(bestprop, (aimpos[0] - cam_get_screen_left()) / (cam_get_screen_width() * 0.5f) - 1);
    
    src/game/prop.c lines 2736–2743 at 46d3393 · upstream ↗
    97void bmove_update_autoaim_y_prop(struct prop *prop, f32 autoaimy)
    ⋮
    99	if (g_Vars.currentplayer->autoyaimtime60 >= 0) {
    100		g_Vars.currentplayer->autoyaimtime60 -= g_Vars.lvupdate60;
    ⋮
    103	if (prop != g_Vars.currentplayer->autoyaimprop) {
    104		if (g_Vars.currentplayer->autoyaimtime60 < 0) {
    105			g_Vars.currentplayer->autoyaimtime60 = TICKS(30);
    106			g_Vars.currentplayer->autoyaimprop = prop;
    107		} else {
    108			return;
    ⋮
    112	g_Vars.currentplayer->autoaimy = autoaimy;
    
    src/game/bondmove.c lines 97–112 at 46d3393 · upstream ↗
    118#else
    119#define TICKS(val)    (val)
    
    src/include/constants.h lines 118–119 at 46d3393 · upstream ↗
    1763			// Auto aim - move crosshair towards target
    ⋮
    1771			if (g_Vars.currentplayer->autoaimdamp > (PAL ? 0.955f : 0.963f)) {
    1772				g_Vars.currentplayer->autoaimdamp -= (PAL ? 0.00037999986670911f : 0.00031999943894334f) * g_Vars.lvupdate60freal;
    ⋮
    1779			x = g_Vars.currentplayer->autoaimx;
    1780			y = g_Vars.currentplayer->autoaimy;
    ⋮
    1785				bgun_swivel_with_damp(x, y, g_Vars.currentplayer->autoaimdamp);
    ⋮
    1788			// This code moves the crosshair as the player turns and makes
    1789			// it return to the centre when not affected by anything else.
    1790			if (g_Vars.currentplayer->autoaimdamp < (PAL ? 0.974f : 0.979f)) {
    1791				g_Vars.currentplayer->autoaimdamp += (PAL ? 0.00037999986670911f : 0.00031999943894334f) * g_Vars.lvupdate60freal;
    
    src/game/bondmove.c lines 1763–1791 at 46d3393 · upstream ↗
    4964	for (l = 0; l < g_Vars.lvupdate240; l++) {
    4965		player->crosspossum[0] = player->crosspossum[0] * crossdamp + screenx;
    4966		player->crosspossum[1] = player->crosspossum[1] * crossdamp + screeny;
    ⋮
    4975	player->crosspos[0] = player->crosspossum[0] * (1.0f - crossdamp) * screenwidth * 0.5f + screenwidth * 0.5f;
    4976	player->crosspos[1] = player->crosspossum[1] * (1.0f - crossdamp) * screenheight * 0.5f + screenheight * 0.5f;
    
    src/game/bondgun.c lines 4964–4976 at 46d3393 · upstream ↗
    112	// names ending in 60 mean their units are 60ths of a second
    113	// names ending in 240 mean their units are 240ths of a second
    
    src/include/types.h lines 112–113 at 46d3393 · upstream ↗
    5123	crosspos[0] = player->crosspos[0] + randfactor * scaledspread * cam_get_screen_width()
    ⋮
    5132	crosspos[1] = player->crosspos[1] + (randfactor * scaledspread * cam_get_screen_height())
    ⋮
    5139	cam0f0b4c3c(crosspos, gundir2d, 1);
    
    src/game/bondgun.c lines 5123–5139 at 46d3393 · upstream ↗
    633    damage = 5*(P_Random ()%3+1);
    634    angle = mo->angle;
    ⋮
    639    P_LineAttack (mo, angle, MISSILERANGE, bulletslope, damage);
    
    linuxdoom-1.10/p_pspr.c lines 633–639 at a77dfb9 · upstream ↗
    489                j = aim( s, AUTO_AIM_ANGLE );
    490                if(j >= 0)
    ⋮
    508                    sa = getangle(sprite[j].x-sx,sprite[j].y-sy);
    
    source/PLAYER.C lines 489–508 at e75bb06 · upstream ↗

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

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

    In the claim ledger

  5. ^Doom aims vertically for the player: P_BulletSlope traces 1,024 units straight ahead, then 5.625° (1<<26) to the left, then to the right, and takes the slope to the middle of the first shootable thing's visible part, within ±100/160; a bullet keeps the player's facing and takes only that slope, but a player's rocket, plasma, or BFG ball also turns to the side trace that found the target and flies level when none did.Codeid-Software/DOOM
    How we know
    601void P_BulletSlope (mobj_t*	mo)
    ⋮
    606    an = mo->angle;
    607    bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    ⋮
    611	an += 1<<26;
    612	bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    ⋮
    615	    an -= 2<<26;
    616	    bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    
    linuxdoom-1.10/p_pspr.c lines 601–619 at a77dfb9 · upstream ↗
    1038    // can't shoot outside view angles
    1039    topslope = 100*FRACUNIT/160;	
    1040    bottomslope = -100*FRACUNIT/160;
    ⋮
    1050    if (linetarget)
    1051	return aimslope;
    ⋮
    1053    return 0;
    
    linuxdoom-1.10/p_map.c lines 1038–1053 at a77dfb9 · upstream ↗
    882    // this thing can be hit!
    883    if (thingtopslope > topslope)
    884	thingtopslope = topslope;
    ⋮
    889    aimslope = (thingtopslope+thingbottomslope)/2;
    890    linetarget = th;
    
    linuxdoom-1.10/p_map.c lines 882–892 at a77dfb9 · upstream ↗
    633    damage = 5*(P_Random ()%3+1);
    634    angle = mo->angle;
    ⋮
    639    P_LineAttack (mo, angle, MISSILERANGE, bulletslope, damage);
    
    linuxdoom-1.10/p_pspr.c lines 633–639 at a77dfb9 · upstream ↗
    948    an = source->angle;
    949    slope = P_AimLineAttack (source, an, 16*64*FRACUNIT);
    ⋮
    951    if (!linetarget)
    ⋮
    954	slope = P_AimLineAttack (source, an, 16*64*FRACUNIT);
    ⋮
    956	if (!linetarget)
    ⋮
    959	    slope = P_AimLineAttack (source, an, 16*64*FRACUNIT);
    ⋮
    962	if (!linetarget)
    ⋮
    964	    an = source->angle;
    965	    slope = 0;
    ⋮
    979    th->angle = an;
    ⋮
    984    th->momz = FixedMul( th->info->speed, slope);
    
    linuxdoom-1.10/p_mobj.c lines 948–984 at a77dfb9 · upstream ↗
    69#define ANG90			0x40000000
    
    linuxdoom-1.10/tables.h line 69 at a77dfb9 · upstream ↗

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

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

    In the claim ledger

  6. ^When a Duke Nukem 3D player fires the pistol, shotgun, or chaingun, aim looks for a target within AUTO_AIM_ANGLE, 48 of 2,048 angle units (about 8.4°, computed), to either side of the facing; of those in sight, more than 512 along the facing, and with a height difference over that distance (scaled by 10) within 100 of the view's pitch, it takes the nearest along the facing, searching the sprite lists 10, 13, 1, and 2 in that order and stopping at the first list with a match, and skipping fellow players in co-op. The shot then turns to the target's angle (getangle) and takes the slope to a point ((xrepeat × tile height) << 1) + (5 << 8) above the target's z. Doom's P_BulletSlope traces only straight ahead and 5.625° to each side, and its bullet keeps the player's facing.Code3D Realms, duke3dsource.zip · id-Software/DOOM
    How we know
    62#define AUTO_AIM_ANGLE          48
    
    source/DUKE3D.H line 62 at e75bb06 · upstream ↗
    204short aim(spritetype *s,short aang)
    ⋮
    208    short aimstats[] = {10,13,1,2};
    ⋮
    222    dx1 = sintable[(a+512-aang)&2047];
    223    dy1 = sintable[(a-aang)&2047];
    224    dx2 = sintable[(a+512+aang)&2047];
    225    dy2 = sintable[(a+aang)&2047];
    
    source/PLAYER.C lines 204–227 at e75bb06 · upstream ↗
    230    for(k=0;k<4;k++)
    ⋮
    232        if( j >= 0 )
    233            break;
    ⋮
    240                    if( PN == APLAYER &&
    ⋮
    242                        ud.coop == 1 &&
    243                        s->picnum == APLAYER &&
    244                        s != &sprite[i])
    245                            continue;
    
    source/PLAYER.C lines 230–246 at e75bb06 · upstream ↗
    273                if( (dy1*xv) <= (dx1*yv) )
    274                    if( ( dy2*xv ) >= (dx2*yv) )
    ⋮
    276                    sdist = mulscale(dx3,xv,14) + mulscale(dy3,yv,14);
    277                    if( sdist > 512 && sdist < smax )
    ⋮
    279                        if(s->picnum == APLAYER)
    280                            a = (klabs(scale(SZ-s->z,10,sdist)-(ps[s->yvel].horiz+ps[s->yvel].horizoff-100)) < 100);
    ⋮
    287                        if( a && cans )
    ⋮
    289                            smax = sdist;
    290                            j = i;
    
    source/PLAYER.C lines 273–291 at e75bb06 · upstream ↗
    489                j = aim( s, AUTO_AIM_ANGLE );
    490                if(j >= 0)
    ⋮
    492                    dal = ((sprite[j].xrepeat*tilesizy[sprite[j].picnum])<<1)+(5<<8);
    ⋮
    507                    zvel = ( ( sprite[j].z-sz-dal )<<8 ) / ldist(&sprite[ps[p].i], &sprite[j]) ;
    508                    sa = getangle(sprite[j].x-sx,sprite[j].y-sy);
    
    source/PLAYER.C lines 489–508 at e75bb06 · upstream ↗
    601void P_BulletSlope (mobj_t*	mo)
    ⋮
    606    an = mo->angle;
    607    bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    ⋮
    611	an += 1<<26;
    612	bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    ⋮
    615	    an -= 2<<26;
    616	    bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
    
    linuxdoom-1.10/p_pspr.c lines 601–619 at a77dfb9 · upstream ↗
    627( mobj_t*	mo,
    628  boolean	accurate )
    ⋮
    634    angle = mo->angle;
    ⋮
    639    P_LineAttack (mo, angle, MISSILERANGE, bulletslope, damage);
    
    linuxdoom-1.10/p_pspr.c lines 626–639 at a77dfb9 · upstream ↗

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

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

    In the claim ledger

  7. ^Perfect Dark's chr_damage scales a hit by the body part (hitpart) it struck: a head hit is multiplied by 4 and a torso hit doubled, the limbs and pelvis keep the damage, and a hit on the held gun or a hat deals none. Outside normal multiplayer a gun shot fired by a player multiplies a head hit by a further 25 (headshotdamagescale), 100 in all, unless a shield took it; a Skedar's head is only doubled, and its tail takes 10 times. A hit with no part (HITPART_GENERAL, as when a guard's bullet hits the player) is halved and then doubled as the torso, so it keeps its damage. Doom's P_LineAttack passes a bullet's damage to the thing it hits with no body part.Coderyandwyer/perfect-dark · id-Software/DOOM
    How we know
    4262void chr_damage(struct chrdata *chr, f32 damage, struct coord *vector, struct gset *gset,
    4263		struct prop *aprop, s32 hitpart, bool damageshield, struct prop *prop2,
    ⋮
    4273	f32 headshotdamagescale = 1;
    
    src/game/chraction.c lines 4262–4273 at 46d3393 · upstream ↗
    4368	if (ismelee) {
    4369		isshoot = false;
    
    src/game/chraction.c lines 4368–4370 at 46d3393 · upstream ↗
    4390	// Apply damage scaling based on difficulty settings
    4391	if (g_Vars.mplayerisrunning == false) {
    4392		// Solo
    ⋮
    4397		} else if (aprop && aprop->type == PROPTYPE_PLAYER) {
    4398			// Player is attacking
    4399			damage *= g_PlayerDamageTxScale;
    4400			headshotdamagescale = 25;
    
    src/game/chraction.c lines 4390–4400 at 46d3393 · upstream ↗
    4414	} else if (g_Vars.coopplayernum >= 0) {
    ⋮
    4420		} else if (aprop && aprop->type == PROPTYPE_PLAYER && vprop->type != PROPTYPE_PLAYER) {
    4421			damage *= g_PlayerDamageTxScale;
    4422			headshotdamagescale = 25;
    
    src/game/chraction.c lines 4413–4422 at 46d3393 · upstream ↗
    4707		if (hitpart == HITPART_GENERAL) {
    4708			// Halve the damage because it's doubled for torso below
    4709			hitpart = HITPART_TORSO;
    4710			damage *= 0.5f;
    ⋮
    4713			hitpart = HITPART_TORSO;
    ⋮
    4717		// Hits to a Skedar's tail are 10x more lethal
    4718		if (race == RACE_SKEDAR && hitpart == HITPART_TAIL) {
    4719			damage *= 10;
    ⋮
    4724		if (hitpart == HITPART_HEAD) {
    4725			if (race == RACE_SKEDAR) {
    4726				damage += damage;
    4727				chr_flinch_head(chr, angle);
    ⋮
    
    src/game/chraction.c lines 4707–4751 at 46d3393 · upstream ↗
    1396#define HITPART_LFOOT       1
    ⋮
    1402#define HITPART_PELVIS      7
    1403#define HITPART_HEAD        8
    ⋮
    1410#define HITPART_TORSO       15
    1411#define HITPART_TAIL        16
    1412#define HITPART_GUN         100
    1413#define HITPART_HAT         110
    
    src/include/constants.h lines 1396–1413 at 46d3393 · upstream ↗
    10361						if (hitplayer) {
    10362							f32 damage = gset_get_damage(&gset);
    ⋮
    10385							chr_damage_by_impact(targetchr, damage, &vector, &gset, chr->prop, HITPART_GENERAL, targetprop, node, model, side, NULL);
    
    src/game/chraction.c lines 10361–10385 at 46d3393 · upstream ↗
    1005    if (in->d.thing->flags & MF_NOBLOOD)
    1006	P_SpawnPuff (x,y,z);
    1007    else
    1008	P_SpawnBlood (x,y,z, la_damage);
    ⋮
    1010    if (la_damage)
    1011	P_DamageMobj (th, shootthing, shootthing, la_damage);
    
    linuxdoom-1.10/p_map.c lines 1004–1011 at a77dfb9 · upstream ↗

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

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

    In the claim ledger

  8. ^The Falcon 2's single shot in Perfect Dark carries a damage of 1 (invfunc_falcon2_singleshot), read by gset_get_damage. A guard adds each hit to its damage against a maxdamage that chr_allocate sets to 4 (a level's AI script can set another), and chr_react_to_damage starts its death once damage reaches it; so on Perfect Agent a default guard takes 4 Falcon 2 shots to a limb, 2 to the torso, or 1 to the head (computed). The player's health starts at 1, and a hit takes damage × 0.125 from it, so a guard's Falcon 2 bullet takes an eighth on Perfect Agent (computed). Each Doom pistol bullet deals 5, 10, or 15 against a zombieman's 20.Coderyandwyer/perfect-dark · id-Software/DOOM
    How we know
    485struct funcdef_shootsingle invfunc_falcon2_singleshot = {
    486	INVENTORYFUNCTYPE_SHOOT_SINGLE,
    ⋮
    495	1, // damage
    
    src/game/invitems.c lines 485–495 at 46d3393 · upstream ↗
    2920struct funcdef_shoot {
    2921	struct funcdef base;
    2922	/*0x14*/ struct recoilsettings *recoilsettings;
    2923	/*0x18*/ s8 recoverytime60;
    2924	/*0x1c*/ f32 damage;
    
    src/include/types.h lines 2920–2924 at 46d3393 · upstream ↗
    475f32 gset_get_damage(struct gset *gset)
    ⋮
    481		if ((func->type & 0xff) == INVENTORYFUNCTYPE_SHOOT) {
    482			struct funcdef_shoot *shootfunc = (struct funcdef_shoot *)func;
    483			damage = shootfunc->damage;
    
    src/game/gset.c lines 475–483 at 46d3393 · upstream ↗
    505	if (bgun_is_firing(HAND_LEFT) && bgun_is_firing(HAND_RIGHT)) {
    506		damage += damage;
    
    src/game/gset.c lines 505–507 at 46d3393 · upstream ↗
    1085void chr_allocate(struct prop *prop, u8 *ailist)
    
    src/game/chr.c line 1085 at 46d3393 · upstream ↗
    1127	chr->maxdamage = 4;
    
    src/game/chr.c line 1127 at 46d3393 · upstream ↗
    3147bool ai_set_max_damage(void)
    ⋮
    3149	u8 *cmd = g_Vars.ailist + g_Vars.aioffset;
    3150	f32 maxdamage = (cmd[4] | (cmd[3] << 8)) * 0.1f;
    
    src/game/chraicommands.c lines 3147–3150 at 46d3393 · upstream ↗
    4965				chr->damage += damage;
    
    src/game/chraction.c line 4965 at 46d3393 · upstream ↗
    3571	if (chr->damage >= chr->maxdamage) {
    3572		chr_begin_death(chr, vector, angle, hitpart, gset, false, aplayernum);
    
    src/game/chraction.c lines 3571–3572 at 46d3393 · upstream ↗
    5077						} else {
    5078							chr_react_to_damage(chr, vector, angle, hitpart, gset, aplayernum);
    
    src/game/chraction.c lines 5077–5078 at 46d3393 · upstream ↗
    257	g_Vars.players[index]->bondhealth = 1;
    
    src/game/playermgr.c line 257 at 46d3393 · upstream ↗
    4795					f32 statsamount = amount = damage * 0.125f;
    ⋮
    4812					g_Vars.currentplayer->bondhealth -= amount / healthscale;
    
    src/game/chraction.c lines 4795–4812 at 46d3393 · upstream ↗
    633    damage = 5*(P_Random ()%3+1);
    
    linuxdoom-1.10/p_pspr.c line 633 at a77dfb9 · upstream ↗
    1134    {		// MT_POSSESSED
    1135	3004,		// doomednum
    1136	S_POSS_STND,		// spawnstate
    1137	20,		// spawnhealth
    
    linuxdoom-1.10/info.c lines 1134–1137 at a77dfb9 · upstream ↗

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

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

    In the claim ledger

  9. ^Solo Perfect Dark scales damage by difficulty in lv_update_solo_handicaps, which runs every tick: a guard's hit on the player is multiplied by 0.5 on Agent, 0.6 on Special Agent, and 1 on Perfect Agent (and on the Perfect Dark difficulty, times its own enemy-damage setting); the player's shots are doubled on Agent; and explosions hurt the player 0.25 times on Agent and 0.75 times on Special Agent. On Agent the scales for the damage the player takes, from guards and from explosions, shrink further once health plus shield is at or below 0.6, falling linearly to half at 0.125, so 0.25 for a guard's hit (computed); the player's ×2 on its own shots stays fixed. Doom halves all damage to the player on its easiest skill, sk_baby.Coderyandwyer/perfect-dark · id-Software/DOOMrevised
    How we know
    1840	} else {
    1841		if (g_Difficulty == DIFF_A) {
    1842			f32 totalhealth;
    1843			f32 frac = 1;
    ⋮
    1846				totalhealth = player_get_health_frac() + player_get_shield_frac();
    ⋮
    1848				if (totalhealth <= 0.125f) {
    1849					frac = 0.5f;
    1850				} else if (totalhealth <= 0.6f) {
    1851					frac = (totalhealth - 0.125f) * 0.5f / 0.47500002384186f + 0.5f;
    ⋮
    1861			g_PlayerDamageRxScale = 0.5f * frac;
    1862			g_PlayerDamageTxScale = 2;
    1863			g_ExplosionDamageTxScale = 0.25f * frac;
    
    src/game/lv.c lines 1840–1866 at 46d3393 · upstream ↗
    1867		} else if (g_Difficulty == DIFF_SA) {
    ⋮
    1874			g_PlayerDamageRxScale = 0.6f;
    1875			g_PlayerDamageTxScale = 1;
    1876			g_ExplosionDamageTxScale = 0.75f;
    
    src/game/lv.c lines 1867–1876 at 46d3393 · upstream ↗
    1884		} else if (g_Difficulty == DIFF_PA) {
    ⋮
    1891			g_PlayerDamageRxScale = 1;
    1892			g_PlayerDamageTxScale = 1;
    1893			g_ExplosionDamageTxScale = 1;
    
    src/game/lv.c lines 1884–1893 at 46d3393 · upstream ↗
    1901		} else if (g_Difficulty == DIFF_PD) {
    ⋮
    1908			g_PlayerDamageRxScale = 1;
    1909			g_PlayerDamageTxScale = 1;
    1910			g_ExplosionDamageTxScale = 1;
    
    src/game/lv.c lines 1901–1910 at 46d3393 · upstream ↗
    2297		lv_update_solo_handicaps();
    
    src/game/lv.c line 2297 at 46d3393 · upstream ↗
    4390	// Apply damage scaling based on difficulty settings
    4391	if (g_Vars.mplayerisrunning == false) {
    4392		// Solo
    4393		if (explosion) {
    4394			if (vprop->type == PROPTYPE_PLAYER) {
    4395				damage *= g_ExplosionDamageTxScale;
    ⋮
    4397		} else if (aprop && aprop->type == PROPTYPE_PLAYER) {
    4398			// Player is attacking
    4399			damage *= g_PlayerDamageTxScale;
    4400			headshotdamagescale = 25;
    4401		} else if (aprop && aprop->type == PROPTYPE_CHR && vprop->type == PROPTYPE_PLAYER) {
    4402			// Chr is attacking player
    ⋮
    
    src/game/chraction.c lines 4390–4404 at 46d3393 · upstream ↗
    27f32 pdmode_get_enemy_damage(void)
    ⋮
    29	if (g_MissionConfig.pdmode) {
    30		return g_MissionConfig.pdmodedamagef;
    ⋮
    33	return 1;
    
    src/game/pdmode.c lines 27–33 at 46d3393 · upstream ↗
    798    player = target->player;
    799    if (player && gameskill == sk_baby)
    800	damage >>= 1; 	// take half damage in trainer mode
    
    linuxdoom-1.10/p_inter.c lines 798–800 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

  10. ^ 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