using Content.Server.Chat.Managers;
using Content.Shared.Bed.Sleep;
using Content.Shared.Chat;
using Content.Shared.Popups;
using Content.Shared.StatusEffect;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Traits.Assorted;
///
/// This handles narcolepsy, causing the affected to fall asleep uncontrollably at a random interval.
///
public sealed class NarcolepsySystem : EntitySystem
{
[ValidatePrototypeId]
private const string StatusEffectKey = "ForcedSleep"; // Same one used by N2O and other sleep chems.
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly SharedPopupSystem _popups = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly IRobustRandom _random = default!;
///
public override void Initialize()
{
SubscribeLocalEvent(SetupNarcolepsy);
SubscribeLocalEvent(OnSleepChanged);
}
private void SetupNarcolepsy(EntityUid uid, NarcolepsyComponent component, ComponentStartup args)
{
PrepareNextIncident((uid, component));
}
private void OnSleepChanged(Entity ent, ref SleepStateChangedEvent args)
{
// When falling asleep while an incident is nigh, force it to happen immediately.
if (args.FellAsleep)
{
if (ent.Comp.NextIncidentTime < ent.Comp.TimeBeforeWarning)
StartIncident(ent);
return;
}
// When waking up after sleeping for at least the minimum time of an incident, reset the incident timer and show a popup.
if (args.TimeSlept is null || args.TimeSlept.Value.TotalSeconds < ent.Comp.DurationOfIncident.X)
return;
ShowRandomPopup(ent, ent.Comp.WakeupLocaleBase, ent.Comp.WakeupLocaleCount);
PrepareNextIncident(ent);
}
public void AdjustNarcolepsyTimer(EntityUid uid, float setTime, NarcolepsyComponent? narcolepsy = null)
{
if (!Resolve(uid, ref narcolepsy, false) || narcolepsy.NextIncidentTime > setTime)
return;
narcolepsy.NextIncidentTime = setTime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var narcolepsy))
{
if (HasComp(uid))
continue;
narcolepsy.NextIncidentTime -= frameTime;
if (narcolepsy.NextIncidentTime <= narcolepsy.TimeBeforeWarning && narcolepsy.NextIncidentTime < narcolepsy.LastWarningRollTime - 1f)
{
// Roll for showing a popup. There should really be a class for doing this.
narcolepsy.LastWarningRollTime = narcolepsy.NextIncidentTime;
if (_random.Prob(narcolepsy.WarningChancePerSecond))
{
ShowRandomPopup(uid, narcolepsy.WarningLocaleBase, narcolepsy.WakeupLocaleCount);
narcolepsy.LastWarningRollTime = 0f; // Do not show any more popups for the upcoming incident
}
}
if (narcolepsy.NextIncidentTime >= 0)
continue;
StartIncident((uid, narcolepsy));
}
}
public void StartIncident(Entity ent)
{
var duration = _random.NextFloat(ent.Comp.DurationOfIncident.X, ent.Comp.DurationOfIncident.Y);
PrepareNextIncident(ent, duration);
_statusEffects.TryAddStatusEffect(ent, StatusEffectKey, TimeSpan.FromSeconds(duration), false);
}
private void PrepareNextIncident(Entity ent, float startingFrom = 0f)
{
var time = _random.NextFloat(ent.Comp.TimeBetweenIncidents.X, ent.Comp.TimeBetweenIncidents.Y);
ent.Comp.NextIncidentTime = startingFrom + time;
ent.Comp.LastWarningRollTime = float.MaxValue;
}
private void ShowRandomPopup(EntityUid uid, string prefix, int count)
{
if (count <= 0 || !TryComp(uid, out var actor))
return;
var popup = Loc.GetString($"{prefix}-{_random.Next(1, count + 1)}");
_popups.PopupEntity(popup, uid, uid, PopupType.MediumCaution);
// This should use ChatChannel.Visual, but it's not displayed on the client.
_chatMan.ChatMessageToOne(ChatChannel.Notifications, popup, popup, uid, false,
actor.PlayerSession.Channel, Color.IndianRed);
}
}