Skip to content

Player Skills and Experience System

Overview

The skills system governs character progression across three specialities—Offense, Defense, and Support—each containing 7–8 individual skills. Experience points (XP) are earned through gameplay actions (kills, resource harvesting, healing, etc.) and spent to raise skill levels. Reputation tracks moral alignment and affects title/color display. Boosts provide temporary passive bonuses. Skill loss occurs on death according to configurable PvP/PvE multipliers.

The primary class is PlayerSkills (1123 lines) at Unturned/Player/PlayerSkills.cs. Each skill is modelled by the Skill class at Unturned/Player/Skill.cs. Enums define the skill taxonomy: EPlayerSpeciality (3 values), EPlayerOffense (7 values), EPlayerDefense (7 values), EPlayerSupport (8 values), EPlayerBoost (4 values + NONE), EPlayerSkillset (11 values).


Core Data Model

Skill

csharp
public class Skill
{
    public byte level;
    public byte max;
    public int maxUnlockableLevel = -1;
    public float costMultiplier = 1.0f;
    public float mastery => level == 0 ? 0f : level >= max ? 1f : level / (float)max;
    public uint cost => RoundAndClampToUInt((baseCost + level * perLevelCostIncrease) * costMultiplier);
}

Each skill stores a current level, a hard max cap, an optional maxUnlockableLevel that overrides the max when set by LevelAsset rules, and a costMultiplier for server-side balancing. The mastery property returns a 0.0–1.0 normalized value used as a multiplier in dozens of game calculations. The cost property computes the XP required for the next upgrade using a linear formula:

cost = (baseCost + level * perLevelCostIncrease) * costMultiplier
  • baseCost and perLevelCostIncrease are derived from the constructor parameters newCost and newDifficulty:
    • baseCost = newCost
    • perLevelCostIncrease = Round(baseCost * newDifficulty)
  • Example with cost=20, difficulty=1.5: level 0 costs 20, level 1 costs 50, level 2 costs 80.

NormalizeLevel(int inputLevel) returns 0.0 when ≤0, 1.0 when ≥max, or inputLevel / (float)max otherwise. GetClampedMaxUnlockableLevel() returns Mathf.Min(max, maxUnlockableLevel).

Skill Array

PlayerSkills._skills is a jagged array: Skill[SPECIALITIES][] where SPECIALITIES = 3. Initialization in InitializePlayer():

OFFENSE (7 skills):
  OVERKILL    (0, 7, 10, 1.0) — damage multiplier
  SHARPSHOOTER(0, 7, 10, 1.0) — recoil/spread reduction
  DEXTERITY   (0, 5, 10, 0.5) — reload/hammer speed, aim duration
  CARDIO      (0, 5, 10, 0.5) — stamina regen
  EXERCISE    (0, 5, 10, 0.5) — stamina consumption reduction
  DIVING      (0, 5, 10, 0.5) — oxygen consumption/scope sway
  PARKOUR     (0, 5, 20, 0.5) — fall damage/landing roll

DEFENSE (7 skills):
  SNEAKYBEAKY (0, 7, 10, 1.0) — zombie aggro range
  VITALITY    (0, 5, 10, 0.5) — health regen
  IMMUNITY    (0, 5, 10, 0.5) — virus/infection resistance
  TOUGHNESS   (0, 5, 10, 0.5) — damage flinch/explosion shake reduction
  STRENGTH    (0, 5, 10, 0.5) — melee damage/throw force
  WARMBLOODED (0, 5, 10, 0.5) — temperature resistance
  SURVIVAL    (0, 5, 10, 0.5) — hunger/thirst drain reduction

SUPPORT (8 skills):
  HEALING     (0, 7, 10, 1.0) — healing amount multiplier
  CRAFTING    (0, 3, 20, 1.5) — crafting output bonus
  OUTDOORS    (0, 5, 10, 0.5) — resource yield multiplier
  COOKING     (0, 3, 20, 1.5) — cooking output bonus
  FISHING     (0, 5, 10, 0.5) — fishing yield
  AGRICULTURE (0, 7, 10, 1.0) — farming yield
  MECHANIC    (0, 5, 10, 0.5) — repair amount multiplier
  ENGINEER    (0, 3, 20, 1.5) — building durability bonus

