| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- 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);
- /// <summary>
- /// Maximum amount of targets allowed for a wide-attack.
- /// </summary>
- public const int MaxTargets = 5;
- /// <summary>
- /// If an attack is released within this buffer it's assumed to be full damage.
- /// </summary>
- public const float GracePeriod = 0.05f;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<MeleeWeaponComponent, HandSelectedEvent>(OnMeleeSelected);
- SubscribeLocalEvent<MeleeWeaponComponent, ShotAttemptedEvent>(OnMeleeShotAttempted);
- SubscribeLocalEvent<MeleeWeaponComponent, GunShotEvent>(OnMeleeShot);
- SubscribeLocalEvent<BonusMeleeDamageComponent, GetMeleeDamageEvent>(OnGetBonusMeleeDamage);
- SubscribeLocalEvent<BonusMeleeDamageComponent, GetHeavyDamageModifierEvent>(OnGetBonusHeavyDamageModifier);
- SubscribeLocalEvent<BonusMeleeAttackRateComponent, GetMeleeAttackRateEvent>(OnGetBonusMeleeAttackRate);
- SubscribeLocalEvent<ItemToggleMeleeWeaponComponent, ItemToggledEvent>(OnItemToggle);
- SubscribeAllEvent<HeavyAttackEvent>(OnHeavyAttack);
- SubscribeAllEvent<LightAttackEvent>(OnLightAttack);
- SubscribeAllEvent<DisarmAttackEvent>(OnDisarmAttack);
- SubscribeAllEvent<StopAttackEvent>(OnStopAttack);
- #if DEBUG
- SubscribeLocalEvent<MeleeWeaponComponent,
- MapInitEvent>(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<GunComponent>(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);
- }
- /// <summary>
- /// Gets the total damage a weapon does, including modifiers like wielding and enablind/disabling
- /// </summary>
- 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<VirtualItemComponent>(held))
- return false;
- }
- // Use hands clothing if applicable.
- if (_inventory.TryGetSlotEntity(entity, "gloves", out var gloves) &&
- TryComp<MeleeWeaponComponent>(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);
- }
- /// <summary>
- /// Called when a windup is finished and an attack is tried.
- /// </summary>
- /// <returns>True if attack successful</returns>
- 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<StaminaComponent>(user, out var stam))
- {
- if (stam.StaminaDamage > stam.SlowdownThreshold)
- { return false; }
- }
- // For consistency with wide attacks stuff needs damageable.
- if (Deleted(target) ||
- !HasComp<DamageableComponent>(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<EntityUid>(), 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<EntityUid> { target.Value }, user, meleeUid, damage, null);
- RaiseLocalEvent(meleeUid, hitEvent);
- if (hitEvent.Handled)
- return true;
- var targets = new List<EntityUid>(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<EntityUid> 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<StaminaComponent>(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<EntityUid>(), 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<EntityUid>();
- var damageQuery = GetEntityQuery<DamageableComponent>();
- 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<EntityUid> 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<EntityUid>();
- 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);
- /// <summary>
- /// Used to update the MeleeWeapon component on item toggle.
- /// </summary>
- 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);
- }
- }
|