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

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