using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Components; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Mobs.Systems; public sealed class MobThresholdSystem : EntitySystem { [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly AlertsSystem _alerts = default!; public override void Initialize() { SubscribeLocalEvent(MobThresholdMapInit); SubscribeLocalEvent(MobThresholdShutdown); SubscribeLocalEvent(OnDamaged); SubscribeLocalEvent(OnGetComponentState); SubscribeLocalEvent(OnHandleComponentState); SubscribeLocalEvent(OnUpdateMobState); } #region Public API /// /// Get the Damage Threshold for the appropriate state if it exists /// /// Target Entity /// MobState we want the Damage Threshold of /// Threshold Component Owned by the target /// the threshold or 0 if it doesn't exist public FixedPoint2 GetThresholdForState(EntityUid target, MobState mobState, MobThresholdsComponent? thresholdComponent = null) { if (!Resolve(target, ref thresholdComponent)) return FixedPoint2.Zero; foreach (var pair in thresholdComponent.Thresholds) { if (pair.Value == mobState) { return pair.Key; } } return FixedPoint2.Zero; } /// /// Try to get the Damage Threshold for the appropriate state if it exists /// /// Target Entity /// MobState we want the Damage Threshold of /// The damage Threshold for the given state /// Threshold Component Owned by the target /// true if successfully retrieved a threshold public bool TryGetThresholdForState(EntityUid target, MobState mobState, [NotNullWhen(true)] out FixedPoint2? threshold, MobThresholdsComponent? thresholdComponent = null) { threshold = null; if (!Resolve(target, ref thresholdComponent)) return false; foreach (var pair in thresholdComponent.Thresholds) { if (pair.Value == mobState) { threshold = pair.Key; return true; } } return false; } /// /// Try to get the a percentage of the Damage Threshold for the appropriate state if it exists /// /// Target Entity /// MobState we want the Damage Threshold of /// The Damage being applied /// Percentage of Damage compared to the Threshold /// Threshold Component Owned by the target /// true if successfully retrieved a percentage public bool TryGetPercentageForState(EntityUid target, MobState mobState, FixedPoint2 damage, [NotNullWhen(true)] out FixedPoint2? percentage, MobThresholdsComponent? thresholdComponent = null) { percentage = null; if (!TryGetThresholdForState(target, mobState, out var threshold, thresholdComponent)) return false; percentage = damage / threshold; return true; } /// /// Try to get the Damage Threshold for crit or death. Outputs the first found threshold. /// /// Target Entity /// The Damage Threshold for incapacitation /// Threshold Component owned by the target /// true if successfully retrieved incapacitation threshold public bool TryGetIncapThreshold(EntityUid target, [NotNullWhen(true)] out FixedPoint2? threshold, MobThresholdsComponent? thresholdComponent = null) { threshold = null; if (!Resolve(target, ref thresholdComponent)) return false; return TryGetThresholdForState(target, MobState.Critical, out threshold, thresholdComponent) || TryGetThresholdForState(target, MobState.Dead, out threshold, thresholdComponent); } /// /// Try to get a percentage of the Damage Threshold for crit or death. Outputs the first found percentage. /// /// Target Entity /// The damage being applied /// Percentage of Damage compared to the Incapacitation Threshold /// Threshold Component Owned by the target /// true if successfully retrieved incapacitation percentage public bool TryGetIncapPercentage(EntityUid target, FixedPoint2 damage, [NotNullWhen(true)] out FixedPoint2? percentage, MobThresholdsComponent? thresholdComponent = null) { percentage = null; if (!TryGetIncapThreshold(target, out var threshold, thresholdComponent)) return false; if (damage == 0) { percentage = 0; return true; } percentage = FixedPoint2.Min(1.0f, damage / threshold.Value); return true; } /// /// Try to get the Damage Threshold for death /// /// Target Entity /// The Damage Threshold for death /// Threshold Component owned by the target /// true if successfully retrieved incapacitation threshold public bool TryGetDeadThreshold(EntityUid target, [NotNullWhen(true)] out FixedPoint2? threshold, MobThresholdsComponent? thresholdComponent = null) { threshold = null; if (!Resolve(target, ref thresholdComponent)) return false; return TryGetThresholdForState(target, MobState.Dead, out threshold, thresholdComponent); } /// /// Try to get a percentage of the Damage Threshold for death /// /// Target Entity /// The damage being applied /// Percentage of Damage compared to the Death Threshold /// Threshold Component Owned by the target /// true if successfully retrieved death percentage public bool TryGetDeadPercentage(EntityUid target, FixedPoint2 damage, [NotNullWhen(true)] out FixedPoint2? percentage, MobThresholdsComponent? thresholdComponent = null) { percentage = null; if (!TryGetDeadThreshold(target, out var threshold, thresholdComponent)) return false; if (damage == 0) { percentage = 0; return true; } percentage = FixedPoint2.Min(1.0f, damage / threshold.Value); return true; } /// /// Takes the damage from one entity and scales it relative to the health of another /// /// The entity whose damage will be scaled /// The entity whose health the damage will scale to /// The newly scaled damage. Can be null public bool GetScaledDamage(EntityUid target1, EntityUid target2, out DamageSpecifier? damage) { damage = null; if (!TryComp(target1, out var oldDamage)) return false; if (!TryComp(target1, out var threshold1) || !TryComp(target2, out var threshold2)) return false; if (!TryGetThresholdForState(target1, MobState.Dead, out var ent1DeadThreshold, threshold1)) ent1DeadThreshold = 0; if (!TryGetThresholdForState(target2, MobState.Dead, out var ent2DeadThreshold, threshold2)) ent2DeadThreshold = 0; damage = (oldDamage.Damage / ent1DeadThreshold.Value) * ent2DeadThreshold.Value; return true; } /// /// Set a MobState Threshold or create a new one if it doesn't exist /// /// Target Entity /// Damageable Component owned by the target /// MobState Component owned by the target /// MobThreshold Component owned by the target public void SetMobStateThreshold(EntityUid target, FixedPoint2 damage, MobState mobState, MobThresholdsComponent? threshold = null) { if (!Resolve(target, ref threshold)) return; threshold.Thresholds[damage] = mobState; VerifyThresholds(target, threshold); } /// /// Checks to see if we should change states based on thresholds. /// Call this if you change the amount of damagable without triggering a damageChangedEvent or if you change /// /// Target Entity /// Threshold Component owned by the Target /// MobState Component owned by the Target /// Damageable Component owned by the Target public void VerifyThresholds(EntityUid target, MobThresholdsComponent? threshold = null, MobStateComponent? mobState = null, DamageableComponent? damageable = null) { if (!Resolve(target, ref mobState, ref threshold, ref damageable)) return; CheckThresholds(target, mobState, threshold, damageable); } #endregion #region Private Implementation private void CheckThresholds(EntityUid target, MobStateComponent mobStateComponent, MobThresholdsComponent thresholdsComponent, DamageableComponent damageableComponent) { foreach (var (threshold, mobState) in thresholdsComponent.Thresholds) { if (damageableComponent.TotalDamage < threshold) continue; TriggerThreshold(target, thresholdsComponent.CurrentThresholdState, mobState, mobStateComponent, thresholdsComponent); } var ev = new MobThresholdChecked(target, mobStateComponent, thresholdsComponent, damageableComponent); RaiseLocalEvent(target, ref ev, true); UpdateAlerts(target, mobStateComponent.CurrentState, thresholdsComponent, damageableComponent); } private void TriggerThreshold( EntityUid target, MobState oldState, MobState newState, MobStateComponent? mobState = null, MobThresholdsComponent? thresholds = null) { if (oldState == newState || !Resolve(target, ref mobState, ref thresholds)) { return; } thresholds.CurrentThresholdState = newState; _mobStateSystem.UpdateMobState(target, mobState); Dirty(target); } private void UpdateAlerts(EntityUid target, MobState currentMobState, MobThresholdsComponent? threshold = null, DamageableComponent? damageable = null) { if (!Resolve(target, ref threshold, ref damageable)) return; // don't handle alerts if they are managed by another system... BobbySim (soon TM) if (!threshold.TriggersAlerts) return; switch (currentMobState) { case MobState.Alive: { var severity = _alerts.GetMinSeverity(AlertType.HumanHealth); if (TryGetIncapPercentage(target, damageable.TotalDamage, out var percentage)) { severity = (short) MathF.Floor(percentage.Value.Float() * _alerts.GetMaxSeverity(AlertType.HumanHealth)); } _alerts.ShowAlert(target, AlertType.HumanHealth, severity); break; } case MobState.Critical: { _alerts.ShowAlert(target, AlertType.HumanCrit); break; } case MobState.Dead: { _alerts.ShowAlert(target, AlertType.HumanDead); break; } case MobState.Invalid: default: throw new ArgumentOutOfRangeException(nameof(currentMobState), currentMobState, null); } } private void OnDamaged(EntityUid target, MobThresholdsComponent mobThresholdsComponent, DamageChangedEvent args) { var mobStateComp = EnsureComp(target); CheckThresholds(target, mobStateComp, mobThresholdsComponent, args.Damageable); } private void OnHandleComponentState(EntityUid target, MobThresholdsComponent component, ref ComponentHandleState args) { if (args.Current is not MobThresholdComponentState state) return; component.Thresholds = new SortedDictionary(state.Thresholds); component.CurrentThresholdState = state.CurrentThresholdState; } private void OnGetComponentState(EntityUid target, MobThresholdsComponent component, ref ComponentGetState args) { args.State = new MobThresholdComponentState(component.CurrentThresholdState, new Dictionary(component.Thresholds)); } private void MobThresholdMapInit(EntityUid target, MobThresholdsComponent component, MapInitEvent args) { // TODO remove when body sim is implemented EnsureComp(target); EnsureComp(target); if (!component.Thresholds.TryFirstOrNull(out var newState)) return; component.CurrentThresholdState = newState.Value.Value; TriggerThreshold(target, MobState.Invalid, newState.Value.Value, thresholds: component); UpdateAlerts(target, newState.Value.Value, component); } private void MobThresholdShutdown(EntityUid target, MobThresholdsComponent component, ComponentShutdown args) { if (component.TriggersAlerts) _alerts.ClearAlertCategory(target, AlertCategory.Health); } private void OnUpdateMobState(EntityUid target, MobThresholdsComponent component, ref UpdateMobStateEvent args) { if (component.CurrentThresholdState != MobState.Invalid) args.State = component.CurrentThresholdState; } #endregion } /// /// Event that triggers when an entity with a mob threshold is checked /// /// Target entity /// Threshold Component owned by the Target /// MobState Component owned by the Target /// Damageable Component owned by the Target [ByRefEvent] public readonly record struct MobThresholdChecked(EntityUid Target, MobStateComponent MobState, MobThresholdsComponent Threshold, DamageableComponent Damageable) { }