Each entry shows constructor params: (initialLevel, max, baseCost, difficulty).

LevelAsset SkillRules

After initialization, if a LevelAsset with skillRules exists and level overrides are not prevented, each skill's maxUnlockableLevel, costMultiplier, baseCost, and perLevelCostIncrease can be overridden per-map. This allows servers to cap certain skills lower or make them more expensive.


Experience System

Experience is a uint field replicated from server to clients. The class provides four public mutation paths:

Server-Only Paths (authoritative)

MethodEffect
askAward(uint award)Increases XP on server, replicates via SendExperience
askSpend(uint cost)Decreases XP on server, replicates
ServerSetExperience(uint newValue)Absolute setter — calls askAward or askSpend
ServerModifyExperience(int delta)Signed delta — positive awards, negative spends (clamped to zero)
askPay(uint pay)Zombie/action XP with Experience_Multiplier applied

Client-Side Paths (prediction/correction)

MethodContext
modXp(uint xp)Local add (non-replicated, broadcast only updates UI)
modXp2(uint xp)Local subtract

Experience Notifications

ReceiveExperience is called when the server sends the authoritative XP value. On the local player it:

  • Updates the Found_Experience stat for Steam achievements.
  • Displays a EPlayerMessage.EXPERIENCE UI notification showing the delta.

Experience Multiplier

askPay applies Provider.modeConfigData.Players.Experience_Multiplier before adding. This is a global multiplier for zombie kills, resource harvesting, and quest rewards.

Experience Loss on Death

In Survival mode, death triggers:

experience *= loseXp

where loseXp is Lose_Experience_PvP or Lose_Experience_PvE from config (default 0.75). In Arena mode, XP resets to 0. Otherwise XP retains 75% on death.


Skill Upgrade System

Client Request

The client calls sendUpgrade(speciality, index, force) which fires the SendUpgradeRequest RPC (rate-limited to 10 Hz, ONLY_FROM_OWNER).

Server Processing (ReceiveUpgradeRequest)

csharp
while (experience >= cost(speciality, index) && skill.level < skill.GetClampedMaxUnlockableLevel())
{
    _experience -= cost(speciality, index);
    skill.level++;
    if (!force) break;
}
  • Validates doesLevelAllowSkills (respects Level.info.configData.Allow_Skills).
  • Validates speciality index < SPECIALITIES (3) and skill index < length.
  • Single upgrade by default (force=false). When force=true (used for admin commands), spends XP until either XP runs out or the max unlockable level is reached.
  • After upgrade, sends updated experience to owner via SendExperience and broadcasts skill level via SendSingleSkillLevel to all clients.
  • Fires OnSkillUpgraded_Global(event) (plugin hook).
  • If XP changed, fires OnExperienceChanged_Global.

Skill Cost Calculation

csharp
public uint cost(int speciality, int index)
  • Base cost from Skill.cost property: (baseCost + level * perLevelCostIncrease) * costMultiplier.
  • If Skillset_Reduces_Skill_Cost is enabled and the skill matches one of the player's skillset speciality pairs, cost is halved.
  • Skill_Cost_Multiplier from config is applied multiplicatively.

Skillset System

EPlayerSkillset defines 11 archetypes: NONE, FIRE, POLICE, ARMY, FARM, FISH, CAMP, WORK, CHEF, THIEF, MEDIC.

SKILLSETS is a static readonly array mapping each skillset to 1–3 SpecialitySkillPair entries that define which skills get reduced cost:

SkillsetSkills with Halved Cost
FIREOffense: CARDIO, Defense: STRENGTH
POLICEOffense: EXERCISE, Defense: TOUGHNESS
ARMYOffense: SHARPSHOOTER, Offense: DEXTERITY
FARMSupport: AGRICULTURE, Defense: SURVIVAL
FISHSupport: FISHING, Offense: DIVING
CAMPDefense: WARMBLOODED, Support: OUTDOORS
WORKSupport: CRAFTING, Support: ENGINEER, Support: MECHANIC
CHEFSupport: COOKING, Defense: VITALITY
THIEFOffense: PARKOUR, Defense: SNEAKYBEAKY
MEDICDefense: IMMUNITY, Support: HEALING

Skillset also prevents skill loss for its paired skills when Skillset_Prevents_Skill_Loss is enabled (default). The CanDecreaseLevelOfSkill method checks the player's skillset and returns false for protected pairs.


