--- /dev/null
+namespace Content.Server.Chat;
+
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+
+/// <summary>
+/// Causes an entity to automatically emote when taking damage.
+/// </summary>
+[RegisterComponent, Access(typeof(EmoteOnDamageSystem))]
+public sealed class EmoteOnDamageComponent : Component
+{
+ /// <summary>
+ /// Chance of preforming an emote when taking damage and not on cooldown.
+ /// </summary>
+ [DataField("emoteChance"), ViewVariables(VVAccess.ReadWrite)]
+ public float EmoteChance = 0.5f;
+
+ /// <summary>
+ /// A set of emotes that will be randomly picked from.
+ /// <see cref="EmotePrototype"/>
+ /// </summary>
+ [DataField("emotes", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<EmotePrototype>)), ViewVariables(VVAccess.ReadWrite)]
+ public HashSet<string> Emotes = new();
+
+ /// <summary>
+ /// Also send the emote in chat.
+ /// <summary>
+ [DataField("withChat"), ViewVariables(VVAccess.ReadWrite)]
+ public bool WithChat = false;
+
+ /// <summary>
+ /// Hide the chat message from the chat window, only showing the popup.
+ /// This does nothing if WithChat is false.
+ /// <summary>
+ [DataField("hiddenFromChatWindow")]
+ public bool HiddenFromChatWindow = false;
+
+ /// <summary>
+ /// The simulation time of the last emote preformed due to taking damage.
+ /// </summary>
+ [DataField("lastEmoteTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan LastEmoteTime = TimeSpan.Zero;
+
+ /// <summary>
+ /// The cooldown between emotes.
+ /// </summary>
+ [DataField("emoteCooldown"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan EmoteCooldown = TimeSpan.FromSeconds(2);
+}
--- /dev/null
+namespace Content.Server.Chat.Systems;
+
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Damage;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+public sealed class EmoteOnDamageSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<EmoteOnDamageComponent, EntityUnpausedEvent>(OnUnpaused);
+ SubscribeLocalEvent<EmoteOnDamageComponent, DamageChangedEvent>(OnDamage);
+ }
+
+ private void OnUnpaused(EntityUid uid, EmoteOnDamageComponent emoteOnDamage, ref EntityUnpausedEvent args)
+ {
+ emoteOnDamage.LastEmoteTime += args.PausedTime;
+ }
+
+ private void OnDamage(EntityUid uid, EmoteOnDamageComponent emoteOnDamage, DamageChangedEvent args)
+ {
+ if (!args.DamageIncreased)
+ return;
+
+ if (emoteOnDamage.LastEmoteTime + emoteOnDamage.EmoteCooldown > _gameTiming.CurTime)
+ return;
+
+ if (emoteOnDamage.Emotes.Count == 0)
+ return;
+
+ if (!_random.Prob(emoteOnDamage.EmoteChance))
+ return;
+
+ var emote = _random.Pick(emoteOnDamage.Emotes);
+ if (emoteOnDamage.WithChat)
+ {
+ _chatSystem.TryEmoteWithChat(uid, emote, emoteOnDamage.HiddenFromChatWindow);
+ }
+ else
+ {
+ _chatSystem.TryEmoteWithoutChat(uid,emote);
+ }
+
+ emoteOnDamage.LastEmoteTime = _gameTiming.CurTime;
+ }
+
+ /// <summary>
+ /// Try to add an emote to the entity, which will be performed at an interval.
+ /// </summary>
+ public bool AddEmote(EntityUid uid, string emotePrototypeId, EmoteOnDamageComponent? emoteOnDamage = null)
+ {
+ if (!Resolve(uid, ref emoteOnDamage, logMissing: false))
+ return false;
+
+ DebugTools.Assert(_prototypeManager.HasIndex<EmotePrototype>(emotePrototypeId), "Prototype not found. Did you make a typo?");
+
+ return emoteOnDamage.Emotes.Add(emotePrototypeId);
+ }
+
+ /// <summary>
+ /// Stop preforming an emote.
+ /// </summary>
+ public bool RemoveEmote(EntityUid uid, string emotePrototypeId, EmoteOnDamageComponent? emoteOnDamage = null)
+ {
+ if (!Resolve(uid, ref emoteOnDamage, logMissing: false))
+ return false;
+
+ DebugTools.Assert(_prototypeManager.HasIndex<EmotePrototype>(emotePrototypeId), "Prototype not found. Did you make a typo?");
+
+ return emoteOnDamage.Emotes.Remove(emotePrototypeId);
+ }
+}
+++ /dev/null
-namespace Content.Server.Zombies;
-
-/// <summary>
-/// Indicates a zombie that is "alive", i.e not crit/dead.
-/// Causes it to emote when damaged.
-/// TODO: move this to generic EmoteWhenDamaged comp/system.
-/// </summary>
-[RegisterComponent]
-public sealed class ActiveZombieComponent : Component
-{
- /// <summary>
- /// What emote to preform.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- public string GroanEmoteId = "Scream";
-
- /// <summary>
- /// Minimum time between groans.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan DamageGroanCooldown = TimeSpan.FromSeconds(2);
-
- /// <summary>
- /// Chance to groan.
- /// </summary>
- public float DamageGroanChance = 0.5f;
-
- /// <summary>
- /// The last time the zombie groaned from taking damage.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan LastDamageGroan = TimeSpan.Zero;
-}
using Content.Shared.Chemistry.Components;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
-using Content.Shared.Damage;
using Content.Shared.Disease.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
[Dependency] private readonly ServerInventorySystem _inv = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
+ [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
SubscribeLocalEvent<ZombieComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<ZombieComponent, MobStateChangedEvent>(OnMobState);
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
- SubscribeLocalEvent<ActiveZombieComponent, DamageChangedEvent>(OnDamage);
- SubscribeLocalEvent<ActiveZombieComponent, AttemptSneezeCoughEvent>(OnSneeze);
- SubscribeLocalEvent<ActiveZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
+ SubscribeLocalEvent<ZombieComponent, AttemptSneezeCoughEvent>(OnSneeze);
+ SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
}
- private void OnSleepAttempt(EntityUid uid, ActiveZombieComponent component, ref TryingToSleepEvent args)
+ private void OnSleepAttempt(EntityUid uid, ZombieComponent component, ref TryingToSleepEvent args)
{
args.Cancelled = true;
}
private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
{
- //BUG: this won't work when an entity becomes a zombie some other way, such as admin smite
if (args.NewMobState == MobState.Alive)
{
// Groaning when damaged
- EnsureComp<ActiveZombieComponent>(uid);
+ EnsureComp<EmoteOnDamageComponent>(uid);
+ _emoteOnDamage.AddEmote(uid, "Scream");
// Random groaning
EnsureComp<AutoEmoteComponent>(uid);
else
{
// Stop groaning when damaged
- RemComp<ActiveZombieComponent>(uid);
+ _emoteOnDamage.RemoveEmote(uid, "Scream");
// Stop random groaning
_autoEmote.RemoveEmote(uid, "ZombieGroan");
}
}
- private void OnDamage(EntityUid uid, ActiveZombieComponent component, DamageChangedEvent args)
- {
- if (args.DamageIncreased)
- AttemptDamageGroan(uid, component);
- }
-
- private void OnSneeze(EntityUid uid, ActiveZombieComponent component, ref AttemptSneezeCoughEvent args)
+ private void OnSneeze(EntityUid uid, ZombieComponent component, ref AttemptSneezeCoughEvent args)
{
args.Cancelled = true;
}
}
}
- private void AttemptDamageGroan(EntityUid uid, ActiveZombieComponent component)
- {
- if (component.LastDamageGroan + component.DamageGroanCooldown > _gameTiming.CurTime)
- return;
-
- if (_robustRandom.Prob(component.DamageGroanChance))
- return;
-
- _chat.TryEmoteWithoutChat(uid, component.GroanEmoteId);
- component.LastDamageGroan = _gameTiming.CurTime;
- }
-
/// <summary>
/// This is the function to call if you want to unzombify an entity.
/// </summary>
using Content.Server.IdentityManagement;
using Content.Shared.Humanoid;
using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Weapons.Melee;
+using Content.Server.Chat;
+using Content.Server.Chat.Systems;
namespace Content.Server.Zombies
{
[Dependency] private readonly HumanoidAppearanceSystem _sharedHuApp = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+ [Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
+ [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
if (args.NewMobState == MobState.Dead ||
args.NewMobState == MobState.Critical)
{
- ZombifyEntity(uid);
+ ZombifyEntity(uid, args.Component);
}
}
/// rewrite this, but this is how it shall lie eternal. Turn back now.
/// -emo
/// </remarks>
- public void ZombifyEntity(EntityUid target)
+ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
{
//Don't zombfiy zombies
if (HasComp<ZombieComponent>(target))
return;
+ if (!Resolve(target, ref mobState, logMissing: false))
+ return;
+
//you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target);
melee.Range = 1.5f;
Dirty(melee);
+ if (mobState.CurrentState == MobState.Alive)
+ {
+ // Groaning when damaged
+ EnsureComp<EmoteOnDamageComponent>(target);
+ _emoteOnDamage.AddEmote(target, "Scream");
+
+ // Random groaning
+ EnsureComp<AutoEmoteComponent>(target);
+ _autoEmote.AddEmote(target, "ZombieGroan");
+ }
+
//We have specific stuff for humanoid zombies because they matter more
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
{