Skip to main content
This page documents perk-related data structures: the static perk metadata table and per-player perk acquisition counts.

Perk Metadata Table

Base: 0x004d8000 (perk_table)
Entry size: 0x20 bytes (32 bytes)
Count: 128 perks (many unused)

Struct Layout

typedef struct {
    i32 name_offset;        // +0x00
    i32 icon_index;         // +0x04
    i32 rarity;             // +0x08
    i32 flags;              // +0x0c
    i32 max_count;          // +0x10
    // ... 12 more bytes
} perk_entry_t;  // 0x20 bytes

Key Fields

OffsetFieldTypeDescription
0x00name_offseti32Offset into string table
0x04icon_indexi32UI icon sprite index
0x08rarityi32Drop weight (higher = rarer)
0x0cflagsi32Behavior flags
0x10max_counti32Stack limit (0 = no limit)

Player Perk Counts

Base: player_health + 0x94 (within player struct)
Type: i32[0x80] (128 entries)
Total size: 0x200 bytes
Each entry stores how many times the player has acquired that perk. Access:
int perk_count = player_perk_counts[player_idx * 0x360 + 0x94 + perk_id * 4];
Helper function:
int perk_count_get(int perk_id) {
    return player_perk_counts[current_player][perk_id];
}

Perk ID Map

Offensive Perks

IDNameEffect
0x01Crimsonier+15% bullet damage
0x02Nerves of Steel+20% accuracy
0x03Ticker+10% fire rate
0x04Fast Reloader-50% reload time
0x05Long Distance Runner+15% movement speed
0x32Angry ReloaderPlasma burst on reload
0x34Poison BulletsBullets poison enemies
0x35Fire BulletsConverts shots to fire

Defensive Perks

IDNameEffect
0x06Armored+20% max health
0x07Tough Reloader-50% damage while reloading
0x08JinxEnemies 20% slower
0x09Second ChanceRevive once with 50% health
0x0aShieldTemporary invulnerability
0x36Living FortressArmor while stationary
0x37Man BombPeriodic damage burst

Utility Perks

IDNameEffect
0x0bHaste+15% all speeds
0x0cReflex Boost+30% dodge chance
0x0dSharp Shooter+25% headshot damage
0x0eFloaterMove through enemies
0x0fFast Learner+50% XP gain
0x38Hot TemperedDamage on collision
0x39Plague BearerInfect nearby enemies
0x3aIon Gun Master+20% ion damage

Perk Rarities

RarityWeightDrop ChanceExamples
1Common40%Crimsonier, Fast Reloader
2Uncommon30%Nerves of Steel, Armored
3Rare20%Second Chance, Jinx
4Very Rare8%Angry Reloader, Ion Gun Master
5Legendary2%Man Bomb, Plague Bearer
Drop logic:
void spawn_perk_choice() {
    int total_weight = 0;
    for (int i = 0; i < perk_count; i++) {
        total_weight += perk_table[i].rarity;
    }
    
    int roll = rand() % total_weight;
    // Select perk based on weighted roll
}

Perk Application

Perks are applied in perk_apply_effect when acquired:
void perk_apply_effect(int player_idx, int perk_id) {
    player_perk_counts[player_idx][perk_id]++;
    
    switch (perk_id) {
        case 0x01:  // Crimsonier
            player_damage_multiplier[player_idx] *= 1.15;
            break;
        
        case 0x04:  // Fast Reloader
            player_reload_multiplier[player_idx] *= 0.5;
            break;
        
        case 0x32:  // Angry Reloader (passive)
            // Checked in reload_complete()
            break;
        
        // ...
    }
}

Runtime Checks

Many perks are checked during gameplay:

Damage Calculation

float calculate_damage(int player_idx, float base_damage) {
    float damage = base_damage;
    
    // Crimsonier stacks
    int crimsonier_count = perk_count_get(0x01);
    damage *= pow(1.15, crimsonier_count);
    
    // Poison Bullets
    if (perk_count_get(0x34) > 0) {
        apply_poison_to_target();
    }
    
    return damage;
}

Reload Handling

void start_reload(int player_idx) {
    float reload_time = weapon_reload_time[player_idx];
    
    // Fast Reloader
    int fast_reload_count = perk_count_get(0x04);
    reload_time *= pow(0.5, fast_reload_count);
    
    player_reload_timer[player_idx] = reload_time;
    player_reload_active[player_idx] = 1;
}

void reload_complete(int player_idx) {
    player_reload_active[player_idx] = 0;
    player_ammo[player_idx] = player_clip_size[player_idx];
    
    // Angry Reloader burst
    if (perk_count_get(0x32) > 0) {
        spawn_plasma_burst(player_pos_x[player_idx], player_pos_y[player_idx]);
    }
}

Movement Update

void player_update_movement(int player_idx) {
    float speed = player_move_speed[player_idx];
    
    // Long Distance Runner
    int runner_count = perk_count_get(0x05);
    speed *= pow(1.15, runner_count);
    
    // Haste
    if (perk_count_get(0x0b) > 0) {
        speed *= 1.15;
    }
    
    player_pos_x[player_idx] += vel_x * speed * delta_time;
    player_pos_y[player_idx] += vel_y * speed * delta_time;
}

Perk Stacking

Multiplicative (most damage/speed perks):
final_value = base * pow(multiplier, count)
Additive (some utility perks):
final_value = base + (bonus * count)
Boolean (on/off perks):
if (perk_count_get(perk_id) > 0) {
    apply_effect();
}

Max Counts

PerkMax CountNotes
Most perks0No limit (can stack infinitely)
Second Chance1One-time revive
Shield1Duration-based, doesn’t stack
Man Bomb3Capped at 3

Game State

Perk drop RNG and spawn logic

Weapon Structures

Weapon-modifying perks

Perk Mechanics

High-level perk behavior documentation

Build docs developers (and LLMs) love