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

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Part.Property;
using Content.Shared.GameObjects.Components.Body.Preset;
@@ -103,8 +104,7 @@ namespace Content.Shared.GameObjects.Components.Body
{
if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0)
{
damageable.CurrentState = DamageState.Dead;
damageable.ForceHealthChangedEvent();
damageable.ChangeDamage(DamageType.Bloodloss, 300, true); // TODO BODY KILL
}
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Damage;
namespace Content.Shared.GameObjects.Components.Damage
{
/// <summary>
/// Data class with information on how the value of a
/// single <see cref="DamageType"/> has changed.
/// </summary>
public struct DamageChangeData
{
/// <summary>
/// Type of damage that changed.
/// </summary>
public DamageType Type;
/// <summary>
/// The new current value for that damage.
/// </summary>
public int NewValue;
/// <summary>
/// How much the health value changed from its last value (negative is heals, positive is damage).
/// </summary>
public int Delta;
public DamageChangeData(DamageType type, int newValue, int delta)
{
Type = type;
NewValue = newValue;
Delta = delta;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using Content.Shared.GameObjects.Components.Body;
namespace Content.Shared.GameObjects.Components.Damage
{
/// <summary>
/// Data class with information on how to damage a
/// <see cref="IDamageableComponent"/>.
/// While not necessary to damage for all instances, classes such as
/// <see cref="SharedBodyComponent"/> may require it for extra data
/// (such as selecting which limb to target).
/// </summary>
public class DamageChangeParams : EventArgs
{
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Content.Shared.Damage;
namespace Content.Shared.GameObjects.Components.Damage
{
public class DamageChangedEventArgs : EventArgs
{
public DamageChangedEventArgs(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
{
Damageable = damageable;
Data = data;
}
public DamageChangedEventArgs(IDamageableComponent damageable, DamageType type, int newValue, int delta)
{
Damageable = damageable;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
Data = data;
}
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public IDamageableComponent Damageable { get; }
/// <summary>
/// List containing data on each <see cref="DamageType"/> that was changed.
/// </summary>
public IReadOnlyList<DamageChangeData> Data { get; }
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using Content.Shared.Damage;
using Robust.Shared.GameObjects;
namespace Content.Shared.GameObjects.Components.Damage
{
public class DamageChangedMessage : ComponentMessage
{
public DamageChangedMessage(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
{
Damageable = damageable;
Data = data;
}
public DamageChangedMessage(IDamageableComponent damageable, DamageType type, int newValue, int delta)
{
Damageable = damageable;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
Data = data;
}
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public IDamageableComponent Damageable { get; }
/// <summary>
/// List containing data on each <see cref="DamageType"/> that was changed.
/// </summary>
public IReadOnlyList<DamageChangeData> Data { get; }
}
}

View File

@@ -1,24 +0,0 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Damage
{
// TODO: Fix summary
/// <summary>
/// Defines what state an <see cref="IEntity"/> with a
/// <see cref="IDamageableComponent"/> is in.
/// Not all states must be supported - for instance, the
/// <see cref="RuinableComponent"/> only supports
/// <see cref="DamageState.Alive"/> and <see cref="DamageState.Dead"/>,
/// as inanimate objects don't go into crit.
/// </summary>
[Serializable, NetSerializable]
public enum DamageState
{
Invalid = 0,
Alive,
Critical,
Dead
}
}

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;
}
}
}

View File

@@ -1,9 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.Interfaces.GameObjects;
@@ -17,20 +15,7 @@ namespace Content.Shared.GameObjects.Components.Damage
/// (including both damage negated by resistance or simply inputting 0 as
/// the amount of damage to deal).
/// </summary>
event Action<HealthChangedEventArgs> HealthChangedEvent;
Dictionary<DamageState, int> Thresholds { get; }
/// <summary>
/// List of all <see cref="Damage.DamageState">DamageStates</see> that
/// <see cref="CurrentState"/> can be.
/// </summary>
List<DamageState> SupportedDamageStates { get; }
/// <summary>
/// The <see cref="Damage.DamageState"/> currently representing this component.
/// </summary>
DamageState CurrentState { get; set; }
event Action<DamageChangedEventArgs> HealthChangedEvent;
/// <summary>
/// Sum of all damages taken.
@@ -71,6 +56,10 @@ namespace Content.Shared.GameObjects.Components.Damage
/// <param name="flag">The flag to remove.</param>
void RemoveFlag(DamageFlag flag);
bool SupportsDamageClass(DamageClass @class);
bool SupportsDamageType(DamageType type);
/// <summary>
/// Gets the amount of damage of a type.
/// </summary>
@@ -79,7 +68,7 @@ namespace Content.Shared.GameObjects.Components.Damage
/// <returns>
/// True if the given <see cref="type"/> is supported, false otherwise.
/// </returns>
bool TryGetDamage(DamageType type, [NotNullWhen(true)] out int damage);
bool TryGetDamage(DamageType type, out int damage);
/// <summary>
/// Changes the specified <see cref="DamageType"/>, applying
@@ -101,10 +90,14 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </param>
/// <returns>
/// False if the given type is not supported or improper
/// <see cref="HealthChangeParams"/> were provided; true otherwise.
/// <see cref="DamageChangeParams"/> were provided; true otherwise.
/// </returns>
bool ChangeDamage(DamageType type, int amount, bool ignoreResistances, IEntity? source = null,
HealthChangeParams? extraParams = null);
bool ChangeDamage(
DamageType type,
int amount,
bool ignoreResistances,
IEntity? source = null,
DamageChangeParams? extraParams = null);
/// <summary>
/// Changes the specified <see cref="DamageClass"/>, applying
@@ -127,10 +120,14 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </param>
/// <returns>
/// Returns false if the given class is not supported or improper
/// <see cref="HealthChangeParams"/> were provided; true otherwise.
/// <see cref="DamageChangeParams"/> were provided; true otherwise.
/// </returns>
bool ChangeDamage(DamageClass @class, int amount, bool ignoreResistances, IEntity? source = null,
HealthChangeParams? extraParams = null);
bool ChangeDamage(
DamageClass @class,
int amount,
bool ignoreResistances,
IEntity? source = null,
DamageChangeParams? extraParams = null);
/// <summary>
/// Forcefully sets the specified <see cref="DamageType"/> to the given
@@ -145,9 +142,13 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </param>
/// <returns>
/// Returns false if the given type is not supported or improper
/// <see cref="HealthChangeParams"/> were provided; true otherwise.
/// <see cref="DamageChangeParams"/> were provided; true otherwise.
/// </returns>
bool SetDamage(DamageType type, int newValue, IEntity? source = null, HealthChangeParams? extraParams = null);
bool SetDamage(
DamageType type,
int newValue,
IEntity? source = null,
DamageChangeParams? extraParams = null);
/// <summary>
/// Sets all damage values to zero.
@@ -158,103 +159,5 @@ namespace Content.Shared.GameObjects.Components.Damage
/// Invokes the HealthChangedEvent with the current values of health.
/// </summary>
void ForceHealthChangedEvent();
/// <summary>
/// Calculates the health of an entity until it enters
/// <see cref="threshold"/>.
/// </summary>
/// <param name="threshold">The state to use as a threshold.</param>
/// <returns>
/// The current and maximum health on this entity based on
/// <see cref="threshold"/>, or null if the state is not supported.
/// </returns>
(int current, int max)? Health(DamageState threshold);
/// <summary>
/// Calculates the health of an entity until it enters
/// <see cref="threshold"/>.
/// </summary>
/// <param name="threshold">The state to use as a threshold.</param>
/// <param name="health">
/// The current and maximum health on this entity based on
/// <see cref="threshold"/>, or null if the state is not supported.
/// </param>
/// <returns>
/// True if <see cref="threshold"/> is supported, false otherwise.
/// </returns>
bool TryHealth(DamageState threshold, [NotNullWhen(true)] out (int current, int max) health);
}
/// <summary>
/// Data class with information on how to damage a
/// <see cref="IDamageableComponent"/>.
/// While not necessary to damage for all instances, classes such as
/// <see cref="SharedBodyComponent"/> may require it for extra data
/// (such as selecting which limb to target).
/// </summary>
public class HealthChangeParams : EventArgs
{
}
/// <summary>
/// Data class with information on how the <see cref="DamageType"/>
/// values of a <see cref="IDamageableComponent"/> have changed.
/// </summary>
public class HealthChangedEventArgs : EventArgs
{
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public readonly IDamageableComponent Damageable;
/// <summary>
/// List containing data on each <see cref="DamageType"/> that was changed.
/// </summary>
public readonly List<HealthChangeData> Data;
public HealthChangedEventArgs(IDamageableComponent damageable, List<HealthChangeData> data)
{
Damageable = damageable;
Data = data;
}
public HealthChangedEventArgs(IDamageableComponent damageable, DamageType type, int newValue, int delta)
{
Damageable = damageable;
var datum = new HealthChangeData(type, newValue, delta);
var data = new List<HealthChangeData> {datum};
Data = data;
}
}
/// <summary>
/// Data class with information on how the value of a
/// single <see cref="DamageType"/> has changed.
/// </summary>
public struct HealthChangeData
{
/// <summary>
/// Type of damage that changed.
/// </summary>
public DamageType Type;
/// <summary>
/// The new current value for that damage.
/// </summary>
public int NewValue;
/// <summary>
/// How much the health value changed from its last value (negative is heals, positive is damage).
/// </summary>
public int Delta;
public HealthChangeData(DamageType type, int newValue, int delta)
{
Type = type;
NewValue = newValue;
Delta = delta;
}
}
}

View File

@@ -1,22 +0,0 @@
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Damage
{
// TODO
/// <summary>
/// Component interface that gets triggered after the values of a
/// <see cref="IDamageableComponent"/> on the same <see cref="IEntity"/> change.
/// </summary>
public interface IOnHealthChangedBehavior
{
/// <summary>
/// Called when the entity's <see cref="IDamageableComponent"/>
/// is healed or hurt.
/// Of note is that a "deal 0 damage" call will still trigger
/// this function (including both damage negated by resistance or
/// simply inputting 0 as the amount of damage to deal).
/// </summary>
/// <param name="e">Details of how the health changed.</param>
public void OnHealthChanged(HealthChangedEventArgs e);
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
namespace Content.Shared.GameObjects.Components.Mobs
{
public static class DamageStateHelpers
{
/// <summary>
/// Enumerates over <see cref="DamageState"/>, returning them in order
/// of alive to dead.
/// </summary>
/// <returns>An enumerable of <see cref="DamageState"/>.</returns>
public static IEnumerable<DamageState> AliveToDead()
{
foreach (DamageState state in Enum.GetValues(typeof(DamageState)))
{
if (state == DamageState.Invalid)
{
continue;
}
yield return state;
}
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs
{
[Serializable, NetSerializable]
public enum DamageStateVisuals
{
State
}
/// <summary>
/// Defines what state an <see cref="IEntity"/> is in.
///
/// Ordered from most alive to least alive.
/// To enumerate them in this way see
/// <see cref="DamageStateHelpers.AliveToDead"/>.
/// </summary>
[Serializable, NetSerializable]
public enum DamageState : byte
{
Invalid = 0,
Alive = 1,
Critical = 2,
Dead = 3
}
}

View File

@@ -1,11 +0,0 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs
{
[Serializable, NetSerializable]
public enum DamageStateVisuals
{
State
}
}

View File

@@ -0,0 +1,98 @@
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
public abstract class BaseMobState : IMobState
{
protected abstract DamageState DamageState { get; }
public virtual bool IsAlive()
{
return DamageState == DamageState.Alive;
}
public virtual bool IsCritical()
{
return DamageState == DamageState.Critical;
}
public virtual bool IsDead()
{
return DamageState == DamageState.Dead;
}
public virtual bool IsIncapacitated()
{
return IsCritical() || IsDead();
}
public virtual void EnterState(IEntity entity) { }
public virtual void ExitState(IEntity entity) { }
public virtual void UpdateState(IEntity entity, int threshold) { }
public virtual void ExposeData(ObjectSerializer serializer) { }
public virtual bool CanInteract()
{
return true;
}
public virtual bool CanMove()
{
return true;
}
public virtual bool CanUse()
{
return true;
}
public virtual bool CanThrow()
{
return true;
}
public virtual bool CanSpeak()
{
return true;
}
public virtual bool CanDrop()
{
return true;
}
public virtual bool CanPickup()
{
return true;
}
public virtual bool CanEmote()
{
return true;
}
public virtual bool CanAttack()
{
return true;
}
public virtual bool CanEquip()
{
return true;
}
public virtual bool CanUnequip()
{
return true;
}
public virtual bool CanChangeDirection()
{
return true;
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
@@ -9,8 +9,21 @@ namespace Content.Shared.GameObjects.Components.Mobs.State
/// (i.e. Normal, Critical, Dead) and what effects to apply upon entering or
/// exiting the state.
/// </summary>
public interface IMobState : IActionBlocker
public interface IMobState : IExposeData, IActionBlocker
{
bool IsAlive();
bool IsCritical();
bool IsDead();
/// <summary>
/// Checks if the mob is in a critical or dead state.
/// See <see cref="IsCritical"/> and <see cref="IsDead"/>.
/// </summary>
/// <returns>true if it is, false otherwise.</returns>
bool IsIncapacitated();
/// <summary>
/// Called when this state is entered.
/// </summary>
@@ -24,6 +37,6 @@ namespace Content.Shared.GameObjects.Components.Mobs.State
/// <summary>
/// Called when this state is updated.
/// </summary>
void UpdateState(IEntity entity);
void UpdateState(IEntity entity, int threshold);
}
}

View File

@@ -0,0 +1,28 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
public interface IMobStateComponent : IComponent
{
IMobState? CurrentState { get; }
bool IsAlive();
bool IsCritical();
bool IsDead();
bool IsIncapacitated();
(IMobState state, int threshold)? GetEarliestIncapacitatedState(int minimumDamage);
bool TryGetEarliestIncapacitatedState(
int minimumDamage,
[NotNullWhen(true)] out IMobState? state,
out int threshold);
void UpdateState(int damage, bool syncing = false);
}
}

View File

@@ -0,0 +1,27 @@
#nullable enable
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
public class MobStateChangedMessage : ComponentMessage
{
public MobStateChangedMessage(
IMobStateComponent component,
IMobState? oldMobState,
IMobState currentMobState)
{
Component = component;
OldMobState = oldMobState;
CurrentMobState = currentMobState;
}
public IEntity Entity => Component.Owner;
public IMobStateComponent Component { get; }
public IMobState? OldMobState { get; }
public IMobState CurrentMobState { get; }
}
}

View File

@@ -0,0 +1,92 @@
using Content.Shared.Alert;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
/// <summary>
/// A state in which an entity is disabled from acting due to sufficient damage (considered unconscious).
/// </summary>
public abstract class SharedCriticalMobState : BaseMobState
{
protected override DamageState DamageState => DamageState.Critical;
public override void EnterState(IEntity entity)
{
base.EnterState(entity);
if (entity.TryGetComponent(out SharedAlertsComponent status))
{
status.ShowAlert(AlertType.HumanCrit); // TODO: combine humancrit-0 and humancrit-1 into a gif and display it
}
}
public override void ExitState(IEntity entity)
{
base.ExitState(entity);
EntitySystem.Get<SharedStandingStateSystem>().Standing(entity);
}
public override bool CanInteract()
{
return false;
}
public override bool CanMove()
{
return false;
}
public override bool CanUse()
{
return false;
}
public override bool CanThrow()
{
return false;
}
public override bool CanSpeak()
{
return false;
}
public override bool CanDrop()
{
return false;
}
public override bool CanPickup()
{
return false;
}
public override bool CanEmote()
{
return false;
}
public override bool CanAttack()
{
return false;
}
public override bool CanEquip()
{
return false;
}
public override bool CanUnequip()
{
return false;
}
public override bool CanChangeDirection()
{
return false;
}
}
}

View File

@@ -1,76 +0,0 @@
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
/// <summary>
/// A state in which an entity is disabled from acting due to sufficient damage (considered unconscious).
/// </summary>
public abstract class SharedCriticalState : IMobState
{
public abstract void EnterState(IEntity entity);
public abstract void ExitState(IEntity entity);
public abstract void UpdateState(IEntity entity);
public bool CanInteract()
{
return false;
}
public bool CanMove()
{
return false;
}
public bool CanUse()
{
return false;
}
public bool CanThrow()
{
return false;
}
public bool CanSpeak()
{
return false;
}
public bool CanDrop()
{
return false;
}
public bool CanPickup()
{
return false;
}
public bool CanEmote()
{
return false;
}
public bool CanAttack()
{
return false;
}
public bool CanEquip()
{
return false;
}
public bool CanUnequip()
{
return false;
}
public bool CanChangeDirection()
{
return false;
}
}
}

View File

@@ -0,0 +1,77 @@
namespace Content.Shared.GameObjects.Components.Mobs.State
{
public abstract class SharedDeadMobState : BaseMobState
{
protected override DamageState DamageState => DamageState.Dead;
public override bool CanInteract()
{
return false;
}
public override bool CanMove()
{
return false;
}
public override bool CanUse()
{
return false;
}
public override bool CanThrow()
{
return false;
}
public override bool CanSpeak()
{
return false;
}
public override bool CanDrop()
{
return false;
}
public override bool CanPickup()
{
return false;
}
public override bool CanEmote()
{
return false;
}
public override bool CanAttack()
{
return false;
}
public override bool CanEquip()
{
return false;
}
public override bool CanUnequip()
{
return false;
}
public override bool CanChangeDirection()
{
return false;
}
public bool CanShiver()
{
return false;
}
public bool CanSweat()
{
return false;
}
}
}

View File

@@ -1,76 +0,0 @@
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
public abstract class SharedDeadState : IMobState
{
public abstract void EnterState(IEntity entity);
public abstract void ExitState(IEntity entity);
public abstract void UpdateState(IEntity entity);
public bool CanInteract()
{
return false;
}
public bool CanMove()
{
return false;
}
public bool CanUse()
{
return false;
}
public bool CanThrow()
{
return false;
}
public bool CanSpeak()
{
return false;
}
public bool CanDrop()
{
return false;
}
public bool CanPickup()
{
return false;
}
public bool CanEmote()
{
return false;
}
public bool CanAttack()
{
return false;
}
public bool CanEquip()
{
return false;
}
public bool CanUnequip()
{
return false;
}
public bool CanChangeDirection()
{
return false;
}
public bool CanShiver() => false;
public bool CanSweat() => false;
}
}

View File

@@ -0,0 +1,345 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
/// <summary>
/// When attached to an <see cref="IDamageableComponent"/>,
/// 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.
/// </summary>
public abstract class SharedMobStateComponent : Component, IMobStateComponent, IActionBlocker
{
public override string Name => "MobState";
public override uint? NetID => ContentNetIDs.MOB_STATE;
/// <summary>
/// States that this <see cref="SharedMobStateComponent"/> 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.
/// </summary>
[ViewVariables]
private SortedDictionary<int, IMobState> _lowestToHighestStates = default!;
// TODO Remove Nullability?
[ViewVariables]
public IMobState? CurrentState { get; private set; }
[ViewVariables]
public int? CurrentThreshold { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction(
"thresholds",
new Dictionary<int, IMobState>(),
thresholds =>
{
_lowestToHighestStates = new SortedDictionary<int, IMobState>(thresholds);
},
() => new Dictionary<int, IMobState>(_lowestToHighestStates));
}
protected override void Startup()
{
base.Startup();
if (CurrentState != null && CurrentThreshold != null)
{
UpdateState(null, (CurrentState, CurrentThreshold.Value));
}
}
public override void OnRemove()
{
if (Owner.TryGetComponent(out SharedAlertsComponent? status))
{
status.ClearAlert(AlertType.HumanHealth);
}
base.OnRemove();
}
public override ComponentState GetComponentState()
{
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(true);
}
else
{
UpdateState(state.CurrentThreshold.Value, true);
}
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
if (msg.Damageable.Owner != Owner)
{
break;
}
UpdateState(msg.Damageable.TotalDamage);
break;
}
}
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, int threshold)? GetState(int damage)
{
foreach (var (threshold, state) in _lowestToHighestStates.Reverse())
{
if (damage >= threshold)
{
return (state, threshold);
}
}
return null;
}
public bool TryGetState(
int damage,
[NotNullWhen(true)] out IMobState? state,
out int threshold)
{
var highestState = GetState(damage);
if (highestState == null)
{
state = default;
threshold = default;
return false;
}
(state, threshold) = highestState.Value;
return true;
}
public (IMobState state, int threshold)? GetEarliestIncapacitatedState(int minimumDamage)
{
foreach (var (threshold, state) in _lowestToHighestStates)
{
if (!state.IsIncapacitated())
{
continue;
}
if (threshold < minimumDamage)
{
continue;
}
return (state, threshold);
}
return null;
}
public bool TryGetEarliestIncapacitatedState(
int minimumDamage,
[NotNullWhen(true)] out IMobState? state,
out int threshold)
{
var earliestState = GetEarliestIncapacitatedState(minimumDamage);
if (earliestState == null)
{
state = default;
threshold = default;
return false;
}
(state, threshold) = earliestState.Value;
return true;
}
private void RemoveState(bool syncing = false)
{
var old = CurrentState;
CurrentState = null;
CurrentThreshold = null;
UpdateState(old, null);
if (!syncing)
{
Dirty();
}
}
public void UpdateState(int damage, bool syncing = false)
{
if (!TryGetState(damage, out var newState, out var threshold))
{
return;
}
UpdateState(CurrentState, (newState, threshold));
if (!syncing)
{
Dirty();
}
}
private void UpdateState(IMobState? old, (IMobState state, int 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);
SendMessage(message);
}
bool IActionBlocker.CanInteract()
{
return CurrentState?.CanInteract() ?? true;
}
bool IActionBlocker.CanMove()
{
return CurrentState?.CanMove() ?? true;
}
bool IActionBlocker.CanUse()
{
return CurrentState?.CanUse() ?? true;
}
bool IActionBlocker.CanThrow()
{
return CurrentState?.CanThrow() ?? true;
}
bool IActionBlocker.CanSpeak()
{
return CurrentState?.CanSpeak() ?? true;
}
bool IActionBlocker.CanDrop()
{
return CurrentState?.CanDrop() ?? true;
}
bool IActionBlocker.CanPickup()
{
return CurrentState?.CanPickup() ?? true;
}
bool IActionBlocker.CanEmote()
{
return CurrentState?.CanEmote() ?? true;
}
bool IActionBlocker.CanAttack()
{
return CurrentState?.CanAttack() ?? true;
}
bool IActionBlocker.CanEquip()
{
return CurrentState?.CanEquip() ?? true;
}
bool IActionBlocker.CanUnequip()
{
return CurrentState?.CanUnequip() ?? true;
}
bool IActionBlocker.CanChangeDirection()
{
return CurrentState?.CanChangeDirection() ?? true;
}
}
[Serializable, NetSerializable]
public class MobStateComponentState : ComponentState
{
public readonly int? CurrentThreshold;
public MobStateComponentState(int? currentThreshold) : base(ContentNetIDs.MOB_STATE)
{
CurrentThreshold = currentThreshold;
}
}
}

View File

@@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
/// <summary>
/// When attacked to an <see cref="IDamageableComponent"/>, 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.
/// </summary>
public abstract class SharedMobStateManagerComponent : Component, IOnHealthChangedBehavior, IActionBlocker
{
public override string Name => "MobStateManager";
public override uint? NetID => ContentNetIDs.MOB_STATE_MANAGER;
protected abstract IReadOnlyDictionary<DamageState, IMobState> Behavior { get; }
public virtual IMobState CurrentMobState { get; protected set; }
public virtual DamageState CurrentDamageState { get; protected set; }
public override void Initialize()
{
base.Initialize();
CurrentDamageState = DamageState.Alive;
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);
CurrentMobState.UpdateState(Owner);
}
bool IActionBlocker.CanInteract()
{
return CurrentMobState.CanInteract();
}
bool IActionBlocker.CanMove()
{
return CurrentMobState.CanMove();
}
bool IActionBlocker.CanUse()
{
return CurrentMobState.CanUse();
}
bool IActionBlocker.CanThrow()
{
return CurrentMobState.CanThrow();
}
bool IActionBlocker.CanSpeak()
{
return CurrentMobState.CanSpeak();
}
bool IActionBlocker.CanDrop()
{
return CurrentMobState.CanDrop();
}
bool IActionBlocker.CanPickup()
{
return CurrentMobState.CanPickup();
}
bool IActionBlocker.CanEmote()
{
return CurrentMobState.CanEmote();
}
bool IActionBlocker.CanAttack()
{
return CurrentMobState.CanAttack();
}
bool IActionBlocker.CanEquip()
{
return CurrentMobState.CanEquip();
}
bool IActionBlocker.CanUnequip()
{
return CurrentMobState.CanUnequip();
}
bool IActionBlocker.CanChangeDirection()
{
return CurrentMobState.CanChangeDirection();
}
public void OnHealthChanged(HealthChangedEventArgs e)
{
if (e.Damageable.CurrentState != CurrentDamageState)
{
CurrentDamageState = e.Damageable.CurrentState;
CurrentMobState.ExitState(Owner);
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);
}
CurrentMobState.UpdateState(Owner);
}
}
[Serializable, NetSerializable]
public class MobStateManagerComponentState : ComponentState
{
public readonly DamageState DamageState;
public MobStateManagerComponentState(DamageState damageState) : base(ContentNetIDs.MOB_STATE_MANAGER)
{
DamageState = damageState;
}
}
}

