From 56d8e23d48ae33e27e36868a9e307cde3f110057 Mon Sep 17 00:00:00 2001 From: pacable <77161122+pxc1984@users.noreply.github.com> Date: Wed, 31 Jul 2024 03:44:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D0=B7=D0=BD=D0=B8:=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20(#256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Казни: возвращение Signed-off-by: JDtrimble * отключил казнь соседей Signed-off-by: JDtrimble --------- Signed-off-by: JDtrimble --- .../_Sunrise/Execution/ExecutionSystem.cs | 405 ++++++++++++++++++ .../_Sunrise/Execution/DoafterEvent.cs | 9 + .../en-US/_sunrise/execution/execution.ftl | 30 ++ .../ru-RU/_sunrise/execution/execution.ftl | 25 ++ 4 files changed, 469 insertions(+) create mode 100644 Content.Server/_Sunrise/Execution/ExecutionSystem.cs create mode 100644 Content.Shared/_Sunrise/Execution/DoafterEvent.cs create mode 100644 Resources/Locale/en-US/_sunrise/execution/execution.ftl create mode 100644 Resources/Locale/ru-RU/_sunrise/execution/execution.ftl diff --git a/Content.Server/_Sunrise/Execution/ExecutionSystem.cs b/Content.Server/_Sunrise/Execution/ExecutionSystem.cs new file mode 100644 index 00000000000..7379a8ac4e4 --- /dev/null +++ b/Content.Server/_Sunrise/Execution/ExecutionSystem.cs @@ -0,0 +1,405 @@ +using Content.Server.Interaction; +using Content.Server.Kitchen.Components; +using Content.Server.Weapons.Ranged.Systems; +using Content.Shared.ActionBlocker; +using Content.Shared.Damage; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared._Sunrise.Execution; +using Content.Shared.Interaction.Components; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Projectiles; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Ranged; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server._Sunrise.Execution; + +/// +/// Verb for violently murdering cuffed creatures. +/// +public sealed class ExecutionSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly InteractionSystem _interactionSystem = 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!; + + private const float MeleeExecutionTimeModifier = 5.0f; + private const float GunExecutionTime = 6.0f; + private const float DamageModifier = 9.0f; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetInteractionVerbsMelee); + SubscribeLocalEvent>(OnGetInteractionVerbsGun); + + SubscribeLocalEvent(OnDoafterMelee); + SubscribeLocalEvent(OnDoafterGun); + } + + private void OnGetInteractionVerbsMelee( + EntityUid uid, + SharpComponent component, + GetVerbsEvent args) + { + if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + return; + + var attacker = args.User; + var weapon = args.Using!.Value; + var victim = args.Target; + + if (!CanExecuteWithMelee(weapon, victim, attacker)) + return; + + UtilityVerb verb = new() + { + Act = () => + { + TryStartMeleeExecutionDoafter(weapon, victim, attacker); + }, + Impact = LogImpact.High, + Text = Loc.GetString("execution-verb-name"), + Message = Loc.GetString("execution-verb-message"), + }; + + args.Verbs.Add(verb); + } + + private void OnGetInteractionVerbsGun( + EntityUid uid, + GunComponent component, + GetVerbsEvent args) + { + if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + return; + + var attacker = args.User; + var weapon = args.Using!.Value; + var victim = args.Target; + + if (!CanExecuteWithGun(weapon, victim, attacker)) + return; + + UtilityVerb verb = new() + { + Act = () => + { + TryStartGunExecutionDoafter(weapon, victim, attacker); + }, + Impact = LogImpact.High, + Text = Loc.GetString("execution-verb-name"), + Message = Loc.GetString("execution-verb-message"), + }; + + 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'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; + + 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; + + 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) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true, + }; + + _doAfterSystem.TryStartDoAfter(doAfter); + } + + private void TryStartGunExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker) + { + if (!CanExecuteWithGun(weapon, victim, attacker)) + 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 doAfter = + new DoAfterArgs(EntityManager, attacker, GunExecutionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + _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) + { + if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) + return; + + var attacker = args.User; + 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) + return; + + _damageableSystem.TryChangeDamage(victim, melee.Damage * DamageModifier, true); + _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); + } + } + + // 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) + { + if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) + return; + + var attacker = args.User; + var weapon = args.Used.Value; + var victim = args.Target.Value; + + if (!CanExecuteWithGun(weapon, victim, attacker)) + return; + + // Check if any systems want to block our shot + var prevention = new ShotAttemptedEvent + { + User = attacker, + Used = (weapon, component), + }; + + RaiseLocalEvent(weapon, ref prevention); + if (prevention.Cancelled) + return; + + RaiseLocalEvent(attacker, ref prevention); + if (prevention.Cancelled) + return; + + // Not sure what this is for but gunsystem uses it so ehhh + var attemptEv = new AttemptShootEvent(attacker, null); + RaiseLocalEvent(weapon, ref attemptEv); + + if (attemptEv.Cancelled) + { + if (attemptEv.Message != null) + { + _popupSystem.PopupClient(attemptEv.Message, weapon, attacker); + return; + } + } + + // 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); + + // Check if there's any ammo left + if (ev.Ammo.Count <= 0) + { + _audioSystem.PlayEntity(component.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(); + + // Get some information from IShootable + var ammoUid = ev.Ammo[0].Entity; + switch (ev.Ammo[0].Shootable) + { + 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) + { + damage = projectileA.Damage * cartridge.Count; + } + + // Expend the cartridge + cartridge.Spent = true; + _appearanceSystem.SetData(ammoUid!.Value, AmmoVisuals.Spent, true); + Dirty(ammoUid.Value, cartridge); + + break; + + case AmmoComponent newAmmo: + TryComp(ammoUid, out var projectileB); + if (projectileB != null) + { + damage = projectileB.Damage; + } + Del(ammoUid); + break; + + case HitscanPrototype hitscan: + damage = hitscan.Damage!; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + // Clumsy people have a chance to shoot themselves + if (TryComp(attacker, out var clumsy) && component.ClumsyProof == false) + { + if (_interactionSystem.TryRollClumsy(attacker, 0.33333333f, clumsy)) + { + ShowExecutionPopup("execution-popup-gun-clumsy-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); + ShowExecutionPopup("execution-popup-gun-clumsy-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); + + // You shoot yourself with the gun (no damage multiplier) + _damageableSystem.TryChangeDamage(attacker, damage, origin: attacker); + _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, true, AudioParams.Default); + return; + } + } + + // Gun successfully fired, deal damage + _damageableSystem.TryChangeDamage(victim, damage * DamageModifier, true); + _audioSystem.PlayEntity(component.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); + } + } + + 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.Shared/_Sunrise/Execution/DoafterEvent.cs b/Content.Shared/_Sunrise/Execution/DoafterEvent.cs new file mode 100644 index 00000000000..9d51420d306 --- /dev/null +++ b/Content.Shared/_Sunrise/Execution/DoafterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared._Sunrise.Execution; + +[Serializable, NetSerializable] +public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Resources/Locale/en-US/_sunrise/execution/execution.ftl b/Resources/Locale/en-US/_sunrise/execution/execution.ftl new file mode 100644 index 00000000000..41559a29163 --- /dev/null +++ b/Resources/Locale/en-US/_sunrise/execution/execution.ftl @@ -0,0 +1,30 @@ +execution-verb-name = Execute +execution-verb-message = Use your weapon to execute someone. + +# All the below localisation strings have access to the following variables +# attacker (the person committing the execution) +# victim (the person being executed) +# weapon (the weapon used for the execution) + +execution-popup-gun-initial-internal = You ready the muzzle of {THE($weapon)} against {$victim}'s head. +execution-popup-gun-initial-external = {$attacker} readies the muzzle of {THE($weapon)} against {$victim}'s head. +execution-popup-gun-complete-internal = You blast {$victim} in the head! +execution-popup-gun-complete-external = {$attacker} blasts {$victim} in the head! +execution-popup-gun-clumsy-internal = You miss {$victim}'s head and shoot your foot instead! +execution-popup-gun-clumsy-external = {$attacker} misses {$victim} and shoots {POSS-ADJ($attacker)} foot instead! +execution-popup-gun-empty = {CAPITALIZE(THE($weapon))} clicks. + +suicide-popup-gun-initial-internal = You place the muzzle of {THE($weapon)} in your mouth. +suicide-popup-gun-initial-external = {$attacker} places the muzzle of {THE($weapon)} in {POSS-ADJ($attacker)} mouth. +suicide-popup-gun-complete-internal = You shoot yourself in the head! +suicide-popup-gun-complete-external = {$attacker} shoots {REFLEXIVE($attacker)} in the head! + +execution-popup-melee-initial-internal = You ready {THE($weapon)} against {$victim}'s throat. +execution-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against the throat of {$victim}. +execution-popup-melee-complete-internal = You slit the throat of {$victim}! +execution-popup-melee-complete-external = {$attacker} slits the throat of {$victim}! + +suicide-popup-melee-initial-internal = You ready {THE($weapon)} against your throat. +suicide-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against {POSS-ADJ($attacker)} throat. +suicide-popup-melee-complete-internal = You slit your throat with {THE($weapon)}! +suicide-popup-melee-complete-external = {$attacker} slits {POSS-ADJ($attacker)} throat with {THE($weapon)}! diff --git a/Resources/Locale/ru-RU/_sunrise/execution/execution.ftl b/Resources/Locale/ru-RU/_sunrise/execution/execution.ftl new file mode 100644 index 00000000000..6e33098b7f0 --- /dev/null +++ b/Resources/Locale/ru-RU/_sunrise/execution/execution.ftl @@ -0,0 +1,25 @@ +execution-verb-name = Казнить +execution-verb-message = Использовать ваше оружие чтоб казнить кого-либо. + +execution-popup-gun-initial-internal = Вы направляете дуло {THE($weapon)} в голову {$victim}. +execution-popup-gun-initial-external = {$attacker} направляет дуло {THE($weapon)} в голову {$victim}. +execution-popup-gun-complete-internal = Вы стреляете {$victim} в голову! +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(THE($weapon))} кликает. + +suicide-popup-gun-initial-internal = Вы кладете дуло {THE($weapon)} себе в рот. +suicide-popup-gun-initial-external = {$attacker} кладет дуло {THE($weapon)} себе в рот {POSS-ADJ($attacker)}. +suicide-popup-gun-complete-internal = Вы стреляете себе в голову! +suicide-popup-gun-complete-external = {$attacker} стреляет {REFLEXIVE($attacker)} в голову! + +execution-popup-melee-initial-internal = Вы приставляете {THE($weapon)} к горлу {$victim}. +execution-popup-melee-initial-external = {$attacker} готовит {POSS-ADJ($attacker)} {$weapon} напротив горла {$victim}. +execution-popup-melee-complete-internal = Вы перерезаете горло {$victim}! +execution-popup-melee-complete-external = {$attacker} перерезает горло {$victim}! + +suicide-popup-melee-initial-internal = Вы готовы {THE($weapon)} напротив вашего горла. +suicide-popup-melee-initial-external = {$attacker} готовит {POSS-ADJ($attacker)} {$weapon} напротив горла {POSS-ADJ($attacker)}. +suicide-popup-melee-complete-internal = Вы перерезаете себе горло с помощью {THE($weapon)}! +suicide-popup-melee-complete-external = {$attacker} перерезает {POSS-ADJ($attacker)} свое горло с помощью {THE($weapon)}!