Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPR Remake #487

Merged
merged 18 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 2 additions & 105 deletions Content.Server/Atmos/Rotting/RottingSystem.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
using Content.Shared.Damage;
using Content.Shared.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Rejuvenate;
using Content.Shared.Damage;
using Robust.Server.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
Expand All @@ -22,83 +16,16 @@ public sealed class RottingSystem : SharedRottingSystem
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<PerishableComponent, MapInitEvent>(OnPerishableMapInit);
SubscribeLocalEvent<PerishableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PerishableComponent, ExaminedEvent>(OnPerishableExamined);

SubscribeLocalEvent<RottingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<RottingComponent, MobStateChangedEvent>(OnRottingMobStateChanged);
SubscribeLocalEvent<RottingComponent, BeingGibbedEvent>(OnGibbed);
SubscribeLocalEvent<RottingComponent, RejuvenateEvent>(OnRejuvenate);

SubscribeLocalEvent<TemperatureComponent, IsRottingEvent>(OnTempIsRotting);
}

private void OnPerishableMapInit(EntityUid uid, PerishableComponent component, MapInitEvent args)
{
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

private void OnMobStateChanged(EntityUid uid, PerishableComponent component, MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead && args.OldMobState != MobState.Dead)
return;

if (HasComp<RottingComponent>(uid))
return;

component.RotAccumulator = TimeSpan.Zero;
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

private void OnShutdown(EntityUid uid, RottingComponent component, ComponentShutdown args)
{
if (TryComp<PerishableComponent>(uid, out var perishable))
{
perishable.RotNextUpdate = TimeSpan.Zero;
}
}

private void OnRottingMobStateChanged(EntityUid uid, RottingComponent component, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
return;
RemCompDeferred(uid, component);
}

public bool IsRotProgressing(EntityUid uid, PerishableComponent? perishable)
{
// things don't perish by default.
if (!Resolve(uid, ref perishable, false))
return false;

// only dead things or inanimate objects can rot
if (TryComp<MobStateComponent>(uid, out var mobState) && !_mobState.IsDead(uid, mobState))
return false;

if (_container.TryGetOuterContainer(uid, Transform(uid), out var container) &&
HasComp<AntiRottingContainerComponent>(container.Owner))
{
return false;
}

var ev = new IsRottingEvent();
RaiseLocalEvent(uid, ref ev);

return !ev.Handled;
}

public bool IsRotten(EntityUid uid, RottingComponent? rotting = null)
{
return Resolve(uid, ref rotting, false);
}

private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEvent args)
{
if (!TryComp<PhysicsComponent>(uid, out var physics))
Expand All @@ -112,36 +39,6 @@ private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEven
tileMix?.AdjustMoles(Gas.Ammonia, molsToDump);
}

private void OnPerishableExamined(Entity<PerishableComponent> perishable, ref ExaminedEvent args)
{
int stage = PerishStage(perishable, MaxStages);
if (stage < 1 || stage > MaxStages)
{
// We dont push an examined string if it hasen't started "perishing" or it's already rotting
return;
}

var isMob = HasComp<MobStateComponent>(perishable);
var description = "perishable-" + stage + (!isMob ? "-nonmob" : string.Empty);
args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(perishable, EntityManager))));
}

/// <summary>
/// Return an integer from 0 to maxStage representing how close to rotting an entity is. Used to
/// generate examine messages for items that are starting to rot.
/// </summary>
public int PerishStage(Entity<PerishableComponent> perishable, int maxStages)
{
if (perishable.Comp.RotAfter.TotalSeconds == 0 || perishable.Comp.RotAccumulator.TotalSeconds == 0)
return 0;
return (int)(1 + maxStages * perishable.Comp.RotAccumulator.TotalSeconds / perishable.Comp.RotAfter.TotalSeconds);
}

private void OnRejuvenate(EntityUid uid, RottingComponent component, RejuvenateEvent args)
{
RemCompDeferred<RottingComponent>(uid);
}

