using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.CombatMode; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Physics; using Content.Shared.Popups; using Content.Shared.Weapons.Melee.Components; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using ItemToggleMeleeWeaponComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleMeleeWeaponComponent; using Content.Shared.Damage.Components; namespace Content.Shared.Weapons.Melee; public abstract class SharedMeleeWeaponSystem : EntitySystem { [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; [Dependency] protected readonly ActionBlockerSystem Blocker = default!; [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!; [Dependency] protected readonly DamageableSystem Damageable = default!; [Dependency] protected readonly SharedInteractionSystem Interaction = default!; [Dependency] protected readonly IMapManager MapManager = default!; [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly MeleeSoundSystem _meleeSound = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly StaminaSystem _stamina = default!; private const int AttackMask = (int)(CollisionGroup.MobMask | CollisionGroup.Opaque); /// /// Maximum amount of targets allowed for a wide-attack. /// public const int MaxTargets = 5; /// /// If an attack is released within this buffer it's assumed to be full damage. /// public const float GracePeriod = 0.05f; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMeleeSelected); SubscribeLocalEvent(OnMeleeShotAttempted); SubscribeLocalEvent(OnMeleeShot); SubscribeLocalEvent(OnGetBonusMeleeDamage); SubscribeLocalEvent(OnGetBonusHeavyDamageModifier); SubscribeLocalEvent(OnGetBonusMeleeAttackRate); SubscribeLocalEvent(OnItemToggle); SubscribeAllEvent(OnHeavyAttack); SubscribeAllEvent(OnLightAttack); SubscribeAllEvent(OnDisarmAttack); SubscribeAllEvent(OnStopAttack); #if DEBUG SubscribeLocalEvent(OnMapInit); } private void OnMapInit(EntityUid uid, MeleeWeaponComponent component, MapInitEvent args) { if (component.NextAttack > Timing.CurTime) Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(uid)}"); #endif } private void OnMeleeShotAttempted(EntityUid uid, MeleeWeaponComponent comp, ref ShotAttemptedEvent args) { if (comp.NextAttack > Timing.CurTime) args.Cancel(); } private void OnMeleeShot(EntityUid uid, MeleeWeaponComponent component, ref GunShotEvent args) { if (!TryComp(uid, out var gun)) return; if (gun.NextFire > component.NextAttack) { component.NextAttack = gun.NextFire; Dirty(uid, component); } } private void OnMeleeSelected(EntityUid uid, MeleeWeaponComponent component, HandSelectedEvent args) { var attackRate = GetAttackRate(uid, args.User, component); if (attackRate.Equals(0f)) return; if (!component.ResetOnHandSelected) return; if (Paused(uid)) return; // If someone swaps to this weapon then reset its cd. var curTime = Timing.CurTime; var minimum = curTime + TimeSpan.FromSeconds(1 / attackRate); if (minimum < component.NextAttack) return; component.NextAttack = minimum; Dirty(uid, component); } private void OnGetBonusMeleeDamage(EntityUid uid, BonusMeleeDamageComponent component, ref GetMeleeDamageEvent args) { if (component.BonusDamage != null) args.Damage += component.BonusDamage; if (component.DamageModifierSet != null) args.Modifiers.Add(component.DamageModifierSet); } private void OnGetBonusHeavyDamageModifier(EntityUid uid, BonusMeleeDamageComponent component, ref GetHeavyDamageModifierEvent args) { args.DamageModifier += component.HeavyDamageFlatModifier; args.Multipliers *= component.HeavyDamageMultiplier; } private void OnGetBonusMeleeAttackRate(EntityUid uid, BonusMeleeAttackRateComponent component, ref GetMeleeAttackRateEvent args) { args.Rate += component.FlatModifier; args.Multipliers *= component.Multiplier; } private void OnStopAttack(StopAttackEvent msg, EntitySessionEventArgs args) { var user = args.SenderSession.AttachedEntity; if (user == null) return; if (!TryGetWeapon(user.Value, out var weaponUid, out var weapon) || weaponUid != GetEntity(msg.Weapon)) { return; } if (!weapon.Attacking) return; weapon.Attacking = false; Dirty(weaponUid, weapon); } private void OnLightAttack(LightAttackEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } user) return; if (!TryGetWeapon(user, out var weaponUid, out var weapon) || weaponUid != GetEntity(msg.Weapon)) { return; } AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } private void OnHeavyAttack(HeavyAttackEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } user) return; if (!TryGetWeapon(user, out var weaponUid, out var weapon) || weaponUid != GetEntity(msg.Weapon) || !weapon.CanWideSwing) // Goobstation Change { return; } AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } private void OnDisarmAttack(DisarmAttackEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } user) return; if (TryGetWeapon(user, out var weaponUid, out var weapon)) AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } /// /// Gets the total damage a weapon does, including modifiers like wielding and enablind/disabling /// public DamageSpecifier GetDamage(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null) { if (!Resolve(uid, ref component, false)) return new DamageSpecifier(); var ev = new GetMeleeDamageEvent(uid, new(component.Damage * Damageable.UniversalMeleeDamageModifier), new(), user, component.ResistanceBypass); RaiseLocalEvent(uid, ref ev); return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers); } public float GetAttackRate(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null) { if (!Resolve(uid, ref component)) return 0; var ev = new GetMeleeAttackRateEvent(uid, component.AttackRate, 1, user); RaiseLocalEvent(uid, ref ev); return ev.Rate * ev.Multipliers; } public FixedPoint2 GetHeavyDamageModifier(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null) { if (!Resolve(uid, ref component)) return FixedPoint2.Zero; var ev = new GetHeavyDamageModifierEvent(uid, component.ClickDamageModifier, 1, user); RaiseLocalEvent(uid, ref ev); return ev.DamageModifier * ev.Multipliers; } public bool GetResistanceBypass(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null) { if (!Resolve(uid, ref component)) return false; var ev = new GetMeleeDamageEvent(uid, new(component.Damage * Damageable.UniversalMeleeDamageModifier), new(), user, component.ResistanceBypass); RaiseLocalEvent(uid, ref ev); return ev.ResistanceBypass; } public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen(true)] out MeleeWeaponComponent? melee) { weaponUid = default; melee = null; var ev = new GetMeleeWeaponEvent(); RaiseLocalEvent(entity, ev); if (ev.Handled) { if (TryComp(ev.Weapon, out melee)) { weaponUid = ev.Weapon.Value; return true; } return false; } // Use inhands entity if we got one. if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) && hands.ActiveHandEntity is { } held) { // Make sure the entity is a weapon AND it doesn't need // to be equipped to be used (E.g boxing gloves). if (EntityManager.TryGetComponent(held, out melee) && !melee.MustBeEquippedToUse) { weaponUid = held; return true; } if (!HasComp(held)) return false; } // Use hands clothing if applicable. if (_inventory.TryGetSlotEntity(entity, "gloves", out var gloves) && TryComp(gloves, out var glovesMelee)) { weaponUid = gloves.Value; melee = glovesMelee; return true; } // Use our own melee if (TryComp(entity, out melee)) { weaponUid = entity; return true; } return false; } public void AttemptLightAttackMiss(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityCoordinates coordinates) { AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(null, GetNetEntity(weaponUid), GetNetCoordinates(coordinates)), null); } public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { if (!TryComp(target, out TransformComponent? targetXform)) return false; return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null); } public bool AttemptDisarmAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { if (!TryComp(target, out TransformComponent? targetXform)) return false; return AttemptAttack(user, weaponUid, weapon, new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(targetXform.Coordinates)), null); } /// /// Called when a windup is finished and an attack is tried. /// /// True if attack successful private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session) { var curTime = Timing.CurTime; if (weapon.NextAttack > curTime) return false; if (!CombatMode.IsInCombatMode(user)) return false; EntityUid? target = null; switch (attack) { case LightAttackEvent light: if (light.Target != null && !TryGetEntity(light.Target, out target)) { // Target was lightly attacked & deleted. return false; } if (!Blocker.CanAttack(user, target, (weaponUid, weapon))) return false; // Can't self-attack if you're the weapon if (weaponUid == target) return false; break; case DisarmAttackEvent disarm: if (disarm.Target != null && !TryGetEntity(disarm.Target, out target)) { // Target was lightly attacked & deleted. return false; } if (!Blocker.CanAttack(user, target, (weaponUid, weapon), true)) return false; break; default: if (!Blocker.CanAttack(user, weapon: (weaponUid, weapon))) return false; break; } // Windup time checked elsewhere. var fireRate = TimeSpan.FromSeconds(1f / GetAttackRate(weaponUid, user, weapon)); var swings = 0; // TODO: If we get autoattacks then probably need a shotcounter like guns so we can do timing properly. if (weapon.NextAttack < curTime) weapon.NextAttack = curTime; while (weapon.NextAttack <= curTime) { weapon.NextAttack += fireRate; swings++; } Dirty(weaponUid, weapon); // Do this AFTER attack so it doesn't spam every tick var ev = new AttemptMeleeEvent(); RaiseLocalEvent(weaponUid, ref ev); if (ev.Cancelled) { if (ev.Message != null) { PopupSystem.PopupClient(ev.Message, weaponUid, user); } return false; } // Attack confirmed for (var i = 0; i < swings; i++) { string animation; switch (attack) { case LightAttackEvent light: if (!DoLightAttack(user, light, weaponUid, weapon, session)) return false; animation = weapon.Animation; break; case DisarmAttackEvent disarm: // DoDisarm already returns bool and is checked if (!DoDisarm(user, disarm, weaponUid, weapon, session)) return false; animation = weapon.Animation; break; case HeavyAttackEvent heavy: if (!DoHeavyAttack(user, heavy, weaponUid, weapon, session)) return false; animation = weapon.WideAnimation; break; default: throw new NotImplementedException(); } DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation); } var attackEv = new MeleeAttackEvent(weaponUid); RaiseLocalEvent(user, ref attackEv); weapon.Attacking = true; return true; } protected abstract bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session); protected virtual bool DoLightAttack(EntityUid user, LightAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { // If I do not come back later to fix Light Attacks being Heavy Attacks you can throw me in the spider pit -Errant var damage = GetDamage(meleeUid, user, component) * GetHeavyDamageModifier(meleeUid, user, component); var target = GetEntity(ev.Target); var resistanceBypass = GetResistanceBypass(meleeUid, user, component); //do a stamina check before attacking if (TryComp(user, out var stam)) { if (stam.StaminaDamage > stam.SlowdownThreshold) { return false; } } // For consistency with wide attacks stuff needs damageable. if (Deleted(target) || !HasComp(target) || !TryComp(target, out TransformComponent? targetXform) || // Not in LOS. !InRange(user, target.Value, component.Range, session)) { // Leave IsHit set to true, because the only time it's set to false // is when a melee weapon is examined. Misses are inferred from an // empty HitEntities. // TODO: This needs fixing if (meleeUid == user) { AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (light) using their hands and missed"); } else { AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed"); } var missEvent = new MeleeHitEvent(new List(), user, meleeUid, damage, null); RaiseLocalEvent(meleeUid, missEvent); _meleeSound.PlaySwingSound(user, meleeUid, component); return true; } // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}"); // Raise event before doing damage so we can cancel damage if the event is handled var hitEvent = new MeleeHitEvent(new List { target.Value }, user, meleeUid, damage, null); RaiseLocalEvent(meleeUid, hitEvent); if (hitEvent.Handled) return true; var targets = new List(1) { target.Value }; var weapon = GetEntity(ev.Weapon); // We skip weapon -> target interaction, as forensics system applies DNA on hit Interaction.DoContactInteraction(user, weapon); // If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a // somewhat messy scuffle. See also, heavy attacks. Interaction.DoContactInteraction(user, target); // For stuff that cares about it being attacked. var attackedEvent = new AttackedEvent(meleeUid, user, targetXform.Coordinates); RaiseLocalEvent(target.Value, attackedEvent); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList); var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin: user, ignoreResistances: resistanceBypass); //drain stamina from the attacker as well //hardcoded at 10 for now, TODO: Make it take into account the weapons weight? _stamina.TakeStaminaDamage(user, 10f, visual: false, source: user, with: meleeUid == user ? null : meleeUid, immediate: false); if (damageResult is { Empty: false }) { // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage)) { _stamina.TakeStaminaDamage(target.Value, (bluntDamage * component.BluntStaminaDamageFactor).Float(), visual: false, source: user, with: meleeUid == user ? null : meleeUid, immediate: false); } if (meleeUid == user) { AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using their hands and dealt {damageResult.GetTotal():damage} damage"); } else { AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage"); } } _meleeSound.PlayHitSound(target.Value, user, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, component); if (damageResult?.GetTotal() > FixedPoint2.Zero) { DoDamageEffect(targets, user, targetXform); } return true; } protected abstract void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform); private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { // TODO: This is copy-paste as fuck with DoPreciseAttack if (!TryComp(user, out TransformComponent? userXform)) return false; var targetMap = TransformSystem.ToMapCoordinates(GetCoordinates(ev.Coordinates)); if (targetMap.MapId != userXform.MapID) return false; //do a stamina check before attacking if (TryComp(user, out var stam)) { if (stam.StaminaDamage > stam.SlowdownThreshold) { return false; } } var userPos = TransformSystem.GetWorldPosition(userXform); var direction = targetMap.Position - userPos; var distance = Math.Min(component.Range, direction.Length()); var damage = GetDamage(meleeUid, user, component); var entities = GetEntityList(ev.Entities); //drain stamina from the attacker as well //hardcoded at 20 for now, TODO: Make it take into account the weapons weight? _stamina.TakeStaminaDamage(user, 20f, visual: false, source: user, with: meleeUid == user ? null : meleeUid, immediate: false); if (entities.Count == 0) { if (meleeUid == user) { AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (heavy) using their hands and missed"); } else { AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed"); } var missEvent = new MeleeHitEvent(new List(), user, meleeUid, damage, direction); RaiseLocalEvent(meleeUid, missEvent); // immediate audio feedback _meleeSound.PlaySwingSound(user, meleeUid, component); return true; } // Naughty input if (entities.Count > MaxTargets) { entities.RemoveRange(MaxTargets, entities.Count - MaxTargets); } // Validate client for (var i = entities.Count - 1; i >= 0; i--) { if (ArcRaySuccessful(entities[i], userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user, session)) { continue; } // Bad input entities.RemoveAt(i); } var targets = new List(); var damageQuery = GetEntityQuery(); foreach (var entity in entities) { if (entity == user || !damageQuery.HasComponent(entity)) continue; targets.Add(entity); } // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}"); // Raise event before doing damage so we can cancel damage if the event is handled var hitEvent = new MeleeHitEvent(targets, user, meleeUid, damage, direction); RaiseLocalEvent(meleeUid, hitEvent); if (hitEvent.Handled) return true; var weapon = GetEntity(ev.Weapon); Interaction.DoContactInteraction(user, weapon); // For stuff that cares about it being attacked. foreach (var target in targets) { // We skip weapon -> target interaction, as forensics system applies DNA on hit // If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a // somewhat messy scuffle. See also, light attacks. Interaction.DoContactInteraction(user, target); } var appliedDamage = new DamageSpecifier(); for (var i = targets.Count - 1; i >= 0; i--) { var entity = targets[i]; // We raise an attack attempt here as well, // primarily because this was an untargeted wideswing: if a subscriber to that event cared about // the potential target (such as for pacifism), they need to be made aware of the target here. // In that case, just continue. if (!Blocker.CanAttack(user, entity, (weapon, component))) { targets.RemoveAt(i); continue; } var attackedEvent = new AttackedEvent(meleeUid, user, GetCoordinates(ev.Coordinates)); RaiseLocalEvent(entity, attackedEvent); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList); var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin: user, canEvade: true, partMultiplier: component.HeavyPartDamageMultiplier); // Shitmed Change // Goobstation if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero) { // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage)) { _stamina.TakeStaminaDamage(entity, (bluntDamage * component.BluntStaminaDamageFactor).Float(), visual: false, source: user, with: meleeUid == user ? null : meleeUid, immediate: false); } appliedDamage += damageResult; if (meleeUid == user) { AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.GetTotal():damage} damage"); } else { AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage"); } } } if (entities.Count != 0) { var target = entities.First(); _meleeSound.PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component); } if (appliedDamage.GetTotal() > FixedPoint2.Zero) { DoDamageEffect(targets, user, Transform(targets[0])); } return true; } protected HashSet ArcRayCast(Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId, EntityUid ignore) { // TODO: This is pretty sucky. var widthRad = arcWidth; var increments = 1 + 35 * (int)Math.Ceiling(widthRad / (2 * Math.PI)); var increment = widthRad / increments; var baseAngle = angle - widthRad / 2; var resSet = new HashSet(); for (var i = 0; i < increments; i++) { var castAngle = new Angle(baseAngle + increment * i); var res = _physics.IntersectRay(mapId, new CollisionRay(position, castAngle.ToWorldVec(), AttackMask), range, ignore, false) .ToList(); if (res.Count != 0) { // If there's exact distance overlap, we simply have to deal with all overlapping objects to avoid selecting randomly. var resChecked = res.Where(x => x.Distance.Equals(res[0].Distance)); foreach (var r in resChecked) { if (Interaction.InRangeUnobstructed(ignore, r.HitEntity, range + 0.1f, overlapCheck: false)) resSet.Add(r.HitEntity); } } } return resSet; } protected virtual bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId, EntityUid ignore, ICommonSession? session) { // Only matters for server. return true; } public static string? GetHighestDamageSound(DamageSpecifier modifiedDamage, IPrototypeManager protoManager) { var groups = modifiedDamage.GetDamagePerGroup(protoManager); // Use group if it's exclusive, otherwise fall back to type. if (groups.Count == 1) { return groups.Keys.First(); } var highestDamage = FixedPoint2.Zero; string? highestDamageType = null; foreach (var (type, damage) in modifiedDamage.DamageDict) { if (damage <= highestDamage) continue; highestDamageType = type; } return highestDamageType; } protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { var target = GetEntity(ev.Target); if (Deleted(target) || user == target) { return false; } // Play a sound to give instant feedback; same with playing the animations _meleeSound.PlaySwingSound(user, meleeUid, component); return true; } private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation) { // TODO: Assert that offset eyes are still okay. if (!TryComp(user, out TransformComponent? userXform)) return; var invMatrix = TransformSystem.GetInvWorldMatrix(userXform); var localPos = Vector2.Transform(coordinates.Position, invMatrix); if (localPos.LengthSquared() <= 0f) return; localPos = userXform.LocalRotation.RotateVec(localPos); // We'll play the effect just short visually so it doesn't look like we should be hitting but actually aren't. const float bufferLength = 0.2f; var visualLength = length - bufferLength; if (localPos.Length() > visualLength) localPos = localPos.Normalized() * visualLength; DoLunge(user, weapon, angle, localPos, animation); } public abstract void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true); /// /// Used to update the MeleeWeapon component on item toggle. /// private void OnItemToggle(EntityUid uid, ItemToggleMeleeWeaponComponent itemToggleMelee, ItemToggledEvent args) { if (!TryComp(uid, out MeleeWeaponComponent? meleeWeapon)) return; if (args.Activated) { if (itemToggleMelee.ActivatedDamage != null) { //Setting deactivated damage to the weapon's regular value before changing it. itemToggleMelee.DeactivatedDamage ??= meleeWeapon.Damage; meleeWeapon.Damage = itemToggleMelee.ActivatedDamage; } meleeWeapon.HitSound = itemToggleMelee.ActivatedSoundOnHit; if (itemToggleMelee.ActivatedSoundOnHitNoDamage != null) { //Setting the deactivated sound on no damage hit to the weapon's regular value before changing it. itemToggleMelee.DeactivatedSoundOnHitNoDamage ??= meleeWeapon.NoDamageSound; meleeWeapon.NoDamageSound = itemToggleMelee.ActivatedSoundOnHitNoDamage; } if (itemToggleMelee.ActivatedSoundOnSwing != null) { //Setting the deactivated sound on no damage hit to the weapon's regular value before changing it. itemToggleMelee.DeactivatedSoundOnSwing ??= meleeWeapon.SwingSound; meleeWeapon.SwingSound = itemToggleMelee.ActivatedSoundOnSwing; } if (itemToggleMelee.DeactivatedSecret) meleeWeapon.Hidden = false; } else { if (itemToggleMelee.DeactivatedDamage != null) meleeWeapon.Damage = itemToggleMelee.DeactivatedDamage; meleeWeapon.HitSound = itemToggleMelee.DeactivatedSoundOnHit; if (itemToggleMelee.DeactivatedSoundOnHitNoDamage != null) meleeWeapon.NoDamageSound = itemToggleMelee.DeactivatedSoundOnHitNoDamage; if (itemToggleMelee.DeactivatedSoundOnSwing != null) meleeWeapon.SwingSound = itemToggleMelee.DeactivatedSoundOnSwing; if (itemToggleMelee.DeactivatedSecret) meleeWeapon.Hidden = true; } Dirty(uid, meleeWeapon); } }