SharedMeleeWeaponSystem.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Shared.ActionBlocker;
  5. using Content.Shared.Administration.Logs;
  6. using Content.Shared.CombatMode;
  7. using Content.Shared.Damage;
  8. using Content.Shared.Damage.Systems;
  9. using Content.Shared.Database;
  10. using Content.Shared.FixedPoint;
  11. using Content.Shared.Hands;
  12. using Content.Shared.Hands.Components;
  13. using Content.Shared.Interaction;
  14. using Content.Shared.Inventory;
  15. using Content.Shared.Inventory.VirtualItem;
  16. using Content.Shared.Item.ItemToggle.Components;
  17. using Content.Shared.Physics;
  18. using Content.Shared.Popups;
  19. using Content.Shared.Weapons.Melee.Components;
  20. using Content.Shared.Weapons.Melee.Events;
  21. using Content.Shared.Weapons.Ranged.Components;
  22. using Content.Shared.Weapons.Ranged.Events;
  23. using Content.Shared.Weapons.Ranged.Systems;
  24. using Robust.Shared.Map;
  25. using Robust.Shared.Physics;
  26. using Robust.Shared.Physics.Systems;
  27. using Robust.Shared.Player;
  28. using Robust.Shared.Prototypes;
  29. using Robust.Shared.Timing;
  30. using ItemToggleMeleeWeaponComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleMeleeWeaponComponent;
  31. namespace Content.Shared.Weapons.Melee;
  32. public abstract class SharedMeleeWeaponSystem : EntitySystem
  33. {
  34. [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
  35. [Dependency] protected readonly ActionBlockerSystem Blocker = default!;
  36. [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!;
  37. [Dependency] protected readonly DamageableSystem Damageable = default!;
  38. [Dependency] protected readonly SharedInteractionSystem Interaction = default!;
  39. [Dependency] protected readonly IMapManager MapManager = default!;
  40. [Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
  41. [Dependency] protected readonly IGameTiming Timing = default!;
  42. [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
  43. [Dependency] private readonly InventorySystem _inventory = default!;
  44. [Dependency] private readonly MeleeSoundSystem _meleeSound = default!;
  45. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  46. [Dependency] private readonly IPrototypeManager _protoManager = default!;
  47. [Dependency] private readonly StaminaSystem _stamina = default!;
  48. private const int AttackMask = (int)(CollisionGroup.MobMask | CollisionGroup.Opaque);
  49. /// <summary>
  50. /// Maximum amount of targets allowed for a wide-attack.
  51. /// </summary>
  52. public const int MaxTargets = 5;
  53. /// <summary>
  54. /// If an attack is released within this buffer it's assumed to be full damage.
  55. /// </summary>
  56. public const float GracePeriod = 0.05f;
  57. public override void Initialize()
  58. {
  59. base.Initialize();
  60. SubscribeLocalEvent<MeleeWeaponComponent, HandSelectedEvent>(OnMeleeSelected);
  61. SubscribeLocalEvent<MeleeWeaponComponent, ShotAttemptedEvent>(OnMeleeShotAttempted);
  62. SubscribeLocalEvent<MeleeWeaponComponent, GunShotEvent>(OnMeleeShot);
  63. SubscribeLocalEvent<BonusMeleeDamageComponent, GetMeleeDamageEvent>(OnGetBonusMeleeDamage);
  64. SubscribeLocalEvent<BonusMeleeDamageComponent, GetHeavyDamageModifierEvent>(OnGetBonusHeavyDamageModifier);
  65. SubscribeLocalEvent<BonusMeleeAttackRateComponent, GetMeleeAttackRateEvent>(OnGetBonusMeleeAttackRate);
  66. SubscribeLocalEvent<ItemToggleMeleeWeaponComponent, ItemToggledEvent>(OnItemToggle);
  67. SubscribeAllEvent<HeavyAttackEvent>(OnHeavyAttack);
  68. SubscribeAllEvent<LightAttackEvent>(OnLightAttack);
  69. SubscribeAllEvent<DisarmAttackEvent>(OnDisarmAttack);
  70. SubscribeAllEvent<StopAttackEvent>(OnStopAttack);
  71. #if DEBUG
  72. SubscribeLocalEvent<MeleeWeaponComponent,
  73. MapInitEvent>(OnMapInit);
  74. }
  75. private void OnMapInit(EntityUid uid, MeleeWeaponComponent component, MapInitEvent args)
  76. {
  77. if (component.NextAttack > Timing.CurTime)
  78. Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(uid)}");
  79. #endif
  80. }
  81. private void OnMeleeShotAttempted(EntityUid uid, MeleeWeaponComponent comp, ref ShotAttemptedEvent args)
  82. {
  83. if (comp.NextAttack > Timing.CurTime)
  84. args.Cancel();
  85. }
  86. private void OnMeleeShot(EntityUid uid, MeleeWeaponComponent component, ref GunShotEvent args)
  87. {
  88. if (!TryComp<GunComponent>(uid, out var gun))
  89. return;
  90. if (gun.NextFire > component.NextAttack)
  91. {
  92. component.NextAttack = gun.NextFire;
  93. Dirty(uid, component);
  94. }
  95. }
  96. private void OnMeleeSelected(EntityUid uid, MeleeWeaponComponent component, HandSelectedEvent args)
  97. {
  98. var attackRate = GetAttackRate(uid, args.User, component);
  99. if (attackRate.Equals(0f))
  100. return;
  101. if (!component.ResetOnHandSelected)
  102. return;
  103. if (Paused(uid))
  104. return;
  105. // If someone swaps to this weapon then reset its cd.
  106. var curTime = Timing.CurTime;
  107. var minimum = curTime + TimeSpan.FromSeconds(1 / attackRate);
  108. if (minimum < component.NextAttack)
  109. return;
  110. component.NextAttack = minimum;
  111. Dirty(uid, component);
  112. }
  113. private void OnGetBonusMeleeDamage(EntityUid uid, BonusMeleeDamageComponent component, ref GetMeleeDamageEvent args)
  114. {
  115. if (component.BonusDamage != null)
  116. args.Damage += component.BonusDamage;
  117. if (component.DamageModifierSet != null)
  118. args.Modifiers.Add(component.DamageModifierSet);
  119. }
  120. private void OnGetBonusHeavyDamageModifier(EntityUid uid, BonusMeleeDamageComponent component, ref GetHeavyDamageModifierEvent args)
  121. {
  122. args.DamageModifier += component.HeavyDamageFlatModifier;
  123. args.Multipliers *= component.HeavyDamageMultiplier;
  124. }
  125. private void OnGetBonusMeleeAttackRate(EntityUid uid, BonusMeleeAttackRateComponent component, ref GetMeleeAttackRateEvent args)
  126. {
  127. args.Rate += component.FlatModifier;
  128. args.Multipliers *= component.Multiplier;
  129. }
  130. private void OnStopAttack(StopAttackEvent msg, EntitySessionEventArgs args)
  131. {
  132. var user = args.SenderSession.AttachedEntity;
  133. if (user == null)
  134. return;
  135. if (!TryGetWeapon(user.Value, out var weaponUid, out var weapon) ||
  136. weaponUid != GetEntity(msg.Weapon))
  137. {
  138. return;
  139. }
  140. if (!weapon.Attacking)
  141. return;
  142. weapon.Attacking = false;
  143. Dirty(weaponUid, weapon);
  144. }
  145. private void OnLightAttack(LightAttackEvent msg, EntitySessionEventArgs args)
  146. {
  147. if (args.SenderSession.AttachedEntity is not { } user)
  148. return;
  149. if (!TryGetWeapon(user, out var weaponUid, out var weapon) ||
  150. weaponUid != GetEntity(msg.Weapon))
  151. {
  152. return;
  153. }
  154. AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession);
  155. }
  156. private void OnHeavyAttack(HeavyAttackEvent msg, EntitySessionEventArgs args)
  157. {
  158. if (args.SenderSession.AttachedEntity is not { } user)
  159. return;
  160. if (!TryGetWeapon(user, out var weaponUid, out var weapon) ||
  161. weaponUid != GetEntity(msg.Weapon))
  162. {
  163. return;
  164. }
  165. AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession);
  166. }
  167. private void OnDisarmAttack(DisarmAttackEvent msg, EntitySessionEventArgs args)
  168. {
  169. if (args.SenderSession.AttachedEntity is not { } user)
  170. return;
  171. if (TryGetWeapon(user, out var weaponUid, out var weapon))
  172. AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession);
  173. }
  174. /// <summary>
  175. /// Gets the total damage a weapon does, including modifiers like wielding and enablind/disabling
  176. /// </summary>
  177. public DamageSpecifier GetDamage(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null)
  178. {
  179. if (!Resolve(uid, ref component, false))
  180. return new DamageSpecifier();
  181. var ev = new GetMeleeDamageEvent(uid, new(component.Damage * Damageable.UniversalMeleeDamageModifier), new(), user, component.ResistanceBypass);
  182. RaiseLocalEvent(uid, ref ev);
  183. return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers);
  184. }
  185. public float GetAttackRate(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null)
  186. {
  187. if (!Resolve(uid, ref component))
  188. return 0;
  189. var ev = new GetMeleeAttackRateEvent(uid, component.AttackRate, 1, user);
  190. RaiseLocalEvent(uid, ref ev);
  191. return ev.Rate * ev.Multipliers;
  192. }
  193. public FixedPoint2 GetHeavyDamageModifier(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null)
  194. {
  195. if (!Resolve(uid, ref component))
  196. return FixedPoint2.Zero;
  197. var ev = new GetHeavyDamageModifierEvent(uid, component.ClickDamageModifier, 1, user);
  198. RaiseLocalEvent(uid, ref ev);
  199. return ev.DamageModifier * ev.Multipliers;
  200. }
  201. public bool GetResistanceBypass(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null)
  202. {
  203. if (!Resolve(uid, ref component))
  204. return false;
  205. var ev = new GetMeleeDamageEvent(uid, new(component.Damage * Damageable.UniversalMeleeDamageModifier), new(), user, component.ResistanceBypass);
  206. RaiseLocalEvent(uid, ref ev);
  207. return ev.ResistanceBypass;
  208. }
  209. public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen(true)] out MeleeWeaponComponent? melee)
  210. {
  211. weaponUid = default;
  212. melee = null;
  213. var ev = new GetMeleeWeaponEvent();
  214. RaiseLocalEvent(entity, ev);
  215. if (ev.Handled)
  216. {
  217. if (TryComp(ev.Weapon, out melee))
  218. {
  219. weaponUid = ev.Weapon.Value;
  220. return true;
  221. }
  222. return false;
  223. }
  224. // Use inhands entity if we got one.
  225. if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) &&
  226. hands.ActiveHandEntity is { } held)
  227. {
  228. // Make sure the entity is a weapon AND it doesn't need
  229. // to be equipped to be used (E.g boxing gloves).
  230. if (EntityManager.TryGetComponent(held, out melee) &&
  231. !melee.MustBeEquippedToUse)
  232. {
  233. weaponUid = held;
  234. return true;
  235. }
  236. if (!HasComp<VirtualItemComponent>(held))
  237. return false;
  238. }
  239. // Use hands clothing if applicable.
  240. if (_inventory.TryGetSlotEntity(entity, "gloves", out var gloves) &&
  241. TryComp<MeleeWeaponComponent>(gloves, out var glovesMelee))
  242. {
  243. weaponUid = gloves.Value;
  244. melee = glovesMelee;
  245. return true;
  246. }
  247. // Use our own melee
  248. if (TryComp(entity, out melee))
  249. {
  250. weaponUid = entity;
  251. return true;
  252. }
  253. return false;
  254. }
  255. public void AttemptLightAttackMiss(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityCoordinates coordinates)
  256. {
  257. AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(null, GetNetEntity(weaponUid), GetNetCoordinates(coordinates)), null);
  258. }
  259. public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target)
  260. {
  261. if (!TryComp(target, out TransformComponent? targetXform))
  262. return false;
  263. return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null);
  264. }
  265. public bool AttemptDisarmAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target)
  266. {
  267. if (!TryComp(target, out TransformComponent? targetXform))
  268. return false;
  269. return AttemptAttack(user, weaponUid, weapon, new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(targetXform.Coordinates)), null);
  270. }
  271. /// <summary>
  272. /// Called when a windup is finished and an attack is tried.
  273. /// </summary>
  274. /// <returns>True if attack successful</returns>
  275. private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session)
  276. {
  277. var curTime = Timing.CurTime;
  278. if (weapon.NextAttack > curTime)
  279. return false;
  280. if (!CombatMode.IsInCombatMode(user))
  281. return false;
  282. EntityUid? target = null;
  283. switch (attack)
  284. {
  285. case LightAttackEvent light:
  286. if (light.Target != null && !TryGetEntity(light.Target, out target))
  287. {
  288. // Target was lightly attacked & deleted.
  289. return false;
  290. }
  291. if (!Blocker.CanAttack(user, target, (weaponUid, weapon)))
  292. return false;
  293. // Can't self-attack if you're the weapon
  294. if (weaponUid == target)
  295. return false;
  296. break;
  297. case DisarmAttackEvent disarm:
  298. if (disarm.Target != null && !TryGetEntity(disarm.Target, out target))
  299. {
  300. // Target was lightly attacked & deleted.
  301. return false;
  302. }
  303. if (!Blocker.CanAttack(user, target, (weaponUid, weapon), true))
  304. return false;
  305. break;
  306. default:
  307. if (!Blocker.CanAttack(user, weapon: (weaponUid, weapon)))
  308. return false;
  309. break;
  310. }
  311. // Windup time checked elsewhere.
  312. var fireRate = TimeSpan.FromSeconds(1f / GetAttackRate(weaponUid, user, weapon));
  313. var swings = 0;
  314. // TODO: If we get autoattacks then probably need a shotcounter like guns so we can do timing properly.
  315. if (weapon.NextAttack < curTime)
  316. weapon.NextAttack = curTime;
  317. while (weapon.NextAttack <= curTime)
  318. {
  319. weapon.NextAttack += fireRate;
  320. swings++;
  321. }
  322. Dirty(weaponUid, weapon);
  323. // Do this AFTER attack so it doesn't spam every tick
  324. var ev = new AttemptMeleeEvent();
  325. RaiseLocalEvent(weaponUid, ref ev);
  326. if (ev.Cancelled)
  327. {
  328. if (ev.Message != null)
  329. {
  330. PopupSystem.PopupClient(ev.Message, weaponUid, user);
  331. }
  332. return false;
  333. }
  334. // Attack confirmed
  335. for (var i = 0; i < swings; i++)
  336. {
  337. string animation;
  338. switch (attack)
  339. {
  340. case LightAttackEvent light:
  341. DoLightAttack(user, light, weaponUid, weapon, session);
  342. animation = weapon.Animation;
  343. break;
  344. case DisarmAttackEvent disarm:
  345. if (!DoDisarm(user, disarm, weaponUid, weapon, session))
  346. return false;
  347. animation = weapon.Animation;
  348. break;
  349. case HeavyAttackEvent heavy:
  350. if (!DoHeavyAttack(user, heavy, weaponUid, weapon, session))
  351. return false;
  352. animation = weapon.WideAnimation;
  353. break;
  354. default:
  355. throw new NotImplementedException();
  356. }
  357. DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation);
  358. }
  359. var attackEv = new MeleeAttackEvent(weaponUid);
  360. RaiseLocalEvent(user, ref attackEv);
  361. weapon.Attacking = true;
  362. return true;
  363. }
  364. protected abstract bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session);
  365. protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
  366. {
  367. // If I do not come back later to fix Light Attacks being Heavy Attacks you can throw me in the spider pit -Errant
  368. var damage = GetDamage(meleeUid, user, component) * GetHeavyDamageModifier(meleeUid, user, component);
  369. var target = GetEntity(ev.Target);
  370. var resistanceBypass = GetResistanceBypass(meleeUid, user, component);
  371. // For consistency with wide attacks stuff needs damageable.
  372. if (Deleted(target) ||
  373. !HasComp<DamageableComponent>(target) ||
  374. !TryComp(target, out TransformComponent? targetXform) ||
  375. // Not in LOS.
  376. !InRange(user, target.Value, component.Range, session))
  377. {
  378. // Leave IsHit set to true, because the only time it's set to false
  379. // is when a melee weapon is examined. Misses are inferred from an
  380. // empty HitEntities.
  381. // TODO: This needs fixing
  382. if (meleeUid == user)
  383. {
  384. AdminLogger.Add(LogType.MeleeHit,
  385. LogImpact.Low,
  386. $"{ToPrettyString(user):actor} melee attacked (light) using their hands and missed");
  387. }
  388. else
  389. {
  390. AdminLogger.Add(LogType.MeleeHit,
  391. LogImpact.Low,
  392. $"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed");
  393. }
  394. var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, null);
  395. RaiseLocalEvent(meleeUid, missEvent);
  396. _meleeSound.PlaySwingSound(user, meleeUid, component);
  397. return;
  398. }
  399. // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}");
  400. // Raise event before doing damage so we can cancel damage if the event is handled
  401. var hitEvent = new MeleeHitEvent(new List<EntityUid> { target.Value }, user, meleeUid, damage, null);
  402. RaiseLocalEvent(meleeUid, hitEvent);
  403. if (hitEvent.Handled)
  404. return;
  405. var targets = new List<EntityUid>(1)
  406. {
  407. target.Value
  408. };
  409. var weapon = GetEntity(ev.Weapon);
  410. // We skip weapon -> target interaction, as forensics system applies DNA on hit
  411. Interaction.DoContactInteraction(user, weapon);
  412. // If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a
  413. // somewhat messy scuffle. See also, heavy attacks.
  414. Interaction.DoContactInteraction(user, target);
  415. // For stuff that cares about it being attacked.
  416. var attackedEvent = new AttackedEvent(meleeUid, user, targetXform.Coordinates);
  417. RaiseLocalEvent(target.Value, attackedEvent);
  418. var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
  419. var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin: user, ignoreResistances: resistanceBypass);
  420. if (damageResult is { Empty: false })
  421. {
  422. // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor
  423. if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage))
  424. {
  425. _stamina.TakeStaminaDamage(target.Value, (bluntDamage * component.BluntStaminaDamageFactor).Float(), visual: false, source: user, with: meleeUid == user ? null : meleeUid);
  426. }
  427. if (meleeUid == user)
  428. {
  429. AdminLogger.Add(LogType.MeleeHit,
  430. LogImpact.Medium,
  431. $"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using their hands and dealt {damageResult.GetTotal():damage} damage");
  432. }
  433. else
  434. {
  435. AdminLogger.Add(LogType.MeleeHit,
  436. LogImpact.Medium,
  437. $"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage");
  438. }
  439. }
  440. _meleeSound.PlayHitSound(target.Value, user, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, component);
  441. if (damageResult?.GetTotal() > FixedPoint2.Zero)
  442. {
  443. DoDamageEffect(targets, user, targetXform);
  444. }
  445. }
  446. protected abstract void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform);
  447. private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
  448. {
  449. // TODO: This is copy-paste as fuck with DoPreciseAttack
  450. if (!TryComp(user, out TransformComponent? userXform))
  451. return false;
  452. var targetMap = TransformSystem.ToMapCoordinates(GetCoordinates(ev.Coordinates));
  453. if (targetMap.MapId != userXform.MapID)
  454. return false;
  455. var userPos = TransformSystem.GetWorldPosition(userXform);
  456. var direction = targetMap.Position - userPos;
  457. var distance = Math.Min(component.Range, direction.Length());
  458. var damage = GetDamage(meleeUid, user, component);
  459. var entities = GetEntityList(ev.Entities);
  460. if (entities.Count == 0)
  461. {
  462. if (meleeUid == user)
  463. {
  464. AdminLogger.Add(LogType.MeleeHit,
  465. LogImpact.Low,
  466. $"{ToPrettyString(user):actor} melee attacked (heavy) using their hands and missed");
  467. }
  468. else
  469. {
  470. AdminLogger.Add(LogType.MeleeHit,
  471. LogImpact.Low,
  472. $"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed");
  473. }
  474. var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, direction);
  475. RaiseLocalEvent(meleeUid, missEvent);
  476. // immediate audio feedback
  477. _meleeSound.PlaySwingSound(user, meleeUid, component);
  478. return true;
  479. }
  480. // Naughty input
  481. if (entities.Count > MaxTargets)
  482. {
  483. entities.RemoveRange(MaxTargets, entities.Count - MaxTargets);
  484. }
  485. // Validate client
  486. for (var i = entities.Count - 1; i >= 0; i--)
  487. {
  488. if (ArcRaySuccessful(entities[i],
  489. userPos,
  490. direction.ToWorldAngle(),
  491. component.Angle,
  492. distance,
  493. userXform.MapID,
  494. user,
  495. session))
  496. {
  497. continue;
  498. }
  499. // Bad input
  500. entities.RemoveAt(i);
  501. }
  502. var targets = new List<EntityUid>();
  503. var damageQuery = GetEntityQuery<DamageableComponent>();
  504. foreach (var entity in entities)
  505. {
  506. if (entity == user ||
  507. !damageQuery.HasComponent(entity))
  508. continue;
  509. targets.Add(entity);
  510. }
  511. // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}");
  512. // Raise event before doing damage so we can cancel damage if the event is handled
  513. var hitEvent = new MeleeHitEvent(targets, user, meleeUid, damage, direction);
  514. RaiseLocalEvent(meleeUid, hitEvent);
  515. if (hitEvent.Handled)
  516. return true;
  517. var weapon = GetEntity(ev.Weapon);
  518. Interaction.DoContactInteraction(user, weapon);
  519. // For stuff that cares about it being attacked.
  520. foreach (var target in targets)
  521. {
  522. // We skip weapon -> target interaction, as forensics system applies DNA on hit
  523. // If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a
  524. // somewhat messy scuffle. See also, light attacks.
  525. Interaction.DoContactInteraction(user, target);
  526. }
  527. var appliedDamage = new DamageSpecifier();
  528. for (var i = targets.Count - 1; i >= 0; i--)
  529. {
  530. var entity = targets[i];
  531. // We raise an attack attempt here as well,
  532. // primarily because this was an untargeted wideswing: if a subscriber to that event cared about
  533. // the potential target (such as for pacifism), they need to be made aware of the target here.
  534. // In that case, just continue.
  535. if (!Blocker.CanAttack(user, entity, (weapon, component)))
  536. {
  537. targets.RemoveAt(i);
  538. continue;
  539. }
  540. var attackedEvent = new AttackedEvent(meleeUid, user, GetCoordinates(ev.Coordinates));
  541. RaiseLocalEvent(entity, attackedEvent);
  542. var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
  543. var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin: user, canEvade: true, partMultiplier: component.HeavyPartDamageMultiplier); // Shitmed Change // Goobstation
  544. if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero)
  545. {
  546. // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor
  547. if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage))
  548. {
  549. _stamina.TakeStaminaDamage(entity, (bluntDamage * component.BluntStaminaDamageFactor).Float(), visual: false, source: user, with: meleeUid == user ? null : meleeUid);
  550. }
  551. appliedDamage += damageResult;
  552. if (meleeUid == user)
  553. {
  554. AdminLogger.Add(LogType.MeleeHit,
  555. LogImpact.Medium,
  556. $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.GetTotal():damage} damage");
  557. }
  558. else
  559. {
  560. AdminLogger.Add(LogType.MeleeHit,
  561. LogImpact.Medium,
  562. $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage");
  563. }
  564. }
  565. }
  566. if (entities.Count != 0)
  567. {
  568. var target = entities.First();
  569. _meleeSound.PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component);
  570. }
  571. if (appliedDamage.GetTotal() > FixedPoint2.Zero)
  572. {
  573. DoDamageEffect(targets, user, Transform(targets[0]));
  574. }
  575. return true;
  576. }
  577. protected HashSet<EntityUid> ArcRayCast(Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId, EntityUid ignore)
  578. {
  579. // TODO: This is pretty sucky.
  580. var widthRad = arcWidth;
  581. var increments = 1 + 35 * (int)Math.Ceiling(widthRad / (2 * Math.PI));
  582. var increment = widthRad / increments;
  583. var baseAngle = angle - widthRad / 2;
  584. var resSet = new HashSet<EntityUid>();
  585. for (var i = 0; i < increments; i++)
  586. {
  587. var castAngle = new Angle(baseAngle + increment * i);
  588. var res = _physics.IntersectRay(mapId,
  589. new CollisionRay(position,
  590. castAngle.ToWorldVec(),
  591. AttackMask),
  592. range,
  593. ignore,
  594. false)
  595. .ToList();
  596. if (res.Count != 0)
  597. {
  598. // If there's exact distance overlap, we simply have to deal with all overlapping objects to avoid selecting randomly.
  599. var resChecked = res.Where(x => x.Distance.Equals(res[0].Distance));
  600. foreach (var r in resChecked)
  601. {
  602. if (Interaction.InRangeUnobstructed(ignore, r.HitEntity, range + 0.1f, overlapCheck: false))
  603. resSet.Add(r.HitEntity);
  604. }
  605. }
  606. }
  607. return resSet;
  608. }
  609. protected virtual bool ArcRaySuccessful(EntityUid targetUid,
  610. Vector2 position,
  611. Angle angle,
  612. Angle arcWidth,
  613. float range,
  614. MapId mapId,
  615. EntityUid ignore,
  616. ICommonSession? session)
  617. {
  618. // Only matters for server.
  619. return true;
  620. }
  621. public static string? GetHighestDamageSound(DamageSpecifier modifiedDamage, IPrototypeManager protoManager)
  622. {
  623. var groups = modifiedDamage.GetDamagePerGroup(protoManager);
  624. // Use group if it's exclusive, otherwise fall back to type.
  625. if (groups.Count == 1)
  626. {
  627. return groups.Keys.First();
  628. }
  629. var highestDamage = FixedPoint2.Zero;
  630. string? highestDamageType = null;
  631. foreach (var (type, damage) in modifiedDamage.DamageDict)
  632. {
  633. if (damage <= highestDamage)
  634. continue;
  635. highestDamageType = type;
  636. }
  637. return highestDamageType;
  638. }
  639. protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
  640. {
  641. var target = GetEntity(ev.Target);
  642. if (Deleted(target) ||
  643. user == target)
  644. {
  645. return false;
  646. }
  647. // Play a sound to give instant feedback; same with playing the animations
  648. _meleeSound.PlaySwingSound(user, meleeUid, component);
  649. return true;
  650. }
  651. private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation)
  652. {
  653. // TODO: Assert that offset eyes are still okay.
  654. if (!TryComp(user, out TransformComponent? userXform))
  655. return;
  656. var invMatrix = TransformSystem.GetInvWorldMatrix(userXform);
  657. var localPos = Vector2.Transform(coordinates.Position, invMatrix);
  658. if (localPos.LengthSquared() <= 0f)
  659. return;
  660. localPos = userXform.LocalRotation.RotateVec(localPos);
  661. // We'll play the effect just short visually so it doesn't look like we should be hitting but actually aren't.
  662. const float bufferLength = 0.2f;
  663. var visualLength = length - bufferLength;
  664. if (localPos.Length() > visualLength)
  665. localPos = localPos.Normalized() * visualLength;
  666. DoLunge(user, weapon, angle, localPos, animation);
  667. }
  668. public abstract void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true);
  669. /// <summary>
  670. /// Used to update the MeleeWeapon component on item toggle.
  671. /// </summary>
  672. private void OnItemToggle(EntityUid uid, ItemToggleMeleeWeaponComponent itemToggleMelee, ItemToggledEvent args)
  673. {
  674. if (!TryComp(uid, out MeleeWeaponComponent? meleeWeapon))
  675. return;
  676. if (args.Activated)
  677. {
  678. if (itemToggleMelee.ActivatedDamage != null)
  679. {
  680. //Setting deactivated damage to the weapon's regular value before changing it.
  681. itemToggleMelee.DeactivatedDamage ??= meleeWeapon.Damage;
  682. meleeWeapon.Damage = itemToggleMelee.ActivatedDamage;
  683. }
  684. meleeWeapon.HitSound = itemToggleMelee.ActivatedSoundOnHit;
  685. if (itemToggleMelee.ActivatedSoundOnHitNoDamage != null)
  686. {
  687. //Setting the deactivated sound on no damage hit to the weapon's regular value before changing it.
  688. itemToggleMelee.DeactivatedSoundOnHitNoDamage ??= meleeWeapon.NoDamageSound;
  689. meleeWeapon.NoDamageSound = itemToggleMelee.ActivatedSoundOnHitNoDamage;
  690. }
  691. if (itemToggleMelee.ActivatedSoundOnSwing != null)
  692. {
  693. //Setting the deactivated sound on no damage hit to the weapon's regular value before changing it.
  694. itemToggleMelee.DeactivatedSoundOnSwing ??= meleeWeapon.SwingSound;
  695. meleeWeapon.SwingSound = itemToggleMelee.ActivatedSoundOnSwing;
  696. }
  697. if (itemToggleMelee.DeactivatedSecret)
  698. meleeWeapon.Hidden = false;
  699. }
  700. else
  701. {
  702. if (itemToggleMelee.DeactivatedDamage != null)
  703. meleeWeapon.Damage = itemToggleMelee.DeactivatedDamage;
  704. meleeWeapon.HitSound = itemToggleMelee.DeactivatedSoundOnHit;
  705. if (itemToggleMelee.DeactivatedSoundOnHitNoDamage != null)
  706. meleeWeapon.NoDamageSound = itemToggleMelee.DeactivatedSoundOnHitNoDamage;
  707. if (itemToggleMelee.DeactivatedSoundOnSwing != null)
  708. meleeWeapon.SwingSound = itemToggleMelee.DeactivatedSoundOnSwing;
  709. if (itemToggleMelee.DeactivatedSecret)
  710. meleeWeapon.Hidden = true;
  711. }
  712. Dirty(uid, meleeWeapon);
  713. }
  714. }