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

@@ -144,11 +144,10 @@ namespace Content.Client.GameObjects.Components.Body.Scanner
{
BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Owner.Name)}";
// TODO BODY Make dead not be the destroy threshold for a body part
if (part.Owner.TryGetComponent(out IDamageableComponent? damageable) &&
damageable.TryHealth(DamageState.Critical, out var health))
// TODO BODY Part damage
if (part.Owner.TryGetComponent(out IDamageableComponent? damageable))
{
BodyPartHealth.Text = $"{health.current} / {health.max}";
BodyPartHealth.Text = Loc.GetString("{0} damage", damageable.TotalDamage);
}
MechanismList.Clear();

View File

@@ -22,8 +22,8 @@ namespace Content.Client.GameObjects.Components.Kitchen
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private GrinderMenu _menu;
private Dictionary<int, EntityUid> _chamberVisualContents = new Dictionary<int, EntityUid>();
private Dictionary<int, Solution.ReagentQuantity> _beakerVisualContents = new Dictionary<int, Solution.ReagentQuantity>();
private Dictionary<int, EntityUid> _chamberVisualContents = new();
private Dictionary<int, Solution.ReagentQuantity> _beakerVisualContents = new();
public ReagentGrinderBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { }
protected override void Open()

View File

@@ -0,0 +1,8 @@
using Content.Shared.GameObjects.Components.Mobs.State;
namespace Content.Client.GameObjects.Components.Mobs.State
{
public class CriticalMobState : SharedCriticalMobState
{
}
}

View File

@@ -1,30 +0,0 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
public class CriticalState : SharedCriticalState
{
public override void EnterState(IEntity entity)
{
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Critical);
}
EntitySystem.Get<StandingStateSystem>().Down(entity);
}
public override void ExitState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
}
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -1,5 +1,4 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects;
@@ -9,10 +8,12 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.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);
@@ -28,6 +29,8 @@ namespace Content.Client.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity)
{
base.ExitState(entity);
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out PhysicsComponent physics))
@@ -35,7 +38,5 @@ namespace Content.Client.GameObjects.Components.Mobs.State
physics.CanCollide = true;
}
}
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -0,0 +1,13 @@
#nullable enable
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
[RegisterComponent]
[ComponentReference(typeof(SharedMobStateComponent))]
[ComponentReference(typeof(IMobStateComponent))]
public class MobStateComponent : SharedMobStateComponent
{
}
}

View File

@@ -1,62 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
[RegisterComponent]
[ComponentReference(typeof(SharedMobStateManagerComponent))]
public class MobStateManagerComponent : SharedMobStateManagerComponent
{
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 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 HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not MobStateManagerComponentState state)
{
return;
}
_currentDamageState = state.DamageState;
CurrentMobState?.ExitState(Owner);
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);
}
}
}

View File

@@ -1,25 +1,20 @@
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
public class NormalState : SharedNormalState
public class NormalMobState : SharedNormalMobState
{
public override void EnterState(IEntity entity)
{
base.EnterState(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) { }
}
}

View File

@@ -2,6 +2,7 @@
using Content.Server.GlobalVerbs;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using NUnit.Framework;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
@@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Commands
[TestOf(typeof(RejuvenateVerb))]
public class RejuvenateTest : ContentIntegrationTest
{
private const string PROTOTYPES = @"
private const string Prototypes = @"
- type: entity
name: DamageableDummy
id: DamageableDummy
@@ -23,12 +24,17 @@ namespace Content.IntegrationTests.Tests.Commands
damagePrototype: biologicalDamageContainer
criticalThreshold: 100
deadThreshold: 200
- type: MobState
thresholds:
0: !type:NormalMobState {}
100: !type:CriticalMobState {}
200: !type:DeadMobState {}
";
[Test]
public async Task RejuvenateDeadTest()
{
var options = new ServerIntegrationOptions{ExtraPrototypes = PROTOTYPES};
var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
var server = StartServerDummyTicker(options);
await server.WaitAssertion(() =>
@@ -43,19 +49,30 @@ namespace Content.IntegrationTests.Tests.Commands
// Sanity check
Assert.True(human.TryGetComponent(out IDamageableComponent damageable));
Assert.That(damageable.CurrentState, Is.EqualTo(DamageState.Alive));
Assert.True(human.TryGetComponent(out IMobStateComponent mobState));
Assert.That(mobState.IsAlive, Is.True);
Assert.That(mobState.IsCritical, Is.False);
Assert.That(mobState.IsDead, Is.False);
Assert.That(mobState.IsIncapacitated, Is.False);
// Kill the entity
damageable.ChangeDamage(DamageClass.Brute, 10000000, true);
// Check that it is dead
Assert.That(damageable.CurrentState, Is.EqualTo(DamageState.Dead));
Assert.That(mobState.IsAlive, Is.False);
Assert.That(mobState.IsCritical, Is.False);
Assert.That(mobState.IsDead, Is.True);
Assert.That(mobState.IsIncapacitated, Is.True);
// Rejuvenate them
RejuvenateVerb.PerformRejuvenate(human);
// Check that it is alive and with no damage
Assert.That(damageable.CurrentState, Is.EqualTo(DamageState.Alive));
Assert.That(mobState.IsAlive, Is.True);
Assert.That(mobState.IsCritical, Is.False);
Assert.That(mobState.IsDead, Is.False);
Assert.That(mobState.IsIncapacitated, Is.False);
Assert.That(damageable.TotalDamage, Is.Zero);
});
}

View File

