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) * costMultiplierbaseCostandperLevelCostIncreaseare derived from the constructor parametersnewCostandnewDifficulty:baseCost = newCostperLevelCostIncrease = 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 bonusEach 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)
| Method | Effect |
|---|---|
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)
| Method | Context |
|---|---|
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_Experiencestat for Steam achievements. - Displays a
EPlayerMessage.EXPERIENCEUI 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 *= loseXpwhere 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(respectsLevel.info.configData.Allow_Skills). - Validates speciality index < SPECIALITIES (3) and skill index < length.
- Single upgrade by default (
force=false). Whenforce=true(used for admin commands), spends XP until either XP runs out or the max unlockable level is reached. - After upgrade, sends updated
experienceto owner viaSendExperienceand broadcasts skill level viaSendSingleSkillLevelto 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.costproperty:(baseCost + level * perLevelCostIncrease) * costMultiplier. - If
Skillset_Reduces_Skill_Costis enabled and the skill matches one of the player's skillset speciality pairs, cost is halved. Skill_Cost_Multiplierfrom 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:
| Skillset | Skills with Halved Cost |
|---|---|
| FIRE | Offense: CARDIO, Defense: STRENGTH |
| POLICE | Offense: EXERCISE, Defense: TOUGHNESS |
| ARMY | Offense: SHARPSHOOTER, Offense: DEXTERITY |
| FARM | Support: AGRICULTURE, Defense: SURVIVAL |
| FISH | Support: FISHING, Offense: DIVING |
| CAMP | Defense: WARMBLOODED, Support: OUTDOORS |
| WORK | Support: CRAFTING, Support: ENGINEER, Support: MECHANIC |
| CHEF | Support: COOKING, Defense: VITALITY |
| THIEF | Offense: PARKOUR, Defense: SNEAKYBEAKY |
| MEDIC | Defense: 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:
SendBoostbroadcastsEPlayerBoostto 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
| Range | Title | Color |
|---|---|---|
| ≤ -200 | Villain | Red |
| -100 to -199 | Bandit | Red-Yellow |
| -33 to -99 | Gangster | Yellow |
| -8 to -32 | Outlaw | Yellow-White |
| -1 to -7 | Thug | White-Yellow |
| 0 | Neutral | White |
| 1–7 | Vigilante | White-Green |
| 8–32 | Constable | Green |
| 33–99 | Deputy | Green |
| 100–199 | Sheriff | Green |
| ≥ 200 | Paragon | Green |
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_PvPorLose_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_PvPorLose_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:
- If
Spawn_With_Max_Skillsis true, all skills are set to max. - If
LevelAsset.skillRulesexist, default levels are applied per-rule. - If
Spawn_With_Stamina_Skillsis true, CARDIO, DIVING, EXERCISE, and PARKOUR are maxed. - The
onApplyingDefaultSkillsevent 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:
- Validates the player has enough XP.
- Subtracts the cost.
- Finds the item asset by node's id.
- For guns, auto-adds the default magazine.
- Attempts to add the item to the player's inventory.
Plugin Hooks
| Event | Description |
|---|---|
onApplyingDefaultSkills | Before default skills are applied to a new/spawned character |
OnExperienceChanged_Global | After any player's XP changes (non-load) |
OnReputationChanged_Global | After any player's reputation changes |
OnSkillUpgraded_Global | After any player upgrades a specific skill |
XP Source Catalogue
Every gameplay action that awards experience routes through one of these paths:
| Source | Entry Point | XP Formula | Multipliers Applied |
|---|---|---|---|
| Zombie kill | PlayerSkills.askPay(xp) from damage callbacks | Base from zombie asset (2–50) | Experience_Multiplier, Headshot bonus |
| Player kill (Arena) | PlayerSkills.askPay(100) | Flat 100 | Experience_Multiplier |
| Resource harvest | PlayerSkills.askPay(xp) from DamageTool | Based on resource asset (1–10) | Experience_Multiplier, Outdoors skill |
| Animal kill | PlayerSkills.askPay(xp) from DamageTool | Based on animal asset (1–15) | Experience_Multiplier |
| Fishing | UseableFisher → askPay | Per-fish (1–30) | Experience_Multiplier, Fishing skill |
| Crafting | PlayerCrafting → askPay | Per-craft (1–10) | Experience_Multiplier, Crafting skill |
| Cooking | Cookable logic → askPay | Per-recipe (1–15) | Experience_Multiplier, Cooking skill |
| Farming | Plant harvest → askPay | Per-plant (2–20) | Experience_Multiplier, Agriculture skill |
| Reputation award | askRep from modRep | +1 per healing/disinfect action | None |
| Consumption quest | ServerModifyExperience(asset.experience) | Per-item (0–50) | None |
| Horde purchase | ReceivePurchaseRequest | Spent from pool | None (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:
| Category | Max Values |
|---|---|
| Offense | OVERKILL=7, SHARPSHOOTER=7, DEXTERITY=5, CARDIO=5, EXERCISE=5, DIVING=5, PARKOUR=5 |
| Defense | SNEAKYBEAKY=7, VITALITY=5, IMMUNITY=5, TOUGHNESS=5, STRENGTH=5, WARMBLOODED=5, SURVIVAL=5 |
| Support | HEALING=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: Usesmax(vanilla behavior).maxUnlockableLevel = 3: Player cannot level past 3 regardless ofmax.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 appliedThis 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
| Boost | Referenced By | Effect |
|---|---|---|
| HARDENED | Damage calculation (inferred) | Damage resistance (no explicit reference in available source) |
| SPLATTERIFIC | Zombie death effects (inferred) | Explosive zombie kills (no explicit reference in available source) |
| FLIGHT | PlayerMovement (inferred) | Reduced gravity (no explicit reference in available source) |
| OLYMPIC | UseableThrowable.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
| Version | Changes |
|---|---|
| 1–4 | Legacy format (unsupported in modern code) |
| 5 | Added _experience as uint32, _boost as byte |
| 6 | Added per-skill level array (flat byte[], all specialities) |
| 7 | Added _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:
SendMultipleSkillLevels: All skill levels packed in order (22 bytes: 7 O + 7 D + 8 S).SendExperience: Current XP value as uint32.SendReputation: Current reputation as int32.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 arenaNon-Survival, Non-Arena (e.g., Horde)
csharp
// Same skill reset as arena, but
_experience = (uint)(experience * 0.75f); // 75% retentionCommon Post-Death
csharp
_boost = EPlayerBoost.NONE;
// Replicate via SendExperience and SendBoost loopbackSkill Mastery Lookup Table — Detailed Effects
| Skill | mastery(level) | Gameplay Effect | Formula | Max Effect |
|---|---|---|---|---|
| OVERKILL | L/7 | Damage multiplier | 1 + mastery * 0.5 | 1.5× damage |
| SHARPSHOOTER | L/7 | Recoil/spread reduction | 1 - mastery * 0.4 | 0.6× recoil/spread |
| DEXTERITY | L/5 | Reload+hammer speed | 1 + mastery * 0.5 | 1.5× speed |
| CARDIO | L/5 | Stamina regen rate | Referenced in PlayerMovement | Faster regen |
| EXERCISE | L/5 | Stamina consumption | 1 - mastery * 0.75 for melee; 1 - mastery * 0.5 for others | 0.25× stamina use |
| DIVING | L/5 | Oxygen + scope sway | Oxygen: 5 - level/2 per tick; Sway: 1 - mastery * 0.5 | 2.5 oxygen/tick, 0.5× sway |
| PARKOUR | L/5 | Fall damage threshold | Referenced in PlayerMovement | Higher safe fall |
| SNEAKYBEAKY | L/7 | Zombie detection range | Referenced in zombie AI | Reduced aggro radius |
| VITALITY | L/5 | Passive health regen | Per-tick health restoration | Faster regen |
| IMMUNITY | L/5 | Infection resistance | 1 - mastery * 0.5 | 0.5× virus intake |
| TOUGHNESS | L/5 | Damage flinch reduction | Flinch: 1 - mastery * 0.75; Explosion: 1 - mastery * 0.5 | 0.25× flinch, 0.5× shake |
| STRENGTH | L/5 | Melee + throw force | Referenced in melee and throwable | Increased force |
| WARMBLOODED | L/5 | Temperature resistance | Referenced in temperature system | Cold resistance |
| SURVIVAL | L/5 | Hunger/thirst drain | Referenced in PlayerLife update | Reduced drain rate |
| HEALING | L/7 | Healing output | 1 + mastery * 0.5 | 1.5× healing |
| CRAFTING | L/3 | Crafting output | Referenced in PlayerCrafting | Bonus yield |
| OUTDOORS | L/5 | Resource harvest | 1 + mastery * 0.5 | 1.5× resource damage |
| COOKING | L/3 | Cooking output | Referenced in cooking system | Bonus yield |
| FISHING | L/5 | Fishing yield | Referenced in UseableFisher | Better fish |
| AGRICULTURE | L/7 | Farming yield | Referenced in farming system | Higher yield |
| MECHANIC | L/5 | Repair amount | 1 + mastery | 2× repair output |
| ENGINEER | L/3 | Building durability | Referenced in build system | More 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:
| Skill | Reader |
|---|---|
| OVERKILL | UseableMelee.fire, UseableGun.fire — * (1 + mastery * 0.5) damage multiplier |
| SHARPSHOOTER | UseableGun.CalculateSpreadAngleRadians, GetSharpshooterRecoilMultiplier — 1 - mastery * 0.4 |
| DEXTERITY | UseableGun.hammer, UseableGun.ReceivePlayReload — speed += mastery * 0.5 |
| HEALING | UseableConsumeable.performHealth — delta * (1 + mastery * 0.5) |
| IMMUNITY | UseableConsumeable.performAid — virus reduction: * (1 - mastery * 0.5) |
| TOUGHNESS | PlayerLook.FlinchFromDamage, FlinchFromExplosion — flinch reduction * (1 - mastery * 0.75/0.5) |
| STRENGTH | UseableMelee.startSecondary — stamina cost reduction * (1 - mastery * 0.5) |
| MECHANIC | UseableMelee.fire — repair amount * (1 + mastery) |
| OUTDOORS | UseableMelee.fire — resource damage * (1 + mastery * 0.5) |
| EXERCISE | UseableMelee.startSecondary — stamina cost * (1 - mastery * 0.75) |
| CARDIO | PlayerMovement (stamina regen rate) |
| PARKOUR | PlayerMovement (fall damage threshold) |
| SNEAKYBEAKY | Zombie aggro radius calculation |
| VITALITY | PlayerLife passive health regen tick |
| SURVIVAL | PlayerLife food/water drain rate |
| WARMBLOODED | Temperature system (hypothermia resistance) |
| DIVING | UseableGun.simulate — steady breathing oxygen cost, scope sway |
Debugging & Testing
- Unlock all skills:
ServerUnlockAllSkills()sets every skill to max and replicates. UsesSendMultipleSkillLevelsloopback. - 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.MaxValueas a sentinel until the server sends the real value.
