using Content.Shared.Alert;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.Prototypes;
using Content.Shared.Rejuvenate;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Nutrition.EntitySystems;
///
/// This system manages . It handles the change of satiations in
/// and external changes to satiations through accessors like .
///
public abstract partial class SatiationSystem : EntitySystem
{
[Dependency] private IGameTiming _timing = default!;
[Dependency] private AlertsSystem _alerts = default!;
///
/// The ID of the Hunger satiation type. Provided because it is so commonly used in Content.
///
public static readonly ProtoId Hunger = "Hunger";
///
/// The ID of the Thirst satiation type. Provided because it is so commonly used in Content.
///
public static readonly ProtoId Thirst = "Thirst";
///
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var component))
{
var entity = new Entity(uid, component);
foreach (var satiation in component.Satiations.Values)
{
if (!ProtoMan.Resolve(satiation.Prototype, out var proto))
continue;
if (_timing.CurTime >= satiation.NextAlertUpdateTime)
{
UpdateAlerts(entity, satiation, proto);
}
if (_timing.CurTime >= satiation.NextChangeRateModUpdateTime)
{
SetAuthoritativeValue(entity, satiation, proto, CalculateCurrentValue(satiation, proto));
}
}
}
}
///
/// Sets starting satiation values.
///
[SubscribeLocalEvent]
private void OnMapInit(Entity entity, ref MapInitEvent args)
{
foreach (var (type, satiation) in entity.Comp.Satiations)
{
AddSatiation(entity.AsNullable(), type, satiation);
}
}
///
/// Clears alerts.
///
[SubscribeLocalEvent]
private void OnShutdown(Entity entity, ref ComponentShutdown args)
{
foreach (var satiation in entity.Comp.Satiations.Values)
{
if (!ProtoMan.Resolve(satiation.Prototype, out var proto))
continue;
_alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory);
}
}
///
/// Sets all satiations to their maximums.
///
[SubscribeLocalEvent]
private void OnRejuvenate(Entity entity, ref RejuvenateEvent args)
{
foreach (var type in entity.Comp.Satiations.Keys)
{
SetValue(entity, type, satiationValue: int.MaxValue);
}
}
///
/// This is basically a special-case reimplementation of .
///
[SubscribeLocalEvent]
private void UpdateAlertsOnSatiationUpdated(Entity entity, ref SatiationUpdateEvent args)
{
if (entity.Comp.GetOrNull(args.Type) is not { } satiation ||
!ProtoMan.Resolve(satiation.Prototype, out var proto))
return;
UpdateAlerts(entity, satiation, proto);
}
///
/// This helper resolves and returns the corresponding from
/// along with its .
/// Returns null if the prototype fails to resolve, or if the component does not have the specified satiation.
///
private (Satiation Satiation, SatiationPrototype Proto)? GetAndResolveSatiationOfType(
SatiationComponent satiations,
[ForbidLiteral] ProtoId type
)
{
if (satiations.GetOrNull(type) is not { } satiation ||
!ProtoMan.Resolve(satiation.Prototype, out var proto))
return null;
return (satiation, proto);
}
///
/// Calculates the current value of the given by linearly extrapolating the change of the
/// value based on ,
/// and
///
private float CalculateCurrentValue(Satiation satiation, SatiationPrototype proto)
{
var dt = _timing.CurTime - satiation.LastAuthoritativeChangeTime;
var value = satiation.LastAuthoritativeValue + (float)dt.TotalSeconds * satiation.ActualChangeRate;
return proto.ClampSatiationWithinThresholds(value);
}
///
/// Calculates when 's value will reach either or
/// , or null if neither will happen based on the current expected linear
/// evolution of the satiation value. A null bound is treated as unreachable, so if both bounds are null, this
/// this function returns null.
///
///
private TimeSpan? EvolvesToBoundAt(
Satiation satiation,
SatiationPrototype proto,
int? upperBound,
int? lowerBound
) => satiation.ActualChangeRate switch
{
> 0 when upperBound is { } t => EvolvesToTargetAt(satiation, proto, t),
< 0 when lowerBound is { } t => EvolvesToTargetAt(satiation, proto, t),
// Change rate is zero or there's no threshold to decay/grow into: we'll never change without outside modification
_ => null,
};
///
/// Calculates when 's value will reach , or null if
/// the current linear evolution will not reach that value.
///
///
private TimeSpan? EvolvesToTargetAt(Satiation satiation, SatiationPrototype proto, int target)
{
var seconds = (target - CalculateCurrentValue(satiation, proto)) / satiation.ActualChangeRate;
if (!seconds.IsValid() || seconds < 0f)
return null;
return _timing.CurTime + TimeSpan.FromSeconds(seconds);
}
///
/// The beating heart of this system, this function sets the given 's
/// to . This involves
/// updating obvious fields on the , but since changes to the value also affect the
/// current threshold, we need to consider all of the effects that has as well.
///
private void SetAuthoritativeValue(
Entity entity,
Satiation satiation,
SatiationPrototype proto,
float value
)
{
// Update the authoritative value itself.
satiation.LastAuthoritativeChangeTime = _timing.CurTime;
satiation.LastAuthoritativeValue = proto.ClampSatiationWithinThresholds(value);
if (!TryGetValueByThreshold(
entity,
satiation.SatiationType,
proto.ChangeModifiers,
out var currentChangeMod,
out var nextHigherThreshold,
out var nextLowerThreshold
))
{
currentChangeMod = 1f;
}
satiation.ActualChangeRate = proto.BaseChangeRate * currentChangeMod;
satiation.NextChangeRateModUpdateTime = EvolvesToBoundAt(
satiation,
proto,
nextHigherThreshold,
nextLowerThreshold
);
var updateEvent = new SatiationUpdateEvent(satiation.SatiationType);
RaiseLocalEvent(entity, ref updateEvent);
DirtyField(entity.AsNullable(), SatiationComponent.SatiationFieldName);
}
private void UpdateAlerts(
Entity entity,
Satiation satiation,
SatiationPrototype proto
)
{
TryGetValueByThreshold(
entity,
satiation.SatiationType,
proto.Alerts,
out var result,
out var nextHigherThreshold,
out var nextLowerThreshold
);
if (result is { } alert)
{
_alerts.ShowAlert(entity.Owner, alert);
}
else
{
_alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory);
}
satiation.NextAlertUpdateTime = EvolvesToBoundAt(
satiation,
proto,
nextHigherThreshold,
nextLowerThreshold
);
DirtyField(entity.AsNullable(), SatiationComponent.SatiationFieldName);
}
}
///
/// This event is raised on entities with when their satiation of
/// is directly set or when the rate of change to that satiation is changed.
///
/// This event may be raised even when no change has occurred.
[ByRefEvent]
public readonly record struct SatiationUpdateEvent(ProtoId Type);