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.