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:
| Mode | primary/secondary | Damage multiplier | Use | Stamina cost |
|---|---|---|---|---|
| WEAK | startPrimary() | 1× base | Regular attack | None |
| STRONG | startSecondary() | equippedMeleeAsset.strength | Power attack | stamina * (1 - Exercise_mastery * 0.75) |
Primary Attack (startPrimary)
For repeated melee weapons (chainsaw, angle grinder):
- Toggles
isSwingingon (start) and off (stop). - Plays
Start_Swing/Stop_Swinganimation. - Continues dealing damage each simulate tick while swinging.
For non-repeated weapons:
- Must be
isUseable(previous animation finished) to initiate. - Sets
isBusy = true, playsWeakanimation viaswing().
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, playsStronganimation.
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
| Entity | Damage Calculation |
|---|---|
| PLAYER | playerDamageMultiplier × times × config multiplier; respects armor, backstab; bypassAllowedToDamagePlayer flag skips PvP check |
| ZOMBIE | zombieOrPlayerDamageMultiplier × times × zombie armor; allowBackstab = true; stun override configurable (critical-only, always, never) |
| ANIMAL | animalOrPlayerDamageMultiplier × times × armor falloff |
| VEHICLE | Repair: vehicleDamage * times * Melee_Repair_Multiplier * (1 + Mechanic_mastery) |
Damage: vehicleDamage * times * Melee_Damage_Multiplier (gated by canBeDamaged and isVulnerable) | |
| BARRICADE | Repair: same as vehicle, Melee_Repair_Multiplier from barricade config |
Damage: barricadeDamage * times * Melee_Damage_Multiplier; sentry alert on hit | |
| STRUCTURE | Repair/Damage: mirrored barricade pattern with structure-specific multipliers |
| RESOURCE | resourceDamage * 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
isRepairflag inDamageTool.damage()). - Uses config's
*_Repair_Multiplierinstead of*_Damage_Multiplier. - Gated by
isRepairableandhp < 100max.
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/thirdLightHooktransforms control visual light model.player.enableItemSpotLight(lightConfig)/player.disableItemSpotLight().- Client-side
firstFakeLightmirrors 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_Swinginstead ofWeak/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:
DamageTool.LocalSpawnBulletImpactEffect(decals, particles).DamageTool.PlayMeleeImpactAudio(material-based sound).equippedMeleeAsset.impactAudiooverridable by mythical skinspecialAudioOverride.
Melee Event Hooks
During equip():
- Animator events on
"Weak","Strong","Start_Swing","Stop_Swing"may triggerUseableEventHookcomponents on the first/third models. onInspectStartedis fired fromPlayerEquipment.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
| Method | Animation Played | Conditions |
|---|---|---|
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_Multiplierfrom 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:
- Rubble exists and section is valid/alive.
- Melee weapon's blade IDs include the rubble's required
rubbleBladeID. - 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_MultiplierWith 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 = falseand 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:
DamageTool.LocalSpawnBulletImpactEffect— spawns decal and particle system appropriate for the material.DamageTool.PlayMeleeImpactAudio— plays material-appropriate impact sound.- 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 + 4msquared distance. Any desync beyond this is rejected. - Invulnerable buildings: Respects
asset.isVulnerableandweapon.isInvulnerableflags for barricades/structures/rubble. - Fake lag penalty: If
player.input.IsUnderFakeLagPenalty, damage is multiplied byFake_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 overridinginfo.material = NONE. - Inspect:
canInspectreturns false whileisUsingorisSwingingto prevent animation conflicts.
