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)}"; BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Owner.Name)}";
// TODO BODY Make dead not be the destroy threshold for a body part // TODO BODY Part damage
if (part.Owner.TryGetComponent(out IDamageableComponent? damageable) && if (part.Owner.TryGetComponent(out IDamageableComponent? damageable))
damageable.TryHealth(DamageState.Critical, out var health))
{ {
BodyPartHealth.Text = $"{health.current} / {health.max}"; BodyPartHealth.Text = Loc.GetString("{0} damage", damageable.TotalDamage);
} }
MechanismList.Clear(); MechanismList.Clear();

View File

@@ -22,8 +22,8 @@ namespace Content.Client.GameObjects.Components.Kitchen
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private GrinderMenu _menu; private GrinderMenu _menu;
private Dictionary<int, EntityUid> _chamberVisualContents = new Dictionary<int, EntityUid>(); private Dictionary<int, EntityUid> _chamberVisualContents = new();
private Dictionary<int, Solution.ReagentQuantity> _beakerVisualContents = new Dictionary<int, Solution.ReagentQuantity>(); private Dictionary<int, Solution.ReagentQuantity> _beakerVisualContents = new();
public ReagentGrinderBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { } public ReagentGrinderBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { }
protected override void Open() 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.Client.GameObjects.EntitySystems;
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 Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
@@ -9,10 +8,12 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State namespace Content.Client.GameObjects.Components.Mobs.State
{ {
public class DeadState : SharedDeadState public class DeadMobState : SharedDeadMobState
{ {
public override void EnterState(IEntity entity) public override void EnterState(IEntity entity)
{ {
base.EnterState(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance)) if (entity.TryGetComponent(out AppearanceComponent appearance))
{ {
appearance.SetData(DamageStateVisuals.State, DamageState.Dead); appearance.SetData(DamageStateVisuals.State, DamageState.Dead);
@@ -28,6 +29,8 @@ namespace Content.Client.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity) public override void ExitState(IEntity entity)
{ {
base.ExitState(entity);
EntitySystem.Get<StandingStateSystem>().Standing(entity); EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out PhysicsComponent physics)) if (entity.TryGetComponent(out PhysicsComponent physics))
@@ -35,7 +38,5 @@ namespace Content.Client.GameObjects.Components.Mobs.State
physics.CanCollide = true; 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 Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State namespace Content.Client.GameObjects.Components.Mobs.State
{ {
public class NormalState : SharedNormalState public class NormalMobState : SharedNormalMobState
{ {
public override void EnterState(IEntity entity) public override void EnterState(IEntity entity)
{ {
base.EnterState(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance)) if (entity.TryGetComponent(out AppearanceComponent appearance))
{ {
appearance.SetData(DamageStateVisuals.State, DamageState.Alive); 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.Server.GlobalVerbs;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Map;
@@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Commands
[TestOf(typeof(RejuvenateVerb))] [TestOf(typeof(RejuvenateVerb))]
public class RejuvenateTest : ContentIntegrationTest public class RejuvenateTest : ContentIntegrationTest
{ {
private const string PROTOTYPES = @" private const string Prototypes = @"
- type: entity - type: entity
name: DamageableDummy name: DamageableDummy
id: DamageableDummy id: DamageableDummy
@@ -23,12 +24,17 @@ namespace Content.IntegrationTests.Tests.Commands
damagePrototype: biologicalDamageContainer damagePrototype: biologicalDamageContainer
criticalThreshold: 100 criticalThreshold: 100
deadThreshold: 200 deadThreshold: 200
- type: MobState
thresholds:
0: !type:NormalMobState {}
100: !type:CriticalMobState {}
200: !type:DeadMobState {}
"; ";
[Test] [Test]
public async Task RejuvenateDeadTest() public async Task RejuvenateDeadTest()
{ {
var options = new ServerIntegrationOptions{ExtraPrototypes = PROTOTYPES}; var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
var server = StartServerDummyTicker(options); var server = StartServerDummyTicker(options);
await server.WaitAssertion(() => await server.WaitAssertion(() =>
@@ -43,19 +49,30 @@ namespace Content.IntegrationTests.Tests.Commands
// Sanity check // Sanity check
Assert.True(human.TryGetComponent(out IDamageableComponent damageable)); 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 // Kill the entity
damageable.ChangeDamage(DamageClass.Brute, 10000000, true); damageable.ChangeDamage(DamageClass.Brute, 10000000, true);
// Check that it is dead // 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 // Rejuvenate them
RejuvenateVerb.PerformRejuvenate(human); RejuvenateVerb.PerformRejuvenate(human);
// Check that it is alive and with no damage // 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); 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.AI.LoadBalancer;
using Content.Server.GameObjects.EntitySystems.JobQueues; using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.AI; using Robust.Server.AI;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
@@ -130,28 +131,18 @@ namespace Content.Server.AI.Utility.AiLogic
_planCooldownRemaining = PlanCooldown; _planCooldownRemaining = PlanCooldown;
_blackboard = new Blackboard(SelfEntity); _blackboard = new Blackboard(SelfEntity);
_planner = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiActionSystem>(); _planner = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiActionSystem>();
if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
damageableComponent.HealthChangedEvent += DeathHandle;
}
} }
public override void Shutdown() 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(); var currentOp = CurrentAction?.ActionOperators.Peek();
currentOp?.Shutdown(Outcome.Failed); currentOp?.Shutdown(Outcome.Failed);
} }
private void DeathHandle(HealthChangedEventArgs eventArgs) public void MobStateChanged(MobStateChangedMessage message)
{ {
var oldDeadState = _isDead; var oldDeadState = _isDead;
_isDead = eventArgs.Damageable.CurrentState == DamageState.Dead || eventArgs.Damageable.CurrentState == DamageState.Critical; _isDead = message.Component.IsIncapacitated();
if (oldDeadState != _isDead) if (oldDeadState != _isDead)
{ {

View File

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

View File

@@ -1,6 +1,7 @@
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
namespace Content.Server.AI.Utility.Considerations.Combat namespace Content.Server.AI.Utility.Considerations.Combat
{ {
@@ -10,12 +11,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{ {
var target = context.GetState<TargetEntityState>().GetValue(); 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; return 0.0f;
} }
if (damageableComponent.CurrentState == DamageState.Dead) if (mobState.IsDead())
{ {
return 1.0f; 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.Mobs;
using Content.Server.GameObjects.Components.Observer; using Content.Server.GameObjects.Components.Observer;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
@@ -6,6 +7,7 @@ using Content.Server.Players;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; 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.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
@@ -21,25 +23,25 @@ namespace Content.Server.Commands.Observer
public string Help => "ghost"; public string Help => "ghost";
public bool CanReturn { get; set; } = true; 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) if (player == null)
{ {
shell.SendText((IPlayerSession) null, "Nah"); shell.SendText(player, "Nah");
return; return;
} }
var mind = player.ContentData().Mind; var mind = player.ContentData()?.Mind;
if (mind == null) if (mind == null)
{ {
shell.SendText(player, "You can't ghost here!"); shell.SendText(player, "You can't ghost here!");
return; return;
} }
var canReturn = player.AttachedEntity != null && CanReturn; var playerEntity = player.AttachedEntity;
var name = player.AttachedEntity?.Name ?? player.Name;
if (player.AttachedEntity != null && player.AttachedEntity.HasComponent<GhostComponent>()) if (playerEntity != null && playerEntity.HasComponent<GhostComponent>())
return; return;
if (mind.VisitingEntity != null) if (mind.VisitingEntity != null)
@@ -48,22 +50,28 @@ namespace Content.Server.Commands.Observer
mind.VisitingEntity.Delete(); 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;
canReturn = true; }
break; else if (mobState.IsCritical())
case DamageState.Critical: {
canReturn = true; canReturn = true;
damageable.ChangeDamage(DamageType.Asphyxiation, 100, true, null); //todo: what if they dont breathe lol
break; if (playerEntity.TryGetComponent(out IDamageableComponent? damageable))
default: {
canReturn = false; //todo: what if they dont breathe lol
break; damageable.ChangeDamage(DamageType.Asphyxiation, 100, true);
}
}
else
{
canReturn = false;
} }
} }
@@ -74,9 +82,10 @@ namespace Content.Server.Commands.Observer
var ghostComponent = ghost.GetComponent<GhostComponent>(); var ghostComponent = ghost.GetComponent<GhostComponent>();
ghostComponent.CanReturnToBody = canReturn; 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) if (canReturn)

View File

@@ -1,12 +1,16 @@
#nullable enable #nullable enable
using System;
using Content.Server.Commands.Observer; using Content.Server.Commands.Observer;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.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.Movement;
using Robust.Server.GameObjects.Components.Container; using Robust.Server.GameObjects.Components.Container;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Players; using Robust.Shared.Players;
@@ -76,10 +80,12 @@ namespace Content.Server.GameObjects.Components.Body
void IRelayMoveInput.MoveInputPressed(ICommonSession session) void IRelayMoveInput.MoveInputPressed(ICommonSession session)
{ {
if (Owner.TryGetComponent(out IDamageableComponent? damageable) && if (Owner.TryGetComponent(out IMobStateComponent? mobState) &&
damageable.CurrentState == DamageState.Dead) 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; } BodyPartType Part { get; }
} }
public class BodyHealthChangeParams : HealthChangeParams, IBodyHealthChangeParams public class BodyDamageChangeParams : DamageChangeParams, IBodyHealthChangeParams
{ {
public BodyHealthChangeParams(BodyPartType part) public BodyDamageChangeParams(BodyPartType part)
{ {
Part = part; Part = part;
} }

View File

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

View File

@@ -1,72 +1,10 @@
using System.Collections.Generic; using Robust.Shared.GameObjects;
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;
namespace Content.Server.GameObjects.Components.Damage 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] [RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))] public class BreakableComponent : Component
public class BreakableComponent : RuinableComponent, IExAct
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override string Name => "Breakable"; 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 #nullable enable
using System;
using Content.Server.GameObjects.Components.Construction; using Content.Server.GameObjects.Components.Construction;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
@@ -10,11 +8,8 @@ using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage namespace Content.Server.GameObjects.Components.Damage
{ {
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))] public class BreakableConstructionComponent : Component, IDestroyAct
public class BreakableConstructionComponent : RuinableComponent
{ {
private ActSystem _actSystem = default!;
public override string Name => "BreakableConstruction"; public override string Name => "BreakableConstruction";
public override void ExposeData(ObjectSerializer serializer) 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); 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; 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; if (Owner.Deleted ||
!Owner.TryGetComponent(out ConstructionComponent? construction) ||
_actSystem.HandleBreakage(Owner); string.IsNullOrEmpty(Node))
{
return;
}
await construction.ChangeNode(Node); 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.Damage;
using Content.Shared.GameObjects.Components.Disposal; using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.Components.Items; using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
@@ -139,11 +140,10 @@ namespace Content.Server.GameObjects.Components.Disposal
return false; return false;
} }
if (!entity.TryGetComponent(out IPhysicsComponent? physics) || if (!entity.TryGetComponent(out IPhysicsComponent? physics) ||
!physics.CanCollide) !physics.CanCollide)
{ {
if (!(entity.TryGetComponent(out IDamageableComponent? damageState) && damageState.CurrentState == DamageState.Dead)) { if (!(entity.TryGetComponent(out IMobStateComponent? state) && state.IsDead())) {
return false; 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.Interactable;
using Content.Server.GameObjects.Components.Power.ApcNetComponents; using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Gravity; using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.Components.Interactable; using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
@@ -108,8 +109,11 @@ namespace Content.Server.GameObjects.Components.Gravity
return false; return false;
// Repair generator // Repair generator
var breakable = Owner.GetComponent<BreakableComponent>(); if (Owner.TryGetComponent(out IDamageableComponent? damageable))
breakable.FixAllDamage(); {
damageable.Heal();
}
_intact = true; _intact = true;
Owner.PopupMessage(eventArgs.User, Owner.PopupMessage(eventArgs.User,

View File

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

View File

@@ -11,6 +11,7 @@ using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Medical; using Content.Shared.GameObjects.Components.Medical;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
@@ -146,14 +147,23 @@ namespace Content.Server.GameObjects.Components.Medical
UserInterface?.SetState(newState); UserInterface?.SetState(newState);
} }
private MedicalScannerStatus GetStatusFromDamageState(DamageState damageState) private MedicalScannerStatus GetStatusFromDamageState(IMobStateComponent state)
{ {
switch (damageState) if (state.IsAlive())
{ {
case DamageState.Alive: return MedicalScannerStatus.Green; return MedicalScannerStatus.Green;
case DamageState.Critical: return MedicalScannerStatus.Red; }
case DamageState.Dead: return MedicalScannerStatus.Death; else if (state.IsCritical())
default: throw new ArgumentException(nameof(damageState)); {
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) if (Powered)
{ {
var body = _bodyContainer.ContainedEntity; var body = _bodyContainer.ContainedEntity;
return body == null var state = body?.GetComponentOrNull<IMobStateComponent>();
return state == null
? MedicalScannerStatus.Open ? MedicalScannerStatus.Open
: GetStatusFromDamageState(body.GetComponent<IDamageableComponent>().CurrentState); : GetStatusFromDamageState(state);
} }
return MedicalScannerStatus.Off; 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;
using Content.Shared.GameObjects.Components.Body.Mechanism; using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Interfaces.Chemistry; using Content.Shared.Interfaces.Chemistry;
@@ -355,8 +356,8 @@ namespace Content.Server.GameObjects.Components.Metabolism
/// </param> /// </param>
public void Update(float frameTime) public void Update(float frameTime)
{ {
if (!Owner.TryGetComponent<IDamageableComponent>(out var damageable) || if (!Owner.TryGetComponent<IMobStateComponent>(out var state) ||
damageable.CurrentState == DamageState.Dead) state.IsDead())
{ {
return; return;
} }

View File

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

View File

@@ -9,20 +9,17 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State namespace Content.Server.GameObjects.Components.Mobs.State
{ {
public class CriticalState : SharedCriticalState public class CriticalMobState : SharedCriticalMobState
{ {
public override void EnterState(IEntity entity) public override void EnterState(IEntity entity)
{ {
base.EnterState(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance)) if (entity.TryGetComponent(out AppearanceComponent appearance))
{ {
appearance.SetData(DamageStateVisuals.State, DamageState.Critical); 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)) if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{ {
overlay.AddOverlay(SharedOverlayID.GradientCircleMaskOverlay); overlay.AddOverlay(SharedOverlayID.GradientCircleMaskOverlay);
@@ -38,12 +35,12 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity) public override void ExitState(IEntity entity)
{ {
base.ExitState(entity);
if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay)) if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{ {
overlay.ClearOverlays(); 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 namespace Content.Server.GameObjects.Components.Mobs.State
{ {
public class DeadState : SharedDeadState public class DeadMobState : SharedDeadMobState
{ {
public override void EnterState(IEntity entity) public override void EnterState(IEntity entity)
{ {
base.EnterState(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance)) if (entity.TryGetComponent(out AppearanceComponent appearance))
{ {
appearance.SetData(DamageStateVisuals.State, DamageState.Dead); appearance.SetData(DamageStateVisuals.State, DamageState.Dead);
@@ -44,6 +46,8 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity) public override void ExitState(IEntity entity)
{ {
base.ExitState(entity);
if (entity.TryGetComponent(out IPhysicsComponent physics)) if (entity.TryGetComponent(out IPhysicsComponent physics))
{ {
physics.CanCollide = true; physics.CanCollide = true;
@@ -54,7 +58,5 @@ namespace Content.Server.GameObjects.Components.Mobs.State
overlay.ClearOverlays(); overlay.ClearOverlays();
} }
} }
public override void UpdateState(IEntity entity) { }
} }
} }

View File

@@ -1,71 +1,22 @@
using System.Collections.Generic; using Content.Shared.GameObjects.Components.Mobs.State;
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.Shared.GameObjects; using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State namespace Content.Server.GameObjects.Components.Mobs.State
{ {
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(SharedMobStateManagerComponent))] [ComponentReference(typeof(SharedMobStateComponent))]
public class MobStateManagerComponent : SharedMobStateManagerComponent [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() public override void OnRemove()
{ {
// TODO: Might want to add an OnRemove() to IMobState since those are where these components are being used // 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)) if (Owner.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{ {
overlay.ClearOverlays(); overlay.ClearOverlays();
} }
}
public override ComponentState GetComponentState() base.OnRemove();
{
return new MobStateManagerComponentState(CurrentDamageState);
} }
} }
} }

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 #nullable enable
using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.EntitySystems.AI; using Content.Server.GameObjects.EntitySystems.AI;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Server.AI;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems; 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)] [ViewVariables(VVAccess.ReadWrite)]
public string? StartingGearPrototype { get; set; } public string? StartingGearPrototype { get; set; }

View File

@@ -4,6 +4,7 @@ using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.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.Movement;
using Content.Shared.GameObjects.Components.Nutrition; using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -185,15 +186,19 @@ namespace Content.Server.GameObjects.Components.Nutrition
HungerThresholdEffect(); HungerThresholdEffect();
Dirty(); 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)) damageable.ChangeDamage(DamageType.Blunt, 2, true);
{
if (damageable.CurrentState != DamageState.Dead)
{
damageable.ChangeDamage(DamageType.Blunt, 2, true, null);
}
}
} }
} }
@@ -209,6 +214,4 @@ namespace Content.Server.GameObjects.Components.Nutrition
return new HungerComponentState(_currentHungerThreshold); return new HungerComponentState(_currentHungerThreshold);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
using Content.Shared; using Content.Shared;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Enums; 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!")); _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; _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
} }
@@ -44,11 +45,11 @@ namespace Content.Server.GameTicking.GameRules
{ {
base.Removed(); base.Removed();
_entityManager.EventBus.UnsubscribeEvent<HealthChangedEventArgs>(EventSource.Local, this); _entityManager.EventBus.UnsubscribeEvent<DamageChangedEventArgs>(EventSource.Local, this);
_playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged; _playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
} }
private void OnHealthChanged(HealthChangedEventArgs message) private void OnHealthChanged(DamageChangedEventArgs message)
{ {
_runDelayedCheck(); _runDelayedCheck();
} }
@@ -63,13 +64,14 @@ namespace Content.Server.GameTicking.GameRules
IPlayerSession winner = null; IPlayerSession winner = null;
foreach (var playerSession in _playerManager.GetAllPlayers()) foreach (var playerSession in _playerManager.GetAllPlayers())
{ {
if (playerSession.AttachedEntity == null var playerEntity = playerSession.AttachedEntity;
|| !playerSession.AttachedEntity.TryGetComponent(out IDamageableComponent damageable)) if (playerEntity == null
|| !playerEntity.TryGetComponent(out IMobStateComponent state))
{ {
continue; continue;
} }
if (damageable.CurrentState != DamageState.Alive) if (!state.IsAlive())
{ {
continue; continue;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#nullable enable #nullable enable
using Content.Server.Mobs; using Content.Server.Mobs;
using Content.Server.Objectives.Interfaces; using Content.Server.Objectives.Interfaces;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Mobs.State;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -24,11 +24,12 @@ 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 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 && public float Progress => _mind?
_mind.OwnedEntity.TryGetComponent<IDamageableComponent>(out var damageableComponent) && .OwnedEntity?
damageableComponent.CurrentState == DamageState.Dead .GetComponentOrNull<IMobStateComponent>()?
? 0f .IsDead() ?? false
: 1f; ? 0f
: 1f;
public float Difficulty => 1f; public float Difficulty => 1f;

View File

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

View File

@@ -18,6 +18,7 @@ namespace Content.Shared.Damage
public static class DamageClassExtensions public static class DamageClassExtensions
{ {
// TODO DAMAGE This but not hardcoded
private static readonly ImmutableDictionary<DamageClass, List<DamageType>> ClassToType = private static readonly ImmutableDictionary<DamageClass, List<DamageType>> ClassToType =
new Dictionary<DamageClass, List<DamageType>> 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -14,18 +15,37 @@ namespace Content.Shared.Damage.DamageContainer
[Serializable, NetSerializable] [Serializable, NetSerializable]
public class DamageContainerPrototype : IPrototype, IIndexedPrototype public class DamageContainerPrototype : IPrototype, IIndexedPrototype
{ {
private List<DamageClass> _activeDamageClasses; private HashSet<DamageClass> _supportedClasses;
private HashSet<DamageType> _supportedTypes;
private string _id; 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; [ViewVariables] public string ID => _id;
public virtual void LoadFrom(YamlMappingNode mapping) public virtual void LoadFrom(YamlMappingNode mapping)
{ {
var serializer = YamlObjectSerializer.NewReader(mapping); var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataField(ref _id, "id", string.Empty); 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.Radiation, DamageClass.Toxin},
{DamageType.Asphyxiation, DamageClass.Airloss}, {DamageType.Asphyxiation, DamageClass.Airloss},
{DamageType.Bloodloss, DamageClass.Airloss}, {DamageType.Bloodloss, DamageClass.Airloss},
{DamageType.Cellular, DamageClass.Genetic } {DamageType.Cellular, DamageClass.Genetic}
}.ToImmutableDictionary(); }.ToImmutableDictionary();
public static DamageClass ToClass(this DamageType type) public static DamageClass ToClass(this DamageType type)

View File

@@ -6,7 +6,9 @@ using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.ResistanceSet namespace Content.Shared.Damage.ResistanceSet
{ {
/// <summary> /// <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> /// </summary>
[NetSerializable] [NetSerializable]
[Serializable] [Serializable]
@@ -33,14 +35,15 @@ namespace Content.Shared.Damage.ResistanceSet
public string ID { get; } public string ID { get; }
/// <summary> /// <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). /// healing (negative).
/// </summary> /// </summary>
/// <param name="damageType">Type of damage.</param> /// <param name="damageType">Type of damage.</param>
/// <param name="amount">Incoming amount of damage.</param> /// <param name="amount">Incoming amount of damage.</param>
public int CalculateDamage(DamageType damageType, int amount) 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; amount -= _resistances[damageType].FlatReduction;
@@ -59,8 +62,7 @@ namespace Content.Shared.Damage.ResistanceSet
/// <summary> /// <summary>
/// Settings for a specific damage type in a resistance set. Flat reduction is applied before the coefficient. /// Settings for a specific damage type in a resistance set. Flat reduction is applied before the coefficient.
/// </summary> /// </summary>
[NetSerializable] [Serializable, NetSerializable]
[Serializable]
public struct ResistanceSetSettings public struct ResistanceSetSettings
{ {
[ViewVariables] public float Coefficient { get; private set; } [ViewVariables] public float Coefficient { get; private set; }

View File

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

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 #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.DamageContainer; using Content.Shared.Damage.DamageContainer;
using Content.Shared.Damage.ResistanceSet; using Content.Shared.Damage.ResistanceSet;
@@ -31,51 +32,28 @@ namespace Content.Shared.GameObjects.Components.Damage
public override string Name => "Damageable"; 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; 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 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 [ViewVariables] public IReadOnlyDictionary<DamageType, int> DamageTypes => _damageList;
{
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;
public DamageFlag Flags public DamageFlag Flags
{ {
@@ -107,41 +85,20 @@ namespace Content.Shared.GameObjects.Components.Damage
Flags &= ~flag; 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) public override void ExposeData(ObjectSerializer serializer)
{ {
base.ExposeData(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( serializer.DataReadWriteFunction(
"flags", "flags",
new List<DamageFlag>(), new List<DamageFlag>(),
@@ -161,7 +118,9 @@ namespace Content.Shared.GameObjects.Components.Damage
var writeFlags = new List<DamageFlag>(); var writeFlags = new List<DamageFlag>();
if (Flags == DamageFlag.None) if (Flags == DamageFlag.None)
{
return writeFlags; return writeFlags;
}
foreach (var flag in (DamageFlag[]) Enum.GetValues(typeof(DamageFlag))) foreach (var flag in (DamageFlag[]) Enum.GetValues(typeof(DamageFlag)))
{ {
@@ -181,9 +140,15 @@ namespace Content.Shared.GameObjects.Components.Damage
prototype => prototype =>
{ {
var damagePrototype = _prototypeManager.Index<DamageContainerPrototype>(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( serializer.DataReadWriteFunction(
"resistancePrototype", "resistancePrototype",
@@ -196,16 +161,6 @@ namespace Content.Shared.GameObjects.Components.Damage
() => Resistances.ID); () => Resistances.ID);
} }
public override void Initialize()
{
base.Initialize();
foreach (var behavior in Owner.GetAllComponents<IOnHealthChangedBehavior>())
{
HealthChangedEvent += behavior.OnHealthChanged;
}
}
protected override void Startup() protected override void Startup()
{ {
base.Startup(); base.Startup();
@@ -213,215 +168,302 @@ namespace Content.Shared.GameObjects.Components.Damage
ForceHealthChangedEvent(); ForceHealthChangedEvent();
} }
public bool TryGetDamage(DamageType type, out int damage) public override ComponentState GetComponentState()
{ {
return Damage.TryGetDamageValue(type, out damage); return new DamageableComponentState(_damageList, _flags);
} }
public bool ChangeDamage(DamageType type, int amount, bool ignoreResistances, public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is DamageableComponentState state))
{
return;
}
_damageList.Clear();
foreach (var (type, damage) in state.DamageList)
{
_damageList[type] = damage;
}
_flags = state.Flags;
}
public int GetDamage(DamageType type)
{
return _damageList.GetValueOrDefault(type);
}
public bool TryGetDamage(DamageType type, out int damage)
{
return _damageList.TryGetValue(type, out damage);
}
public int GetDamage(DamageClass @class)
{
if (!SupportsDamageClass(@class))
{
return 0;
}
var damage = 0;
foreach (var type in @class.ToTypes())
{
damage += GetDamage(type);
}
return damage;
}
public bool TryGetDamage(DamageClass @class, out int damage)
{
if (!SupportsDamageClass(@class))
{
damage = 0;
return false;
}
damage = GetDamage(@class);
return true;
}
/// <summary>
/// Attempts to set the damage value for the given <see cref="DamageType"/>.
/// </summary>
/// <returns>
/// True if successful, false if this container does not support that type.
/// </returns>
public bool TrySetDamage(DamageType type, int newValue)
{
if (newValue < 0)
{
return false;
}
var damageClass = type.ToClass();
if (_supportedClasses.Contains(damageClass))
{
var old = _damageList[type] = newValue;
_damageList[type] = newValue;
var delta = newValue - old;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
return false;
}
public void Heal(DamageType type)
{
SetDamage(type, 0);
}
public void Heal()
{
foreach (var type in _supportedTypes)
{
Heal(type);
}
}
public bool ChangeDamage(
DamageType type,
int amount,
bool ignoreResistances,
IEntity? source = null, IEntity? source = null,
HealthChangeParams? extraParams = null) DamageChangeParams? extraParams = null)
{ {
if (amount > 0 && HasFlag(DamageFlag.Invulnerable)) if (amount > 0 && HasFlag(DamageFlag.Invulnerable))
{ {
return false; return false;
} }
if (Damage.SupportsDamageType(type)) if (!SupportsDamageType(type))
{ {
var finalDamage = amount; return false;
if (!ignoreResistances)
{
finalDamage = Resistances.CalculateDamage(type, amount);
}
Damage.ChangeDamageValue(type, finalDamage);
return true;
} }
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, public bool ChangeDamage(DamageClass @class, int amount, bool ignoreResistances,
IEntity? source = null, IEntity? source = null,
HealthChangeParams? extraParams = null) DamageChangeParams? extraParams = null)
{ {
if (amount > 0 && HasFlag(DamageFlag.Invulnerable)) if (amount > 0 && HasFlag(DamageFlag.Invulnerable))
{ {
return false; return false;
} }
if (Damage.SupportsDamageClass(@class)) if (!SupportsDamageClass(@class))
{ {
var types = @class.ToTypes(); return false;
}
if (amount < 0) var types = @class.ToTypes();
if (amount < 0)
{
// Changing multiple types is a bit more complicated. Might be a better way (formula?) to do this,
// but essentially just loops between each damage category until all healing is used up.
var healingLeft = amount;
var healThisCycle = 1;
// While we have healing left...
while (healingLeft > 0 && healThisCycle != 0)
{ {
// Changing multiple types is a bit more complicated. Might be a better way (formula?) to do this, // Infinite loop fallback, if no healing was done in a cycle
// but essentially just loops between each damage category until all healing is used up. // then exit
var healingLeft = amount; healThisCycle = 0;
var healThisCycle = 1;
// While we have healing left... int healPerType;
while (healingLeft > 0 && healThisCycle != 0) if (healingLeft > -types.Count)
{ {
// Infinite loop fallback, if no healing was done in a cycle // Say we were to distribute 2 healing between 3
// then exit // this will distribute 1 to each (and stop after 2 are given)
healThisCycle = 0; healPerType = -1;
int healPerType;
if (healingLeft > -types.Count && healingLeft < 0)
{
// Say we were to distribute 2 healing between 3
// this will distribute 1 to each (and stop after 2 are given)
healPerType = -1;
}
else
{
// Say we were to distribute 62 healing between 3
// this will distribute 20 to each, leaving 2 for next loop
healPerType = healingLeft / types.Count;
}
foreach (var type in types)
{
var healAmount =
Math.Max(Math.Max(healPerType, -Damage.GetDamageValue(type)),
healingLeft);
Damage.ChangeDamageValue(type, healAmount);
healThisCycle += healAmount;
healingLeft -= healAmount;
}
}
return true;
}
var damageLeft = amount;
while (damageLeft > 0)
{
int damagePerType;
if (damageLeft < types.Count && damageLeft > 0)
{
damagePerType = 1;
} }
else else
{ {
damagePerType = damageLeft / types.Count; // Say we were to distribute 62 healing between 3
// this will distribute 20 to each, leaving 2 for next loop
healPerType = healingLeft / types.Count;
} }
foreach (var type in types) foreach (var type in types)
{ {
var damageAmount = Math.Min(damagePerType, damageLeft); var healAmount =
Damage.ChangeDamageValue(type, damageAmount); Math.Max(Math.Max(healPerType, -GetDamage(type)), healingLeft);
damageLeft -= damageAmount;
ChangeDamage(type, healAmount, true);
healThisCycle += healAmount;
healingLeft -= healAmount;
} }
} }
return true; return true;
} }
return false; var damageLeft = amount;
while (damageLeft > 0)
{
int damagePerType;
if (damageLeft < types.Count)
{
damagePerType = 1;
}
else
{
damagePerType = damageLeft / types.Count;
}
foreach (var type in types)
{
var damageAmount = Math.Min(damagePerType, damageLeft);
ChangeDamage(type, damageAmount, true);
damageLeft -= damageAmount;
}
}
return true;
} }
public bool SetDamage(DamageType type, int newValue, IEntity? source = null, public bool SetDamage(DamageType type, int newValue, IEntity? source = null, DamageChangeParams? extraParams = null)
HealthChangeParams? extraParams = null)
{ {
if (newValue >= TotalDamage && HasFlag(DamageFlag.Invulnerable)) if (newValue >= TotalDamage && HasFlag(DamageFlag.Invulnerable))
{ {
return false; return false;
} }
if (Damage.SupportsDamageType(type)) if (newValue < 0)
{ {
Damage.SetDamageValue(type, newValue); return false;
return true;
} }
return false; if (!_damageList.ContainsKey(type))
} {
return false;
}
public void Heal() var old = _damageList[type];
{ _damageList[type] = newValue;
Damage.Heal();
var delta = newValue - old;
var datum = new DamageChangeData(type, 0, delta);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
} }
public void ForceHealthChangedEvent() 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 damage = GetDamage(type);
var datum = new HealthChangeData(type, damage, 0); var datum = new DamageChangeData(type, damage, 0);
data.Add(datum); data.Add(datum);
} }
OnHealthChanged(data); OnHealthChanged(data);
} }
public (int current, int max)? Health(DamageState threshold) private void OnHealthChanged(List<DamageChangeData> changes)
{ {
if (!SupportedDamageStates.Contains(threshold) || var args = new DamageChangedEventArgs(this, changes);
!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);
OnHealthChanged(args); OnHealthChanged(args);
} }
protected virtual void EnterState(DamageState state) { } protected virtual void OnHealthChanged(DamageChangedEventArgs e)
protected virtual void OnHealthChanged(HealthChangedEventArgs 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); Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, e);
HealthChangedEvent?.Invoke(e); HealthChangedEvent?.Invoke(e);
var message = new DamageChangedMessage(this, e.Data);
SendMessage(message);
Dirty(); Dirty();
} }
@@ -446,4 +488,17 @@ namespace Content.Shared.GameObjects.Components.Damage
ChangeDamage(DamageType.Heat, damage, false); 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 #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.Interfaces.GameObjects; 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 /// (including both damage negated by resistance or simply inputting 0 as
/// the amount of damage to deal). /// the amount of damage to deal).
/// </summary> /// </summary>
event Action<HealthChangedEventArgs> HealthChangedEvent; event Action<DamageChangedEventArgs> 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; }
/// <summary> /// <summary>
/// Sum of all damages taken. /// Sum of all damages taken.
@@ -71,6 +56,10 @@ namespace Content.Shared.GameObjects.Components.Damage
/// <param name="flag">The flag to remove.</param> /// <param name="flag">The flag to remove.</param>
void RemoveFlag(DamageFlag flag); void RemoveFlag(DamageFlag flag);
bool SupportsDamageClass(DamageClass @class);
bool SupportsDamageType(DamageType type);
/// <summary> /// <summary>
/// Gets the amount of damage of a type. /// Gets the amount of damage of a type.
/// </summary> /// </summary>
@@ -79,7 +68,7 @@ namespace Content.Shared.GameObjects.Components.Damage
/// <returns> /// <returns>
/// True if the given <see cref="type"/> is supported, false otherwise. /// True if the given <see cref="type"/> is supported, false otherwise.
/// </returns> /// </returns>
bool TryGetDamage(DamageType type, [NotNullWhen(true)] out int damage); bool TryGetDamage(DamageType type, out int damage);
/// <summary> /// <summary>
/// Changes the specified <see cref="DamageType"/>, applying /// Changes the specified <see cref="DamageType"/>, applying
@@ -101,10 +90,14 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </param> /// </param>
/// <returns> /// <returns>
/// False if the given type is not supported or improper /// 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> /// </returns>
bool ChangeDamage(DamageType type, int amount, bool ignoreResistances, IEntity? source = null, bool ChangeDamage(
HealthChangeParams? extraParams = null); DamageType type,
int amount,
bool ignoreResistances,
IEntity? source = null,
DamageChangeParams? extraParams = null);
/// <summary> /// <summary>
/// Changes the specified <see cref="DamageClass"/>, applying /// Changes the specified <see cref="DamageClass"/>, applying
@@ -127,10 +120,14 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </param> /// </param>
/// <returns> /// <returns>
/// Returns false if the given class is not supported or improper /// 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> /// </returns>
bool ChangeDamage(DamageClass @class, int amount, bool ignoreResistances, IEntity? source = null, bool ChangeDamage(
HealthChangeParams? extraParams = null); DamageClass @class,
int amount,
bool ignoreResistances,
IEntity? source = null,
DamageChangeParams? extraParams = null);
/// <summary> /// <summary>
/// Forcefully sets the specified <see cref="DamageType"/> to the given /// Forcefully sets the specified <see cref="DamageType"/> to the given
@@ -145,9 +142,13 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </param> /// </param>
/// <returns> /// <returns>
/// Returns false if the given type is not supported or improper /// 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> /// </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> /// <summary>
/// Sets all damage values to zero. /// 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. /// Invokes the HealthChangedEvent with the current values of health.
/// </summary> /// </summary>
void ForceHealthChangedEvent(); 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.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs.State 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 /// (i.e. Normal, Critical, Dead) and what effects to apply upon entering or
/// exiting the state. /// exiting the state.
/// </summary> /// </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> /// <summary>
/// Called when this state is entered. /// Called when this state is entered.
/// </summary> /// </summary>
@@ -24,6 +37,6 @@ namespace Content.Shared.GameObjects.Components.Mobs.State
/// <summary> /// <summary>
/// Called when this state is updated. /// Called when this state is updated.
/// </summary> /// </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 BATTERY_BARREL = 1067;
public const uint SUSPICION_ROLE = 1068; public const uint SUSPICION_ROLE = 1068;
public const uint ROTATION = 1069; 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 SLIP = 1071;
public const uint SPACE_VILLAIN_ARCADE = 1072; public const uint SPACE_VILLAIN_ARCADE = 1072;
public const uint BLOCKGAME_ARCADE = 1073; public const uint BLOCKGAME_ARCADE = 1073;
@@ -86,6 +86,7 @@
public const uint SINGULARITY = 1080; public const uint SINGULARITY = 1080;
public const uint CHARACTERINFO = 1081; public const uint CHARACTERINFO = 1081;
public const uint REAGENT_GRINDER = 1082; public const uint REAGENT_GRINDER = 1082;
public const uint DAMAGEABLE = 1083;
// Net IDs for integration tests. // Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001; public const uint PREDICTION_TEST = 10001;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,77 +1,80 @@
 - type: entity - type: entity
name: baseinstrument name: baseinstrument
id: BasePlaceableInstrument id: BasePlaceableInstrument
abstract: true abstract: true
placement: placement:
mode: SnapgridCenter mode: SnapgridCenter
components: components:
- type: Instrument - type: Instrument
handheld: false handheld: false
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: Anchorable - type: Anchorable
- type: Physics - type: Physics
shapes: shapes:
- !type:PhysShapeAabb - !type:PhysShapeAabb
layer: [MobMask] layer: [MobMask]
mask: mask:
- Impassable - Impassable
- MobImpassable - MobImpassable
- VaultImpassable - VaultImpassable
- type: SnapGrid - type: SnapGrid
offset: Center offset: Center
- type: Destructible - type: Damageable
deadThreshold: 50 resistances: metallicResistances
resistances: metallicResistances - type: Destructible
- type: UserInterface thresholds:
interfaces: 50:
- key: enum.InstrumentUiKey.Key Acts: ["Destruction"]
type: InstrumentBoundUserInterface - type: UserInterface
interfaces:
- key: enum.InstrumentUiKey.Key
type: InstrumentBoundUserInterface
- type: entity - type: entity
name: piano name: piano
parent: BasePlaceableInstrument parent: BasePlaceableInstrument
id: PianoInstrument id: PianoInstrument
description: Play Needles Piano Now. description: Play Needles Piano Now.
components: components:
- type: Instrument - type: Instrument
program: 1 program: 1
- type: Sprite - type: Sprite
sprite: Objects/Fun/Instruments/otherinstruments.rsi sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: piano state: piano
- type: entity - type: entity
name: minimoog name: minimoog
parent: BasePlaceableInstrument parent: BasePlaceableInstrument
id: MinimoogInstrument id: MinimoogInstrument
description: 'This is a minimoog, like a space piano, but more spacey!' description: 'This is a minimoog, like a space piano, but more spacey!'
components: components:
- type: Instrument - type: Instrument
program: 81 program: 81
- type: Sprite - type: Sprite
sprite: Objects/Fun/Instruments/otherinstruments.rsi sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: minimoog state: minimoog
- type: entity - type: entity
name: church organ name: church organ
parent: BasePlaceableInstrument parent: BasePlaceableInstrument
id: ChurchOrganInstrument id: ChurchOrganInstrument
description: This thing really blows! description: This thing really blows!
components: components:
- type: Instrument - type: Instrument
program: 20 program: 20
- type: Sprite - type: Sprite
sprite: Objects/Fun/Instruments/otherinstruments.rsi sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: church_organ state: church_organ
- type: entity - type: entity
name: xylophone name: xylophone
parent: BasePlaceableInstrument parent: BasePlaceableInstrument
id: XylophoneInstrument id: XylophoneInstrument
description: Rainbow colored glockenspiel. description: Rainbow colored glockenspiel.
components: components:
- type: Instrument - type: Instrument
program: 13 program: 13
- type: Sprite - type: Sprite
sprite: Objects/Fun/Instruments/otherinstruments.rsi sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: xylophone state: xylophone

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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