using Content.Server.Popups; using Content.Server.Sound.Components; using Content.Shared.Actions; using Content.Shared.Audio; using Content.Shared.Bed.Sleep; using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Slippery; using Content.Shared.StatusEffect; using Content.Shared.Stunnable; using Content.Shared.Verbs; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Bed.Sleep { public sealed class SleepingSystem : SharedSleepingSystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; [ValidatePrototypeId] public const string SleepActionId = "ActionSleep"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnSleepStateChanged); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnSleepAction); SubscribeLocalEvent(OnBedSleepAction); SubscribeLocalEvent(OnWakeAction); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent>(AddWakeVerb); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnSlip); SubscribeLocalEvent(OnInit); } /// /// when sleeping component is added or removed, we do some stuff with other components. /// private void OnSleepStateChanged(EntityUid uid, MobStateComponent component, SleepStateChangedEvent args) { if (args.FellAsleep) { // Expiring status effects would remove the components needed for sleeping _statusEffectsSystem.TryRemoveStatusEffect(uid, "Stun"); _statusEffectsSystem.TryRemoveStatusEffect(uid, "KnockedDown"); EnsureComp(uid); EnsureComp(uid); if (TryComp(uid, out var sleepSound)) { var emitSound = EnsureComp(uid); emitSound.Sound = sleepSound.Snore; emitSound.PlayChance = sleepSound.Chance; emitSound.RollInterval = sleepSound.Interval; emitSound.PopUp = sleepSound.PopUp; } return; } RemComp(uid); RemComp(uid); RemComp(uid); } /// /// Wake up on taking an instance of damage at least the value of WakeThreshold. /// private void OnDamageChanged(EntityUid uid, SleepingComponent component, DamageChangedEvent args) { if (!args.DamageIncreased || args.DamageDelta == null) return; if (args.DamageDelta.GetTotal() >= component.WakeThreshold) TryWaking(uid, component); } private void OnSleepAction(EntityUid uid, MobStateComponent component, SleepActionEvent args) { TrySleeping(uid); } private void OnBedSleepAction(EntityUid uid, ActionsContainerComponent component, SleepActionEvent args) { TrySleeping(args.Performer); } private void OnWakeAction(EntityUid uid, MobStateComponent component, WakeActionEvent args) { if (!TryWakeCooldown(uid)) return; if (TryWaking(uid)) args.Handled = true; } /// /// In crit, we wake up if we are not being forced to sleep. /// And, you can't sleep when dead... /// private void OnMobStateChanged(EntityUid uid, SleepingComponent component, MobStateChangedEvent args) { if (args.NewMobState == MobState.Dead) { RemComp(uid); RemComp(uid); return; } if (TryComp(uid, out var spam)) spam.Enabled = args.NewMobState == MobState.Alive; } private void AddWakeVerb(EntityUid uid, SleepingComponent component, GetVerbsEvent args) { if (!args.CanInteract || !args.CanAccess) return; AlternativeVerb verb = new() { Act = () => { if (!TryWakeCooldown(uid)) return; TryWaking(args.Target, user: args.User); }, Text = Loc.GetString("action-name-wake"), Priority = 2 }; args.Verbs.Add(verb); } /// /// When you click on a sleeping person with an empty hand, try to wake them. /// private void OnInteractHand(EntityUid uid, SleepingComponent component, InteractHandEvent args) { args.Handled = true; if (!TryWakeCooldown(uid)) return; TryWaking(args.Target, user: args.User); } private void OnExamined(EntityUid uid, SleepingComponent component, ExaminedEvent args) { if (args.IsInDetailsRange) { args.PushMarkup(Loc.GetString("sleep-examined", ("target", Identity.Entity(uid, EntityManager)))); } } private void OnSlip(EntityUid uid, SleepingComponent component, SlipAttemptEvent args) { args.Cancel(); } private void OnInit(EntityUid uid, ForcedSleepingComponent component, ComponentInit args) { TrySleeping(uid); } /// /// Try sleeping. Only mobs can sleep. /// public bool TrySleeping(EntityUid uid) { if (!HasComp(uid)) return false; var tryingToSleepEvent = new TryingToSleepEvent(uid); RaiseLocalEvent(uid, ref tryingToSleepEvent); if (tryingToSleepEvent.Cancelled) return false; EnsureComp(uid); return true; } private bool TryWakeCooldown(EntityUid uid, SleepingComponent? component = null) { if (!Resolve(uid, ref component, false)) return false; var curTime = _gameTiming.CurTime; if (curTime < component.CoolDownEnd) { return false; } component.CoolDownEnd = curTime + component.Cooldown; return true; } /// /// Try to wake up. /// public bool TryWaking(EntityUid uid, SleepingComponent? component = null, bool force = false, EntityUid? user = null) { if (!Resolve(uid, ref component, false)) return false; if (!force && HasComp(uid)) { if (user != null) { _audio.PlayPvs("/Audio/Effects/thudswoosh.ogg", uid, AudioHelpers.WithVariation(0.05f, _robustRandom)); _popupSystem.PopupEntity(Loc.GetString("wake-other-failure", ("target", Identity.Entity(uid, EntityManager))), uid, Filter.Entities(user.Value), true, Shared.Popups.PopupType.SmallCaution); } return false; } if (user != null) { _audio.PlayPvs("/Audio/Effects/thudswoosh.ogg", uid, AudioHelpers.WithVariation(0.05f, _robustRandom)); _popupSystem.PopupEntity(Loc.GetString("wake-other-success", ("target", Identity.Entity(uid, EntityManager))), uid, Filter.Entities(user.Value), true); } RemComp(uid); return true; } } }