Boost System

EPlayerBoost defines 5 states: NONE, HARDENED, SPLATTERIFIC, FLIGHT, OLYMPIC.

Mechanics

  • Cost: 25 XP per reroll (BOOST_COST = 25).
  • Request: sendBoost()ReceiveBoostRequest() (rate-limited, 10 Hz).
  • Reroll: Picks a random value from 1–4, excluding the current boost, ensuring no consecutive same boost.
  • Replication: SendBoost broadcasts EPlayerBoost to all clients.

Boost Effects

The source files for boost gameplay logic are distributed across other systems, but the enum values imply:

  • HARDENED: Damage resistance
  • SPLATTERIFIC: Explosive effects on kills
  • FLIGHT: Low gravity / jump boost
  • OLYMPIC: Increased throw force (directly referenced in UseableThrowable.tick(): forceMagnitude *= equippedThrowableAsset.boostForceMultiplier)

Boost is cleared on death (reset to NONE).


Reputation System

Reputation is an int ranging from negative (villain) to positive (paragon). It is replicated via SendReputation.

Reputation Tiers

RangeTitleColor
≤ -200VillainRed
-100 to -199BanditRed-Yellow
-33 to -99GangsterYellow
-8 to -32OutlawYellow-White
-1 to -7ThugWhite-Yellow
0NeutralWhite
1–7VigilanteWhite-Green
8–32ConstableGreen
33–99DeputyGreen
100–199SheriffGreen
≥ 200ParagonGreen

Reputation Changes

  • askRep(int rep) replicates additive reputation change to all clients.
  • modRep(int rep) applies locally (for single-player/listen server).
  • Earning XP from aiding others also grants +1 reputation per stat improved.
  • Achievements are awarded at thresholds (≥200 = "Paragon", ≤ -200 = "Villain").

Skill Loss on Death

The onLifeUpdated handler (subscribed to player.life.onLifeUpdated) manages skill level reductions when the player dies.

Multiplicative Loss

csharp
byte newLevel = (byte)(specialitySkills[skillIndex].level * loseSkills);
  • loseSkills = Lose_Skills_PvP or Lose_Skills_PvE (default 1.0, no loss).
  • Skills protected by skillset are excluded (via CanDecreaseLevelOfSkill).

Level Count Loss

csharp
LoseNumberOfSkills(numberOfSkillsToLose, ...);
  • Removes N random skill levels from unprotected skills with level > 0.
  • numberOfSkillsToLose = Lose_Skill_Levels_PvP or Lose_Skill_Levels_PvE (default 0).

Both loss types are applied cumulatively in Survival mode. In Arena mode, all skills reset to zero and applyDefaultSkills() is called.


Default Skills Application

applyDefaultSkills() is called when:

  • No save file exists.
  • In Arena mode after death.
  • Level rules dictate starting levels.

Logic:

  1. If Spawn_With_Max_Skills is true, all skills are set to max.
  2. If LevelAsset.skillRules exist, default levels are applied per-rule.
  3. If Spawn_With_Stamina_Skills is true, CARDIO, DIVING, EXERCISE, and PARKOUR are maxed.
  4. The onApplyingDefaultSkills event is fired for plugin customization.

Save/Load System

Save Format (/Player/Skills.dat, version 7)

byte   SAVEDATA_VERSION (7)
uint   experience
int    reputation
byte   boost (EPlayerBoost)
byte[] skill_levels (flat, 22 bytes: 7 offense + 7 defense + 8 support)

Load

  • If save file exists and level type is SURVIVAL, deserialized from block.
  • Version 5+ includes experience and boost.
  • Version 7 added reputation.
  • Version 6+ reads all skill levels with max clamping.
  • Otherwise, applyDefaultSkills() is called.

Save

Writes current state to disk. Only proceeds if wasLoadCalled is true, preventing writes before initial load.


Horde Mode Purchases

ReceivePurchaseRequest handles buying items from Horde mode purchase volumes. The server:

  1. Validates the player has enough XP.
  2. Subtracts the cost.
  3. Finds the item asset by node's id.
  4. For guns, auto-adds the default magazine.
  5. Attempts to add the item to the player's inventory.

Plugin Hooks

