Files
wwdpublic/Content.Server/Mood/MoodSystem.cs
VMSolidus dc52f8bf2b Mood Rework Part 1 (#2425)
This PR significantly reworks some parts of the mood system, namely by
completely restoring and reworking the saturation scale shader so that
its not completely terrible. Additionally, I've added numerous new
instances and locations where Moodlets can be found in the game,
particularly when it comes to food and drugs, as well as a new Mood
interaction with the Deep Fryer. Chef gameplay is significantly expanded
via the introduction of flavor related moodlets, as well as the almighty
deep fryer giving a unique, moderately strong, and long lasting moodlet
to anyone who eats whatever you deep fry.

Go ahead, give someone a deep fried stick of salted butter coated in
chocolate. You'll make their day.

The big differences with the Saturation Scale are that its now variable,
with smooth transitions, with the scale scaling with your character's
mood. The more depressed you are, the more desaturated the world
becomes. Whereas if you have entirely too many positive mood bonuses,
the world becomes incredibly vibrant.

<details><summary><h1>Media</h1></summary>
<p>

Shoukou's Bar as seen by someone with the Sanguine trait(and no other
moodlets)

![image](https://github.com/user-attachments/assets/bf8e7b25-5243-41ee-a6ad-3170444faae6)

Max mood

![image](https://github.com/user-attachments/assets/fc03ee20-37a5-4163-ac35-8f2735f8b531)

Saturnine trait:

![image](https://github.com/user-attachments/assets/fc21fc20-81e5-4364-807f-fcef40837ade)

Minimum mood(dead)

![image](https://github.com/user-attachments/assets/b38e8ce8-0ea2-436d-b298-b1a715b0a6c2)

Smooth transitions for shader tone.

https://github.com/user-attachments/assets/3ab55da1-eca6-4cc5-9489-f4ad13ed0f27

</p>
</details>

🆑
- add: Re-enabled the "Mood shader" after significantly reworking it.
Mood visual effects now scale with your character's mood, instead of
only ever being near-greyscale. Being high life now makes the world more
colorful and saturated.
- add: A huge variety of medicines, drugs, and even food items(based on
flavor!) now have mood effects. Reaching for the packet of salt now
actually makes food provide a better mood buff.
- add: Being Tear-gassed causes a massive mood penalty.
- add: Deep frying food provides a strong mood bonus.
- add: Added new Manic, Mercurial, and Dead Emotions traits.

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
2025-07-12 00:55:42 +10:00

461 lines
17 KiB
C#

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<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);
}
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.ContainsKey(prototypeId))
return;
comp.UncategorisedEffects.Remove(prototypeId);
}
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
}
[UsedImplicitly, DataDefinition]
public sealed partial class ShowMoodEffects : IAlertClick
{
public void AlertClicked(EntityUid uid)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var chatManager = IoCManager.Resolve<IChatManager>();
var playerManager = IoCManager.Resolve<IPlayerManager>();
if (!entityManager.TryGetComponent<MoodComponent>(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<MoodEffectPrototype>(protoId, out var proto)
|| proto.Hidden)
continue;
SendDescToChat(proto, session);
}
foreach (var (protoId, _) in comp.UncategorisedEffects)
{
if (!prototypeManager.TryIndex<MoodEffectPrototype>(protoId, out var proto)
|| proto.Hidden)
continue;
SendDescToChat(proto, session);
}
}
private void SendDescToChat(MoodEffectPrototype proto, ICommonSession session)
{
var chatManager = IoCManager.Resolve<IChatManager>();
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);
}
}