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

@@ -0,0 +1,365 @@
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Execution;
using Content.Shared.FixedPoint;
using Content.Shared.Ghost;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Commands;
[TestFixture]
public sealed class SuicideCommandTests
{
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: SharpTestObject
name: very sharp test object
components:
- type: Item
- type: MeleeWeapon
damage:
types:
Slash: 5
- type: Execution
- type: entity
id: MixedDamageTestObject
name: mixed damage test object
components:
- type: Item
- type: MeleeWeapon
damage:
types:
Slash: 5
Blunt: 5
- type: Execution
- type: entity
id: TestMaterialReclaimer
name: test version of the material reclaimer
components:
- type: MaterialReclaimer";
/// <summary>
/// Run the suicide command in the console
/// Should successfully kill the player and ghost them
/// </summary>
[Test]
public async Task TestSuicide()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Connected = true,
Dirty = true,
DummyTicker = false
});
var server = pair.Server;
var consoleHost = server.ResolveDependency<IConsoleHost>();
var entManager = server.ResolveDependency<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
var mind = mindSystem.GetMind(player);
MindComponent mindComponent = default;
MobStateComponent mobStateComp = default;
await server.WaitPost(() =>
{
if (mind != null)
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
});
// Check that running the suicide command kills the player
// and properly ghosts them without them being able to return to their body
await server.WaitAssertion(() =>
{
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
Assert.Multiple(() =>
{
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
!ghostComp.CanReturnToBody);
});
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Run the suicide command while the player is already injured
/// This should only deal as much damage as necessary to get to the dead threshold
/// </summary>
[Test]
public async Task TestSuicideWhileDamaged()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Connected = true,
Dirty = true,
DummyTicker = false
});
var server = pair.Server;
var consoleHost = server.ResolveDependency<IConsoleHost>();
var entManager = server.ResolveDependency<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var damageableSystem = entManager.System<DamageableSystem>();
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
var mind = mindSystem.GetMind(player);
MindComponent mindComponent = default;
MobStateComponent mobStateComp = default;
MobThresholdsComponent mobThresholdsComp = default;
DamageableComponent damageableComp = default;
await server.WaitPost(() =>
{
if (mind != null)
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
damageableComp = entManager.GetComponent<DamageableComponent>(player);
});
if (protoMan.TryIndex<DamageTypePrototype>("Slash", out var slashProto))
damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
// Check that running the suicide command kills the player
// and properly ghosts them without them being able to return to their body
// and that all the damage is concentrated in the Slash category
await server.WaitAssertion(() =>
{
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
Assert.Multiple(() =>
{
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
!ghostComp.CanReturnToBody);
Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(lethalDamageThreshold));
});
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Run the suicide command in the console
/// Should only ghost the player but not kill them
/// </summary>
[Test]
public async Task TestSuicideWhenCannotSuicide()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Connected = true,
Dirty = true,
DummyTicker = false
});
var server = pair.Server;
var consoleHost = server.ResolveDependency<IConsoleHost>();
var entManager = server.ResolveDependency<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
var tagSystem = entManager.System<TagSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
var mind = mindSystem.GetMind(player);
MindComponent mindComponent = default;
MobStateComponent mobStateComp = default;
await server.WaitPost(() =>
{
if (mind != null)
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
});
tagSystem.AddTag(player, "CannotSuicide");
// Check that running the suicide command kills the player
// and properly ghosts them without them being able to return to their body
await server.WaitAssertion(() =>
{
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
Assert.Multiple(() =>
{
Assert.That(mobStateSystem.IsAlive(player, mobStateComp));
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
!ghostComp.CanReturnToBody);
});
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Run the suicide command while the player is holding an execution-capable weapon
/// </summary>
[Test]
public async Task TestSuicideByHeldItem()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Connected = true,
Dirty = true,
DummyTicker = false
});
var server = pair.Server;
var consoleHost = server.ResolveDependency<IConsoleHost>();
var entManager = server.ResolveDependency<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var handsSystem = entManager.System<SharedHandsSystem>();
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
var transformSystem = entManager.System<TransformSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
var mind = mindSystem.GetMind(player);
MindComponent mindComponent = default;
MobStateComponent mobStateComp = default;
MobThresholdsComponent mobThresholdsComp = default;
DamageableComponent damageableComp = default;
HandsComponent handsComponent = default;
await server.WaitPost(() =>
{
if (mind != null)
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
damageableComp = entManager.GetComponent<DamageableComponent>(player);
handsComponent = entManager.GetComponent<HandsComponent>(player);
});
// Spawn the weapon of choice and put it in the player's hands
await server.WaitPost(() =>
{
var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
Assert.That(executionComponent, Is.Not.EqualTo(null));
});
// Check that running the suicide command kills the player
// and properly ghosts them without them being able to return to their body
// and that all the damage is concentrated in the Slash category
await server.WaitAssertion(() =>
{
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
Assert.Multiple(() =>
{
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
!ghostComp.CanReturnToBody);
Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold));
});
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Run the suicide command while the player is holding an execution-capable weapon
/// with damage spread between slash and blunt
/// </summary>
[Test]
public async Task TestSuicideByHeldItemSpreadDamage()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Connected = true,
Dirty = true,
DummyTicker = false
});
var server = pair.Server;
var consoleHost = server.ResolveDependency<IConsoleHost>();
var entManager = server.ResolveDependency<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var handsSystem = entManager.System<SharedHandsSystem>();
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
var transformSystem = entManager.System<TransformSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
var mind = mindSystem.GetMind(player);
MindComponent mindComponent = default;
MobStateComponent mobStateComp = default;
MobThresholdsComponent mobThresholdsComp = default;
DamageableComponent damageableComp = default;
HandsComponent handsComponent = default;
await server.WaitPost(() =>
{
if (mind != null)
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
damageableComp = entManager.GetComponent<DamageableComponent>(player);
handsComponent = entManager.GetComponent<HandsComponent>(player);
});
// Spawn the weapon of choice and put it in the player's hands
await server.WaitPost(() =>
{
var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
Assert.That(executionComponent, Is.Not.EqualTo(null));
});
// Check that running the suicide command kills the player
// and properly ghosts them without them being able to return to their body
// and that slash damage is split in half
await server.WaitAssertion(() =>
{
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
Assert.Multiple(() =>
{
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
!ghostComp.CanReturnToBody);
Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2));
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.GameTicking;
using Content.Server.Popups;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Mind;
using Robust.Shared.Console;
using Robust.Shared.Enums;
@@ -32,15 +33,13 @@ namespace Content.Server.Chat.Commands
var minds = _e.System<SharedMindSystem>();
// This check also proves mind not-null for at the end when the mob is ghosted.
if (!minds.TryGetMind(player, out var mindId, out var mind) ||
mind.OwnedEntity is not { Valid: true } victim)
if (!minds.TryGetMind(player, out var mindId, out var mindComp) ||
mindComp.OwnedEntity is not { Valid: true } victim)
{
shell.WriteLine(Loc.GetString("suicide-command-no-mind"));
return;
}
var gameTicker = _e.System<GameTicker>();
var suicideSystem = _e.System<SuicideSystem>();
if (_e.HasComponent<AdminFrozenComponent>(victim))
@@ -53,14 +52,6 @@ namespace Content.Server.Chat.Commands
}
if (suicideSystem.Suicide(victim))
{
// Prevent the player from returning to the body.
// Note that mind cannot be null because otherwise victim would be null.
gameTicker.OnGhostAttempt(mindId, false, mind: mind);
return;
}
if (gameTicker.OnGhostAttempt(mindId, true, mind: mind))
return;
shell.WriteLine(Loc.GetString("ghost-command-denied"));

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;
}
}

