diff --git a/Content.Client/Overlays/SaturationScaleOverlay.cs b/Content.Client/Overlays/SaturationScaleOverlay.cs new file mode 100644 index 00000000000..199b54f8c9f --- /dev/null +++ b/Content.Client/Overlays/SaturationScaleOverlay.cs @@ -0,0 +1,39 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client.Overlays; + +public sealed class SaturationScaleOverlay : Overlay +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private readonly ShaderInstance _shader; + private const float Saturation = 0.5f; + + + public SaturationScaleOverlay() + { + IoCManager.InjectDependencies(this); + + _shader = _prototypeManager.Index("SaturationScale").InstanceUnique(); + } + + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _shader.SetParameter("saturation", Saturation); + + var handle = args.WorldHandle; + + handle.UseShader(_shader); + handle.DrawRect(args.WorldBounds, Color.White); + handle.UseShader(null); + } +} diff --git a/Content.Client/Overlays/SaturationScaleSystem.cs b/Content.Client/Overlays/SaturationScaleSystem.cs new file mode 100644 index 00000000000..57ce4d41682 --- /dev/null +++ b/Content.Client/Overlays/SaturationScaleSystem.cs @@ -0,0 +1,63 @@ +using Content.Shared.GameTicking; +using Content.Shared.Overlays; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.Overlays; + +public sealed class SaturationScaleSystem : EntitySystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private SaturationScaleOverlay _overlay = default!; + + + public override void Initialize() + { + base.Initialize(); + + _overlay = new(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + SubscribeNetworkEvent(RoundRestartCleanup); + } + + + private void RoundRestartCleanup(RoundRestartCleanupEvent ev) + { + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnPlayerDetached(EntityUid uid, SaturationScaleOverlayComponent component, PlayerDetachedEvent args) + { + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnPlayerAttached(EntityUid uid, SaturationScaleOverlayComponent component, PlayerAttachedEvent args) + { + _overlayMan.AddOverlay(_overlay); + } + + private void OnShutdown(EntityUid uid, SaturationScaleOverlayComponent component, ComponentShutdown args) + { + if (_player.LocalSession?.AttachedEntity != uid) + return; + + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnInit(EntityUid uid, SaturationScaleOverlayComponent component, ComponentInit args) + { + if (_player.LocalSession?.AttachedEntity != uid) + return; + + _overlayMan.AddOverlay(_overlay); + } +} diff --git a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs index 511a720ed07..61dcc3331da 100644 --- a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs @@ -37,8 +37,8 @@ public async Task BananaSlipTest() var sprintWalks = sys.Config.GetCVar(CCVars.GamePressToSprint); await SpawnTarget("TrashBananaPeel"); - var modifier = Comp(Player).SprintSpeedModifier; - Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); + // var modifier = Comp(Player).SprintSpeedModifier; + // Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); // Yeeting this pointless Assert because it's not actually important. // Player is to the left of the banana peel and has not slipped. #pragma warning disable NUnit2045 diff --git a/Content.Server/Arcade/BlockGame/BlockGame.cs b/Content.Server/Arcade/BlockGame/BlockGame.cs index 3af1828d564..675776828f8 100644 --- a/Content.Server/Arcade/BlockGame/BlockGame.cs +++ b/Content.Server/Arcade/BlockGame/BlockGame.cs @@ -2,6 +2,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Random; using System.Linq; +using Content.Shared.Mood; namespace Content.Server.Arcade.BlockGame; @@ -82,6 +83,8 @@ private void InvokeGameover() { _highScorePlacement = _arcadeSystem.RegisterHighScore(meta.EntityName, Points); SendHighscoreUpdate(); + var ev = new MoodEffectEvent("ArcadePlay"); + _entityManager.EventBus.RaiseLocalEvent(meta.Owner, ev); } SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement)); } diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs index f60d88ebf78..a5ca626d131 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.UserInterface; using Content.Server.Advertise; using Content.Server.Advertise.Components; +using Content.Shared.Mood; using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; using Robust.Server.GameObjects; using Robust.Shared.Audio; @@ -76,6 +77,9 @@ private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent compone if (!TryComp(uid, out var power) || !power.Powered) return; + if (msg.Session.AttachedEntity != null) + RaiseLocalEvent(msg.Session.AttachedEntity.Value, new MoodEffectEvent("ArcadePlay")); + switch (msg.PlayerAction) { case PlayerAction.Attack: diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs index 948373940e4..9bea58330cd 100644 --- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs +++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.FixedPoint; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; +using Content.Shared.Mood; using Robust.Shared.Containers; namespace Content.Server.Atmos.EntitySystems @@ -239,14 +240,16 @@ public override void Update(float frameTime) barotrauma.TakingDamage = true; _adminLogger.Add(LogType.Barotrauma, $"{ToPrettyString(uid):entity} started taking low pressure damage"); } - - _alertsSystem.ShowAlert(uid, AlertType.LowPressure, 2); + RaiseLocalEvent(uid, new MoodEffectEvent("MobLowPressure")); + _alertsSystem.ShowAlert(uid, AlertType.LowPressure, 2); } else if (pressure >= Atmospherics.HazardHighPressure) { var damageScale = MathF.Min(((pressure / Atmospherics.HazardHighPressure) - 1) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage); _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false); + RaiseLocalEvent(uid, new MoodEffectEvent("MobHighPressure")); + if (!barotrauma.TakingDamage) { barotrauma.TakingDamage = true; diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index d34de937a41..7890b7751ac 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -23,6 +23,7 @@ using Content.Shared.Weapons.Melee.Events; using Content.Shared.FixedPoint; using Robust.Server.Audio; +using Content.Shared.Mood; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; @@ -410,10 +411,12 @@ public override void Update(float frameTime) if (!flammable.OnFire) { _alertsSystem.ClearAlert(uid, AlertType.Fire); + RaiseLocalEvent(uid, new MoodRemoveEffectEvent("OnFire")); continue; } _alertsSystem.ShowAlert(uid, AlertType.Fire); + RaiseLocalEvent(uid, new MoodEffectEvent("OnFire")); if (flammable.FireStacks > 0) { diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs index c845b17230a..2798103cc1c 100644 --- a/Content.Server/Bible/BibleSystem.cs +++ b/Content.Server/Bible/BibleSystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Popups; using Content.Shared.Timing; using Content.Shared.Verbs; +using Content.Shared.Mood; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; @@ -153,6 +154,8 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter _audio.PlayPvs(component.HealSoundPath, args.User); _delay.TryResetDelay((uid, useDelay)); } + + RaiseLocalEvent(args.Target.Value, new MoodEffectEvent("GotBlessed")); } private void AddSummonVerb(EntityUid uid, SummonableComponent component, GetVerbsEvent args) diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index c7266e2c463..389e5fbab72 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.Mobs.Systems; +using Content.Shared.Mood; using JetBrains.Annotations; using Robust.Shared.Timing; @@ -177,6 +178,7 @@ private void TakeSuffocationDamage(Entity ent) { _alertsSystem.ShowAlert(ent, comp.Alert); } + RaiseLocalEvent(ent, new MoodEffectEvent("Suffocating")); } _damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false); diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index fc9f0a9a9ff..a6f8522346f 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -21,6 +21,7 @@ using Robust.Shared.Timing; using System.Linq; using System.Text; +using Content.Shared.Mood; namespace Content.Server.GameTicking.Rules; @@ -199,6 +200,8 @@ public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool _npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false); _npcFaction.AddFaction(traitor, component.SyndicateFaction); + RaiseLocalEvent(traitor, new MoodEffectEvent("TraitorFocused")); + // Give traitors their objectives if (giveObjectives) { diff --git a/Content.Server/Interaction/InteractionPopupSystem.cs b/Content.Server/Interaction/InteractionPopupSystem.cs index 77b76f898a7..a028598df03 100644 --- a/Content.Server/Interaction/InteractionPopupSystem.cs +++ b/Content.Server/Interaction/InteractionPopupSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Interaction; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Mood; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; @@ -78,7 +79,19 @@ private void SharedInteract( if (_random.Prob(component.SuccessChance)) { if (component.InteractSuccessString != null) + { msg = Loc.GetString(component.InteractSuccessString, ("target", Identity.Entity(uid, EntityManager))); // Success message (localized). + if (component.InteractSuccessString == "hugging-success-generic") + { + var ev = new MoodEffectEvent("BeingHugged"); + RaiseLocalEvent(target, ev); + } + else if (component.InteractSuccessString.Contains("petting-success-")) + { + var ev = new MoodEffectEvent("PetAnimal"); + RaiseLocalEvent(user, ev); + } + } if (component.InteractSuccessSound != null) sfx = component.InteractSuccessSound; diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs index 8c3b15aed33..dc049b2a1d6 100644 --- a/Content.Server/Medical/VomitSystem.cs +++ b/Content.Server/Medical/VomitSystem.cs @@ -11,6 +11,7 @@ using Content.Shared.Nutrition.EntitySystems; using Content.Shared.StatusEffect; using Robust.Server.Audio; +using Content.Shared.Mood; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -94,6 +95,8 @@ public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = - // Force sound to play as spill doesn't work if solution is empty. _audio.PlayPvs("/Audio/Effects/Fluids/splat.ogg", uid, AudioParams.Default.WithVariation(0.2f).WithVolume(-4f)); _popup.PopupEntity(Loc.GetString("disease-vomit", ("person", Identity.Entity(uid, EntityManager))), uid); + + RaiseLocalEvent(uid, new MoodEffectEvent("MobVomit")); } } } diff --git a/Content.Server/Mood/MoodComponent.cs b/Content.Server/Mood/MoodComponent.cs new file mode 100644 index 00000000000..7fd4a7136f3 --- /dev/null +++ b/Content.Server/Mood/MoodComponent.cs @@ -0,0 +1,111 @@ +using Content.Shared.Alert; +using Content.Shared.FixedPoint; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +namespace Content.Server.Mood; + +[RegisterComponent] +public sealed partial class MoodComponent : Component +{ + [DataField] + public float CurrentMoodLevel; + + [DataField] + public MoodThreshold CurrentMoodThreshold; + + [DataField] + public MoodThreshold LastThreshold; + + [ViewVariables(VVAccess.ReadOnly)] + public readonly Dictionary CategorisedEffects = new(); + + [ViewVariables(VVAccess.ReadOnly)] + public readonly Dictionary UncategorisedEffects = new(); + + /// + /// The formula for the movement speed modifier is SpeedBonusGrowth ^ (MoodLevel - MoodThreshold.Neutral). + /// Change this ONLY BY 0.001 AT A TIME. + /// + [DataField] + public float SpeedBonusGrowth = 1.003f; + + /// + /// The lowest point that low morale can multiply our movement speed by. Lowering speed follows a linear curve, rather than geometric. + /// + [DataField] + public float MinimumSpeedModifier = 0.75f; + + /// + /// The maximum amount that high morale can multiply our movement speed by. This follows a significantly slower geometric sequence. + /// + [DataField] + public float MaximumSpeedModifier = 1.15f; + + [DataField] + public float IncreaseCritThreshold = 1.2f; + + [DataField] + public float DecreaseCritThreshold = 0.9f; + + [ViewVariables(VVAccess.ReadOnly)] + public FixedPoint2 CritThresholdBeforeModify; + + [DataField(customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary MoodThresholds = new() + { + { MoodThreshold.Perfect, 100f }, + { MoodThreshold.Exceptional, 80f }, + { MoodThreshold.Great, 70f }, + { MoodThreshold.Good, 60f }, + { MoodThreshold.Neutral, 50f }, + { MoodThreshold.Meh, 40f }, + { MoodThreshold.Bad, 30f }, + { MoodThreshold.Terrible, 20f }, + { MoodThreshold.Horrible, 10f }, + { MoodThreshold.Dead, 0f } + }; + + [DataField(customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary MoodThresholdsAlerts = new() + { + { MoodThreshold.Dead, AlertType.MoodDead }, + { MoodThreshold.Horrible, AlertType.Horrible }, + { MoodThreshold.Terrible, AlertType.Terrible }, + { MoodThreshold.Bad, AlertType.Bad }, + { MoodThreshold.Meh, AlertType.Meh }, + { MoodThreshold.Neutral, AlertType.Neutral }, + { MoodThreshold.Good, AlertType.Good }, + { MoodThreshold.Great, AlertType.Great }, + { MoodThreshold.Exceptional, AlertType.Exceptional }, + { MoodThreshold.Perfect, AlertType.Perfect }, + { MoodThreshold.Insane, AlertType.Insane } + }; + + /// + /// These thresholds represent a percentage of Crit-Threshold, 0.8 corresponding with 80%. + /// + [DataField(customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary HealthMoodEffectsThresholds = new() + { + { "HealthHeavyDamage", 0.8f }, + { "HealthSevereDamage", 0.5f }, + { "HealthLightDamage", 0.1f }, + { "HealthNoDamage", 0.05f } + }; +} + +[Serializable] +public enum MoodThreshold : ushort +{ + Insane = 1, + Horrible = 2, + Terrible = 3, + Bad = 4, + Meh = 5, + Neutral = 6, + Good = 7, + Great = 8, + Exceptional = 9, + Perfect = 10, + Dead = 0 +} diff --git a/Content.Server/Mood/MoodSystem.cs b/Content.Server/Mood/MoodSystem.cs new file mode 100644 index 00000000000..f0c293971a1 --- /dev/null +++ b/Content.Server/Mood/MoodSystem.cs @@ -0,0 +1,422 @@ +using Content.Server.Chat.Managers; +using Content.Server.Popups; +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 Content.Shared.Traits.Assorted.Components; +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(OnMobStateChanged); + SubscribeLocalEvent(OnMoodEffect); + SubscribeLocalEvent(OnDamageChange); + SubscribeLocalEvent(OnRefreshMoveSpeed); + SubscribeLocalEvent(OnRemoveEffect); + + SubscribeLocalEvent(OnTraitStartup); + } + + + private void OnRemoveEffect(EntityUid uid, MoodComponent component, MoodRemoveEffectEvent args) + { + 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 (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 OnTraitStartup(EntityUid uid, MoodModifyTraitComponent component, ComponentStartup args) + { + if (component.MoodId != null) + RaiseLocalEvent(uid, new MoodEffectEvent($"{component.MoodId}")); + } + + 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; + + if (prototype.ID != oldPrototype.ID) + { + SendEffectText(uid, prototype); + 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; + + 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) + _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); + } + + RefreshMood(uid, comp); + } + + private void OnMobStateChanged(EntityUid uid, MoodComponent component, MobStateChangedEvent args) + { + 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. + // + private void RefreshMood(EntityUid uid, MoodComponent component) + { + 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 (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; + else + { + uid = ev.Receiver; + amount = ev.MoodChangedAmount; + } + + var newMoodLevel = amount + neutral; + 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); + } + + 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); + RefreshShaders(uid, modifier); + } + + // Modify interface + if (component.MoodThresholdsAlerts.TryGetValue(component.CurrentMoodThreshold, out var alertId)) + _alerts.ShowAlert(uid, alertId); + else + _alerts.ClearAlertCategory(uid, AlertCategory.Mood); + + component.LastThreshold = component.CurrentMoodThreshold; + } + + private void RefreshShaders(EntityUid uid, int modifier) + { + if (modifier == -1) + EnsureComp(uid); + else + RemComp(uid); + } + + private void SetCritThreshold(EntityUid uid, MoodComponent component, int modifier) + { + if (!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) + { + return threshold switch + { + >= MoodThreshold.Good => 1, + <= MoodThreshold.Meh => -1, + _ => 0 + }; + } + + private void OnDamageChange(EntityUid uid, MoodComponent component, DamageChangedEvent args) + { + if (!_mobThreshold.TryGetPercentageForState(uid, MobState.Critical, args.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; + } + + var ev = new MoodEffectEvent(protoId); + RaiseLocalEvent(uid, ev); + } +} + +[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) + || comp.CurrentMoodThreshold == MoodThreshold.Dead + || !playerManager.TryGetSessionByEntity(uid, out var session) + || session == null) + 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) + { + if (session == null) + return; + + 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); + } +} diff --git a/Content.Server/Traits/Assorted/ModifyMoodTraitComponent.cs b/Content.Server/Traits/Assorted/ModifyMoodTraitComponent.cs new file mode 100644 index 00000000000..f9ae3b36f3a --- /dev/null +++ b/Content.Server/Traits/Assorted/ModifyMoodTraitComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Traits.Assorted.Components; + +/// +/// Used for traits that add a starting moodlet. +/// +[RegisterComponent] +public sealed partial class MoodModifyTraitComponent : Component +{ + [DataField] + public string? MoodId = null; +} diff --git a/Content.Shared/Alert/AlertCategory.cs b/Content.Shared/Alert/AlertCategory.cs index 7450f585a4e..57a3e40f70e 100644 --- a/Content.Shared/Alert/AlertCategory.cs +++ b/Content.Shared/Alert/AlertCategory.cs @@ -10,6 +10,7 @@ public enum AlertCategory Breathing, Buckled, Health, + Mood, Internals, Stamina, Piloting, diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index dc323dc64a8..1130e25b66d 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -25,6 +25,22 @@ public enum AlertType : byte HumanHealth, BorgBattery, BorgBatteryNone, + + // Mood + Bleeding, + Insane, + Horrible, + Terrible, + Bad, + Meh, + Neutral, + Good, + Great, + Exceptional, + Perfect, + MoodDead, + CultBuffed, + PilotingShuttle, Peckish, Starving, diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index ff2a915b1ba..19d0ca6461c 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2381,6 +2381,12 @@ public static readonly CVarDef public static readonly CVarDef DoMindContests = CVarDef.Create("contests.do_mind_contests", true, CVar.REPLICATED | CVar.SERVER); + /// + /// Toggles all MoodContest functions. All mood contests output 1f when false. + /// + public static readonly CVarDef DoMoodContests = + CVarDef.Create("contests.do_mood_contests", true, CVar.REPLICATED | CVar.SERVER); + /// /// The maximum amount that Mass Contests can modify a physics multiplier, given as a +/- percentage /// Default of 0.25f outputs between * 0.75f and 1.25f @@ -2438,5 +2444,18 @@ public static readonly CVarDef CVarDef.Create("supermatter.rads_modifier", 1f, CVar.SERVER); #endregion + + #region Mood System + + public static readonly CVarDef MoodEnabled = + CVarDef.Create("mood.enabled", true, CVar.SERVER); + + public static readonly CVarDef MoodIncreasesSpeed = + CVarDef.Create("mood.increases_speed", true, CVar.SERVER); + + public static readonly CVarDef MoodDecreasesSpeed = + CVarDef.Create("mood.decreases_speed", true, CVar.SERVER); + + #endregion } } diff --git a/Content.Shared/Contests/ContestsSystem.cs b/Content.Shared/Contests/ContestsSystem.cs index 47924f4d795..008e5e9ca97 100644 --- a/Content.Shared/Contests/ContestsSystem.cs +++ b/Content.Shared/Contests/ContestsSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Mood; using Robust.Shared.Configuration; using Robust.Shared.Physics.Components; @@ -251,6 +252,52 @@ public float MindContest(EntityUid performer, EntityUid target, bool bypassClamp #endregion + #region Mood Contests + + /// + /// Outputs the ratio of an Entity's mood level and its Neutral Mood threshold. + /// + /// + /// + /// + public float MoodContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMoodContests) + || !TryComp(performer, out var mood)) + return 1f; + + return _cfg.GetCVar(CCVars.AllowClampOverride) && bypassClamp + ? mood.CurrentMoodLevel / mood.NeutralMoodThreshold + : Math.Clamp(mood.CurrentMoodLevel / mood.NeutralMoodThreshold, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor); + } + + /// + /// Outputs the ratio of mood level between two Entities. + /// + /// + /// + /// + /// + public float MoodContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMoodContests) + || !TryComp(performer, out var performerMood) + || !TryComp(target, out var targetMood)) + return 1f; + + return _cfg.GetCVar(CCVars.AllowClampOverride) && bypassClamp + ? performerMood.CurrentMoodLevel / targetMood.CurrentMoodLevel + : Math.Clamp(performerMood.CurrentMoodLevel / targetMood.CurrentMoodLevel, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor); + } + + #endregion + #region EVERY CONTESTS public float EveryContest( @@ -259,34 +306,40 @@ public float EveryContest( bool bypassClampStamina = false, bool bypassClampHealth = false, bool bypassClampMind = false, + bool bypassClampMood = false, float rangeFactorMass = 1f, float rangeFactorStamina = 1f, float rangeFactorHealth = 1f, float rangeFactorMind = 1f, + float rangeFactorMood = 1f, float weightMass = 1f, float weightStamina = 1f, float weightHealth = 1f, float weightMind = 1f, + float weightMood = 1f, bool sumOrMultiply = false) { if (!_cfg.GetCVar(CCVars.DoContestsSystem)) return 1f; - var weightTotal = weightMass + weightStamina + weightHealth + weightMind; + var weightTotal = weightMass + weightStamina + weightHealth + weightMind + weightMood; var massMultiplier = weightMass / weightTotal; var staminaMultiplier = weightStamina / weightTotal; var healthMultiplier = weightHealth / weightTotal; var mindMultiplier = weightMind / weightTotal; + var moodMultiplier = weightMood / weightTotal; return sumOrMultiply ? MassContest(performer, bypassClampMass, rangeFactorMass) * massMultiplier + StaminaContest(performer, bypassClampStamina, rangeFactorStamina) * staminaMultiplier + HealthContest(performer, bypassClampHealth, rangeFactorHealth) * healthMultiplier + MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier + + MoodContest(performer, bypassClampMood, rangeFactorMood) * moodMultiplier : MassContest(performer, bypassClampMass, rangeFactorMass) * massMultiplier * StaminaContest(performer, bypassClampStamina, rangeFactorStamina) * staminaMultiplier * HealthContest(performer, bypassClampHealth, rangeFactorHealth) * healthMultiplier - * MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier; + * MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier + * MoodContest(performer, bypassClampMood, rangeFactorMood) * moodMultiplier; } public float EveryContest( @@ -296,34 +349,40 @@ public float EveryContest( bool bypassClampStamina = false, bool bypassClampHealth = false, bool bypassClampMind = false, + bool bypassClampMood = false, float rangeFactorMass = 1f, float rangeFactorStamina = 1f, float rangeFactorHealth = 1f, float rangeFactorMind = 1f, + float rangeFactorMood = 1f, float weightMass = 1f, float weightStamina = 1f, float weightHealth = 1f, float weightMind = 1f, + float weightMood = 1f, bool sumOrMultiply = false) { if (!_cfg.GetCVar(CCVars.DoContestsSystem)) return 1f; - var weightTotal = weightMass + weightStamina + weightHealth + weightMind; + var weightTotal = weightMass + weightStamina + weightHealth + weightMind + weightMood; var massMultiplier = weightMass / weightTotal; var staminaMultiplier = weightStamina / weightTotal; var healthMultiplier = weightHealth / weightTotal; var mindMultiplier = weightMind / weightTotal; + var moodMultiplier = weightMood / weightTotal; return sumOrMultiply ? MassContest(performer, target, bypassClampMass, rangeFactorMass) * massMultiplier + StaminaContest(performer, target, bypassClampStamina, rangeFactorStamina) * staminaMultiplier + HealthContest(performer, target, bypassClampHealth, rangeFactorHealth) * healthMultiplier + MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier + + MoodContest(performer, target, bypassClampMood, rangeFactorMood) * moodMultiplier : MassContest(performer, target, bypassClampMass, rangeFactorMass) * massMultiplier * StaminaContest(performer, target, bypassClampStamina, rangeFactorStamina) * staminaMultiplier * HealthContest(performer, target, bypassClampHealth, rangeFactorHealth) * healthMultiplier - * MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier; + * MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier + * MoodContest(performer, target, bypassClampMood, rangeFactorMood) * moodMultiplier; } #endregion } diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index b47fa08bc78..ebbafef7f0e 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -28,6 +28,7 @@ using Content.Shared.Verbs; using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio.Systems; +using Content.Shared.Mood; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Player; @@ -174,9 +175,15 @@ public void UpdateCuffState(EntityUid uid, CuffableComponent component) _actionBlocker.UpdateCanMove(uid); if (component.CanStillInteract) + { _alerts.ClearAlert(uid, AlertType.Handcuffed); + RaiseLocalEvent(uid, new MoodRemoveEffectEvent("Handcuffed")); + } else + { _alerts.ShowAlert(uid, AlertType.Handcuffed); + RaiseLocalEvent(uid, new MoodEffectEvent("Handcuffed")); + } var ev = new CuffedStateChangeEvent(); RaiseLocalEvent(uid, ref ev); diff --git a/Content.Shared/Mood/MoodCategoryPrototype.cs b/Content.Shared/Mood/MoodCategoryPrototype.cs new file mode 100644 index 00000000000..13d5f8b7ea6 --- /dev/null +++ b/Content.Shared/Mood/MoodCategoryPrototype.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Mood; + +/// +/// A prototype defining a category for moodlets, where only a single moodlet of a given category is permitted. +/// +[Prototype] +public sealed class MoodCategoryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; } = default!; +} diff --git a/Content.Shared/Mood/MoodEffectPrototype.cs b/Content.Shared/Mood/MoodEffectPrototype.cs new file mode 100644 index 00000000000..ad21faec807 --- /dev/null +++ b/Content.Shared/Mood/MoodEffectPrototype.cs @@ -0,0 +1,35 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Mood; + +[Prototype] +public sealed class MoodEffectPrototype : IPrototype +{ + /// + /// The ID of the moodlet to use. + /// + [IdDataField] + public string ID { get; } = default!; + + public string Description => Loc.GetString($"mood-effect-{ID}"); + /// + /// If they already have an effect with the same category, the new one will replace the old one. + /// + [DataField, ValidatePrototypeId] + public string? Category; + /// + /// How much should this moodlet modify an entity's Mood. + /// + [DataField(required: true)] + public float MoodChange; + /// + /// How long, in Seconds, does this moodlet last? If omitted, the moodlet will last until canceled by any system. + /// + [DataField] + public int Timeout; + /// + /// Should this moodlet be hidden from the player? EG: No popups or chat messages. + /// + [DataField] + public bool Hidden; +} diff --git a/Content.Shared/Mood/MoodEvents.cs b/Content.Shared/Mood/MoodEvents.cs new file mode 100644 index 00000000000..58a993d2b7f --- /dev/null +++ b/Content.Shared/Mood/MoodEvents.cs @@ -0,0 +1,59 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Mood; + +[Serializable, NetSerializable] +public sealed class MoodEffectEvent : EntityEventArgs +{ + /// + /// ID of the moodlet prototype to use + /// + public string EffectId; + + /// + /// How much should the mood change be multiplied by + ///
+ /// This does nothing if the moodlet ID matches one with the same Category + ///
+ public float EffectModifier = 1f; + + /// + /// How much should the mood change be offset by, after multiplication + ///
+ /// This does nothing if the moodlet ID matches one with the same Category + ///
+ public float EffectOffset = 0f; + + public MoodEffectEvent(string effectId, float effectModifier = 1f, float effectOffset = 0f) + { + EffectId = effectId; + EffectModifier = effectModifier; + EffectOffset = effectOffset; + } +} + +[Serializable, NetSerializable] +public sealed class MoodRemoveEffectEvent : EntityEventArgs +{ + public string EffectId; + + public MoodRemoveEffectEvent(string effectId) + { + EffectId = effectId; + } +} + +/// +/// This event is raised whenever an entity sets their mood, allowing other systems to modify the end result of mood math. +/// EG: The end result after tallying up all Moodlets comes out to 70, but a trait multiplies it by 0.8 to make it 56. +/// +[ByRefEvent] +public record struct OnSetMoodEvent(EntityUid Receiver, float MoodChangedAmount, bool Cancelled); + +/// +/// This event is raised on an entity when it receives a mood effect, but before the effects are calculated. +/// Allows for other systems to pick and choose specific events to modify. +/// +[ByRefEvent] +public record struct OnMoodEffect(EntityUid Receiver, string EffectId, float EffectModifier = 1, float EffectOffset = 0); + diff --git a/Content.Shared/Mood/SharedMoodComponent.cs b/Content.Shared/Mood/SharedMoodComponent.cs new file mode 100644 index 00000000000..566f5c7b668 --- /dev/null +++ b/Content.Shared/Mood/SharedMoodComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Shared.Mood; + +/// +/// This component exists solely to network CurrentMoodLevel, so that clients can make use of its value for math Prediction. +/// All mood logic is otherwise handled by the Server, and the client is not allowed to know the identity of its mood events. +/// +[RegisterComponent, AutoGenerateComponentState] +public sealed partial class NetMoodComponent : Component +{ + [DataField, AutoNetworkedField] + public float CurrentMoodLevel; + + [DataField, AutoNetworkedField] + public float NeutralMoodThreshold; +} \ No newline at end of file diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs index d8808b6e4ab..1bc2a945f31 100644 --- a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs @@ -4,8 +4,12 @@ using Content.Shared.Movement.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Rejuvenate; +using Content.Shared.Mood; +using Robust.Shared.Network; using Robust.Shared.Random; using Robust.Shared.Timing; +using Robust.Shared.Configuration; +using Content.Shared.CCVar; namespace Content.Shared.Nutrition.EntitySystems; @@ -18,6 +22,8 @@ public sealed class HungerSystem : EntitySystem [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly SharedJetpackSystem _jetpack = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IConfigurationManager _config = default!; public override void Initialize() { @@ -44,10 +50,9 @@ private void OnShutdown(EntityUid uid, HungerComponent component, ComponentShutd private void OnRefreshMovespeed(EntityUid uid, HungerComponent component, RefreshMovementSpeedModifiersEvent args) { - if (component.CurrentThreshold > HungerThreshold.Starving) - return; - - if (_jetpack.IsUserFlying(uid)) + if (_config.GetCVar(CCVars.MoodEnabled) + || component.CurrentThreshold > HungerThreshold.Starving + || _jetpack.IsUserFlying(uid)) return; args.ModifySpeed(component.StarvingSlowdownModifier, component.StarvingSlowdownModifier); @@ -111,7 +116,13 @@ private void DoHungerThresholdEffects(EntityUid uid, HungerComponent? component if (GetMovementThreshold(component.CurrentThreshold) != GetMovementThreshold(component.LastThreshold)) { - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); + if (!_config.GetCVar(CCVars.MoodEnabled)) + _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); + else if (_net.IsServer) + { + var ev = new MoodEffectEvent("Hunger" + component.CurrentThreshold); + RaiseLocalEvent(uid, ev); + } } if (component.HungerThresholdAlerts.TryGetValue(component.CurrentThreshold, out var alertId)) diff --git a/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs index bd7251b9438..c61e8e5ba42 100644 --- a/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Stunnable; using Content.Shared.Throwing; +using Content.Shared.Mood; using JetBrains.Annotations; namespace Content.Shared.Nutrition.EntitySystems @@ -44,6 +45,11 @@ public void SetCreamPied(EntityUid uid, CreamPiedComponent creamPied, bool value { _appearance.SetData(uid, CreamPiedVisuals.Creamed, value, appearance); } + + if (value) + RaiseLocalEvent(uid, new MoodEffectEvent("Creampied")); + else + RaiseLocalEvent(uid, new MoodRemoveEffectEvent("Creampied")); } private void OnCreamPieLand(EntityUid uid, CreamPieComponent component, ref LandEvent args) diff --git a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs index 29218f57198..f1ddc9b2b5e 100644 --- a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs @@ -6,6 +6,9 @@ using JetBrains.Annotations; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Mood; +using Robust.Shared.Configuration; +using Content.Shared.CCVar; namespace Content.Shared.Nutrition.EntitySystems; @@ -17,6 +20,7 @@ public sealed class ThirstSystem : EntitySystem [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; [Dependency] private readonly SharedJetpackSystem _jetpack = default!; + [Dependency] private readonly IConfigurationManager _config = default!; public override void Initialize() { @@ -49,7 +53,8 @@ private void OnMapInit(EntityUid uid, ThirstComponent component, MapInitEvent ar private void OnRefreshMovespeed(EntityUid uid, ThirstComponent component, RefreshMovementSpeedModifiersEvent args) { // TODO: This should really be taken care of somewhere else - if (_jetpack.IsUserFlying(uid)) + if (_config.GetCVar(CCVars.MoodEnabled) + || _jetpack.IsUserFlying(uid)) return; var mod = component.CurrentThirstThreshold <= ThirstThreshold.Parched ? 0.75f : 1.0f; @@ -109,8 +114,9 @@ private bool IsMovementThreshold(ThirstThreshold threshold) private void UpdateEffects(EntityUid uid, ThirstComponent component) { - if (IsMovementThreshold(component.LastThirstThreshold) != IsMovementThreshold(component.CurrentThirstThreshold) && - TryComp(uid, out MovementSpeedModifierComponent? movementSlowdownComponent)) + if (!_config.GetCVar(CCVars.MoodEnabled) + && IsMovementThreshold(component.LastThirstThreshold) != IsMovementThreshold(component.CurrentThirstThreshold) + && TryComp(uid, out MovementSpeedModifierComponent? movementSlowdownComponent)) { _movement.RefreshMovementSpeedModifiers(uid, movementSlowdownComponent); } @@ -125,6 +131,9 @@ private void UpdateEffects(EntityUid uid, ThirstComponent component) _alerts.ClearAlertCategory(uid, AlertCategory.Thirst); } + var ev = new MoodEffectEvent("Thirst" + component.CurrentThirstThreshold); + RaiseLocalEvent(uid, ev); + switch (component.CurrentThirstThreshold) { case ThirstThreshold.OverHydrated: diff --git a/Content.Shared/Overlays/SaturationScaleComponent.cs b/Content.Shared/Overlays/SaturationScaleComponent.cs new file mode 100644 index 00000000000..3318ddff6d4 --- /dev/null +++ b/Content.Shared/Overlays/SaturationScaleComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Overlays; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SaturationScaleOverlayComponent : Component { } diff --git a/Content.Shared/Slippery/SlipperySystem.cs b/Content.Shared/Slippery/SlipperySystem.cs index ff8b597a0d5..0b52cdde92e 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -7,6 +7,7 @@ using Content.Shared.StepTrigger.Systems; using Content.Shared.Stunnable; using Content.Shared.Throwing; +using Content.Shared.Mood; using JetBrains.Annotations; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; @@ -101,6 +102,8 @@ private void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other _stun.TryParalyze(other, TimeSpan.FromSeconds(component.ParalyzeTime), true); + RaiseLocalEvent(other, new MoodEffectEvent("MobSlipped")); + // Preventing from playing the slip sound when you are already knocked down. if (playSound) { diff --git a/Resources/Locale/en-US/mood/mood-alerts.ftl b/Resources/Locale/en-US/mood/mood-alerts.ftl new file mode 100644 index 00000000000..c5f76c5fb83 --- /dev/null +++ b/Resources/Locale/en-US/mood/mood-alerts.ftl @@ -0,0 +1,32 @@ +alerts-mood-dead-name = Dead +alerts-mood-dead-desc = Eternal emptiness has enveloped me, and the world no longer has power over my soul. + +alerts-mood-insane-name = Insane +alerts-mood-insane-desc = Darkness and hopelessness smolder in my soul, the world is doomed to absolute evil. + +alerts-mood-horrible-name = Horrible +alerts-mood-horrible-desc = I struggle with pain and fears, my fate is a series of torments and sufferings. + +alerts-mood-terrible-name = Terrible +alerts-mood-terrible-desc = My life has dried up like blood from a wound, and there is only darkness and despair all around. + +alerts-mood-bad-name = Bad +alerts-mood-bad-desc = My strength is leaving me, and every day becomes a difficult ordeal. + +alerts-mood-meh-name = Mediocre +alerts-mood-meh-desc = The world is full of dangers and pain, and my hopes are slowly dying. + +alerts-mood-neutral-name = Neutral +alerts-mood-neutral-desc = I continue on my way, despite threats and hardships, looking for the slightest light in the darkness. + +alerts-mood-good-name = Good +alerts-mood-good-desc = In this world of suffering, I find a little relief and hope. + +alerts-mood-great-name = Great +alerts-mood-great-desc = My strength is restored, and the world seems to be the lesser evil and pain. + +alerts-mood-exceptional-name = Exceptional +alerts-mood-exceptional-desc = Strength and hope fills me, despite the threats that lurk around me. + +alerts-mood-perfect-name = Perfect +alerts-mood-perfect-desc = My soul is full of light and power, and I am ready to fight the darkness in this cruel world. diff --git a/Resources/Locale/en-US/mood/mood.ftl b/Resources/Locale/en-US/mood/mood.ftl new file mode 100644 index 00000000000..c12ec7246ec --- /dev/null +++ b/Resources/Locale/en-US/mood/mood.ftl @@ -0,0 +1,54 @@ +mood-show-effects-start = [font size=12]Mood:[/font] + +mood-effect-HungerOverfed = I ate so much, I feel as though I'm about to burst! +mood-effect-HungerOkay = I am feeling full. +mood-effect-HungerPeckish = I could go for a snack right about now. +mood-effect-HungerStarving = I NEED FOOD! + +mood-effect-ThirstOverHydrated = I feel dizzy after drinking too much. +mood-effect-ThirstOkay = I'm feeling refreshed. +mood-effect-ThirstThirsty = My lips are a little dry. +mood-effect-ThirstParched = I NEED WATER! + +mood-effect-HealthNoDamage = I'm in no pain. +mood-effect-HealthLightDamage = It's just a scratch, but it hurts nonetheless +mood-effect-HealthSevereDamage = The pain is almost unbearable! +mood-effect-HealthHeavyDamage = Agony gnaws at my soul! + +mood-effect-Handcuffed = I am being held captive. + +mood-effect-Suffocating = I.. Can't.. Breathe... + +mood-effect-OnFire = IT BURNS!!! + +mood-effect-Creampied = I was baptized. It tastes like pie. + +mood-effect-MobSlipped = I slipped! I should be more careful next time. + +mood-effect-MobVomit = My lunch tasted awful coming back up. + +mood-effect-MobLowPressure = My whole body feels like it's going to burst! + +mood-effect-MobHighPressure = I feel as though I am being crushed on all sides! + +mood-effect-TraitSaturnine = Everything kind of sucks. I hate this job. + +mood-effect-Dead = You are dead. + +mood-effect-BeingHugged = Hugs are nice. + +mood-effect-ArcadePlay = I had fun playing an interesting arcade game. + +mood-effect-GotBlessed = I was blessed. + +mood-effect-PetAnimal = Animals are so cute, I can't stop petting them! + +mood-effect-SavedLife = It's so nice to save someone's life + +mood-effect-TraitorFocused = I have a goal, and I will accomplish it no matter what. + +mood-effect-RevolutionFocused = VIVA LA REVOLUTION!!! + +mood-effect-CultFocused = Dark Gods, grant me strength! + +mood-effect-TraitSanguine = I have nothing to worry about. I'm sure everything will turn out well in the end! \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 828afa76669..2db791538b6 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -86,6 +86,12 @@ trait-description-Foreigner = For one reason or another you do not speak this station's primary language. Instead, you have a translator issued to you that only you can use. +trait-name-Saturnine = Saturnine +trait-description-Saturnine = You are naturally dour and morose. Your mood is permanently decreased by a large amount. + +trait-name-Sanguine = Sanguine +trait-description-Sanguine = You are naturally upbeat and cheerful! Your mood is permanently increased by a large amount. + trait-name-WillToLive = Will To Live trait-description-WillToLive = You have an unusually strong "will to live", and will resist death more than others. diff --git a/Resources/Locale/ru-RU/mood/mood-alerts.ftl b/Resources/Locale/ru-RU/mood/mood-alerts.ftl new file mode 100644 index 00000000000..e96fb1f09f5 --- /dev/null +++ b/Resources/Locale/ru-RU/mood/mood-alerts.ftl @@ -0,0 +1,22 @@ +alerts-mood-insane-name = Безумие +alerts-mood-insane-desc = В моей душе тлеют мрак и безнадежность, мир обречен на абсолютное зло. +alerts-mood-horrible-name = Печально +alerts-mood-horrible-desc = Я борюсь с болями и страхами, моя судьба - череда мучений и страданий. +alerts-mood-terrible-name = Очень плохо +alerts-mood-terrible-desc = Моя жизнь иссякла, как кровь из раны, и вокруг лишь мрак и отчаяние. +alerts-mood-bad-name = Плохо +alerts-mood-bad-desc = Силы покидают меня, и каждый день становится тяжелым испытанием. +alerts-mood-meh-name = Нехорошо +alerts-mood-meh-desc = Мир полон угроз и боли, и мои надежды медленно умирают. +alerts-mood-neutral-name = Нормально +alerts-mood-neutral-desc = Я продолжаю свой путь, несмотря на угрозы и лишения, ища хоть малейший свет во мраке. +alerts-mood-good-name = Неплохо +alerts-mood-good-desc = В этом мире полном страданий, я обретаю небольшое облегчение и надежду. +alerts-mood-great-name = Хорошо +alerts-mood-great-desc = Моя сила восстанавливается, и мир кажется меньшим злом и болью. +alerts-mood-exceptional-name = Очень хорошо +alerts-mood-exceptional-desc = Я ощущаю в себе силы и надежду на лучшие дни, несмотря на угрозы, что таятся вокруг. +alerts-mood-perfect-name = Великолепно +alerts-mood-perfect-desc = Моя душа полна света и силы, и я готов сразиться с тьмой в этом жестоком мире. +alerts-mood-dead-name = Мёртв +alerts-mood-dead-desc = Вечная пустота окутала меня, и мир больше не имеет власти над моей душой. diff --git a/Resources/Locale/ru-RU/mood/mood.ftl b/Resources/Locale/ru-RU/mood/mood.ftl new file mode 100644 index 00000000000..b9619035ea2 --- /dev/null +++ b/Resources/Locale/ru-RU/mood/mood.ftl @@ -0,0 +1 @@ +mood-show-effects-start = [font size=12]Настроение:[/font] diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 00b799670f6..47015bf21f5 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -5,6 +5,7 @@ id: BaseAlertOrder order: - category: Health + - category: Mood - category: Stamina - alertType: SuitPower - category: Internals @@ -496,3 +497,114 @@ state: critical name: Debug6 description: Debug + +# Moods +- type: alert + id: Insane + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood_insane + name: alerts-mood-insane-name + description: alerts-mood-insane-desc + +- type: alert + id: Horrible + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood1 + name: alerts-mood-horrible-name + description: alerts-mood-horrible-desc + +- type: alert + id: Terrible + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood2 + name: alerts-mood-terrible-name + description: alerts-mood-terrible-desc + +- type: alert + id: Bad + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood3 + name: alerts-mood-bad-name + description: alerts-mood-bad-desc + +- type: alert + id: Meh + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood4 + name: alerts-mood-meh-name + description: alerts-mood-meh-desc + +- type: alert + id: Neutral + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood5 + name: alerts-mood-neutral-name + description: alerts-mood-neutral-desc + +- type: alert + id: Good + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood6 + name: alerts-mood-good-name + description: alerts-mood-good-desc + +- type: alert + id: Great + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood7 + name: alerts-mood-great-name + description: alerts-mood-great-desc + +- type: alert + id: Exceptional + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood8 + name: alerts-mood-exceptional-name + description: alerts-mood-exceptional-desc + +- type: alert + id: Perfect + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood9 + name: alerts-mood-perfect-name + description: alerts-mood-perfect-desc + +- type: alert + id: MoodDead + category: Mood + onClick: !type:ShowMoodEffects { } + icons: + - sprite: /Textures/Interface/Alerts/mood.rsi + state: mood_happiness_bad + name: alerts-mood-dead-name + description: alerts-mood-dead-desc diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index d812433b44a..940e3ebccb0 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -104,6 +104,7 @@ layer: - MobLayer - type: FloorOcclusion + - type: Mood - type: RangedDamageSound soundGroups: Brute: diff --git a/Resources/Prototypes/Mood/categories.yml b/Resources/Prototypes/Mood/categories.yml new file mode 100644 index 00000000000..8c03338ca8b --- /dev/null +++ b/Resources/Prototypes/Mood/categories.yml @@ -0,0 +1,9 @@ +# Alphabetically Ordered +- type: moodCategory + id: Health + +- type: moodCategory + id: Hunger + +- type: moodCategory + id: Thirst \ No newline at end of file diff --git a/Resources/Prototypes/Mood/genericNeeds.yml b/Resources/Prototypes/Mood/genericNeeds.yml new file mode 100644 index 00000000000..d0b24b7d7fe --- /dev/null +++ b/Resources/Prototypes/Mood/genericNeeds.yml @@ -0,0 +1,63 @@ +# Hunger +- type: moodEffect + id: HungerOverfed + moodChange: -10 + category: "Hunger" + +- type: moodEffect + id: HungerOkay + moodChange: 7 + category: "Hunger" + +- type: moodEffect + id: HungerPeckish + moodChange: -3 + category: "Hunger" + +- type: moodEffect + id: HungerStarving + moodChange: -7 + category: "Hunger" + +# Thirst +- type: moodEffect + id: ThirstOverHydrated + moodChange: -3 + category: "Thirst" + +- type: moodEffect + id: ThirstOkay + moodChange: 7 + category: "Thirst" + +- type: moodEffect + id: ThirstThirsty + moodChange: -3 + category: "Thirst" + +- type: moodEffect + id: ThirstParched + moodChange: -7 + category: "Thirst" + +# Health +- type: moodEffect + id: HealthNoDamage + moodChange: 0 + hidden: true + category: "Health" + +- type: moodEffect + id: HealthLightDamage + moodChange: -3 + category: "Health" + +- type: moodEffect + id: HealthSevereDamage + moodChange: -7 + category: "Health" + +- type: moodEffect + id: HealthHeavyDamage + moodChange: -20 + category: "Health" diff --git a/Resources/Prototypes/Mood/genericNegativeEffects.yml b/Resources/Prototypes/Mood/genericNegativeEffects.yml new file mode 100644 index 00000000000..0e1014d9074 --- /dev/null +++ b/Resources/Prototypes/Mood/genericNegativeEffects.yml @@ -0,0 +1,45 @@ +- type: moodEffect + id: Handcuffed + moodChange: -3 + +- type: moodEffect + id: Suffocating + moodChange: -7 + timeout: 6 + +- type: moodEffect + id: OnFire + moodChange: -10 + timeout: 600 + +- type: moodEffect + id: Creampied + moodChange: -3 + +- type: moodEffect + id: MobSlipped + moodChange: -3 + timeout: 180 + +- type: moodEffect + id: MobVomit + moodChange: -3 + timeout: 480 + +- type: moodEffect + id: MobLowPressure + moodChange: -7 + timeout: 10 + +- type: moodEffect + id: MobHighPressure + moodChange: -7 + timeout: 10 + +- type: moodEffect + id: TraitSaturnine + moodChange: -20 + +- type: moodEffect + id: Dead + moodChange: -1000 diff --git a/Resources/Prototypes/Mood/genericPositiveEffects.yml b/Resources/Prototypes/Mood/genericPositiveEffects.yml new file mode 100644 index 00000000000..8ac5b25dc12 --- /dev/null +++ b/Resources/Prototypes/Mood/genericPositiveEffects.yml @@ -0,0 +1,40 @@ +- type: moodEffect + id: BeingHugged + moodChange: 3 + timeout: 120 + +- type: moodEffect + id: ArcadePlay + moodChange: 3 + timeout: 480 + +- type: moodEffect + id: GotBlessed + moodChange: 3 + timeout: 480 + +- type: moodEffect + id: PetAnimal + moodChange: 3 + timeout: 300 + +- type: moodEffect + id: SavedLife + moodChange: 7 + timeout: 480 + +- type: moodEffect + id: TraitorFocused # Used for traitors to boost their goals completion. + moodChange: 7 + +- type: moodEffect + id: RevolutionFocused # Used for revolution + moodChange: 7 + +- type: moodEffect + id: CultFocused + moodChange: 10 + +- type: moodEffect + id: TraitSanguine + moodChange: 15 \ No newline at end of file diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index e286dcb7a41..b495490201c 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -99,3 +99,8 @@ id: Cataracts kind: source path: "/Textures/Shaders/cataracts.swsl" + +- type: shader + id: SaturationScale + kind: source + path: "/Textures/Shaders/saturationscale.swsl" diff --git a/Resources/Prototypes/Traits/neutral.yml b/Resources/Prototypes/Traits/neutral.yml index ab5bcb238d7..16e70a2cc63 100644 --- a/Resources/Prototypes/Traits/neutral.yml +++ b/Resources/Prototypes/Traits/neutral.yml @@ -38,3 +38,39 @@ - Vulpkanin components: - type: NormalVision + +- type: trait + id: Saturnine + category: Mental + points: 3 + requirements: + - !type:CharacterJobRequirement + inverted: true + jobs: + - Borg + - MedicalBorg + - !type:CharacterTraitRequirement + inverted: true + traits: + - Sanguine + components: + - type: MoodModifyTrait + moodId: TraitSaturnine + +- type: trait + id: Sanguine + category: Mental + points: -3 + requirements: + - !type:CharacterJobRequirement + inverted: true + jobs: + - Borg + - MedicalBorg + - !type:CharacterTraitRequirement + inverted: true + traits: + - Saturnine + components: + - type: MoodModifyTrait + moodId: TraitSanguine \ No newline at end of file diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/meta.json b/Resources/Textures/Interface/Alerts/mood.rsi/meta.json new file mode 100644 index 00000000000..0f6726a48d0 --- /dev/null +++ b/Resources/Textures/Interface/Alerts/mood.rsi/meta.json @@ -0,0 +1,60 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from NSV13 at b6b1e2bf2cc60455851317d8e82cca8716d9dac1", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "mood1" + }, + { + "name": "mood2" + }, + { + "name": "mood3" + }, + { + "name": "mood4" + }, + { + "name": "mood5" + }, + { + "name": "mood6" + }, + { + "name": "mood7" + }, + { + "name": "mood8" + }, + { + "name": "mood9" + }, + { + "name": "mood_happiness_bad" + }, + { + "name": "mood_happiness_good" + }, + { + "name": "mood_insane", + "delays": [ + [ + 0.07, + 0.07, + 0.07, + 0.07, + 0.07, + 0.07, + 0.07, + 0.07, + 0.07 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood1.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood1.png new file mode 100644 index 00000000000..ae1e1386db4 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood1.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood2.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood2.png new file mode 100644 index 00000000000..41be928f025 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood2.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood3.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood3.png new file mode 100644 index 00000000000..179991e1984 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood3.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood4.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood4.png new file mode 100644 index 00000000000..4ea7d701171 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood4.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood5.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood5.png new file mode 100644 index 00000000000..c4c3370f625 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood5.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood6.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood6.png new file mode 100644 index 00000000000..388483e1438 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood6.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood7.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood7.png new file mode 100644 index 00000000000..b4f944d045e Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood7.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood8.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood8.png new file mode 100644 index 00000000000..f12f71b7ffa Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood8.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood9.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood9.png new file mode 100644 index 00000000000..e65c6501495 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood9.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood_happiness_bad.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood_happiness_bad.png new file mode 100644 index 00000000000..4ed8f4d68f8 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood_happiness_bad.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood_happiness_good.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood_happiness_good.png new file mode 100644 index 00000000000..eb9943a3015 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood_happiness_good.png differ diff --git a/Resources/Textures/Interface/Alerts/mood.rsi/mood_insane.png b/Resources/Textures/Interface/Alerts/mood.rsi/mood_insane.png new file mode 100644 index 00000000000..b2407bbdad8 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/mood.rsi/mood_insane.png differ diff --git a/Resources/Textures/Shaders/saturationscale.swsl b/Resources/Textures/Shaders/saturationscale.swsl new file mode 100644 index 00000000000..9829e207629 --- /dev/null +++ b/Resources/Textures/Shaders/saturationscale.swsl @@ -0,0 +1,12 @@ +uniform highp float saturation; // Between 0 and 2; +uniform sampler2D SCREEN_TEXTURE; + +void fragment() { + highp vec4 color = texture(SCREEN_TEXTURE, UV); + + highp float brightness = (color.r + color.g + color.b) / 3.0; + + color.rgb = mix(vec3(brightness), color.rgb, saturation); + + COLOR = color; +}