Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Melee Executions #30104

Merged
merged 35 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9e000af
melee executions
Scribbles0 Jul 16, 2024
e9cc495
fix damage bug
Scribbles0 Jul 16, 2024
14e48f0
cleanup
Scribbles0 Jul 16, 2024
5db5c0f
address reviews hopefully
Scribbles0 Jul 17, 2024
05d3688
resistance bypass mechanic
Scribbles0 Jul 17, 2024
12dc6f8
component changes
Scribbles0 Jul 17, 2024
ac52248
self executions (not finished yet)
Scribbles0 Jul 28, 2024
cd799ff
self execs part two
Scribbles0 Jul 28, 2024
75de264
ok i fixed things (still not finished)
Scribbles0 Jul 29, 2024
529bd4e
finish everything
Scribbles0 Jul 30, 2024
6895d97
review stuff
Scribbles0 Jul 31, 2024
0d15591
nuke if (kind = special)
Scribbles0 Jul 31, 2024
32b8817
more review stuffs
Scribbles0 Aug 6, 2024
a29fae1
Make suicide system much less hardcoded and make much more use of events
Plykiya Aug 6, 2024
c1138b6
Fix a dumb bug I introduced
Plykiya Aug 6, 2024
2688885
self execution popups
Scribbles0 Aug 7, 2024
d22297f
Integration tests
Plykiya Aug 7, 2024
c8ab74e
Merge remote-tracking branch 'github-desktop-Scribbles0/melee-executi…
Plykiya Aug 7, 2024
f7df312
Why did they even take 0.5 blunt damage?
Plykiya Aug 7, 2024
b530d81
More consistent integration tests
Plykiya Aug 7, 2024
00a71a9
Destructive equals true
Plykiya Aug 7, 2024
4515174
Allow it to dirty-dispose
Plykiya Aug 7, 2024
4829889
IS THIS WHAT YOU WANT?
Plykiya Aug 7, 2024
e8fcb61
FRESH AND CLEAN
Plykiya Aug 7, 2024
7542799
modifier to multiplier
Scribbles0 Aug 7, 2024
4227a0c
Merge branch 'space-wizards:master' into melee-executions
Plykiya Aug 8, 2024
d311a3b
don't jinx the integration tests
Plykiya Aug 8, 2024
4311cd4
no file-scoped namespace
Plykiya Aug 9, 2024
78f217b
Move the rest of execution to shared, create SuicideGhostEvent
Plykiya Aug 9, 2024
802ac54
handled
Plykiya Aug 9, 2024
fc687d1
Get rid of unused code and add a comment
Plykiya Aug 9, 2024
b3668ea
ghost before suicide
Plykiya Aug 9, 2024
458799d
stop cat suicides
Plykiya Aug 9, 2024
5f5a1c8
popup fix + small suicide change
Scribbles0 Aug 9, 2024
3298292
make it a bit better
Scribbles0 Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Content.Shared/Execution/DoAfterEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;

namespace Content.Shared.Execution;

[Serializable, NetSerializable]
public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent
{
}
26 changes: 26 additions & 0 deletions Content.Shared/Execution/ExecutionComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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;

[DataField, AutoNetworkedField]
public float DamageModifier = 9f;

// 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;
}
208 changes: 208 additions & 0 deletions Content.Shared/Execution/ExecutionSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using Content.Shared.ActionBlocker;
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 Robust.Shared.Player;

namespace Content.Shared.Execution;

/// <summary>
/// Verb for violently murdering cuffed creatures.
/// </summary>
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 ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
[Dependency] private readonly SharedMeleeWeaponSystem _meleeSystem = default!;

// TODO: Still needs more cleaning up.
private const string DefaultInternalMeleeExecutionMessage = "execution-popup-melee-initial-internal";
private const string DefaultExternalMeleeExecutionMessage = "execution-popup-melee-initial-external";
private const string DefaultCompleteInternalMeleeExecutionMessage = "execution-popup-melee-complete-internal";
private const string DefaultCompleteExternalMeleeExecutionMessage = "execution-popup-melee-complete-external";