View File

@@ -38,16 +38,20 @@ public sealed partial class TriggerSystem
Trigger(uid);
}
/// <summary>
/// Checks if the user has any implants that prevent suicide to avoid some cheesy strategies
/// Prevents suicide by handling the event without killing the user
/// </summary>
private void OnSuicide(EntityUid uid, TriggerOnMobstateChangeComponent component, SuicideEvent args)
{
if (args.Handled)
return;
if (component.PreventSuicide)
{
_popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
args.BlockSuicideAttempt(component.PreventSuicide);
}
if (!component.PreventSuicide)
return;
_popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
args.Handled = true;
}
private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<SuicideEvent> args)

View File

@@ -2,6 +2,8 @@ using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
@@ -16,7 +18,6 @@ using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
@@ -36,6 +37,7 @@ namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
public override void Initialize()
{
@@ -48,31 +50,38 @@ namespace Content.Server.Kitchen.EntitySystems
//DoAfter
SubscribeLocalEvent<KitchenSpikeComponent, SpikeDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<KitchenSpikeComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ButcherableComponent, CanDropDraggedEvent>(OnButcherableCanDrop);
}
private void OnButcherableCanDrop(EntityUid uid, ButcherableComponent component, ref CanDropDraggedEvent args)
private void OnButcherableCanDrop(Entity<ButcherableComponent> entity, ref CanDropDraggedEvent args)
{
args.Handled = true;
args.CanDrop |= component.Type != ButcheringType.Knife;
args.CanDrop |= entity.Comp.Type != ButcheringType.Knife;
}
private void OnSuicide(EntityUid uid, KitchenSpikeComponent component, SuicideEvent args)
/// <summary>
/// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them.
/// </summary>
private void OnSuicideByEnvironment(Entity<KitchenSpikeComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Piercing);
var victim = args.Victim;
var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", victim));
_popupSystem.PopupEntity(othersMessage, victim);
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Piercing");
var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", args.Victim));
_popupSystem.PopupEntity(othersMessage, args.Victim, Filter.PvsExcept(args.Victim), true);
var selfMessage = Loc.GetString("comp-kitchen-spike-suicide-self");
_popupSystem.PopupEntity(selfMessage, victim, victim);
_popupSystem.PopupEntity(selfMessage, args.Victim, args.Victim);
args.Handled = true;
}
private void OnDoAfter(EntityUid uid, KitchenSpikeComponent component, DoAfterEvent args)
private void OnDoAfter(Entity<KitchenSpikeComponent> entity, ref SpikeDoAfterEvent args)
{
if (args.Args.Target == null)
return;
@@ -82,49 +91,49 @@ namespace Content.Server.Kitchen.EntitySystems
if (args.Cancelled)
{
component.InUse = false;
entity.Comp.InUse = false;
return;
}
if (args.Handled)
return;
if (Spikeable(uid, args.Args.User, args.Args.Target.Value, component, butcherable))
Spike(uid, args.Args.User, args.Args.Target.Value, component);
if (Spikeable(entity, args.Args.User, args.Args.Target.Value, entity.Comp, butcherable))
Spike(entity, args.Args.User, args.Args.Target.Value, entity.Comp);
component.InUse = false;
entity.Comp.InUse = false;
args.Handled = true;
}
private void OnDragDrop(EntityUid uid, KitchenSpikeComponent component, ref DragDropTargetEvent args)
private void OnDragDrop(Entity<KitchenSpikeComponent> entity, ref DragDropTargetEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (Spikeable(uid, args.User, args.Dragged, component))
TrySpike(uid, args.User, args.Dragged, component);
if (Spikeable(entity, args.User, args.Dragged, entity.Comp))
TrySpike(entity, args.User, args.Dragged, entity.Comp);
}
private void OnInteractHand(EntityUid uid, KitchenSpikeComponent component, InteractHandEvent args)
private void OnInteractHand(Entity<KitchenSpikeComponent> entity, ref InteractHandEvent args)
{
if (args.Handled)
return;
if (component.PrototypesToSpawn?.Count > 0)
if (entity.Comp.PrototypesToSpawn?.Count > 0)
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User);
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), entity, args.User);
args.Handled = true;
}
}
private void OnInteractUsing(EntityUid uid, KitchenSpikeComponent component, InteractUsingEvent args)
private void OnInteractUsing(Entity<KitchenSpikeComponent> entity, ref InteractUsingEvent args)
{
if (args.Handled)
return;
if (TryGetPiece(uid, args.User, args.Used))
if (TryGetPiece(entity, args.User, args.Used))
args.Handled = true;
}