EventDescription
onApplyingDefaultSkillsBefore default skills are applied to a new/spawned character
OnExperienceChanged_GlobalAfter any player's XP changes (non-load)
OnReputationChanged_GlobalAfter any player's reputation changes
OnSkillUpgraded_GlobalAfter any player upgrades a specific skill

XP Source Catalogue

Every gameplay action that awards experience routes through one of these paths:

SourceEntry PointXP FormulaMultipliers Applied
Zombie killPlayerSkills.askPay(xp) from damage callbacksBase from zombie asset (2–50)Experience_Multiplier, Headshot bonus
Player kill (Arena)PlayerSkills.askPay(100)Flat 100Experience_Multiplier
Resource harvestPlayerSkills.askPay(xp) from DamageToolBased on resource asset (1–10)Experience_Multiplier, Outdoors skill
Animal killPlayerSkills.askPay(xp) from DamageToolBased on animal asset (1–15)Experience_Multiplier
FishingUseableFisheraskPayPer-fish (1–30)Experience_Multiplier, Fishing skill
CraftingPlayerCraftingaskPayPer-craft (1–10)Experience_Multiplier, Crafting skill
CookingCookable logic → askPayPer-recipe (1–15)Experience_Multiplier, Cooking skill
FarmingPlant harvest → askPayPer-plant (2–20)Experience_Multiplier, Agriculture skill
Reputation awardaskRep from modRep+1 per healing/disinfect actionNone
Consumption questServerModifyExperience(asset.experience)Per-item (0–50)None
Horde purchaseReceivePurchaseRequestSpent from poolNone (spending, not earning)

Statistics Tracking

XP collection is mirrored to Steam stats for achievements:

csharp
if (channel.IsLocalPlayer && newExperience > experience && Level.info.type != ELevelType.HORDE)
{
    int data;
    if (Provider.provider.statisticsService.userStatisticsService.getStatistic("Found_Experience", out data))
    {
        Provider.provider.statisticsService.userStatisticsService.setStatistic("Found_Experience",
            data + (int)(newExperience - experience));
    }
    PlayerUI.message(EPlayerMessage.EXPERIENCE, (newExperience - experience).ToString());
}

The "Found_Experience" stat accumulates all XP ever gained (not spent). The UI delta message shows how much was gained in that transaction.

Experience Multiplier Config

From Provider.modeConfigData.Players:

csharp
// Applied in askPay()
pay = (uint)(pay * Provider.modeConfigData.Players.Experience_Multiplier);

Default is 1.0. Servers can reduce or increase all XP gains through this single multiplier. Additional per-source multipliers (e.g., zombie difficulty bonus) stack multiplicatively.


Skill Level Limits in Depth

Vanilla Max Limits

Each skill has a vanilla max that defines the theoretical ceiling:

CategoryMax Values
OffenseOVERKILL=7, SHARPSHOOTER=7, DEXTERITY=5, CARDIO=5, EXERCISE=5, DIVING=5, PARKOUR=5
DefenseSNEAKYBEAKY=7, VITALITY=5, IMMUNITY=5, TOUGHNESS=5, STRENGTH=5, WARMBLOODED=5, SURVIVAL=5
SupportHEALING=7, CRAFTING=3, OUTDOORS=5, COOKING=3, FISHING=5, AGRICULTURE=7, MECHANIC=5, ENGINEER=3

MaxUnlockableLevel vs Max

The maxUnlockableLevel field (-1 by default) enables per-map restrictions:

  • maxUnlockableLevel = -1: Uses max (vanilla behavior).
  • maxUnlockableLevel = 3: Player cannot level past 3 regardless of max.
  • maxUnlockableLevel = 0: Skill is locked at zero — cannot be upgraded at all.

Set by LevelAsset.skillRules[speciality][skill].maxUnlockableLevel. The clamping logic:

csharp
public int GetClampedMaxUnlockableLevel()
{
    return maxUnlockableLevel > -1 ? Mathf.Min(max, maxUnlockableLevel) : max;
}

This means maxUnlockableLevel can never exceed the vanilla max.

Skill Cost Override Logic

When LevelAsset.skillRules overrides are applied (during InitializePlayer):

csharp
if (skillRule.baseCostOverride > -1)
    skill.baseCost = skillRule.baseCostOverride;