View File

@@ -0,0 +1,70 @@
namespace Content.Shared.GameObjects.Components.Mobs.State
{
/// <summary>
/// The standard state an entity is in; no negative effects.
/// </summary>
public abstract class SharedNormalMobState : BaseMobState
{
protected override DamageState DamageState => DamageState.Alive;
public override bool CanInteract()
{
return true;
}
public override bool CanMove()
{
return true;
}
public override bool CanUse()
{
return true;
}
public override bool CanThrow()
{
return true;
}
public override bool CanSpeak()
{
return true;
}
public override bool CanDrop()
{
return true;
}
public override bool CanPickup()
{
return true;
}
public override bool CanEmote()
{
return true;
}
public override bool CanAttack()
{
return true;
}
public override bool CanEquip()
{
return true;
}
public override bool CanUnequip()
{
return true;
}
public override bool CanChangeDirection()
{
return true;
}
}
}

View File

@@ -1,76 +0,0 @@
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Mobs.State
{
/// <summary>
/// The standard state an entity is in; no negative effects.
/// </summary>
public abstract class SharedNormalState : IMobState
{
public abstract void EnterState(IEntity entity);
public abstract void ExitState(IEntity entity);
public abstract void UpdateState(IEntity entity);
public bool CanInteract()
{
return true;
}
public bool CanMove()
{
return true;
}
public bool CanUse()
{
return true;
}
public bool CanThrow()
{
return true;
}
public bool CanSpeak()
{
return true;
}
public bool CanDrop()
{
return true;
}
public bool CanPickup()
{
return true;
}
public bool CanEmote()
{
return true;
}
public bool CanAttack()
{
return true;
}
public bool CanEquip()
{
return true;
}
public bool CanUnequip()
{
return true;
}
public bool CanChangeDirection()
{
return true;
}
}
}