View File

@@ -39,6 +39,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.Stacks;
using Content.Server.Construction.Components;
using Content.Shared.Chat;
using Content.Shared.Damage;
namespace Content.Server.Kitchen.EntitySystems
{
@@ -65,6 +67,7 @@ namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string MalfunctionSpark = "Spark";
@@ -83,7 +86,7 @@ namespace Content.Server.Kitchen.EntitySystems
SubscribeLocalEvent<MicrowaveComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<MicrowaveComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MicrowaveComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<MicrowaveComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<MicrowaveComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<MicrowaveComponent, SignalReceivedEvent>(OnSignalReceived);
@@ -260,12 +263,22 @@ namespace Content.Server.Kitchen.EntitySystems
_deviceLink.EnsureSinkPorts(ent, ent.Comp.OnPort);
}
private void OnSuicide(Entity<MicrowaveComponent> ent, ref SuicideEvent args)
/// <summary>
/// Kills the user by microwaving their head
/// TODO: Make this not awful, it keeps any items attached to your head still on and you can revive someone and cogni them so you have some dumb headless fuck running around. I've seen it happen.
/// </summary>
private void OnSuicideByEnvironment(Entity<MicrowaveComponent> ent, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Heat);
// The act of getting your head microwaved doesn't actually kill you
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
// The application of lethal damage is what kills you...
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat");
var victim = args.Victim;
var headCount = 0;
@@ -295,6 +308,7 @@ namespace Content.Server.Kitchen.EntitySystems
ent.Comp.CurrentCookTimerTime = 10;
Wzhzhzh(ent.Owner, ent.Comp, args.Victim);
UpdateUserInterfaceState(ent.Owner, ent.Comp);
args.Handled = true;
}
private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionContainerChangedEvent args)

