Glorfcode (force say on damage/stun/crit) (#20562)

This commit is contained in:
Kara
2023-09-28 18:05:36 -07:00
committed by GitHub
parent 6c8e79adfa
commit 80f36ea6d4
10 changed files with 321 additions and 3 deletions

View File

@@ -16,6 +16,7 @@ using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Radio; using Content.Shared.Radio;
@@ -30,6 +31,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Replays; using Robust.Shared.Replays;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -164,6 +166,7 @@ public sealed class ChatUIController : UIController
_player.LocalPlayerChanged += OnLocalPlayerChanged; _player.LocalPlayerChanged += OnLocalPlayerChanged;
_state.OnStateChanged += StateChanged; _state.OnStateChanged += StateChanged;
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage); _net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
_speechBubbleRoot = new LayoutContainer(); _speechBubbleRoot = new LayoutContainer();
@@ -774,6 +777,37 @@ public sealed class ChatUIController : UIController
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel); _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) private void OnChatMessage(MsgChatMessage message)
{ {
var msg = message.Message; var msg = message.Message;

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

View File

@@ -1,5 +1,6 @@
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Eye.Blinding.Systems; using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.Speech; using Content.Shared.Speech;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -54,6 +55,13 @@ namespace Content.Server.Bed.Sleep
private void OnSpeakAttempt(EntityUid uid, SleepingComponent component, SpeakAttemptEvent args) 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(); args.Cancel();
} }

View File

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

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

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

View File

@@ -1,4 +1,5 @@
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Emoting; using Content.Shared.Emoting;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Interaction; using Content.Shared.Interaction;
@@ -27,7 +28,7 @@ public partial class MobStateSystem
SubscribeLocalEvent<MobStateComponent, AttackAttemptEvent>(CheckAct); SubscribeLocalEvent<MobStateComponent, AttackAttemptEvent>(CheckAct);
SubscribeLocalEvent<MobStateComponent, InteractionAttemptEvent>(CheckAct); SubscribeLocalEvent<MobStateComponent, InteractionAttemptEvent>(CheckAct);
SubscribeLocalEvent<MobStateComponent, ThrowAttemptEvent>(CheckAct); SubscribeLocalEvent<MobStateComponent, ThrowAttemptEvent>(CheckAct);
SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(CheckAct); SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(OnSpeakAttempt);
SubscribeLocalEvent<MobStateComponent, IsEquippingAttemptEvent>(OnEquipAttempt); SubscribeLocalEvent<MobStateComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
SubscribeLocalEvent<MobStateComponent, EmoteAttemptEvent>(CheckAct); SubscribeLocalEvent<MobStateComponent, EmoteAttemptEvent>(CheckAct);
SubscribeLocalEvent<MobStateComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt); SubscribeLocalEvent<MobStateComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
@@ -116,6 +117,17 @@ public partial class MobStateSystem
args.Multiplier /= 2; 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) private void CheckAct(EntityUid target, MobStateComponent component, CancellableEntityEventArgs args)
{ {
switch (component.CurrentState) switch (component.CurrentState)

View File

@@ -155,6 +155,10 @@ public abstract class SharedStunSystem : EntitySystem
if (!_statusEffect.TryAddStatusEffect<StunnedComponent>(uid, "Stun", time, refresh)) if (!_statusEffect.TryAddStatusEffect<StunnedComponent>(uid, "Stun", time, refresh))
return false; return false;
var ev = new StunnedEvent();
RaiseLocalEvent(uid, ref ev);
_adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} stunned for {time.Seconds} seconds"); _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} stunned for {time.Seconds} seconds");
return true; return true;
} }
@@ -171,7 +175,13 @@ public abstract class SharedStunSystem : EntitySystem
if (!Resolve(uid, ref status, false)) if (!Resolve(uid, ref status, false))
return 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> /// <summary>
@@ -271,5 +281,16 @@ public abstract class SharedStunSystem : EntitySystem
} }
#endregion #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;

View 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...

View File

@@ -200,6 +200,7 @@
- type: Puller - type: Puller
- type: Speech - type: Speech
speechSounds: Alto speechSounds: Alto
- type: DamageForceSay
- type: Vocal - type: Vocal
sounds: sounds:
Male: MaleHuman Male: MaleHuman