Skip to content

UseableMelee — The Attack System ​

Overview ​

UseableMelee (1142 lines) at Unturned/Useable/UseableMelee.cs implements the runtime behavior for all melee weapons. It supports two swing modes (weak and strong), repeated-use weapons (chainsaws, grinders), light sources, repair functionality, vehicle/barricade/structure interaction, and resource harvesting with blade-ID matching.

Supporting types: ESwingMode (WEAK, STRONG), ItemMeleeAsset, ItemWeaponAsset.


Attack Mode ​

ESwingMode defines two attack types:

Modeprimary/secondaryDamage multiplierUseStamina cost
WEAKstartPrimary()1× baseRegular attackNone
STRONGstartSecondary()equippedMeleeAsset.strengthPower attackstamina * (1 - Exercise_mastery * 0.75)

Primary Attack (startPrimary) ​

For repeated melee weapons (chainsaw, angle grinder):

  • Toggles isSwinging on (start) and off (stop).
  • Plays Start_Swing / Stop_Swing animation.
  • Continues dealing damage each simulate tick while swinging.

For non-repeated weapons:

  • Must be isUseable (previous animation finished) to initiate.
  • Sets isBusy = true, plays Weak animation via swing().

Secondary Attack (startSecondary) ​

Non-repeated only:

  • Checks stamina: must have enough for stamina * (1 - Exercise_mastery * 0.5).
  • Consumes stamina via player.life.askTire().
  • Sets swingMode = STRONG, plays Strong animation.

Swing Timing ​

csharp
// Animation lengths from equipped asset
weakAttackAnimLengthSeconds = GetAnimationLength("Weak");
strongAttackAnimLengthSeconds = GetAnimationLength("Strong");
weakAttackAnimLengthFrames = (uint)(length / PlayerInput.RATE);
strongAttackAnimLengthFrames = (uint)(length / PlayerInput.RATE);

isUseable (animation complete) ​

csharp
bool isUseable =>
    swingMode == WEAK ? simulation - startedUse > weakAttackAnimLengthFrames
    : swingMode == STRONG ? simulation - startedUse > strongAttackAnimLengthFrames
    : false;

isDamageable (damage window) ​

csharp
bool isDamageable =>
    swingMode == WEAK ? simulation - startedUse > weakAttackAnimLengthFrames * asset.weak
    : swingMode == STRONG ? simulation - startedUse > strongAttackAnimLengthFrames * asset.strong
    : false;

asset.weak and asset.strong (float 0.0–1.0) define when in the animation the attack becomes actionable.

Simulate Tick ​

csharp
override void simulate(uint simulation, bool inputSteady)
{
    if (isUsing && isDamageable)
    {
        if (isRepeated) startedUse = simulation; // reset timer for continuous damage
        else { isBusy = false; isUsing = false; }
        fire();
    }
}

For repeated weapons, startedUse resets each simulate tick, allowing continuous damage while swinging. For single-swing weapons, only one damage tick per swing.


Damage Application (fire()) ​

Damage Multiplier Stacking ​

csharp
float times = 1;
times *= 1f + OVERKILL_skill_mastery * 0.5f;
times *= swingMode == STRONG ? equippedMeleeAsset.strength : 1f;
times *= quality < 0.5f ? (0.5f + quality) : 1f;

Raycast ​

csharp
Ray ray = new Ray(player.look.aim.position, player.look.aim.forward);
RaycastInfo info = DamageTool.raycast(ray, range, DAMAGE_CLIENT, ignorePlayer: player);

Range comes from ItemWeaponAsset.range.

Entity Type Dispatch ​