View File

@@ -1,4 +1,4 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.GameTicking;
using Content.Server.Popups;
@@ -48,7 +48,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
SubscribeLocalEvent<MaterialReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MaterialReclaimerComponent, InteractUsingEvent>(OnInteractUsing,
before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)});
SubscribeLocalEvent<MaterialReclaimerComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<MaterialReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ActiveMaterialReclaimerComponent, PowerChangedEvent>(OnActivePowerChanged);
}
private void OnStartup(Entity<MaterialReclaimerComponent> entity, ref ComponentStartup args)
@@ -86,12 +86,11 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
args.Handled = TryStartProcessItem(entity.Owner, args.Used, entity.Comp, args.User);
}
private void OnSuicide(Entity<MaterialReclaimerComponent> entity, ref SuicideEvent args)
private void OnSuicideByEnvironment(Entity<MaterialReclaimerComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Bloodloss);
var victim = args.Victim;
if (TryComp(victim, out ActorComponent? actor) &&
_mind.TryGetMind(actor.PlayerSession, out var mindId, out var mind))
@@ -103,12 +102,15 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
}
}
_popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others", ("victim", Identity.Entity(victim, EntityManager))),
_popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others",
("victim", Identity.Entity(victim, EntityManager))),
victim,
Filter.PvsExcept(victim, entityManager: EntityManager), true);
Filter.PvsExcept(victim, entityManager: EntityManager),
true);
_body.GibBody(victim, true);
_appearance.SetData(entity.Owner, RecyclerVisuals.Bloody, true);
args.Handled = true;
}
private void OnActivePowerChanged(Entity<ActiveMaterialReclaimerComponent> entity, ref PowerChangedEvent args)

View File

@@ -106,11 +106,11 @@ namespace Content.Server.Medical.BiomassReclaimer
SubscribeLocalEvent<BiomassReclaimerComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<BiomassReclaimerComponent, ClimbedOnEvent>(OnClimbedOn);
SubscribeLocalEvent<BiomassReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<BiomassReclaimerComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<BiomassReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<BiomassReclaimerComponent, ReclaimerDoAfterEvent>(OnDoAfter);
}
private void OnSuicide(Entity<BiomassReclaimerComponent> ent, ref SuicideEvent args)
private void OnSuicideByEnvironment(Entity<BiomassReclaimerComponent> ent, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
@@ -123,7 +123,7 @@ namespace Content.Server.Medical.BiomassReclaimer
_popup.PopupEntity(Loc.GetString("biomass-reclaimer-suicide-others", ("victim", args.Victim)), ent, PopupType.LargeCaution);
StartProcessing(args.Victim, ent);
args.SetHandled(SuicideKind.Blunt);
args.Handled = true;
}
private void OnInit(EntityUid uid, ActiveBiomassReclaimerComponent component, ComponentInit args)

View File