@@ -0,0 +1,294 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Destructible;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Destructible
{
[TestFixture]
[TestOf(typeof(DestructibleComponent))]
[TestOf(typeof(Threshold))]
public class DestructibleTests : ContentIntegrationTest
{
private static readonly string DestructibleEntityId = "DestructibleTestsDestructibleEntity";
private static readonly string Prototypes = $@"
- type: entity
id: {DestructibleEntityId}
name: {DestructibleEntityId}
components:
- type: Damageable
- type: Destructible
thresholds:
20:
TriggersOnce: false
50:
Sound: /Audio/Effects/woodhit.ogg
Spawn:
WoodPlank:
Min: 1
Max: 1
Acts: [""Breakage""]
TriggersOnce: false
- type: TestThresholdListener
";
private class TestThresholdListenerComponent : Component
{
public override string Name => "TestThresholdListener";
public List<DestructibleThresholdReachedMessage> ThresholdsReached { get; } = new();
public override void HandleMessage(ComponentMessage message, IComponent component)
{
base.HandleMessage(message, component);
switch (message)
{
case DestructibleThresholdReachedMessage msg:
ThresholdsReached.Add(msg);
break;
}
}
}
[Test]
public async Task TestThresholdActivation()
{
var server = StartServerDummyTicker(new ServerContentIntegrationOption
{
ExtraPrototypes = Prototypes,
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().Register<TestThresholdListenerComponent>();
}
});
await server.WaitIdleAsync();
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
IEntity sDestructibleEntity = null;
IDamageableComponent sDamageableComponent = null;
DestructibleComponent sDestructibleComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null;
await server.WaitPost(() =>
{
var mapId = new MapId(1);
var coordinates = new MapCoordinates(0, 0, mapId);
sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>();
sDestructibleComponent = sDestructibleEntity.GetComponent<DestructibleComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>();
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
{
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.Zero);
});
await server.WaitAssertion(() =>
{
Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true));
// No thresholds reached yet, the earliest one is at 20 damage
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.Zero);
Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true));
// Only one threshold reached, 20
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
var msg = sThresholdListenerComponent.ThresholdsReached[0];
// Check that it matches the total damage dealt
Assert.That(msg.TotalDamage, Is.EqualTo(20));
var threshold = msg.Threshold;
// Check that it matches the YAML prototype
Assert.That(threshold.Acts, Is.EqualTo(0));
Assert.That(threshold.Sound, Is.Null.Or.Empty);
Assert.That(threshold.Spawn, Is.Null);
Assert.That(threshold.SoundCollection, Is.Null.Or.Empty);
Assert.That(threshold.Triggered, Is.True);
sThresholdListenerComponent.ThresholdsReached.Clear();
Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 30, true));
// Only one threshold reached, 50, since 20 was already reached before
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
msg = sThresholdListenerComponent.ThresholdsReached[0];
// Check that it matches the total damage dealt
Assert.That(msg.TotalDamage, Is.EqualTo(50));
threshold = msg.Threshold;
// Check that it matches the YAML prototype
Assert.That(threshold.Acts, Is.EqualTo((int) ThresholdActs.Breakage));
Assert.That(threshold.Sound, Is.EqualTo("/Audio/Effects/woodhit.ogg"));
Assert.That(threshold.Spawn, Is.Not.Null);
Assert.That(threshold.Spawn.Count, Is.EqualTo(1));
Assert.That(threshold.Spawn.Single().Key, Is.EqualTo("WoodPlank"));
Assert.That(threshold.Spawn.Single().Value.Min, Is.EqualTo(1));
Assert.That(threshold.Spawn.Single().Value.Max, Is.EqualTo(1));
Assert.That(threshold.SoundCollection, Is.Null.Or.Empty);
Assert.That(threshold.Triggered, Is.True);
sThresholdListenerComponent.ThresholdsReached.Clear();
// Damage for 50 again, up to 100 now
Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 50, true));
// No new thresholds reached as even though they don't only trigger once, the entity was not healed below the threshold
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
// Heal the entity for 40 damage, down to 60
sDamageableComponent.ChangeDamage(DamageType.Blunt, -40, true);
// Thresholds don't work backwards
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
// Damage for 10, up to 70
sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true);
// Not enough healing to de-trigger a threshold
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
// Heal by 30, down to 40
sDamageableComponent.ChangeDamage(DamageType.Blunt, -30, true);
// Thresholds don't work backwards
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
// Damage up to 50 again
sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true);
// The 50 threshold should have triggered again, after being healed
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
msg = sThresholdListenerComponent.ThresholdsReached[0];
// Check that it matches the total damage dealt
Assert.That(msg.TotalDamage, Is.EqualTo(50));
threshold = msg.Threshold;
// Check that it matches the YAML prototype
Assert.That(threshold.Acts, Is.EqualTo((int) ThresholdActs.Breakage));
Assert.That(threshold.Sound, Is.EqualTo("/Audio/Effects/woodhit.ogg"));
Assert.That(threshold.Spawn, Is.Not.Null);
Assert.That(threshold.Spawn.Count, Is.EqualTo(1));
Assert.That(threshold.Spawn.Single().Key, Is.EqualTo("WoodPlank"));
Assert.That(threshold.Spawn.Single().Value.Min, Is.EqualTo(1));
Assert.That(threshold.Spawn.Single().Value.Max, Is.EqualTo(1));
Assert.That(threshold.SoundCollection, Is.Null.Or.Empty);
Assert.That(threshold.Triggered, Is.True);
// Reset thresholds reached
sThresholdListenerComponent.ThresholdsReached.Clear();
// Heal all damage
sDamageableComponent.Heal();
// Damage up to 50
sDamageableComponent.ChangeDamage(DamageType.Blunt, 50, true);
// Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// Both thresholds should have triggered
Assert.That(sThresholdListenerComponent.ThresholdsReached, Has.Exactly(2).Items);
// Verify the first one, should be the lowest one (20)
msg = sThresholdListenerComponent.ThresholdsReached[0];
Assert.That(msg.ThresholdAmount, Is.EqualTo(20));
// The total damage should be 50
Assert.That(msg.TotalDamage, Is.EqualTo(50));
threshold = msg.Threshold;
// Check that it matches the YAML prototype
Assert.That(threshold.Acts, Is.EqualTo(0));
Assert.That(threshold.Sound, Is.Null.Or.Empty);
Assert.That(threshold.Spawn, Is.Null);
Assert.That(threshold.SoundCollection, Is.Null.Or.Empty);
Assert.That(threshold.Triggered, Is.True);
// Verify the second one, should be the highest one (50)
msg = sThresholdListenerComponent.ThresholdsReached[1];
Assert.That(msg.ThresholdAmount, Is.EqualTo(50));
// Check that it matches the total damage dealt
Assert.That(msg.TotalDamage, Is.EqualTo(50));
threshold = msg.Threshold;
// Check that it matches the YAML prototype
Assert.That(threshold.Acts, Is.EqualTo((int) ThresholdActs.Breakage));
Assert.That(threshold.Sound, Is.EqualTo("/Audio/Effects/woodhit.ogg"));
Assert.That(threshold.Spawn, Is.Not.Null);
Assert.That(threshold.Spawn.Count, Is.EqualTo(1));
Assert.That(threshold.Spawn.Single().Key, Is.EqualTo("WoodPlank"));
Assert.That(threshold.Spawn.Single().Value.Min, Is.EqualTo(1));
Assert.That(threshold.Spawn.Single().Value.Max, Is.EqualTo(1));
Assert.That(threshold.SoundCollection, Is.Null.Or.Empty);
Assert.That(threshold.Triggered, Is.True);
// Reset thresholds reached
sThresholdListenerComponent.ThresholdsReached.Clear();
// Heal the entity completely
sDamageableComponent.Heal();
// Check that the entity has 0 damage
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
// Set both thresholds to only trigger once
foreach (var destructibleThreshold in sDestructibleComponent.LowestToHighestThresholds.Values)
{
destructibleThreshold.TriggersOnce = true;
}
// Damage the entity up to 50 damage again
sDamageableComponent.ChangeDamage(DamageType.Blunt, 50, true);
// Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// No thresholds should have triggered as they were already triggered before, and they are set to only trigger once
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
// Set both thresholds to trigger multiple times
foreach (var destructibleThreshold in sDestructibleComponent.LowestToHighestThresholds.Values)
{
destructibleThreshold.TriggersOnce = false;
}
// Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// They shouldn't have been triggered by changing TriggersOnce
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
});
}
}
}

