Damage rework (#2525)

* Make damage work through messages and events, make destructible not inherit ruinable or reference damageable

* Copy sound logic to destructible component for now

* Fix typo

* Fix prototype error

* Remove breakable component damageable reference

* Remove breakable construction reference

* Remove ruinable component

* Move thresholds to individual components and away from damageable

* Add threshold property to damageable component code

* Add thresholds to destructible component, add states to damageable, remove damage container, fix up mob states

* Being alive isn't normal

* Fix not reading the id

* Merge fixes

* YAML fixes

* Grammar moment

* Remove unnecessary dependency

* Update thresholds doc

* Change naming of thresholds to states in MobStateComponent

* Being alive is once again normal

* Make DamageState a byte

* Bring out classes structs and enums from DestructibleComponent

* Add test for destructible thresholds

* Merge fixes

* More merge fixes and fix rejuvenate test

* Remove IMobState.IsConscious

* More merge fixes someone please god review this shit already

* Fix rejuvenate test

* Update outdated destructible in YAML

* Fix repeatedly entering the current state

* Fix repeatedly entering the current state, add Threshold.TriggersOnce and expand test

* Update saltern
This commit is contained in:
DrSmugleaf
2020-12-07 14:52:55 +01:00
committed by GitHub
parent 9a187629ba
commit 02bca4c0d8
133 changed files with 3195 additions and 5897 deletions

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.DamageContainer;
using Content.Shared.Damage.ResistanceSet;
@@ -31,51 +32,28 @@ namespace Content.Shared.GameObjects.Components.Damage
public override string Name => "Damageable";
private DamageState _damageState;
public override uint? NetID => ContentNetIDs.DAMAGEABLE;
private readonly Dictionary<DamageType, int> _damageList = DamageTypeExtensions.ToDictionary();
private readonly HashSet<DamageType> _supportedTypes = new();
private readonly HashSet<DamageClass> _supportedClasses = new();
private DamageFlag _flags;
public event Action<HealthChangedEventArgs>? HealthChangedEvent;
public event Action<DamageChangedEventArgs>? HealthChangedEvent;
// TODO DAMAGE Use as default values, specify overrides in a separate property through yaml for better (de)serialization
[ViewVariables] public string DamageContainerId { get; set; } = default!;
[ViewVariables] private ResistanceSet Resistances { get; set; } = default!;
[ViewVariables] private DamageContainer Damage { get; set; } = default!;
// TODO DAMAGE Cache this
[ViewVariables] public int TotalDamage => _damageList.Values.Sum();
public Dictionary<DamageState, int> Thresholds { get; set; } = new();
[ViewVariables]
public IReadOnlyDictionary<DamageClass, int> DamageClasses =>
DamageTypeExtensions.ToClassDictionary(_damageList);
public virtual List<DamageState> SupportedDamageStates
{
get
{
var states = new List<DamageState> {DamageState.Alive};
states.AddRange(Thresholds.Keys);
return states;
}
}
public virtual DamageState CurrentState
{
get => _damageState;
set
{
var old = _damageState;
_damageState = value;
if (old != value)
{
EnterState(value);
}
Dirty();
}
}
[ViewVariables] public int TotalDamage => Damage.TotalDamage;
public IReadOnlyDictionary<DamageClass, int> DamageClasses => Damage.DamageClasses;
public IReadOnlyDictionary<DamageType, int> DamageTypes => Damage.DamageTypes;
[ViewVariables] public IReadOnlyDictionary<DamageType, int> DamageTypes => _damageList;
public DamageFlag Flags
{
@@ -107,41 +85,20 @@ namespace Content.Shared.GameObjects.Components.Damage
Flags &= ~flag;
}
public bool SupportsDamageClass(DamageClass @class)
{
return _supportedClasses.Contains(@class);
}
public bool SupportsDamageType(DamageType type)
{
return _supportedTypes.Contains(type);
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
// TODO DAMAGE Serialize as a dictionary of damage states to thresholds
serializer.DataReadWriteFunction(
"criticalThreshold",
null,
t =>
{
if (t == null)
{
return;
}
Thresholds[DamageState.Critical] = t.Value;
},
() => Thresholds.TryGetValue(DamageState.Critical, out var value) ? value : (int?) null);
serializer.DataReadWriteFunction(
"deadThreshold",
null,
t =>
{
if (t == null)
{
return;
}
Thresholds[DamageState.Dead] = t.Value;
},
() => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null);
serializer.DataField(ref _damageState, "damageState", DamageState.Alive);
serializer.DataReadWriteFunction(
"flags",
new List<DamageFlag>(),
@@ -161,7 +118,9 @@ namespace Content.Shared.GameObjects.Components.Damage
var writeFlags = new List<DamageFlag>();
if (Flags == DamageFlag.None)
{
return writeFlags;
}
foreach (var flag in (DamageFlag[]) Enum.GetValues(typeof(DamageFlag)))
{
@@ -181,9 +140,15 @@ namespace Content.Shared.GameObjects.Components.Damage
prototype =>
{
var damagePrototype = _prototypeManager.Index<DamageContainerPrototype>(prototype);
Damage = new DamageContainer(OnHealthChanged, damagePrototype);
_supportedClasses.Clear();
_supportedTypes.Clear();
DamageContainerId = damagePrototype.ID;
_supportedClasses.UnionWith(damagePrototype.SupportedClasses);
_supportedTypes.UnionWith(damagePrototype.SupportedTypes);
},
() => Damage.ID);
() => DamageContainerId);
serializer.DataReadWriteFunction(
"resistancePrototype",
@@ -196,16 +161,6 @@ namespace Content.Shared.GameObjects.Components.Damage
() => Resistances.ID);
}
public override void Initialize()
{
base.Initialize();
foreach (var behavior in Owner.GetAllComponents<IOnHealthChangedBehavior>())
{
HealthChangedEvent += behavior.OnHealthChanged;
}
}
protected override void Startup()
{
base.Startup();
@@ -213,215 +168,302 @@ namespace Content.Shared.GameObjects.Components.Damage
ForceHealthChangedEvent();
}
public bool TryGetDamage(DamageType type, out int damage)
public override ComponentState GetComponentState()
{
return Damage.TryGetDamageValue(type, out damage);
return new DamageableComponentState(_damageList, _flags);
}
public bool ChangeDamage(DamageType type, int amount, bool ignoreResistances,
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is DamageableComponentState state))
{
return;
}
_damageList.Clear();
foreach (var (type, damage) in state.DamageList)
{
_damageList[type] = damage;
}
_flags = state.Flags;
}
public int GetDamage(DamageType type)
{
return _damageList.GetValueOrDefault(type);
}
public bool TryGetDamage(DamageType type, out int damage)
{
return _damageList.TryGetValue(type, out damage);
}
public int GetDamage(DamageClass @class)
{
if (!SupportsDamageClass(@class))
{
return 0;
}
var damage = 0;
foreach (var type in @class.ToTypes())
{
damage += GetDamage(type);
}
return damage;
}
public bool TryGetDamage(DamageClass @class, out int damage)
{
if (!SupportsDamageClass(@class))
{
damage = 0;
return false;
}
damage = GetDamage(@class);
return true;
}
/// <summary>
/// Attempts to set the damage value for the given <see cref="DamageType"/>.
/// </summary>
/// <returns>
/// True if successful, false if this container does not support that type.
/// </returns>
public bool TrySetDamage(DamageType type, int newValue)
{
if (newValue < 0)
{
return false;
}
var damageClass = type.ToClass();
if (_supportedClasses.Contains(damageClass))
{
var old = _damageList[type] = newValue;
_damageList[type] = newValue;
var delta = newValue - old;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
return false;
}
public void Heal(DamageType type)
{
SetDamage(type, 0);
}
public void Heal()
{
foreach (var type in _supportedTypes)
{
Heal(type);
}
}
public bool ChangeDamage(
DamageType type,
int amount,
bool ignoreResistances,
IEntity? source = null,
HealthChangeParams? extraParams = null)
DamageChangeParams? extraParams = null)
{
if (amount > 0 && HasFlag(DamageFlag.Invulnerable))
{
return false;
}
if (Damage.SupportsDamageType(type))
if (!SupportsDamageType(type))
{
var finalDamage = amount;
if (!ignoreResistances)
{
finalDamage = Resistances.CalculateDamage(type, amount);
}
Damage.ChangeDamageValue(type, finalDamage);
return true;
return false;
}
return false;
var finalDamage = amount;
if (!ignoreResistances)
{
finalDamage = Resistances.CalculateDamage(type, amount);
}
if (!_damageList.TryGetValue(type, out var current))
{
return false;
}
_damageList[type] = current + finalDamage;
if (_damageList[type] < 0)
{
_damageList[type] = 0;
finalDamage = -current;
}
current = _damageList[type];
var datum = new DamageChangeData(type, current, finalDamage);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
public bool ChangeDamage(DamageClass @class, int amount, bool ignoreResistances,
IEntity? source = null,
HealthChangeParams? extraParams = null)
DamageChangeParams? extraParams = null)
{
if (amount > 0 && HasFlag(DamageFlag.Invulnerable))
{
return false;
}
if (Damage.SupportsDamageClass(@class))
if (!SupportsDamageClass(@class))
{
var types = @class.ToTypes();
return false;
}
if (amount < 0)
var types = @class.ToTypes();
if (amount < 0)
{
// Changing multiple types is a bit more complicated. Might be a better way (formula?) to do this,
// but essentially just loops between each damage category until all healing is used up.
var healingLeft = amount;
var healThisCycle = 1;
// While we have healing left...
while (healingLeft > 0 && healThisCycle != 0)
{
// Changing multiple types is a bit more complicated. Might be a better way (formula?) to do this,
// but essentially just loops between each damage category until all healing is used up.
var healingLeft = amount;
var healThisCycle = 1;
// Infinite loop fallback, if no healing was done in a cycle
// then exit
healThisCycle = 0;
// While we have healing left...
while (healingLeft > 0 && healThisCycle != 0)
int healPerType;
if (healingLeft > -types.Count)
{
// Infinite loop fallback, if no healing was done in a cycle
// then exit
healThisCycle = 0;
int healPerType;
if (healingLeft > -types.Count && healingLeft < 0)
{
// Say we were to distribute 2 healing between 3
// this will distribute 1 to each (and stop after 2 are given)
healPerType = -1;
}
else
{
// Say we were to distribute 62 healing between 3
// this will distribute 20 to each, leaving 2 for next loop
healPerType = healingLeft / types.Count;
}
foreach (var type in types)
{
var healAmount =
Math.Max(Math.Max(healPerType, -Damage.GetDamageValue(type)),
healingLeft);
Damage.ChangeDamageValue(type, healAmount);
healThisCycle += healAmount;
healingLeft -= healAmount;
}
}
return true;
}
var damageLeft = amount;
while (damageLeft > 0)
{
int damagePerType;
if (damageLeft < types.Count && damageLeft > 0)
{
damagePerType = 1;
// Say we were to distribute 2 healing between 3
// this will distribute 1 to each (and stop after 2 are given)
healPerType = -1;
}
else
{
damagePerType = damageLeft / types.Count;
// Say we were to distribute 62 healing between 3
// this will distribute 20 to each, leaving 2 for next loop
healPerType = healingLeft / types.Count;
}
foreach (var type in types)
{
var damageAmount = Math.Min(damagePerType, damageLeft);
Damage.ChangeDamageValue(type, damageAmount);
damageLeft -= damageAmount;
var healAmount =
Math.Max(Math.Max(healPerType, -GetDamage(type)), healingLeft);
ChangeDamage(type, healAmount, true);
healThisCycle += healAmount;
healingLeft -= healAmount;
}
}
return true;
}
return false;
var damageLeft = amount;
while (damageLeft > 0)
{
int damagePerType;
if (damageLeft < types.Count)
{
damagePerType = 1;
}
else
{
damagePerType = damageLeft / types.Count;
}
foreach (var type in types)
{
var damageAmount = Math.Min(damagePerType, damageLeft);
ChangeDamage(type, damageAmount, true);
damageLeft -= damageAmount;
}
}
return true;
}
public bool SetDamage(DamageType type, int newValue, IEntity? source = null,
HealthChangeParams? extraParams = null)
public bool SetDamage(DamageType type, int newValue, IEntity? source = null, DamageChangeParams? extraParams = null)
{
if (newValue >= TotalDamage && HasFlag(DamageFlag.Invulnerable))
{
return false;
}
if (Damage.SupportsDamageType(type))
if (newValue < 0)
{
Damage.SetDamageValue(type, newValue);
return true;
return false;
}
return false;
}
if (!_damageList.ContainsKey(type))
{
return false;
}
public void Heal()
{
Damage.Heal();
var old = _damageList[type];
_damageList[type] = newValue;
var delta = newValue - old;
var datum = new DamageChangeData(type, 0, delta);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
public void ForceHealthChangedEvent()
{
var data = new List<HealthChangeData>();
var data = new List<DamageChangeData>();
foreach (var type in Damage.SupportedTypes)
foreach (var type in _supportedTypes)
{
var damage = Damage.GetDamageValue(type);
var datum = new HealthChangeData(type, damage, 0);
var damage = GetDamage(type);
var datum = new DamageChangeData(type, damage, 0);
data.Add(datum);
}
OnHealthChanged(data);
}
public (int current, int max)? Health(DamageState threshold)
private void OnHealthChanged(List<DamageChangeData> changes)
{
if (!SupportedDamageStates.Contains(threshold) ||
!Thresholds.TryGetValue(threshold, out var thresholdValue))
{
return null;
}
var current = thresholdValue - TotalDamage;
return (current, thresholdValue);
}
public bool TryHealth(DamageState threshold, out (int current, int max) health)
{
var temp = Health(threshold);
if (temp == null)
{
health = (default, default);
return false;
}
health = temp.Value;
return true;
}
private void OnHealthChanged(List<HealthChangeData> changes)
{
var args = new HealthChangedEventArgs(this, changes);
var args = new DamageChangedEventArgs(this, changes);
OnHealthChanged(args);
}
protected virtual void EnterState(DamageState state) { }
protected virtual void OnHealthChanged(HealthChangedEventArgs e)
protected virtual void OnHealthChanged(DamageChangedEventArgs e)
{
if (CurrentState != DamageState.Dead)
{
if (Thresholds.TryGetValue(DamageState.Dead, out var deadThreshold) &&
TotalDamage > deadThreshold)
{
CurrentState = DamageState.Dead;
}
else if (Thresholds.TryGetValue(DamageState.Critical, out var critThreshold) &&
TotalDamage > critThreshold)
{
CurrentState = DamageState.Critical;
}
else
{
CurrentState = DamageState.Alive;
}
}
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, e);
HealthChangedEvent?.Invoke(e);
var message = new DamageChangedMessage(this, e.Data);
SendMessage(message);
Dirty();
}
@@ -446,4 +488,17 @@ namespace Content.Shared.GameObjects.Components.Damage
ChangeDamage(DamageType.Heat, damage, false);
}
}
[Serializable, NetSerializable]
public class DamageableComponentState : ComponentState
{
public readonly Dictionary<DamageType, int> DamageList;
public readonly DamageFlag Flags;
public DamageableComponentState(Dictionary<DamageType, int> damageList, DamageFlag flags) : base(ContentNetIDs.DAMAGEABLE)
{
DamageList = damageList;
Flags = flags;
}
}
}