@@ -38,7 +38,7 @@ public sealed class CrematoriumSystem : EntitySystem
SubscribeLocalEvent<CrematoriumComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<CrematoriumComponent, GetVerbsEvent<AlternativeVerb>>(AddCremateVerb);
SubscribeLocalEvent<CrematoriumComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<CrematoriumComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ActiveCrematoriumComponent, StorageOpenAttemptEvent>(OnAttemptOpen);
}
@@ -146,11 +146,10 @@ public sealed class CrematoriumSystem : EntitySystem
_audio.PlayPvs(component.CremateFinishSound, uid);
}
private void OnSuicide(EntityUid uid, CrematoriumComponent component, SuicideEvent args)
private void OnSuicideByEnvironment(EntityUid uid, CrematoriumComponent component, SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Heat);
var victim = args.Victim;
if (TryComp(victim, out ActorComponent? actor) && _minds.TryGetMind(victim, out var mindId, out var mind))
@@ -179,6 +178,7 @@ public sealed class CrematoriumSystem : EntitySystem
}
_entityStorage.CloseStorage(uid);
Cremate(uid, component);
args.Handled = true;
}
public override void Update(float frameTime)

View File

@@ -0,0 +1,67 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Mobs.Components;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Shared.Chat;
public sealed class SharedSuicideSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
/// <summary>
/// Applies lethal damage spread out across the damage types given.
/// </summary>
public void ApplyLethalDamage(Entity<DamageableComponent> target, DamageSpecifier damageSpecifier)
{
// Create a new damageSpecifier so that we don't make alterations to the original DamageSpecifier
// Failing to do this will permanently change a weapon's damage making it insta-kill people
var appliedDamageSpecifier = new DamageSpecifier(damageSpecifier);
if (!TryComp<MobThresholdsComponent>(target, out var mobThresholds))
return;
// Mob thresholds are sorted from alive -> crit -> dead,
// grabbing the last key will give us how much damage is needed to kill a target from zero
// The exact lethal damage amount is adjusted based on their current damage taken
var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage;
var totalDamage = appliedDamageSpecifier.GetTotal();
// Removing structural because it causes issues against entities that cannot take structural damage,
// then getting the total to use in calculations for spreading out damage.
appliedDamageSpecifier.DamageDict.Remove("Structural");
// Split the total amount of damage needed to kill the target by every damage type in the DamageSpecifier
foreach (var (key, value) in appliedDamageSpecifier.DamageDict)
{
appliedDamageSpecifier.DamageDict[key] = Math.Ceiling((double) (value * lethalAmountOfDamage / totalDamage));
}
_damageableSystem.TryChangeDamage(target, appliedDamageSpecifier, true, origin: target);
}
/// <summary>
/// Applies lethal damage in a single type, specified by a single damage type.
/// </summary>
public void ApplyLethalDamage(Entity<DamageableComponent> target, ProtoId<DamageTypePrototype>? damageType)
{
if (!TryComp<MobThresholdsComponent>(target, out var mobThresholds))
return;
// Mob thresholds are sorted from alive -> crit -> dead,
// grabbing the last key will give us how much damage is needed to kill a target from zero
// The exact lethal damage amount is adjusted based on their current damage taken
var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage;
// We don't want structural damage for the same reasons listed above
if (!_prototypeManager.TryIndex(damageType, out var damagePrototype) || damagePrototype.ID == "Structural")
{
Log.Error($"{nameof(SharedSuicideSystem)} could not find the damage type prototype associated with {damageType}. Falling back to Blunt");
damagePrototype = _prototypeManager.Index<DamageTypePrototype>("Blunt");
}
var damage = new DamageSpecifier(damagePrototype, lethalAmountOfDamage);
_damageableSystem.TryChangeDamage(target, damage, true, origin: target);
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Execution;
[Serializable, NetSerializable]
public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -0,0 +1,77 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Execution;
/// <summary>
/// Added to entities that can be used to execute another target.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExecutionComponent : Component
{
/// <summary>
/// How long the execution duration lasts.
/// </summary>
[DataField, AutoNetworkedField]
public float DoAfterDuration = 5f;
/// <summary>
/// Arbitrarily chosen number to multiply damage by, used to deal reasonable amounts of damage to a victim of an execution.
/// /// </summary>
[DataField, AutoNetworkedField]
public float DamageMultiplier = 9f;
/// <summary>
/// Shown to the person performing the melee execution (attacker) upon starting a melee execution.
/// </summary>
[DataField]
public LocId InternalMeleeExecutionMessage = "execution-popup-melee-initial-internal";
/// <summary>
/// Shown to bystanders and the victim of a melee execution when a melee execution is started.
/// </summary>
[DataField]
public LocId ExternalMeleeExecutionMessage = "execution-popup-melee-initial-external";
/// <summary>
/// Shown to the attacker upon completion of a melee execution.
/// </summary>
[DataField]
public LocId CompleteInternalMeleeExecutionMessage = "execution-popup-melee-complete-internal";
/// <summary>
/// Shown to bystanders and the victim of a melee execution when a melee execution is completed.
/// </summary>
[DataField]
public LocId CompleteExternalMeleeExecutionMessage = "execution-popup-melee-complete-external";
/// <summary>
/// Shown to the person performing the self execution when starting one.
/// </summary>
[DataField]
public LocId InternalSelfExecutionMessage = "execution-popup-self-initial-internal";
/// <summary>
/// Shown to bystanders near a self execution when one is started.
/// </summary>
[DataField]
public LocId ExternalSelfExecutionMessage = "execution-popup-self-initial-external";
/// <summary>
/// Shown to the person performing a self execution upon completion of a do-after or on use of /suicide with a weapon that has the Execution component.
/// </summary>
[DataField]
public LocId CompleteInternalSelfExecutionMessage = "execution-popup-self-complete-internal";
/// <summary>
/// Shown to bystanders when a self execution is completed or a suicide via execution weapon happens nearby.
/// </summary>
[DataField]
public LocId CompleteExternalSelfExecutionMessage = "execution-popup-self-complete-external";
// Not networked because this is transient inside of a tick.
/// <summary>
/// True if it is currently executing for handlers.
/// </summary>
[DataField]
public bool Executing = false;
}

View File

@@ -0,0 +1,234 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Chat;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind;
using Robust.Shared.Player;
using Robust.Shared.Audio.Systems;
namespace Content.Shared.Execution;
/// <summary>
/// Verb for violently murdering cuffed creatures.
/// </summary>
public sealed class SharedExecutionSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly SharedExecutionSystem _execution = default!;
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExecutionComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionsVerbs);
SubscribeLocalEvent<ExecutionComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
SubscribeLocalEvent<ExecutionComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ExecutionComponent, ExecutionDoAfterEvent>(OnExecutionDoAfter);
}
private void OnGetInteractionsVerbs(EntityUid uid, ExecutionComponent comp, GetVerbsEvent<UtilityVerb> 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 (!CanBeExecuted(victim, attacker))
return;
UtilityVerb verb = new()
{
Act = () => TryStartExecutionDoAfter(weapon, victim, attacker, comp),
Impact = LogImpact.High,
Text = Loc.GetString("execution-verb-name"),
Message = Loc.GetString("execution-verb-message"),
};
args.Verbs.Add(verb);
}
private void TryStartExecutionDoAfter(EntityUid weapon, EntityUid victim, EntityUid attacker, ExecutionComponent comp)
{
if (!CanBeExecuted(victim, attacker))
return;
if (attacker == victim)
{
ShowExecutionInternalPopup(comp.InternalSelfExecutionMessage, attacker, victim, weapon);
ShowExecutionExternalPopup(comp.ExternalSelfExecutionMessage, attacker, victim, weapon);
}
else
{
ShowExecutionInternalPopup(comp.InternalMeleeExecutionMessage, attacker, victim, weapon);
ShowExecutionExternalPopup(comp.ExternalMeleeExecutionMessage, attacker, victim, weapon);
}
var doAfter =
new DoAfterArgs(EntityManager, attacker, comp.DoAfterDuration, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon)
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = true
};
_doAfter.TryStartDoAfter(doAfter);
}
public bool CanBeExecuted(EntityUid victim, EntityUid attacker)
{
// No point executing someone if they can't take damage
if (!HasComp<DamageableComponent>(victim))
return false;
// You can't execute something that cannot die
if (!TryComp<MobStateComponent>(victim, out var mobState))
return false;
// You're not allowed to execute dead people (no fun allowed)
if (_mobState.IsDead(victim, mobState))
return false;
// You must be able to attack people to execute
if (!_actionBlocker.CanAttack(attacker, victim))
return false;
// The victim must be incapacitated to be executed
if (victim != attacker && _actionBlocker.CanInteract(victim, null))
return false;
// All checks passed
return true;
}
private void OnGetMeleeDamage(Entity<ExecutionComponent> entity, ref GetMeleeDamageEvent args)
{
if (!TryComp<MeleeWeaponComponent>(entity, out var melee) || !entity.Comp.Executing)
{
return;
}
var bonus = melee.Damage * entity.Comp.DamageMultiplier - melee.Damage;
args.Damage += bonus;
args.ResistanceBypass = true;
}
private void OnSuicideByEnvironment(Entity<ExecutionComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (!TryComp<MeleeWeaponComponent>(entity, out var melee))
return;
string? internalMsg = entity.Comp.CompleteInternalSelfExecutionMessage;
string? externalMsg = entity.Comp.CompleteExternalSelfExecutionMessage;
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
ShowExecutionInternalPopup(internalMsg, args.Victim, args.Victim, entity, false);
ShowExecutionExternalPopup(externalMsg, args.Victim, args.Victim, entity);
_audio.PlayPredicted(melee.HitSound, args.Victim, args.Victim);
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), melee.Damage);
args.Handled = true;
}
private void ShowExecutionInternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon, bool predict = true)
{
if (predict)
{
_popup.PopupClient(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
attacker,
PopupType.MediumCaution
);
}
else
{
_popup.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
attacker,
PopupType.MediumCaution
);
}
}
private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon)
{
_popup.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
Filter.PvsExcept(attacker),
true,
PopupType.MediumCaution
);
}
private void OnExecutionDoAfter(Entity<ExecutionComponent> entity, ref ExecutionDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
return;
if (!TryComp<MeleeWeaponComponent>(entity, out var meleeWeaponComp))
return;
var attacker = args.User;
var victim = args.Target.Value;
var weapon = args.Used.Value;
if (!_execution.CanBeExecuted(victim, attacker))
return;
// This is needed so the melee system does not stop it.
var prev = _combat.IsInCombatMode(attacker);
_combat.SetInCombatMode(attacker, true);
entity.Comp.Executing = true;
var internalMsg = entity.Comp.CompleteInternalMeleeExecutionMessage;
var externalMsg = entity.Comp.CompleteExternalMeleeExecutionMessage;
if (attacker == victim)
{
var suicideEvent = new SuicideEvent(victim);
RaiseLocalEvent(victim, suicideEvent);
var suicideGhostEvent = new SuicideGhostEvent(victim);
RaiseLocalEvent(victim, suicideGhostEvent);
}
else
{
_melee.AttemptLightAttack(attacker, weapon, meleeWeaponComp, victim);
}
_combat.SetInCombatMode(attacker, prev);
entity.Comp.Executing = false;
args.Handled = true;
if (attacker != victim)
{
_execution.ShowExecutionInternalPopup(internalMsg, attacker, victim, entity);
_execution.ShowExecutionExternalPopup(externalMsg, attacker, victim, entity);
}
}
}

