Melee Executions (#30104)

* melee executions

* fix damage bug

* cleanup

* address reviews hopefully

* resistance bypass mechanic

* component changes

* self executions (not finished yet)

* self execs part two

* ok i fixed things (still not finished)

* finish everything

* review stuff

* nuke if (kind = special)

* more review stuffs

* Make suicide system much less hardcoded and make much more use of events

* Fix a dumb bug I introduced

* self execution popups

* Integration tests

* Why did they even take 0.5 blunt damage?

* More consistent integration tests

* Destructive equals true

* Allow it to dirty-dispose

* IS THIS WHAT YOU WANT?

* FRESH AND CLEAN

* modifier to multiplier

* don't jinx the integration tests

* no file-scoped namespace

* Move the rest of execution to shared, create SuicideGhostEvent

* handled

* Get rid of unused code and add a comment

* ghost before suicide

* stop cat suicides

* popup fix + small suicide change

* make it a bit better

---------

Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
This commit is contained in:
Scribbles0
2024-08-10 20:05:54 -07:00
committed by GitHub
parent c25c5ec666
commit 220aff21eb
26 changed files with 1048 additions and 219 deletions

View File

@@ -1,141 +1,154 @@
using Content.Server.Administration.Logs;
using Content.Server.Popups;
using Content.Server.GameTicking;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared.Administration.Logs;
using Content.Shared.Chat;
using Content.Shared.Mind.Components;
namespace Content.Server.Chat
namespace Content.Server.Chat;
public sealed class SuicideSystem : EntitySystem
{
public sealed class SuicideSystem : EntitySystem
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
public override void Initialize()
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
base.Initialize();
public bool Suicide(EntityUid victim)
{
// Checks to see if the CannotSuicide tag exits, ghosts instead.
if (_tagSystem.HasTag(victim, "CannotSuicide"))
return false;
// Checks to see if the player is dead.
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
return false;
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
var suicideEvent = new SuicideEvent(victim);
//Check to see if there were any systems blocking this suicide
if (SuicideAttemptBlocked(victim, suicideEvent))
return false;
bool environmentSuicide = false;
// If you are critical, you wouldn't be able to use your surroundings to suicide, so you do the default suicide
if (!_mobState.IsCritical(victim, mobState))
{
environmentSuicide = EnvironmentSuicideHandler(victim, suicideEvent);
}
if (suicideEvent.AttemptBlocked)
return false;
DefaultSuicideHandler(victim, suicideEvent);
ApplyDeath(victim, suicideEvent.Kind!.Value);
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided{(environmentSuicide ? " (environment)" : "")}");
return true;
}
/// <summary>
/// If not handled, does the default suicide, which is biting your own tongue
/// </summary>
private void DefaultSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
{
if (suicideEvent.Handled)
return;
var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
_popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
var selfMessage = Loc.GetString("suicide-command-default-text-self");
_popup.PopupEntity(selfMessage, victim, victim);
suicideEvent.SetHandled(SuicideKind.Bloodloss);
}
/// <summary>
/// Checks to see if there are any other systems that prevent suicide
/// </summary>
/// <returns>Returns true if there was a blocked attempt</returns>
private bool SuicideAttemptBlocked(EntityUid victim, SuicideEvent suicideEvent)
{
RaiseLocalEvent(victim, suicideEvent, true);
if (suicideEvent.AttemptBlocked)
return true;
SubscribeLocalEvent<DamageableComponent, SuicideEvent>(OnDamageableSuicide);
SubscribeLocalEvent<MobStateComponent, SuicideEvent>(OnEnvironmentalSuicide);
SubscribeLocalEvent<MindContainerComponent, SuicideGhostEvent>(OnSuicideGhost);
}
/// <summary>
/// Calling this function will attempt to kill the user by suiciding on objects in the surrounding area
/// or by applying a lethal amount of damage to the user with the default method.
/// Used when writing /suicide
/// </summary>
public bool Suicide(EntityUid victim)
{
// Can't suicide if we're already dead
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
return false;
}
/// <summary>
/// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
/// </summary>
private bool EnvironmentSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
{
var itemQuery = GetEntityQuery<ItemComponent>();
// Suicide by held item
if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
&& handsComponent.ActiveHandEntity is { } item)
{
RaiseLocalEvent(item, suicideEvent, false);
if (suicideEvent.Handled)
return true;
}
// Suicide by nearby entity (ex: Microwave)
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
{
// Skip any nearby items that can be picked up, we already checked the active held item above
if (itemQuery.HasComponent(entity))
continue;
RaiseLocalEvent(entity, suicideEvent);
if (suicideEvent.Handled)
return true;
}
var suicideGhostEvent = new SuicideGhostEvent(victim);
RaiseLocalEvent(victim, suicideGhostEvent);
// Suicide is considered a fail if the user wasn't able to ghost
// Suiciding with the CannotSuicide tag will ghost the player but not kill the body
if (!suicideGhostEvent.Handled || _tagSystem.HasTag(victim, "CannotSuicide"))
return false;
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
var suicideEvent = new SuicideEvent(victim);
RaiseLocalEvent(victim, suicideEvent);
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided.");
return true;
}
/// <summary>
/// Event subscription created to handle the ghosting aspect relating to suicides
/// Mainly useful when you can raise an event in Shared and can't call Suicide() directly
/// </summary>
private void OnSuicideGhost(Entity<MindContainerComponent> victim, ref SuicideGhostEvent args)
{
if (args.Handled)
return;
if (victim.Comp.Mind == null)
return;
if (!TryComp<MindComponent>(victim.Comp.Mind, out var mindComponent))
return;
// CannotSuicide tag will allow the user to ghost, but also return to their mind
// This is kind of weird, not sure what it applies to?
if (_tagSystem.HasTag(victim, "CannotSuicide"))
args.CanReturnToBody = true;
if (_gameTicker.OnGhostAttempt(victim.Comp.Mind.Value, args.CanReturnToBody, mind: mindComponent))
args.Handled = true;
}
/// <summary>
/// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
/// </summary>
private void OnEnvironmentalSuicide(Entity<MobStateComponent> victim, ref SuicideEvent args)
{
if (args.Handled || _mobState.IsCritical(victim))
return;
var suicideByEnvironmentEvent = new SuicideByEnvironmentEvent(victim);
// Try to suicide by raising an event on the held item
if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
&& handsComponent.ActiveHandEntity is { } item)
{
RaiseLocalEvent(item, suicideByEnvironmentEvent);
if (suicideByEnvironmentEvent.Handled)
{
args.Handled = suicideByEnvironmentEvent.Handled;
return;
}
}
private void ApplyDeath(EntityUid target, SuicideKind kind)
// Try to suicide by nearby entities, like Microwaves or Crematoriums, by raising an event on it
// Returns upon being handled by any entity
var itemQuery = GetEntityQuery<ItemComponent>();
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
{
if (kind == SuicideKind.Special)
return;
// Skip any nearby items that can be picked up, we already checked the active held item above
if (itemQuery.HasComponent(entity))
continue;
if (!_prototypeManager.TryIndex<DamageTypePrototype>(kind.ToString(), out var damagePrototype))
{
const SuicideKind fallback = SuicideKind.Blunt;
Log.Error($"{nameof(SuicideSystem)} could not find the damage type prototype associated with {kind}. Falling back to {fallback}");
damagePrototype = _prototypeManager.Index<DamageTypePrototype>(fallback.ToString());
}
const int lethalAmountOfDamage = 200; // TODO: Would be nice to get this number from somewhere else
_damageableSystem.TryChangeDamage(target, new(damagePrototype, lethalAmountOfDamage), true, origin: target);
RaiseLocalEvent(entity, suicideByEnvironmentEvent);
if (!suicideByEnvironmentEvent.Handled)
continue;
args.Handled = suicideByEnvironmentEvent.Handled;
return;
}
}
/// <summary>
/// Default suicide behavior for any kind of entity that can take damage
/// </summary>
private void OnDamageableSuicide(Entity<DamageableComponent> victim, ref SuicideEvent args)
{
if (args.Handled)
return;
var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
_popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
var selfMessage = Loc.GetString("suicide-command-default-text-self");
_popup.PopupEntity(selfMessage, victim, victim);
if (args.DamageSpecifier != null)
{
_suicide.ApplyLethalDamage(victim, args.DamageSpecifier);
args.Handled = true;
return;
}
args.DamageType ??= "Bloodloss";
_suicide.ApplyLethalDamage(victim, args.DamageType);
args.Handled = true;
}
}