private void OnTempIsRotting(EntityUid uid, TemperatureComponent component, ref IsRottingEvent args)
{
if (args.Handled)
Expand Down
141 changes: 134 additions & 7 deletions Content.Shared/Atmos/Rotting/SharedRottingSystem.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,85 @@
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Rejuvenate;
using Robust.Shared.Containers;
using Robust.Shared.Timing;

namespace Content.Shared.Atmos.Rotting;

public abstract class SharedRottingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;

public const int MaxStages = 3;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<PerishableComponent, MapInitEvent>(OnPerishableMapInit);
SubscribeLocalEvent<PerishableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PerishableComponent, ExaminedEvent>(OnPerishableExamined);

SubscribeLocalEvent<RottingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<RottingComponent, MobStateChangedEvent>(OnRottingMobStateChanged);
SubscribeLocalEvent<RottingComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<RottingComponent, ExaminedEvent>(OnExamined);
}

/// <summary>
/// Return the rot stage, usually from 0 to 2 inclusive.
/// </summary>
public int RotStage(EntityUid uid, RottingComponent? comp = null, PerishableComponent? perishable = null)
private void OnPerishableMapInit(EntityUid uid, PerishableComponent component, MapInitEvent args)
{
if (!Resolve(uid, ref comp, ref perishable))
return 0;
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

return (int) (comp.TotalRotTime.TotalSeconds / perishable.RotAfter.TotalSeconds);
private void OnMobStateChanged(EntityUid uid, PerishableComponent component, MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead && args.OldMobState != MobState.Dead)
return;

if (HasComp<RottingComponent>(uid))
return;

component.RotAccumulator = TimeSpan.Zero;
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

private void OnPerishableExamined(Entity<PerishableComponent> perishable, ref ExaminedEvent args)
{
int stage = PerishStage(perishable, MaxStages);
if (stage < 1 || stage > MaxStages)
{
// We dont push an examined string if it hasen't started "perishing" or it's already rotting
return;
}

var isMob = HasComp<MobStateComponent>(perishable);
var description = "perishable-" + stage + (!isMob ? "-nonmob" : string.Empty);
args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(perishable, EntityManager))));
}

private void OnShutdown(EntityUid uid, RottingComponent component, ComponentShutdown args)
{
if (TryComp<PerishableComponent>(uid, out var perishable))
{
perishable.RotNextUpdate = TimeSpan.Zero;
}
}

private void OnRottingMobStateChanged(EntityUid uid, RottingComponent component, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
return;
RemCompDeferred(uid, component);
}

private void OnRejuvenate(EntityUid uid, RottingComponent component, RejuvenateEvent args)
{
RemCompDeferred<RottingComponent>(uid);
}

private void OnExamined(EntityUid uid, RottingComponent component, ExaminedEvent args)
Expand All @@ -41,4 +97,75 @@ private void OnExamined(EntityUid uid, RottingComponent component, ExaminedEvent

args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(uid, EntityManager))));
}

/// <summary>
/// Return an integer from 0 to maxStage representing how close to rotting an entity is. Used to
/// generate examine messages for items that are starting to rot.
/// </summary>
public int PerishStage(Entity<PerishableComponent> perishable, int maxStages)
{
if (perishable.Comp.RotAfter.TotalSeconds == 0 || perishable.Comp.RotAccumulator.TotalSeconds == 0)
return 0;
return (int)(1 + maxStages * perishable.Comp.RotAccumulator.TotalSeconds / perishable.Comp.RotAfter.TotalSeconds);
}

public bool IsRotProgressing(EntityUid uid, PerishableComponent? perishable)
{
// things don't perish by default.
if (!Resolve(uid, ref perishable, false))
return false;

// only dead things or inanimate objects can rot
if (TryComp<MobStateComponent>(uid, out var mobState) && !_mobState.IsDead(uid, mobState))
return false;

if (_container.TryGetOuterContainer(uid, Transform(uid), out var container) &&
HasComp<AntiRottingContainerComponent>(container.Owner))
{
return false;
}

var ev = new IsRottingEvent();
RaiseLocalEvent(uid, ref ev);

return !ev.Handled;
}

public bool IsRotten(EntityUid uid, RottingComponent? rotting = null)
{
return Resolve(uid, ref rotting, false);
}

