using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.FixedPoint; using Content.Shared.MobState.State; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Players; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Shared.MobState.Components { /// /// When attached to an , /// this component will handle critical and death behaviors for mobs. /// Additionally, it handles sending effects to clients /// (such as blur effect for unconsciousness) and managing the health HUD. /// [RegisterComponent] [NetworkedComponent] public class MobStateComponent : Component { public override string Name => "MobState"; /// /// States that this mapped to /// the amount of damage at which they are triggered. /// A threshold is reached when the total damage of an entity is equal /// to or higher than the int key, but lower than the next threshold. /// Ordered from lowest to highest. /// [ViewVariables] [DataField("thresholds")] private readonly SortedDictionary _lowestToHighestStates = new(); // TODO Remove Nullability? [ViewVariables] public IMobState? CurrentState { get; private set; } [ViewVariables] public FixedPoint2? CurrentThreshold { get; private set; } public IEnumerable> _highestToLowestStates => _lowestToHighestStates.Reverse(); protected override void Startup() { base.Startup(); if (CurrentState != null && CurrentThreshold != null) { // Initialize with given states SetMobState(null, (CurrentState, CurrentThreshold.Value)); } else { // Initialize with some amount of damage, defaulting to 0. UpdateState(Owner.GetComponentOrNull()?.TotalDamage ?? FixedPoint2.Zero); } } protected override void OnRemove() { if (Owner.TryGetComponent(out SharedAlertsComponent? status)) { status.ClearAlert(AlertType.HumanHealth); } base.OnRemove(); } public override ComponentState GetComponentState(ICommonSession player) { return new MobStateComponentState(CurrentThreshold); } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { base.HandleComponentState(curState, nextState); if (curState is not MobStateComponentState state) { return; } if (CurrentThreshold == state.CurrentThreshold) { return; } if (state.CurrentThreshold == null) { RemoveState(); } else { UpdateState(state.CurrentThreshold.Value); } } public bool IsAlive() { return CurrentState?.IsAlive() ?? false; } public bool IsCritical() { return CurrentState?.IsCritical() ?? false; } public bool IsDead() { return CurrentState?.IsDead() ?? false; } public bool IsIncapacitated() { return CurrentState?.IsIncapacitated() ?? false; } public (IMobState state, FixedPoint2 threshold)? GetState(FixedPoint2 damage) { foreach (var (threshold, state) in _highestToLowestStates) { if (damage >= threshold) { return (state, threshold); } } return null; } public bool TryGetState( FixedPoint2 damage, [NotNullWhen(true)] out IMobState? state, out FixedPoint2 threshold) { var highestState = GetState(damage); if (highestState == null) { state = default; threshold = default; return false; } (state, threshold) = highestState.Value; return true; } private (IMobState state, FixedPoint2 threshold)? GetEarliestState(FixedPoint2 minimumDamage, Predicate predicate) { foreach (var (threshold, state) in _lowestToHighestStates) { if (threshold < minimumDamage || !predicate(state)) { continue; } return (state, threshold); } return null; } private (IMobState state, FixedPoint2 threshold)? GetPreviousState(FixedPoint2 maximumDamage, Predicate predicate) { foreach (var (threshold, state) in _highestToLowestStates) { if (threshold > maximumDamage || !predicate(state)) { continue; } return (state, threshold); } return null; } public (IMobState state, FixedPoint2 threshold)? GetEarliestCriticalState(FixedPoint2 minimumDamage) { return GetEarliestState(minimumDamage, s => s.IsCritical()); } public (IMobState state, FixedPoint2 threshold)? GetEarliestIncapacitatedState(FixedPoint2 minimumDamage) { return GetEarliestState(minimumDamage, s => s.IsIncapacitated()); } public (IMobState state, FixedPoint2 threshold)? GetEarliestDeadState(FixedPoint2 minimumDamage) { return GetEarliestState(minimumDamage, s => s.IsDead()); } public (IMobState state, FixedPoint2 threshold)? GetPreviousCriticalState(FixedPoint2 minimumDamage) { return GetPreviousState(minimumDamage, s => s.IsCritical()); } private bool TryGetState( (IMobState state, FixedPoint2 threshold)? tuple, [NotNullWhen(true)] out IMobState? state, out FixedPoint2 threshold) { if (tuple == null) { state = default; threshold = default; return false; } (state, threshold) = tuple.Value; return true; } public bool TryGetEarliestCriticalState( FixedPoint2 minimumDamage, [NotNullWhen(true)] out IMobState? state, out FixedPoint2 threshold) { var earliestState = GetEarliestCriticalState(minimumDamage); return TryGetState(earliestState, out state, out threshold); } public bool TryGetEarliestIncapacitatedState( FixedPoint2 minimumDamage, [NotNullWhen(true)] out IMobState? state, out FixedPoint2 threshold) { var earliestState = GetEarliestIncapacitatedState(minimumDamage); return TryGetState(earliestState, out state, out threshold); } public bool TryGetEarliestDeadState( FixedPoint2 minimumDamage, [NotNullWhen(true)] out IMobState? state, out FixedPoint2 threshold) { var earliestState = GetEarliestDeadState(minimumDamage); return TryGetState(earliestState, out state, out threshold); } public bool TryGetPreviousCriticalState( FixedPoint2 maximumDamage, [NotNullWhen(true)] out IMobState? state, out FixedPoint2 threshold) { var earliestState = GetPreviousCriticalState(maximumDamage); return TryGetState(earliestState, out state, out threshold); } private void RemoveState() { var old = CurrentState; CurrentState = null; CurrentThreshold = null; SetMobState(old, null); } /// /// Updates the mob state.. /// public void UpdateState(FixedPoint2 damage) { if (!TryGetState(damage, out var newState, out var threshold)) { return; } SetMobState(CurrentState, (newState, threshold)); } /// /// Sets the mob state and marks the component as dirty. /// private void SetMobState(IMobState? old, (IMobState state, FixedPoint2 threshold)? current) { if (!current.HasValue) { old?.ExitState(Owner); return; } var (state, threshold) = current.Value; CurrentThreshold = threshold; if (state == old) { state.UpdateState(Owner, threshold); return; } old?.ExitState(Owner); CurrentState = state; state.EnterState(Owner); state.UpdateState(Owner, threshold); var message = new MobStateChangedMessage(this, old, state); #pragma warning disable 618 SendMessage(message); #pragma warning restore 618 Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); Dirty(); } } [Serializable, NetSerializable] public class MobStateComponentState : ComponentState { public readonly FixedPoint2? CurrentThreshold; public MobStateComponentState(FixedPoint2? currentThreshold) { CurrentThreshold = currentThreshold; } } }