EntityDamage Calculation
PLAYERplayerDamageMultiplier × times × config multiplier; respects armor, backstab; bypassAllowedToDamagePlayer flag skips PvP check
ZOMBIEzombieOrPlayerDamageMultiplier × times × zombie armor; allowBackstab = true; stun override configurable (critical-only, always, never)
ANIMALanimalOrPlayerDamageMultiplier × times × armor falloff
VEHICLERepair: vehicleDamage * times * Melee_Repair_Multiplier * (1 + Mechanic_mastery)
Damage: vehicleDamage * times * Melee_Damage_Multiplier (gated by canBeDamaged and isVulnerable)
BARRICADERepair: same as vehicle, Melee_Repair_Multiplier from barricade config
Damage: barricadeDamage * times * Melee_Damage_Multiplier; sentry alert on hit
STRUCTURERepair/Damage: mirrored barricade pattern with structure-specific multipliers
RESOURCEresourceDamage * times * (1 + Outdoors_mastery * 0.5); blade ID must match or vulnerableToAllMeleeWeapons must be true
OBJECT (rubble)objectDamage * times; blade ID check against rubbleBladeID; vulnerability check

Repair vs Damage ​

When equippedMeleeAsset.isRepair is true:

  • Negative damage (positive isRepair flag in DamageTool.damage()).
  • Uses config's *_Repair_Multiplier instead of *_Damage_Multiplier.
  • Gated by isRepairable and hp < 100 max.

Weapon Quality Degradation ​

csharp
if (ShouldWeaponTakeDamage && quality > 0 && Random.value < durability)
    quality -= wear;

Only triggers when the raycast hits something (info.type != NONE && type != SKIP).

XP and Kill Rewards ​

PLAYER kill in ARENA: askPay(100)
ZOMBIE kill in HORDE: askPay(25) body, askPay(50) headshot
ZOMBIE hit in HORDE: askPay(5) body, askPay(10) headshot
General: sendStat(kill), askPay(xp)

Quest rewards for weak/strong attacks are granted from equippedMeleeAsset.weakAttackQuestRewards and strongAttackQuestRewards.


Resource Harvesting Details ​

Resource interaction uses ResourceManager for region lookup and damage:

csharp
byte x, y;
ushort index;
ResourceManager.tryGetRegion(info.transform, out x, out y, out index);
ResourceSpawnpoint spawnpoint = ResourceManager.getResourceSpawnpoint(x, y, index);
bool vulnerable = spawnpoint.asset.vulnerableToAllMeleeWeapons || equippedMeleeAsset.hasBladeID(spawnpoint.asset.bladeID);

hasBladeID checks the melee weapon's bladeID array for a match with the resource asset's required blade ID. This determines which tools can harvest which resources (e.g., pickaxe for ore, axe for trees).


Light System ​

When equippedMeleeAsset.isLight is true:

  • State [0] stores toggle (0 = off, 1 = on).
  • firstLightHook / thirdLightHook transforms control visual light model.
  • player.enableItemSpotLight(lightConfig) / player.disableItemSpotLight().
  • Client-side firstFakeLight mirrors third-person light position for first-person view.
  • Toggled via askInteractMelee/ReceiveInteractMelee (rate-limited 10 Hz).

Repeated Weapons (Chainsaw/Grinder) ​

Animation ​

  • Uses Start_Swing / Stop_Swing instead of Weak / Strong.
  • Weak and strong attack length seconds come from Start_Swing and Stop_Swing respectively.

Continuous Effects ​

csharp
if (Time.realtimeSinceStartup - startedSwing > 0.1)
{
    firstEmitter?.Emit(4);   // particle hit sparks
    thirdEmitter?.Emit(4);
    playSound(asset.use, volume);  // continuous use sound
    startedSwing = Time.realtimeSinceStartup;
}

Viewmodel Shake (non-repair only) ​

csharp
viewmodelCameraLocalPositionOffset = new Vector3(Random.Range(-0.05f, 0.05f), ...);

Sound Timing ​

For repeated weapons, the playUseSoundTime is set but not explicitly used — instead, the tick() loop handles sound every 100ms.


Aggressor Detection ​

