Skip to content
This repository has been archived by the owner on Sep 14, 2024. It is now read-only.

Commit

Permalink
Morale System (Port From White Dream) (#620)
Browse files Browse the repository at this point in the history
This Feature has been graciously provided for Einstein Engines to port
from the White Dream codebase.

Mood is a system for tracking a character's current Mental State, which
fluctuates throughout the round as a result of various events that can
modify it. Each consisting of a single line event that can be trivially
inserted into any other system, and a yml configured "Moodlet", which is
applied to said character. Moodlets can be temporary or permanent, and
can also modify a characters mood in either positive or negative
directions. Things like, "Being Hungry", "Being Injured", "Petting a
cute animal", "Being Hugged", all create a Moodlet.

Mood can provide buffs or debuffs, primarily to movement speed. In fact
Mood's movement speed modifier actually completely replaces the movement
speed modifiers from Hunger & Thirst. Instead Hunger & Thirst create a
negative moodlet that persists until you eat and drink, which _can_ give
you a speed penalty. But you might for instance diminish the negative
effects by seeking out other positive sources. Or they might just get
worse, who knows what could happen?

Mood takes the form of a series of Moodlets, which modify your
character's internal Mood stat. It's kinda like a healthbar, but for
your mental state. Whenever you gain a moodlet, it appears in a popup.
White text for standard moodlets, red text for negative moodlets. By
clicking on your mood icon, text will show up displaying all of your
currently active Moodlets.

https://github.com/user-attachments/assets/3e9420bb-3a43-4d97-9127-31d704c15287

New traits!

![image](https://github.com/user-attachments/assets/4ddf968e-3dbd-44e1-a53e-79bb7b955d01)

Permission from Codeowners:
![morale code
permission](https://github.com/user-attachments/assets/c3d089fa-3e0f-4402-8757-c47e911c3554)

- [x] Refactor the Crit Threshold modification, and Movement Speed
Modification to make it more granular.

:cl: VMSolidus & Skubman
- add: The Mood System has been ported from White Dream. Mood acts as a
3rd healthbar, alongside Health and Stamina, representing your
character's current mental state. Having either high or low mood can
modify certain physical attributes.
- add: Mood modifies your Critical Threshold. Your critical threshold
can be increased or decreased depending on how high or low your
character's mood is.
- add: Mood modifies your Movement Speed. Characters move faster when
they have an overall high mood, and move slower when they have a lower
mood.
- add: Saturnine and Sanguine have been added to the list of Mental
traits, both providing innate modifiers to a character's Morale.

---------

Signed-off-by: VMSolidus <[email protected]>
Co-authored-by: Danger Revolution! <[email protected]>
Co-authored-by: Angelo Fallaria <[email protected]>
Co-authored-by: DEATHB4DEFEAT <[email protected]>
  • Loading branch information
4 people authored and PuroSlavKing committed Aug 31, 2024
1 parent 2f056a4 commit 47e9861
Show file tree
Hide file tree
Showing 54 changed files with 1,838 additions and 15 deletions.
39 changes: 39 additions & 0 deletions Content.Client/Overlays/SaturationScaleOverlay.cs
Original file line number Diff line number Diff line change
@@ -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<ShaderPrototype>("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);
}
}
63 changes: 63 additions & 0 deletions Content.Client/Overlays/SaturationScaleSystem.cs
Original file line number Diff line number Diff line change
@@ -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<SaturationScaleOverlayComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SaturationScaleOverlayComponent, ComponentShutdown>(OnShutdown);

SubscribeLocalEvent<SaturationScaleOverlayComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<SaturationScaleOverlayComponent, PlayerDetachedEvent>(OnPlayerDetached);

SubscribeNetworkEvent<RoundRestartCleanupEvent>(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);
}
}
4 changes: 2 additions & 2 deletions Content.IntegrationTests/Tests/Movement/SlippingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public async Task BananaSlipTest()
var sys = SEntMan.System<SlipTestSystem>();
await SpawnTarget("TrashBananaPeel");

var modifier = Comp<MovementSpeedModifierComponent>(Player).SprintSpeedModifier;
Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed.");
// var modifier = Comp<MovementSpeedModifierComponent>(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.
Assert.That(Delta(), Is.GreaterThan(0.5f));
Expand Down
3 changes: 3 additions & 0 deletions Content.Server/Arcade/BlockGame/BlockGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Mood;

