From e971a6c1929dd697473c35ca77d7c380a32c30ab Mon Sep 17 00:00:00 2001 From: Daniel <77834935+Orvex07@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:10:34 +0100 Subject: [PATCH] Fix: ExecutionSystem (#3657) --- .../Execution/ExecutionSystem.Helpers.cs | 306 +++++++++++++ .../_Sunrise/Execution/ExecutionSystem.cs | 427 ++++++++++-------- .../Footprints/PuddleFootPrintsSystem.cs | 101 +++-- .../ru-RU/_strings/execution/execution.ftl | 5 + 4 files changed, 601 insertions(+), 238 deletions(-) create mode 100644 Content.Server/_Sunrise/Execution/ExecutionSystem.Helpers.cs diff --git a/Content.Server/_Sunrise/Execution/ExecutionSystem.Helpers.cs b/Content.Server/_Sunrise/Execution/ExecutionSystem.Helpers.cs new file mode 100644 index 0000000000..ab42c2066d --- /dev/null +++ b/Content.Server/_Sunrise/Execution/ExecutionSystem.Helpers.cs @@ -0,0 +1,306 @@ +using System.Linq; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Explosion.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Content.Shared.Projectiles; +using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Hitscan.Components; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Ranged.Components; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Containers; + +namespace Content.Server._Sunrise.Execution; + +public sealed partial class ExecutionSystem +{ + private static bool TryGetVerbContext( + ref GetVerbsEvent args, + out EntityUid attacker, + out EntityUid weapon, + out EntityUid victim, + out bool suicide) + { + attacker = default; + weapon = default; + victim = default; + suicide = false; + + if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + return false; + + attacker = args.User; + weapon = args.Using.Value; + victim = args.Target; + suicide = attacker == victim; + return true; + } + + private bool CanExecuteWithAny(EntityUid victim, EntityUid attacker) + { + // No point executing someone if they can't take damage + if (!HasComp(victim)) + return false; + + // You can't execute something that cannot die + if (!TryComp(victim, out var mobState)) + return false; + + // You can't execute borgs + if (HasComp(victim)) + return false; + + // You're not allowed to execute dead people (no fun allowed) + if (_mobStateSystem.IsDead(victim, mobState)) + return false; + + // You must be able to attack people to execute + if (!_actionBlockerSystem.CanAttack(attacker, victim)) + return false; + + // The victim must be incapacitated to be executed + if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null)) + return false; + + return true; + } + + private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user) + { + if (!CanExecuteWithAny(victim, user)) + return false; + + // We must be able to actually hurt people with the weapon + if (!TryComp(weapon, out var melee) || melee.Damage.GetTotal() <= 0.0f) + return false; + + return true; + } + + private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user) + { + if (!CanExecuteWithAny(victim, user)) + return false; + + // We must be able to actually fire the gun + if (!TryComp(weapon, out var gun) || !_gunSystem.CanShoot(gun)) + return false; + + if (TryComp(victim, out var damageable)) + { + if (TryComp(weapon, out var battery) && + !PrototypeHasLethalEffect(damageable, battery.Prototype)) + { + return false; + } + + if (HasNonLethalAmmoPrototype(weapon)) + return false; + } + + return true; + } + + private bool HasNonLethalAmmoPrototype(EntityUid weapon) + { + if (TryComp(weapon, out var ballistic) && + ballistic.Proto != null && + IsNonLethalAmmo(ballistic.Proto.Value)) + { + return true; + } + + if (TryComp(weapon, out var revolver) && + revolver.FillPrototype != null && + IsNonLethalAmmo(revolver.FillPrototype)) + { + return true; + } + + if (TryComp(weapon, out var basic) && + IsNonLethalAmmo(basic.Proto)) + { + return true; + } + + if (_containerSystem.TryGetContainer(weapon, GunMagazineContainerId, out var container) && + container is ContainerSlot slot && + slot.ContainedEntity is { } magEntity) + { + if (TryComp(magEntity, out var magBallistic) && + magBallistic.Proto != null && + IsNonLethalAmmo(magBallistic.Proto.Value)) + { + return true; + } + + if (TryComp(magEntity, out var magBasic) && + IsNonLethalAmmo(magBasic.Proto)) + { + return true; + } + } + + return false; + } + + // Чтобы не убивало от пустых патрон + // Вероятно можно было сделать это более элегантно, но лень переделывать сейча. Сорян + private bool PrototypeHasLethalEffect(DamageableComponent damageable, EntProtoId ammoPrototype) + { + var proto = _prototypeManager.Index(ammoPrototype); + + if (proto.TryGetComponent(out _, _componentFactory)) + return true; + + DamageSpecifier? damage = null; + + if (proto.TryGetComponent(out var projectile, _componentFactory) && projectile != null && !projectile.Damage.Empty) + damage = projectile.Damage; + else if (proto.TryGetComponent(out var hitscan, _componentFactory) && hitscan != null && !hitscan.Damage.Empty) + damage = hitscan.Damage; + + if (damage == null) + return false; + + foreach (var (type, value) in damage.DamageDict) + { + if (value > FixedPoint2.Zero && damageable.Damage.DamageDict.ContainsKey(type)) + return true; + } + + return false; + } + + private static bool IsNonLethalAmmo(string? prototypeId) + { + if (string.IsNullOrWhiteSpace(prototypeId)) + return false; + + foreach (var token in NonLethalAmmoIdTokens) + { + if (prototypeId.Contains(token, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private static DamageSpecifier FilterToSupportedDamage(DamageableComponent damageable, DamageSpecifier damage) + { + if (damage.Empty) + return new DamageSpecifier(); + + var filtered = new DamageSpecifier(); + + foreach (var (type, value) in damage.DamageDict) + { + if (value <= FixedPoint2.Zero) + continue; + + if (!damageable.Damage.DamageDict.ContainsKey(type)) + continue; + + filtered.DamageDict[type] = value; + } + + return filtered; + } + + private bool ApplyExecutionDamage( + EntityUid victim, + EntityUid weapon, + DamageSpecifier baseDamage, + bool forceLethal, + float overkillFractionMin, + float overkillFractionMax) + { + if (!TryComp(victim, out var damageable)) + return false; + + var damage = FilterToSupportedDamage(damageable, baseDamage); + if (damage.Empty || !damage.AnyPositive()) + return false; + + if (!forceLethal) + { + _damageableSystem.ChangeDamage( + victim, + damage, + ignoreResistances: false, + origin: weapon, + useVariance: false, + ignoreGlobalModifiers: false); + + return true; + } + + if (!TryComp(victim, out var thresholds)) + return false; + + damage.DamageDict.Remove(StructuralDamageType); + if (damage.Empty || !damage.AnyPositive()) + return false; + + var lethalRemaining = thresholds.Thresholds.Keys.Last() - damageable.TotalDamage; + if (lethalRemaining <= FixedPoint2.Zero) + return true; + + var overkillFraction = _random.NextFloat(overkillFractionMin, overkillFractionMax); + var overkill = lethalRemaining * overkillFraction; + var totalToApply = lethalRemaining + overkill; + + var finalDamage = DistributeDamage(damage, totalToApply); + if (finalDamage.Empty || !finalDamage.AnyPositive()) + return false; + + _damageableSystem.ChangeDamage( + victim, + finalDamage, + ignoreResistances: true, + origin: weapon, + useVariance: false, + ignoreGlobalModifiers: true); + + return true; + } + + private static DamageSpecifier DistributeDamage(DamageSpecifier weights, FixedPoint2 total) + { + if (total <= FixedPoint2.Zero) + return new DamageSpecifier(); + + var result = new DamageSpecifier(weights); + var weightsTotal = result.GetTotal(); + if (weightsTotal <= FixedPoint2.Zero) + return new DamageSpecifier(); + + foreach (var type in result.DamageDict.Keys.ToArray()) + { + var value = result.DamageDict[type]; + if (value <= FixedPoint2.Zero) + { + result.DamageDict.Remove(type); + continue; + } + + // Сделано по патерну SharedSuicideSystem + result.DamageDict[type] = Math.Ceiling((double)(value * total / weightsTotal)); + } + + return result; + } + + private void ShowExecutionPopup(string locString, Filter filter, PopupType type, + EntityUid attacker, EntityUid victim, EntityUid weapon) + { + _popupSystem.PopupEntity(Loc.GetString( + locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, filter, true, type); + } +} diff --git a/Content.Server/_Sunrise/Execution/ExecutionSystem.cs b/Content.Server/_Sunrise/Execution/ExecutionSystem.cs index cba6bbfedd..0cf97ff2fd 100644 --- a/Content.Server/_Sunrise/Execution/ExecutionSystem.cs +++ b/Content.Server/_Sunrise/Execution/ExecutionSystem.cs @@ -1,13 +1,13 @@ -using Content.Server.Kitchen.Components; -using Content.Server.Weapons.Ranged.Systems; +using Content.Server.Weapons.Ranged.Systems; using Content.Shared._Starlight.Weapon.Components; using Content.Shared.ActionBlocker; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared._Sunrise.Execution; +using Content.Shared.Body.Components; using Content.Shared.Kitchen.Components; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Content.Shared.Projectiles; @@ -17,37 +17,69 @@ using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; +using Content.Server.Explosion.EntitySystems; +using Content.Server.Body.Systems; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Containers; -using Content.Shared.Silicons.Borgs.Components; -using Content.Shared.Damage.Systems; -using Content.Shared.Damage.Components; +using Content.Shared.Explosion; +using Content.Shared.Explosion.Components; +using Content.Shared.Weapons.Hitscan.Components; +using Robust.Shared.Random; namespace Content.Server._Sunrise.Execution; /// /// Verb for violently murdering cuffed creatures. /// -public sealed class ExecutionSystem : EntitySystem +public sealed partial class ExecutionSystem : EntitySystem { [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly GunSystem _gunSystem = default!; + [Dependency] private readonly ExplosionSystem _explosionSystem = default!; + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; private const float MeleeExecutionTimeModifier = 5.0f; private const float GunExecutionTime = 6.0f; - private const float DamageModifier = 9.0f; + private const float SuicideGunTimeMultiplier = 0.5f; + private const int GunExecutionShots = 1; + private const float AttackRateToSeconds = 1.0f; + private const float OverkillFractionMin = 0.05f; + private const float OverkillFractionMax = 0.20f; + private const float ExplosiveOverkillFractionMin = 2.0f; + private const float ExplosiveOverkillFractionMax = 4.0f; + private const float SuicideExplosionIntensityScale = 0.45f; + private const int SuicideExplosionMaxTileBreak = 0; + private const bool SuicideExplosionCanCreateVacuum = false; + private const string GunChamberContainerId = "gun_chamber"; + private const string GunMagazineContainerId = "gun_magazine"; + private const string StructuralDamageType = "Structural"; + + private static readonly string[] NonLethalAmmoIdTokens = + { + "Practice", + "Rubber", + }; + + private readonly record struct SuicideExplosionInfo( + ProtoId ExplosionType, + float TotalIntensity, + float IntensitySlope, + float MaxIntensity, + float TileBreakScale); /// public override void Initialize() @@ -61,19 +93,11 @@ public sealed class ExecutionSystem : EntitySystem SubscribeLocalEvent(OnDoafterGun); } - private void OnGetInteractionVerbsMelee( - EntityUid uid, - SharpComponent component, - GetVerbsEvent args) + private void OnGetInteractionVerbsMelee(Entity ent, ref GetVerbsEvent args) { - if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + if (!TryGetVerbContext(ref args, out var attacker, out var weapon, out var victim, out var suicide)) return; - var attacker = args.User; - var weapon = args.Using!.Value; - var victim = args.Target; - var suicide = attacker == victim; - if (!CanExecuteWithMelee(weapon, victim, attacker)) return; @@ -91,19 +115,11 @@ public sealed class ExecutionSystem : EntitySystem args.Verbs.Add(verb); } - private void OnGetInteractionVerbsGun( - EntityUid uid, - GunComponent component, - GetVerbsEvent args) + private void OnGetInteractionVerbsGun(Entity ent, ref GetVerbsEvent args) { - if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + if (!TryGetVerbContext(ref args, out var attacker, out var weapon, out var victim, out var suicide)) return; - var attacker = args.User; - var weapon = args.Using!.Value; - var victim = args.Target; - var suicide = attacker == victim; - if (!CanExecuteWithGun(weapon, victim, attacker)) return; @@ -122,93 +138,24 @@ public sealed class ExecutionSystem : EntitySystem args.Verbs.Add(verb); } - private bool CanExecuteWithAny(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - if (attacker != victim) - return false; - - // No point executing someone if they can't take damage - if (!TryComp(victim, out var damage)) - return false; - - // You can't execute something that cannot die - if (!TryComp(victim, out var mobState)) - return false; - - // You can't execute borgs - if (TryComp(victim, out var borgChassis)) - return false; - - // You're not allowed to execute dead people (no fun allowed) - if (_mobStateSystem.IsDead(victim, mobState)) - return false; - - // You must be able to attack people to execute - if (!_actionBlockerSystem.CanAttack(attacker, victim)) - return false; - - // The victim must be incapacitated to be executed - if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null)) - return false; - - // All checks passed - return true; - } - - private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user) - { - if (user != victim) - return false; - - if (!CanExecuteWithAny(weapon, victim, user)) return false; - - // We must be able to actually hurt people with the weapon - if (!TryComp(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) - return false; - - return true; - } - - private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user) - { - if (user != victim) - return false; - - if (!CanExecuteWithAny(weapon, victim, user)) return false; - - // We must be able to actually fire the gun - if (!TryComp(weapon, out var gun) && _gunSystem.CanShoot(gun!)) - return false; - - if (_containerSystem.TryGetContainer(weapon, "gun_chamber", out var chamberContainer)) - { - foreach (var contained in chamberContainer.ContainedEntities) - { - if (TryComp(contained, out var cartridge) && cartridge.Spent) - return false; - } - } - - return true; - } - private void TryStartMeleeExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker) { if (!CanExecuteWithMelee(weapon, victim, attacker)) return; - var executionTime = (1.0f / Comp(weapon).AttackRate) * MeleeExecutionTimeModifier; + var executionTime = AttackRateToSeconds / Comp(weapon).AttackRate * MeleeExecutionTimeModifier; + + var internalKey = attacker == victim + ? "suicide-popup-melee-initial-internal" + : "execution-popup-melee-initial-internal"; + + var externalKey = attacker == victim + ? "suicide-popup-melee-initial-external" + : "execution-popup-melee-initial-external"; + + ShowExecutionPopup(internalKey, Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); + ShowExecutionPopup(externalKey, Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } var doAfter = new DoAfterArgs(EntityManager, attacker, executionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) @@ -243,19 +190,25 @@ public sealed class ExecutionSystem : EntitySystem return; } - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } + var internalKey = attacker == victim + ? "suicide-popup-gun-initial-internal" + : "execution-popup-gun-initial-internal"; + + var externalKey = attacker == victim + ? "suicide-popup-gun-initial-external" + : "execution-popup-gun-initial-external"; + + ShowExecutionPopup(internalKey, Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); + ShowExecutionPopup(externalKey, Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); var doAfter = - new DoAfterArgs(EntityManager, attacker, attacker == victim ? GunExecutionTime / 2 : GunExecutionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) + new DoAfterArgs(EntityManager, + attacker, + attacker == victim ? GunExecutionTime * SuicideGunTimeMultiplier : GunExecutionTime, + new ExecutionDoAfterEvent(), + weapon, + target: victim, + used: weapon) { BreakOnMove = true, BreakOnDamage = true, @@ -265,19 +218,8 @@ public sealed class ExecutionSystem : EntitySystem _doAfterSystem.TryStartDoAfter(doAfter); } - private bool OnDoafterChecks(EntityUid uid, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return false; - if (!CanExecuteWithAny(args.Used.Value, args.Target.Value, uid)) - return false; - - // All checks passed - return true; - } - - private void OnDoafterMelee(EntityUid uid, SharpComponent component, DoAfterEvent args) + private void OnDoafterMelee(Entity ent, ref ExecutionDoAfterEvent args) { if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) return; @@ -286,28 +228,30 @@ public sealed class ExecutionSystem : EntitySystem var victim = args.Target!.Value; var weapon = args.Used!.Value; - if (!CanExecuteWithMelee(weapon, victim, attacker)) return; - - if (!TryComp(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) + if (!CanExecuteWithMelee(weapon, victim, attacker)) return; - _damageableSystem.ChangeDamage(victim, melee.Damage * DamageModifier, true, useVariance: false, ignoreGlobalModifiers: true); + if (!TryComp(weapon, out var melee)) + return; + + ApplyExecutionDamage(victim, weapon, melee.Damage, forceLethal: true, OverkillFractionMin, OverkillFractionMax); _audioSystem.PlayEntity(melee.HitSound, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } + var internalKey = attacker == victim + ? "suicide-popup-melee-complete-internal" + : "execution-popup-melee-complete-internal"; + + var externalKey = attacker == victim + ? "suicide-popup-melee-complete-external" + : "execution-popup-melee-complete-external"; + + ShowExecutionPopup(internalKey, Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); + ShowExecutionPopup(externalKey, Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); } + // TODO: This repeats a lot of the code of the serverside GunSystem, make it not do that - private void OnDoafterGun(EntityUid uid, GunComponent component, DoAfterEvent args) + private void OnDoafterGun(Entity ent, ref ExecutionDoAfterEvent args) { if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) return; @@ -323,7 +267,7 @@ public sealed class ExecutionSystem : EntitySystem var prevention = new ShotAttemptedEvent { User = attacker, - Used = (weapon, component), + Used = (weapon, ent.Comp), }; RaiseLocalEvent(weapon, ref prevention); @@ -347,67 +291,150 @@ public sealed class ExecutionSystem : EntitySystem // Take some ammunition for the shot (one bullet) var fromCoordinates = Transform(attacker).Coordinates; - var ev = new TakeAmmoEvent(1, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, attacker); - RaiseLocalEvent(weapon, ev); + var takeAmmoEvent = new TakeAmmoEvent( + GunExecutionShots, + new List<(EntityUid? Entity, IShootable Shootable)>(), + fromCoordinates, + attacker); + RaiseLocalEvent(weapon, takeAmmoEvent); // Check if there's any ammo left - if (ev.Ammo.Count <= 0) + if (takeAmmoEvent.Ammo.Count <= 0) { - _audioSystem.PlayEntity(component.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default); + _audioSystem.PlayEntity(ent.Comp.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default); ShowExecutionPopup("execution-popup-gun-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim, weapon); return; } // Information about the ammo like damage - DamageSpecifier damage = new DamageSpecifier(); + DamageSpecifier damage = new(); + SuicideExplosionInfo? explosiveToTrigger = null; + string? firedPrototypeId = null; // Get some information from IShootable - var ammoUid = ev.Ammo[0].Entity; - switch (ev.Ammo[0].Shootable) + var ammoUid = takeAmmoEvent.Ammo[0].Entity; + var shootable = takeAmmoEvent.Ammo[0].Shootable; + var isSpent = shootable switch + { + CartridgeAmmoComponent x => x.Spent, + HitScanCartridgeAmmoComponent x => x.Spent, + _ => false + }; + + if (isSpent) + { + if (ammoUid != null) + { + if (_containerSystem.TryGetContainer(weapon, GunChamberContainerId, out var chamberContainer)) + _containerSystem.Remove(ammoUid.Value, chamberContainer); + + _transformSystem.DropNextTo(ammoUid.Value, weapon); + } + + _audioSystem.PlayEntity(ent.Comp.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default); + ShowExecutionPopup("execution-popup-ammo-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim, weapon); + return; + } + + + switch (takeAmmoEvent.Ammo[0].Shootable) { //🌟Starlight🌟 start case HitScanCartridgeAmmoComponent cartridge: var hitscanProto = _prototypeManager.Index(cartridge.Hitscan); + firedPrototypeId = cartridge.Hitscan.Id; + if (hitscanProto.Damage is not null) damage = hitscanProto.Damage * hitscanProto.Count; cartridge.Spent = true; _appearanceSystem.SetData(ammoUid!.Value, AmmoVisuals.Spent, true); + Dirty(ammoUid.Value, cartridge); + if (cartridge.DeleteOnSpawn) + Del(ammoUid.Value); + break; //🌟Starlight🌟 end case CartridgeAmmoComponent cartridge: // Get the damage value var prototype = _prototypeManager.Index(cartridge.Prototype); - prototype.TryGetComponent(out var projectileA, _componentFactory); // sloth forgive me - if (projectileA != null) + firedPrototypeId = cartridge.Prototype.Id; + prototype.TryGetComponent(out var projectilePrototype, _componentFactory); + + if (projectilePrototype != null) + damage = projectilePrototype.Damage; + + else if (prototype.TryGetComponent(out var hitscanDamage, _componentFactory) && hitscanDamage != null) + damage = hitscanDamage.Damage; + + if (prototype.TryGetComponent(out var explosiveProto, _componentFactory) && explosiveProto != null) { - damage = projectileA.Damage; - } - prototype.TryGetComponent(out var projectilespreaderA, _componentFactory); - if (projectilespreaderA != null) - { - damage *= projectilespreaderA.Count; + explosiveToTrigger = new SuicideExplosionInfo( + explosiveProto.ExplosionType, + explosiveProto.TotalIntensity, + explosiveProto.IntensitySlope, + explosiveProto.MaxIntensity, + explosiveProto.TileBreakScale); } + prototype.TryGetComponent(out var projectileSpread, _componentFactory); + + if (projectileSpread != null) + damage *= projectileSpread.Count; + // Expend the cartridge cartridge.Spent = true; _appearanceSystem.SetData(ammoUid!.Value, AmmoVisuals.Spent, true); Dirty(ammoUid.Value, cartridge); + if (cartridge.DeleteOnSpawn) + Del(ammoUid.Value); + break; - case AmmoComponent newAmmo: - TryComp(ammoUid, out var projectileB); - if (projectileB != null) + case AmmoComponent: + if (ammoUid != null) + firedPrototypeId = MetaData(ammoUid.Value).EntityPrototype?.ID; + + TryComp(ammoUid, out var projectileAmmo); + + if (projectileAmmo != null) + damage = projectileAmmo.Damage; + + else if (ammoUid != null && TryComp(ammoUid, out var hitscanAmmoDamage) && !hitscanAmmoDamage.Damage.Empty) + damage = hitscanAmmoDamage.Damage; + + if (ammoUid != null && TryComp(ammoUid.Value, out var explosiveAmmo)) { - damage = projectileB.Damage; + explosiveToTrigger = new SuicideExplosionInfo( + explosiveAmmo.ExplosionType, + explosiveAmmo.TotalIntensity, + explosiveAmmo.IntensitySlope, + explosiveAmmo.MaxIntensity, + explosiveAmmo.TileBreakScale); } - Del(ammoUid); + + if (ammoUid != null) + Del(ammoUid.Value); + + break; + + case HitscanAmmoComponent: + if (ammoUid != null) + firedPrototypeId = MetaData(ammoUid.Value).EntityPrototype?.ID; + + if (ammoUid != null && TryComp(ammoUid, out var hitscanAmmo) && !hitscanAmmo.Damage.Empty) + damage = hitscanAmmo.Damage; + + if (ammoUid != null) + Del(ammoUid.Value); + break; case HitscanPrototype hitscan: + firedPrototypeId = hitscan.ID; damage = hitscan.Damage!; break; @@ -415,28 +442,50 @@ public sealed class ExecutionSystem : EntitySystem throw new ArgumentOutOfRangeException(); } - // Gun successfully fired, deal damage - _damageableSystem.ChangeDamage(victim, damage * DamageModifier, true, useVariance: false, ignoreGlobalModifiers: true); - _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, false, AudioParams.Default); + var forceLethal = !IsNonLethalAmmo(firedPrototypeId); + var isExplosive = explosiveToTrigger != null; + + if (isExplosive && forceLethal) + { + var explosionType = _prototypeManager.Index(explosiveToTrigger!.Value.ExplosionType); + ApplyExecutionDamage(victim, weapon, explosionType.DamagePerIntensity, forceLethal: true, ExplosiveOverkillFractionMin, ExplosiveOverkillFractionMax); + + if (TryComp(victim, out var bloodstream)) + _bloodstreamSystem.SpillAllSolutions((victim, bloodstream)); + } + + else + ApplyExecutionDamage(victim, weapon, damage, forceLethal, OverkillFractionMin, OverkillFractionMax); + + if (explosiveToTrigger != null) + { + var explosive = explosiveToTrigger.Value; + _explosionSystem.QueueExplosion( + victim, + explosive.ExplosionType, + explosive.TotalIntensity * SuicideExplosionIntensityScale, + explosive.IntensitySlope, + explosive.MaxIntensity * SuicideExplosionIntensityScale, + explosive.TileBreakScale, + SuicideExplosionMaxTileBreak, + SuicideExplosionCanCreateVacuum, + attacker); + } + _audioSystem.PlayEntity(ent.Comp.SoundGunshot, Filter.Pvs(weapon), weapon, false, AudioParams.Default); // Popups - if (attacker != victim) - { - ShowExecutionPopup("execution-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("suicide-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.LargeCaution, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); - } + + var internalKey = attacker != victim + ? "execution-popup-gun-complete-internal" + : "suicide-popup-gun-complete-internal"; + + var externalKey = attacker != victim + ? "execution-popup-gun-complete-external" + : "suicide-popup-gun-complete-external"; + + + ShowExecutionPopup(internalKey, Filter.Entities(attacker), PopupType.LargeCaution, attacker, victim, weapon); + ShowExecutionPopup(externalKey, Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); } - private void ShowExecutionPopup(string locString, Filter filter, PopupType type, - EntityUid attacker, EntityUid victim, EntityUid weapon) - { - _popupSystem.PopupEntity(Loc.GetString( - locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), - attacker, filter, true, type); - } } diff --git a/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs b/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs index e98b9e6804..dc5de42e7d 100644 --- a/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs +++ b/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs @@ -27,59 +27,62 @@ public sealed class PuddleFootprintSystem : EntitySystem /// /// Handles puddle interaction and footprint creation when entity exits the puddle /// - private void OnPuddleInteraction(EntityUid uid, PuddleFootprintComponent component, ref EndCollideEvent args) -{ - if (!TryComp(uid, out var puddle) - || !TryComp(args.OtherEntity, out var emitter) - || !TryComp(uid, out var solutionManager) - || !_solutionSystem.ResolveSolution((uid, solutionManager), puddle.SolutionName, ref puddle.Solution, out var puddleSolutions) - || !TryComp(args.OtherEntity, out var emitterSolutionManager)) - return; - - var stand = !_standingStateSystem.IsDown(args.OtherEntity); - - var solCont = (args.OtherEntity, emitterSolutionManager); - Solution solution; - Entity solComp; - - if (stand) + private void OnPuddleInteraction(Entity ent, ref EndCollideEvent args) { - if (!_solutionSystem.ResolveSolution(solCont, emitter.FootsSolutionName, ref emitter.FootsSolution, out var footsSolution)) + + if (TerminatingOrDeleted(ent) || TerminatingOrDeleted(args.OtherEntity)) return; - solution = footsSolution; - solComp = emitter.FootsSolution.Value; - } - else - { - if (!_solutionSystem.ResolveSolution(solCont, emitter.BodySurfaceSolutionName, ref emitter.BodySurfaceSolution, out var bodySurfaceSolution)) + if (!TryComp(ent, out var puddle) + || !TryComp(args.OtherEntity, out var emitter) + || !TryComp(ent, out var solutionManager) + || !_solutionSystem.ResolveSolution((ent, solutionManager), puddle.SolutionName, ref puddle.Solution, out var puddleSolutions) + || !TryComp(args.OtherEntity, out var emitterSolutionManager)) return; - solution = bodySurfaceSolution; - solComp = emitter.BodySurfaceSolution.Value; + var stand = !_standingStateSystem.IsDown(args.OtherEntity); + + var solCont = (args.OtherEntity, emitterSolutionManager); + Solution solution; + Entity solComp; + if (stand) + { + if (!_solutionSystem.ResolveSolution(solCont, emitter.FootsSolutionName, ref emitter.FootsSolution, out var footsSolution)) + return; + + solution = footsSolution; + solComp = emitter.FootsSolution.Value; + } + else + { + if (!_solutionSystem.ResolveSolution(solCont, emitter.BodySurfaceSolutionName, ref emitter.BodySurfaceSolution, out var bodySurfaceSolution)) + return; + + solution = bodySurfaceSolution; + solComp = emitter.BodySurfaceSolution.Value; + } + + var totalSolutionQuantity = puddleSolutions.Contents.Sum(sol => (float)sol.Quantity); + var waterQuantity = (from sol in puddleSolutions.Contents where sol.Reagent.Prototype == "Water" select (float)sol.Quantity).FirstOrDefault(); + + if (waterQuantity / (totalSolutionQuantity / 100f) > ent.Comp.WaterThresholdPercent || puddleSolutions.Contents.Count <= 0) + return; + + var availableSpace = solution.MaxVolume.Float() - solution.Volume.Float(); + + if (availableSpace <= 0) + return; + + var transferVolume = Math.Min(ent.Comp.TransferVolume, availableSpace); + + if (puddleSolutions.Volume < transferVolume) + transferVolume = puddleSolutions.Volume.Float(); + + if (transferVolume <= 0) + return; + + var splitSolution = _solutionSystem.SplitSolution(puddle.Solution.Value, transferVolume); + + _solutionSystem.AddSolution(solComp, splitSolution); } - - var totalSolutionQuantity = puddleSolutions.Contents.Sum(sol => (float)sol.Quantity); - var waterQuantity = (from sol in puddleSolutions.Contents where sol.Reagent.Prototype == "Water" select (float)sol.Quantity).FirstOrDefault(); - - if (waterQuantity / (totalSolutionQuantity / 100f) > component.WaterThresholdPercent || puddleSolutions.Contents.Count <= 0) - return; - - var availableSpace = solution.MaxVolume.Float() - solution.Volume.Float(); - - if (availableSpace <= 0) - return; - - var transferVolume = Math.Min(component.TransferVolume, availableSpace); - - if (puddleSolutions.Volume < transferVolume) - transferVolume = puddleSolutions.Volume.Float(); - - if (transferVolume <= 0) - return; - - var splitSolution = _solutionSystem.SplitSolution(puddle.Solution.Value, transferVolume); - - _solutionSystem.AddSolution(solComp, splitSolution); -} } diff --git a/Resources/Locale/ru-RU/_strings/execution/execution.ftl b/Resources/Locale/ru-RU/_strings/execution/execution.ftl index b522d31151..4d690b794a 100644 --- a/Resources/Locale/ru-RU/_strings/execution/execution.ftl +++ b/Resources/Locale/ru-RU/_strings/execution/execution.ftl @@ -15,10 +15,15 @@ execution-popup-gun-complete-external = { $attacker } стреляет { $victim execution-popup-gun-clumsy-internal = Вы промахиваетесь по голове { $victim } и вместо этого стреляете себе в ногу! execution-popup-gun-clumsy-external = { $attacker } промахивается по { $victim } и вместо этого стреляет себе в ногу! execution-popup-gun-empty = { CAPITALIZE($weapon) } кликает. +execution-popup-ammo-empty = Патрон израсходован. suicide-popup-gun-initial-internal = Вы кладете дуло { $weapon } себе в рот. suicide-popup-gun-initial-external = { $attacker } кладет дуло { $weapon } себе в рот. suicide-popup-gun-complete-internal = Вы стреляете себе в голову! suicide-popup-gun-complete-external = { $attacker } стреляет себе в голову! +suicide-popup-melee-complete-internal = Вы перерезаете себе горло! +suicide-popup-melee-complete-external = { $attacker } перерезает себе горло! +suicide-popup-melee-initial-internal = Вы приставляете { $weapon } к своему горлу. +suicide-popup-melee-initial-external = { $attacker } приставляет { $weapon } к своему горлу. execution-popup-melee-initial-internal = Вы приставляете { $weapon } к горлу { $victim }. execution-popup-melee-initial-external = { $attacker } приставляет { $weapon } напротив горла { $victim }. execution-popup-melee-complete-internal = Вы перерезаете горло { $victim }!