csharp
if (info.type != PLAYER && info.type != ZOMBIE && info.type != ANIMAL)
{
    if (!player.life.isAggressor)
    {
        float bulletRange = range + Ray_Aggressor_Distance;
        float rayAggressor = Ray_Aggressor_Distance;
        Vector3 bulletNorm = aim.forward;
        for each enemy:
            enemyOffset = enemy.aim - player.aim;
            bulletProj = Project(enemyOffset, bulletNorm);
            if (proj.mag < bulletRange && (proj - enemy).mag < rayAggressor)
                markAggressive(false);
    }
}

Determines if the melee attack was near another player, marking the attacker as aggressive for PvP flagging.


Impact Effects ​

ServerSpawnMeleeImpact sends to nearby clients:

csharp
SendSpawnMeleeImpact(position + normal * random(0.04, 0.06), normal, materialName, colliderTransform)

Client-side ReceiveSpawnMeleeImpact:

  1. DamageTool.LocalSpawnBulletImpactEffect (decals, particles).
  2. DamageTool.PlayMeleeImpactAudio (material-based sound).
  3. equippedMeleeAsset.impactAudio overridable by mythical skin specialAudioOverride.

Melee Event Hooks ​

During equip():

  • Animator events on "Weak", "Strong", "Start_Swing", "Stop_Swing" may trigger UseableEventHook components on the first/third models.
  • onInspectStarted is fired from PlayerEquipment.inspect().

Player Life Integration — Stamina and Damage ​

Stamina Consumption (Strong Attack) ​

csharp
if (player.life.stamina >= equippedMeleeAsset.stamina * (1f - (Exercise_mastery * 0.75f)))
{
    player.life.askTire((byte)(equippedMeleeAsset.stamina * (1f - (Exercise_mastery * 0.5f))));
    // Proceed with strong attack
}

Two different mastery formulas are at play:

  • Gate (minimum required stamina): stamina * (1 - mastery * 0.75) — 25% penalty at max skill.
  • Consumption (stamina actually subtracted): stamina * (1 - mastery * 0.5) — 50% reduction at max skill.

At max Exercise (level 5, mastery 1.0): a weapon with stamina = 30 requires 30 * 0.25 = 7.5 stamina and consumes 30 * 0.5 = 15 stamina.

Health Damage Application ​

csharp
DamagePlayerParameters parameters = DamagePlayerParameters.make(info.player, EDeathCause.MELEE,
    info.direction, multiplier, info.limb);
parameters.killer = channel.owner.playerID.steamID;
parameters.times = times;
parameters.respectArmor = true;
parameters.trackKill = true;
parameters.ragdollEffect = ragdollEffect;

The respectArmor flag means the melee damage passes through the target's armor calculations (vest damage reduction). trackKill enables kill stat tracking for the weapon.

Zombie Damage ​

csharp
EZombieStunOverride stunOverride = equippedMeleeAsset.zombieStunOverride;
if (Provider.modeConfigData.Zombies.Only_Critical_Stuns && stunOverride == EZombieStunOverride.None)
{
    if (swingMode == ESwingMode.STRONG)
        stunOverride = EZombieStunOverride.Always;
}

When Only_Critical_Stuns is enabled, only strong attacks to the skull cause zombie stuns. Otherwise, the melee asset's zombieStunOverride controls stun behavior.


Animator Integration ​

Animation Playback ​

MethodAnimation PlayedConditions
equip()"Equip" (blended, true)Always on equip
startPrimary() (repeated)"Start_Swing"isSwinging == false
stopPrimary() (repeated)"Stop_Swing"isSwinging == true
startPrimary() (non-repeated)"Weak"Must be isUseable
startSecondary()"Strong"Must have enough stamina
swing()"Weak" or "Strong"Internal animation utility

Animation Length Calculation ​

