using Content.Server.Chat.Managers; using Content.Server.Popups; using Content.Shared._Shitmed.Medical.Surgery; using Content.Shared.Alert; using Content.Shared.Chat; using Content.Shared.Damage; using Content.Shared.FixedPoint; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Systems; using Content.Shared.Mood; using Content.Shared.Overlays; using Content.Shared.Popups; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Timer = Robust.Shared.Timing.Timer; using Robust.Server.Player; using Robust.Shared.Player; using Robust.Shared.Configuration; using Content.Shared.CCVar; namespace Content.Server.Mood; public sealed class MoodSystem : EntitySystem { [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly SharedJetpackSystem _jetpack = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly IConfigurationManager _config = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnMoodEffect); SubscribeLocalEvent(OnDamageChange); SubscribeLocalEvent(OnRefreshMoveSpeed); SubscribeLocalEvent(OnRemoveEffect); } private void OnShutdown(EntityUid uid, MoodComponent component, ComponentShutdown args) { _alerts.ClearAlertCategory(uid, component.MoodCategory); RemComp(uid); } private void OnRemoveEffect(EntityUid uid, MoodComponent component, MoodRemoveEffectEvent args) { if (!_config.GetCVar(CCVars.MoodEnabled)) return; if (component.UncategorisedEffects.TryGetValue(args.EffectId, out _)) RemoveTimedOutEffect(uid, args.EffectId); else foreach (var (category, id) in component.CategorisedEffects) if (id == args.EffectId) { RemoveTimedOutEffect(uid, args.EffectId, category); return; } } private void OnRefreshMoveSpeed(EntityUid uid, MoodComponent component, RefreshMovementSpeedModifiersEvent args) { if (!_config.GetCVar(CCVars.MoodEnabled) || component.CurrentMoodThreshold is > MoodThreshold.Meh and < MoodThreshold.Good or MoodThreshold.Dead || _jetpack.IsUserFlying(uid)) return; // This ridiculous math serves a purpose making high mood less impactful on movement speed than low mood var modifier = Math.Clamp( (component.CurrentMoodLevel >= component.MoodThresholds[MoodThreshold.Neutral]) ? _config.GetCVar(CCVars.MoodIncreasesSpeed) ? MathF.Pow(1.003f, component.CurrentMoodLevel - component.MoodThresholds[MoodThreshold.Neutral]) : 1 : _config.GetCVar(CCVars.MoodDecreasesSpeed) ? 2 - component.MoodThresholds[MoodThreshold.Neutral] / component.CurrentMoodLevel : 1, component.MinimumSpeedModifier, component.MaximumSpeedModifier); args.ModifySpeed(1, modifier); } private void OnMoodEffect(EntityUid uid, MoodComponent component, MoodEffectEvent args) { if (!_config.GetCVar(CCVars.MoodEnabled) || !_prototypeManager.TryIndex(args.EffectId, out var prototype) ) return; var ev = new OnMoodEffect(uid, args.EffectId, args.EffectModifier, args.EffectOffset); RaiseLocalEvent(uid, ref ev); ApplyEffect(uid, component, prototype, ev.EffectModifier, ev.EffectOffset); } private void ApplyEffect(EntityUid uid, MoodComponent component, MoodEffectPrototype prototype, float eventModifier = 1, float eventOffset = 0) { // Apply categorised effect if (prototype.Category != null) { if (component.CategorisedEffects.TryGetValue(prototype.Category, out var oldPrototypeId)) { if (!_prototypeManager.TryIndex(oldPrototypeId, out var oldPrototype)) return; // Don't send the moodlet popup if we already have the moodlet. if (!component.CategorisedEffects.ContainsValue(prototype.ID)) SendEffectText(uid, prototype); if (prototype.ID != oldPrototype.ID) component.CategorisedEffects[prototype.Category] = prototype.ID; } else component.CategorisedEffects.Add(prototype.Category, prototype.ID); if (prototype.Timeout != 0) Timer.Spawn(TimeSpan.FromSeconds(prototype.Timeout), () => RemoveTimedOutEffect(uid, prototype.ID, prototype.Category)); } // Apply uncategorised effect else { if (component.UncategorisedEffects.TryGetValue(prototype.ID, out _)) return; var moodChange = prototype.MoodChange * eventModifier + eventOffset; if (moodChange == 0) return; // Don't send the moodlet popup if we already have the moodlet. if (!component.UncategorisedEffects.ContainsKey(prototype.ID)) SendEffectText(uid, prototype); component.UncategorisedEffects.Add(prototype.ID, moodChange); if (prototype.Timeout != 0) Timer.Spawn(TimeSpan.FromSeconds(prototype.Timeout), () => RemoveTimedOutEffect(uid, prototype.ID)); } RefreshMood(uid, component); } private void SendEffectText(EntityUid uid, MoodEffectPrototype prototype) { if (prototype.Hidden) return; _popup.PopupEntity(prototype.Description, uid, uid, (prototype.MoodChange > 0) ? PopupType.Medium : PopupType.MediumCaution); } private void RemoveTimedOutEffect(EntityUid uid, string prototypeId, string? category = null) { if (!TryComp(uid, out var comp)) return; if (category == null) { if (!comp.UncategorisedEffects.ContainsKey(prototypeId)) return; comp.UncategorisedEffects.Remove(prototypeId); } else { if (!comp.CategorisedEffects.TryGetValue(category, out var currentProtoId) || currentProtoId != prototypeId || !_prototypeManager.HasIndex(currentProtoId)) return; comp.CategorisedEffects.Remove(category); } ReplaceMood(uid, prototypeId); RefreshMood(uid, comp); } /// /// Some moods specifically create a moodlet upon expiration. This is normally used for "Addiction" type moodlets, /// such as a positive moodlet from an addictive substance that becomes a negative moodlet when a timer ends. /// /// /// Moodlets that use this should probably also share a category with each other, but this isn't necessarily required. /// Only if you intend that "Re-using the drug" should also remove the negative moodlet. /// private void ReplaceMood(EntityUid uid, string prototypeId) { if (!_prototypeManager.TryIndex(prototypeId, out var proto) || proto.MoodletOnEnd is null) return; var ev = new MoodEffectEvent(proto.MoodletOnEnd); EntityManager.EventBus.RaiseLocalEvent(uid, ev); } private void OnMobStateChanged(EntityUid uid, MoodComponent component, MobStateChangedEvent args) { if (!_config.GetCVar(CCVars.MoodEnabled)) return; if (args.NewMobState == MobState.Dead && args.OldMobState != MobState.Dead) { var ev = new MoodEffectEvent("Dead"); RaiseLocalEvent(uid, ev); } else if (args.OldMobState == MobState.Dead && args.NewMobState != MobState.Dead) { var ev = new MoodRemoveEffectEvent("Dead"); RaiseLocalEvent(uid, ev); } RefreshMood(uid, component); } // // Recalculate the mood level of an entity by summing up all moodlets. // public void RefreshMood(EntityUid uid, MoodComponent component) // WWDP made public { var amount = 0f; foreach (var (_, protoId) in component.CategorisedEffects) { if (!_prototypeManager.TryIndex(protoId, out var prototype)) continue; amount += prototype.MoodChange; } foreach (var (_, value) in component.UncategorisedEffects) amount += value; SetMood(uid, amount, component, refresh: true); } private void OnInit(EntityUid uid, MoodComponent component, ComponentStartup args) { if (!_config.GetCVar(CCVars.MoodEnabled)) return; if (_config.GetCVar(CCVars.MoodModifiesThresholds) && TryComp(uid, out var mobThresholdsComponent) && _mobThreshold.TryGetThresholdForState(uid, MobState.Critical, out var critThreshold, mobThresholdsComponent)) component.CritThresholdBeforeModify = critThreshold.Value; EnsureComp(uid); RefreshMood(uid, component); } private void SetMood(EntityUid uid, float amount, MoodComponent? component = null, bool force = false, bool refresh = false) { if (!_config.GetCVar(CCVars.MoodEnabled) || !Resolve(uid, ref component) || component.CurrentMoodThreshold == MoodThreshold.Dead && !refresh) return; var neutral = component.MoodThresholds[MoodThreshold.Neutral]; var ev = new OnSetMoodEvent(uid, amount, false); RaiseLocalEvent(uid, ref ev); if (ev.Cancelled) return; uid = ev.Receiver; amount = ev.MoodChangedAmount; var newMoodLevel = amount + neutral + ev.MoodOffset; if (!force) newMoodLevel = Math.Clamp( amount + neutral, component.MoodThresholds[MoodThreshold.Dead], component.MoodThresholds[MoodThreshold.Perfect]); component.CurrentMoodLevel = newMoodLevel; if (TryComp(uid, out var mood)) { mood.CurrentMoodLevel = component.CurrentMoodLevel; mood.NeutralMoodThreshold = component.MoodThresholds.GetValueOrDefault(MoodThreshold.Neutral); } RefreshShaders(uid, component.CurrentMoodLevel); UpdateCurrentThreshold(uid, component); } private void UpdateCurrentThreshold(EntityUid uid, MoodComponent? component = null) { if (!Resolve(uid, ref component)) return; var calculatedThreshold = GetMoodThreshold(component); if (calculatedThreshold == component.CurrentMoodThreshold) return; component.CurrentMoodThreshold = calculatedThreshold; DoMoodThresholdsEffects(uid, component); } private void DoMoodThresholdsEffects(EntityUid uid, MoodComponent? component = null, bool force = false) { if (!Resolve(uid, ref component) || component.CurrentMoodThreshold == component.LastThreshold && !force) return; var modifier = GetMovementThreshold(component.CurrentMoodThreshold); // Modify mob stats if (modifier != GetMovementThreshold(component.LastThreshold)) { _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); SetCritThreshold(uid, component, modifier); } // Modify interface if (component.MoodThresholdsAlerts.TryGetValue(component.CurrentMoodThreshold, out var alertId)) _alerts.ShowAlert(uid, alertId); else _alerts.ClearAlertCategory(uid, component.MoodCategory); component.LastThreshold = component.CurrentMoodThreshold; } private void RefreshShaders(EntityUid uid, float mood) { EnsureComp(uid, out var comp); comp.SaturationScale = mood / 50; Dirty(uid, comp); } private void SetCritThreshold(EntityUid uid, MoodComponent component, int modifier) { if (!_config.GetCVar(CCVars.MoodModifiesThresholds) || !TryComp(uid, out var mobThresholds) || !_mobThreshold.TryGetThresholdForState(uid, MobState.Critical, out var key)) return; var newKey = modifier switch { 1 => FixedPoint2.New(key.Value.Float() * component.IncreaseCritThreshold), -1 => FixedPoint2.New(key.Value.Float() * component.DecreaseCritThreshold), _ => component.CritThresholdBeforeModify, }; component.CritThresholdBeforeModify = key.Value; _mobThreshold.SetMobStateThreshold(uid, newKey, MobState.Critical, mobThresholds); } private MoodThreshold GetMoodThreshold(MoodComponent component, float? moodLevel = null) { moodLevel ??= component.CurrentMoodLevel; var result = MoodThreshold.Dead; var value = component.MoodThresholds[MoodThreshold.Perfect]; foreach (var threshold in component.MoodThresholds) if (threshold.Value <= value && threshold.Value >= moodLevel) { result = threshold.Key; value = threshold.Value; } return result; } private int GetMovementThreshold(MoodThreshold threshold) => threshold switch { >= MoodThreshold.Good => 1, <= MoodThreshold.Meh => -1, _ => 0, }; // WWDP edit start public void UpdateDamageState(EntityUid uid, MoodComponent component) { if (!TryComp(uid, out var damageable)) return; if (!_mobThreshold.TryGetPercentageForState(uid, MobState.Critical, damageable.TotalDamage, out var damage)) return; var protoId = "HealthNoDamage"; var value = component.HealthMoodEffectsThresholds["HealthNoDamage"]; foreach (var threshold in component.HealthMoodEffectsThresholds) if (threshold.Value <= damage && threshold.Value >= value) { protoId = threshold.Key; value = threshold.Value; } if (HasComp(uid)) // WWDP painkillers protoId = "HealthNoDamage"; var ev = new MoodEffectEvent(protoId); RaiseLocalEvent(uid, ev); } private void OnDamageChange(EntityUid uid, MoodComponent component, DamageChangedEvent args) => UpdateDamageState(uid, component); // WWDP edit end } [UsedImplicitly, DataDefinition] public sealed partial class ShowMoodEffects : IAlertClick { public void AlertClicked(EntityUid uid) { var entityManager = IoCManager.Resolve(); var prototypeManager = IoCManager.Resolve(); var chatManager = IoCManager.Resolve(); var playerManager = IoCManager.Resolve(); if (!entityManager.TryGetComponent(uid, out var comp) || !playerManager.TryGetSessionByEntity(uid, out var session)) return; var msgStart = Loc.GetString("mood-show-effects-start"); chatManager.ChatMessageToOne(ChatChannel.Emotes, msgStart, msgStart, EntityUid.Invalid, false, session.Channel); foreach (var (_, protoId) in comp.CategorisedEffects) { if (!prototypeManager.TryIndex(protoId, out var proto) || proto.Hidden) continue; SendDescToChat(proto, session); } foreach (var (protoId, _) in comp.UncategorisedEffects) { if (!prototypeManager.TryIndex(protoId, out var proto) || proto.Hidden) continue; SendDescToChat(proto, session); } } private void SendDescToChat(MoodEffectPrototype proto, ICommonSession session) { var chatManager = IoCManager.Resolve(); var color = (proto.MoodChange > 0) ? "#008000" : "#BA0000"; var msg = $"[font size=10][color={color}]{proto.Description}[/color][/font]"; chatManager.ChatMessageToOne( ChatChannel.Emotes, msg, msg, EntityUid.Invalid, false, session.Channel); } }