if (skillRule.perLevelCostIncreaseOverride > -1)
    skill.perLevelCostIncrease = skillRule.perLevelCostIncreaseOverride;

skill.costMultiplier = skillRule.costMultiplier; // always applied

This allows map authors to fine-tune individual skill costs. The costMultiplier is always applied from the rule; baseCost and perLevelCostIncrease only override if >= 0.


Boost System — Detailed Mechanics

Reroll Algorithm

csharp
byte newBoost;
do
{
    newBoost = (byte)Random.Range(1, BOOST_COUNT + 1); // 1..4
}
while (newBoost == (byte)boost);
_boost = (EPlayerBoost)newBoost;

This ensures the new boost is never the same as the current one. Since BOOST_COUNT = 4, each reroll has a 1/4 chance per attempt (with rejection sampling for the current boost: 1/3 effective chance for any specific non-current boost, or 33%).

Boost Cost

Fixed at BOOST_COST = 25 XP per reroll. This is not configurable via mode config. The cost is subtracted before the reroll.

csharp
if (experience >= BOOST_COST)
{
    _experience -= BOOST_COST;
    // ... reroll and replicate
}

Boost Consumption

Boost is cleared to NONE on death (onLifeUpdated). There is no duration-based expiration — the boost persists until death or manual reroll.

Cross-System Boost References

BoostReferenced ByEffect
HARDENEDDamage calculation (inferred)Damage resistance (no explicit reference in available source)
SPLATTERIFICZombie death effects (inferred)Explosive zombie kills (no explicit reference in available source)
FLIGHTPlayerMovement (inferred)Reduced gravity (no explicit reference in available source)
OLYMPICUseableThrowable.tick()forceMagnitude *= equippedThrowableAsset.boostForceMultiplier

Reputation — Detailed Mechanics

Reputation Change Boundaries

csharp
// In modRep():
_reputation += rep;
onReputationUpdated?.Invoke(reputation);
OnReputationChanged_Global?.Invoke(this, oldReputation);

The modRep method modifies reputation locally without network replication. For replicated changes, askRep(int rep) uses SendReputation.InvokeAndLoopback.

Achievement Integration

  • reputation <= -200: Achievement "Villain" unlocked (if not already).
  • reputation >= 200: Achievement "Paragon" unlocked (if not already).
  • Only checked once per change event, not on initial load.

Plugin Widget Flag

Reputation change notifications respect EPluginWidgetFlags.ShowReputationChangeNotification:

csharp
if (player.isPluginWidgetFlagActive(EPluginWidgetFlags.ShowReputationChangeNotification))
{
    string text = (newReputation - reputation).ToString();
    if (newReputation > reputation) text = '+' + text;
    PlayerUI.message(EPlayerMessage.REPUTATION, text);
}

Negative Reputation Floor

Reputation can go arbitrarily negative — there is no clamping to a minimum value. The title system only defines labels down to -200, but values below -200 still display as "Villain" since the key function uses <= -200.


Save/Load — Detailed Binary Format

Block Serialization Version History

VersionChanges
1–4Legacy format (unsupported in modern code)
5Added _experience as uint32, _boost as byte
6Added per-skill level array (flat byte[], all specialities)
7Added _reputation as int32

Read Path

csharp
// Server-side only (Provider.isServer guard)
Block block = PlayerSavedata.readBlock(owner.playerID, "/Player/Skills.dat", 0);
byte version = block.readByte();

if (version > 4)
    _experience = block.readUInt32();

if (version >= 7)
    _reputation = block.readInt32();
else
    _reputation = 0; // default for older saves

_boost = (EPlayerBoost)block.readByte();

if (version >= 6)
{
    for (byte special = 0; special < skills.Length; special++)
        for (byte index = 0; index < skills[special].Length; index++)
        {
            skills[special][index].level = block.readByte();
            if (skills[special][index].level > skills[special][index].max)
                skills[special][index].level = skills[special][index].max;
        }
}

Write Path

csharp
Block block = new Block();
block.writeByte(SAVEDATA_VERSION); // 7
block.writeUInt32(experience);
block.writeInt32(reputation);
block.writeByte((byte)boost);

for (byte special = 0; special < skills.Length; special++)
    if (skills[special] != null)
        for (byte index = 0; index < skills[special].Length; index++)
            block.writeByte(skills[special][index].level);

