Perk implementation matrix (rewrite)¶
This page tracks where perks are wired in the Python rewrite under src/,
and which perks have direct scenario tests.
Generated by uv run python scripts/gen_perk_matrix.py.
Notes:
- Port refs excludes perk metadata (perks.py), quests, modes, and UI views.
- Original hook is not filled yet; use the decompile + runtime evidence to promote it.
| ID | PerkId | Name | Original hook | Port refs | Tests |
|---|---|---|---|---|---|
| 0 | ANTIPERK | AntiPerk | Never offered: perk_can_offer explicitly rejects ANTIPERK. |
src/crimson/gameplay.py:perk_can_offer |
tests/test_antiperk_perk.py |
| 1 | BLOODY_MESS_QUICK_LEARNER | Bloody Mess | CreaturePool._start_death: if the killer has Bloody Mess, uses xp_base = int(reward_value * 1.3). GameWorld._queue_projectile_decals: when active, spawns extra random decals and blood splatter particles on projectile hits. |
src/crimson/creatures/runtime.py:CreaturePool._start_deathsrc/crimson/game_world.py:GameWorld._queue_projectile_decals |
tests/test_bloody_mess_quick_learner_perk.pytests/test_radioactive_perk.py |
| 2 | SHARPSHOOTER | Sharpshooter | player_fire_weapon: if Sharpshooter is active, multiplies shot_cooldown by 1.05 and does not increase player.spread_heat by weapon.spread_heat * 1.3 after firing. player_update: while active, forces player.spread_heat = 0.02. |
src/crimson/gameplay.py:perk_generate_choicessrc/crimson/gameplay.py:player_fire_weaponsrc/crimson/gameplay.py:player_update |
tests/test_perk_selection.pytests/test_sharpshooter_perk.py |
| 3 | FASTLOADER | Fastloader | player_start_reload (0x00413430): if Fastloader is active, multiplies weapon reload_time by 0.7. |
src/crimson/gameplay.py:player_start_reload |
tests/test_fastloader_perk.py |
| 4 | LEAN_MEAN_EXP_MACHINE | Lean Mean Exp Machine | perks_update_effects: every 0.25 seconds, each player with this perk gains +perk_count * 10 experience. |
src/crimson/gameplay.py:perks_update_effects |
tests/test_lean_mean_exp_machine.py |
| 5 | LONG_DISTANCE_RUNNER | Long Distance Runner | player_update: while moving, move_speed normally increases by frame_dt * 5.0 (clamped to 2.0). With Long Distance Runner active, move_speed continues to ramp up above 2.0 by frame_dt per frame, clamped to 2.8. |
src/crimson/gameplay.py:perk_generate_choicessrc/crimson/gameplay.py:player_update |
tests/test_long_distance_runner_perk.pytests/test_perk_selection.py |
| 6 | PYROKINETIC | Pyrokinetic | perks_update_effects: picks creature_find_in_radius(&aim, 12.0, 0); while aiming at a creature, decrements collision_timer by dt and, when it wraps, sets it to 0.5 and spawns 5 particles (intensity 0.8, 0.6, 0.4, 0.3, 0.2), plus fx_queue_add_random at the creature position. |
src/crimson/gameplay.py:perks_update_effects |
tests/test_pyrokinetic_perk.py |
| 7 | INSTANT_WINNER | Instant Winner | perk_apply (0x004055e0): grants +2500 experience to the perk owner. |
src/crimson/gameplay.py:perk_apply |
tests/test_instant_winner_perk.pytests/test_perk_selection.py |
| 8 | GRIM_DEAL | Grim Deal | perk_apply (0x004055e0): sets player.health = -1.0 and grants +int(experience * 0.18). |
src/crimson/gameplay.py:perk_apply |
tests/test_grim_deal_perk.py |
| 9 | ALTERNATE_WEAPON | Alternate Weapon | player_apply_move_with_spawn_avoidance (0x0041e290): scales movement delta by 0.8. player_update: pressing reload swaps the primary and alternate weapon runtime blocks, plays the new weapon's reload SFX, and adds +0.1 to shot_cooldown. |
src/crimson/gameplay.py:bonus_applysrc/crimson/gameplay.py:player_update |
tests/test_alternate_weapon_perk.pytests/test_game_mode_ids.py |
| 10 | PLAGUEBEARER | Plaguebearer | Sets player_plaguebearer_active (DAT_004908b9). In creature_update_all, infected creatures (collision_flag != 0) take 15 damage every 0.5 seconds via collision_timer; on an infection kill, increments plaguebearer_infection_count. While plaguebearer_infection_count < 60, plaguebearer_spread_infection spreads infection between creatures within 45 units when the target has <150 HP. While plaguebearer_infection_count < 50, the player infects nearby creatures (<30 units) with <150 HP. |
src/crimson/creatures/runtime.py:CreaturePool.updatesrc/crimson/gameplay.py:perk_apply |
tests/test_plaguebearer_perk.py |
| 11 | EVIL_EYES | Evil Eyes | perks_update_effects sets evil_eyes_target_creature (DAT_00490bbc) to creature_find_in_radius(&aim, 12.0, 0) when the perk is active (else -1). creature_update_all checks this index and skips the target's AI update. |
src/crimson/creatures/runtime.py:CreaturePool.updatesrc/crimson/gameplay.py:perk_generate_choicessrc/crimson/gameplay.py:perks_update_effects |
tests/test_evil_eyes_perk.pytests/test_perk_selection.py |
| 12 | AMMO_MANIAC | Ammo Maniac | perk_apply (0x004055e0): on pick, calls weapon_assign_player(player_index, weapon_id) for each player using their current weapon id. weapon_assign_player (0x00452d40): when Ammo Maniac is active, increases clip_size by max(1, int(clip_size * 0.25)) before refilling ammo. |
src/crimson/gameplay.py:perk_applysrc/crimson/gameplay.py:weapon_assign_player |
tests/test_ammo_maniac_perk.py |
| 13 | RADIOACTIVE | Radioactive | creature_update_all: if a creature is within 100 units and Radioactive is active, decrement collision_timer by dt * 1.5; when it wraps, set it to 0.5 and deal (100 - dist) * 0.3 damage. Kills bypass creature_handle_death: experience = int(float(experience) + creature.reward_value); start death via hitbox_size -= dt. |
src/crimson/creatures/runtime.py:CreaturePool.updatesrc/crimson/gameplay.py:perk_generate_choicessrc/crimson/render/world_renderer.py:WorldRenderer._draw_player_trooper_sprite |
tests/test_perk_selection.pytests/test_radioactive_perk.py |
| 14 | FASTSHOT | Fastshot | player_fire_weapon: while active, multiplies shot_cooldown by 0.88. |
src/crimson/gameplay.py:perk_generate_choicessrc/crimson/gameplay.py:player_fire_weapon |
tests/test_fastshot_perk.pytests/test_perk_selection.py |
| 15 | FATAL_LOTTERY | Fatal Lottery | perk_apply (0x004055e0): rolls crt_rand() & 1. If the result is 0, grants +10000 experience; otherwise sets player.health = -1.0. |
src/crimson/gameplay.py:perk_apply |
tests/test_fatal_lottery_perk.py |
| 16 | RANDOM_WEAPON | Random Weapon | perk_apply (0x004055e0): picks a random available weapon (retries up to 100), skipping the pistol and the currently equipped weapon, then calls weapon_assign_player. |
src/crimson/gameplay.py:perk_apply |
tests/test_random_weapon_perk.py |
| 17 | MR_MELEE | Mr. Melee | creature_update_all (0x00426220): on a melee hit, if Mr. Melee is active, deals creature_apply_damage(creature, 25.0, damage_type=2, impulse=(0,0)) to the attacker. |
src/crimson/creatures/runtime.py:CreaturePool.update |
tests/test_mr_melee_perk.py |
| 18 | ANXIOUS_LOADER | Anxious Loader | player_update (0x004136b0): while reloading (reload_timer > 0), fire_pressed subtracts 0.05 from reload_timer. |
src/crimson/gameplay.py:player_update |
tests/test_anxious_loader_perk.py |
| 19 | FINAL_REVENGE | Final Revenge | player_take_damage (0x00425e50): on death (health < 0), if Final Revenge is active, spawns an explosion burst (scale 1.8), sets bonus_spawn_guard, and applies radius damage within 512 units: damage = (512 - dist) * 5.0 via creature_apply_damage(damage_type=3, impulse=(0,0)); plays sfx_explosion_large and sfx_shockwave. |
src/crimson/sim/world_state.py:WorldState.step |
tests/test_final_revenge_perk.py |
| 20 | TELEKINETIC | Telekinetic | bonus_telekinetic_update: aim at a bonus within 24 units for >650ms to pick it up remotely. |
src/crimson/gameplay.py:bonus_telekinetic_update |
tests/test_telekinetic_perk.py |
| 21 | PERK_EXPERT | Perk Expert | perk_choice_count: while active, offers 6 perk choices per selection (vs 5 baseline). |
src/crimson/gameplay.py:perk_choice_count |
tests/test_perk_expert_perk.pytests/test_perk_master_perk.py |
| 22 | UNSTOPPABLE | Unstoppable | player_take_damage (0x00425e50): while active, suppresses the on-hit heading jitter + spread heat increase. |
src/crimson/player_damage.py:player_take_damage |
tests/test_highlander_perk.pytests/test_unstoppable_perk.py |
| 23 | REGRESSION_BULLETS | Regression Bullets | player_update: while reloading with an empty clip, firing a shot costs experience int(experience - weapon.reload_time * factor) where factor=200 for most weapons and factor=4 when weapon_ammo_class == 1 (fire). |
src/crimson/gameplay.py:player_fire_weaponsrc/crimson/gameplay.py:player_start_reload |
tests/test_regression_bullets_perk.py |
| 24 | INFERNAL_CONTRACT | Infernal Contract | perk_apply: grants the perk owner +3 levels (and +3 pending perk picks), and sets all alive players to health = 0.1. |
src/crimson/gameplay.py:perk_apply |
tests/test_infernal_contract_perk.pytests/test_perk_selection.py |
| 25 | POISON_BULLETS | Poison Bullets | projectile_update: on creature hit, if Poison Bullets is active and (crt_rand() & 7) == 1, sets creature.flags \|= 0x01 (self-damage tick). creature_update_all applies this via creature_apply_damage(creature, frame_dt * 60.0, damage_type=0, impulse=(0,0)). |
src/crimson/projectiles.py:ProjectilePool.update |
tests/test_poison_bullets_perk.py |
| 26 | DODGER | Dodger | player_take_damage (0x00425e50): if Dodger is active and Ninja is not, ⅕ chance to ignore the hit (crt_rand() % 5 == 0). |
src/crimson/player_damage.py:player_take_damage |
tests/test_player_damage.py |
| 27 | BONUS_MAGNET | Bonus Magnet | BonusPool.try_spawn_on_kill: if the base roll fails (crt_rand() % 9 != 1) but any player has Bonus Magnet, a second roll (crt_rand() % 10 == 2) can still spawn a bonus. |
src/crimson/gameplay.py:BonusPool.try_spawn_on_kill |
tests/test_bonus_magnet_perk.py |
| 28 | URANIUM_FILLED_BULLETS | Uranium Filled Bullets | creature_apply_damage (0x004207c0): when damage_type == 1 and Uranium Filled Bullets is active, doubles the applied damage (damage = damage + damage). |
src/crimson/creatures/damage.py:creature_apply_damage |
tests/test_uranium_filled_bullets_perk.py |
| 29 | DOCTOR | Doctor | creature_apply_damage (0x004207c0): when damage_type == 1 and Doctor is active, multiplies damage by 1.2. |
src/crimson/creatures/damage.py:creature_apply_damage |
tests/test_doctor_perk.py |
| 30 | MONSTER_VISION | Monster Vision | creature_render_all (0x00419680): when active, draws a yellow 90x90 quad behind each active creature using effect_select_texture(0x10), with alpha fading by clamp((hitbox_size + 10) * 0.1) during corpse despawn. creature_render_type (0x00418b60): disables the creature shadow pass while Monster Vision is active. |
src/crimson/render/world_renderer.py:WorldRenderer.draw |
tests/test_monster_vision_perk.py |
| 31 | HOT_TEMPERED | Hot Tempered | player_update: periodically spawns an 8-shot ring alternating projectile types 0x0b/0x09; the interval is randomized to (crt_rand() % 8) + 2 seconds. |
src/crimson/gameplay.py:player_update |
tests/test_game_world_audio.pytests/test_player_update.py+1 more |
| 32 | BONUS_ECONOMIST | Bonus Economist | bonus_apply: while active, scales bonus timer increments by 1.0 + 0.5 * perk_count. |
src/crimson/gameplay.py:bonus_apply |
tests/test_bonus_economist_perk.py |
| 33 | THICK_SKINNED | Thick Skinned | perk_apply: on pick, scales player.health *= 2/3 (clamped to >=1). player_take_damage (0x00425e50): scales incoming damage by 2/3. |
src/crimson/gameplay.py:perk_applysrc/crimson/player_damage.py:player_take_damage |
tests/test_perk_selection.py |
| 34 | BARREL_GREASER | Barrel Greaser | creature_apply_damage (0x004207c0): when damage_type == 1 and Barrel Greaser is active, multiplies damage by 1.4. projectile_update (0x00420b90): when Barrel Greaser is active and projectile.owner_id < 0, doubles the per-frame movement step count (steps *= 2). |
src/crimson/creatures/damage.py:creature_apply_damagesrc/crimson/projectiles.py:ProjectilePool.update |
tests/test_barrel_greaser_perk.py |
| 35 | AMMUNITION_WITHIN | Ammunition Within | player_update (0x004136b0): while reloading with an empty clip, if Ammunition Within is active and experience > 0, firing a shot costs health via player_take_damage (cost = 1.0 normally, 0.15 when weapon_ammo_class == 1). Regression Bullets takes precedence when both are active. |
src/crimson/gameplay.py:player_fire_weaponsrc/crimson/gameplay.py:player_start_reload |
tests/test_ammunition_within_perk.py |
| 36 | VEINS_OF_POISON | Veins of Poison | creature_update_all: on a melee hit (when player.shield_timer <= 0), if Veins of Poison is active and Toxic Avenger is not, sets creature.flags \|= 0x01 (self-damage tick, frame_dt * 60). |
src/crimson/creatures/runtime.py:CreaturePool.update |
tests/test_veins_of_poison_perk.py |
| 37 | TOXIC_AVENGER | Toxic Avenger | creature_update_all: on a melee hit (when player.shield_timer <= 0), if Toxic Avenger is active sets creature.flags \|= 0x03, enabling the strong self-damage tick (frame_dt * 180). |
src/crimson/creatures/runtime.py:CreaturePool.update |
tests/test_toxic_avenger_perk.py |
| 38 | REGENERATION | Regeneration | perks_update_effects (0x00406b40): if Regeneration is active and (crt_rand() & 1) != 0, heals each alive player with 0 < health < 100 by +dt (clamped to 100). |
src/crimson/gameplay.py:perk_applysrc/crimson/gameplay.py:perks_update_effects |
tests/test_death_clock_perk.pytests/test_regeneration_perk.py |
| 39 | PYROMANIAC | Pyromaniac | creature_apply_damage (0x004207c0): when damage_type == 4 and Pyromaniac is active, multiplies damage by 1.5 and consumes one crt_rand(). |
src/crimson/creatures/damage.py:creature_apply_damage |
tests/test_pyromaniac_damage_perk.py |
| 40 | NINJA | Ninja | player_take_damage (0x00425e50): ⅓ chance to ignore the hit (crt_rand() % 3 == 0) (takes precedence over Dodger). |
src/crimson/player_damage.py:player_take_damage |
tests/test_player_damage.py |
| 41 | HIGHLANDER | Highlander | player_take_damage (0x00425e50): when active, does not subtract damage; instead crt_rand() % 10 == 0 sets health = 0.0 (instant death). |
src/crimson/player_damage.py:player_take_damage |
tests/test_highlander_perk.py |
| 42 | JINXED | Jinxed | Global timer data_4aaf1c in perks_update_effects: every ~2.0–3.9s, 1/10 chance to deal 5 self-damage and call fx_queue_add_random twice at the player position. If Freeze bonus is inactive, kills a random active creature (index = rand() % 0x17f, 10 retries) by setting health=-1 and decrementing hitbox_size -= dt*20, then awards experience = int(float(experience) + creature.reward_value) and plays sfx_trooper_inpain_01. |
src/crimson/gameplay.py:perks_update_effects |
tests/test_jinxed_perk.py |
| 43 | PERK_MASTER | Perk Master | perk_choice_count: while active, offers 7 perk choices per selection (vs 5 baseline). |
src/crimson/gameplay.py:perk_choice_count |
tests/test_perk_master_perk.py |
| 44 | REFLEX_BOOSTED | Reflex Boosted | Main loop (grim_update): when in gameplay (game_state_id == 9) and Reflex Boosted is active, scales frame_dt *= 0.9. |
src/crimson/sim/world_state.py:WorldState.step |
tests/test_reflex_boosted_perk.py |
| 45 | GREATER_REGENERATION | Greater Regeneration | No runtime hook found in this build: perk_id_greater_regeneration is only referenced in perk selection and perk_apply (Death Clock clears it). No reads appear in perks_update_effects / player_update. |
src/crimson/gameplay.py:perk_apply |
tests/test_death_clock_perk.py |
| 46 | BREATHING_ROOM | Breathing Room | perk_apply (0x004055e0): scales player.health down by ⅔ for each player, then for every active creature does hitbox_size -= frame_dt (starting death staging without XP/bonus logic) and clears bonus_spawn_guard. |
src/crimson/gameplay.py:perk_apply |
tests/test_breathing_room_perk.py |
| 47 | DEATH_CLOCK | Death Clock | player_take_damage (0x00425e50): if Death Clock is active, returns immediately (immune to damage). perk_apply (0x004055e0): clears Regeneration and Greater Regeneration perk counts and sets player.health = 100.0 when health > 0.0. |
src/crimson/gameplay.py:bonus_pick_random_typesrc/crimson/gameplay.py:perk_applysrc/crimson/player_damage.py:player_take_damage |
tests/test_death_clock_perk.py |
| 48 | MY_FAVOURITE_WEAPON | My Favourite Weapon | perk_apply (0x004055e0): on pick, increases clip_size by 2 for each player. weapon_assign_player (0x00452d40): also applies +2 to clip_size on every weapon assignment while the perk is active. |
src/crimson/gameplay.py:bonus_pick_random_typesrc/crimson/gameplay.py:perk_applysrc/crimson/gameplay.py:weapon_assign_player |
tests/test_my_favourite_weapon_perk.py |
| 49 | BANDAGE | Bandage | perk_apply (0x004055e0): multiplies player.health by (crt_rand() % 50 + 1) and clamps to 100.0, then calls effect_spawn_burst(player.pos, 8). |
src/crimson/gameplay.py:perk_apply |
tests/test_bandage_perk.py |
| 50 | ANGRY_RELOADER | Angry Reloader | Spawns a ring of projectile type 0x0b when the reload timer crosses the half threshold. |
src/crimson/gameplay.py:player_update |
tests/test_game_world_audio.pytests/test_player_update.py+1 more |
| 51 | ION_GUN_MASTER | Ion Gun Master | projectile_update (0x00420b90): sets ion_aoe_scale to 1.2 when active, scaling ion AoE radii (Ion Rifle: 88, Ion Minigun: 60, Ion Cannon: 128). creature_apply_damage (0x004207c0): when damage_type == 7 and Ion Gun Master exists (global perk count), multiplies damage by 1.2. |
src/crimson/creatures/damage.py:creature_apply_damagesrc/crimson/projectiles.py:ProjectilePool.update |
tests/test_ion_gun_master_perk.py |
| 52 | STATIONARY_RELOADER | Stationary Reloader | player_update (0x004136b0): while stationary, applies reload_scale = 3.0 when decrementing reload_timer. |
src/crimson/gameplay.py:player_update |
tests/test_player_update.pytests/test_stationary_reloader_perk.py |
| 53 | MAN_BOMB | Man Bomb | Burst spawns projectile types 0x15/0x16. |
src/crimson/gameplay.py:player_update |
tests/test_game_world_audio.pytests/test_player_update.py+1 more |
| 54 | FIRE_CAUGH | Fire Caugh | Uses projectile type 0x2d (see Fire Bullets in the atlas notes). |
src/crimson/gameplay.py:player_update |
tests/test_player_update.py |
| 55 | LIVING_FORTRESS | Living Fortress | player_update (0x00412d70): while stationary and Living Fortress is active, increments player.living_fortress_timer += frame_dt (caps at 30.0; resets to 0.0 when moving). creature_apply_damage (0x004207c0): for damage_type == 1, multiplies damage by (living_fortress_timer * 0.05 + 1.0) for each alive player. |
src/crimson/creatures/damage.py:creature_apply_damagesrc/crimson/gameplay.py:player_update |
tests/test_living_fortress_perk.py |
| 56 | TOUGH_RELOADER | Tough Reloader | player_take_damage (0x00425e50): if reload_active is set, multiplies incoming damage by 0.5. |
src/crimson/player_damage.py:player_take_damage |
tests/test_tough_reloader_perk.py |
| 57 | LIFELINE_50_50 | Lifeline 50-50 | perk_apply (0x004055e0): iterates creature_pool in slot order, deactivating every other slot (bVar8 toggles each iteration) when it is active, has health <= 500.0, and (flags & 4) == 0; for each deactivated creature it calls effect_spawn_burst(creature.pos, 4). |
src/crimson/gameplay.py:perk_apply |
tests/test_lifeline_50_50_perk.py |