data/claims/perfect-dark.yamlPerfect Dark (ryandwyer/perfect-dark decompilation, ntsc-final)
Game: Perfect Dark.
The 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. perfect-dark/source-version
Value: ntsc-final
Codeperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) ·README.md#L5The 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.perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · README.md#L34-L35| ntsc-1.0 | NTSC 8.7 final (the initial, buggy release) | 7f4171b0c8d17815be37913f535e4e93 |
| ntsc-final | NTSC 8.7 final | e03b088b6ac9e0080440efed07c1e40f |perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · Makefile#L17ROMID ?= ntsc-finalperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · Makefile#L87-L88ifeq ($(ROMID), ntsc-final)
VERSION = 2Checked by claude, 2026-10-22 — read the README's version table and the Makefile's default ROMID and VERSIONEvery other claim here reads the code the ntsc-final build compiles. The #if VERSION branches for the PAL and Japanese ROMs differ in places these claims name (PAL's 50 Hz constants, the Japanese autoaim scales).
Cited by: Perfect Dark
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. perfect-dark/autoaim
About: Perfect Dark Autoaim
Value: 1.5 target widths
Codeperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) ·src/game/prop.c#L60-L64/**
* Populate g_Vars.onscreenprops. This is an array of prop pointers, filtered by
* props that are on screen and sorted by distance descending (furthest first).
*/
void props_sort(void)perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/prop.c#L94-L98for (j = i; j < count; j++) {
if (depths[j] > depth) {
depth = depths[j];
swapindex = j;
}perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/prop.c#L2699-L2725struct prop **ptr = g_Vars.endonscreenprops - 1;
…
while (ptr >= g_Vars.onscreenprops) {
…
if (!chr_compare_teams(g_Vars.currentplayer->prop->chr, chr, COMPARE_FRIENDS)
&& (chr_get_held_prop(chr, HAND_RIGHT)
|| chr_get_held_prop(chr, HAND_LEFT)
|| (chr->chrflags & CHRCFLAG_FORCEAUTOAIM)
|| chr->gunprop)
&& chr_calculate_autoaim(prop, &screenpos, xrange, yrange)) {
f32 score = prop_calculate_autoaim_score(prop, &screenpos, xrange, yrange, thisaimpos, false, cangangsta, 0);
…
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chr.c#L5014-L5025bool chr_calculate_autoaim(struct prop *prop, struct coord *screenpos, f32 *xrange, f32 *yrange)
…
&& chr->actiontype != ACT_DIE
…
&& chr->actiontype != ACT_DEADperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/prop.c#L2412-L2417} else {
top = cam_get_screen_top() + cam_get_screen_height() * 0.175f;
bottom = cam_get_screen_top() + cam_get_screen_height() * 0.825f;
left = cam_get_screen_left() + cam_get_screen_width() * 0.25f;
right = cam_get_screen_left() + cam_get_screen_width() * 0.75f;
}perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/prop.c#L2441-L2458if (sp74[1] >= top && bottom >= sp7c[1]) {
…
sp48 = (sp84[0] - sp8c[0]) * 1.5f;
…
if (!g_Vars.normmplayerisrunning) {
sp48 = sp48 * g_AutoAimScale;
}
…
sp4c = cam_get_screen_left() + 0.5f * cam_get_screen_width() >= (sp8c[0] + sp84[0]) * 0.5f - sp48
&& cam_get_screen_left() + 0.5f * cam_get_screen_width() <= (sp8c[0] + sp84[0]) * 0.5f + sp48
&& left <= spa0[0]
&& right >= spa0[0];perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/prop.c#L2474-L2508ok = cd_test_los_oobok(&playerprop->pos, playerprop->rooms, &prop->pos,
CDTYPE_OBJS | CDTYPE_DOORS | CDTYPE_PATHBLOCKER | CDTYPE_BG,
…
if (ok) {
…
if (cam_get_screen_left() + 0.5f * cam_get_screen_width() >= sp8c[0]
&& cam_get_screen_left() + 0.5f * cam_get_screen_width() <= sp84[0]) {
result = 1;
} else if (cam_get_screen_left() + 0.5f * cam_get_screen_width() >= sp8c[0]) {
result = 1 - ((cam_get_screen_left() + 0.5f * cam_get_screen_width()) - sp84[0]) / sp48;
} else {
result = 1 - (sp8c[0] - (cam_get_screen_left() + 0.5f * cam_get_screen_width())) / sp48;DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_pspr.c#L601-L619void P_BulletSlope (mobj_t* mo)
…
an = mo->angle;
bulletslope = P_AimLineAttack (mo, an, 16*64*FRACUNIT);
…
an += 1<<26;
…
an -= 2<<26;Duke Nukem 3D: 3D Realms' source release (version 1.5, 2003) · source/DUKE3D.H#L62#define AUTO_AIM_ANGLE 48Duke Nukem 3D: 3D Realms' source release (version 1.5, 2003) · source/PLAYER.C#L273-L291if( (dy1*xv) <= (dx1*yv) )
if( ( dy2*xv ) >= (dx2*yv) )
{
sdist = mulscale(dx3,xv,14) + mulscale(dy3,yv,14);
if( sdist > 512 && sdist < smax )
…
if( a && cans )
{
smax = sdist;
j = i;Checked by claude, 2026-10-22 — read props_sort's selection sort, autoaim_tick's standard branch, chr_calculate_autoaim's eligibility test, and prop_calculate_autoaim_score through its result; Doom's P_BulletSlope and Duke's AUTO_AIM_ANGLE and aim() (its cone test, keeping the smallest sdist), as read for doom/autoaim and duke3d/autoaimsp8c and sp84 are the target's left and right edges on screen, spa0 its center, and sp48 the margin; the names are the decompilers' placeholders. The score is 1 within the target's width and falls linearly by the gap over the margin outside it. The window is the whole screen for a weapon bgun_allows_fullscreen_autoaim allows. A melee function skips the standard branch, and the CMP150's secondary function (target lock) has its own. The multiplayer margin is unscaled. Duke's cone is an angle around the facing; Perfect Dark's window is a share of the screen and scales with the target's size on it.
Cited by: Perfect Dark
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. perfect-dark/autoaim-scale
About: Perfect Dark Autoaim
Value
{
"agent": 1.5,
"special-agent": 0.75,
"perfect-agent": 0.2,
"perfect-dark": 1
}src/game/lv.c#L1840-L1864} else {
if (g_Difficulty == DIFF_A) {
…
g_AutoAimScale = 1.5f;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L1867-L1881} else if (g_Difficulty == DIFF_SA) {
…
#if VERSION >= VERSION_JPN_FINAL
g_AutoAimScale = 0.75f;
#else
g_AutoAimScale = g_Jpn ? 1.1f : 0.75f;
#endifperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L1884-L1898} else if (g_Difficulty == DIFF_PA) {
…
g_AutoAimScale = g_Jpn ? 0.75f : 0.2f;
#endifperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L1901-L1911} else if (g_Difficulty == DIFF_PD) {
…
g_AutoAimScale = 1;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lang.c#L53-L54#if VERSION >= VERSION_PAL_BETA
bool g_Jpn = VERSION == VERSION_JPN_FINAL ? true : false;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lang.c#L66bool g_Jpn = false;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/prop.c#L2451-L2453if (!g_Vars.normmplayerisrunning) {
sp48 = sp48 * g_AutoAimScale;
}Checked by claude, 2026-10-22 — read lv_update_solo_handicaps' solo branch (the last else) for each difficulty, g_Jpn's two definitions (VERSION 2 takes the second), and the margin's scaling in prop_calculate_autoaim_scoreThe function runs every tick of a level (lv.c L2297). Perfect Dark is the custom difficulty unlocked by finishing the game, and its autoaim margin is five times Perfect Agent's. In the Japanese build Special Agent and Perfect Agent take 1.1 and 0.75 when g_Jpn is set.
Cited by: Autoaim, Difficulty level, Perfect Dark
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. perfect-dark/autoaim-pull
About: Perfect Dark Autoaim
Value: 0.963 kept per 1/240 s
Codeperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) ·src/game/prop.c#L2736-L2743if (bestprop) {
if (bmove_is_autoaim_y_enabled_for_current_weapon() || iscmpsec) {
bmove_update_autoaim_y_prop(bestprop, (aimpos[1] - cam_get_screen_top()) / (cam_get_screen_height() * 0.5f) - 1);
}
…
bmove_update_autoaim_x_prop(bestprop, (aimpos[0] - cam_get_screen_left()) / (cam_get_screen_width() * 0.5f) - 1);perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bondmove.c#L97-L112void bmove_update_autoaim_y_prop(struct prop *prop, f32 autoaimy)
{
if (g_Vars.currentplayer->autoyaimtime60 >= 0) {
g_Vars.currentplayer->autoyaimtime60 -= g_Vars.lvupdate60;
}
…
if (prop != g_Vars.currentplayer->autoyaimprop) {
if (g_Vars.currentplayer->autoyaimtime60 < 0) {
g_Vars.currentplayer->autoyaimtime60 = TICKS(30);
g_Vars.currentplayer->autoyaimprop = prop;
} else {
return;
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/include/constants.h#L118-L119#else
#define TICKS(val) (val)perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bondmove.c#L1763-L1791// Auto aim - move crosshair towards target
…
if (g_Vars.currentplayer->autoaimdamp > (PAL ? 0.955f : 0.963f)) {
g_Vars.currentplayer->autoaimdamp -= (PAL ? 0.00037999986670911f : 0.00031999943894334f) * g_Vars.lvupdate60freal;
}
…
x = g_Vars.currentplayer->autoaimx;
y = g_Vars.currentplayer->autoaimy;
…
bgun_swivel_with_damp(x, y, g_Vars.currentplayer->autoaimdamp);
…
// This code moves the crosshair as the player turns and makes
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bondgun.c#L4964-L4976for (l = 0; l < g_Vars.lvupdate240; l++) {
player->crosspossum[0] = player->crosspossum[0] * crossdamp + screenx;
player->crosspossum[1] = player->crosspossum[1] * crossdamp + screeny;
…
player->crosspos[0] = player->crosspossum[0] * (1.0f - crossdamp) * screenwidth * 0.5f + screenwidth * 0.5f;
player->crosspos[1] = player->crosspossum[1] * (1.0f - crossdamp) * screenheight * 0.5f + screenheight * 0.5f;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/include/types.h#L112-L113// names ending in 60 mean their units are 60ths of a second
// names ending in 240 mean their units are 240ths of a secondperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bondgun.c#L5123-L5139crosspos[0] = player->crosspos[0] + randfactor * scaledspread * cam_get_screen_width()
…
crosspos[1] = player->crosspos[1] + (randfactor * scaledspread * cam_get_screen_height())
…
cam0f0b4c3c(crosspos, gundir2d, 1);DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_pspr.c#L633-L639damage = 5*(P_Random ()%3+1);
angle = mo->angle;
…
P_LineAttack (mo, angle, MISSILERANGE, bulletslope, damage);Duke Nukem 3D: 3D Realms' source release (version 1.5, 2003) · source/PLAYER.C#L489-L508j = aim( s, AUTO_AIM_ANGLE );
if(j >= 0)
{
…
sa = getangle(sprite[j].x-sx,sprite[j].y-sy);Checked by claude, 2026-10-22 — read autoaim_tick's tail, both update_autoaim_prop functions, the swivel branch of the movement tick, bgun_swivel's averaging loop, and bgun_calculate_player_shot_spread; crosspos settles at the target because crosspossum tends to target/(1 − crossdamp); (0.979 − 0.963)/0.00032 = 50; 0.963⁴ = 0.860The swivel branch also needs movedata.canautoaim, both props set, and the weapon's INVAIMFLAG_AUTOAIM aim flag (bondmove.c L1754-L1760). PAL uses 0.955 and 0.974 with a larger step. The x and y holds run on separate timers. Holding the aim button switches to manual aiming, which moves the crosshair with the stick instead. The CMP150's target lock swivels with a fixed 0.915.
Cited by: Autoaim, Perfect Dark
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. perfect-dark/hit-location
About: Perfect Dark Locational damage
Value
{
"head": 4,
"head-player-shot": 100,
"torso": 2,
"limbs": 1,
"gun": 0
}src/game/chraction.c#L4262-L4273void chr_damage(struct chrdata *chr, f32 damage, struct coord *vector, struct gset *gset,
struct prop *aprop, s32 hitpart, bool damageshield, struct prop *prop2,
…
f32 headshotdamagescale = 1;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4368-L4370if (ismelee) {
isshoot = false;
}perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4390-L4400// Apply damage scaling based on difficulty settings
if (g_Vars.mplayerisrunning == false) {
// Solo
…
} else if (aprop && aprop->type == PROPTYPE_PLAYER) {
// Player is attacking
damage *= g_PlayerDamageTxScale;
headshotdamagescale = 25;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4413-L4422} else if (g_Vars.coopplayernum >= 0) {
…
} else if (aprop && aprop->type == PROPTYPE_PLAYER && vprop->type != PROPTYPE_PLAYER) {
damage *= g_PlayerDamageTxScale;
headshotdamagescale = 25;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4707-L4751if (hitpart == HITPART_GENERAL) {
// Halve the damage because it's doubled for torso below
hitpart = HITPART_TORSO;
damage *= 0.5f;
…
// Hits to a Skedar's tail are 10x more lethal
if (race == RACE_SKEDAR && hitpart == HITPART_TAIL) {
damage *= 10;
}
…
if (hitpart == HITPART_HEAD) {
if (race == RACE_SKEDAR) {
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/include/constants.h#L1396-L1413#define HITPART_LFOOT 1
…
#define HITPART_PELVIS 7
#define HITPART_HEAD 8
…
#define HITPART_TORSO 15
#define HITPART_TAIL 16
#define HITPART_GUN 100
#define HITPART_HAT 110perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L10361-L10385if (hitplayer) {
f32 damage = gset_get_damage(&gset);
…
chr_damage_by_impact(targetchr, damage, &vector, &gset, chr->prop, HITPART_GENERAL, targetprop, node, model, side, NULL);DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_map.c#L1004-L1011if (in->d.thing->flags & MF_NOBLOOD)
P_SpawnPuff (x,y,z);
else
P_SpawnBlood (x,y,z, la_damage);
…
if (la_damage)
P_DamageMobj (th, shootthing, shootthing, la_damage);Checked by claude, 2026-10-22 — read chr_damage from its head through the hit-part block, the difficulty block's three branches that set headshotdamagescale (solo, co-op, Counter-Operative), the HITPART constants, chr_shoot's call when a guard hits the player, and Doom's PTR_ShootTraverseThe player's own shots get their hitpart from the model part the trace struck (model_test_for_hit in chr.c, then chr_hit). Counter-Operative applies the 25 to shots by the Bond player. A thrown combat knife's head hit doubles again. The pelvis counts as body in the shot statistics but takes no multiplier. Melee hits (isshoot false) take the 4 but not the 25.
Cited by: Perfect Dark
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. perfect-dark/falcon2-damage
About: Perfect Dark Damage formula
Value: 1 damage
Codeperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) ·src/game/invitems.c#L485-L495struct funcdef_shootsingle invfunc_falcon2_singleshot = {
INVENTORYFUNCTYPE_SHOOT_SINGLE,
…
1, // damageperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/include/types.h#L2920-L2924struct funcdef_shoot {
struct funcdef base;
/*0x14*/ struct recoilsettings *recoilsettings;
/*0x18*/ s8 recoverytime60;
/*0x1c*/ f32 damage;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/gset.c#L475-L483f32 gset_get_damage(struct gset *gset)
…
if ((func->type & 0xff) == INVENTORYFUNCTYPE_SHOOT) {
struct funcdef_shoot *shootfunc = (struct funcdef_shoot *)func;
damage = shootfunc->damage;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/gset.c#L505-L507if (bgun_is_firing(HAND_LEFT) && bgun_is_firing(HAND_RIGHT)) {
damage += damage;
}perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chr.c#L1085void chr_allocate(struct prop *prop, u8 *ailist)perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chr.c#L1127chr->maxdamage = 4;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraicommands.c#L3147-L3150bool ai_set_max_damage(void)
{
u8 *cmd = g_Vars.ailist + g_Vars.aioffset;
f32 maxdamage = (cmd[4] | (cmd[3] << 8)) * 0.1f;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4965chr->damage += damage;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L3571-L3572if (chr->damage >= chr->maxdamage) {
chr_begin_death(chr, vector, angle, hitpart, gset, false, aplayernum);perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L5077-L5078} else {
chr_react_to_damage(chr, vector, angle, hitpart, gset, aplayernum);perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/playermgr.c#L257g_Vars.players[index]->bondhealth = 1;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4795-L4812f32 statsamount = amount = damage * 0.125f;
…
g_Vars.currentplayer->bondhealth -= amount / healthscale;DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_pspr.c#L633damage = 5*(P_Random ()%3+1);DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/info.c#L1134-L1137{ // MT_POSSESSED
3004, // doomednum
S_POSS_STND, // spawnstate
20, // spawnhealthChecked by claude, 2026-10-22 — read the Falcon 2 funcdef against struct funcdef_shoot's field order, gset_get_damage, chr_allocate's default, the AI command that sets maxdamage, the NPC and player damage lines in chr_damage, chr_react_to_damage's death test (called from chr_damage, L5078, or after the pre-argh animation, L8701), and the player's starting health; a limb keeps 1, the torso doubles to 2, the head takes 4 × 25 (perfect-dark/hit-location); a guard's hit is general (×1) and Perfect Agent's received scale is 1 (perfect-dark/difficulty-damage), so 1 × 0.125The counts leave out armour (a negative starting damage) and shields. gset_get_damage doubles the damage while the current player is firing both hands; it reads the player's hands even when a guard's shot asks, so in the code a guard's bullet is doubled too while the player fires two guns at once. Not checked in play. The player's healthscale and armour are 1 unless a level changes them.
Cited by: Perfect Dark
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. perfect-dark/difficulty-damage
About: Perfect Dark Difficulty level
Value
{
"agent": 0.5,
"special-agent": 0.6,
"perfect-agent": 1
}src/game/lv.c#L1840-L1866} else {
if (g_Difficulty == DIFF_A) {
f32 totalhealth;
f32 frac = 1;
…
totalhealth = player_get_health_frac() + player_get_shield_frac();
…
if (totalhealth <= 0.125f) {
frac = 0.5f;
} else if (totalhealth <= 0.6f) {
frac = (totalhealth - 0.125f) * 0.5f / 0.47500002384186f + 0.5f;
}
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L1867-L1876} else if (g_Difficulty == DIFF_SA) {
…
g_PlayerDamageRxScale = 0.6f;
g_PlayerDamageTxScale = 1;
g_ExplosionDamageTxScale = 0.75f;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L1884-L1893} else if (g_Difficulty == DIFF_PA) {
…
g_PlayerDamageRxScale = 1;
g_PlayerDamageTxScale = 1;
g_ExplosionDamageTxScale = 1;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L1901-L1910} else if (g_Difficulty == DIFF_PD) {
…
g_PlayerDamageRxScale = 1;
g_PlayerDamageTxScale = 1;
g_ExplosionDamageTxScale = 1;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/lv.c#L2297lv_update_solo_handicaps();perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L4390-L4404// Apply damage scaling based on difficulty settings
if (g_Vars.mplayerisrunning == false) {
// Solo
if (explosion) {
if (vprop->type == PROPTYPE_PLAYER) {
damage *= g_ExplosionDamageTxScale;
}
} else if (aprop && aprop->type == PROPTYPE_PLAYER) {
// Player is attacking
damage *= g_PlayerDamageTxScale;
headshotdamagescale = 25;
} else if (aprop && aprop->type == PROPTYPE_CHR && vprop->type == PROPTYPE_PLAYER) {
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/pdmode.c#L27-L33f32 pdmode_get_enemy_damage(void)
{
if (g_MissionConfig.pdmode) {
return g_MissionConfig.pdmodedamagef;
}
…
return 1;DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_inter.c#L798-L800player = target->player;
if (player && gameskill == sk_baby)
damage >>= 1; // take half damage in trainer modeChecked by claude, 2026-10-22 — read lv_update_solo_handicaps' solo branch for each difficulty, its call in the level tick, chr_damage's solo scaling, pdmode_get_enemy_damage, and Doom's P_DamageMobj; at health plus shield 0.125 or less frac is 0.5, so 0.5 × 0.5 = 0.25lv_update_solo_handicaps also sets enemy accuracy (0.6, 0.8, 1.175, 1.1), ammo pickups (2, 1.5, 1, 1), and autogun and CCTV scales per difficulty; co-op and Counter-Operative read their own rows. The Perfect Dark difficulty's enemy health divides the player's damage to guards (pdmode_get_enemy_health). Health and shield are fractions of 1.
Cited by: Difficulty level, Perfect Dark
A 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). perfect-dark/simulant-shoot-delay
About: Perfect Dark Reaction delay
Value
{
"meat": 90,
"easy": 60,
"normal": 30,
"hard": 15,
"perfect": 0,
"dark": 0
}src/game/bot.c#L87-L95struct botdifficulty g_BotDifficulties[] = {
// shootdelay60 turnunzeromult forcezerominspeed
…
/* meat */ { TICKS(90), BADDTOR2(15), BADDTOR2(30), TICKS(600), 10, BADDTOR2(40), BADDTOR2(20), TICKS(1000) },
/* easy */ { TICKS(60), BADDTOR2(7), BADDTOR2(14), TICKS(360), 10, BADDTOR2(28.5f), BADDTOR2(8), TICKS(1000) },
/* norm */ { TICKS(30), BADDTOR2(4), BADDTOR2(8), TICKS(180), 4, BADDTOR2(20), BADDTOR2(5), TICKS(1500) },
/* hard */ { TICKS(15), BADDTOR2(1.5f), BADDTOR2(4), TICKS(90), 2, BADDTOR2(14), BADDTOR2(2), TICKS(2500) },
/* perf */ { TICKS(0), BADDTOR2(0), BADDTOR2(2), TICKS(45), 1, BADDTOR2(10), BADDTOR2(0), TICKS(4000) },
/* dark */ { TICKS(0), BADDTOR2(0), BADDTOR2(0), TICKS(0), 0, BADDTOR2(8), BADDTOR2(0), TICKS(4000) },perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bot.c#L45-L46struct botdifficulty {
u8 shootdelay60;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/include/constants.h#L118-L119#else
#define TICKS(val) (val)perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bot.c#L1365-L1388if (botchr->target != propnum) {
botchr->target = propnum;
botchr->aibot->shootdelaytimer60 = 0;
…
} else {
if (botchr->aibot->targetinsight) {
if (g_Vars.lvupdate240 > 0) {
botchr->aibot->shootdelaytimer60 += g_Vars.diffframe60;
}
} else {
if (g_Vars.lvupdate240 > 0) {
botchr->aibot->shootdelaytimer60 -= g_Vars.diffframe60;
…perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/include/types.h#L116/*0x000*/ s32 diffframe60; // time between current frame start and previous frame start in 60ths of a secondperfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/bot.c#L3600-L3608if (canshoot) {
if (aibot->cyclonedischarging[i] || aibot->burstsdone[i] > 0) {
firing = true;
} else if (chr->target != -1
&& aibot->targetinsight
&& aibot->shootdelaytimer60 >= g_BotDifficulties[aibot->config->difficulty].shootdelay60
&& (bot_is_dizzy(chr) || chr_is_target_in_fov(chr, 45, false))
&& !chr_is_dead(chr_get_target_prop(chr)->chr)) {
firing = true;perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L13931#define D256TOR(degrees256) ((degrees256) * (360.0f * M_BADPI / 180.0f / 256.0f))perfect-dark: a decompilation of Perfect Dark (Ryan Dwyer) · src/game/chraction.c#L13979-L13984bool chr_is_target_in_fov(struct chrdata *chr, u8 degrees256, u8 reverse)
{
f32 angle = chr_get_angle_to_target2(chr, reverse);
…
if ((angle < D256TOR(degrees256) && angle < DTOR(180))
|| (angle > BADDTOR(360) - D256TOR(degrees256) && angle > DTOR(180))) {DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_mobj.c#L503-L504if (gameskill != sk_nightmare)
mobj->reactiontime = info->reactiontime;DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/info.c#L1134-L1140{ // MT_POSSESSED
…
8, // reactiontimeDOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_enemy.c#L672-L677void A_Chase (mobj_t* actor)
…
if (actor->reactiontime)
actor->reactiontime--;DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10) · linuxdoom-1.10/p_enemy.c#L204-L213if ( actor->flags & MF_JUSTHIT )
{
// the target just hit the enemy,
// so fight back!
actor->flags &= ~MF_JUSTHIT;
return true;
}
…
if (actor->reactiontime)
return false; // do not attack yetChecked by claude, 2026-10-22 — read the botdifficulty struct and table, the timer's update in the target-picking code, the gun-firing test in the bot tick, chr_is_target_in_fov and D256TOR; Doom's P_SpawnMobj, the zombieman's info entry, A_Chase's countdown, and P_CheckMissileRange; 45/256 × 360° = 63.3°Thrown weapons and punches test the same timer (bot.c L3432, L3542). The table's other columns set how fast and how long a simulant takes to zero in its aim (minzerospeed, maxzerospeed, zerotime60), which this claim doesn't cover. PAL's TICKS scales each delay by 50/60. Doom's countdown runs per chase step, not per tic, and its melee check doesn't read reactiontime.
Cited by: Perfect Dark, Reaction delay