1
0

SharedMeleeWeaponSystem.cs 34 KB

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