View File

@@ -10,6 +10,7 @@ using Content.Server.GameObjects.EntitySystems.AI;
using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
@@ -130,28 +131,18 @@ namespace Content.Server.AI.Utility.AiLogic
_planCooldownRemaining = PlanCooldown;
_blackboard = new Blackboard(SelfEntity);
_planner = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiActionSystem>();
if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
damageableComponent.HealthChangedEvent += DeathHandle;
}
}
public override void Shutdown()
{
// TODO: If DamageableComponent removed still need to unsubscribe?
if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
damageableComponent.HealthChangedEvent -= DeathHandle;
}
var currentOp = CurrentAction?.ActionOperators.Peek();
currentOp?.Shutdown(Outcome.Failed);
}
private void DeathHandle(HealthChangedEventArgs eventArgs)
public void MobStateChanged(MobStateChangedMessage message)
{
var oldDeadState = _isDead;
_isDead = eventArgs.Damageable.CurrentState == DamageState.Dead || eventArgs.Damageable.CurrentState == DamageState.Critical;
_isDead = message.Component.IsIncapacitated();
if (oldDeadState != _isDead)
{

View File

@@ -1,6 +1,7 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,12 +11,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
if (target == null || !target.TryGetComponent(out IMobStateComponent mobState))
{
return 0.0f;
}
if (damageableComponent.CurrentState == DamageState.Critical)
if (mobState.IsCritical())
{
return 1.0f;
}

View File

@@ -1,6 +1,7 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,12 +11,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
if (target == null || !target.TryGetComponent(out IMobStateComponent mobState))
{
return 0.0f;
}
if (damageableComponent.CurrentState == DamageState.Dead)
if (mobState.IsDead())
{
return 1.0f;
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration;
#nullable enable
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.Interfaces.GameTicking;
@@ -6,6 +7,7 @@ using Content.Server.Players;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
@@ -21,25 +23,25 @@ namespace Content.Server.Commands.Observer
public string Help => "ghost";
public bool CanReturn { get; set; } = true;
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText((IPlayerSession) null, "Nah");
shell.SendText(player, "Nah");
return;
}
var mind = player.ContentData().Mind;
var mind = player.ContentData()?.Mind;
if (mind == null)
{
shell.SendText(player, "You can't ghost here!");
return;
}
var canReturn = player.AttachedEntity != null && CanReturn;
var name = player.AttachedEntity?.Name ?? player.Name;
var playerEntity = player.AttachedEntity;
if (player.AttachedEntity != null && player.AttachedEntity.HasComponent<GhostComponent>())
if (playerEntity != null && playerEntity.HasComponent<GhostComponent>())
return;
if (mind.VisitingEntity != null)
@@ -48,22 +50,28 @@ namespace Content.Server.Commands.Observer
mind.VisitingEntity.Delete();
}
var position = player.AttachedEntity?.Transform.Coordinates ?? IoCManager.Resolve<IGameTicker>().GetObserverSpawnPoint();
var position = playerEntity?.Transform.Coordinates ?? IoCManager.Resolve<IGameTicker>().GetObserverSpawnPoint();
var canReturn = false;
if (canReturn && player.AttachedEntity.TryGetComponent(out IDamageableComponent damageable))
if (playerEntity != null && CanReturn && playerEntity.TryGetComponent(out IMobStateComponent? mobState))
{
switch (damageable.CurrentState)
if (mobState.IsDead())
{
case DamageState.Dead:
canReturn = true;
break;
case DamageState.Critical:
}
else if (mobState.IsCritical())
{
canReturn = true;
damageable.ChangeDamage(DamageType.Asphyxiation, 100, true, null); //todo: what if they dont breathe lol
break;
default:
if (playerEntity.TryGetComponent(out IDamageableComponent? damageable))
{
//todo: what if they dont breathe lol
damageable.ChangeDamage(DamageType.Asphyxiation, 100, true);
}
}
else
{
canReturn = false;
break;
}
}
@@ -74,9 +82,10 @@ namespace Content.Server.Commands.Observer
var ghostComponent = ghost.GetComponent<GhostComponent>();
ghostComponent.CanReturnToBody = canReturn;
if (player.AttachedEntity.TryGetComponent(out ServerOverlayEffectsComponent overlayComponent))
if (playerEntity != null &&
playerEntity.TryGetComponent(out ServerOverlayEffectsComponent? overlayComponent))
{
overlayComponent?.RemoveOverlay(SharedOverlayID.CircleMaskOverlay);
overlayComponent.RemoveOverlay(SharedOverlayID.CircleMaskOverlay);
}
if (canReturn)

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,18 +184,20 @@ namespace Content.Server.GameObjects.Components.Nutrition
Dirty();
}
if (_currentThirstThreshold == ThirstThreshold.Dead)
{
if (Owner.TryGetComponent(out IDamageableComponent damageable))
{
if (damageable.CurrentState != DamageState.Dead)
{
damageable.ChangeDamage(DamageType.Blunt, 2, true, null);
}
}
}
}
if (_currentThirstThreshold != ThirstThreshold.Dead)
return;
if (!Owner.TryGetComponent(out IDamageableComponent damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent mobState))
return;
if (!mobState.IsDead())
{
damageable.ChangeDamage(DamageType.Blunt, 2, true);
}
}
public void ResetThirst()
{
@@ -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)
{
if (!Owner.TryGetComponent(out IDamageableComponent damageableComponent)) return null;
return damageableComponent.TotalDamage;
base.HandleMessage(message, component);
switch (message)
{
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:

View File

@@ -1,8 +1,10 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.AiLogic;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared;
using Content.Shared.Administration;
using Content.Shared.GameObjects.Components.Movement;
@@ -38,6 +40,8 @@ namespace Content.Server.GameObjects.EntitySystems.AI
// To avoid modifying awakeAi while iterating over it.
private readonly List<SleepAiMessage> _queuedSleepMessages = new();
private readonly List<MobStateChangedMessage> _queuedMobStateMessages = new();
public bool IsAwake(AiLogicProcessor processor) => _awakeAi.Contains(processor);
/// <inheritdoc />
@@ -45,8 +49,9 @@ namespace Content.Server.GameObjects.EntitySystems.AI
{
base.Initialize();
SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep);
SubscribeLocalEvent<MobStateChangedMessage>(MobStateChanged);
var processors = _reflectionManager.GetAllChildren<AiLogicProcessor>();
var processors = _reflectionManager.GetAllChildren<UtilityAi>();
foreach (var processor in processors)
{
var att = (AiLogicProcessorAttribute) Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute))!;
@@ -63,6 +68,18 @@ namespace Content.Server.GameObjects.EntitySystems.AI
if (cvarMaxUpdates <= 0)
return;
foreach (var message in _queuedMobStateMessages)
{
if (!message.Entity.TryGetComponent(out AiControllerComponent? controller))
{
continue;
}
controller.Processor?.MobStateChanged(message);
}
_queuedMobStateMessages.Clear();
foreach (var message in _queuedSleepMessages)
{
switch (message.Sleep)
@@ -118,6 +135,16 @@ namespace Content.Server.GameObjects.EntitySystems.AI
_queuedSleepMessages.Add(message);
}
private void MobStateChanged(MobStateChangedMessage message)
{
if (!message.Entity.HasComponent<AiControllerComponent>())
{
return;
}
_queuedMobStateMessages.Add(message);
}
/// <summary>
/// Will start up the controller's processor if not already done so.
/// Also add them to the awakeAi for updates.
@@ -132,11 +159,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI
_awakeAi.Add(controller.Processor);
}
private AiLogicProcessor CreateProcessor(string name)
private UtilityAi CreateProcessor(string name)
{
if (_processorTypes.TryGetValue(name, out var type))
{
return (AiLogicProcessor)_typeFactory.CreateInstance(type);
return (UtilityAi)_typeFactory.CreateInstance(type);
}
// processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name

View File

@@ -63,7 +63,7 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
AsTask = Tcs.Task;
}
public void HandleDamage(HealthChangedEventArgs args)
public void HandleDamage(DamageChangedEventArgs args)
{
_tookDamage = true;
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameTicking;
using Content.Shared;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
@@ -36,7 +37,7 @@ namespace Content.Server.GameTicking.GameRules
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("The game is now a death match. Kill everybody else to win!"));
_entityManager.EventBus.SubscribeEvent<HealthChangedEventArgs>(EventSource.Local, this, OnHealthChanged);
_entityManager.EventBus.SubscribeEvent<DamageChangedEventArgs>(EventSource.Local, this, OnHealthChanged);
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
@@ -44,11 +45,11 @@ namespace Content.Server.GameTicking.GameRules
{
base.Removed();
_entityManager.EventBus.UnsubscribeEvent<HealthChangedEventArgs>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<DamageChangedEventArgs>(EventSource.Local, this);
_playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
}
private void OnHealthChanged(HealthChangedEventArgs message)
private void OnHealthChanged(DamageChangedEventArgs message)
{
_runDelayedCheck();
}
@@ -63,13 +64,14 @@ namespace Content.Server.GameTicking.GameRules
IPlayerSession winner = null;
foreach (var playerSession in _playerManager.GetAllPlayers())
{
if (playerSession.AttachedEntity == null
|| !playerSession.AttachedEntity.TryGetComponent(out IDamageableComponent damageable))
var playerEntity = playerSession.AttachedEntity;
if (playerEntity == null
|| !playerEntity.TryGetComponent(out IMobStateComponent state))
{
continue;
}
if (damageable.CurrentState != DamageState.Alive)
if (!state.IsAlive())
{
continue;
}

View File

@@ -8,6 +8,7 @@ using Content.Server.Mobs.Roles.Suspicion;
using Content.Server.Players;
using Content.Shared;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Audio;
@@ -111,13 +112,13 @@ namespace Content.Server.GameTicking.GameRules
foreach (var playerSession in _playerManager.GetAllPlayers())
{
if (playerSession.AttachedEntity == null
|| !playerSession.AttachedEntity.TryGetComponent(out IDamageableComponent damageable)
|| !playerSession.AttachedEntity.TryGetComponent(out IMobStateComponent mobState)
|| !playerSession.AttachedEntity.HasComponent<SuspicionRoleComponent>())
{
continue;
}
if (damageable.CurrentState != DamageState.Alive)
if (!mobState.IsAlive())
{
continue;
}

View File

@@ -2,6 +2,7 @@
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Nutrition;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Verbs;
using Robust.Server.Console;
using Robust.Server.Interfaces.GameObjects;
@@ -58,7 +59,11 @@ namespace Content.Server.GlobalVerbs
if (target.TryGetComponent(out IDamageableComponent damage))
{
damage.Heal();
damage.CurrentState = DamageState.Alive;
}
if (target.TryGetComponent(out IMobStateComponent mobState))
{
mobState.UpdateState(0);
}
if (target.TryGetComponent(out HungerComponent hunger))

View File

@@ -1,7 +1,7 @@
#nullable enable
using Content.Server.Mobs;
using Content.Server.Objectives.Interfaces;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using JetBrains.Annotations;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
@@ -24,9 +24,10 @@ namespace Content.Server.Objectives.Conditions
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResourcePath("Mobs/Ghosts/ghost_human.rsi"), "icon");
public float Progress => _mind?.OwnedEntity != null &&
_mind.OwnedEntity.TryGetComponent<IDamageableComponent>(out var damageableComponent) &&
damageableComponent.CurrentState != DamageState.Dead
public float Progress => _mind?
.OwnedEntity?
.GetComponentOrNull<IMobStateComponent>()?
.IsDead() ?? false
? 0f
: 1f;

View File

@@ -1,7 +1,7 @@
#nullable enable
using Content.Server.Mobs;
using Content.Server.Objectives.Interfaces;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
@@ -18,10 +18,10 @@ namespace Content.Server.Objectives.Conditions
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResourcePath("Objects/Weapons/Guns/Pistols/mk58_wood.rsi"), "icon");
public float Progress => Target?.OwnedEntity != null &&
Target.OwnedEntity
.TryGetComponent<IDamageableComponent>(out var damageableComponent) &&
damageableComponent.CurrentState == DamageState.Dead
public float Progress => Target?
.OwnedEntity?
.GetComponentOrNull<IMobStateComponent>()?
.IsDead() ?? false
? 1f
: 0f;