/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<ExecutionComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionsVerbs);
SubscribeLocalEvent<ExecutionComponent, ExecutionDoAfterEvent>(OnExecutionDoAfter);
SubscribeLocalEvent<ExecutionComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
}

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 (!CanExecuteWithAny(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 (!CanExecuteWithAny(victim, attacker))
return;

// TODO: This should just be on the weapons as a single execution message.
var defaultExecutionInternal = DefaultInternalMeleeExecutionMessage;
var defaultExecutionExternal = DefaultExternalMeleeExecutionMessage;

var internalMsg = defaultExecutionInternal;
var externalMsg = defaultExecutionExternal;
ShowExecutionInternalPopup(internalMsg, attacker, victim, weapon);
ShowExecutionExternalPopup(externalMsg, attacker, victim, weapon);

var doAfter =
new DoAfterArgs(EntityManager, attacker, comp.DoAfterDuration, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon)
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = true
};

_doAfterSystem.TryStartDoAfter(doAfter);

}

private bool CanExecuteWithAny(EntityUid victim, EntityUid attacker)
{
// Use suicide.
if (victim == attacker)
return false;

// No point executing someone if they can't take damage
if (!TryComp<DamageableComponent>(victim, out _))
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 (_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 void OnExecutionDoAfter(EntityUid uid, ExecutionComponent component, ExecutionDoAfterEvent 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 (!CanExecuteWithAny(victim, attacker))
return;

// This is needed so the melee system does not stop it.
var prev = _combatSystem.IsInCombatMode(attacker);
_combatSystem.SetInCombatMode(attacker, true);
component.Executing = true;
string? internalMsg = null;
string? externalMsg = null;

if (TryComp(uid, out MeleeWeaponComponent? melee))
{
_meleeSystem.AttemptLightAttack(attacker, weapon, melee, victim);
internalMsg = DefaultCompleteInternalMeleeExecutionMessage;
externalMsg = DefaultCompleteExternalMeleeExecutionMessage;
}

_combatSystem.SetInCombatMode(attacker, prev);
component.Executing = false;
args.Handled = true;

if (internalMsg != null && externalMsg != null)
{
ShowExecutionInternalPopup(internalMsg, attacker, victim, uid);
ShowExecutionExternalPopup(externalMsg, attacker, victim, uid);
}
}

private void OnGetMeleeDamage(EntityUid uid, ExecutionComponent comp, ref GetMeleeDamageEvent args)
{
if (!TryComp<MeleeWeaponComponent>(uid, out var melee) ||
!TryComp<ExecutionComponent>(uid, out var execComp) ||
!execComp.Executing)
{
return;
}

var bonus = melee.Damage * execComp.DamageModifier - melee.Damage;
args.Damage += bonus;
}

private void ShowExecutionInternalPopup(string locString,
EntityUid attacker, EntityUid victim, EntityUid weapon, bool predict = true)
{
if (predict)
{
_popupSystem.PopupClient(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
attacker,
PopupType.Medium
);
}
else
{
_popupSystem.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
Filter.Entities(attacker),
true,
PopupType.Medium
);
}

}

private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon)
{
_popupSystem.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
Filter.PvsExcept(attacker),
true,
PopupType.MediumCaution
);
}
}
12 changes: 12 additions & 0 deletions Resources/Locale/en-US/execution/execution.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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}!
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Resources/Prototypes/Entities/Objects/Materials/shards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
tags:
- FireAxe
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite
sprite: Objects/Weapons/Melee/fireaxe.rsi
state: icon
Expand Down
2 changes: 2 additions & 0 deletions Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
tags:
- Knife
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Utensil
types:
- Knife
Expand Down
2 changes: 2 additions & 0 deletions Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
abstract: true
components:
- type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: MeleeWeapon
wideAnimationRotation: -135
- type: Sprite
Expand Down
Loading