Perfect Dark
Rare's 2000 first-person shooter for the Nintendo 64, read through Ryan Dwyer's matching decompilation.
On this page
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
| Mechanic | Kind | How |
|---|---|---|
| Autoaim | Crosshair pull | the 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 damage | Hit location | a head hit ×4 and a torso hit ×2; in solo a player's gun shot multiplies a head hit by 25 more |
| Damage formula | Fixed table | the Falcon 2's shot carries a fixed damage of 1 with no roll, scaled by body part and difficulty |
| Difficulty level | Table of scales | each solo difficulty sets multipliers for damage taken and dealt, enemy accuracy, autoaim, and ammo, and Agent eases off further at low health |
| Reaction delay | 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 game, by concept (8)
References
- ^ abThe Perfect Dark decompilation builds six ROMs and defaults to
ntsc-final, which its README describes as "NTSC 8.7 final" besidentsc-1.0, "the initial, buggy release"; the Makefile setsVERSION = 2for it.Coderyandwyer/perfect-darkHow 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-finalMakefile 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.
- ^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 listprops_sortorders byprop->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'saimtakes the target within 8.4° of the facing that is nearest along it.Coderyandwyer/perfect-dark · id-Software/DOOM · 3D Realms, duke3dsource.ziprevisedHow 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 48source/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 solo Perfect Dark the autoaim margin (perfect-dark/autoaim) is multiplied by
g_AutoAimScale, whichlv_update_solo_handicapssets 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_Jpnis false). Co-op and Counter-Operative use their own values.Coderyandwyer/perfect-darkHow 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.
- ^Perfect Dark's autoaim moves the crosshair, not the view: once a target is picked,
bmove_update_autoaim_x_propand_y_propset 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_swivelthen eases the crosshair toward the point, keeping a fractioncrossdampof 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.zipHow 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.
- ^Doom aims vertically for the player:
P_BulletSlopetraces 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/DOOMHow 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 0x40000000linuxdoom-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.
- ^When a Duke Nukem 3D player fires the pistol, shotgun, or chaingun,
aimlooks for a target withinAUTO_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'sP_BulletSlopetraces only straight ahead and 5.625° to each side, and its bullet keeps the player's facing.Code3D Realms, duke3dsource.zip · id-Software/DOOMHow we know
62#define AUTO_AIM_ANGLE 48source/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.
- ^Perfect Dark's
chr_damagescales 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'sP_LineAttackpasses a bullet's damage to the thing it hits with no body part.Coderyandwyer/perfect-dark · id-Software/DOOMHow 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.
- ^The Falcon 2's single shot in Perfect Dark carries a damage of 1 (
invfunc_falcon2_singleshot), read bygset_get_damage. A guard adds each hit to itsdamageagainst amaxdamagethatchr_allocatesets to 4 (a level's AI script can set another), andchr_react_to_damagestarts its death oncedamagereaches 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 takesdamage × 0.125from 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/DOOMHow 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.
- ^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/DOOMrevisedHow 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.
- ^ 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
- 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
- Software, id. 2024. “DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10).” Commit a77dfb9. id-Software/DOOM @ a77dfb9 · licence, all uses
- Realms, 3D. 2003. “Duke Nukem 3D: 3D Realms' source release (version 1.5, 2003).” Commit e75bb06. commercial-game-sources/duke_nukem_3d @ e75bb06 · licence, all uses