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,12 +1,16 @@
#nullable enable
using System;
using Content.Server.Commands.Observer;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Movement;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Players;
@@ -76,10 +80,12 @@ namespace Content.Server.GameObjects.Components.Body
void IRelayMoveInput.MoveInputPressed(ICommonSession session)
{
if (Owner.TryGetComponent(out IDamageableComponent? damageable) &&
damageable.CurrentState == DamageState.Dead)
if (Owner.TryGetComponent(out IMobStateComponent? mobState) &&
mobState.IsDead())
{
new Ghost().Execute(null, (IPlayerSession) session, null);
var shell = IoCManager.Resolve<IConsoleShell>();
new Ghost().Execute(shell, (IPlayerSession) session, Array.Empty<string>());
}
}
}

View File

@@ -8,9 +8,9 @@ namespace Content.Server.GameObjects.Components.Body
BodyPartType Part { get; }
}
public class BodyHealthChangeParams : HealthChangeParams, IBodyHealthChangeParams
public class BodyDamageChangeParams : DamageChangeParams, IBodyHealthChangeParams
{
public BodyHealthChangeParams(BodyPartType part)
public BodyDamageChangeParams(BodyPartType part)
{
Part = part;
}

View File

@@ -40,7 +40,7 @@ namespace Content.Server.GameObjects.Components.Buckle
[ComponentDependency] public readonly AppearanceComponent? AppearanceComponent = null;
[ComponentDependency] private readonly ServerAlertsComponent? _serverAlertsComponent = null;
[ComponentDependency] private readonly StunnableComponent? _stunnableComponent = null;
[ComponentDependency] private readonly MobStateManagerComponent? _mobStateManagerComponent = null;
[ComponentDependency] private readonly MobStateComponent? _mobStateComponent = null;
private int _size;
@@ -351,7 +351,7 @@ namespace Content.Server.GameObjects.Components.Buckle
EntitySystem.Get<StandingStateSystem>().Standing(Owner);
}
_mobStateManagerComponent?.CurrentMobState.EnterState(Owner);
_mobStateComponent?.CurrentState?.EnterState(Owner);
UpdateBuckleStatus();

View File

@@ -1,72 +1,10 @@
using System.Collections.Generic;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Random;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Damage
{
// TODO: Repair needs to set CurrentDamageState to DamageState.Alive, but it doesn't exist... should be easy enough if it's just an interface you can slap on BreakableComponent
/// <summary>
/// When attached to an <see cref="IEntity"/>, allows it to take damage and sets it to a "broken state" after taking
/// enough damage.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
public class BreakableComponent : RuinableComponent, IExAct
public class BreakableComponent : Component
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override string Name => "Breakable";
private ActSystem _actSystem;
public override List<DamageState> SupportedDamageStates =>
new() {DamageState.Alive, DamageState.Dead};
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
switch (eventArgs.Severity)
{
case ExplosionSeverity.Destruction:
case ExplosionSeverity.Heavy:
PerformDestruction();
break;
case ExplosionSeverity.Light:
if (_random.Prob(0.5f))
{
PerformDestruction();
}
break;
}
}
public override void Initialize()
{
base.Initialize();
_actSystem = _entitySystemManager.GetEntitySystem<ActSystem>();
}
// Might want to move this down and have a more standardized method of revival
public void FixAllDamage()
{
Heal();
CurrentState = DamageState.Alive;
}
protected override void DestructionBehavior()
{
_actSystem.HandleBreakage(Owner);
}
}
}

View File

@@ -1,7 +1,5 @@
#nullable enable
using System;
using Content.Server.GameObjects.Components.Construction;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
@@ -10,11 +8,8 @@ using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
public class BreakableConstructionComponent : RuinableComponent
public class BreakableConstructionComponent : Component, IDestroyAct
{
private ActSystem _actSystem = default!;
public override string Name => "BreakableConstruction";
public override void ExposeData(ObjectSerializer serializer)
@@ -24,20 +19,16 @@ namespace Content.Server.GameObjects.Components.Damage
serializer.DataField(this, x => x.Node, "node", string.Empty);
}
public override void Initialize()
{
base.Initialize();
_actSystem = EntitySystem.Get<ActSystem>();
}
public string Node { get; private set; } = string.Empty;
protected override async void DestructionBehavior()
async void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
if (Owner.Deleted || !Owner.TryGetComponent(out ConstructionComponent? construction) || string.IsNullOrEmpty(Node)) return;
_actSystem.HandleBreakage(Owner);
if (Owner.Deleted ||
!Owner.TryGetComponent(out ConstructionComponent? construction) ||
string.IsNullOrEmpty(Node))
{
return;
}
await construction.ChangeNode(Node);
}