View File

@@ -1,48 +1,41 @@
namespace Content.Shared.Interaction.Events
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised Directed at an entity to check whether they will handle the suicide.
/// </summary>
public sealed class SuicideEvent : HandledEntityEventArgs
{
/// <summary>
/// Raised Directed at an entity to check whether they will handle the suicide.
/// </summary>
public sealed class SuicideEvent : EntityEventArgs
public SuicideEvent(EntityUid victim)
{
public SuicideEvent(EntityUid victim)
{
Victim = victim;
}
public void SetHandled(SuicideKind kind)
{
if (Handled)
throw new InvalidOperationException("Suicide was already handled");
Kind = kind;
}
public void BlockSuicideAttempt(bool suicideAttempt)
{
if (suicideAttempt)
AttemptBlocked = suicideAttempt;
}
public SuicideKind? Kind { get; private set; }
public EntityUid Victim { get; private set; }
public bool AttemptBlocked { get; private set; }
public bool Handled => Kind != null;
Victim = victim;
}
public enum SuicideKind
{
Special, //Doesn't damage the mob, used for "weird" suicides like gibbing
//Damage type suicides
Blunt,
Slash,
Piercing,
Heat,
Shock,
Cold,
Poison,
Radiation,
Asphyxiation,
Bloodloss
}
public DamageSpecifier? DamageSpecifier;
public ProtoId<DamageTypePrototype>? DamageType;
public EntityUid Victim { get; private set; }
}
public sealed class SuicideByEnvironmentEvent : HandledEntityEventArgs
{
public SuicideByEnvironmentEvent(EntityUid victim)
{
Victim = victim;
}
public EntityUid Victim { get; set; }
}
public sealed class SuicideGhostEvent : HandledEntityEventArgs
{
public SuicideGhostEvent(EntityUid victim)
{
Victim = victim;
}
public EntityUid Victim { get; set; }
public bool CanReturnToBody;
}

