Files
wwdpublic/Content.Server/Mood/MoodSystem.cs
Nemanja 1408d6c712 Replace IClickAlert with events (#30728)
* Replace IAlertClick with events

* whoop

* eek!
2025-07-19 11:12:44 +10:00

449 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Robust.Shared.Prototypes;
using Timer = Robust.Shared.Timing.Timer;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Content.Shared.CCVar;
using Robust.Shared.Utility;
namespace Content.Server.Mood;
public sealed class MoodSystem : EntitySystem
{
[Dependency] private readonly IChatManager _сhatManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedJetpackSystem _jetpack = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MoodComponent, ComponentStartup>(OnInit);
SubscribeLocalEvent<MoodComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<MoodComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<MoodComponent, MoodEffectEvent>(OnMoodEffect);
SubscribeLocalEvent<MoodComponent, DamageChangedEvent>(OnDamageChange);
SubscribeLocalEvent<MoodComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMoveSpeed);
SubscribeLocalEvent<MoodComponent, MoodRemoveEffectEvent>(OnRemoveEffect);
SubscribeLocalEvent<MoodComponent, ShowMoodAlertEvent>(OnShowMoodAlert);
}
private void OnShowMoodAlert(EntityUid uid, MoodComponent component, ShowMoodAlertEvent args)
{
if (!_playerManager.TryGetSessionByEntity(uid, out var session))
return;
var msg = $"{Loc.GetString("mood-show-effects-start")}\n";
foreach (var (_, protoId) in component.CategorisedEffects)
{
if (!_prototypeManager.TryIndex<MoodEffectPrototype>(protoId, out var proto)
|| proto.Hidden)
continue;
var color = proto.MoodChange > 0 ? "#008000" : "#BA0000";
msg += $"[font size=10][color={color}]{proto.Description}[/color][/font]\n";
}
foreach (var (protoId, _) in component.UncategorisedEffects)
{
if (!_prototypeManager.TryIndex<MoodEffectPrototype>(protoId, out var proto)
|| proto.Hidden)
continue;
var color = proto.MoodChange > 0 ? "#008000" : "#BA0000";
msg += $"[font size=10][color={color}]{proto.Description}[/color][/font]\n";
}
_сhatManager.ChatMessageToOne(
ChatChannel.Emotes,
msg,
msg,
EntityUid.Invalid,
false,
session.Channel);
}
private void OnShutdown(EntityUid uid, MoodComponent component, ComponentShutdown args)
{
_alerts.ClearAlertCategory(uid, component.MoodCategory);
RemComp<SaturationScaleOverlayComponent>(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<MoodEffectPrototype>(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<MoodEffectPrototype>(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<MoodComponent>(uid, out var comp))
return;
if (category == null)
{
if (!comp.UncategorisedEffects.Remove(prototypeId))
return;
}
else
{
if (!comp.CategorisedEffects.TryGetValue(category, out var currentProtoId)
|| currentProtoId != prototypeId
|| !_prototypeManager.HasIndex<MoodEffectPrototype>(currentProtoId))
return;
comp.CategorisedEffects.Remove(category);
}
ReplaceMood(uid, prototypeId);
RefreshMood(uid, comp);
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private void ReplaceMood(EntityUid uid, string prototypeId)
{
if (!_prototypeManager.TryIndex<MoodEffectPrototype>(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);
}
// <summary>
// Recalculate the mood level of an entity by summing up all moodlets.
// </summary>
public void RefreshMood(EntityUid uid, MoodComponent component) // WWDP made public
{
var amount = 0f;
foreach (var (_, protoId) in component.CategorisedEffects)
{
if (!_prototypeManager.TryIndex<MoodEffectPrototype>(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<MobThresholdsComponent>(uid, out var mobThresholdsComponent)
&& _mobThreshold.TryGetThresholdForState(uid, MobState.Critical, out var critThreshold, mobThresholdsComponent))
component.CritThresholdBeforeModify = critThreshold.Value;
EnsureComp<NetMoodComponent>(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<NetMoodComponent>(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<SaturationScaleOverlayComponent>(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<MobThresholdsComponent>(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<DamageableComponent>(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<NoScreamComponent>(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
}