Glorfcode (force say on damage/stun/crit) (#20562)
This commit is contained in:
@@ -16,6 +16,7 @@ using Content.Client.UserInterface.Systems.Gameplay;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Radio;
|
||||
@@ -30,6 +31,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -164,6 +166,7 @@ public sealed class ChatUIController : UIController
|
||||
_player.LocalPlayerChanged += OnLocalPlayerChanged;
|
||||
_state.OnStateChanged += StateChanged;
|
||||
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
||||
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
|
||||
|
||||
_speechBubbleRoot = new LayoutContainer();
|
||||
|
||||
@@ -774,6 +777,37 @@ public sealed class ChatUIController : UIController
|
||||
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
|
||||
}
|
||||
|
||||
private void OnDamageForceSay(DamageForceSayEvent ev, EntitySessionEventArgs _)
|
||||
{
|
||||
if (UIManager.ActiveScreen?.GetWidget<ChatBox>() is not { } chatBox)
|
||||
return;
|
||||
|
||||
// Don't send on OOC/LOOC obviously!
|
||||
if (chatBox.SelectedChannel is not
|
||||
(ChatSelectChannel.Local or
|
||||
ChatSelectChannel.Radio or
|
||||
ChatSelectChannel.Whisper))
|
||||
return;
|
||||
|
||||
if (_player.LocalPlayer?.ControlledEntity is not { } ent
|
||||
|| !EntityManager.TryGetComponent<DamageForceSayComponent>(ent, out var forceSay))
|
||||
return;
|
||||
|
||||
var msg = chatBox.ChatInput.Input.Text.TrimEnd();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(msg))
|
||||
return;
|
||||
|
||||
var modifiedText = ev.Suffix != null
|
||||
? Loc.GetString(forceSay.ForceSayMessageWrap,
|
||||
("message", msg), ("suffix", ev.Suffix))
|
||||
: Loc.GetString(forceSay.ForceSayMessageWrapNoSuffix,
|
||||
("message", msg));
|
||||
|
||||
chatBox.ChatInput.Input.SetText(modifiedText);
|
||||
chatBox.ChatInput.Input.ForceSubmitText();
|
||||
}
|
||||
|
||||
private void OnChatMessage(MsgChatMessage message)
|
||||
{
|
||||
var msg = message.Message;
|
||||
|
||||
126
Content.Server/Damage/ForceSay/DamageForceSaySystem.cs
Normal file
126
Content.Server/Damage/ForceSay/DamageForceSaySystem.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Damage.ForceSay;
|
||||
|
||||
/// <inheritdoc cref="DamageForceSayComponent"/>
|
||||
public sealed class DamageForceSaySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DamageForceSayComponent, StunnedEvent>(OnStunned);
|
||||
SubscribeLocalEvent<DamageForceSayComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
|
||||
// need to raise after mobthreshold
|
||||
// so that we don't accidentally raise one for damage before one for mobstate
|
||||
// (this won't double raise, because of the cooldown)
|
||||
SubscribeLocalEvent<DamageForceSayComponent, DamageChangedEvent>(OnDamageChanged, after: new []{ typeof(MobThresholdSystem)} );
|
||||
SubscribeLocalEvent<DamageForceSayComponent, SleepStateChangedEvent>(OnSleep);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = AllEntityQuery<AllowNextCritSpeechComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (_timing.CurTime < comp.Timeout)
|
||||
continue;
|
||||
|
||||
RemCompDeferred<AllowNextCritSpeechComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true, string? suffixOverride = null)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(uid, out var actor))
|
||||
return;
|
||||
|
||||
// disallow if cooldown hasn't ended
|
||||
if (component.NextAllowedTime != null &&
|
||||
_timing.CurTime < component.NextAllowedTime)
|
||||
return;
|
||||
|
||||
var suffix = Loc.GetString(suffixOverride ?? component.ForceSayStringPrefix + _random.Next(1, component.ForceSayStringCount));
|
||||
|
||||
// set cooldown & raise event
|
||||
component.NextAllowedTime = _timing.CurTime + component.Cooldown;
|
||||
RaiseNetworkEvent(new DamageForceSayEvent { Suffix = useSuffix ? suffix : null }, actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void AllowNextSpeech(EntityUid uid)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(uid, out var actor))
|
||||
return;
|
||||
|
||||
var nextCrit = EnsureComp<AllowNextCritSpeechComponent>(uid);
|
||||
|
||||
// timeout is *3 ping to compensate for roundtrip + leeway
|
||||
nextCrit.Timeout = _timing.CurTime + TimeSpan.FromMilliseconds(actor.PlayerSession.Ping * 3);
|
||||
}
|
||||
|
||||
private void OnSleep(EntityUid uid, DamageForceSayComponent component, SleepStateChangedEvent args)
|
||||
{
|
||||
if (!args.FellAsleep)
|
||||
return;
|
||||
|
||||
TryForceSay(uid, component, true, "damage-force-say-sleep");
|
||||
AllowNextSpeech(uid);
|
||||
}
|
||||
|
||||
private void OnStunned(EntityUid uid, DamageForceSayComponent component, ref StunnedEvent args)
|
||||
{
|
||||
TryForceSay(uid, component);
|
||||
}
|
||||
|
||||
private void OnDamageChanged(EntityUid uid, DamageForceSayComponent component, DamageChangedEvent args)
|
||||
{
|
||||
if (args.DamageDelta == null || !args.DamageIncreased || args.DamageDelta.Total < component.DamageThreshold)
|
||||
return;
|
||||
|
||||
if (component.ValidDamageGroups != null)
|
||||
{
|
||||
var totalApplicableDamage = FixedPoint2.Zero;
|
||||
foreach (var (group, value) in args.DamageDelta.GetDamagePerGroup(_prototype))
|
||||
{
|
||||
if (!component.ValidDamageGroups.Contains(group))
|
||||
continue;
|
||||
|
||||
totalApplicableDamage += value;
|
||||
}
|
||||
|
||||
if (totalApplicableDamage < component.DamageThreshold)
|
||||
return;
|
||||
}
|
||||
|
||||
TryForceSay(uid, component);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(EntityUid uid, DamageForceSayComponent component, MobStateChangedEvent args)
|
||||
{
|
||||
if (args is not { OldMobState: MobState.Alive, NewMobState: MobState.Critical or MobState.Dead })
|
||||
return;
|
||||
|
||||
// no suffix for the drama
|
||||
// LING IN MAI-
|
||||
TryForceSay(uid, component, false);
|
||||
AllowNextSpeech(uid);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Eye.Blinding.Systems;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Network;
|
||||
@@ -54,6 +55,13 @@ namespace Content.Server.Bed.Sleep
|
||||
|
||||
private void OnSpeakAttempt(EntityUid uid, SleepingComponent component, SpeakAttemptEvent args)
|
||||
{
|
||||
// TODO reduce duplication of this behavior with MobStateSystem somehow
|
||||
if (HasComp<AllowNextCritSpeechComponent>(uid))
|
||||
{
|
||||
RemCompDeferred<AllowNextCritSpeechComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Damage.ForceSay;
|
||||
|
||||
/// <summary>
|
||||
/// The reason for this component's existence is slightly unintuitive, so for context: this is put on an entity
|
||||
/// to allow its next speech attempt to bypass <see cref="MobStateComponent"/> checks. The reason for this is to allow
|
||||
/// 'force saying'--for instance, with deathgasping or with <see cref="DamageForceSayComponent"/>.
|
||||
///
|
||||
/// This component is either removed in the <see cref="MobStateSystem"/> speech attempt check, or after <see cref="Timeout"/>
|
||||
/// has passed. This is to allow a player-submitted forced message in the case of <see cref="DamageForceSayComponent"/>,
|
||||
/// while also ensuring that it isn't valid forever. It has to work this way, because the server is not a keylogger and doesn't
|
||||
/// have any knowledge of what the client might actually have typed, so it gives them some leeway for ping.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class AllowNextCritSpeechComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Should be set when adding the component to specify the time that this should be valid for,
|
||||
/// if it should stay valid for some amount of time.
|
||||
/// </summary>
|
||||
public TimeSpan? Timeout = null;
|
||||
}
|
||||
66
Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs
Normal file
66
Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Shared.Damage.ForceSay;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for forcing clients to send messages with a suffix attached (like -GLORF) when taking large amounts
|
||||
/// of damage, or things like entering crit or being stunned.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class DamageForceSayComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The localization string that the message & suffix will be passed into
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ForceSayMessageWrap = "damage-force-say-message-wrap";
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="ForceSayMessageWrap"/> but for cases where no suffix is used,
|
||||
/// such as when going into crit.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ForceSayMessageWrapNoSuffix = "damage-force-say-message-wrap-no-suffix";
|
||||
|
||||
/// <summary>
|
||||
/// The fluent string prefix to use when picking a random suffix
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string ForceSayStringPrefix = "damage-force-say-";
|
||||
|
||||
/// <summary>
|
||||
/// The number of suffixes that exist for use with <see cref="ForceSayStringPrefix"/>.
|
||||
/// i.e. (prefix)-1 through (prefix)-(count)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int ForceSayStringCount = 7;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of total damage between <see cref="ValidDamageGroups"/> that needs to be taken before
|
||||
/// a force say occurs.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2 DamageThreshold = FixedPoint2.New(10);
|
||||
|
||||
/// <summary>
|
||||
/// A list of damage group types that are considered when checking <see cref="DamageThreshold"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<DamageGroupPrototype>>? ValidDamageGroups = new()
|
||||
{
|
||||
"Brute",
|
||||
"Burn",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The time enforced between force says to avoid spam.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Cooldown = TimeSpan.FromSeconds(5.0);
|
||||
|
||||
public TimeSpan? NextAllowedTime = null;
|
||||
}
|
||||
13
Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs
Normal file
13
Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Damage.ForceSay;
|
||||
|
||||
/// <summary>
|
||||
/// Sent to clients as a network event when their entity contains <see cref="DamageForceSayComponent"/>
|
||||
/// that COMMANDS them to speak the current message in their chatbox
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class DamageForceSayEvent : EntityEventArgs
|
||||
{
|
||||
public string? Suffix;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
@@ -27,7 +28,7 @@ public partial class MobStateSystem
|
||||
SubscribeLocalEvent<MobStateComponent, AttackAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, InteractionAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, ThrowAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(OnSpeakAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, EmoteAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
|
||||
@@ -116,6 +117,17 @@ public partial class MobStateSystem
|
||||
args.Multiplier /= 2;
|
||||
}
|
||||
|
||||
private void OnSpeakAttempt(EntityUid uid, MobStateComponent component, SpeakAttemptEvent args)
|
||||
{
|
||||
if (HasComp<AllowNextCritSpeechComponent>(uid))
|
||||
{
|
||||
RemCompDeferred<AllowNextCritSpeechComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
CheckAct(uid, component, args);
|
||||
}
|
||||
|
||||
private void CheckAct(EntityUid target, MobStateComponent component, CancellableEntityEventArgs args)
|
||||
{
|
||||
switch (component.CurrentState)
|
||||
|
||||
@@ -155,6 +155,10 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
|
||||
if (!_statusEffect.TryAddStatusEffect<StunnedComponent>(uid, "Stun", time, refresh))
|
||||
return false;
|
||||
|
||||
var ev = new StunnedEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
_adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} stunned for {time.Seconds} seconds");
|
||||
return true;
|
||||
}
|
||||
@@ -171,7 +175,13 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
if (!Resolve(uid, ref status, false))
|
||||
return false;
|
||||
|
||||
return _statusEffect.TryAddStatusEffect<KnockedDownComponent>(uid, "KnockedDown", time, refresh);
|
||||
if (!_statusEffect.TryAddStatusEffect<KnockedDownComponent>(uid, "KnockedDown", time, refresh))
|
||||
return false;
|
||||
|
||||
var ev = new KnockedDownEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -271,5 +281,16 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on an entity when it is stunned.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct StunnedEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on an entity when it is knocked down.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct KnockedDownEvent;
|
||||
|
||||
12
Resources/Locale/en-US/damage/damage-force-say.ftl
Normal file
12
Resources/Locale/en-US/damage/damage-force-say.ftl
Normal file
@@ -0,0 +1,12 @@
|
||||
damage-force-say-message-wrap = {$message}-{$suffix}
|
||||
damage-force-say-message-wrap-no-suffix = {$message}-
|
||||
|
||||
damage-force-say-1 = GACK!
|
||||
damage-force-say-2 = GLORF!
|
||||
damage-force-say-3 = OOF!
|
||||
damage-force-say-4 = AUGH!
|
||||
damage-force-say-5 = OW!
|
||||
damage-force-say-6 = URGH!
|
||||
damage-force-say-7 = HRNK!
|
||||
|
||||
damage-force-say-sleep = zzz...
|
||||
@@ -200,6 +200,7 @@
|
||||
- type: Puller
|
||||
- type: Speech
|
||||
speechSounds: Alto
|
||||
- type: DamageForceSay
|
||||
- type: Vocal
|
||||
sounds:
|
||||
Male: MaleHuman
|
||||
|
||||
Reference in New Issue
Block a user