View File

@@ -2,7 +2,7 @@
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.Mobs;
using Content.Server.Objectives.Interfaces;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
@@ -20,10 +20,7 @@ namespace Content.Server.Objectives.Conditions
var allHumans = entityMgr.ComponentManager.EntityQuery<MindComponent>().Where(mc =>
{
var entity = mc.Mind?.OwnedEntity;
return entity != null &&
entity.TryGetComponent<IDamageableComponent>(out var damageableComponent) &&
damageableComponent.CurrentState == DamageState.Alive
&& mc.Mind != mind;
return (entity?.GetComponentOrNull<IMobStateComponent>()?.IsAlive() ?? false) && mc.Mind != mind;
}).Select(mc => mc.Mind).ToList();
return new KillRandomPersonCondition {Target = IoCManager.Resolve<IRobustRandom>().Pick(allHumans)};
}

View File

@@ -1,7 +1,7 @@
#nullable enable
using Content.Server.Mobs;
using Content.Server.Objectives.Interfaces;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using JetBrains.Annotations;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
@@ -24,9 +24,10 @@ namespace Content.Server.Objectives.Conditions
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResourcePath("Objects/Misc/skub.rsi"), "icon"); //didn't know what else would have been a good icon for staying alive
public float Progress => _mind?.OwnedEntity != null &&
_mind.OwnedEntity.TryGetComponent<IDamageableComponent>(out var damageableComponent) &&
damageableComponent.CurrentState == DamageState.Dead
public float Progress => _mind?
.OwnedEntity?
.GetComponentOrNull<IMobStateComponent>()?
.IsDead() ?? false
? 0f
: 1f;