PlayerSavedata.writeBlock(owner.playerID, "/Player/Skills.dat", block);

Save Frequency

Save is triggered on:

  • Player disconnect / server shutdown (via PlayerSavedata).
  • Manual calls from plugin hooks or admin commands.
  • Not called on every skill upgrade — only on persistent events.

Network Replication — Packet Format

Initial Player State

When a new player joins, SendInitialPlayerState transmits four RPCs:

  1. SendMultipleSkillLevels: All skill levels packed in order (22 bytes: 7 O + 7 D + 8 S).
  2. SendExperience: Current XP value as uint32.
  3. SendReputation: Current reputation as int32.
  4. SendBoost: Current boost as EPlayerBoost (byte).

Skill Level Updates

SendSingleSkillLevel is used for individual level changes (upgrades):

NetId (8 bytes) + speciality (1 byte) + index (1 byte) + level (1 byte)

Loopback to all clients plus owner.

Experience Updates

SendExperience is used for XP changes:

NetId (8 bytes) + experience (4 bytes, uint32)

Sent only to the owning client (via channel.GetOwnerTransportConnection()).


Death Penalty — Complete Algorithm

When the player dies (onLifeUpdated(isDead=true)):

Survival Mode

csharp
float loseSkills = wasPvPDeath ? Lose_Skills_PvP : Lose_Skills_PvE;

// 1. Multiplicative skill loss
if (loseSkills < 0.999f)
{
    for each skill:
        if (CanDecreaseLevelOfSkill(speciality, index)):
            newLevel = (byte)(level * loseSkills);
            // e.g., loseSkills = 0.75 → level 7 → 5
}

// 2. Random level loss
uint numberOfSkillLevelsToLose = wasPvPDeath ? Lose_Skill_Levels_PvP : Lose_Skill_Levels_PvE;
if (numberOfSkillLevelsToLose > 0)
{
    // Collect all protectable skills with level > 0
    // Pick random ones, decrement each by 1
    // Remove decremented skills from pool (can't lose more than 1 per skill per death)
}

// 3. Experience loss
_experience = (uint)(experience * loseXp);
// loseXp = Lose_Experience_PvP or Lose_Experience_PvE (default 0.75)

Arena Mode

csharp
// All skills reset to zero
for each skill:
    skills[speciality][index].level = 0;

applyDefaultSkills(); // Apply spawn defaults

_experience = 0; // Full reset in arena

Non-Survival, Non-Arena (e.g., Horde)

csharp
// Same skill reset as arena, but
_experience = (uint)(experience * 0.75f); // 75% retention

Common Post-Death

csharp
_boost = EPlayerBoost.NONE;
// Replicate via SendExperience and SendBoost loopback

Skill Mastery Lookup Table — Detailed Effects

Skillmastery(level)Gameplay EffectFormulaMax Effect
OVERKILLL/7Damage multiplier1 + mastery * 0.51.5× damage
SHARPSHOOTERL/7Recoil/spread reduction1 - mastery * 0.40.6× recoil/spread
DEXTERITYL/5Reload+hammer speed1 + mastery * 0.51.5× speed
CARDIOL/5Stamina regen rateReferenced in PlayerMovementFaster regen
EXERCISEL/5Stamina consumption1 - mastery * 0.75 for melee; 1 - mastery * 0.5 for others0.25× stamina use
DIVINGL/5Oxygen + scope swayOxygen: 5 - level/2 per tick; Sway: 1 - mastery * 0.52.5 oxygen/tick, 0.5× sway
PARKOURL/5Fall damage thresholdReferenced in PlayerMovementHigher safe fall
SNEAKYBEAKYL/7Zombie detection rangeReferenced in zombie AIReduced aggro radius
VITALITYL/5Passive health regenPer-tick health restorationFaster regen
IMMUNITYL/5Infection resistance1 - mastery * 0.50.5× virus intake
TOUGHNESSL/5Damage flinch reductionFlinch: 1 - mastery * 0.75; Explosion: 1 - mastery * 0.50.25× flinch, 0.5× shake
STRENGTHL/5Melee + throw forceReferenced in melee and throwableIncreased force
WARMBLOODEDL/5Temperature resistanceReferenced in temperature systemCold resistance
SURVIVALL/5Hunger/thirst drainReferenced in PlayerLife updateReduced drain rate
HEALINGL/7Healing output1 + mastery * 0.51.5× healing
CRAFTINGL/3Crafting outputReferenced in PlayerCraftingBonus yield
OUTDOORSL/5Resource harvest1 + mastery * 0.51.5× resource damage
COOKINGL/3Cooking outputReferenced in cooking systemBonus yield
FISHINGL/5Fishing yieldReferenced in UseableFisherBetter fish
AGRICULTUREL/7Farming yieldReferenced in farming systemHigher yield
MECHANICL/5Repair amount1 + mastery2× repair output
ENGINEERL/3Building durabilityReferenced in build systemMore durable builds

Key Code Paths

XP Award → Replication Flow

askPay(pay)
  → pay *= Experience_Multiplier
  → _experience += pay
  → SendExperience(GetNetId(), ..., experience)
  → ReceiveExperience(uint newExperience)
    → statisticsService.setStatistic("Found_Experience", ...)
    → PlayerUI.message(EPlayerMessage.EXPERIENCE, ...)
    → onExperienceUpdated.Invoke()
    → OnExperienceChanged_Global.Invoke()

Skill Upgrade → Server Validation Flow

sendUpgrade(speciality, index, force)
  → SendUpgradeRequest.Invoke()
  → ReceiveUpgradeRequest(speciality, index, force)
    → doesLevelAllowSkills check
    → bounds check (speciality < 3, index < length)
    → while(experience >= cost && level < maxUnlockableLevel)
        _experience -= cost
        skill.level++
        if !force break
    → if level changed:
        SendExperience(owner)
        SendSingleSkillLevel(broadcast)
        OnSkillUpgraded_Global.Invoke()

Death → Skill Penalty Flow

onLifeUpdated(isDead=true)
  → Lose_Skills_PvP/PvE multiplier applied per skill
  → Lose_Skill_Levels_PvP/PvE random level removals
  → experience *= loseXp
  → boost = NONE
  → SendExperience, SendBoost (loopback)

Skill Mastery Integration Points (Cross-System)

The mastery property is queried by dozens of gameplay systems:

SkillReader
OVERKILLUseableMelee.fire, UseableGun.fire — * (1 + mastery * 0.5) damage multiplier
SHARPSHOOTERUseableGun.CalculateSpreadAngleRadians, GetSharpshooterRecoilMultiplier — 1 - mastery * 0.4
DEXTERITYUseableGun.hammer, UseableGun.ReceivePlayReload — speed += mastery * 0.5
HEALINGUseableConsumeable.performHealth — delta * (1 + mastery * 0.5)
IMMUNITYUseableConsumeable.performAid — virus reduction: * (1 - mastery * 0.5)
TOUGHNESSPlayerLook.FlinchFromDamage, FlinchFromExplosion — flinch reduction * (1 - mastery * 0.75/0.5)
STRENGTHUseableMelee.startSecondary — stamina cost reduction * (1 - mastery * 0.5)
MECHANICUseableMelee.fire — repair amount * (1 + mastery)
OUTDOORSUseableMelee.fire — resource damage * (1 + mastery * 0.5)
EXERCISEUseableMelee.startSecondary — stamina cost * (1 - mastery * 0.75)
CARDIOPlayerMovement (stamina regen rate)
PARKOURPlayerMovement (fall damage threshold)
SNEAKYBEAKYZombie aggro radius calculation
VITALITYPlayerLife passive health regen tick
SURVIVALPlayerLife food/water drain rate
WARMBLOODEDTemperature system (hypothermia resistance)
DIVINGUseableGun.simulate — steady breathing oxygen cost, scope sway

Debugging & Testing

  • Unlock all skills: ServerUnlockAllSkills() sets every skill to max and replicates. Uses SendMultipleSkillLevels loopback.
  • Force set level: ServerSetSkillLevel(int specialityIndex, int skillIndex, int newLevel) overrides any skill with bounds checking.
  • TryParseIndices: Parse string input to (specialityIndex, skillIndex) for console commands. Tries each enum (EPlayerOffense, EPlayerDefense, EPlayerSupport) in order.
  • Client prediction: Clients load with _experience = uint.MaxValue as a sentinel until the server sends the real value.