csharp
weakAttackAnimLengthSeconds = player.animator.GetAnimationLength("Weak");
strongAttackAnimLengthSeconds = player.animator.GetAnimationLength("Strong");
weakAttackAnimLengthFrames = (uint)(weakAttackAnimLengthSeconds / PlayerInput.RATE);
strongAttackAnimLengthFrames = (uint)(strongAttackAnimLengthSeconds / PlayerInput.RATE);

PlayerInput.RATE is the simulation tick rate (0.02s = 50 ticks/second). The frame counts are used for server-side animation timing.

For repeated weapons:

csharp
weakAttackAnimLengthSeconds = player.animator.GetAnimationLength("Start_Swing");
strongAttackAnimLengthSeconds = player.animator.GetAnimationLength("Stop_Swing");

Sound Timing ​

csharp
// Non-repeated weak swing
playUseSoundTime = Time.timeAsDouble + weakAttackAnimLengthSeconds * equippedMeleeAsset.weak;
// At playUseSoundTime in tick():
playSound(asset.use, 0.5f);
isSwinging = false;

The asset.weak multiplier (typically 0.3–0.5) determines at what fraction of the swing animation the impact sound plays. This should align with the isDamageable window:

csharp
isDamageable = simulation - startedUse > weakAttackAnimLengthFrames * asset.weak;

Both use the same multiplier, ensuring sound and damage timing are consistent.


Particle Effects ​

Non-Repeated Weapons ​

No particle effects are emitted by the UseableMelee base logic. Impact effects are handled by DamageTool.ServerSpawnBulletImpact which is called indirectly through ReceiveSpawnMeleeImpact.

Repeated Weapons ​

Chainsaw-type weapons emit sparks continuously:

csharp
if (Time.realtimeSinceStartup - startedSwing > 0.1)
{
    startedSwing = Time.realtimeSinceStartup;
    if (firstEmitter != null && perspective == FIRST)
        firstEmitter.Emit(4);  // 4 particles per emission
    if (thirdEmitter != null && (!isLocalPlayer || perspective == THIRD))
        thirdEmitter.Emit(4);
}

The Hit transform on the melee weapon model locates the ParticleSystem used for spark effects. The 100ms emission interval provides a continuous spark stream without overloading the particle system.


Vehicle Repair Mechanics ​

Repair Validation ​

csharp
// Server-side only
if (equippedMeleeAsset.isRepair)
{
    if (!info.vehicle.isExploded && !info.vehicle.isRepaired && info.vehicle.canPlayerRepair(player))
    {
        times *= 1f + Mechanic_mastery; // Up to 2x repair at max skill
        DamageTool.damage(vehicle, true, point, true, vehicleDamage,
            times * Melee_Repair_Multiplier, true, ...);
    }
}

The DamageTool.damage overload with isRepair=true:

  • Heals the vehicle instead of damaging it.
  • Applies Melee_Repair_Multiplier from config (typically 0.1–0.5 per hit).
  • Mechanic skill provides up to 2× multiplier.

Sentry Damage Alert ​

When damaging a barricade that is a sentry (InteractableSentry):

csharp
if (barricade.interactable is InteractableSentry sentry)
{
    sentry.AlertDamagedBy(player);
}

The sentry marks the player as a hostile target and begins tracking them.


Object Rubble Destruction ​

Rubble destruction requires blade ID matching:

csharp
InteractableObjectRubble rubble = info.transform.GetComponentInParent<InteractableObjectRubble>();
if (rubble != null && rubble.IsSectionIndexValid(info.section)
    && !rubble.isSectionDead(info.section)
    && equippedMeleeAsset.hasBladeID(rubble.asset.rubbleBladeID))
{
    if (rubble.asset.rubbleIsVulnerable || weapon.isInvulnerable)
    {
        DamageTool.damage(rubble.transform, direction, section, objectDamage, times, ...);
    }
}