View File

@@ -168,15 +168,17 @@ public abstract class SharedMindSystem : EntitySystem
args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]");
}
/// <summary>
/// Checks to see if the user's mind prevents them from suicide
/// Handles the suicide event without killing the user if true
/// </summary>
private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args)
{
if (args.Handled)
return;
if (TryComp(component.Mind, out MindComponent? mind) && mind.PreventSuicide)
{
args.BlockSuicideAttempt(true);
}
args.Handled = true;
}
public EntityUid? GetMind(EntityUid uid, MindContainerComponent? mind = null)

View File

@@ -80,7 +80,7 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs
/// Raised on a melee weapon to calculate potential damage bonuses or decreases.
/// </summary>
[ByRefEvent]
public record struct GetMeleeDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid User);
public record struct GetMeleeDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid User, bool ResistanceBypass = false);
/// <summary>
/// Raised on a melee weapon to calculate the attack rate.

View File

@@ -67,6 +67,12 @@ public sealed partial class MeleeWeaponComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool AutoAttack;
/// <summary>
/// If true, attacks will bypass armor resistances.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool ResistanceBypass = false;
/// <summary>
/// Base damage for this weapon. Can be modified via heavy damage or other means.
/// </summary>

View File

@@ -216,7 +216,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (!Resolve(uid, ref component, false))
return new DamageSpecifier();
var ev = new GetMeleeDamageEvent(uid, new (component.Damage), new(), user);
var ev = new GetMeleeDamageEvent(uid, new(component.Damage), new(), user, component.ResistanceBypass);
RaiseLocalEvent(uid, ref ev);
return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers);
@@ -244,6 +244,17 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
return ev.DamageModifier * ev.Multipliers;
}
public bool GetResistanceBypass(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
var ev = new GetMeleeDamageEvent(uid, new(component.Damage), new(), user, component.ResistanceBypass);
RaiseLocalEvent(uid, ref ev);
return ev.ResistanceBypass;
}
public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen(true)] out MeleeWeaponComponent? melee)
{
weaponUid = default;
@@ -441,6 +452,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
// If I do not come back later to fix Light Attacks being Heavy Attacks you can throw me in the spider pit -Errant
var damage = GetDamage(meleeUid, user, component) * GetHeavyDamageModifier(meleeUid, user, component);
var target = GetEntity(ev.Target);
var resistanceBypass = GetResistanceBypass(meleeUid, user, component);
// For consistency with wide attacks stuff needs damageable.
if (Deleted(target) ||
@@ -497,7 +509,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
RaiseLocalEvent(target.Value, attackedEvent);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user);
var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user, ignoreResistances:resistanceBypass);
if (damageResult is {Empty: false})
{

View File

@@ -0,0 +1,17 @@
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-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}!
execution-popup-self-initial-internal = You ready {THE($weapon)} against your own throat.
execution-popup-self-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against their own throat.
execution-popup-self-complete-internal = You slit your own throat!
execution-popup-self-complete-external = {$attacker} slits their own throat!

View File

@@ -6,6 +6,8 @@
description: A small piece of crystal.
components:
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite
layers:
- sprite: Objects/Materials/Shards/crystal.rsi

View File

@@ -5,6 +5,8 @@
description: It's a shard of some unknown material.
components:
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite
layers:
- sprite: Objects/Materials/Shards/shard.rsi

View File

@@ -5,6 +5,8 @@
description: In Space Glasgow this is called a conversation starter.
components:
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: MeleeWeapon
attackRate: 1.5
damage:

View File

@@ -5,6 +5,8 @@
description: A grotesque blade made out of bone and flesh that cleaves through people as a hot knife through butter.
components:
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite
sprite: Objects/Weapons/Melee/armblade.rsi
state: icon

View File

@@ -8,6 +8,8 @@
tags:
- FireAxe
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite
sprite: Objects/Weapons/Melee/fireaxe.rsi
state: icon

View File

@@ -7,6 +7,8 @@
tags:
- Knife
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Utensil
types:
- Knife

View File

@@ -4,6 +4,8 @@
abstract: true
components:
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: MeleeWeapon
wideAnimationRotation: -135
- type: Sprite