using Content.Shared.Movement.Events;
using Content.Shared.Speech;
using Content.Shared.Throwing;
+using Content.Shared.Weapons.Melee;
using JetBrains.Annotations;
using Robust.Shared.Containers;
return !ev.Cancelled;
}
- public bool CanAttack(EntityUid uid, EntityUid? target = null)
+ public bool CanAttack(EntityUid uid, EntityUid? target = null, Entity<MeleeWeaponComponent>? weapon = null, bool disarm = false)
{
_container.TryGetOuterContainer(uid, Transform(uid), out var outerContainer);
if (target != null && target != outerContainer?.Owner && _container.IsEntityInContainer(uid))
return containerEv.CanAttack;
}
- var ev = new AttackAttemptEvent(uid, target);
+ var ev = new AttackAttemptEvent(uid, target, weapon, disarm);
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
+using System.Diagnostics.CodeAnalysis;
using Content.Shared.Actions;
using Content.Shared.Alert;
+using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Timing;
namespace Content.Shared.CombatMode.Pacification;
-/// <summary>
-/// Raised when a Pacified entity attempts to throw something.
-/// The throw is only permitted if this event is not cancelled.
-/// </summary>
-[ByRefEvent]
-public struct AttemptPacifiedThrowEvent
-{
- public EntityUid ItemUid;
- public EntityUid PlayerUid;
-
- public AttemptPacifiedThrowEvent(EntityUid itemUid, EntityUid playerUid)
- {
- ItemUid = itemUid;
- PlayerUid = playerUid;
- }
-
- public bool Cancelled { get; private set; } = false;
- public string? CancelReasonMessageId { get; private set; }
-
- /// <param name="reasonMessageId">
- /// Localization string ID for the reason this event has been cancelled.
- /// If null, a generic message will be shown to the player.
- /// Note that any supplied localization string MUST accept a '$projectile'
- /// parameter specifying the name of the thrown entity.
- /// </param>
- public void Cancel(string? reasonMessageId = null)
- {
- Cancelled = true;
- CancelReasonMessageId = reasonMessageId;
- }
-}
-
public sealed class PacificationSystem : EntitySystem
{
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
SubscribeLocalEvent<PacifiedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<PacifiedComponent, BeforeThrowEvent>(OnBeforeThrow);
SubscribeLocalEvent<PacifiedComponent, AttackAttemptEvent>(OnAttackAttempt);
+ SubscribeLocalEvent<PacifiedComponent, ShotAttemptedEvent>(OnShootAttempt);
+ SubscribeLocalEvent<PacifiedComponent, EntityUnpausedEvent>(OnUnpaused);
+ SubscribeLocalEvent<PacifismDangerousAttackComponent, AttemptPacifiedAttackEvent>(OnPacifiedDangerousAttack);
+ }
+
+ private void OnUnpaused(Entity<PacifiedComponent> ent, ref EntityUnpausedEvent args)
+ {
+ if (ent.Comp.NextPopupTime != null)
+ ent.Comp.NextPopupTime = ent.Comp.NextPopupTime.Value + args.PausedTime;
+ }
+
+ private bool PacifiedCanAttack(EntityUid user, EntityUid target, [NotNullWhen(false)] out string? reason)
+ {
+ var ev = new AttemptPacifiedAttackEvent(user);
+
+ RaiseLocalEvent(target, ref ev);
+
+ if (ev.Cancelled)
+ {
+ reason = ev.Reason;
+ return false;
+ }
+
+ reason = null;
+ return true;
+ }
+
+ private void ShowPopup(Entity<PacifiedComponent> user, EntityUid target, string reason)
+ {
+ // Popup logic.
+ // Cooldown is needed because the input events for melee/shooting etc. will fire continuously
+ if (target == user.Comp.LastAttackedEntity
+ && !(_timing.CurTime > user.Comp.NextPopupTime))
+ return;
+
+ _popup.PopupClient(Loc.GetString(reason, ("entity", target)), user, user);
+ user.Comp.NextPopupTime = _timing.CurTime + user.Comp.PopupCooldown;
+ user.Comp.LastAttackedEntity = target;
+ }
+
+ private void OnShootAttempt(Entity<PacifiedComponent> ent, ref ShotAttemptedEvent args)
+ {
+ // Disallow firing guns in all cases.
+ ShowPopup(ent, args.Used, "pacified-cannot-fire-gun");
+ args.Cancel();
}
private void OnAttackAttempt(EntityUid uid, PacifiedComponent component, AttackAttemptEvent args)
{
+ if (component.DisallowAllCombat || args.Disarm && component.DisallowDisarm)
+ {
+ args.Cancel();
+ return;
+ }
+
+ // If it's a disarm, let it go through (unless we disallow them, which is handled earlier)
+ if (args.Disarm)
+ return;
+
+ // Allow attacking with no target. This should be fine.
+ // If it's a wide swing, that will be handled with a later AttackAttemptEvent raise.
+ if (args.Target == null)
+ return;
+
+ // If we would do zero damage, it should be fine.
+ if (args.Weapon != null && args.Weapon.Value.Comp.Damage.GetTotal() == FixedPoint2.Zero)
+ return;
+
+ if (PacifiedCanAttack(uid, args.Target.Value, out var reason))
+ return;
+
+ ShowPopup((uid, component), args.Target.Value, reason);
args.Cancel();
}
if (!TryComp<CombatModeComponent>(uid, out var combatMode))
return;
- if (combatMode.CanDisarm != null)
+ if (component.DisallowDisarm && combatMode.CanDisarm != null)
_combatSystem.SetCanDisarm(uid, false, combatMode);
- _combatSystem.SetInCombatMode(uid, false, combatMode);
- _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
+ if (component.DisallowAllCombat)
+ {
+ _combatSystem.SetInCombatMode(uid, false, combatMode);
+ _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
+ }
+
_alertsSystem.ShowAlert(uid, AlertType.Pacified);
}
var cannotThrowMessage = ev.CancelReasonMessageId ?? "pacified-cannot-throw";
_popup.PopupEntity(Loc.GetString(cannotThrowMessage, ("projectile", itemName)), ent, ent);
}
+
+ private void OnPacifiedDangerousAttack(Entity<PacifismDangerousAttackComponent> ent, ref AttemptPacifiedAttackEvent args)
+ {
+ args.Cancelled = true;
+ args.Reason = "pacified-cannot-harm-indirect";
+ }
}
+
+
+/// <summary>
+/// Raised when a Pacified entity attempts to throw something.
+/// The throw is only permitted if this event is not cancelled.
+/// </summary>
+[ByRefEvent]
+public struct AttemptPacifiedThrowEvent
+{
+ public EntityUid ItemUid;
+ public EntityUid PlayerUid;
+
+ public AttemptPacifiedThrowEvent(EntityUid itemUid, EntityUid playerUid)
+ {
+ ItemUid = itemUid;
+ PlayerUid = playerUid;
+ }
+
+ public bool Cancelled { get; private set; } = false;
+ public string? CancelReasonMessageId { get; private set; }
+
+ /// <param name="reasonMessageId">
+ /// Localization string ID for the reason this event has been cancelled.
+ /// If null, a generic message will be shown to the player.
+ /// Note that any supplied localization string MUST accept a '$projectile'
+ /// parameter specifying the name of the thrown entity.
+ /// </param>
+ public void Cancel(string? reasonMessageId = null)
+ {
+ Cancelled = true;
+ CancelReasonMessageId = reasonMessageId;
+ }
+}
+
+/// <summary>
+/// Raised ref directed on an entity when a pacified user is attempting to attack it.
+/// If <see cref="Cancelled"/> is true, don't allow attacking.
+/// <see cref="Reason"/> should be a loc string, if there needs to be special text for why the user isn't able to attack this.
+/// </summary>
+[ByRefEvent]
+public record struct AttemptPacifiedAttackEvent(EntityUid User, bool Cancelled = false, string Reason = "pacified-cannot-harm-directly");
namespace Content.Shared.CombatMode.Pacification;
/// <summary>
-/// Status effect that disables combat mode and restricts aggressive actions.
+/// Status effect that disallows harming living things and restricts aggressive actions.
+///
+/// There is a caveat with pacifism. It's not intended to be wholly encompassing: there are ways of harming people
+/// while pacified--plenty of them, even! The goal is to restrict the obvious ones to make gameplay more interesting
+/// while not overly limiting.
+///
+/// If you want full-pacifism (no combat mode at all), you can simply set <see cref="DisallowAllCombat"/> before adding.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(PacificationSystem))]
public sealed partial class PacifiedComponent : Component
{
+ [DataField]
+ public bool DisallowDisarm = false;
+
+ /// <summary>
+ /// If true, this will disable combat entirely instead of only disallowing attacking living creatures and harmful things.
+ /// </summary>
+ [DataField]
+ public bool DisallowAllCombat = false;
+
+
+ /// <summary>
+ /// When attempting attack against the same entity multiple times,
+ /// don't spam popups every frame and instead have a cooldown.
+ /// </summary>
+ [DataField]
+ public TimeSpan PopupCooldown = TimeSpan.FromSeconds(3.0);
+
+ [DataField]
+ public TimeSpan? NextPopupTime = null;
+
+ /// <summary>
+ /// The last entity attacked, used for popup purposes (avoid spam)
+ /// </summary>
+ [DataField]
+ public EntityUid? LastAttackedEntity = null;
}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.CombatMode.Pacification;
+
+/// <summary>
+/// This is used for marking entities which could cause serious harm if attacked and should not be able to be harmed by
+/// pacifists.
+/// TODO ideally destructible is shared + converted to components so we can just check for a harmful damage trigger instead of this.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PacifismDangerousAttackComponent : Component
+{
+}
+using Content.Shared.Weapons.Melee;
+
namespace Content.Shared.Interaction.Events
{
/// <summary>
public EntityUid Uid { get; }
public EntityUid? Target { get; }
- public AttackAttemptEvent(EntityUid uid, EntityUid? target = null)
+ public Entity<MeleeWeaponComponent>? Weapon { get; }
+
+ /// <summary>
+ /// If this attempt is a disarm as opposed to an actual attack, for things that care about the difference.
+ /// </summary>
+ public bool Disarm { get; }
+
+ public AttackAttemptEvent(EntityUid uid, EntityUid? target = null, Entity<MeleeWeaponComponent>? weapon = null, bool disarm = false)
{
Uid = uid;
Target = target;
+ Weapon = weapon;
+ Disarm = disarm;
}
}
using Content.Shared.Bed.Sleep;
+using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Emoting;
using Content.Shared.Hands;
SubscribeLocalEvent<MobStateComponent, StandAttemptEvent>(CheckAct);
SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
+ SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
}
private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
args.Cancelled = true;
}
+ private void OnAttemptPacifiedAttack(Entity<MobStateComponent> ent, ref AttemptPacifiedAttackEvent args)
+ {
+ args.Cancelled = true;
+ }
+
#endregion
}
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
+using Robust.Shared.Toolshed.Syntax;
namespace Content.Shared.Weapons.Melee;
case LightAttackEvent light:
var lightTarget = GetEntity(light.Target);
- if (!Blocker.CanAttack(user, lightTarget))
+ if (!Blocker.CanAttack(user, lightTarget, (weaponUid, weapon)))
return false;
// Can't self-attack if you're the weapon
case DisarmAttackEvent disarm:
var disarmTarget = GetEntity(disarm.Target);
- if (!Blocker.CanAttack(user, disarmTarget))
+ if (!Blocker.CanAttack(user, disarmTarget, (weaponUid, weapon), true))
return false;
break;
default:
- if (!Blocker.CanAttack(user))
+ if (!Blocker.CanAttack(user, weapon: (weaponUid, weapon)))
return false;
break;
}
foreach (var entity in targets)
{
+ // We raise an attack attempt here as well,
+ // primarily because this was an untargeted wideswing: if a subscriber to that event cared about
+ // the potential target (such as for pacifism), they need to be made aware of the target here.
+ // In that case, just continue.
+ if (!Blocker.CanAttack(user, entity, (weapon, component)))
+ continue;
+
var attackedEvent = new AttackedEvent(meleeUid, user, GetCoordinates(ev.Coordinates));
RaiseLocalEvent(entity, attackedEvent);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin:user);
- if (damageResult != null && damageResult.Total > FixedPoint2.Zero)
+ if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero)
{
appliedDamage += damageResult;
if (meleeUid == user)
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium,
- $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.Total:damage} damage");
+ $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.GetTotal():damage} damage");
}
else
{
if (entities.Count != 0)
{
- if (appliedDamage.Total > FixedPoint2.Zero)
+ if (appliedDamage.GetTotal() > FixedPoint2.Zero)
{
var target = entities.First();
PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component.HitSound);
}
}
- if (appliedDamage.Total > FixedPoint2.Zero)
+ if (appliedDamage.GetTotal() > FixedPoint2.Zero)
{
DoDamageEffect(targets, user, Transform(targets[0]));
}
alerts-bleed-desc = You're [color=red]bleeding[/color].
alerts-pacified-name = [color=green]Pacified[/color]
-alerts-pacified-desc = You're pacified; you won't be able to attack anyone directly.
+alerts-pacified-desc = You're pacified; you won't be able to harm living creatures.
alerts-suit-power-name = Suit Power
alerts-suit-power-desc = How much power your space ninja suit has.
## Messages shown to Pacified players when they try to do violence:
# With projectiles:
-pacified-cannot-throw = You can't bring yourself to throw { THE($projectile) }, that could hurt someone!
+pacified-cannot-throw = I can't bring myself to throw { THE($projectile) }, that could hurt someone!
# With embedding projectiles:
-pacified-cannot-throw-embed = No way you can throw { THE($projectile) }, that could get lodged inside someone!
+pacified-cannot-throw-embed = No way I could throw { THE($projectile) }, that could get lodged inside someone!
# With liquid-spilling projectiles:
-pacified-cannot-throw-spill = You can't possibly throw { THE($projectile) }, that could spill nasty stuff on someone!
+pacified-cannot-throw-spill = I can't possibly throw { THE($projectile) }, that could spill nasty stuff on someone!
# With bolas and snares:
-pacified-cannot-throw-snare = You can't throw { THE($projectile) }, what if someone trips?!
+pacified-cannot-throw-snare = I can't throw { THE($projectile) }, what if someone trips?!
+
+pacified-cannot-harm-directly = I can't bring myself to hurt { THE($entity) }!
+pacified-cannot-harm-indirect = I can't damage { THE($entity) }, it could hurt someone!
+pacified-cannot-fire-gun = I can't fire { THE($entity) }, it could hurt someone!
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: PacifismDangerousAttack
- type: Destructible
thresholds:
- trigger:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: StrongMetallic
+ - type: PacifismDangerousAttack
- type: Destructible
thresholds:
- trigger:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: PacifismDangerousAttack
- type: Destructible
thresholds:
- trigger:
weldingDamage:
types:
Heat: 10
+ - type: PacifismDangerousAttack
- type: Explosive
explosionType: Default
totalIntensity: 120 # ~ 5 tile radius