View File

@@ -5,6 +5,7 @@ using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Audio;
@@ -40,17 +41,23 @@ namespace Content.Server.StationEvents
var playerManager = IoCManager.Resolve<IPlayerManager>();
foreach (var player in playerManager.GetAllPlayers())
{
if (player.AttachedEntity == null || !player.AttachedEntity.TryGetComponent(out InventoryComponent? inventory)) return;
var playerEntity = player.AttachedEntity;
if (playerEntity == null || !playerEntity.TryGetComponent(out InventoryComponent? inventory)) return;
if (inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.BELT, out ItemComponent? item)
&& item?.Owner.Prototype?.ID == "UtilityBeltClothingFilledEvent") return;
if (player.AttachedEntity.TryGetComponent<IDamageableComponent>(out var damageable)
&& damageable.CurrentState == DamageState.Dead) return;
if (playerEntity.TryGetComponent(out IDamageableComponent? damageable) &&
playerEntity.TryGetComponent(out IMobStateComponent? mobState) &&
mobState.IsDead())
{
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var playerPos = player.AttachedEntity.Transform.Coordinates;
var playerPos = playerEntity.Transform.Coordinates;
entityManager.SpawnEntity("UtilityBeltClothingFilledEvent", playerPos);
}
}
public override void Shutdown()
{
base.Shutdown();
@@ -59,6 +66,7 @@ namespace Content.Server.StationEvents
var componentManager = IoCManager.Resolve<IComponentManager>();
foreach (var component in componentManager.EntityQuery<AirlockComponent>()) component.BoltsDown = false;
}
public override void Update(float frameTime)
{
if (!Running)

View File

@@ -18,6 +18,7 @@ namespace Content.Shared.Damage
public static class DamageClassExtensions
{
// TODO DAMAGE This but not hardcoded
private static readonly ImmutableDictionary<DamageClass, List<DamageType>> ClassToType =
new Dictionary<DamageClass, List<DamageType>>
{

View File

@@ -1,285 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.DamageContainer
{
/// <summary>
/// Holds the information regarding the various forms of damage an object has
/// taken (i.e. brute, burn, or toxic damage).
/// </summary>
[Serializable, NetSerializable]
public class DamageContainer
{
private Dictionary<DamageType, int> _damageList = DamageTypeExtensions.ToDictionary();
public delegate void HealthChangedDelegate(List<HealthChangeData> changes);
[NonSerialized] public readonly HealthChangedDelegate OnHealthChanged;
public DamageContainer(HealthChangedDelegate onHealthChanged, DamageContainerPrototype data)
{
ID = data.ID;
OnHealthChanged = onHealthChanged;
SupportedClasses = data.ActiveDamageClasses;
}
public string ID { get; }
[ViewVariables] public virtual List<DamageClass> SupportedClasses { get; }
[ViewVariables]
public virtual List<DamageType> SupportedTypes
{
get
{
var toReturn = new List<DamageType>();
foreach (var @class in SupportedClasses)
{
toReturn.AddRange(@class.ToTypes());
}
return toReturn;
}
}
/// <summary>
/// Sum of all damages kept on record.
/// </summary>
[ViewVariables]
public int TotalDamage => _damageList.Values.Sum();
public IReadOnlyDictionary<DamageClass, int> DamageClasses =>
DamageTypeExtensions.ToClassDictionary(DamageTypes);
public IReadOnlyDictionary<DamageType, int> DamageTypes => _damageList;
public bool SupportsDamageClass(DamageClass @class)
{
return SupportedClasses.Contains(@class);
}
public bool SupportsDamageType(DamageType type)
{
return SupportedClasses.Contains(type.ToClass());
}
/// <summary>
/// Attempts to grab the damage value for the given <see cref="DamageType"/>.
/// </summary>
/// <returns>
/// False if the container does not support that type, true otherwise.
/// </returns>
public bool TryGetDamageValue(DamageType type, [NotNullWhen(true)] out int damage)
{
return _damageList.TryGetValue(type, out damage);
}
/// <summary>
/// Grabs the damage value for the given <see cref="DamageType"/>.
/// </summary>
public int GetDamageValue(DamageType type)
{
return TryGetDamageValue(type, out var damage) ? damage : 0;
}
/// <summary>
/// Attempts to grab the sum of damage values for the given
/// <see cref="DamageClasses"/>.
/// </summary>
/// <param name="class">The class to get the sum for.</param>
/// <param name="damage">The resulting amount of damage, if any.</param>
/// <returns>
/// True if the class is supported in this container, false otherwise.
/// </returns>
public bool TryGetDamageClassSum(DamageClass @class, [NotNullWhen(true)] out int damage)
{
damage = 0;
if (SupportsDamageClass(@class))
{
foreach (var type in @class.ToTypes())
{
damage += GetDamageValue(type);
}
return true;
}
return false;
}
/// <summary>
/// Grabs the sum of damage values for the given <see cref="DamageClasses"/>.
/// </summary>
public int GetDamageClassSum(DamageClass damageClass)
{
var sum = 0;
foreach (var type in damageClass.ToTypes())
{
sum += GetDamageValue(type);
}
return sum;
}
/// <summary>
/// Attempts to change 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 TryChangeDamageValue(DamageType type, int delta)
{
var damageClass = type.ToClass();
if (SupportsDamageClass(damageClass))
{
var current = _damageList[type];
current = _damageList[type] = current + delta;
if (_damageList[type] < 0)
{
_damageList[type] = 0;
delta = -current;
current = 0;
}
var datum = new HealthChangeData(type, current, delta);
var data = new List<HealthChangeData> {datum};
OnHealthChanged(data);
return true;
}
return false;
}
/// <summary>
/// Changes the damage value for the given <see cref="DamageType"/>.
/// </summary>
/// <param name="type">The type of damage to change.</param>
/// <param name="delta">The amount to change it by.</param>
/// <param name="quiet">
/// Whether or not to suppress the health change event.
/// </param>
/// <returns>
/// True if successful, false if this container does not support that type.
/// </returns>
public bool ChangeDamageValue(DamageType type, int delta, bool quiet = false)
{
if (!_damageList.TryGetValue(type, out var current))
{
return false;
}
_damageList[type] = current + delta;
if (_damageList[type] < 0)
{
_damageList[type] = 0;
delta = -current;
}
current = _damageList[type];
var datum = new HealthChangeData(type, current, delta);
var data = new List<HealthChangeData> {datum};
OnHealthChanged(data);
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 TrySetDamageValue(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 HealthChangeData(type, newValue, delta);
var data = new List<HealthChangeData> {datum};
OnHealthChanged(data);
return true;
}
return false;
}
/// <summary>
/// Tries to set the damage value for the given <see cref="DamageType"/>.
/// </summary>
/// <param name="type">The type of damage to set.</param>
/// <param name="newValue">The value to set it to.</param>
/// <param name="quiet">
/// Whether or not to suppress the health changed event.
/// </param>
/// <returns>True if successful, false otherwise.</returns>
public bool SetDamageValue(DamageType type, int newValue, bool quiet = false)
{
if (newValue < 0)
{
return false;
}
if (!_damageList.ContainsKey(type))
{
return false;
}
var old = _damageList[type];
_damageList[type] = newValue;
if (!quiet)
{
var delta = newValue - old;
var datum = new HealthChangeData(type, 0, delta);
var data = new List<HealthChangeData> {datum};
OnHealthChanged(data);
}
return true;
}
public void Heal()
{
var data = new List<HealthChangeData>();
foreach (var type in SupportedTypes)
{
var delta = -GetDamageValue(type);
var datum = new HealthChangeData(type, 0, delta);
data.Add(datum);
SetDamageValue(type, 0, true);
}
OnHealthChanged(data);
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -14,18 +15,37 @@ namespace Content.Shared.Damage.DamageContainer
[Serializable, NetSerializable]
public class DamageContainerPrototype : IPrototype, IIndexedPrototype
{
private List<DamageClass> _activeDamageClasses;
private HashSet<DamageClass> _supportedClasses;
private HashSet<DamageType> _supportedTypes;
private string _id;
[ViewVariables] public List<DamageClass> ActiveDamageClasses => _activeDamageClasses;
// TODO NET 5 IReadOnlySet
[ViewVariables] public IReadOnlyCollection<DamageClass> SupportedClasses => _supportedClasses;
[ViewVariables] public IReadOnlyCollection<DamageType> SupportedTypes => _supportedTypes;
[ViewVariables] public string ID => _id;
public virtual void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataField(ref _id, "id", string.Empty);
serializer.DataField(ref _activeDamageClasses, "activeDamageClasses", new List<DamageClass>());
serializer.DataField(ref _supportedClasses, "supportedClasses", new HashSet<DamageClass>());
serializer.DataField(ref _supportedTypes, "supportedTypes", new HashSet<DamageType>());
var originalTypes = _supportedTypes.ToArray();
foreach (var supportedClass in _supportedClasses)
foreach (var supportedType in supportedClass.ToTypes())
{
_supportedTypes.Add(supportedType);
}
foreach (var originalType in originalTypes)
{
_supportedClasses.Add(originalType.ToClass());
}
}
}
}

View File

@@ -38,8 +38,7 @@ namespace Content.Shared.Damage
{DamageType.Radiation, DamageClass.Toxin},
{DamageType.Asphyxiation, DamageClass.Airloss},
{DamageType.Bloodloss, DamageClass.Airloss},
{DamageType.Cellular, DamageClass.Genetic }
{DamageType.Cellular, DamageClass.Genetic}
}.ToImmutableDictionary();
public static DamageClass ToClass(this DamageType type)

View File

@@ -6,7 +6,9 @@ using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.ResistanceSet
{
/// <summary>
/// Set of resistances used by damageable objects. Each DamageType has a multiplier and flat damage reduction value.
/// Set of resistances used by damageable objects.
/// Each <see cref="DamageType"/> has a multiplier and flat damage
/// reduction value.
/// </summary>
[NetSerializable]
[Serializable]
@@ -33,14 +35,15 @@ namespace Content.Shared.Damage.ResistanceSet
public string ID { get; }
/// <summary>
/// Adjusts input damage with the resistance set values. Only applies reduction if the amount is damage (positive), not
/// Adjusts input damage with the resistance set values.
/// Only applies reduction if the amount is damage (positive), not
/// healing (negative).
/// </summary>
/// <param name="damageType">Type of damage.</param>
/// <param name="amount">Incoming amount of damage.</param>
public int CalculateDamage(DamageType damageType, int amount)
{
if (amount > 0) //Only apply reduction if it's healing, not damage.
if (amount > 0) // Only apply reduction if it's healing, not damage.
{
amount -= _resistances[damageType].FlatReduction;
@@ -59,8 +62,7 @@ namespace Content.Shared.Damage.ResistanceSet
/// <summary>
/// Settings for a specific damage type in a resistance set. Flat reduction is applied before the coefficient.
/// </summary>
[NetSerializable]
[Serializable]
[Serializable, NetSerializable]
public struct ResistanceSetSettings
{
[ViewVariables] public float Coefficient { get; private set; }

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,29 +168,94 @@ 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,
IEntity? source = null,
HealthChangeParams? extraParams = null)
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (amount > 0 && HasFlag(DamageFlag.Invulnerable))
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;
}
if (Damage.SupportsDamageType(type))
{
var finalDamage = amount;
if (!ignoreResistances)
{
finalDamage = Resistances.CalculateDamage(type, amount);
}
var damageClass = type.ToClass();
Damage.ChangeDamageValue(type, finalDamage);
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;
}
@@ -243,17 +263,80 @@ namespace Content.Shared.GameObjects.Components.Damage
return false;
}
public bool ChangeDamage(DamageClass @class, int amount, bool ignoreResistances,
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.SupportsDamageClass(@class))
if (!SupportsDamageType(type))
{
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,
DamageChangeParams? extraParams = null)
{
if (amount > 0 && HasFlag(DamageFlag.Invulnerable))
{
return false;
}
if (!SupportsDamageClass(@class))
{
return false;
}
var types = @class.ToTypes();
if (amount < 0)
@@ -271,7 +354,7 @@ namespace Content.Shared.GameObjects.Components.Damage
healThisCycle = 0;
int healPerType;
if (healingLeft > -types.Count && healingLeft < 0)
if (healingLeft > -types.Count)
{
// Say we were to distribute 2 healing between 3
// this will distribute 1 to each (and stop after 2 are given)
@@ -287,10 +370,9 @@ namespace Content.Shared.GameObjects.Components.Damage
foreach (var type in types)
{
var healAmount =
Math.Max(Math.Max(healPerType, -Damage.GetDamageValue(type)),
healingLeft);
Math.Max(Math.Max(healPerType, -GetDamage(type)), healingLeft);
Damage.ChangeDamageValue(type, healAmount);
ChangeDamage(type, healAmount, true);
healThisCycle += healAmount;
healingLeft -= healAmount;
}
@@ -305,7 +387,7 @@ namespace Content.Shared.GameObjects.Components.Damage
{
int damagePerType;
if (damageLeft < types.Count && damageLeft > 0)
if (damageLeft < types.Count)
{
damagePerType = 1;
}
@@ -317,7 +399,7 @@ namespace Content.Shared.GameObjects.Components.Damage
foreach (var type in types)
{
var damageAmount = Math.Min(damagePerType, damageLeft);
Damage.ChangeDamageValue(type, damageAmount);
ChangeDamage(type, damageAmount, true);
damageLeft -= damageAmount;
}
}
@@ -325,103 +407,63 @@ namespace Content.Shared.GameObjects.Components.Damage
return true;
}
return false;
}
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 false;
}
if (!_damageList.ContainsKey(type))
{
return false;
}
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;
}
return false;
}
public void Heal()
{
Damage.Heal();
}
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;
}
}
}