View File

@@ -1,101 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Stack;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
/// <summary>
/// When attached to an <see cref="IEntity"/>, allows it to take damage and deletes it after taking enough damage.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
public class DestructibleComponent : RuinableComponent, IDestroyAct
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected ActSystem ActSystem;
/// <inheritdoc />
public override string Name => "Destructible";
/// <summary>
/// Entities spawned on destruction plus the min and max amount spawned.
/// </summary>
public Dictionary<string, MinMax> SpawnOnDestroy { get; private set; }
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
if (SpawnOnDestroy == null || !eventArgs.IsSpawnWreck) return;
foreach (var (key, value) in SpawnOnDestroy)
{
int count;
if (value.Min >= value.Max)
{
count = value.Min;
}
else
{
count = _random.Next(value.Min, value.Max + 1);
}
if (count == 0) continue;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(key))
{
var spawned = Owner.EntityManager.SpawnEntity(key, Owner.Transform.Coordinates);
var stack = spawned.GetComponent<StackComponent>();
stack.Count = count;
spawned.RandomOffset(0.5f);
}
else
{
for (var i = 0; i < count; i++)
{
var spawned = Owner.EntityManager.SpawnEntity(key, Owner.Transform.Coordinates);
spawned.RandomOffset(0.5f);
}
}
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, d => d.SpawnOnDestroy, "spawnOnDestroy", null);
}
public override void Initialize()
{
base.Initialize();
ActSystem = _entitySystemManager.GetEntitySystem<ActSystem>();
}
protected override void DestructionBehavior()
{
if (!Owner.Deleted)
{
var pos = Owner.Transform.Coordinates;
ActSystem.HandleDestruction(Owner,
true); //This will call IDestroyAct.OnDestroy on this component (and all other components on this entity)
}
}
public struct MinMax
{
public int Min;
public int Max;
}
}
}

View File

@@ -1,104 +0,0 @@
using System.Collections.Generic;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Logger = Robust.Shared.Log.Logger;
namespace Content.Server.GameObjects.Components.Damage
{
/// <summary>
/// When attached to an <see cref="IEntity"/>, allows it to take damage and
/// "ruins" or "destroys" it after enough damage is taken.
/// </summary>
[ComponentReference(typeof(IDamageableComponent))]
public abstract class RuinableComponent : DamageableComponent
{
/// <summary>
/// Sound played upon destruction.
/// </summary>
[ViewVariables]
protected string DestroySound { get; private set; }
/// <summary>
/// Used instead of <see cref="DestroySound"/> if specified.
/// </summary>
[ViewVariables]
protected string DestroySoundCollection { get; private set; }
public override List<DamageState> SupportedDamageStates =>
new() {DamageState.Alive, DamageState.Dead};
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction(
"deadThreshold",
100,
t =>
{
if (t == null)
{
return;
}
Thresholds[DamageState.Dead] = t.Value;
},
() => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null);
serializer.DataField(this, ruinable => ruinable.DestroySound, "destroySound", string.Empty);
serializer.DataField(this, ruinable => ruinable.DestroySoundCollection, "destroySoundCollection", string.Empty);
}
protected override void EnterState(DamageState state)
{
base.EnterState(state);
if (state == DamageState.Dead)
{
PerformDestruction();
}
}
/// <summary>
/// Destroys the Owner <see cref="IEntity"/>, setting
/// <see cref="IDamageableComponent.CurrentState"/> to
/// <see cref="Shared.GameObjects.Components.Damage.DamageState.Dead"/>
/// </summary>
protected void PerformDestruction()
{
CurrentState = DamageState.Dead;
if (!Owner.Deleted)
{
var pos = Owner.Transform.Coordinates;
string sound = string.Empty;
if (DestroySoundCollection != string.Empty)
{
sound = AudioHelpers.GetRandomFileFromSoundCollection(DestroySoundCollection);
}
else if (DestroySound != string.Empty)
{
sound = DestroySound;
}
if (sound != string.Empty)
{
Logger.Debug("Playing destruction sound");
EntitySystem.Get<AudioSystem>().PlayAtCoords(sound, pos, AudioHelpers.WithVariation(0.125f));
}
}
DestructionBehavior();
}
protected abstract void DestructionBehavior();
}
}

