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);
}
}