public void ReduceAccumulator(EntityUid uid, TimeSpan time)
{
if (!TryComp<PerishableComponent>(uid, out var perishable))
return;

if (!TryComp<RottingComponent>(uid, out var rotting))
{
perishable.RotAccumulator -= time;
return;
}
var total = (rotting.TotalRotTime + perishable.RotAccumulator) - time;

if (total < perishable.RotAfter)
{
RemCompDeferred(uid, rotting);
perishable.RotAccumulator = total;
}

else
rotting.TotalRotTime = total - perishable.RotAfter;
}

/// <summary>
/// Return the rot stage, usually from 0 to 2 inclusive.
/// </summary>
public int RotStage(EntityUid uid, RottingComponent? comp = null, PerishableComponent? perishable = null)
{
if (!Resolve(uid, ref comp, ref perishable))
return 0;

return (int) (comp.TotalRotTime.TotalSeconds / perishable.RotAfter.TotalSeconds);
}
}
49 changes: 49 additions & 0 deletions Content.Shared/CCVar/CCVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2291,6 +2291,55 @@ public static readonly CVarDef<float>
/// </summary>
public static readonly CVarDef<float> StationGoalsChance =
CVarDef.Create("game.station_goals_chance", 0.1f, CVar.SERVERONLY);


#region CPR System
/// <summary>
/// Controls whether the entire CPR system runs. When false, nobody can perform CPR. You should probably remove the trait too
/// if you are wishing to permanently disable the system on your server.
/// </summary>
public static readonly CVarDef<bool> EnableCPR =
CVarDef.Create("cpr.enable", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// Toggles whether or not CPR reduces rot timers(As an abstraction of delaying brain death, the IRL actual purpose of CPR)
/// </summary>
public static readonly CVarDef<bool> CPRReducesRot =
CVarDef.Create("cpr.reduces_rot", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// Toggles whether or not CPR heals airloss, included for completeness sake. I'm not going to stop you if your intention is to make CPR do nothing.
/// I guess it might be funny to troll your players with? I won't judge.
/// </summary>
public static readonly CVarDef<bool> CPRHealsAirloss =
CVarDef.Create("cpr.heals_airloss", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// The chance for a patient to be resuscitated when CPR is successfully performed.
/// Setting this above 0 isn't very realistic, but people who see CPR in movies and TV will expect CPR to work this way.
/// </summary>
public static readonly CVarDef<float> CPRResuscitationChance =
CVarDef.Create("cpr.resuscitation_chance", 0.05f, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// By default, CPR reduces rot timers by an amount of seconds equal to the time spent performing CPR. This is an optional multiplier that can increase or decrease the amount
/// of rot reduction. Set it to 2 for if you want 3 seconds of CPR to reduce 6 seconds of rot.
/// </summary>
/// <remarks>
/// If you're wondering why there isn't a CVar for setting the duration of the doafter, that's because it's not actually possible to have a timespan in cvar form
/// Curiously, it's also not possible for **shared** systems to set variable timespans. Which is where this system lives.
/// </remarks>
public static readonly CVarDef<float> CPRRotReductionMultiplier =
CVarDef.Create("cpr.rot_reduction_multiplier", 1f, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// By default, CPR heals airloss by 1 point for every second spent performing CPR. Just like above, this directly multiplies the healing amount.
/// Set it to 2 to get 6 points of airloss healing for every 3 seconds of CPR.
/// </summary>
public static readonly CVarDef<float> CPRAirlossReductionMultiplier =
CVarDef.Create("cpr.airloss_reduction_multiplier", 1f, CVar.REPLICATED | CVar.SERVER);

#endregion

#region Contests System

Expand Down
33 changes: 33 additions & 0 deletions Content.Shared/Medical/CPR/Components/CPRTrainingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Robust.Shared.GameStates;
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.Serialization;

namespace Content.Shared.Medical.CPR
{
[RegisterComponent, NetworkedComponent]
public sealed partial class CPRTrainingComponent : Component
{
[DataField]
public SoundSpecifier CPRSound = new SoundPathSpecifier("/Audio/Effects/CPR.ogg");

/// <summary>
/// How long the doafter for CPR takes
/// </summary>
[DataField]
public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3);

[DataField]
public int AirlossHeal = 6;

[DataField]
public float CrackRibsModifier = 1f;
public EntityUid? CPRPlayingStream;
}

[Serializable, NetSerializable]
public sealed partial class CPRDoAfterEvent : SimpleDoAfterEvent
{

}
}
Loading
Loading