Key checks:

  1. Rubble exists and section is valid/alive.
  2. Melee weapon's blade IDs include the rubble's required rubbleBladeID.
  3. Rubble is flagged as vulnerable OR the weapon is flagged as invulnerable-breaking.

Repair Mode — for Barricades and Structures ​

When equippedMeleeAsset.isRepair is true and a barricade/structure is hit:

csharp
// Barricade repair
if (asset.isRepairable)
{
    times *= 1f + Mechanic_mastery;
    DamageTool.damage(transform, true, barricadeDamage, times * Melee_Repair_Multiplier, ...);
}

The true second parameter in DamageTool.damage signals an additive (healing) operation. The repair amount is:

repairAmount = barricadeDamage * times * Melee_Repair_Multiplier

With Mechanic skill at max (mastery 1.0): repairAmount = barricadeDamage * 2.0 * configMultiplier.

Repair Rate Limiting ​

Repair is only applied when:

  • The object is isRepairable.
  • HP is below 100 (full health objects return isRepaired = false and are skipped).
  • canPlayerRepair(player) returns true (ownership check).

Horde Mode XP Rewards ​

In Horde mode (Level.info.type == ELevelType.HORDE):

csharp
if (info.zombie != null)
{
    if (info.limb == ELimb.SKULL)
        player.skills.askPay(10);   // 10 XP per headshot
    else
        player.skills.askPay(5);    // 5 XP per body hit
}

if (kill == EPlayerKill.ZOMBIE)
{
    if (info.limb == ELimb.SKULL)
        player.skills.askPay(50);   // 50 XP for headshot kill
    else
        player.skills.askPay(25);   // 25 XP for body kill
}

XP rewards stack: a headshot kill awards 10 (hit) + 50 (kill) = 60 total XP before the Experience_Multiplier.


Network Protocol ​

Swing Animation Replication ​

Server → Client (non-owner, unreliable):
  SendPlaySwing(ESwingMode)   -- "Weak" or "Strong" animation
  SendPlaySwingStart()        -- "Start_Swing" (repeated weapons)
  SendPlaySwingStop()         -- "Stop_Swing" (repeated weapons)

All swing RPCs are gated by IsEquipAnimationFinished on the receiving client to prevent animation glitches during equip transitions.

Impact Effect Replication ​

Server → Client (in range, unreliable):
  SendSpawnMeleeImpact(Vector3 position, Vector3 normal, string materialName, Transform colliderTransform)

The receiving client calls:

  1. DamageTool.LocalSpawnBulletImpactEffect — spawns decal and particle system appropriate for the material.
  2. DamageTool.PlayMeleeImpactAudio — plays material-appropriate impact sound.
  3. Plays special audio override (mythical skin or asset's impactAudio).

Client-Server Interaction — InputInfo ​

The client sends raycast results to the server via player.input.sendRaycast(info, ERaycastInfoUsage.Melee). The server retrieves this in fire() with:

csharp
InputInfo info = player.input.getInput(true, ERaycastInfoUsage.Melee);

The consume=true parameter marks the input as consumed — each attack consumes exactly one raycast input. The ERaycastInfoUsage.Melee tag allows the input system to differentiate melee inputs from gun, consumable, or detonator inputs.


Edge Cases and Safeguards ​

  • Server distance validation: Hit point must be within range + 4m squared distance. Any desync beyond this is rejected.
  • Invulnerable buildings: Respects asset.isVulnerable and weapon.isInvulnerable flags for barricades/structures/rubble.
  • Fake lag penalty: If player.input.IsUnderFakeLagPenalty, damage is multiplied by Fake_Lag_Damage_Penalty_Multiplier.
  • isRepair gate: Repair weapons only affect objects at < 100 HP that are isRepairable.
  • Flesh FX toggle: When !allowFleshFx, blood effects are suppressed by overriding info.material = NONE.
  • Inspect: canInspect returns false while isUsing or isSwinging to prevent animation conflicts.