using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Damage.Prototypes; using Content.Shared.Database; using Content.Shared.FixedPoint; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Prototypes; namespace Content.Shared.Damage { public class DamageableSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedAdminLogSystem _logs = default!; public override void Initialize() { SubscribeLocalEvent(DamageableInit); SubscribeLocalEvent(DamageableHandleState); SubscribeLocalEvent(DamageableGetState); } /// /// Update the total damage value and optionally add to admin logs /// protected virtual void SetTotalDamage(DamageableComponent damageable, FixedPoint2 @new, bool logChange) { var owner = damageable.Owner; var old = damageable.TotalDamage; if (@new == old) { return; } damageable.TotalDamage = @new; if (!logChange) return; LogType logType; string type; FixedPoint2 change; if (@new > old) { logType = LogType.Damaged; type = "received"; change = @new - old; } else { logType = LogType.Healed; type = "healed"; change = old - @new; } _logs.Add(logType, $"{owner} {type} {change} damage. Old: {old} | New: {@new}"); } /// /// Initialize a damageable component /// private void DamageableInit(EntityUid uid, DamageableComponent component, ComponentInit _) { if (component.DamageContainerID != null && _prototypeManager.TryIndex(component.DamageContainerID, out var damageContainerPrototype)) { // Initialize damage dictionary, using the types and groups from the damage // container prototype foreach (var type in damageContainerPrototype.SupportedTypes) { component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero); } foreach (var groupID in damageContainerPrototype.SupportedGroups) { var group = _prototypeManager.Index(groupID); foreach (var type in group.DamageTypes) { component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero); } } } else { // No DamageContainerPrototype was given. So we will allow the container to support all damage types foreach (var type in _prototypeManager.EnumeratePrototypes()) { component.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero); } } component.DamagePerGroup = component.Damage.GetDamagePerGroup(); component.TotalDamage = component.Damage.Total; } /// /// Directly sets the damage specifier of a damageable component. /// /// /// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed /// event is raised. /// public void SetDamage(DamageableComponent damageable, DamageSpecifier damage) { damageable.Damage = damage; DamageChanged(damageable, false); } /// /// If the damage in a DamageableComponent was changed, this function should be called. /// /// /// This updates cached damage information, flags the component as dirty, and raises a damage changed event. /// The damage changed event is used by other systems, such as damage thresholds. /// public void DamageChanged(DamageableComponent component, bool logChange, DamageSpecifier? damageDelta = null, bool interruptsDoAfters = true) { component.DamagePerGroup = component.Damage.GetDamagePerGroup(); SetTotalDamage(component, component.Damage.Total, logChange); component.Dirty(); if (EntityManager.TryGetComponent(component.OwnerUid, out var appearance) && damageDelta != null) appearance.SetData(DamageVisualizerKeys.DamageUpdateGroups, damageDelta.GetDamagePerGroup().Keys.ToList()); RaiseLocalEvent(component.OwnerUid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters), false); } /// /// Applies damage specified via a . /// /// /// is effectively just a dictionary of damage types and damage values. This /// function just applies the container's resistances (unless otherwise specified) and then changes the /// stored damage data. Division of group damage into types is managed by . /// /// /// Returns a with information about the actual damage changes. This will be /// null if the user had no applicable components that can take damage. /// public DamageSpecifier? TryChangeDamage(EntityUid uid, DamageSpecifier damage, bool ignoreResistances = false, bool interruptsDoAfters = true, bool logChange = false) { if (!EntityManager.TryGetComponent(uid, out var damageable)) { // TODO BODY SYSTEM pass damage onto body system return null; } if (damage == null) { Logger.Error("Null DamageSpecifier. Probably because a required yaml field was not given."); return null; } if (damage.Empty) { return damage; } // Apply resistances if (!ignoreResistances) { if (damageable.DamageModifierSetId != null && _prototypeManager.TryIndex(damageable.DamageModifierSetId, out var modifierSet)) { damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); } var ev = new DamageModifyEvent(damage); RaiseLocalEvent(uid, ev, false); damage = ev.Damage; if (damage.Empty) { return damage; } } // Copy the current damage, for calculating the difference DamageSpecifier oldDamage = new(damageable.Damage); damageable.Damage.ExclusiveAdd(damage); damageable.Damage.ClampMin(FixedPoint2.Zero); var delta = damageable.Damage - oldDamage; delta.TrimZeros(); if (!delta.Empty) { DamageChanged(damageable, logChange, delta, interruptsDoAfters); } return delta; } /// /// Sets all damage types supported by a to the specified value. /// /// /// Does nothing If the given damage value is negative. /// public void SetAllDamage(DamageableComponent component, FixedPoint2 newValue) { if (newValue < 0) { // invalid value return; } foreach (var type in component.Damage.DamageDict.Keys) { component.Damage.DamageDict[type] = newValue; } // Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an // empty damage delta. DamageChanged(component, false, new DamageSpecifier()); } private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args) { args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId); } private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args) { if (args.Current is not DamageableComponentState state) { return; } component.DamageModifierSetId = state.ModifierSetId; // Has the damage actually changed? DamageSpecifier newDamage = new() { DamageDict = state.DamageDict }; var delta = component.Damage - newDamage; delta.TrimZeros(); if (!delta.Empty) { component.Damage = newDamage; DamageChanged(component, false, delta); } } } /// /// Raised on an entity when damage is about to be dealt, /// in case anything else needs to modify it other than the base /// damageable component. /// /// For example, armor. /// public class DamageModifyEvent : EntityEventArgs { public DamageSpecifier Damage; public DamageModifyEvent(DamageSpecifier damage) { Damage = damage; } } public class DamageChangedEvent : EntityEventArgs { /// /// This is the component whose damage was changed. /// /// /// Given that nearly every component that cares about a change in the damage, needs to know the /// current damage values, directly passing this information prevents a lot of duplicate /// Owner.TryGetComponent() calls. /// public readonly DamageableComponent Damageable; /// /// The amount by which the damage has changed. If the damage was set directly to some number, this will be /// null. /// public readonly DamageSpecifier? DamageDelta; /// /// Was any of the damage change dealing damage, or was it all healing? /// public readonly bool DamageIncreased = false; /// /// Does this event interrupt DoAfters? /// Note: As provided in the constructor, this *does not* account for DamageIncreased. /// As written into the event, this *does* account for DamageIncreased. /// public readonly bool InterruptsDoAfters = false; public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters) { Damageable = damageable; DamageDelta = damageDelta; if (DamageDelta == null) return; foreach (var damageChange in DamageDelta.DamageDict.Values) { if (damageChange > 0) { DamageIncreased = true; break; } } InterruptsDoAfters = interruptsDoAfters && DamageIncreased; } } }