View File

@@ -73,7 +73,7 @@
public const uint BATTERY_BARREL = 1067;
public const uint SUSPICION_ROLE = 1068;
public const uint ROTATION = 1069;
public const uint MOB_STATE_MANAGER = 1070;
public const uint MOB_STATE = 1070;
public const uint SLIP = 1071;
public const uint SPACE_VILLAIN_ARCADE = 1072;
public const uint BLOCKGAME_ARCADE = 1073;
@@ -86,6 +86,7 @@
public const uint SINGULARITY = 1080;
public const uint CHARACTERINFO = 1081;
public const uint REAGENT_GRINDER = 1082;
public const uint DAMAGEABLE = 1083;
// Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001;

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Map;
namespace Content.Shared.GameObjects.EntitySystems
{
/// <summary>
/// This interface gives components behavior on getting destoyed.
/// This interface gives components behavior on getting destroyed.
/// </summary>
public interface IDestroyAct
{
@@ -21,7 +21,6 @@ namespace Content.Shared.GameObjects.EntitySystems
public class DestructionEventArgs : EventArgs
{
public IEntity Owner { get; set; }
public bool IsSpawnWreck { get; set; }
}
public class BreakageEventArgs : EventArgs
@@ -55,19 +54,20 @@ namespace Content.Shared.GameObjects.EntitySystems
[UsedImplicitly]
public sealed class ActSystem : EntitySystem
{
public void HandleDestruction(IEntity owner, bool isWreck)
public void HandleDestruction(IEntity owner)
{
var eventArgs = new DestructionEventArgs
{
Owner = owner,
IsSpawnWreck = isWreck
Owner = owner
};
var destroyActs = owner.GetAllComponents<IDestroyAct>().ToList();
foreach (var destroyAct in destroyActs)
{
destroyAct.OnDestroy(eventArgs);
}
owner.Delete();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
# TODO BODY: Part damage
- type: entity
id: PartHuman
name: "human body part"
@@ -30,8 +31,8 @@
# TODO BODY DettachableDamageableComponent?
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 100
deadThreshold: 150
# criticalThreshold: 100
# deadThreshold: 150
- type: entity
id: HeadHuman
@@ -57,8 +58,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 50
deadThreshold: 120
# criticalThreshold: 50
# deadThreshold: 120
- type: entity
id: LeftArmHuman
@@ -81,8 +82,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 40
deadThreshold: 80
# criticalThreshold: 40
# deadThreshold: 80
- type: Extension
distance: 2.4
@@ -107,8 +108,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 40
deadThreshold: 80
# criticalThreshold: 40
# deadThreshold: 80
- type: Extension
distance: 2.4
@@ -133,8 +134,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 30
deadThreshold: 60
# criticalThreshold: 30
# deadThreshold: 60
- type: Grasp
- type: entity
@@ -151,8 +152,6 @@
state: "r_hand"
- type: BodyPart
partType: Hand
durability: 30
destroyThreshold: -60
size: 3
compatibility: Biological
symmetry: Right
@@ -160,8 +159,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 30
deadThreshold: 60
# criticalThreshold: 30
# deadThreshold: 60
- type: Grasp
- type: entity
@@ -185,8 +184,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 45
deadThreshold: 90
# criticalThreshold: 45
# deadThreshold: 90
- type: Leg
speed: 2.6
- type: Extension
@@ -206,8 +205,6 @@
state: "r_leg"
- type: BodyPart
partType: Leg
durability: 45
destroyThreshold: -90
size: 6
compatibility: Biological
symmetry: Right
@@ -215,8 +212,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 45
deadThreshold: 90
# criticalThreshold: 45
# deadThreshold: 90
- type: Leg
speed: 2.6
- type: Extension
@@ -243,8 +240,8 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 30
deadThreshold: 60
# criticalThreshold: 30
# deadThreshold: 60
- type: entity
id: RightFootHuman
@@ -267,5 +264,5 @@
- type: Damageable
damageContainer: biologicalDamageContainer
resistances: defaultResistances
criticalThreshold: 30
deadThreshold: 60
# criticalThreshold: 30
# deadThreshold: 60

View File

@@ -1,6 +1,6 @@
- type: damageContainer
id: biologicalDamageContainer
activeDamageClasses:
supportedClasses:
- Brute
- Burn
- Toxin
@@ -9,6 +9,6 @@
- type: damageContainer
id: metallicDamageContainer
activeDamageClasses:
supportedClasses:
- Brute
- Burn

View File

@@ -56,9 +56,12 @@
- type: Occluder
- type: SnapGrid
offset: Center
- type: Destructible
deadThreshold: 500
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
500:
Acts: ["Destruction"]
placement:
mode: SnapgridCenter

View File

@@ -27,8 +27,11 @@
- type: Strap
position: Down
rotation: -90
- type: Destructible
deadThreshold: 75
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
75:
Acts: ["Destruction"]
placement:
mode: SnapgridCenter

View File

@@ -19,14 +19,17 @@
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Damageable
resistances: metallicResistances
- type: Destructible
deadThreshold: 30
destroySound: /Audio/Effects/woodhit.ogg
spawnOnDestroy:
thresholds:
30:
Sound: /Audio/Effects/woodhit.ogg
Spawn:
WoodPlank:
Min: 1
Max: 1
resistances: metallicResistances
Acts: ["Destruction"]
- type: Occluder
sizeX: 32
sizeY: 32

View File

@@ -1,4 +1,4 @@
 - type: entity
- type: entity
name: baseinstrument
id: BasePlaceableInstrument
abstract: true
@@ -20,15 +20,18 @@
- VaultImpassable
- type: SnapGrid
offset: Center
- type: Destructible
deadThreshold: 50
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
50:
Acts: ["Destruction"]
- type: UserInterface
interfaces:
- key: enum.InstrumentUiKey.Key
type: InstrumentBoundUserInterface
- type: entity
- type: entity
name: piano
parent: BasePlaceableInstrument
id: PianoInstrument
@@ -40,7 +43,7 @@
sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: piano
- type: entity
- type: entity
name: minimoog
parent: BasePlaceableInstrument
id: MinimoogInstrument
@@ -52,7 +55,7 @@
sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: minimoog
- type: entity
- type: entity
name: church organ
parent: BasePlaceableInstrument
id: ChurchOrganInstrument
@@ -64,7 +67,7 @@
sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: church_organ
- type: entity
- type: entity
name: xylophone
parent: BasePlaceableInstrument
id: XylophoneInstrument

View File

@@ -11,9 +11,12 @@
- type: Physics
- type: Clickable
- type: InteractionOutline
- type: Destructible
deadThreshold: 100
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
100:
Acts: ["Destruction"]
- type: ShuttleController
- type: Strap
position: Stand

View File

@@ -30,9 +30,12 @@
position: Stand
- type: Anchorable
- type: Pullable
- type: Destructible
deadThreshold: 50
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
50:
Acts: ["Destruction"]
- type: entity
name: chair

View File

@@ -27,14 +27,17 @@
- VaultImpassable
- type: Pullable
- type: Anchorable
- type: Damageable
resistances: metallicResistances
- type: Destructible
deadThreshold: 30
destroySound: /Audio/Effects/metalbreak.ogg
spawnOnDestroy:
thresholds:
30:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
resistances: metallicResistances
Acts: ["Destruction"]
- type: entity
id: Shelf
@@ -65,11 +68,14 @@
- VaultImpassable
- type: Pullable
- type: Anchorable
- type: Damageable
resistances: metallicResistances
- type: Destructible
deadThreshold: 30
destroySound: /Audio/Effects/metalbreak.ogg
spawnOnDestroy:
thresholds:
30:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
resistances: metallicResistances
Acts: ["Destruction"]

View File

@@ -35,14 +35,17 @@
sprite: Constructible/Structures/Tables/generic.rsi
- type: Icon
sprite: Constructible/Structures/Tables/generic.rsi
- type: Destructible
deadThreshold: 15
destroySound: /Audio/Effects/metalbreak.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
15:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: entity
id: TableFrame
@@ -54,14 +57,17 @@
sprite: Constructible/Structures/Tables/frame.rsi
- type: Icon
sprite: Constructible/Structures/Tables/frame.rsi
- type: Destructible
deadThreshold: 1
destroySound: /Audio/Effects/metalbreak.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
1:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: TableFrame
@@ -76,14 +82,17 @@
sprite: Constructible/Structures/Tables/bar.rsi
- type: Icon
sprite: Constructible/Structures/Tables/bar.rsi
- type: Destructible
deadThreshold: 1
destroySound: /Audio/Effects/metalbreak.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
1:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: entity
id: TableMetal
@@ -95,14 +104,17 @@
sprite: Constructible/Structures/Tables/metal.rsi
- type: Icon
sprite: Constructible/Structures/Tables/metal.rsi
- type: Destructible
deadThreshold: 15
destroySound: /Audio/Effects/metalbreak.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
15:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: MetalTable
@@ -117,14 +129,17 @@
sprite: Constructible/Structures/Tables/reinforced.rsi
- type: Icon
sprite: Constructible/Structures/Tables/reinforced.rsi
- type: Destructible
deadThreshold: 75
destroySound: /Audio/Effects/metalbreak.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
75:
Sound: /Audio/Effects/metalbreak.ogg
Spawn:
SteelSheet1:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: ReinforcedTable
@@ -139,14 +154,17 @@
sprite: Constructible/Structures/Tables/glass.rsi
- type: Icon
sprite: Constructible/Structures/Tables/glass.rsi
- type: Destructible
deadThreshold: 5
destroySound: /Audio/Effects/glass_break2.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
5:
Sound: /Audio/Effects/glass_break2.ogg
Spawn:
ShardGlass:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: GlassTable
@@ -161,14 +179,17 @@
sprite: Constructible/Structures/Tables/r_glass.rsi
- type: Icon
sprite: Constructible/Structures/Tables/r_glass.rsi
- type: Destructible
deadThreshold: 20
destroySound: /Audio/Effects/glass_break2.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
20:
Sound: /Audio/Effects/glass_break2.ogg
Spawn:
ShardGlass:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: RGlassTable
@@ -183,14 +204,17 @@
sprite: Constructible/Structures/Tables/wood.rsi
- type: Icon
sprite: Constructible/Structures/Tables/wood.rsi
- type: Destructible
deadThreshold: 15
destroySound: /Audio/Effects/woodhit.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
15:
Sound: /Audio/Effects/woodhit.ogg
Spawn:
WoodPlank:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: WoodTable
@@ -205,14 +229,17 @@
sprite: Constructible/Structures/Tables/carpet.rsi
- type: Icon
sprite: Constructible/Structures/Tables/carpet.rsi
- type: Destructible
deadThreshold: 15
destroySound: /Audio/Effects/woodhit.ogg
- type: Damageable
resistances: metallicResistances
spawnOnDestroy:
- type: Destructible
thresholds:
15:
Sound: /Audio/Effects/woodhit.ogg
Spawn:
WoodPlank:
Min: 1
Max: 1
Acts: ["Destruction"]
- type: Construction
graph: Tables
node: PokerTable
@@ -227,10 +254,13 @@
sprite: Constructible/Structures/Tables/stone.rsi
- type: Icon
sprite: Constructible/Structures/Tables/stone.rsi
- type: Destructible
deadThreshold: 50
destroySound: /Audio/Effects/picaxe2.ogg
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
50:
Sound: /Audio/Effects/picaxe2.ogg
Acts: ["Destruction"]
- type: entity
id: TableDebug
@@ -242,6 +272,9 @@
sprite: Constructible/Structures/Tables/debug.rsi
- type: Icon
sprite: Constructible/Structures/Tables/debug.rsi
- type: Destructible
deadThreshold: 1
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
1:
Acts: ["Destruction"]

View File

@@ -10,8 +10,11 @@
- type: SnapGrid
offset: Center
- type: Sprite
- type: Damageable
- type: Destructible
thresholdvalue: 100
thresholds:
100:
Acts: ["Destruction"]
- type: GasCanisterPort
- type: entity

View File

@@ -10,8 +10,11 @@
- type: SnapGrid
offset: Center
- type: Sprite
- type: Damageable
- type: Destructible
thresholdvalue: 100
thresholds:
100:
Acts: ["Destruction"]
- type: GasCanister
- type: Anchorable
- type: Pullable

View File

@@ -20,6 +20,9 @@
state: spike
- type: Anchorable
- type: Pullable
- type: Damageable
- type: Destructible
deadThreshold: 50
thresholds:
50:
Acts: ["Destruction"]
- type: KitchenSpike

View File

@@ -11,8 +11,11 @@
- type: Physics
- type: SnapGrid
offset: Center
- type: Damageable
- type: Destructible
thresholdvalue: 100
thresholds:
100:
Acts: ["Destruction"]
- type: Sprite
- type: Appearance
visuals:

View File

@@ -9,9 +9,12 @@
- type: Physics
- type: SnapGrid
offset: Center
- type: Destructible
thresholdvalue: 100
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
100:
Acts: ["Destruction"]
- type: Sprite
sprite: Constructible/Atmos/pump.rsi
- type: Icon

Some files were not shown because too many files have changed in this diff Show More