View File

@@ -0,0 +1,4 @@
namespace Content.Server.GameObjects.Components.Destructible
{
public sealed class ActsFlags { }
}

View File

@@ -0,0 +1,97 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Destructible
{
/// <summary>
/// When attached to an <see cref="IEntity"/>, allows it to take damage
/// and triggers thresholds when reached.
/// </summary>
[RegisterComponent]
public class DestructibleComponent : Component
{
[Dependency] private readonly IRobustRandom _random = default!;
private ActSystem _actSystem = default!;
public override string Name => "Destructible";
[ViewVariables]
private SortedDictionary<int, Threshold> _lowestToHighestThresholds = new();
[ViewVariables] private int PreviousTotalDamage { get; set; }
public IReadOnlyDictionary<int, Threshold> LowestToHighestThresholds => _lowestToHighestThresholds;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction(
"thresholds",
new Dictionary<int, Threshold>(),
thresholds => _lowestToHighestThresholds = new SortedDictionary<int, Threshold>(thresholds),
() => new Dictionary<int, Threshold>(_lowestToHighestThresholds));
}
public override void Initialize()
{
base.Initialize();
_actSystem = EntitySystem.Get<ActSystem>();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
{
if (msg.Damageable.Owner != Owner)
{
break;
}
foreach (var (damage, threshold) in _lowestToHighestThresholds)
{
if (threshold.Triggered)
{
if (threshold.TriggersOnce)
{
continue;
}
if (PreviousTotalDamage >= damage)
{
continue;
}
}
if (msg.Damageable.TotalDamage >= damage)
{
var thresholdMessage = new DestructibleThresholdReachedMessage(this, threshold, msg.Damageable.TotalDamage, damage);
SendMessage(thresholdMessage);
threshold.Trigger(Owner, _random, _actSystem);
}
}
PreviousTotalDamage = msg.Damageable.TotalDamage;
break;
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Destructible
{
public class DestructibleThresholdReachedMessage : ComponentMessage
{
public DestructibleThresholdReachedMessage(DestructibleComponent parent, Threshold threshold, int totalDamage, int thresholdAmount)
{
Parent = parent;
Threshold = threshold;
TotalDamage = totalDamage;
ThresholdAmount = thresholdAmount;
}
public DestructibleComponent Parent { get; }
public Threshold Threshold { get; }
/// <summary>
/// The amount of total damage currently had that triggered this threshold.
/// </summary>
public int TotalDamage { get; }
/// <summary>
/// The amount of damage at which this threshold triggers.
/// </summary>
public int ThresholdAmount { get; }
}
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Destructible
{
public struct MinMax
{
[ViewVariables]
public int Min;
[ViewVariables]
public int Max;
}
}

View File

@@ -0,0 +1,148 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Stack;
using Content.Shared.Audio;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Utility;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Destructible
{
public class Threshold : IExposeData
{
/// <summary>
/// Entities spawned on reaching this threshold, from a min to a max.
/// </summary>
[ViewVariables] public Dictionary<string, MinMax>? Spawn;
/// <summary>
/// Sound played upon destruction.
/// </summary>
[ViewVariables] public string Sound = string.Empty;
/// <summary>
/// Used instead of <see cref="Sound"/> if specified.
/// </summary>
[ViewVariables] public string SoundCollection = string.Empty;
/// <summary>
/// What acts this threshold should trigger upon activation.
/// See <see cref="ActSystem"/>.
/// </summary>
[ViewVariables] public int Acts;
/// <summary>
/// Whether or not this threshold has already been triggered.
/// </summary>
[ViewVariables] public bool Triggered;
/// <summary>
/// Whether or not this threshold only triggers once.
/// If false, it will trigger again once the entity is healed
/// and then damaged to reach this threshold once again.
/// It will not repeatedly trigger as damage rises beyond that.
/// </summary>
[ViewVariables] public bool TriggersOnce;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref Spawn, "Spawn", null);
serializer.DataField(ref Sound, "Sound", string.Empty);
serializer.DataField(ref SoundCollection, "SoundCollection", string.Empty);
serializer.DataField(ref Acts, "Acts", 0, WithFormat.Flags<ActsFlags>());
serializer.DataField(ref Triggered, "Triggered", false);
serializer.DataField(ref TriggersOnce, "TriggersOnce", false);
}
/// <summary>
/// Triggers this threshold.
/// </summary>
/// <param name="owner">The entity that owns this threshold.</param>
/// <param name="random">
/// An instance of <see cref="IRobustRandom"/> to get randomness from, if relevant.
/// </param>
/// <param name="actSystem">
/// An instance of <see cref="ActSystem"/> to call acts on, if relevant.
/// </param>
public void Trigger(IEntity owner, IRobustRandom random, ActSystem actSystem)
{
Triggered = true;
PlaySound(owner);
DoSpawn(owner, random);
DoActs(owner, actSystem);
}
private void PlaySound(IEntity owner)
{
var pos = owner.Transform.Coordinates;
var actualSound = string.Empty;
if (SoundCollection != string.Empty)
{
actualSound = AudioHelpers.GetRandomFileFromSoundCollection(SoundCollection);
}
else if (Sound != string.Empty)
{
actualSound = Sound;
}
if (actualSound != string.Empty)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(actualSound, pos, AudioHelpers.WithVariation(0.125f));
}
}
private void DoSpawn(IEntity owner, IRobustRandom random)
{
if (Spawn == null)
{
return;
}
foreach (var (key, value) in Spawn)
{
var count = value.Min >= value.Max
? value.Min
: random.Next(value.Min, value.Max + 1);
if (count == 0) continue;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(key))
{
var spawned = owner.EntityManager.SpawnEntity(key, owner.Transform.Coordinates);
var stack = spawned.GetComponent<StackComponent>();
stack.Count = count;
spawned.RandomOffset(0.5f);
}
else
{
for (var i = 0; i < count; i++)
{
var spawned = owner.EntityManager.SpawnEntity(key, owner.Transform.Coordinates);
spawned.RandomOffset(0.5f);
}
}
}
}
private void DoActs(IEntity owner, ActSystem acts)
{
if ((Acts & (int) ThresholdActs.Breakage) != 0)
{
acts.HandleBreakage(owner);
}
if ((Acts & (int) ThresholdActs.Destruction) != 0)
{
acts.HandleDestruction(owner);
}
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Destructible
{
[Flags, FlagsFor(typeof(ActsFlags))]
[Serializable]
public enum ThresholdActs
{
Invalid = 0,
Breakage,
Destruction
}
}

View File

@@ -16,6 +16,7 @@ using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
@@ -139,11 +140,10 @@ namespace Content.Server.GameObjects.Components.Disposal
return false;
}
if (!entity.TryGetComponent(out IPhysicsComponent? physics) ||
!physics.CanCollide)
{
if (!(entity.TryGetComponent(out IDamageableComponent? damageState) && damageState.CurrentState == DamageState.Dead)) {
if (!(entity.TryGetComponent(out IMobStateComponent? state) && state.IsDead())) {
return false;
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
@@ -108,8 +109,11 @@ namespace Content.Server.GameObjects.Components.Gravity
return false;
// Repair generator
var breakable = Owner.GetComponent<BreakableComponent>();
breakable.FixAllDamage();
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
damageable.Heal();
}
_intact = true;
Owner.PopupMessage(eventArgs.User,

View File

@@ -9,6 +9,8 @@ using Content.Server.Mobs;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Medical;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
@@ -163,8 +165,8 @@ namespace Content.Server.GameObjects.Components.Medical
}
var dead =
mind.OwnedEntity.TryGetComponent<IDamageableComponent>(out var damageable) &&
damageable.CurrentState == DamageState.Dead;
mind.OwnedEntity.TryGetComponent<IMobStateComponent>(out var state) &&
state.IsDead();
if (!dead) return;

View File

@@ -11,6 +11,7 @@ using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Medical;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components;
@@ -146,14 +147,23 @@ namespace Content.Server.GameObjects.Components.Medical
UserInterface?.SetState(newState);
}
private MedicalScannerStatus GetStatusFromDamageState(DamageState damageState)
private MedicalScannerStatus GetStatusFromDamageState(IMobStateComponent state)
{
switch (damageState)
if (state.IsAlive())
{
case DamageState.Alive: return MedicalScannerStatus.Green;
case DamageState.Critical: return MedicalScannerStatus.Red;
case DamageState.Dead: return MedicalScannerStatus.Death;
default: throw new ArgumentException(nameof(damageState));
return MedicalScannerStatus.Green;
}
else if (state.IsCritical())
{
return MedicalScannerStatus.Red;
}
else if (state.IsDead())
{
return MedicalScannerStatus.Death;
}
else
{
return MedicalScannerStatus.Yellow;
}
}
@@ -162,9 +172,11 @@ namespace Content.Server.GameObjects.Components.Medical
if (Powered)
{
var body = _bodyContainer.ContainedEntity;
return body == null
var state = body?.GetComponentOrNull<IMobStateComponent>();
return state == null
? MedicalScannerStatus.Open
: GetStatusFromDamageState(body.GetComponent<IDamageableComponent>().CurrentState);
: GetStatusFromDamageState(state);
}
return MedicalScannerStatus.Off;

View File

@@ -12,6 +12,7 @@ using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.Chemistry;
@@ -355,8 +356,8 @@ namespace Content.Server.GameObjects.Components.Metabolism
/// </param>
public void Update(float frameTime)
{
if (!Owner.TryGetComponent<IDamageableComponent>(out var damageable) ||
damageable.CurrentState == DamageState.Dead)
if (!Owner.TryGetComponent<IMobStateComponent>(out var state) ||
state.IsDead())
{
return;
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Mobs;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects;
@@ -174,8 +175,8 @@ namespace Content.Server.GameObjects.Components.Mobs
}
var dead =
Owner.TryGetComponent<IDamageableComponent>(out var damageable) &&
damageable.CurrentState == DamageState.Dead;
Owner.TryGetComponent<IMobStateComponent>(out var state) &&
state.IsDead();
if (!HasMind)
{

View File

@@ -9,20 +9,17 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State
{
public class CriticalState : SharedCriticalState
public class CriticalMobState : SharedCriticalMobState
{
public override void EnterState(IEntity entity)
{
base.EnterState(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Critical);
}
if (entity.TryGetComponent(out ServerAlertsComponent status))
{
status.ShowAlert(AlertType.HumanCrit); //Todo: combine humancrit-0 and humancrit-1 into a gif and display it
}
if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{
overlay.AddOverlay(SharedOverlayID.GradientCircleMaskOverlay);
@@ -38,12 +35,12 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity)
{
base.ExitState(entity);
if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{
overlay.ClearOverlays();
}
}
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -10,10 +10,12 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State
{
public class DeadState : SharedDeadState
public class DeadMobState : SharedDeadMobState
{
public override void EnterState(IEntity entity)
{
base.EnterState(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Dead);
@@ -44,6 +46,8 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity)
{
base.ExitState(entity);
if (entity.TryGetComponent(out IPhysicsComponent physics))
{
physics.CanCollide = true;
@@ -54,7 +58,5 @@ namespace Content.Server.GameObjects.Components.Mobs.State
overlay.ClearOverlays();
}
}
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -1,71 +1,22 @@
using System.Collections.Generic;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State
{
[RegisterComponent]
[ComponentReference(typeof(SharedMobStateManagerComponent))]
public class MobStateManagerComponent : SharedMobStateManagerComponent
[ComponentReference(typeof(SharedMobStateComponent))]
[ComponentReference(typeof(IMobStateComponent))]
public class MobStateComponent : SharedMobStateComponent
{
private readonly Dictionary<DamageState, IMobState> _behavior = new()
{
{DamageState.Alive, new NormalState()},
{DamageState.Critical, new CriticalState()},
{DamageState.Dead, new DeadState()}
};
private DamageState _currentDamageState;
protected override IReadOnlyDictionary<DamageState, IMobState> Behavior => _behavior;
public override IMobState CurrentMobState { get; protected set; }
public override DamageState CurrentDamageState
{
get => _currentDamageState;
protected set
{
if (_currentDamageState == value)
{
return;
}
if (_currentDamageState != DamageState.Invalid)
{
CurrentMobState.ExitState(Owner);
}
_currentDamageState = value;
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);
Dirty();
}
}
public override void OnRemove()
{
// TODO: Might want to add an OnRemove() to IMobState since those are where these components are being used
base.OnRemove();
if (Owner.TryGetComponent(out ServerAlertsComponent status))
{
status.ClearAlert(AlertType.HumanHealth);
}
if (Owner.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{
overlay.ClearOverlays();
}
}
public override ComponentState GetComponentState()
{
return new MobStateManagerComponentState(CurrentDamageState);
base.OnRemove();
}
}
}

View File

@@ -0,0 +1,56 @@
#nullable enable
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State
{
public class NormalMobState : SharedNormalMobState
{
public override void EnterState(IEntity entity)
{
base.EnterState(entity);
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Alive);
}
}
public override void UpdateState(IEntity entity, int threshold)
{
base.UpdateState(entity, threshold);
if (!entity.TryGetComponent(out IDamageableComponent? damageable))
{
return;
}
if (!entity.TryGetComponent(out ServerAlertsComponent? alerts))
{
return;
}
if (!entity.TryGetComponent(out IMobStateComponent? stateComponent))
{
return;
}
short modifier = 0;
if (stateComponent.TryGetEarliestIncapacitatedState(threshold, out _, out var earliestThreshold))
{
modifier = (short) (damageable.TotalDamage / (earliestThreshold / 7f));
}
alerts.ShowAlert(AlertType.HumanHealth, modifier);
}
}
}

View File

@@ -1,73 +0,0 @@
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State
{
public class NormalState : SharedNormalState
{
public override void EnterState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Alive);
}
UpdateState(entity);
}
public override void ExitState(IEntity entity) { }
public override void UpdateState(IEntity entity)
{
if (!entity.TryGetComponent(out ServerAlertsComponent status))
{
return;
}
if (!entity.TryGetComponent(out IDamageableComponent damageable))
{
status.ShowAlert(AlertType.HumanHealth, 0);
return;
}
// TODO
switch (damageable)
{
case RuinableComponent ruinable:
{
if (!ruinable.Thresholds.TryGetValue(DamageState.Dead, out var threshold))
{
return;
}
var modifier = (short) (ruinable.TotalDamage / (threshold / 7f));
status.ShowAlert(AlertType.HumanHealth, modifier);
break;
}
default:
{
if (!damageable.Thresholds.TryGetValue(DamageState.Critical, out var threshold))
{
return;
}
var modifier = (short) (damageable.TotalDamage / (threshold / 7f));
status.ShowAlert(AlertType.HumanHealth, modifier);
break;
}
}
}
}
}

View File

@@ -1,9 +1,9 @@
#nullable enable
using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.EntitySystems.AI;
using Content.Server.Interfaces.GameTicking;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.Roles;
using Robust.Server.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
@@ -38,7 +38,7 @@ namespace Content.Server.GameObjects.Components.Movement
}
}
public AiLogicProcessor? Processor { get; set; }
public UtilityAi? Processor { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public string? StartingGearPrototype { get; set; }

View File

@@ -4,6 +4,7 @@ using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
@@ -185,15 +186,19 @@ namespace Content.Server.GameObjects.Components.Nutrition
HungerThresholdEffect();
Dirty();
}
if (_currentHungerThreshold == HungerThreshold.Dead)
if (_currentHungerThreshold != HungerThreshold.Dead)
return;
if (!Owner.TryGetComponent(out IDamageableComponent damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent mobState))
return;
if (!mobState.IsDead())
{
if (Owner.TryGetComponent(out IDamageableComponent damageable))
{
if (damageable.CurrentState != DamageState.Dead)
{
damageable.ChangeDamage(DamageType.Blunt, 2, true, null);
}
}
damageable.ChangeDamage(DamageType.Blunt, 2, true);
}
}
@@ -209,6 +214,4 @@ namespace Content.Server.GameObjects.Components.Nutrition
return new HungerComponentState(_currentHungerThreshold);
}
}
}

