using Content.Shared.Administration.Logs; using Content.Shared.Camera; using Content.Shared.CombatMode.Pacification; using Content.Shared.Contests; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Events; using Content.Shared.Database; using Content.Shared.Effects; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Mobs.Components; using Content.Shared.Projectiles; using Content.Shared.Popups; using Content.Shared.Throwing; using Content.Shared.Weapons.Melee; using Robust.Shared.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Content.Shared.Standing; namespace Content.Shared.Damage.Systems { public abstract partial class SharedDamageOtherOnHitSystem : EntitySystem { [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly ThrownItemSystem _thrownItem = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly MeleeSoundSystem _meleeSound = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly ContestsSystem _contests = default!; [Dependency] private readonly StandingStateSystem _standing = default!; public override void Initialize() { SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnDoHit); SubscribeLocalEvent(OnThrown); SubscribeLocalEvent(OnAttemptPacifiedThrow); SubscribeLocalEvent(OnItemToggleMapInit); SubscribeLocalEvent(OnItemToggle); } /// /// Inherit stats from MeleeWeapon. /// private void OnMapInit(EntityUid uid, DamageOtherOnHitComponent component, MapInitEvent args) { if (TryComp(uid, out var melee)) { if (component.Damage.Empty) component.Damage = melee.Damage * component.MeleeDamageMultiplier; if (component.SoundHit == null) component.SoundHit = melee.SoundHit; if (component.SoundNoDamage == null) { if (melee.SoundNoDamage != null) component.SoundNoDamage = melee.SoundNoDamage; else component.SoundNoDamage = new SoundCollectionSpecifier("WeakHit"); } } RaiseLocalEvent(uid, new DamageOtherOnHitStartupEvent((uid, component))); } /// /// Inherit stats from ItemToggleMeleeWeaponComponent. /// private void OnItemToggleMapInit(EntityUid uid, ItemToggleDamageOtherOnHitComponent component, MapInitEvent args) { if (!TryComp(uid, out var itemToggleMelee) || !TryComp(uid, out var damage)) return; if (component.ActivatedDamage == null && itemToggleMelee.ActivatedDamage is {} activatedDamage) component.ActivatedDamage = activatedDamage * damage.MeleeDamageMultiplier; if (component.ActivatedSoundHit == null) component.ActivatedSoundHit = itemToggleMelee.ActivatedSoundOnHit; if (component.ActivatedSoundNoDamage == null && itemToggleMelee.ActivatedSoundOnHitNoDamage is {} activatedSoundOnHitNoDamage) component.ActivatedSoundNoDamage = activatedSoundOnHitNoDamage; RaiseLocalEvent(uid, new ItemToggleDamageOtherOnHitStartupEvent((uid, component))); } private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args) { if (TerminatingOrDeleted(args.Target) || component.HitQuantity >= component.MaxHitQuantity || _standing.IsDown(args.Target)) return; var modifiedDamage = _damageable.TryChangeDamage(args.Target, GetDamage(uid, component, args.Component.Thrower), component.IgnoreResistances, origin: args.Component.Thrower, targetPart: args.TargetPart); // Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying. if (modifiedDamage != null) { if (HasComp(args.Target)) _adminLogger.Add(LogType.ThrowHit, $"{ToPrettyString(args.Target):target} received {modifiedDamage.GetTotal():damage} damage from collision"); _meleeSound.PlayHitSound(args.Target, null, SharedMeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, _protoManager), null, component.SoundHit, component.SoundNoDamage); } if (modifiedDamage is { Empty: false }) _color.RaiseEffect(Color.Red, new List() { args.Target }, Filter.Pvs(args.Target, entityManager: EntityManager)); if (TryComp(uid, out var body) && body.LinearVelocity.LengthSquared() > 0f) { var direction = body.LinearVelocity.Normalized(); _sharedCameraRecoil.KickCamera(args.Target, direction); } // TODO: If more stuff touches this then handle it after. if (TryComp(uid, out var physics)) { _thrownItem.LandComponent(args.Thrown, args.Component, physics, false); if (!HasComp(args.Thrown)) { var newVelocity = physics.LinearVelocity; newVelocity.X = -newVelocity.X / 4; newVelocity.Y = -newVelocity.Y / 4; _physics.SetLinearVelocity(uid, newVelocity, body: physics); } } component.HitQuantity += 1; } /// /// Used to update the DamageOtherOnHit component on item toggle. /// private void OnItemToggle(EntityUid uid, DamageOtherOnHitComponent component, ItemToggledEvent args) { if (!TryComp(uid, out var itemToggle)) return; if (args.Activated) { if (itemToggle.ActivatedDamage is {} activatedDamage) { itemToggle.DeactivatedDamage ??= component.Damage; component.Damage = activatedDamage * component.MeleeDamageMultiplier; } if (itemToggle.ActivatedStaminaCost is {} activatedStaminaCost) { itemToggle.DeactivatedStaminaCost ??= component.StaminaCost; component.StaminaCost = activatedStaminaCost; } itemToggle.DeactivatedSoundHit ??= component.SoundHit; component.SoundHit = itemToggle.ActivatedSoundHit; if (itemToggle.ActivatedSoundNoDamage is {} activatedSoundNoDamage) { itemToggle.DeactivatedSoundNoDamage ??= component.SoundNoDamage; component.SoundNoDamage = activatedSoundNoDamage; } } else { if (itemToggle.DeactivatedDamage is {} deactivatedDamage) component.Damage = deactivatedDamage; if (itemToggle.DeactivatedStaminaCost is {} deactivatedStaminaCost) component.StaminaCost = deactivatedStaminaCost; component.SoundHit = itemToggle.DeactivatedSoundHit; if (itemToggle.DeactivatedSoundNoDamage is {} deactivatedSoundNoDamage) component.SoundNoDamage = deactivatedSoundNoDamage; } } private void OnThrown(EntityUid uid, DamageOtherOnHitComponent component, ThrownEvent args) { component.HitQuantity = 0; } /// /// Prevent Pacified entities from throwing damaging items. /// private void OnAttemptPacifiedThrow(EntityUid uid, DamageOtherOnHitComponent comp, ref AttemptPacifiedThrowEvent args) { // Allow healing projectiles, forbid any that do damage if (comp.Damage.AnyPositive()) args.Cancel("pacified-cannot-throw"); } /// /// Gets the total damage a throwing weapon does. /// public DamageSpecifier GetDamage(EntityUid uid, DamageOtherOnHitComponent? component = null, EntityUid? user = null) { if (!Resolve(uid, ref component, false)) return new DamageSpecifier(); var ev = new GetThrowingDamageEvent(uid, component.Damage, new(), user); RaiseLocalEvent(uid, ref ev); if (component.ContestArgs is not null && user is EntityUid userUid) ev.Damage *= _contests.ContestConstructor(userUid, component.ContestArgs); return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers); } } }