namespace Content.Server.Arcade.BlockGame;

Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Shared.Power;
using Content.Shared.Mood;
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
Expand Down Expand Up @@ -77,6 +78,9 @@ private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent compone
if (!TryComp<ApcPowerReceiverComponent>(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:
Expand Down
3 changes: 3 additions & 0 deletions Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -246,13 +247,15 @@ public override void Update(float frameTime)
}

_alertsSystem.ShowAlert(uid, barotrauma.LowPressureAlert, 2);
RaiseLocalEvent(uid, new MoodEffectEvent("MobLowPressure"));
}
else if (pressure >= Atmospherics.HazardHighPressure)
{
var damageScale = MathF.Min(((pressure / Atmospherics.HazardHighPressure) - 1) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);

// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear.
_damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false);
RaiseLocalEvent(uid, new MoodEffectEvent("MobHighPressure"));

if (!barotrauma.TakingDamage)
{
Expand Down
8 changes: 5 additions & 3 deletions Content.Server/Atmos/EntitySystems/FlammableSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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;
Expand Down Expand Up @@ -425,12 +426,13 @@ public override void Update(float frameTime)

if (!flammable.OnFire)
{
_alertsSystem.ClearAlert(uid, flammable.FireAlert);
_alertsSystem.ClearAlert(uid, AlertType.Fire);
RaiseLocalEvent(uid, new MoodRemoveEffectEvent("OnFire"));
continue;
}

_alertsSystem.ShowAlert(uid, flammable.FireAlert);

_alertsSystem.ShowAlert(uid, AlertType.Fire);
RaiseLocalEvent(uid, new MoodEffectEvent("OnFire"));
if (flammable.FireStacks > 0)
{
var air = _atmosphereSystem.GetContainingMixture(uid);
Expand Down
3 changes: 3 additions & 0 deletions Content.Server/Bible/BibleSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<AlternativeVerb> args)
Expand Down
2 changes: 2 additions & 0 deletions Content.Server/Body/Systems/RespiratorSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Mobs.Systems;
using Content.Shared.Mood;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
Expand Down Expand Up @@ -292,6 +293,7 @@ private void TakeSuffocationDamage(Entity<RespiratorComponent> ent)
{
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
RaiseLocalEvent(ent, new MoodEffectEvent("Suffocating"));
}

_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
Expand Down
23 changes: 23 additions & 0 deletions Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Robust.Shared.Random;
using System.Linq;
using System.Text;
using Content.Shared.Mood;

namespace Content.Server.GameTicking.Rules;

Expand Down Expand Up @@ -126,6 +127,28 @@ 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)
{
var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
var difficulty = 0f;
Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
if (objective == null)
continue;

_mindSystem.AddObjective(mindId, mind, objective.Value);
var adding = Comp<ObjectiveComponent>(objective.Value).Difficulty;
difficulty += adding;
Log.Debug($"Added objective {ToPrettyString(objective):objective} with {adding} difficulty");
}
}

return true;
}

Expand Down
3 changes: 3 additions & 0 deletions Content.Server/Medical/VomitSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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;

Expand Down Expand Up @@ -96,6 +97,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"));
}
}
}
111 changes: 111 additions & 0 deletions Content.Server/Mood/MoodComponent.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> CategorisedEffects = new();

[ViewVariables(VVAccess.ReadOnly)]
public readonly Dictionary<string, float> UncategorisedEffects = new();

/// <summary>
/// The formula for the movement speed modifier is SpeedBonusGrowth ^ (MoodLevel - MoodThreshold.Neutral).
/// Change this ONLY BY 0.001 AT A TIME.
/// </summary>
[DataField]
public float SpeedBonusGrowth = 1.003f;

/// <summary>
/// The lowest point that low morale can multiply our movement speed by. Lowering speed follows a linear curve, rather than geometric.
/// </summary>
[DataField]
public float MinimumSpeedModifier = 0.75f;

/// <summary>
/// The maximum amount that high morale can multiply our movement speed by. This follows a significantly slower geometric sequence.
/// </summary>
[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<MoodThreshold, float>))]
public Dictionary<MoodThreshold, float> 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<MoodThreshold, AlertType>))]
public Dictionary<MoodThreshold, AlertType> 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 }
};

/// <summary>
/// These thresholds represent a percentage of Crit-Threshold, 0.8 corresponding with 80%.
/// </summary>
[DataField(customTypeSerializer: typeof(DictionarySerializer<string, float>))]
public Dictionary<string, float> 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
}
Loading

0 comments on commit 47e9861

Please sign in to comment.