View File

@@ -5,6 +5,7 @@ using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
@@ -183,19 +184,21 @@ namespace Content.Server.GameObjects.Components.Nutrition
Dirty();
}
if (_currentThirstThreshold == ThirstThreshold.Dead)
if (_currentThirstThreshold != ThirstThreshold.Dead)
return;
if (!Owner.TryGetComponent(out IDamageableComponent damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent mobState))
return;
if (!mobState.IsDead())
{
if (Owner.TryGetComponent(out IDamageableComponent damageable))
{
if (damageable.CurrentState != DamageState.Dead)
{
damageable.ChangeDamage(DamageType.Blunt, 2, true, null);
}
}
damageable.ChangeDamage(DamageType.Blunt, 2, true);
}
}
public void ResetThirst()
{
_currentThirstThreshold = ThirstThreshold.Okay;
@@ -208,5 +211,4 @@ namespace Content.Server.GameObjects.Components.Nutrition
return new ThirstComponentState(_currentThirstThreshold);
}
}
}

View File

@@ -7,6 +7,7 @@ using Content.Server.Mobs;
using Content.Server.Mobs.Roles;
using Content.Server.Mobs.Roles.Suspicion;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Suspicion;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects;
@@ -60,8 +61,8 @@ namespace Content.Server.GameObjects.Components.Suspicion
public bool IsDead()
{
return Owner.TryGetComponent(out IDamageableComponent? damageable) &&
damageable.CurrentState == DamageState.Dead;
return Owner.TryGetComponent(out IMobStateComponent? state) &&
state.IsDead();
}
public bool IsInnocent()

