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
FishingUseableFisher → askPayPer-fish (1–30)Experience_Multiplier, Fishing skill
CraftingPlayerCrafting → askPayPer-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.