View File

@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using Content.Server.Utility;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components;
@@ -10,7 +11,9 @@ using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.Components
@@ -19,57 +22,46 @@ namespace Content.Server.GameObjects.Components
[ComponentReference(typeof(SharedWindowComponent))]
public class WindowComponent : SharedWindowComponent, IExamine, IInteractHand
{
private int? Damage
private int _maxDamage;
public override void ExposeData(ObjectSerializer serializer)
{
get
base.ExposeData(serializer);
serializer.DataField(ref _maxDamage, "maxDamage", 100);
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
if (!Owner.TryGetComponent(out IDamageableComponent damageableComponent)) return null;
return damageableComponent.TotalDamage;
case DamageChangedMessage msg:
{
var current = msg.Damageable.TotalDamage;
UpdateVisuals(current);
break;
}
}
}
private int? MaxDamage
private void UpdateVisuals(int currentDamage)
{
get
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
if (!Owner.TryGetComponent(out IDamageableComponent damageableComponent)) return null;
return damageableComponent.Thresholds[DamageState.Dead];
appearance.SetData(WindowVisuals.Damage, (float) currentDamage / _maxDamage);
}
}
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent(out IDamageableComponent damageableComponent))
{
damageableComponent.HealthChangedEvent += OnDamage;
}
}
private void OnDamage(HealthChangedEventArgs eventArgs)
{
int current = eventArgs.Damageable.TotalDamage;
int max = eventArgs.Damageable.Thresholds[DamageState.Dead];
if (eventArgs.Damageable.CurrentState == DamageState.Dead) return;
UpdateVisuals(current, max);
}
private void UpdateVisuals(int currentDamage, int maxDamage)
{
if (Owner.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(WindowVisuals.Damage, (float) currentDamage / maxDamage);
}
}
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
int? damage = Damage;
int? maxDamage = MaxDamage;
if (damage == null || maxDamage == null) return;
float fraction = ((damage == 0 || maxDamage == 0) ? 0f : (float) damage / maxDamage) ?? 0f;
int level = Math.Min(ContentHelpers.RoundToLevels(fraction, 1, 7), 5);
var damage = Owner.GetComponentOrNull<IDamageableComponent>()?.TotalDamage;
if (damage == null) return;
var fraction = ((damage == 0 || _maxDamage == 0)
? 0f
: (float) damage / _maxDamage);
var level = Math.Min(ContentHelpers.RoundToLevels(fraction, 1, 7), 5);
switch (level)
{
case 0: