diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj index 956f2fd0351a98..816cd2f7afff2d 100644 --- a/Content.Client/Content.Client.csproj +++ b/Content.Client/Content.Client.csproj @@ -25,6 +25,7 @@ + diff --git a/Content.Client/Medical/MedicalMenuSystem.cs b/Content.Client/Medical/MedicalMenuSystem.cs new file mode 100644 index 00000000000000..9efd535bbd27b0 --- /dev/null +++ b/Content.Client/Medical/MedicalMenuSystem.cs @@ -0,0 +1,82 @@ +using System.Linq; +using Content.Client.Administration.Managers; +using Content.Client.Interactable; +using Content.Client.UserInterface.Systems.MedicalMenu; +using Content.Shared.Administration; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Components; +using Content.Shared.Verbs; +using Robust.Client.Console; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Shared.Console; +using Robust.Shared.Containers; +using Robust.Shared.Player; +using Robust.Shared.Toolshed; +using Robust.Shared.Utility; + +namespace Content.Client.Medical; + +public sealed class MedicalMenuSystem : EntitySystem +{ + [Dependency] private readonly InteractionSystem _interaction = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly IUserInterfaceManager _uiManager = default!; + [Dependency] private readonly IClientAdminManager _adminManager = default!; + [Dependency] private readonly IConsoleHost _console = default!; + [Dependency] private readonly EntityManager _entityManager = default!; + + private MedicalMenuUIController _medMenuController = default!; + + //TODO: make this a CVAR + private readonly float MaxMedInspectRange = 15f; + + public override void Initialize() + { + _medMenuController = _uiManager.GetUIController(); + SubscribeLocalEvent>(SetupMedicalUI); + + } + + private void SetupMedicalUI(EntityUid uid, MedicalDataComponent medData, GetVerbsEvent args) + { + if (!args.CanInteract + || !_containerSystem.IsInSameOrParentContainer(args.User, args.Target) + || !_interaction.InRangeUnobstructed(args.User, args.Target, MaxMedInspectRange)) + return; + + args.Verbs.Add(new Verb() + { + Text = "Open Medical Menu", //TODO localize + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/plus.svg.192dpi.png")), + Act = () => { OpenUI(args.Target);}, + ClientExclusive = true + }); + + // View variables verbs + if (_adminManager.HasFlag(AdminFlags.Debug) && _player.LocalSession != null) + { + var verb = new VvVerb() + { + Text = Loc.GetString("Print All Wounds"), + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")), + Act = () => _console.RemoteExecuteCommand(_player.LocalSession, $"PrintAllWounds \"{_entityManager.GetNetEntity(args.Target)}\""), + ClientExclusive = true // opening VV window is client-side. Don't ask server to run this verb. + }; + args.Verbs.Add(verb); + } + } + + private void OpenUI(EntityUid target) + { + if (!_medMenuController.IsOpen) + { + //Opens a window and sets target + _medMenuController.OpenWindow(target); + return; + } + //updates our target if the window is open already + _medMenuController.SetTarget(target); + } +} diff --git a/Content.Client/Medical/Respiration/LungsSystem.cs b/Content.Client/Medical/Respiration/LungsSystem.cs new file mode 100644 index 00000000000000..26365498d844a6 --- /dev/null +++ b/Content.Client/Medical/Respiration/LungsSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Medical.Respiration.Systems; + +namespace Content.Client.Medical.Respiration; + +public sealed class LungsSystem : SharedLungsSystem +{ +} diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/BodyPartStatusControl.xaml b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/BodyPartStatusControl.xaml new file mode 100644 index 00000000000000..18dae1a08f335b --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/BodyPartStatusControl.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/BodyPartStatusControl.xaml.cs b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/BodyPartStatusControl.xaml.cs new file mode 100644 index 00000000000000..727ad338c606b9 --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/BodyPartStatusControl.xaml.cs @@ -0,0 +1,64 @@ +using Content.Shared.Body.Part; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Common; +using Content.Shared.Medical.Wounding.Components; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.UserInterface.Systems.MedicalMenu.Controls; + +[GenerateTypedNameReferences] +public sealed partial class BodyPartStatusControl : Collapsible +{ + + private string _partName = "UnknownBodyPart"; + public string PartName + { + get => _partName; + set + { + _partName = value; + BodyPartLabel.Text = value; + } + } + + private Entity? _part = null; + public Entity? LinkedPart => _part; + + + private static readonly Color Healthy = Color.DarkGreen; + private static readonly Color Unhealthy = Color.DarkRed; + + public BodyPartStatusControl() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + //setup chevron + Chevron.AddStyleClass(OptionButton.StyleClassOptionTriangle); + } + + public void SetPartCondition(FixedPoint2 conditionPercentage) + { + BodyPartStatusLabel.Text = SeverityHelper.GetVisibleConditionString(conditionPercentage); + BodyPartStatusLabel.FontColorOverride = Color.InterpolateBetween( Unhealthy, Healthy, + FixedPoint2.Clamp(conditionPercentage, 0, 1).Float()); + } + + public void LinkPart(Entity? newPart) + { + _part = newPart; + if (newPart == null) + { + SetPartCondition(0); + return; + } + SetPartCondition(newPart.Value.Comp2.HitPointPercent); + } + + public void AddChildPart(BodyPartStatusControl childPart) + { + Contents.AddChild(childPart); + } +} diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/OverviewPage.xaml b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/OverviewPage.xaml new file mode 100644 index 00000000000000..0e31c695f73eaa --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/OverviewPage.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/OverviewPage.xaml.cs b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/OverviewPage.xaml.cs new file mode 100644 index 00000000000000..105f843924bb25 --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/OverviewPage.xaml.cs @@ -0,0 +1,35 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.UserInterface.Systems.MedicalMenu.Controls; + +[GenerateTypedNameReferences] +public sealed partial class OverviewPage : BoxContainer +{ + public OverviewPage() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + Contents.OnChildRemoved += OnChildPartRemoved; + } + + private void OnChildPartRemoved(Control obj) + { + if (ChildCount == 0) + NoPartErrorText.Visible = true; + } + + public void ClearChildParts() + { + Contents.RemoveAllChildren(); + } + + public void AddChildPart(BodyPartStatusControl child) + { + Contents.AddChild(child); + NoPartErrorText.Visible = false; + } +} + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/SurgeryPage.xaml b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/SurgeryPage.xaml new file mode 100644 index 00000000000000..e17d5d987e572d --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/SurgeryPage.xaml @@ -0,0 +1,10 @@ + + + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/SurgeryPage.xaml.cs b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/SurgeryPage.xaml.cs new file mode 100644 index 00000000000000..acb809b831345f --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/SurgeryPage.xaml.cs @@ -0,0 +1,17 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.UserInterface.Systems.MedicalMenu.Controls; + +[GenerateTypedNameReferences] +public sealed partial class SurgeryPage : BoxContainer +{ + public SurgeryPage() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } +} + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TargetBodyStatus.xaml b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TargetBodyStatus.xaml new file mode 100644 index 00000000000000..2766c6862c48e3 --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TargetBodyStatus.xaml @@ -0,0 +1,19 @@ + + + + + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TargetBodyStatus.xaml.cs b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TargetBodyStatus.xaml.cs new file mode 100644 index 00000000000000..dc862c9b5a39cd --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TargetBodyStatus.xaml.cs @@ -0,0 +1,24 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.UserInterface.Systems.MedicalMenu.Controls; + +[GenerateTypedNameReferences] +public sealed partial class TargetBodyStatus : BoxContainer +{ + + private const string UnknownTargetText = "Unknown Target"; + + public TargetBodyStatus() + { + RobustXamlLoader.Load(this); + } + + public void SetTarget(string? targetName) + { + TargetName.Text = targetName ?? UnknownTargetText; + } +} + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TriagePage.xaml b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TriagePage.xaml new file mode 100644 index 00000000000000..62df9446e1a476 --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TriagePage.xaml @@ -0,0 +1,9 @@ + + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TriagePage.xaml.cs b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TriagePage.xaml.cs new file mode 100644 index 00000000000000..09ab5b03610d6e --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Controls/TriagePage.xaml.cs @@ -0,0 +1,17 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.UserInterface.Systems.MedicalMenu.Controls; + +[GenerateTypedNameReferences] +public sealed partial class TriagePage : BoxContainer +{ + public TriagePage() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } +} + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/MedicalMenuUIController.cs b/Content.Client/UserInterface/Systems/MedicalMenu/MedicalMenuUIController.cs new file mode 100644 index 00000000000000..f0f80b446169ea --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/MedicalMenuUIController.cs @@ -0,0 +1,137 @@ +using Content.Client.Body.Systems; +using Content.Client.UserInterface.Systems.MedicalMenu.Controls; +using Content.Client.UserInterface.Systems.MedicalMenu.Windows; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Medical.Wounding.Components; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controllers; + +namespace Content.Client.UserInterface.Systems.MedicalMenu; + +public sealed class MedicalMenuUIController : UIController +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [UISystemDependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly ILogManager _logManager = default!; + private MedicalMenuWindow _medicalWindow = default!; + private bool _isOpen = false; + public bool IsOpen => _isOpen; + private EntityUid? _target; + public EntityUid? CurrentTarget => _target; + + private BodyPartStatusControl? bodyPartStatusTree = null; + private ISawmill _sawmill = default!; + + public override void Initialize() + { + _medicalWindow = UIManager.CreateWindow(); + _medicalWindow.OnClose += CloseWindow; + _sawmill = _logManager.GetSawmill("Medical.Menu"); + } + + public void OpenWindow() + { + _target ??= _playerManager.LocalSession?.AttachedEntity; + OpenWindow(_target); + } + + public void OpenWindow(EntityUid? target) + { + SetTarget(target); + _medicalWindow.Open(); + _isOpen = true; + } + + public void CloseWindow() + { + _medicalWindow.Close(); + _isOpen = false; + } + + private void ClearBodyStatusTree() + { + if (bodyPartStatusTree == null) + return; + bodyPartStatusTree.Parent?.RemoveChild(bodyPartStatusTree); + bodyPartStatusTree = null; + } + + private BodyPartStatusControl CreateBodyPartStatusLeaf( + EntityUid partEntity, + BodyPartComponent bodyPart, + WoundableComponent woundable) + { + var newStatusLeaf = new BodyPartStatusControl(); + newStatusLeaf.LinkPart(new Entity(partEntity, bodyPart, woundable)); + newStatusLeaf.PartName = EntityManager.GetComponent(partEntity).EntityName; + return newStatusLeaf; + } + + private void RecursivelyAddChildParts(BodyPartStatusControl parentControl, EntityUid parentPartEnt, BodyPartComponent parentPart) + { + if (parentControl.LinkedPart == null) + return; + + foreach (var (childPartEnt, childPart) in + _bodySystem.GetBodyPartDirectChildren(parentPartEnt, parentPart)) + { + if (!EntityManager.TryGetComponent(childPartEnt, out var woundable)) + continue; + var leaf = CreateBodyPartStatusLeaf(childPartEnt, childPart, woundable); + parentControl.AddChildPart(leaf); + RecursivelyAddChildParts(leaf, childPartEnt, childPart); + } + } + + //Recreates the part status tree, this should be called when parts are attached or detached + public void RefreshPartStatusTree() + { + if (bodyPartStatusTree == null) + return; + var oldData = bodyPartStatusTree.LinkedPart; + ClearBodyStatusTree(); + bodyPartStatusTree = CreateBodyPartStatusLeaf(oldData!.Value.Owner, oldData.Value.Comp1, oldData.Value.Comp2); + RecursivelyAddChildParts(bodyPartStatusTree, oldData.Value.Owner, oldData.Value.Comp1); + } + + + private void CreateBodyPartStatusTree(EntityUid target, BodyComponent body) + { + if (bodyPartStatusTree != null) + { + //This should never happen unless someone is doing something stupid and in which case this error message is for YOU!!! + _sawmill.Error("Tried to create body part status tree when one already exists!"); + return; + } + + if (!_bodySystem.TryGetRootBodyPart(target, out var rootPart, body) + || !EntityManager.TryGetComponent(rootPart.Value.Owner, out var woundable) + ) + return; + bodyPartStatusTree = CreateBodyPartStatusLeaf(rootPart.Value.Owner, rootPart, woundable); + RecursivelyAddChildParts(bodyPartStatusTree, rootPart.Value.Owner, rootPart); + _medicalWindow.OverviewTab.AddChildPart(bodyPartStatusTree); + } + + + public void SetTarget(EntityUid? targetEntity) + { + if (targetEntity == null || _target == targetEntity) + return; + ClearBodyStatusTree(); + if (!EntityManager.TryGetComponent(targetEntity.Value, out var body)) + { + _target = null; + _medicalWindow.OverviewTab.TargetStatus.SetTarget(null); + return; + } + + _target = targetEntity; + var targetEntName = EntityManager.GetComponent(targetEntity.Value).EntityName; + _medicalWindow.OverviewTab.TargetStatus.SetTarget(targetEntName); + CreateBodyPartStatusTree(targetEntity.Value, body); + } + +} diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Windows/MedicalMenuWindow.xaml b/Content.Client/UserInterface/Systems/MedicalMenu/Windows/MedicalMenuWindow.xaml new file mode 100644 index 00000000000000..3effd249bb6ae6 --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Windows/MedicalMenuWindow.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/Content.Client/UserInterface/Systems/MedicalMenu/Windows/MedicalMenuWindow.xaml.cs b/Content.Client/UserInterface/Systems/MedicalMenu/Windows/MedicalMenuWindow.xaml.cs new file mode 100644 index 00000000000000..84ab668c7084e0 --- /dev/null +++ b/Content.Client/UserInterface/Systems/MedicalMenu/Windows/MedicalMenuWindow.xaml.cs @@ -0,0 +1,16 @@ +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.UserInterface.Systems.MedicalMenu.Windows; + +[GenerateTypedNameReferences] +public sealed partial class MedicalMenuWindow : FancyWindow +{ + public MedicalMenuWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } +} diff --git a/Content.IntegrationTests/Tests/Body/LungTest.cs b/Content.IntegrationTests/Tests/Body/LungTest.cs index dce3741c98dcf7..98d6856c45bb5a 100644 --- a/Content.IntegrationTests/Tests/Body/LungTest.cs +++ b/Content.IntegrationTests/Tests/Body/LungTest.cs @@ -14,188 +14,189 @@ namespace Content.IntegrationTests.Tests.Body { - [TestFixture] - [TestOf(typeof(LungSystem))] - public sealed class LungTest - { - [TestPrototypes] - private const string Prototypes = @" -- type: entity - name: HumanLungDummy - id: HumanLungDummy - components: - - type: SolutionContainerManager - - type: Body - prototype: Human - - type: MobState - allowedStates: - - Alive - - type: Damageable - - type: ThermalRegulator - metabolismHeat: 5000 - radiatedHeat: 400 - implicitHeatRegulation: 5000 - sweatHeatRegulation: 5000 - shiveringHeatRegulation: 5000 - normalBodyTemperature: 310.15 - thermalRegulationTemperatureThreshold: 25 - - type: Respirator - damage: - types: - Asphyxiation: 1.5 - damageRecovery: - types: - Asphyxiation: -1.5 -"; - - [Test] - public async Task AirConsistencyTest() - { - // --- Setup - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - await server.WaitIdleAsync(); - - var mapManager = server.ResolveDependency(); - var entityManager = server.ResolveDependency(); - var mapLoader = entityManager.System(); - - MapId mapId; - EntityUid? grid = null; - BodyComponent body = default; - RespiratorComponent resp = default; - EntityUid human = default; - GridAtmosphereComponent relevantAtmos = default; - var startingMoles = 0.0f; - - var testMapName = "Maps/Test/Breathing/3by3-20oxy-80nit.yml"; - - await server.WaitPost(() => - { - mapId = mapManager.CreateMap(); - Assert.That(mapLoader.TryLoad(mapId, testMapName, out var roots)); - - var query = entityManager.GetEntityQuery(); - var grids = roots.Where(x => query.HasComponent(x)); - Assert.That(grids, Is.Not.Empty); - grid = grids.First(); - }); - - Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); - - float GetMapMoles() - { - var totalMapMoles = 0.0f; - foreach (var tile in relevantAtmos.Tiles.Values) - { - totalMapMoles += tile.Air?.TotalMoles ?? 0.0f; - } - - return totalMapMoles; - } - - await server.WaitAssertion(() => - { - var center = new Vector2(0.5f, 0.5f); - var coordinates = new EntityCoordinates(grid.Value, center); - human = entityManager.SpawnEntity("HumanLungDummy", coordinates); - relevantAtmos = entityManager.GetComponent(grid.Value); - startingMoles = 100f; // Hardcoded because GetMapMoles returns 900 here for some reason. - -#pragma warning disable NUnit2045 - Assert.That(entityManager.TryGetComponent(human, out body), Is.True); - Assert.That(entityManager.TryGetComponent(human, out resp), Is.True); -#pragma warning restore NUnit2045 - }); - - // --- End setup - - var inhaleCycles = 100; - for (var i = 0; i < inhaleCycles; i++) - { - // Breathe in - await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Exhaling); - Assert.That( - GetMapMoles(), Is.LessThan(startingMoles), - "Did not inhale in any gas" - ); - - // Breathe out - await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Inhaling); - Assert.That( - GetMapMoles(), Is.EqualTo(startingMoles).Within(0.0002), - "Did not exhale as much gas as was inhaled" - ); - } - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task NoSuffocationTest() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var mapManager = server.ResolveDependency(); - var entityManager = server.ResolveDependency(); - var cfg = server.ResolveDependency(); - var mapLoader = entityManager.System(); - - MapId mapId; - EntityUid? grid = null; - RespiratorComponent respirator = null; - EntityUid human = default; - - var testMapName = "Maps/Test/Breathing/3by3-20oxy-80nit.yml"; - - await server.WaitPost(() => - { - mapId = mapManager.CreateMap(); - - Assert.That(mapLoader.TryLoad(mapId, testMapName, out var ents), Is.True); - var query = entityManager.GetEntityQuery(); - grid = ents - .Select(x => x) - .FirstOrDefault((uid) => uid.HasValue && query.HasComponent(uid.Value), null); - Assert.That(grid, Is.Not.Null); - }); - - Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); - - await server.WaitAssertion(() => - { - var center = new Vector2(0.5f, 0.5f); - - var coordinates = new EntityCoordinates(grid.Value, center); - human = entityManager.SpawnEntity("HumanLungDummy", coordinates); - - var mixture = entityManager.System().GetContainingMixture(human); -#pragma warning disable NUnit2045 - Assert.That(mixture.TotalMoles, Is.GreaterThan(0)); - Assert.That(entityManager.HasComponent(human), Is.True); - Assert.That(entityManager.TryGetComponent(human, out respirator), Is.True); - Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold)); -#pragma warning restore NUnit2045 - }); - - var increment = 10; - - // 20 seconds - var total = 20 * cfg.GetCVar(CVars.NetTickrate); - - for (var tick = 0; tick < total; tick += increment) - { - await server.WaitRunTicks(increment); - await server.WaitAssertion(() => - { - Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold), - $"Entity {entityManager.GetComponent(human).EntityName} is suffocating on tick {tick}"); - }); - } - - await pair.CleanReturnAsync(); - } - } + //TODO Respiration: reimplement this + // [TestFixture] + // //[TestOf(typeof(LungSystem))] + // public sealed class LungTest + // { +// [TestPrototypes] +// private const string Prototypes = @" +// - type: entity +// name: HumanLungDummy +// id: HumanLungDummy +// components: +// - type: SolutionContainerManager +// - type: Body +// prototype: Human +// - type: MobState +// allowedStates: +// - Alive +// - type: Damageable +// - type: ThermalRegulator +// metabolismHeat: 5000 +// radiatedHeat: 400 +// implicitHeatRegulation: 5000 +// sweatHeatRegulation: 5000 +// shiveringHeatRegulation: 5000 +// normalBodyTemperature: 310.15 +// thermalRegulationTemperatureThreshold: 25 +// - type: Respirator +// damage: +// types: +// Asphyxiation: 1.5 +// damageRecovery: +// types: +// Asphyxiation: -1.5 +// "; +// +// [Test] +// public async Task AirConsistencyTest() +// { +// // --- Setup +// await using var pair = await PoolManager.GetServerClient(); +// var server = pair.Server; +// +// await server.WaitIdleAsync(); +// +// var mapManager = server.ResolveDependency(); +// var entityManager = server.ResolveDependency(); +// var mapLoader = entityManager.System(); +// +// MapId mapId; +// EntityUid? grid = null; +// BodyComponent body = default; +// RespiratorComponent resp = default; +// EntityUid human = default; +// GridAtmosphereComponent relevantAtmos = default; +// var startingMoles = 0.0f; +// +// var testMapName = "Maps/Test/Breathing/3by3-20oxy-80nit.yml"; +// +// await server.WaitPost(() => +// { +// mapId = mapManager.CreateMap(); +// Assert.That(mapLoader.TryLoad(mapId, testMapName, out var roots)); +// +// var query = entityManager.GetEntityQuery(); +// var grids = roots.Where(x => query.HasComponent(x)); +// Assert.That(grids, Is.Not.Empty); +// grid = grids.First(); +// }); +// +// Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); +// +// float GetMapMoles() +// { +// var totalMapMoles = 0.0f; +// foreach (var tile in relevantAtmos.Tiles.Values) +// { +// totalMapMoles += tile.Air?.TotalMoles ?? 0.0f; +// } +// +// return totalMapMoles; +// } +// +// await server.WaitAssertion(() => +// { +// var center = new Vector2(0.5f, 0.5f); +// var coordinates = new EntityCoordinates(grid.Value, center); +// human = entityManager.SpawnEntity("HumanLungDummy", coordinates); +// relevantAtmos = entityManager.GetComponent(grid.Value); +// startingMoles = 100f; // Hardcoded because GetMapMoles returns 900 here for some reason. +// +// #pragma warning disable NUnit2045 +// Assert.That(entityManager.TryGetComponent(human, out body), Is.True); +// Assert.That(entityManager.TryGetComponent(human, out resp), Is.True); +// #pragma warning restore NUnit2045 +// }); +// +// // --- End setup +// +// var inhaleCycles = 100; +// for (var i = 0; i < inhaleCycles; i++) +// { +// // Breathe in +// await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Exhaling); +// Assert.That( +// GetMapMoles(), Is.LessThan(startingMoles), +// "Did not inhale in any gas" +// ); +// +// // Breathe out +// await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Inhaling); +// Assert.That( +// GetMapMoles(), Is.EqualTo(startingMoles).Within(0.0002), +// "Did not exhale as much gas as was inhaled" +// ); +// } +// +// await pair.CleanReturnAsync(); +// } +// +// [Test] +// public async Task NoSuffocationTest() +// { +// await using var pair = await PoolManager.GetServerClient(); +// var server = pair.Server; +// +// var mapManager = server.ResolveDependency(); +// var entityManager = server.ResolveDependency(); +// var cfg = server.ResolveDependency(); +// var mapLoader = entityManager.System(); +// +// MapId mapId; +// EntityUid? grid = null; +// RespiratorComponent respirator = null; +// EntityUid human = default; +// +// var testMapName = "Maps/Test/Breathing/3by3-20oxy-80nit.yml"; +// +// await server.WaitPost(() => +// { +// mapId = mapManager.CreateMap(); +// +// Assert.That(mapLoader.TryLoad(mapId, testMapName, out var ents), Is.True); +// var query = entityManager.GetEntityQuery(); +// grid = ents +// .Select(x => x) +// .FirstOrDefault((uid) => uid.HasValue && query.HasComponent(uid.Value), null); +// Assert.That(grid, Is.Not.Null); +// }); +// +// Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); +// +// await server.WaitAssertion(() => +// { +// var center = new Vector2(0.5f, 0.5f); +// +// var coordinates = new EntityCoordinates(grid.Value, center); +// human = entityManager.SpawnEntity("HumanLungDummy", coordinates); +// +// var mixture = entityManager.System().GetContainingMixture(human); +// #pragma warning disable NUnit2045 +// Assert.That(mixture.TotalMoles, Is.GreaterThan(0)); +// Assert.That(entityManager.HasComponent(human), Is.True); +// Assert.That(entityManager.TryGetComponent(human, out respirator), Is.True); +// Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold)); +// #pragma warning restore NUnit2045 +// }); +// +// var increment = 10; +// +// // 20 seconds +// var total = 20 * cfg.GetCVar(CVars.NetTickrate); +// +// for (var tick = 0; tick < total; tick += increment) +// { +// await server.WaitRunTicks(increment); +// await server.WaitAssertion(() => +// { +// Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold), +// $"Entity {entityManager.GetComponent(human).EntityName} is suffocating on tick {tick}"); +// }); +// } +// +// await pair.CleanReturnAsync(); +// } +// } } diff --git a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs b/Content.IntegrationTests/Tests/Chemistry/ReagentDiscriminatorTest.cs similarity index 87% rename from Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs rename to Content.IntegrationTests/Tests/Chemistry/ReagentDiscriminatorTest.cs index f488734655afb2..2bdf62d9b2234c 100644 --- a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/ReagentDiscriminatorTest.cs @@ -7,8 +7,8 @@ namespace Content.IntegrationTests.Tests.Chemistry; [TestFixture] -[TestOf(typeof(ReagentData))] -public sealed class ReagentDataTest : InteractionTest +[TestOf(typeof(ReagentDiscriminator))] +public sealed class ReagentDiscriminatorTest : InteractionTest { [Test] public async Task ReagentDataIsSerializable() @@ -18,7 +18,7 @@ public async Task ReagentDataIsSerializable() Assert.Multiple(() => { - foreach (var instance in reflection.GetAllChildren(typeof(ReagentData))) + foreach (var instance in reflection.GetAllChildren(typeof(ReagentDiscriminator))) { Assert.That(instance.HasCustomAttribute(), $"{instance} must have the NetSerializable attribute."); Assert.That(instance.HasCustomAttribute(), $"{instance} must have the serializable attribute."); diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs index ddfe7b3481e374..b9c897e234827e 100644 --- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs @@ -66,7 +66,7 @@ await server.WaitAssertion(() => var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace); var mixerComponent = entityManager.AddComponent(dummyEntity); mixerComponent.ReactionTypes = reactionPrototype.MixingCategories; - solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, mixerComponent); + solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, false, mixerComponent); } }); diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index fc50d0bd334f14..86f4774c05b709 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -226,12 +226,14 @@ void CheckDummy(int i) var totalSeconds = 30; var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds); int increment = 5; - var resp = entMan.GetComponent(player); + //TODO respiration: Reimplement this + //var resp = entMan.GetComponent(player); var damage = entMan.GetComponent(player); for (var tick = 0; tick < totalTicks; tick += increment) { await pair.RunTicksSync(increment); - Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold)); + //TODO respiration: Reimplement this + //Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold)); Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index eb21662719e2e4..29e7c9232aefbe 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -30,6 +30,10 @@ using Content.Shared.Electrocution; using Content.Shared.Interaction.Components; using Content.Shared.Inventory; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -49,6 +53,8 @@ using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; +using BrainComponent = Content.Shared.Medical.Organs.Components.BrainComponent; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.Administration.Systems; @@ -265,7 +271,8 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"), Act = () => { - _bloodstreamSystem.SpillAllSolutions(args.Target, bloodstream); + //TODO: re-implement admin remove blood smite + //_bloodstreamSystem.SpillAllSolutions(args.Target, bloodstream); var xform = Transform(args.Target); _popupSystem.PopupEntity(Loc.GetString("admin-smite-remove-blood-self"), args.Target, args.Target, PopupType.LargeCaution); @@ -361,10 +368,11 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"), Act = () => { - foreach (var (component, _) in _bodySystem.GetBodyOrganComponents(args.Target, body)) - { - QueueDel(component.Owner); - } + //TODO Digestion: Re-Implement this + // foreach (var (component, _) in _bodySystem.GetBodyOrganComponents(args.Target, body)) + // { + // QueueDel(component.Owner); + // } _popupSystem.PopupEntity(Loc.GetString("admin-smite-stomach-removal-self"), args.Target, args.Target, PopupType.LargeCaution); @@ -381,10 +389,11 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"), Act = () => { - foreach (var (component, _) in _bodySystem.GetBodyOrganComponents(args.Target, body)) - { - QueueDel(component.Owner); - } + //TODO Lungs: Reimplement this + // foreach (var (component, _) in _bodySystem.GetBodyOrganComponents(args.Target, body)) + // { + // QueueDel(component.Owner); + // } _popupSystem.PopupEntity(Loc.GetString("admin-smite-lung-removal-self"), args.Target, args.Target, PopupType.LargeCaution); diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index a6b61da591f349..7866919816b080 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -92,8 +92,9 @@ private void OnStasisStrapped(Entity bed, ref StrappedEvent if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) return; - var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true); - RaiseLocalEvent(args.Buckle, ref metabolicEvent); + //TODO Metabolism: reimplement this + //var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true); + //RaiseLocalEvent(args.Buckle, ref metabolicEvent); } private void OnStasisUnstrapped(Entity bed, ref UnstrappedEvent args) @@ -101,8 +102,9 @@ private void OnStasisUnstrapped(Entity bed, ref UnstrappedEv if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) return; - var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false); - RaiseLocalEvent(args.Buckle, ref metabolicEvent); + //TODO Metabolism: reimplement this + //var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false); + //RaiseLocalEvent(args.Buckle, ref metabolicEvent); } private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args) @@ -126,11 +128,12 @@ private void UpdateMetabolisms(EntityUid uid, StasisBedComponent component, bool if (!TryComp(uid, out var strap) || strap.BuckledEntities.Count == 0) return; - foreach (var buckledEntity in strap.BuckledEntities) - { - var metabolicEvent = new ApplyMetabolicMultiplierEvent(buckledEntity, component.Multiplier, shouldApply); - RaiseLocalEvent(buckledEntity, ref metabolicEvent); - } + //TODO Metabolism: reimplement this + // foreach (var buckledEntity in strap.BuckledEntities) + // { + // var metabolicEvent = new ApplyMetabolicMultiplierEvent(buckledEntity, component.Multiplier, shouldApply); + // RaiseLocalEvent(buckledEntity, ref metabolicEvent); + // } } } } diff --git a/Content.Server/Body/Components/BloodstreamComponent.cs b/Content.Server/Body/Components/BloodstreamComponent.cs deleted file mode 100644 index a6d2afab2191a0..00000000000000 --- a/Content.Server/Body/Components/BloodstreamComponent.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Server.Chemistry.EntitySystems; -using Content.Shared.Alert; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.Body.Components -{ - [RegisterComponent, Access(typeof(BloodstreamSystem), typeof(ReactionMixerSystem))] - public sealed partial class BloodstreamComponent : Component - { - public static string DefaultChemicalsSolutionName = "chemicals"; - public static string DefaultBloodSolutionName = "bloodstream"; - public static string DefaultBloodTemporarySolutionName = "bloodstreamTemporary"; - - /// - /// The next time that blood level will be updated and bloodloss damage dealt. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate; - - /// - /// The interval at which this component updates. - /// - [DataField] - public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3); - - /// - /// How much is this entity currently bleeding? - /// Higher numbers mean more blood lost every tick. - /// - /// Goes down slowly over time, and items like bandages - /// or clotting reagents can lower bleeding. - /// - /// - /// This generally corresponds to an amount of damage and can't go above 100. - /// - [ViewVariables(VVAccess.ReadWrite)] - public float BleedAmount; - - /// - /// How much should bleeding be reduced every update interval? - /// - [DataField] - public float BleedReductionAmount = 0.33f; - - /// - /// How high can go? - /// - [DataField] - public float MaxBleedAmount = 10.0f; - - /// - /// What percentage of current blood is necessary to avoid dealing blood loss damage? - /// - [DataField] - public float BloodlossThreshold = 0.9f; - - /// - /// The base bloodloss damage to be incurred if below - /// The default values are defined per mob/species in YML. - /// - [DataField(required: true)] - public DamageSpecifier BloodlossDamage = new(); - - /// - /// The base bloodloss damage to be healed if above - /// The default values are defined per mob/species in YML. - /// - [DataField(required: true)] - public DamageSpecifier BloodlossHealDamage = new(); - - // TODO shouldn't be hardcoded, should just use some organ simulation like bone marrow or smth. - /// - /// How much reagent of blood should be restored each update interval? - /// - [DataField] - public FixedPoint2 BloodRefreshAmount = 1.0f; - - /// - /// How much blood needs to be in the temporary solution in order to create a puddle? - /// - [DataField] - public FixedPoint2 BleedPuddleThreshold = 1.0f; - - /// - /// A modifier set prototype ID corresponding to how damage should be modified - /// before taking it into account for bloodloss. - /// - /// - /// For example, piercing damage is increased while poison damage is nullified entirely. - /// - [DataField] - public ProtoId DamageBleedModifiers = "BloodlossHuman"; - - /// - /// The sound to be played when a weapon instantly deals blood loss damage. - /// - [DataField] - public SoundSpecifier InstantBloodSound = new SoundCollectionSpecifier("blood"); - - /// - /// The sound to be played when some damage actually heals bleeding rather than starting it. - /// - [DataField] - public SoundSpecifier BloodHealedSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg"); - - // TODO probably damage bleed thresholds. - - /// - /// Max volume of internal chemical solution storage - /// - [DataField] - public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250); - - /// - /// Max volume of internal blood storage, - /// and starting level of blood. - /// - [DataField] - public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300); - - /// - /// Which reagent is considered this entities 'blood'? - /// - /// - /// Slime-people might use slime as their blood or something like that. - /// - [DataField] - public ProtoId BloodReagent = "Blood"; - - /// Name/Key that is indexed by. - [DataField] - public string BloodSolutionName = DefaultBloodSolutionName; - - /// Name/Key that is indexed by. - [DataField] - public string ChemicalSolutionName = DefaultChemicalsSolutionName; - - /// Name/Key that is indexed by. - [DataField] - public string BloodTemporarySolutionName = DefaultBloodTemporarySolutionName; - - /// - /// Internal solution for blood storage - /// - [DataField] - public Entity? BloodSolution = null; - - /// - /// Internal solution for reagent storage - /// - [DataField] - public Entity? ChemicalSolution = null; - - /// - /// Temporary blood solution. - /// When blood is lost, it goes to this solution, and when this - /// solution hits a certain cap, the blood is actually spilled as a puddle. - /// - [DataField] - public Entity? TemporarySolution = null; - - /// - /// Variable that stores the amount of status time added by having a low blood level. - /// - [ViewVariables(VVAccess.ReadWrite)] - public TimeSpan StatusTime; - - [DataField] - public ProtoId BleedingAlert = "Bleed"; - } -} diff --git a/Content.Server/Body/Components/BrainComponent.cs b/Content.Server/Body/Components/BrainComponent.cs deleted file mode 100644 index 004ff24eaff695..00000000000000 --- a/Content.Server/Body/Components/BrainComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.Body.Systems; - -namespace Content.Server.Body.Components -{ - [RegisterComponent, Access(typeof(BrainSystem))] - public sealed partial class BrainComponent : Component - { - } -} diff --git a/Content.Server/Body/Components/LungComponent.cs b/Content.Server/Body/Components/LungComponent.cs deleted file mode 100644 index 72af4d9e63a991..00000000000000 --- a/Content.Server/Body/Components/LungComponent.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Alert; -using Content.Shared.Atmos; -using Content.Shared.Chemistry.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.Body.Components; - -[RegisterComponent, Access(typeof(LungSystem))] -public sealed partial class LungComponent : Component -{ - [DataField] - [Access(typeof(LungSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends - public GasMixture Air = new() - { - Volume = 6, - Temperature = Atmospherics.NormalBodyTemperature - }; - - /// - /// The name/key of the solution on this entity which these lungs act on. - /// - [DataField] - public string SolutionName = LungSystem.LungSolutionName; - - /// - /// The solution on this entity that these lungs act on. - /// - [DataField] - public Entity? Solution = null; - - /// - /// The type of gas this lung needs. Used only for the breathing alerts, not actual metabolism. - /// - [DataField] - public ProtoId Alert = "LowOxygen"; -} diff --git a/Content.Server/Body/Components/MetabolizerComponent.cs b/Content.Server/Body/Components/MetabolizerComponent.cs deleted file mode 100644 index 90c99df7db2c61..00000000000000 --- a/Content.Server/Body/Components/MetabolizerComponent.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Body.Prototypes; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.Body.Components -{ - /// - /// Handles metabolizing various reagents with given effects. - /// - [RegisterComponent, Access(typeof(MetabolizerSystem))] - public sealed partial class MetabolizerComponent : Component - { - /// - /// The next time that reagents will be metabolized. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate; - - /// - /// How often to metabolize reagents. - /// - /// - [DataField] - public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); - - /// - /// From which solution will this metabolizer attempt to metabolize chemicals - /// - [DataField("solution")] - public string SolutionName = BloodstreamComponent.DefaultChemicalsSolutionName; - - /// - /// Does this component use a solution on it's parent entity (the body) or itself - /// - /// - /// Most things will use the parent entity (bloodstream). - /// - [DataField] - public bool SolutionOnBody = true; - - /// - /// List of metabolizer types that this organ is. ex. Human, Slime, Felinid, w/e. - /// - [DataField] - [Access(typeof(MetabolizerSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends - public HashSet>? MetabolizerTypes = null; - - /// - /// Should this metabolizer remove chemicals that have no metabolisms defined? - /// As a stop-gap, basically. - /// - [DataField] - public bool RemoveEmpty = false; - - /// - /// How many reagents can this metabolizer process at once? - /// Used to nerf 'stacked poisons' where having 5+ different poisons in a syringe, even at low - /// quantity, would be muuuuch better than just one poison acting. - /// - [DataField("maxReagents")] - public int MaxReagentsProcessable = 3; - - /// - /// A list of metabolism groups that this metabolizer will act on, in order of precedence. - /// - [DataField("groups")] - public List? MetabolismGroups = default!; - } - - /// - /// Contains data about how a metabolizer will metabolize a single group. - /// This allows metabolizers to remove certain groups much faster, or not at all. - /// - [DataDefinition] - public sealed partial class MetabolismGroupEntry - { - [DataField(required: true)] - public ProtoId Id = default!; - - [DataField("rateModifier")] - public FixedPoint2 MetabolismRateModifier = 1.0; - } -} diff --git a/Content.Server/Body/Components/RespiratorComponent.cs b/Content.Server/Body/Components/RespiratorComponent.cs deleted file mode 100644 index a81062362aebe3..00000000000000 --- a/Content.Server/Body/Components/RespiratorComponent.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Chat.Prototypes; -using Content.Shared.Damage; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.Body.Components -{ - [RegisterComponent, Access(typeof(RespiratorSystem))] - public sealed partial class RespiratorComponent : Component - { - /// - /// The next time that this body will inhale or exhale. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate; - - /// - /// The interval between updates. Each update is either inhale or exhale, - /// so a full cycle takes twice as long. - /// - [DataField] - public TimeSpan UpdateInterval = TimeSpan.FromSeconds(2); - - /// - /// Saturation level. Reduced by UpdateInterval each tick. - /// Can be thought of as 'how many seconds you have until you start suffocating' in this configuration. - /// - [DataField] - public float Saturation = 5.0f; - - /// - /// At what level of saturation will you begin to suffocate? - /// - [DataField] - public float SuffocationThreshold; - - [DataField] - public float MaxSaturation = 5.0f; - - [DataField] - public float MinSaturation = -2.0f; - - // TODO HYPEROXIA? - - [DataField(required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier Damage = default!; - - [DataField(required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier DamageRecovery = default!; - - [DataField] - public TimeSpan GaspEmoteCooldown = TimeSpan.FromSeconds(8); - - [ViewVariables] - public TimeSpan LastGaspEmoteTime; - - /// - /// The emote when gasps - /// - [DataField] - public ProtoId GaspEmote = "Gasp"; - - /// - /// How many cycles in a row has the mob been under-saturated? - /// - [ViewVariables] - public int SuffocationCycles = 0; - - /// - /// How many cycles in a row does it take for the suffocation alert to pop up? - /// - [ViewVariables] - public int SuffocationCycleThreshold = 3; - - [ViewVariables] - public RespiratorStatus Status = RespiratorStatus.Inhaling; - } -} - -public enum RespiratorStatus -{ - Inhaling, - Exhaling -} diff --git a/Content.Server/Body/Components/StomachComponent.cs b/Content.Server/Body/Components/StomachComponent.cs deleted file mode 100644 index d541ca4d7c4536..00000000000000 --- a/Content.Server/Body/Components/StomachComponent.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Server.Nutrition.EntitySystems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Whitelist; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.Body.Components -{ - [RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))] - public sealed partial class StomachComponent : Component - { - /// - /// The next time that the stomach will try to digest its contents. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate; - - /// - /// The interval at which this stomach digests its contents. - /// - [DataField] - public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); - - /// - /// The solution inside of this stomach this transfers reagents to the body. - /// - [DataField] - public Entity? Solution = null; - - /// - /// What solution should this stomach push reagents into, on the body? - /// - [DataField] - public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName; - - /// - /// Time between reagents being ingested and them being - /// transferred to - /// - [DataField] - public TimeSpan DigestionDelay = TimeSpan.FromSeconds(20); - - /// - /// A whitelist for what special-digestible-required foods this stomach is capable of eating. - /// - [DataField] - public EntityWhitelist? SpecialDigestible = null; - - /// - /// Used to track how long each reagent has been in the stomach - /// - [ViewVariables] - public readonly List ReagentDeltas = new(); - - /// - /// Used to track quantity changes when ingesting & digesting reagents - /// - public sealed class ReagentDelta - { - public readonly ReagentQuantity ReagentQuantity; - public TimeSpan Lifetime { get; private set; } - - public ReagentDelta(ReagentQuantity reagentQuantity) - { - ReagentQuantity = reagentQuantity; - Lifetime = TimeSpan.Zero; - } - - public void Increment(TimeSpan delta) => Lifetime += delta; - } - } -} diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs deleted file mode 100644 index eaa7b62f25ae9e..00000000000000 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ /dev/null @@ -1,469 +0,0 @@ -using Content.Server.Body.Components; -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Chemistry.ReactionEffects; -using Content.Server.Fluids.EntitySystems; -using Content.Server.Forensics; -using Content.Server.Popups; -using Content.Shared.Alert; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Reaction; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.Drunk; -using Content.Shared.FixedPoint; -using Content.Shared.HealthExaminable; -using Content.Shared.Mobs.Systems; -using Content.Shared.Popups; -using Content.Shared.Rejuvenate; -using Content.Shared.Speech.EntitySystems; -using Robust.Server.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Timing; - -namespace Content.Server.Body.Systems; - -public sealed class BloodstreamSystem : EntitySystem -{ - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly AudioSystem _audio = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly PuddleSystem _puddleSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly SharedDrunkSystem _drunkSystem = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!; - [Dependency] private readonly AlertsSystem _alertsSystem = default!; - [Dependency] private readonly ForensicsSystem _forensicsSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnDamageChanged); - SubscribeLocalEvent(OnHealthBeingExamined); - SubscribeLocalEvent(OnBeingGibbed); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - SubscribeLocalEvent(OnReactionAttempt); - SubscribeLocalEvent>(OnReactionAttempt); - SubscribeLocalEvent(OnRejuvenate); - } - - private void OnMapInit(Entity ent, ref MapInitEvent args) - { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - private void OnReactionAttempt(Entity entity, ref ReactionAttemptEvent args) - { - if (args.Cancelled) - return; - - foreach (var effect in args.Reaction.Effects) - { - switch (effect) - { - case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream - case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels. - args.Cancelled = true; - return; - } - } - - // The area-reaction effect canceling is part of avoiding smoke-fork-bombs (create two smoke bombs, that when - // ingested by mobs create more smoke). This also used to act as a rapid chemical-purge, because all the - // reagents would get carried away by the smoke/foam. This does still work for the stomach (I guess people vomit - // up the smoke or spawned entities?). - - // TODO apply organ damage instead of just blocking the reaction? - // Having cheese-clots form in your veins can't be good for you. - } - - private void OnReactionAttempt(Entity entity, ref SolutionRelayEvent args) - { - if (args.Name != entity.Comp.BloodSolutionName - && args.Name != entity.Comp.ChemicalSolutionName - && args.Name != entity.Comp.BloodTemporarySolutionName) - { - return; - } - - OnReactionAttempt(entity, ref args.Event); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var bloodstream)) - { - if (_gameTiming.CurTime < bloodstream.NextUpdate) - continue; - - bloodstream.NextUpdate += bloodstream.UpdateInterval; - - if (!_solutionContainerSystem.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) - continue; - - // Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive. - if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid)) - { - TryModifyBloodLevel(uid, bloodstream.BloodRefreshAmount, bloodstream); - } - - // Removes blood from the bloodstream based on bleed amount (bleed rate) - // as well as stop their bleeding to a certain extent. - if (bloodstream.BleedAmount > 0) - { - // Blood is removed from the bloodstream at a 1-1 rate with the bleed amount - TryModifyBloodLevel(uid, (-bloodstream.BleedAmount), bloodstream); - // Bleed rate is reduced by the bleed reduction amount in the bloodstream component. - TryModifyBleedAmount(uid, -bloodstream.BleedReductionAmount, bloodstream); - } - - // deal bloodloss damage if their blood level is below a threshold. - var bloodPercentage = GetBloodLevelPercentage(uid, bloodstream); - if (bloodPercentage < bloodstream.BloodlossThreshold && !_mobStateSystem.IsDead(uid)) - { - // bloodloss damage is based on the base value, and modified by how low your blood level is. - var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage); - - _damageableSystem.TryChangeDamage(uid, amt, - ignoreResistances: false, interruptsDoAfters: false); - - // Apply dizziness as a symptom of bloodloss. - // The effect is applied in a way that it will never be cleared without being healthy. - // Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out - _drunkSystem.TryApplyDrunkenness( - uid, - (float) bloodstream.UpdateInterval.TotalSeconds * 2, - applySlur: false); - _stutteringSystem.DoStutter(uid, bloodstream.UpdateInterval * 2, refresh: false); - - // storing the drunk and stutter time so we can remove it independently from other effects additions - bloodstream.StatusTime += bloodstream.UpdateInterval * 2; - } - else if (!_mobStateSystem.IsDead(uid)) - { - // If they're healthy, we'll try and heal some bloodloss instead. - _damageableSystem.TryChangeDamage( - uid, - bloodstream.BloodlossHealDamage * bloodPercentage, - ignoreResistances: true, interruptsDoAfters: false); - - // Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level - _drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds); - _stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds); - // Reset the drunk and stutter time to zero - bloodstream.StatusTime = TimeSpan.Zero; - } - } - } - - private void OnComponentInit(Entity entity, ref ComponentInit args) - { - var chemicalSolution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.ChemicalSolutionName); - var bloodSolution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.BloodSolutionName); - var tempSolution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.BloodTemporarySolutionName); - - chemicalSolution.MaxVolume = entity.Comp.ChemicalMaxVolume; - bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume; - tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well - - // Fill blood solution with BLOOD - bloodSolution.AddReagent(entity.Comp.BloodReagent, entity.Comp.BloodMaxVolume - bloodSolution.Volume); - } - - private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) - { - if (args.DamageDelta is null || !args.DamageIncreased) - { - return; - } - - // TODO probably cache this or something. humans get hurt a lot - if (!_prototypeManager.TryIndex(ent.Comp.DamageBleedModifiers, out var modifiers)) - return; - - var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers); - - if (bloodloss.Empty) - return; - - // Does the calculation of how much bleed rate should be added/removed, then applies it - var oldBleedAmount = ent.Comp.BleedAmount; - var total = bloodloss.GetTotal(); - var totalFloat = total.Float(); - TryModifyBleedAmount(ent, totalFloat, ent); - - /// - /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5 - /// The crit chance is currently the bleed rate modifier divided by 25. - /// Higher damage weapons have a higher chance to crit! - /// - var prob = Math.Clamp(totalFloat / 25, 0, 1); - if (totalFloat > 0 && _robustRandom.Prob(prob)) - { - TryModifyBloodLevel(ent, (-total) / 5, ent); - _audio.PlayPvs(ent.Comp.InstantBloodSound, ent); - } - - // Heat damage will cauterize, causing the bleed rate to be reduced. - else if (totalFloat < 0 && oldBleedAmount > 0) - { - // Magically, this damage has healed some bleeding, likely - // because it's burn damage that cauterized their wounds. - - // We'll play a special sound and popup for feedback. - _audio.PlayPvs(ent.Comp.BloodHealedSound, ent); - _popupSystem.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), ent, - ent, PopupType.Medium); - } - } - /// - /// Shows text on health examine, based on bleed rate and blood level. - /// - private void OnHealthBeingExamined(Entity ent, ref HealthBeingExaminedEvent args) - { - // Shows profusely bleeding at half the max bleed rate. - if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2) - { - args.Message.PushNewline(); - args.Message.AddMarkup(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner))); - } - // Shows bleeding message when bleeding, but less than profusely. - else if (ent.Comp.BleedAmount > 0) - { - args.Message.PushNewline(); - args.Message.AddMarkup(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner))); - } - - // If the mob's blood level is below the damage threshhold, the pale message is added. - if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold) - { - args.Message.PushNewline(); - args.Message.AddMarkup(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner))); - } - } - - private void OnBeingGibbed(Entity ent, ref BeingGibbedEvent args) - { - SpillAllSolutions(ent, ent); - } - - private void OnApplyMetabolicMultiplier( - Entity ent, - ref ApplyMetabolicMultiplierEvent args) - { - // TODO REFACTOR THIS - // This will slowly drift over time due to floating point errors. - // Instead, raise an event with the base rates and allow modifiers to get applied to it. - if (args.Apply) - { - ent.Comp.UpdateInterval *= args.Multiplier; - return; - } - ent.Comp.UpdateInterval /= args.Multiplier; - } - - private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) - { - TryModifyBleedAmount(entity.Owner, -entity.Comp.BleedAmount, entity.Comp); - - if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution)) - TryModifyBloodLevel(entity.Owner, bloodSolution.AvailableVolume, entity.Comp); - - if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.ChemicalSolutionName, ref entity.Comp.ChemicalSolution)) - _solutionContainerSystem.RemoveAllSolution(entity.Comp.ChemicalSolution.Value); - } - - /// - /// Attempt to transfer provided solution to internal solution. - /// - public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component = null) - { - return Resolve(uid, ref component, logMissing: false) - && _solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution) - && _solutionContainerSystem.TryAddSolution(component.ChemicalSolution.Value, solution); - } - - public bool FlushChemicals(EntityUid uid, string excludedReagentID, FixedPoint2 quantity, BloodstreamComponent? component = null) - { - if (!Resolve(uid, ref component, logMissing: false) - || !_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution)) - return false; - - for (var i = chemSolution.Contents.Count - 1; i >= 0; i--) - { - var (reagentId, _) = chemSolution.Contents[i]; - if (reagentId.Prototype != excludedReagentID) - { - _solutionContainerSystem.RemoveReagent(component.ChemicalSolution.Value, reagentId, quantity); - } - } - - return true; - } - - public float GetBloodLevelPercentage(EntityUid uid, BloodstreamComponent? component = null) - { - if (!Resolve(uid, ref component) - || !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution)) - { - return 0.0f; - } - - return bloodSolution.FillFraction; - } - - public void SetBloodLossThreshold(EntityUid uid, float threshold, BloodstreamComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.BloodlossThreshold = threshold; - } - - /// - /// Attempts to modify the blood level of this entity directly. - /// - public bool TryModifyBloodLevel(EntityUid uid, FixedPoint2 amount, BloodstreamComponent? component = null) - { - if (!Resolve(uid, ref component, logMissing: false) - || !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution)) - { - return false; - } - - if (amount >= 0) - return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, out _); - - // Removal is more involved, - // since we also wanna handle moving it to the temporary solution - // and then spilling it if necessary. - var newSol = _solutionContainerSystem.SplitSolution(component.BloodSolution.Value, -amount); - - if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution)) - return true; - - tempSolution.AddSolution(newSol, _prototypeManager); - - if (tempSolution.Volume > component.BleedPuddleThreshold) - { - // Pass some of the chemstream into the spilled blood. - if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution)) - { - var temp = _solutionContainerSystem.SplitSolution(component.ChemicalSolution.Value, tempSolution.Volume / 10); - tempSolution.AddSolution(temp, _prototypeManager); - } - - if (_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false)) - { - _forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false); - } - - tempSolution.RemoveAllSolution(); - } - - _solutionContainerSystem.UpdateChemicals(component.TemporarySolution.Value); - - return true; - } - - /// - /// Tries to make an entity bleed more or less - /// - public bool TryModifyBleedAmount(EntityUid uid, float amount, BloodstreamComponent? component = null) - { - if (!Resolve(uid, ref component, logMissing: false)) - return false; - - component.BleedAmount += amount; - component.BleedAmount = Math.Clamp(component.BleedAmount, 0, component.MaxBleedAmount); - - if (component.BleedAmount == 0) - _alertsSystem.ClearAlert(uid, component.BleedingAlert); - else - { - var severity = (short) Math.Clamp(Math.Round(component.BleedAmount, MidpointRounding.ToZero), 0, 10); - _alertsSystem.ShowAlert(uid, component.BleedingAlert, severity); - } - - return true; - } - - /// - /// BLOOD FOR THE BLOOD GOD - /// - public void SpillAllSolutions(EntityUid uid, BloodstreamComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var tempSol = new Solution(); - - if (_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution)) - { - tempSol.MaxVolume += bloodSolution.MaxVolume; - tempSol.AddSolution(bloodSolution, _prototypeManager); - _solutionContainerSystem.RemoveAllSolution(component.BloodSolution.Value); - } - - if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution)) - { - tempSol.MaxVolume += chemSolution.MaxVolume; - tempSol.AddSolution(chemSolution, _prototypeManager); - _solutionContainerSystem.RemoveAllSolution(component.ChemicalSolution.Value); - } - - if (_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution)) - { - tempSol.MaxVolume += tempSolution.MaxVolume; - tempSol.AddSolution(tempSolution, _prototypeManager); - _solutionContainerSystem.RemoveAllSolution(component.TemporarySolution.Value); - } - - if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid)) - { - _forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false); - } - } - - /// - /// Change what someone's blood is made of, on the fly. - /// - public void ChangeBloodReagent(EntityUid uid, string reagent, BloodstreamComponent? component = null) - { - if (!Resolve(uid, ref component, logMissing: false) - || reagent == component.BloodReagent) - { - return; - } - - if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution)) - { - component.BloodReagent = reagent; - return; - } - - var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume); - - component.BloodReagent = reagent; - - if (currentVolume > 0) - _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, out _); - } -} diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index e10158cf357cec..0f76855befe74d 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -28,7 +28,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnRelayMoveInput); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); + // SubscribeLocalEvent(OnApplyMetabolicMultiplier); } private void OnRelayMoveInput(Entity ent, ref MoveInputEvent args) @@ -47,15 +47,15 @@ private void OnRelayMoveInput(Entity ent, ref MoveInputEvent args } } - private void OnApplyMetabolicMultiplier( - Entity ent, - ref ApplyMetabolicMultiplierEvent args) - { - foreach (var organ in GetBodyOrgans(ent, ent)) - { - RaiseLocalEvent(organ.Id, ref args); - } - } + // private void OnApplyMetabolicMultiplier( + // Entity ent, + // ref ApplyMetabolicMultiplierEvent args) + // { + // foreach (var organ in GetBodyOrgans(ent, ent)) + // { + // RaiseLocalEvent(organ.Id, ref args); + // } + // } protected override void AddPart( Entity bodyEnt, diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs index 86d2cb61ffe931..689fc353233fe4 100644 --- a/Content.Server/Body/Systems/BrainSystem.cs +++ b/Content.Server/Body/Systems/BrainSystem.cs @@ -1,10 +1,10 @@ -using Content.Server.Body.Components; using Content.Server.Ghost.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Events; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Pointing; +using BrainComponent = Content.Shared.Medical.Organs.Components.BrainComponent; namespace Content.Server.Body.Systems { diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index 922d48f13ed2a2..b82d0a476dcc91 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -23,7 +23,8 @@ public sealed class InternalsSystem : EntitySystem [Dependency] private readonly GasTankSystem _gasTank = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly RespiratorSystem _respirator = default!; + //TODO respiration: reimplement this + //[Dependency] private readonly RespiratorSystem _respirator = default!; private EntityQuery _internalsQuery; @@ -32,8 +33,8 @@ public override void Initialize() base.Initialize(); _internalsQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnInhaleLocation); + //TODO respiration: reimplement this + //SubscribeLocalEvent(OnInhaleLocation); SubscribeLocalEvent(OnInternalsStartup); SubscribeLocalEvent(OnInternalsShutdown); SubscribeLocalEvent>(OnGetInteractionVerbs); @@ -51,16 +52,16 @@ private void OnStartingGear(EntityUid uid, InternalsComponent component, ref Sta return; // already connected // Can the entity breathe the air it is currently exposed to? - if (_respirator.CanMetabolizeInhaledAir(uid)) - return; + //if (_respirator.CanMetabolizeInhaledAir(uid)) + // return; var tank = FindBestGasTank(uid); if (tank == null) return; // Could the entity metabolise the air in the linked gas tank? - if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air)) - return; + //if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air)) + // return; ToggleInternals(uid, uid, force: false, component); } @@ -168,16 +169,17 @@ private void OnInternalsShutdown(Entity ent, ref ComponentSh _alerts.ClearAlert(ent, ent.Comp.InternalsAlert); } - private void OnInhaleLocation(Entity ent, ref InhaleLocationEvent args) - { - if (AreInternalsWorking(ent)) - { - var gasTank = Comp(ent.Comp.GasTankEntity!.Value); - args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume); - // TODO: Should listen to gas tank updates instead I guess? - _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); - } - } +// private void OnInhaleLocation(Entity ent, ref InhaleLocationEvent args) +// { +// if (AreInternalsWorking(ent)) +// { +// var gasTank = Comp(ent.Comp.GasTankEntity!.Value); +// args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume); +// // TODO: Should listen to gas tank updates instead I guess? +// _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); +// } +// } + public void DisconnectBreathTool(Entity ent, EntityUid toolEntity) { ent.Comp.BreathTools.Remove(toolEntity); diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs deleted file mode 100644 index a3c185d5cc67e6..00000000000000 --- a/Content.Server/Body/Systems/LungSystem.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Atmos; -using Content.Shared.Chemistry.Components; -using Content.Shared.Clothing; -using Content.Shared.Inventory.Events; - -namespace Content.Server.Body.Systems; - -public sealed class LungSystem : EntitySystem -{ - [Dependency] private readonly AtmosphereSystem _atmos = default!; - [Dependency] private readonly InternalsSystem _internals = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - - public static string LungSolutionName = "Lung"; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent(OnMaskToggled); - } - - private void OnGotUnequipped(Entity ent, ref GotUnequippedEvent args) - { - _atmosphereSystem.DisconnectInternals(ent); - } - - private void OnGotEquipped(Entity ent, ref GotEquippedEvent args) - { - if ((args.SlotFlags & ent.Comp.AllowedSlots) == 0) - { - return; - } - - ent.Comp.IsFunctional = true; - - if (TryComp(args.Equipee, out InternalsComponent? internals)) - { - ent.Comp.ConnectedInternalsEntity = args.Equipee; - _internals.ConnectBreathTool((args.Equipee, internals), ent); - } - } - - private void OnComponentInit(Entity entity, ref ComponentInit args) - { - var solution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName); - solution.MaxVolume = 100.0f; - solution.CanReact = false; // No dexalin lungs - } - - private void OnMaskToggled(Entity ent, ref ItemMaskToggledEvent args) - { - if (args.IsToggled || args.IsEquip) - { - _atmos.DisconnectInternals(ent); - } - else - { - ent.Comp.IsFunctional = true; - - if (TryComp(args.Wearer, out InternalsComponent? internals)) - { - ent.Comp.ConnectedInternalsEntity = args.Wearer; - _internals.ConnectBreathTool((args.Wearer, internals), ent); - } - } - } - - public void GasToReagent(EntityUid uid, LungComponent lung) - { - if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution)) - return; - - GasToReagent(lung.Air, solution); - _solutionContainerSystem.UpdateChemicals(lung.Solution.Value); - } - - private void GasToReagent(GasMixture gas, Solution solution) - { - foreach (var gasId in Enum.GetValues()) - { - var i = (int) gasId; - var moles = gas[i]; - if (moles <= 0) - continue; - - var reagent = _atmosphereSystem.GasReagents[i]; - if (reagent is null) - continue; - - var amount = moles * Atmospherics.BreathMolesToReagentMultiplier; - solution.AddReagent(reagent, amount); - } - } - - public Solution GasToReagent(GasMixture gas) - { - var solution = new Solution(); - GasToReagent(gas, solution); - return solution; - } -} diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs deleted file mode 100644 index 8394d9999bcf16..00000000000000 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ /dev/null @@ -1,259 +0,0 @@ -using Content.Server.Body.Components; -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Organ; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Robust.Shared.Collections; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Timing; - -namespace Content.Server.Body.Systems -{ - public sealed class MetabolizerSystem : EntitySystem - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - - private EntityQuery _organQuery; - private EntityQuery _solutionQuery; - - public override void Initialize() - { - base.Initialize(); - - _organQuery = GetEntityQuery(); - _solutionQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnMetabolizerInit); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - } - - private void OnMapInit(Entity ent, ref MapInitEvent args) - { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - private void OnMetabolizerInit(Entity entity, ref ComponentInit args) - { - if (!entity.Comp.SolutionOnBody) - { - _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName); - } - else if (_organQuery.CompOrNull(entity)?.Body is { } body) - { - _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName); - } - } - - private void OnApplyMetabolicMultiplier( - Entity ent, - ref ApplyMetabolicMultiplierEvent args) - { - // TODO REFACTOR THIS - // This will slowly drift over time due to floating point errors. - // Instead, raise an event with the base rates and allow modifiers to get applied to it. - if (args.Apply) - { - ent.Comp.UpdateInterval *= args.Multiplier; - return; - } - - ent.Comp.UpdateInterval /= args.Multiplier; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count()); - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var comp)) - { - metabolizers.Add((uid, comp)); - } - - foreach (var (uid, metab) in metabolizers) - { - // Only update as frequently as it should - if (_gameTiming.CurTime < metab.NextUpdate) - continue; - - metab.NextUpdate += metab.UpdateInterval; - TryMetabolize((uid, metab)); - } - } - - private void TryMetabolize(Entity ent) - { - _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false); - - // First step is get the solution we actually care about - var solutionName = ent.Comp1.SolutionName; - Solution? solution = null; - Entity? soln = default!; - EntityUid? solutionEntityUid = null; - - if (ent.Comp1.SolutionOnBody) - { - if (ent.Comp2?.Body is { } body) - { - if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false)) - return; - - _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution); - solutionEntityUid = body; - } - } - else - { - if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false)) - return; - - _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution); - solutionEntityUid = ent; - } - - if (solutionEntityUid is null - || soln is null - || solution is null - || solution.Contents.Count == 0) - { - return; - } - - // randomize the reagent list so we don't have any weird quirks - // like alphabetical order or insertion order mattering for processing - var list = solution.Contents.ToArray(); - _random.Shuffle(list); - - int reagents = 0; - foreach (var (reagent, quantity) in list) - { - if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto)) - continue; - - var mostToRemove = FixedPoint2.Zero; - if (proto.Metabolisms is null) - { - if (ent.Comp1.RemoveEmpty) - { - solution.RemoveReagent(reagent, FixedPoint2.New(1)); - } - - continue; - } - - // we're done here entirely if this is true - if (reagents >= ent.Comp1.MaxReagentsProcessable) - return; - - - // loop over all our groups and see which ones apply - if (ent.Comp1.MetabolismGroups is null) - continue; - - foreach (var group in ent.Comp1.MetabolismGroups) - { - if (!proto.Metabolisms.TryGetValue(group.Id, out var entry)) - continue; - - var rate = entry.MetabolismRate * group.MetabolismRateModifier; - - // Remove $rate, as long as there's enough reagent there to actually remove that much - mostToRemove = FixedPoint2.Clamp(rate, 0, quantity); - - float scale = (float) mostToRemove / (float) rate; - - // if it's possible for them to be dead, and they are, - // then we shouldn't process any effects, but should probably - // still remove reagents - if (TryComp(solutionEntityUid.Value, out var state)) - { - if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state)) - continue; - } - - var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value; - var args = new ReagentEffectArgs(actualEntity, ent, solution, proto, mostToRemove, - EntityManager, null, scale); - - // do all effects, if conditions apply - foreach (var effect in entry.Effects) - { - if (!effect.ShouldApply(args, _random)) - continue; - - if (effect.ShouldLog) - { - _adminLogger.Add( - LogType.ReagentEffect, - effect.LogImpact, - $"Metabolism effect {effect.GetType().Name:effect}" - + $" of reagent {proto.LocalizedName:reagent}" - + $" applied on entity {actualEntity:entity}" - + $" at {Transform(actualEntity).Coordinates:coordinates}" - ); - } - - effect.Effect(args); - } - } - - // remove a certain amount of reagent - if (mostToRemove > FixedPoint2.Zero) - { - solution.RemoveReagent(reagent, mostToRemove); - - // We have processed a reagant, so count it towards the cap - reagents += 1; - } - } - - _solutionContainerSystem.UpdateChemicals(soln.Value); - } - } - - // TODO REFACTOR THIS - // This will cause rates to slowly drift over time due to floating point errors. - // Instead, the system that raised this should trigger an update and subscribe to get-modifier events. - [ByRefEvent] - public readonly record struct ApplyMetabolicMultiplierEvent( - EntityUid Uid, - float Multiplier, - bool Apply) - { - /// - /// The entity whose metabolism is being modified. - /// - public readonly EntityUid Uid = Uid; - - /// - /// What the metabolism's update rate will be multiplied by. - /// - public readonly float Multiplier = Multiplier; - - /// - /// If true, apply the multiplier. If false, revert it. - /// - public readonly bool Apply = Apply; - } -} diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs deleted file mode 100644 index 5461f68db2f85f..00000000000000 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ /dev/null @@ -1,353 +0,0 @@ -using Content.Server.Administration.Logs; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Chat.Systems; -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Chemistry.ReagentEffectConditions; -using Content.Server.Chemistry.ReagentEffects; -using Content.Shared.Alert; -using Content.Shared.Atmos; -using Content.Shared.Body.Components; -using Content.Shared.Body.Prototypes; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Damage; -using Content.Shared.Database; -using Content.Shared.Mobs.Systems; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Server.Body.Systems; - -[UsedImplicitly] -public sealed class RespiratorSystem : EntitySystem -{ - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly AlertsSystem _alertsSystem = default!; - [Dependency] private readonly AtmosphereSystem _atmosSys = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; - [Dependency] private readonly DamageableSystem _damageableSys = default!; - [Dependency] private readonly LungSystem _lungSystem = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly IPrototypeManager _protoMan = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly ChatSystem _chat = default!; - - private static readonly ProtoId GasId = new("Gas"); - - public override void Initialize() - { - base.Initialize(); - - // We want to process lung reagents before we inhale new reagents. - UpdatesAfter.Add(typeof(MetabolizerSystem)); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - } - - private void OnMapInit(Entity ent, ref MapInitEvent args) - { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var respirator, out var body)) - { - if (_gameTiming.CurTime < respirator.NextUpdate) - continue; - - respirator.NextUpdate += respirator.UpdateInterval; - - if (_mobState.IsDead(uid)) - continue; - - UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator); - - if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit. - { - switch (respirator.Status) - { - case RespiratorStatus.Inhaling: - Inhale(uid, body); - respirator.Status = RespiratorStatus.Exhaling; - break; - case RespiratorStatus.Exhaling: - Exhale(uid, body); - respirator.Status = RespiratorStatus.Inhaling; - break; - } - } - - if (respirator.Saturation < respirator.SuffocationThreshold) - { - if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown) - { - respirator.LastGaspEmoteTime = _gameTiming.CurTime; - _chat.TryEmoteWithChat(uid, respirator.GaspEmote, ChatTransmitRange.HideChat, ignoreActionBlocker: true); - } - - TakeSuffocationDamage((uid, respirator)); - respirator.SuffocationCycles += 1; - continue; - } - - StopSuffocation((uid, respirator)); - respirator.SuffocationCycles = 0; - } - } - - public void Inhale(EntityUid uid, BodyComponent? body = null) - { - if (!Resolve(uid, ref body, logMissing: false)) - return; - - var organs = _bodySystem.GetBodyOrganComponents(uid, body); - - // Inhale gas - var ev = new InhaleLocationEvent(); - RaiseLocalEvent(uid, ref ev); - - ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true); - - if (ev.Gas is null) - { - return; - } - - var actualGas = ev.Gas.RemoveVolume(Atmospherics.BreathVolume); - - var lungRatio = 1.0f / organs.Count; - var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio); - foreach (var (lung, _) in organs) - { - // Merge doesn't remove gas from the giver. - _atmosSys.Merge(lung.Air, gas); - _lungSystem.GasToReagent(lung.Owner, lung); - } - } - - public void Exhale(EntityUid uid, BodyComponent? body = null) - { - if (!Resolve(uid, ref body, logMissing: false)) - return; - - var organs = _bodySystem.GetBodyOrganComponents(uid, body); - - // exhale gas - - var ev = new ExhaleLocationEvent(); - RaiseLocalEvent(uid, ref ev, broadcast: false); - - if (ev.Gas is null) - { - ev.Gas = _atmosSys.GetContainingMixture(uid, excite: true); - - // Walls and grids without atmos comp return null. I guess it makes sense to not be able to exhale in walls, - // but this also means you cannot exhale on some grids. - ev.Gas ??= GasMixture.SpaceGas; - } - - var outGas = new GasMixture(ev.Gas.Volume); - foreach (var (lung, _) in organs) - { - _atmosSys.Merge(outGas, lung.Air); - lung.Air.Clear(); - - if (_solutionContainerSystem.ResolveSolution(lung.Owner, lung.SolutionName, ref lung.Solution)) - _solutionContainerSystem.RemoveAllSolution(lung.Solution.Value); - } - - _atmosSys.Merge(ev.Gas, outGas); - } - - /// - /// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic - /// gasses). - /// - public bool CanMetabolizeInhaledAir(Entity ent) - { - if (!Resolve(ent, ref ent.Comp)) - return false; - - var ev = new InhaleLocationEvent(); - RaiseLocalEvent(ent, ref ev); - - var gas = ev.Gas ?? _atmosSys.GetContainingMixture(ent.Owner); - if (gas == null) - return false; - - return CanMetabolizeGas(ent, gas); - } - - /// - /// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage - /// (i.e., no toxic gasses). - /// - public bool CanMetabolizeGas(Entity ent, GasMixture gas) - { - if (!Resolve(ent, ref ent.Comp)) - return false; - - var organs = _bodySystem.GetBodyOrganComponents(ent); - if (organs.Count == 0) - return false; - - gas = new GasMixture(gas); - var lungRatio = 1.0f / organs.Count; - gas.Multiply(MathF.Min(lungRatio * gas.Volume/Atmospherics.BreathVolume, lungRatio)); - var solution = _lungSystem.GasToReagent(gas); - - float saturation = 0; - foreach (var organ in organs) - { - saturation += GetSaturation(solution, organ.Comp.Owner, out var toxic); - if (toxic) - return false; - } - - return saturation > ent.Comp.UpdateInterval.TotalSeconds; - } - - /// - /// Get the amount of saturation that would be generated if the lung were to metabolize the given solution. - /// - /// - /// This assumes the metabolism rate is unbounded, which generally should be the case for lungs, otherwise we get - /// back to the old pulmonary edema bug. - /// - /// The reagents to metabolize - /// The entity doing the metabolizing - /// Whether or not any of the reagents would deal damage to the entity - private float GetSaturation(Solution solution, Entity lung, out bool toxic) - { - toxic = false; - if (!Resolve(lung, ref lung.Comp)) - return 0; - - if (lung.Comp.MetabolismGroups == null) - return 0; - - float saturation = 0; - foreach (var (id, quantity) in solution.Contents) - { - var reagent = _protoMan.Index(id.Prototype); - if (reagent.Metabolisms == null) - continue; - - if (!reagent.Metabolisms.TryGetValue(GasId, out var entry)) - continue; - - foreach (var effect in entry.Effects) - { - if (effect is HealthChange health) - toxic |= CanMetabolize(health) && health.Damage.AnyPositive(); - else if (effect is Oxygenate oxy && CanMetabolize(oxy)) - saturation += oxy.Factor * quantity.Float(); - } - } - - // TODO generalize condition checks - // this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture - // Applying actual reaction effects require a full ReagentEffectArgs struct. - bool CanMetabolize(ReagentEffect effect) - { - if (effect.Conditions == null) - return true; - - foreach (var cond in effect.Conditions) - { - if (cond is OrganType organ && !organ.Condition(lung, EntityManager)) - return false; - } - - return true; - } - - return saturation; - } - - private void TakeSuffocationDamage(Entity ent) - { - if (ent.Comp.SuffocationCycles == 2) - _adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating"); - - if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold) - { - // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility - var organs = _bodySystem.GetBodyOrganComponents(ent); - foreach (var (comp, _) in organs) - { - _alertsSystem.ShowAlert(ent, comp.Alert); - } - } - - _damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false); - } - - private void StopSuffocation(Entity ent) - { - if (ent.Comp.SuffocationCycles >= 2) - _adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating"); - - // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility - var organs = _bodySystem.GetBodyOrganComponents(ent); - foreach (var (comp, _) in organs) - { - _alertsSystem.ClearAlert(ent, comp.Alert); - } - - _damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery); - } - - public void UpdateSaturation(EntityUid uid, float amount, - RespiratorComponent? respirator = null) - { - if (!Resolve(uid, ref respirator, false)) - return; - - respirator.Saturation += amount; - respirator.Saturation = - Math.Clamp(respirator.Saturation, respirator.MinSaturation, respirator.MaxSaturation); - } - - private void OnApplyMetabolicMultiplier( - Entity ent, - ref ApplyMetabolicMultiplierEvent args) - { - // TODO REFACTOR THIS - // This will slowly drift over time due to floating point errors. - // Instead, raise an event with the base rates and allow modifiers to get applied to it. - if (args.Apply) - { - ent.Comp.UpdateInterval *= args.Multiplier; - ent.Comp.Saturation *= args.Multiplier; - ent.Comp.MaxSaturation *= args.Multiplier; - ent.Comp.MinSaturation *= args.Multiplier; - return; - } - - // This way we don't have to worry about it breaking if the stasis bed component is destroyed - ent.Comp.UpdateInterval /= args.Multiplier; - ent.Comp.Saturation /= args.Multiplier; - ent.Comp.MaxSaturation /= args.Multiplier; - ent.Comp.MinSaturation /= args.Multiplier; - } -} - -[ByRefEvent] -public record struct InhaleLocationEvent(GasMixture? Gas); - -[ByRefEvent] -public record struct ExhaleLocationEvent(GasMixture? Gas); diff --git a/Content.Server/Body/Systems/StomachSystem.cs b/Content.Server/Body/Systems/StomachSystem.cs deleted file mode 100644 index a4c2e8292dd50b..00000000000000 --- a/Content.Server/Body/Systems/StomachSystem.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Content.Server.Body.Components; -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Body.Organ; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Robust.Shared.Timing; -using Robust.Shared.Utility; - -namespace Content.Server.Body.Systems -{ - public sealed class StomachSystem : EntitySystem - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - - public const string DefaultSolutionName = "stomach"; - - public override void Initialize() - { - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - } - - private void OnMapInit(Entity ent, ref MapInitEvent args) - { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - public override void Update(float frameTime) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var stomach, out var organ, out var sol)) - { - if (_gameTiming.CurTime < stomach.NextUpdate) - continue; - - stomach.NextUpdate += stomach.UpdateInterval; - - // Get our solutions - if (!_solutionContainerSystem.ResolveSolution((uid, sol), DefaultSolutionName, ref stomach.Solution, out var stomachSolution)) - continue; - - if (organ.Body is not { } body || !_solutionContainerSystem.TryGetSolution(body, stomach.BodySolutionName, out var bodySolution)) - continue; - - var transferSolution = new Solution(); - - var queue = new RemQueue(); - foreach (var delta in stomach.ReagentDeltas) - { - delta.Increment(stomach.UpdateInterval); - if (delta.Lifetime > stomach.DigestionDelay) - { - if (stomachSolution.TryGetReagent(delta.ReagentQuantity.Reagent, out var reagent)) - { - if (reagent.Quantity > delta.ReagentQuantity.Quantity) - reagent = new(reagent.Reagent, delta.ReagentQuantity.Quantity); - - stomachSolution.RemoveReagent(reagent); - transferSolution.AddReagent(reagent); - } - - queue.Add(delta); - } - } - - foreach (var item in queue) - { - stomach.ReagentDeltas.Remove(item); - } - - _solutionContainerSystem.UpdateChemicals(stomach.Solution.Value); - - // Transfer everything to the body solution! - _solutionContainerSystem.TryAddSolution(bodySolution.Value, transferSolution); - } - } - - private void OnApplyMetabolicMultiplier( - Entity ent, - ref ApplyMetabolicMultiplierEvent args) - { - if (args.Apply) - { - ent.Comp.UpdateInterval *= args.Multiplier; - return; - } - - // This way we don't have to worry about it breaking if the stasis bed component is destroyed - ent.Comp.UpdateInterval /= args.Multiplier; - } - - public bool CanTransferSolution( - EntityUid uid, - Solution solution, - StomachComponent? stomach = null, - SolutionContainerManagerComponent? solutions = null) - { - return Resolve(uid, ref stomach, ref solutions, logMissing: false) - && _solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution) - // TODO: For now no partial transfers. Potentially change by design - && stomachSolution.CanAddSolution(solution); - } - - public bool TryTransferSolution( - EntityUid uid, - Solution solution, - StomachComponent? stomach = null, - SolutionContainerManagerComponent? solutions = null) - { - if (!Resolve(uid, ref stomach, ref solutions, logMissing: false) - || !_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution) - || !CanTransferSolution(uid, solution, stomach, solutions)) - { - return false; - } - - _solutionContainerSystem.TryAddSolution(stomach.Solution.Value, solution); - // Add each reagent to ReagentDeltas. Used to track how long each reagent has been in the stomach - foreach (var reagent in solution.Contents) - { - stomach.ReagentDeltas.Add(new StomachComponent.ReagentDelta(reagent)); - } - - return true; - } - } -} diff --git a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs index 7b70497c7d3fab..62368beadeaf89 100644 --- a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs +++ b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs @@ -17,6 +17,7 @@ using Robust.Shared.GameStates; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Shared.Medical.Blood.Components; using Robust.Server.Audio; namespace Content.Server.Chemistry.EntitySystems; diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs index aac171371fb11a..e502175599e673 100644 --- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs @@ -11,14 +11,18 @@ using Content.Shared.Forensics; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using Content.Shared.Mobs.Components; using Content.Shared.Stacks; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; namespace Content.Server.Chemistry.EntitySystems; public sealed class InjectorSystem : SharedInjectorSystem { - [Dependency] private readonly BloodstreamSystem _blood = default!; + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; public override void Initialize() @@ -50,12 +54,14 @@ private bool TryUseInjector(Entity injector, EntityUid target if (injector.Comp.ToggleState == InjectorToggleMode.Draw) { + //TODO: hook up TryDraw again on InjectorSystem // Draw from a bloodstream, if the target has that - if (TryComp(target, out var stream) && - SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution)) - { - return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user); - } + // // Draw from a bloodstream, if the target has that + // if (TryComp(target, out var stream) && + // SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution)) + // { + // return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user); + // } // Draw from an object (food, beaker, etc) if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _)) @@ -208,37 +214,38 @@ private void InjectDoAfter(Entity injector, EntityUid target, private bool TryInjectIntoBloodstream(Entity injector, Entity target, EntityUid user) { + //TODO: re-implement injection on InjectorSystem // Get transfer amount. May be smaller than _transferAmount if not enough room - if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, - ref target.Comp.ChemicalSolution, out var chemSolution)) - { - Popup.PopupEntity( - Loc.GetString("injector-component-cannot-inject-message", - ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); - return false; - } - - var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume); - if (realTransferAmount <= 0) - { - Popup.PopupEntity( - Loc.GetString("injector-component-cannot-inject-message", - ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); - return false; - } - - // Move units from attackSolution to targetSolution - var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount); - - _blood.TryAddToChemicals(target, removedSolution, target.Comp); - - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - - Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message", - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); - - Dirty(injector); +// if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, +// ref target.Comp.ChemicalSolution, out var chemSolution)) +// { +// Popup.PopupEntity( +// Loc.GetString("injector-component-cannot-inject-message", +// ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); +// return false; +// } +// +// var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume); +// if (realTransferAmount <= 0) +// { +// Popup.PopupEntity( +// Loc.GetString("injector-component-cannot-inject-message", +// ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); +// return false; +// } +// +// // Move units from attackSolution to targetSolution +// var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount); +// +// _blood.TryAddToChemicals(target, removedSolution, target.Comp); +// +// _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); +// +// Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message", +// ("amount", removedSolution.Volume), +// ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); + + Dirty(injector); AfterInject(injector, target); return true; } @@ -365,20 +372,21 @@ private void DrawFromBlood(Entity injector, Entity entity, ref AfterInt _popup.PopupEntity(Loc.GetString(entity.Comp.MixMessage, ("mixed", Identity.Entity(args.Target.Value, EntityManager)), ("mixer", Identity.Entity(entity.Owner, EntityManager))), args.User, args.User); - _solutionContainers.UpdateChemicals(solution.Value, true, entity.Comp); + _solutionContainers.UpdateChemicals(solution.Value, true, true, entity.Comp); var afterMixingEvent = new AfterMixingEvent(entity, args.Target.Value); RaiseLocalEvent(entity, afterMixingEvent); diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs index 3c57cc31afd3ea..d555b2f004a0a8 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs @@ -3,6 +3,8 @@ using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Shared.Inventory; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using Content.Shared.Popups; using Content.Shared.Projectiles; using Content.Shared.Tag; @@ -138,8 +140,9 @@ private bool TryInjectTargets(Entity injecto // Take our portion of the adjusted solution for this target var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream); // Inject our portion into the target's bloodstream - if (_bloodstream.TryAddToChemicals(targetBloodstream.Owner, individualInjection, targetBloodstream.Comp)) - anySuccess = true; + //TODO Bloodstream: Reimplement this + // if (_bloodstream.TryAddToChemicals(targetBloodstream.Owner, individualInjection, targetBloodstream.Comp)) + // anySuccess = true; } // Huzzah! diff --git a/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs b/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs index 986c3d79c8d2f2..b2603703451de8 100644 --- a/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs +++ b/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs @@ -1,6 +1,6 @@ -using Content.Server.Body.Components; using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Medical.Metabolism.Components; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -30,11 +30,12 @@ public override bool Condition(ReagentEffectArgs args) public bool Condition(Entity metabolizer, IEntityManager entMan) { - metabolizer.Comp ??= entMan.GetComponentOrNull(metabolizer.Owner); - if (metabolizer.Comp != null - && metabolizer.Comp.MetabolizerTypes != null - && metabolizer.Comp.MetabolizerTypes.Contains(Type)) - return ShouldHave; + //TODO Metabolism: Reimplement this +// metabolizer.Comp ??= entMan.GetComponentOrNull(metabolizer.Owner); +// if (metabolizer.Comp != null +// && metabolizer.Comp.MetabolizerTypes != null +// && metabolizer.Comp.MetabolizerTypes.Contains(Type)) +// return ShouldHave; return !ShouldHave; } diff --git a/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs b/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs index ace73c84f3ae48..0fc2094dd09f79 100644 --- a/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs +++ b/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs @@ -26,8 +26,10 @@ public override void Effect(ReagentEffectArgs args) cleanseRate *= args.Scale; - var bloodstreamSys = args.EntityManager.System(); - bloodstreamSys.FlushChemicals(args.SolutionEntity, args.Reagent.ID, cleanseRate); + //TODO: refactor chem flushing from bloodstream (why does this even exist) + + //var bloodstreamSys = args.EntityManager.System(); + //bloodstreamSys.FlushChemicals(args.SolutionEntity, args.Reagent.ID, cleanseRate); } } } diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs b/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs index ecd9c86255fcec..9262643e8ef455 100644 --- a/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs +++ b/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs @@ -1,7 +1,11 @@ using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using Robust.Shared.Prototypes; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; namespace Content.Server.Chemistry.ReagentEffects; @@ -17,6 +21,8 @@ public sealed partial class ModifyBleedAmount : ReagentEffect => Loc.GetString("reagent-effect-guidebook-modify-bleed-amount", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); + + //TODO: Refactor modify bleed amount in reagent effects public override void Effect(ReagentEffectArgs args) { if (args.EntityManager.TryGetComponent(args.SolutionEntity, out var blood)) @@ -24,8 +30,7 @@ public override void Effect(ReagentEffectArgs args) var sys = args.EntityManager.System(); var amt = Scaled ? Amount * args.Quantity.Float() : Amount; amt *= args.Scale; - - sys.TryModifyBleedAmount(args.SolutionEntity, amt, blood); + //sys.TryModifyBleedAmount(args.SolutionEntity, amt, blood); } } } diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs b/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs index b136ff9bb8c156..38c27cfd8c7819 100644 --- a/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs +++ b/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs @@ -2,7 +2,11 @@ using Content.Server.Body.Systems; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using Robust.Shared.Prototypes; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; namespace Content.Server.Chemistry.ReagentEffects; @@ -18,6 +22,7 @@ public sealed partial class ModifyBloodLevel : ReagentEffect => Loc.GetString("reagent-effect-guidebook-modify-blood-level", ("chance", Probability), ("deltasign", MathF.Sign(Amount.Float()))); + //TODO: Refactor modify bleed level in reagent effects public override void Effect(ReagentEffectArgs args) { if (args.EntityManager.TryGetComponent(args.SolutionEntity, out var blood)) @@ -26,7 +31,7 @@ public override void Effect(ReagentEffectArgs args) var amt = Scaled ? Amount * args.Quantity : Amount; amt *= args.Scale; - sys.TryModifyBloodLevel(args.SolutionEntity, amt, blood); + // sys.TryModifyBloodLevel(args.SolutionEntity, amt, blood); } } } diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs b/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs index e7466fbc85d7db..d0ba837dbe644c 100644 --- a/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs +++ b/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs @@ -16,15 +16,16 @@ public sealed partial class ModifyLungGas : ReagentEffect public override void Effect(ReagentEffectArgs args) { - if (!args.EntityManager.TryGetComponent(args.OrganEntity, out var lung)) - return; - - foreach (var (gas, ratio) in _ratios) - { - var quantity = ratio * args.Quantity.Float() / Atmospherics.BreathMolesToReagentMultiplier; - if (quantity < 0) - quantity = Math.Max(quantity, -lung.Air[(int)gas]); - lung.Air.AdjustMoles(gas, quantity); - } + //TODO Lungs: Reimplement this + // if (!args.EntityManager.TryGetComponent(args.OrganEntity, out var lung)) + // return; + // + // foreach (var (gas, ratio) in _ratios) + // { + // var quantity = ratio * args.Quantity.Float() / Atmospherics.BreathMolesToReagentMultiplier; + // if (quantity < 0) + // quantity = Math.Max(quantity, -lung.Air[(int)gas]); + // lung.Air.AdjustMoles(gas, quantity); + // } } } diff --git a/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs b/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs index 6c5ab155e44011..56fb3e8f10e21a 100644 --- a/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs +++ b/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs @@ -16,10 +16,11 @@ public sealed partial class Oxygenate : ReagentEffect public override void Effect(ReagentEffectArgs args) { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out var resp)) - { - var respSys = args.EntityManager.System(); - respSys.UpdateSaturation(args.SolutionEntity, args.Quantity.Float() * Factor, resp); - } + //TODO Metabolism: reimplement this + // if (args.EntityManager.TryGetComponent(args.SolutionEntity, out var resp)) + // { + // var respSys = EntitySystem.Get(); + // respSys.UpdateSaturation(args.SolutionEntity, args.Quantity.Float() * Factor, resp); + // } } } diff --git a/Content.Server/Devour/DevourSystem.cs b/Content.Server/Devour/DevourSystem.cs index d9c50f260a311d..03d2c7673eb1f1 100644 --- a/Content.Server/Devour/DevourSystem.cs +++ b/Content.Server/Devour/DevourSystem.cs @@ -4,6 +4,8 @@ using Content.Shared.Devour; using Content.Shared.Devour.Components; using Content.Shared.Humanoid; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Systems; namespace Content.Server.Devour; @@ -35,7 +37,8 @@ private void OnDoAfter(EntityUid uid, DevourerComponent component, DevourDoAfter { ContainerSystem.Insert(args.Args.Target.Value, component.Stomach); } - _bloodstreamSystem.TryAddToChemicals(uid, ichorInjection); + //TODO: re-implement bloodstream injection for devour system + //_bloodstreamSystem.TryAddToChemicals(uid, ichorInjection); } //TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow. diff --git a/Content.Server/Disposal/Unit/EntitySystems/BeingDisposedSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/BeingDisposedSystem.cs index 6fbfb1523a1a56..76e16986bf22fb 100644 --- a/Content.Server/Disposal/Unit/EntitySystems/BeingDisposedSystem.cs +++ b/Content.Server/Disposal/Unit/EntitySystems/BeingDisposedSystem.cs @@ -10,8 +10,8 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnInhaleLocation); - SubscribeLocalEvent(OnExhaleLocation); + // SubscribeLocalEvent(OnInhaleLocation); + // SubscribeLocalEvent(OnExhaleLocation); SubscribeLocalEvent(OnGetAir); } @@ -23,20 +23,20 @@ private void OnGetAir(EntityUid uid, BeingDisposedComponent component, ref Atmos args.Handled = true; } } - - private void OnInhaleLocation(EntityUid uid, BeingDisposedComponent component, InhaleLocationEvent args) - { - if (TryComp(component.Holder, out var holder)) - { - args.Gas = holder.Air; - } - } - - private void OnExhaleLocation(EntityUid uid, BeingDisposedComponent component, ExhaleLocationEvent args) - { - if (TryComp(component.Holder, out var holder)) - { - args.Gas = holder.Air; - } - } + //TODO Metabolism: reimplement this + // private void OnInhaleLocation(EntityUid uid, BeingDisposedComponent component, InhaleLocationEvent args) + // { + // if (TryComp(component.Holder, out var holder)) + // { + // args.Gas = holder.Air; + // } + // } + // + // private void OnExhaleLocation(EntityUid uid, BeingDisposedComponent component, ExhaleLocationEvent args) + // { + // if (TryComp(component.Holder, out var holder)) + // { + // args.Gas = holder.Air; + // } + // } } diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs index 9638dabf28ea69..3743ec32ce4164 100644 --- a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs +++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs @@ -22,7 +22,9 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using System.Linq; - +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; namespace Content.Server.Fluids.EntitySystems; @@ -39,7 +41,7 @@ public sealed class SmokeSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly AppearanceSystem _appearance = default!; - [Dependency] private readonly BloodstreamSystem _blood = default!; + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; [Dependency] private readonly InternalsSystem _internals = default!; [Dependency] private readonly ReactiveSystem _reactive = default!; [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; @@ -262,38 +264,39 @@ private void ReactWithEntity(EntityUid entity, EntityUid smokeUid, Solution solu if (!Resolve(smokeUid, ref component)) return; - if (!TryComp(entity, out var bloodstream)) - return; - - if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.ChemicalSolutionName, ref bloodstream.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0) - return; - - var blockIngestion = _internals.AreInternalsWorking(entity); - - var cloneSolution = solution.Clone(); - var availableTransfer = FixedPoint2.Min(cloneSolution.Volume, component.TransferRate); - var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume); - var transferSolution = cloneSolution.SplitSolution(transferAmount); - - foreach (var reagentQuantity in transferSolution.Contents.ToArray()) - { - if (reagentQuantity.Quantity == FixedPoint2.Zero) - continue; - var reagentProto = _prototype.Index(reagentQuantity.Reagent.Prototype); - - _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution); - if (!blockIngestion) - _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution); - } - - if (blockIngestion) - return; - - if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream)) - { - // Log solution addition by smoke - _logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}"); - } + //TODO: refactor smoke system to use respiration instead of bloodstream + + // if (!TryComp(entity, out var bloodstream)) + // return; + // if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.ChemicalSolutionName, ref bloodstream.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0) + // return; + // + // var blockIngestion = _internals.AreInternalsWorking(entity); + // + // var cloneSolution = solution.Clone(); + // var availableTransfer = FixedPoint2.Min(cloneSolution.Volume, component.TransferRate); + // var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume); + // var transferSolution = cloneSolution.SplitSolution(transferAmount); + // + // foreach (var reagentQuantity in transferSolution.Contents.ToArray()) + // { + // if (reagentQuantity.Quantity == FixedPoint2.Zero) + // continue; + // var reagentProto = _prototype.Index(reagentQuantity.Reagent.Prototype); + // + // _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution); + // if (!blockIngestion) + // _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution); + // } + // + // if (blockIngestion) + // return; + // + // if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream)) + // { + // // Log solution addition by smoke + // _logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}"); + // } } private void ReactOnTile(EntityUid uid, SmokeComponent? component = null, TransformComponent? xform = null) diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs index b738d28b467402..9056ec8872a7de 100644 --- a/Content.Server/Mech/Systems/MechSystem.cs +++ b/Content.Server/Mech/Systems/MechSystem.cs @@ -61,8 +61,8 @@ public override void Initialize() SubscribeLocalEvent(OnToolUseAttempt); - SubscribeLocalEvent(OnInhale); - SubscribeLocalEvent(OnExhale); + // SubscribeLocalEvent(OnInhale); + // SubscribeLocalEvent(OnExhale); SubscribeLocalEvent(OnExpose); SubscribeLocalEvent(OnGetFilterAir); @@ -378,29 +378,30 @@ public void RemoveBattery(EntityUid uid, MechComponent? component = null) } #region Atmos Handling - private void OnInhale(EntityUid uid, MechPilotComponent component, InhaleLocationEvent args) - { - if (!TryComp(component.Mech, out var mech) || - !TryComp(component.Mech, out var mechAir)) - { - return; - } - - if (mech.Airtight) - args.Gas = mechAir.Air; - } - - private void OnExhale(EntityUid uid, MechPilotComponent component, ExhaleLocationEvent args) - { - if (!TryComp(component.Mech, out var mech) || - !TryComp(component.Mech, out var mechAir)) - { - return; - } - - if (mech.Airtight) - args.Gas = mechAir.Air; - } + //TODO Metabolism: reimplement this + // private void OnInhale(EntityUid uid, MechPilotComponent component, InhaleLocationEvent args) + // { + // if (!TryComp(component.Mech, out var mech) || + // !TryComp(component.Mech, out var mechAir)) + // { + // return; + // } + // + // if (mech.Airtight) + // args.Gas = mechAir.Air; + // } + // + // private void OnExhale(EntityUid uid, MechPilotComponent component, ExhaleLocationEvent args) + // { + // if (!TryComp(component.Mech, out var mech) || + // !TryComp(component.Mech, out var mechAir)) + // { + // return; + // } + // + // if (mech.Airtight) + // args.Gas = mechAir.Air; + // } private void OnExpose(EntityUid uid, MechPilotComponent component, ref AtmosExposedGetAirEvent args) { diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index a6285294c94c1d..8c1cd1594e85c1 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -1,5 +1,4 @@ using System.Numerics; -using Content.Server.Body.Components; using Content.Server.Botany.Components; using Content.Server.Fluids.EntitySystems; using Content.Server.Materials; @@ -20,6 +19,7 @@ using Content.Shared.Medical; using Content.Shared.Mind; using Content.Shared.Materials; +using Content.Shared.Medical.Blood.Components; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; @@ -31,6 +31,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; namespace Content.Server.Medical.BiomassReclaimer { diff --git a/Content.Server/Medical/Commands/Debug/CreateWoundOnTarget.cs b/Content.Server/Medical/Commands/Debug/CreateWoundOnTarget.cs new file mode 100644 index 00000000000000..a3a341a0a75356 --- /dev/null +++ b/Content.Server/Medical/Commands/Debug/CreateWoundOnTarget.cs @@ -0,0 +1,95 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.Medical.Wounding.Components; +using Content.Shared.Medical.Wounding.Systems; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; + +namespace Content.Server.Medical.Commands.Debug; + +[AdminCommand(AdminFlags.Debug)] +public sealed class CreateWoundOnTarget : LocalizedCommands +{ + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + + public override string Command { get; } = "CreateWoundOnTarget"; + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length is < 2 or > 3) + { + shell.WriteError("Incorrect arguments"); + return; + } + + if (!int.TryParse(args[0], out var rawId)) + { + shell.WriteError("Target entityId is not an number"); + return; + } + + var target = _entityManager.GetEntity(new NetEntity(rawId)); + if (!target.IsValid()) + { + shell.WriteError("Target EntityId is invalid"); + return; + } + + if (!_prototypeManager.HasIndex(args[1])) + { + shell.WriteError("Wound prototypeId is invalid"); + return; + } + + CreateWound(target, args[1], null ,shell); + } + private void CreateWound(EntityUid target, string protoId, EntityUid? woundableEnt, IConsoleShell shell) + { + if (!_entityManager.TrySystem(out SharedBodySystem? bodySystem) || + !_entityManager.TrySystem(out WoundSystem? woundSystem) || + !_entityManager.TryGetComponent(target, out BodyComponent? body)) + return; + shell.WriteLine($"Creating wound on Entity:{target}"); + if (woundableEnt == null) + { + shell.WriteLine($"Target woundable not specified using RootPart!"); + if (body.RootContainer.ContainedEntity == null) + { + shell.WriteLine($"Target does not have a rootPart!"); + return; + } + if (!_entityManager.TryGetComponent(body.RootContainer.ContainedEntity, + out var woundable)) + { + shell.WriteLine($"RootPart Entity:{body.RootContainer.ContainedEntity} is not Woundable. Failed to create Wound!"); + return; + } + woundableEnt = body.RootContainer.ContainedEntity; + if (!woundSystem.CreateWoundOnWoundable( + new(woundableEnt.Value, woundable), protoId)) + { + shell.WriteLine($"Failed to create Wound!"); + return; + } + } + else + { + if (!_entityManager.TryGetComponent(woundableEnt.Value, out var woundable2)) + { + shell.WriteLine($"Target Entity:{body.RootContainer.ContainedEntity} is not Woundable. Failed to create Wound!"); + return; + } + + if (!woundSystem.CreateWoundOnWoundable( + new(woundableEnt.Value, woundable2), protoId)) + { + shell.WriteLine($"Failed to create Wound!"); + return; + } + } + shell.WriteLine($"{protoId} Wound created on Entity{woundableEnt}!"); + } +} + diff --git a/Content.Server/Medical/Commands/Debug/PrintAllWounds.cs b/Content.Server/Medical/Commands/Debug/PrintAllWounds.cs new file mode 100644 index 00000000000000..8eba20ff1c8d15 --- /dev/null +++ b/Content.Server/Medical/Commands/Debug/PrintAllWounds.cs @@ -0,0 +1,73 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.IdentityManagement; +using Content.Shared.Medical.Wounding.Components; +using Content.Shared.Medical.Wounding.Systems; +using Robust.Shared.Console; + +namespace Content.Server.Medical.Commands.Debug; + +[AdminCommand(AdminFlags.Debug)] +public sealed class PrintAllWounds : LocalizedCommands +{ + [Dependency] private IEntityManager _entityManager = default!; + + public override string Command { get; } = "PrintAllWounds"; + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Incorrect arguments"); + return; + } + + if (!int.TryParse(args[0], out var rawId)) + { + shell.WriteError("Argument is not an number"); + return; + } + + + var target = _entityManager.GetEntity(new NetEntity(rawId)); + if (!target.IsValid()) + { + shell.WriteError("EntityId Invalid"); + return; + } + + PrintAllWoundData(target, shell); + } + private void PrintAllWoundData(EntityUid target, IConsoleShell shell) + { + if (!_entityManager.TrySystem(out SharedBodySystem? bodySystem) || + !_entityManager.TrySystem(out WoundSystem? woundSystem) || + !_entityManager.TryGetComponent(target, out BodyComponent? body)) + return; + string output = ""; + + output += "=====================================================\n"; + output += $"Printing wounds for Entity:{target} {Identity.Name(target, _entityManager)}\n"; + + var woundsFound = false; + foreach (var (bodyPartId, _) in bodySystem.GetBodyChildren(target, body)) + { + if (!_entityManager.TryGetComponent(bodyPartId, out var woundable)) + continue; + output += "----------------------------------------------\n"; + output += $"BodyPart:{bodyPartId} {Identity.Name(bodyPartId, _entityManager)} HP:{woundable.Health} Int:{woundable.Integrity}\n"; + foreach (var wound in woundSystem.GetAllWounds(new (bodyPartId, woundable))) + { + output += $"WoundEntityId:{wound.Owner} {_entityManager.GetComponent(wound.Owner).EntityPrototype!.ID} | Severity {wound.Comp.Severity}\n"; + } + woundsFound = true; + } + if (!woundsFound) + { + output += "No wounds found!\n"; + } + output += "=====================================================\n"; + shell.WriteLine(output); + } +} diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 8d54fc6dd951d0..85f7326da83457 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -27,12 +27,16 @@ using Content.Shared.Emag.Systems; using Content.Shared.Examine; using Content.Shared.Interaction; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using Content.Shared.Medical.Cryogenics; using Content.Shared.MedicalScanner; using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.Timing; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem; namespace Content.Server.Medical; @@ -116,7 +120,8 @@ public override void Update(float frameTime) } var solutionToInject = _solutionContainerSystem.SplitSolution(containerSolution.Value, cryoPod.BeakerTransferAmount); - _bloodstreamSystem.TryAddToChemicals(patient.Value, solutionToInject, bloodstream); + //TODO: re-implement cryopod IV / chem injection + //_bloodstreamSystem.TryAddToChemicals(patient.Value, solutionToInject, bloodstream); _reactiveSystem.DoEntityReaction(patient.Value, solutionToInject, ReactionMethod.Injection); } } @@ -194,19 +199,33 @@ private void OnActivateUI(Entity entity, ref AfterActivatableU healthAnalyzer.ScannedEntity = entity.Comp.BodyContainer.ContainedEntity; } + //TODO: re-implement cryopod analyzer UI // TODO: This should be a state my dude - _userInterfaceSystem.ServerSendUiMessage( - entity.Owner, - HealthAnalyzerUiKey.Key, - new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity), - temp?.CurrentTemperature ?? 0, - (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value, - bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) - ? bloodSolution.FillFraction - : 0, - null, - null - )); + // _userInterfaceSystem.ServerSendUiMessage( + // entity.Owner, + // HealthAnalyzerUiKey.Key, + // new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity), + // temp?.CurrentTemperature ?? 0, + // (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value, + // bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) + // ? bloodSolution.FillFraction + // : 0, + // null, + // null + // )); + //TODO: re-implement cryopod analyzer UI + // _userInterfaceSystem.TrySendUiMessage( + // entity.Owner, + // HealthAnalyzerUiKey.Key, + // new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity), + // temp?.CurrentTemperature ?? 0, + // (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value, + // bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) + // ? bloodSolution.FillFraction + // : 0, + // null, + // null + // )); } private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs index ed33e60dac4806..acf3a548ad6b37 100644 --- a/Content.Server/Medical/HealingSystem.cs +++ b/Content.Server/Medical/HealingSystem.cs @@ -13,6 +13,9 @@ using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Medical; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Systems; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -61,22 +64,23 @@ entity.Comp.DamageContainerID is not null && return; } + //TODO: remove bloodloss healing entirely. // Heal some bloodloss damage. - if (healing.BloodlossModifier != 0) - { - if (!TryComp(entity, out var bloodstream)) - return; - var isBleeding = bloodstream.BleedAmount > 0; - _bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier); - if (isBleeding != bloodstream.BleedAmount > 0) - { - _popupSystem.PopupEntity(Loc.GetString("medical-item-stop-bleeding"), entity, args.User); - } - } - - // Restores missing blood - if (healing.ModifyBloodLevel != 0) - _bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel); + // if (healing.BloodlossModifier != 0) + // { + // if (!TryComp(entity, out var bloodstream)) + // return; + // var isBleeding = bloodstream.BleedAmount > 0; + // _bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier); + // if (isBleeding != bloodstream.BleedAmount > 0) + // { + // _popupSystem.PopupEntity(Loc.GetString("medical-item-stop-bleeding"), entity, args.User); + // } + // } + // + // // Restores missing blood + // if (healing.ModifyBloodLevel != 0) + // _bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel); var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage, true, origin: args.Args.User); @@ -170,12 +174,13 @@ targetDamage.DamageContainerID is not null && if (TryComp(uid, out var stack) && stack.Count < 1) return false; - var anythingToDo = - HasDamage(targetDamage, component) || - component.ModifyBloodLevel > 0 // Special case if healing item can restore lost blood... - && TryComp(target, out var bloodstream) - && _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution) - && bloodSolution.Volume < bloodSolution.MaxVolume; // ...and there is lost blood to restore. + //TODO: healing system needs to be completely refactored to fit newMed. + var anythingToDo = HasDamage(targetDamage, component); + // HasDamage(targetDamage, component) || + // component.ModifyBloodLevel > 0 // Special case if healing item can restore lost blood... + // && TryComp(target, out var bloodstream) + // && _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution) + // && bloodSolution.Volume < bloodSolution.MaxVolume; // ...and there is lost blood to restore. if (!anythingToDo) { diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 7282ea197c9887..fc53841b9ee1df 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Body.Components; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Medical.Components; using Content.Server.PowerCell; @@ -186,13 +185,14 @@ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool s var bloodAmount = float.NaN; var bleeding = false; - if (TryComp(target, out var bloodstream) && - _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, - ref bloodstream.BloodSolution, out var bloodSolution)) - { - bloodAmount = bloodSolution.FillFraction; - bleeding = bloodstream.BleedAmount > 0; - } + //TODO: Refactor health analyzer to check blood volume/blood pressure + // if (TryComp(target, out var bloodstream) && + // _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, + // ref bloodstream.BloodSolution, out var bloodSolution)) + // { + // bloodAmount = bloodSolution.FillFraction; + // bleeding = bloodstream.BleedAmount > 0; + // } _uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage( GetNetEntity(target), diff --git a/Content.Server/Medical/InsideCryoPodSystem.cs b/Content.Server/Medical/InsideCryoPodSystem.cs index 21827c105f9904..d6e65fb6df1e15 100644 --- a/Content.Server/Medical/InsideCryoPodSystem.cs +++ b/Content.Server/Medical/InsideCryoPodSystem.cs @@ -11,8 +11,8 @@ public override void InitializeInsideCryoPod() { base.InitializeInsideCryoPod(); // Atmos overrides - SubscribeLocalEvent(OnInhaleLocation); - SubscribeLocalEvent(OnExhaleLocation); + // SubscribeLocalEvent(OnInhaleLocation); + // SubscribeLocalEvent(OnExhaleLocation); SubscribeLocalEvent(OnGetAir); } @@ -26,22 +26,22 @@ private void OnGetAir(EntityUid uid, InsideCryoPodComponent component, ref Atmos args.Handled = true; } } - - private void OnInhaleLocation(EntityUid uid, InsideCryoPodComponent component, InhaleLocationEvent args) - { - if (TryComp(Transform(uid).ParentUid, out var cryoPodAir)) - { - args.Gas = cryoPodAir.Air; - } - } - - private void OnExhaleLocation(EntityUid uid, InsideCryoPodComponent component, ExhaleLocationEvent args) - { - if (TryComp(Transform(uid).ParentUid, out var cryoPodAir)) - { - args.Gas = cryoPodAir.Air; - } - } + //TODO Metabolism: reimplement this + // private void OnInhaleLocation(EntityUid uid, InsideCryoPodComponent component, InhaleLocationEvent args) + // { + // if (TryComp(Transform(uid).ParentUid, out var cryoPodAir)) + // { + // args.Gas = cryoPodAir.Air; + // } + // } + // + // private void OnExhaleLocation(EntityUid uid, InsideCryoPodComponent component, ExhaleLocationEvent args) + // { + // if (TryComp(Transform(uid).ParentUid, out var cryoPodAir)) + // { + // args.Gas = cryoPodAir.Air; + // } + // } #endregion } diff --git a/Content.Server/Medical/Respiration/LungsSystem.cs b/Content.Server/Medical/Respiration/LungsSystem.cs new file mode 100644 index 00000000000000..5920f3ae1e2d61 --- /dev/null +++ b/Content.Server/Medical/Respiration/LungsSystem.cs @@ -0,0 +1,83 @@ +using Content.Server.Atmos; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Body.Systems; +using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Body.Events; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Respiration.Components; +using Content.Shared.Medical.Respiration.Events; +using Content.Shared.Medical.Respiration.Systems; + +namespace Content.Server.Medical.Respiration; + +public sealed class LungsSystem : SharedLungsSystem +{ + [Dependency] private AtmosphereSystem _atmosSystem = default!; + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var lungsTickingComp, out var lungsComp)) + { + if (GameTiming.CurTime >= lungsTickingComp.NextPhasedUpdate) + { + var lungs = (uid, lungsComp); + UpdateBreathability(lungs); + var attempt = new BreathAttemptEvent((uid, lungsComp)); + RaiseLocalEvent(uid, ref attempt); + if (!attempt.Canceled) + BreathCycle(lungs); + SetNextPhaseDelay((uid, lungsComp, lungsTickingComp)); + if (lungsComp.Phase != BreathingPhase.Suffocating) + AbsorbGases(lungs); + } + + if (GameTiming.CurTime >= lungsTickingComp.NextUpdate) + { + var lungs = (uid, lungsComp); + UpdateBreathability(lungs); + AbsorbGases(lungs); + lungsTickingComp.NextUpdate = GameTiming.CurTime + lungsTickingComp.UpdateRate; + } + } + } + + /// + /// Equalizes lung pressure, this should move air appropriately while inhaling/exhaling. This will also forcibly remove all + /// air in the lungs when the owner is exposed to low pressure or vacuum. + /// + /// lung gas mixture holder component + protected override void EqualizeLungPressure(Entity lungs) + { + var extGas = GetBreathingAtmosphere(lungs); + if (extGas == null) + return; + if (lungs.Comp.ContainedGas.Pressure > extGas.Pressure) + { + _atmosSystem.PumpGasTo(lungs.Comp.ContainedGas, extGas, lungs.Comp.ContainedGas.Pressure); + } + if (lungs.Comp.ContainedGas.Pressure <= extGas.Pressure) + { + _atmosSystem.PumpGasTo(extGas, lungs.Comp.ContainedGas, extGas.Pressure); + } + Dirty(lungs); + } + + protected override void EmptyLungs(Entity lungs) + { + var externalGas = _atmosSystem.GetContainingMixture(lungs.Comp.SolutionOwnerEntity, excite: true); + _atmosSystem.ReleaseGasTo(lungs.Comp.ContainedGas, externalGas, lungs.Comp.ContainedGas.Volume); + base.EmptyLungs(lungs); + } + + protected override GasMixture? GetBreathingAtmosphere(Entity lungs) + { + return _atmosSystem.GetContainingMixture(lungs.Comp.SolutionOwnerEntity, excite: true); + } + +} diff --git a/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs b/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs index b8304c562a4e21..f198ef4a0f966e 100644 --- a/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs +++ b/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs @@ -121,11 +121,12 @@ private void OnDoAfter(EntityUid uid, StethoscopeComponent component, DoAfterEve public void ExamineWithStethoscope(EntityUid user, EntityUid target) { // The mob check seems a bit redundant but (1) they could conceivably have lost it since when the doafter started and (2) I need it for .IsDead() - if (!HasComp(target) || !TryComp(target, out var mobState) || _mobStateSystem.IsDead(target, mobState)) - { - _popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, user); - return; - } + //TODO Respiration: reimplement this + // if (!HasComp(target) || !TryComp(target, out var mobState) || _mobStateSystem.IsDead(target, mobState)) + // { + // _popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, user); + // return; + // } if (!TryComp(target, out var damage)) return; diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index e0917f32a84426..8075eaf35763a3 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -342,8 +342,9 @@ public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = n // Get mob total damage crit threshold int? totalDamageThreshold = null; - if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, Shared.Mobs.MobState.Critical, out var critThreshold)) - totalDamageThreshold = critThreshold.Value.Int(); + //TODO: NewMed convert this not to require mob thresholds! + // if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, Shared.Mobs.MobState.Critical, out var critThreshold)) + // totalDamageThreshold = critThreshold.Value.Int(); // finally, form suit sensor status var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob, userJobIcon, userJobDepartments); diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs index 8c3b15aed33aba..8ce2b3a003cf08 100644 --- a/Content.Server/Medical/VomitSystem.cs +++ b/Content.Server/Medical/VomitSystem.cs @@ -7,12 +7,14 @@ using Content.Server.Stunnable; using Content.Shared.Chemistry.Components; using Content.Shared.IdentityManagement; +using Content.Shared.Medical.Blood.Components; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.StatusEffect; using Robust.Server.Audio; using Robust.Shared.Audio; using Robust.Shared.Prototypes; +using BloodstreamComponent = Content.Shared.Medical.Blood.Components.BloodstreamComponent; namespace Content.Server.Medical { @@ -35,8 +37,9 @@ public sealed class VomitSystem : EntitySystem public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = -40f) { // Main requirement: You have a stomach - var stomachList = _body.GetBodyOrganComponents(uid); - if (stomachList.Count == 0) + //TODO Digestion: Re-Implement this + // var stomachList = _body.GetBodyOrganComponents(uid); + // if (stomachList.Count == 0) return; // Vomiting makes you hungrier and thirstier @@ -56,15 +59,16 @@ public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = - var solution = new Solution(); // Empty the stomach out into it - foreach (var stomach in stomachList) - { - if (_solutionContainer.ResolveSolution(stomach.Comp.Owner, StomachSystem.DefaultSolutionName, ref stomach.Comp.Solution, out var sol)) - { - solution.AddSolution(sol, _proto); - sol.RemoveAllSolution(); - _solutionContainer.UpdateChemicals(stomach.Comp.Solution.Value); - } - } + //TODO Digestion: Re-Implement this + // foreach (var stomach in stomachList) + // { + // if (_solutionContainer.ResolveSolution(stomach.Comp.Owner, StomachSystem.DefaultSolutionName, ref stomach.Comp.Solution, out var sol)) + // { + // solution.AddSolution(sol, _proto); + // sol.RemoveAllSolution(); + // _solutionContainer.UpdateChemicals(stomach.Comp.Solution.Value); + // } + // } // Adds a tiny amount of the chem stream from earlier along with vomit if (TryComp(uid, out var bloodStream)) { @@ -72,15 +76,16 @@ public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = - var vomitAmount = solutionSize; - // Takes 10% of the chemicals removed from the chem stream - if (_solutionContainer.ResolveSolution(uid, bloodStream.ChemicalSolutionName, ref bloodStream.ChemicalSolution)) - { - var vomitChemstreamAmount = _solutionContainer.SplitSolution(bloodStream.ChemicalSolution.Value, vomitAmount); - vomitChemstreamAmount.ScaleSolution(chemMultiplier); - solution.AddSolution(vomitChemstreamAmount, _proto); - - vomitAmount -= (float) vomitChemstreamAmount.Volume; - } + //TODO: refactor vomit with digestion/better metabolism. Also why are we adding bloodstream contents to vomit? + // // Takes 10% of the chemicals removed from the chem stream + // if (_solutionContainer.ResolveSolution(uid, bloodStream.ChemicalSolutionName, ref bloodStream.ChemicalSolution)) + // { + // var vomitChemstreamAmount = _solutionContainer.SplitSolution(bloodStream.ChemicalSolution.Value, vomitAmount); + // vomitChemstreamAmount.ScaleSolution(chemMultiplier); + // solution.AddSolution(vomitChemstreamAmount, _proto); + // + // vomitAmount -= (float) vomitChemstreamAmount.Volume; + // } // Makes a vomit solution the size of 90% of the chemicals removed from the chemstream solution.AddReagent("Vomit", vomitAmount); // TODO: Dehardcode vomit prototype diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index aa2ed71d8f36ec..25935b647f1111 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -49,7 +49,8 @@ public sealed class DrinkSystem : SharedDrinkSystem [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; - [Dependency] private readonly StomachSystem _stomach = default!; + //TODO Digestion: Re-Implement this + //[Dependency] private readonly StomachSystem _stomach = default!; [Dependency] private readonly ForensicsSystem _forensics = default!; public override void Initialize() @@ -251,37 +252,38 @@ private void OnDoAfter(Entity entity, ref ConsumeDoAfterEvent ar if (transferAmount <= 0) return; - if (!_body.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) - { - _popup.PopupEntity(Loc.GetString(forceDrink ? "drink-component-try-use-drink-cannot-drink-other" : "drink-component-try-use-drink-had-enough"), args.Target.Value, args.User); - - if (HasComp(args.Target.Value)) - { - _puddle.TrySpillAt(args.User, drained, out _); - return; - } - - _solutionContainer.Refill(args.Target.Value, soln.Value, drained); - return; - } - - var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Comp.Owner, drained, stomach.Comp)); - - //All stomachs are full or can't handle whatever solution we have. - if (firstStomach == null) - { - _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"), args.Target.Value, args.Target.Value); - - if (forceDrink) - { - _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Target.Value, args.User); - _puddle.TrySpillAt(args.Target.Value, drained, out _); - } - else - _solutionContainer.TryAddSolution(soln.Value, drained); - - return; - } + //TODO Digestion: Re-Implement this + // if (!_body.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) + // { + // _popup.PopupEntity(Loc.GetString(forceDrink ? "drink-component-try-use-drink-cannot-drink-other" : "drink-component-try-use-drink-had-enough"), args.Target.Value, args.User); + // + // if (HasComp(args.Target.Value)) + // { + // _puddle.TrySpillAt(args.User, drained, out _); + // return; + // } + // + // _solutionContainer.Refill(args.Target.Value, soln.Value, drained); + // return; + // } + + // var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Comp.Owner, drained, stomach.Comp)); + // + // //All stomachs are full or can't handle whatever solution we have. + // if (firstStomach == null) + // { + // _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"), args.Target.Value, args.Target.Value); + // + // if (forceDrink) + // { + // _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Target.Value, args.User); + // _puddle.TrySpillAt(args.Target.Value, drained, out _); + // } + // else + // _solutionContainer.TryAddSolution(soln.Value, drained); + // + // return; + // } var flavors = args.FlavorMessage; @@ -315,7 +317,8 @@ private void OnDoAfter(Entity entity, ref ConsumeDoAfterEvent ar _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion); //TODO: Grab the stomach UIDs somehow without using Owner - _stomach.TryTransferSolution(firstStomach.Value.Comp.Owner, drained, firstStomach.Value.Comp); + //TODO Digestion: Re-Implement this + // _stomach.TryTransferSolution(firstStomach.Value.Comp.Owner, drained, firstStomach.Value.Comp); _forensics.TransferDna(entity, args.Target.Value); @@ -327,9 +330,9 @@ private void AddDrinkVerb(Entity entity, ref GetVerbsEvent(ev.User, out var body) || - !_body.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) + !ev.CanAccess || //TODO Digestion: Re-Implement this + !TryComp(ev.User, out var body) )//|| + //!_body.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) return; // Make sure the solution exists diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index 1862b4e19f1469..bbffab3eb7eccd 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Inventory; @@ -56,7 +55,8 @@ public sealed class FoodSystem : EntitySystem [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly StackSystem _stack = default!; - [Dependency] private readonly StomachSystem _stomach = default!; + //TODO Digestion: Re-Implement this + //[Dependency] private readonly StomachSystem _stomach = default!; [Dependency] private readonly UtensilSystem _utensil = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; @@ -118,12 +118,14 @@ private void OnFeedFood(Entity entity, ref AfterInteractEvent arg if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution)) return (false, false); - if (!_body.TryGetBodyOrganComponents(target, out var stomachs, body)) - return (false, false); + //TODO Digestion: Re-Implement this + // if (!_body.TryGetBodyOrganComponents(target, out var stomachs, body)) + // return (false, false); // Check for special digestibles - if (!IsDigestibleBy(food, foodComp, stomachs)) - return (false, false); + //TODO Digestion: Re-Implement this + // if (!IsDigestibleBy(food, foodComp, stomachs)) + // return (false, false); if (!TryGetRequiredUtensils(user, foodComp, out _)) return (false, false); @@ -206,8 +208,9 @@ private void OnDoAfter(Entity entity, ref ConsumeDoAfterEvent arg if (!TryComp(args.Target.Value, out var body)) return; - if (!_body.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) - return; + //TODO Digestion: Re-Implement this + // if (!_body.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) + // return; if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution)) return; @@ -233,33 +236,36 @@ private void OnDoAfter(Entity entity, ref ConsumeDoAfterEvent arg //TODO: Get the stomach UID somehow without nabbing owner // Get the stomach with the highest available solution volume var highestAvailable = FixedPoint2.Zero; - StomachComponent? stomachToUse = null; - foreach (var (stomach, _) in stomachs) - { - var owner = stomach.Owner; - if (!_stomach.CanTransferSolution(owner, split, stomach)) - continue; - - if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref stomach.Solution, out var stomachSol)) - continue; - - if (stomachSol.AvailableVolume <= highestAvailable) - continue; - - stomachToUse = stomach; - highestAvailable = stomachSol.AvailableVolume; - } + //TODO Digestion: Re-Implement this + // StomachComponent? stomachToUse = null; + // foreach (var (stomach, _) in stomachs) + // { + // var owner = stomach.Owner; + // if (!_stomach.CanTransferSolution(owner, split, stomach)) + // continue; + // + // if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref stomach.Solution, out var stomachSol)) + // continue; + // + // if (stomachSol.AvailableVolume <= highestAvailable) + // continue; + // + // stomachToUse = stomach; + // highestAvailable = stomachSol.AvailableVolume; + // } // No stomach so just popup a message that they can't eat. - if (stomachToUse == null) - { - _solutionContainer.TryAddSolution(soln.Value, split); - _popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other") : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User); - return; - } + //TODO Digestion: Re-Implement this + // if (stomachToUse == null) + // { + // _solutionContainer.TryAddSolution(soln.Value, split); + // _popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other") : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User); + // return; + // } _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion); - _stomach.TryTransferSolution(stomachToUse.Owner, split, stomachToUse); + //TODO Digestion: Re-Implement this + // _stomach.TryTransferSolution(stomachToUse.Owner, split, stomachToUse); var flavors = args.FlavorMessage; @@ -349,9 +355,9 @@ private void AddEatVerb(Entity entity, ref GetVerbsEvent(ev.User, out var body) || - !_body.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) + !ev.CanAccess || //TODO Digestion: Re-Implement this + !TryComp(ev.User, out var body) )//|| + // !_body.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) return; // have to kill mouse before eating it @@ -359,8 +365,9 @@ private void AddEatVerb(Entity entity, ref GetVerbsEvent(uid, out var stomachs)) - return false; + //TODO Digestion: Re-Implement this + // if (!_body.TryGetBodyOrganComponents(uid, out var stomachs)) + // return false; - return IsDigestibleBy(food, foodComp, stomachs); + //return IsDigestibleBy(food, foodComp, stomachs); + return false; } /// /// Returns true if has a that whitelists /// this (or if they even have enough stomachs in the first place). /// - private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(StomachComponent, OrganComponent)> stomachs) - { - var digestible = true; - - // Does the mob have enough stomachs? - if (stomachs.Count < component.RequiredStomachs) - return false; - - // Run through the mobs' stomachs - foreach (var (comp, _) in stomachs) - { - // Find a stomach with a SpecialDigestible - if (comp.SpecialDigestible == null) - continue; - // Check if the food is in the whitelist - if (_whitelistSystem.IsWhitelistPass(comp.SpecialDigestible, food)) - return true; - // They can only eat whitelist food and the food isn't in the whitelist. It's not edible. - return false; - } - - if (component.RequiresSpecialDigestion) - return false; - - return digestible; - } + //TODO Digestion: Re-Implement this + // private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(StomachComponent, OrganComponent)> stomachs) + // { + // var digestible = true; + // + // // Does the mob have enough stomachs? + // if (stomachs.Count < component.RequiredStomachs) + // return false; + // + // // Run through the mobs' stomachs + // foreach (var (comp, _) in stomachs) + // { + // // Find a stomach with a SpecialDigestible + // if (comp.SpecialDigestible == null) + // continue; + // // Check if the food is in the whitelist + // if (_whitelistSystem.IsWhitelistPass(comp.SpecialDigestible, food)) + // return true; + // // They can only eat whitelist food and the food isn't in the whitelist. It's not edible. + // return false; + // } + // + // if (component.RequiresSpecialDigestion) + // return false; + // + // return digestible; + // } private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component, out List utensils, HandsComponent? hands = null) diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs index f7650f599b4c61..77ae797395f57f 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs @@ -14,6 +14,7 @@ using Content.Shared.Nutrition; using System.Threading; using Content.Shared.Atmos; +using Content.Shared.Medical.Blood.Components; /// /// System for vapes diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs index a33c944c99482e..e37a8750084694 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs @@ -1,10 +1,7 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Body.Systems; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Forensics; using Content.Shared.Chemistry; -using Content.Shared.Chemistry.Reagent; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.FixedPoint; @@ -24,7 +21,6 @@ public sealed partial class SmokingSystem : EntitySystem { [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; @@ -140,17 +136,17 @@ public override void Update(float frameTime) if (inhaledSolution.Volume == FixedPoint2.Zero) continue; - // This is awful. I hate this so much. - // TODO: Please, someone refactor containers and free me from this bullshit. - if (!_container.TryGetContainingContainer(uid, out var containerManager) || - !(_inventorySystem.TryGetSlotEntity(containerManager.Owner, "mask", out var inMaskSlotUid) && inMaskSlotUid == uid) || - !TryComp(containerManager.Owner, out BloodstreamComponent? bloodstream)) - { - continue; - } - - _reactiveSystem.DoEntityReaction(containerManager.Owner, inhaledSolution, ReactionMethod.Ingestion); - _bloodstreamSystem.TryAddToChemicals(containerManager.Owner, inhaledSolution, bloodstream); + //TODO: convert this to use the new breathing system instead of directly injecting chems into bloodstream when smoking + // // This is awful. I hate this so much. + // if (!_container.TryGetContainingContainer(uid, out var containerManager) || + // !(_inventorySystem.TryGetSlotEntity(containerManager.Owner, "mask", out var inMaskSlotUid) && inMaskSlotUid == uid) || + // !TryComp(containerManager.Owner, out BloodstreamComponent? bloodstream)) + // { + // continue; + // } + // + // _reactiveSystem.DoEntityReaction(containerManager.Owner, inhaledSolution, ReactionMethod.Ingestion); + // _bloodstreamSystem.TryAddToChemicals(containerManager.Owner, inhaledSolution, bloodstream); } _timer -= UpdateTimer; diff --git a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs index 8b31f598d0de40..82c55d1320dcbc 100644 --- a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs @@ -51,8 +51,8 @@ public override void Initialize() SubscribeLocalEvent(OnWeldableAttempt); SubscribeLocalEvent(OnExploded); - SubscribeLocalEvent(OnInsideInhale); - SubscribeLocalEvent(OnInsideExhale); + // SubscribeLocalEvent(OnInsideInhale); + // SubscribeLocalEvent(OnInsideExhale); SubscribeLocalEvent(OnInsideExposed); SubscribeLocalEvent(OnRemoved); @@ -158,21 +158,22 @@ private void OnRemoved(EntityUid uid, InsideEntityStorageComponent component, En #region Gas mix event handlers - private void OnInsideInhale(EntityUid uid, InsideEntityStorageComponent component, InhaleLocationEvent args) - { - if (TryComp(component.Storage, out var storage) && storage.Airtight) - { - args.Gas = storage.Air; - } - } - - private void OnInsideExhale(EntityUid uid, InsideEntityStorageComponent component, ExhaleLocationEvent args) - { - if (TryComp(component.Storage, out var storage) && storage.Airtight) - { - args.Gas = storage.Air; - } - } + //TODO Metabolism: reimplement this + // private void OnInsideInhale(EntityUid uid, InsideEntityStorageComponent component, InhaleLocationEvent args) + // { + // if (TryComp(component.Storage, out var storage) && storage.Airtight) + // { + // args.Gas = storage.Air; + // } + // } + // + // private void OnInsideExhale(EntityUid uid, InsideEntityStorageComponent component, ExhaleLocationEvent args) + // { + // if (TryComp(component.Storage, out var storage) && storage.Airtight) + // { + // args.Gas = storage.Air; + // } + // } private void OnInsideExposed(EntityUid uid, InsideEntityStorageComponent component, ref AtmosExposedGetAirEvent args) { diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index a8952009e66eac..0f5c111fca834f 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -100,7 +100,8 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) //we need to basically remove all of these because zombies shouldn't //get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic. - RemComp(target); + //TODO Respiration: reimplement this + //RemComp(target); RemComp(target); RemComp(target); RemComp(target); @@ -151,8 +152,9 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor; zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor; zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers); - if (TryComp(target, out var stream)) - zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent; + //TODO: re-implement bloodType saving for zombies + //if (TryComp(target, out var stream)) + //zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent; _humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp); @@ -194,9 +196,11 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) //This makes it so the zombie doesn't take bloodloss damage. //NOTE: they are supposed to bleed, just not take damage - _bloodstream.SetBloodLossThreshold(target, 0f); + //_bloodstream.SetBloodLossThreshold(target, 0f); //Give them zombie blood - _bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent); + //_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent); + //TODO: re-implement zombie blood + //This is specifically here to combat insuls, because frying zombies on grilles is funny as shit. _inventory.TryUnequip(target, "gloves", true, true); diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 371c6f1222aeaa..1ccca7804e0bf5 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -29,7 +29,6 @@ public sealed partial class ZombieSystem : SharedZombieSystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly BloodstreamSystem _bloodstream = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly ActionsSystem _actions = default!; @@ -280,7 +279,8 @@ public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombi appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor; } _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false); - _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent); + //TODO: re-implement zombie bloodstream swapping + //_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent); _nameMod.RefreshNameModifiers(target); return true; diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 6e640b287b691a..9081e3a993e839 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -14,6 +14,16 @@ public static class Atmospherics /// public const float R = 8.314462618f; + public static float MolsToVolume(float mols, float pressure = OneAtmosphere, float temp = T20C) + { + return mols * temp * R / pressure; + } + + public static float VolumeToMols(float volume, float pressure = OneAtmosphere, float temp = T20C) + { + return pressure * volume / (R * temp); + } + /// /// 1 ATM in kPA. /// @@ -301,11 +311,6 @@ public static class Atmospherics /// public const float NormalBodyTemperature = 37f; - /// - /// I hereby decree. This is Arbitrary Suck my Dick - /// - public const float BreathMolesToReagentMultiplier = 1144; - #region Pipes /// diff --git a/Content.Shared/Atmos/GasMixture.cs b/Content.Shared/Atmos/GasMixture.cs index 0f1efba97666f6..2586ccf3f90008 100644 --- a/Content.Shared/Atmos/GasMixture.cs +++ b/Content.Shared/Atmos/GasMixture.cs @@ -232,7 +232,15 @@ void ISerializationHooks.AfterDeserialization() // TODO add fixed-length-array serializer // The arrays MUST have a specific length. - Array.Resize(ref Moles, Atmospherics.AdjustedNumberOfGases); + + //TODO: re-enable this when the sandbox actually behaves + //Array.Resize(ref Moles, Atmospherics.AdjustedNumberOfGases); + var newMols = new float[Atmospherics.AdjustedNumberOfGases]; + for (var i = 0; i < Moles.Length; i++) + { + newMols[i] = Moles[i]; + } + Moles = newMols; } public GasMixtureStringRepresentation ToPrettyString() diff --git a/Content.Shared/Body/Components/BodyComponent.cs b/Content.Shared/Body/Components/BodyComponent.cs index 481e22150b0d6f..c866d7db33533a 100644 --- a/Content.Shared/Body/Components/BodyComponent.cs +++ b/Content.Shared/Body/Components/BodyComponent.cs @@ -38,6 +38,9 @@ public sealed partial class BodyComponent : Component [DataField, AutoNetworkedField] public int RequiredLegs; + [AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public bool BodyInitialized = false; + [ViewVariables] [DataField, AutoNetworkedField] public HashSet LegEntities = new(); diff --git a/Content.Shared/Body/Events/BodyEvents.cs b/Content.Shared/Body/Events/BodyEvents.cs new file mode 100644 index 00000000000000..c887d6eafa5f9b --- /dev/null +++ b/Content.Shared/Body/Events/BodyEvents.cs @@ -0,0 +1,7 @@ +using Content.Shared.Body.Components; + +namespace Content.Shared.Body.Events; + + +[ByRefEvent] +public readonly record struct BodyInitializedEvent(Entity Body); diff --git a/Content.Shared/Body/Events/MechanismBodyEvents.cs b/Content.Shared/Body/Events/MechanismBodyEvents.cs index 968b172aef5e99..b5fc90cd2630ae 100644 --- a/Content.Shared/Body/Events/MechanismBodyEvents.cs +++ b/Content.Shared/Body/Events/MechanismBodyEvents.cs @@ -1,28 +1,32 @@ -namespace Content.Shared.Body.Events; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; -// All of these events are raised on a mechanism entity when added/removed to a body in different -// ways. +namespace Content.Shared.Body.Events +{ + // All of these events are raised on a mechanism entity when added/removed to a body in different + // ways. -/// -/// Raised on a mechanism when it is added to a body part. -/// -[ByRefEvent] -public readonly record struct OrganAddedEvent(EntityUid Part); + /// + /// Raised on a mechanism when it is added to a body part. + /// + [ByRefEvent] + public readonly record struct OrganAddedEvent(Entity Part); -/// -/// Raised on a mechanism when it is added to a body part within a body. -/// -[ByRefEvent] -public readonly record struct OrganAddedToBodyEvent(EntityUid Body, EntityUid Part); + /// + /// Raised on a mechanism when it is added to a body part within a body. + /// + [ByRefEvent] + public readonly record struct OrganAddedToBodyEvent(Entity Body, Entity Part); -/// -/// Raised on a mechanism when it is removed from a body part. -/// -[ByRefEvent] -public readonly record struct OrganRemovedEvent(EntityUid OldPart); + /// + /// Raised on a mechanism when it is removed from a body part. + /// + [ByRefEvent] + public readonly record struct OrganRemovedEvent(Entity OldPart); -/// -/// Raised on a mechanism when it is removed from a body part within a body. -/// -[ByRefEvent] -public readonly record struct OrganRemovedFromBodyEvent(EntityUid OldBody, EntityUid OldPart); + /// + /// Raised on a mechanism when it is removed from a body part within a body. + /// + [ByRefEvent] + public readonly record struct OrganRemovedFromBodyEvent(Entity OldBody, Entity OldPart); +} diff --git a/Content.Shared/Body/Organ/OrganComponent.cs b/Content.Shared/Body/Organ/OrganComponent.cs index 3048927b5fb719..949d9ebc6b260a 100644 --- a/Content.Shared/Body/Organ/OrganComponent.cs +++ b/Content.Shared/Body/Organ/OrganComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Body.Systems; +using Content.Shared.FixedPoint; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -13,4 +14,9 @@ public sealed partial class OrganComponent : Component /// [DataField, AutoNetworkedField] public EntityUid? Body; + + /// + /// This number reflects the capability of this bodypart as a number between 0 and 1. + /// + public FixedPoint2 Efficiency = 1.0f; } diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs index c4e65c06a3f842..9195657542d01d 100644 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ b/Content.Shared/Body/Part/BodyPartComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Body.Components; using Content.Shared.Body.Systems; +using Content.Shared.FixedPoint; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -28,6 +29,11 @@ public sealed partial class BodyPartComponent : Component [DataField("vital"), AutoNetworkedField] public bool IsVital; + /// + /// This number reflects the capability of this part as a number between 0 and 1. + /// + public FixedPoint2 Efficiency = 1.0f; + [DataField, AutoNetworkedField] public BodyPartSymmetry Symmetry = BodyPartSymmetry.None; diff --git a/Content.Shared/Body/Part/BodyPartEvents.cs b/Content.Shared/Body/Part/BodyPartEvents.cs index 0d8d2c8a268139..dbbda10e89b2ce 100644 --- a/Content.Shared/Body/Part/BodyPartEvents.cs +++ b/Content.Shared/Body/Part/BodyPartEvents.cs @@ -1,3 +1,5 @@ +using Content.Shared.Body.Components; + namespace Content.Shared.Body.Part; [ByRefEvent] @@ -5,3 +7,10 @@ namespace Content.Shared.Body.Part; [ByRefEvent] public readonly record struct BodyPartRemovedEvent(string Slot, Entity Part); + + +[ByRefEvent] +public readonly record struct BodyPartAddedToBodyEvent(string Slot, Entity Body, Entity Part); + +[ByRefEvent] +public readonly record struct BodyPartRemovedFromBodyEvent(string Slot, Entity OldBody, Entity Part); diff --git a/Content.Shared/Body/Prototypes/BodyPartTypePrototype.cs b/Content.Shared/Body/Prototypes/BodyPartTypePrototype.cs new file mode 100644 index 00000000000000..6eea3a228a1a25 --- /dev/null +++ b/Content.Shared/Body/Prototypes/BodyPartTypePrototype.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body.Prototypes; + +/// +/// This is a prototype for... +/// +[Prototype()] +public sealed partial class BodyPartTypePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; +} diff --git a/Content.Shared/Body/Prototypes/OrganTypePrototype.cs b/Content.Shared/Body/Prototypes/OrganTypePrototype.cs new file mode 100644 index 00000000000000..c344b7e8af101b --- /dev/null +++ b/Content.Shared/Body/Prototypes/OrganTypePrototype.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body.Prototypes; + +[Prototype] +public sealed partial class OrganTypePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; +} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 250f90db8f37e8..a48eed743d009f 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -1,6 +1,8 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Content.Shared.Body.Components; +using Content.Shared.Body.Events; using Content.Shared.Body.Organ; using Content.Shared.Body.Part; using Content.Shared.Body.Prototypes; @@ -9,6 +11,7 @@ using Content.Shared.Gibbing.Events; using Content.Shared.Gibbing.Systems; using Content.Shared.Inventory; +using Content.Shared.Medical.Blood.Systems; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; @@ -94,32 +97,47 @@ private void OnBodyInit(Entity ent, ref ComponentInit args) ent.Comp.RootContainer = Containers.EnsureContainer(ent, BodyRootContainerId); } - private void OnBodyMapInit(Entity ent, ref MapInitEvent args) + private void OnBodyMapInit(Entity body, ref MapInitEvent args) { - if (ent.Comp.Prototype is null) + if (body.Comp.Prototype is null) return; // One-time setup // Obviously can't run in Init to avoid double-spawns on save / load. - var prototype = Prototypes.Index(ent.Comp.Prototype.Value); - MapInitBody(ent, prototype); + var prototype = Prototypes.Index(body.Comp.Prototype.Value); + MapInitBody(body, prototype); } - private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype) + private void MapInitBody(Entity body, BodyPrototype prototype) { var protoRoot = prototype.Slots[prototype.Root]; if (protoRoot.Part is null) return; // This should already handle adding the entity to the root. - var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId); + var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, body, BodyRootContainerId); var rootPart = Comp(rootPartUid); - rootPart.Body = bodyEntity; + rootPart.Body = body; Dirty(rootPartUid, rootPart); // Setup the rest of the body entities. - SetupOrgans((rootPartUid, rootPart), protoRoot.Organs); MapInitParts(rootPartUid, prototype); + SetupOrgans((rootPartUid, rootPart), protoRoot.Organs); + + body.Comp.BodyInitialized = true; + + //Raise body initialized events on all bodyParts that are initialized + var ev = new BodyInitializedEvent(body); + RaiseLocalEvent(body, ref ev); + foreach (var (partId, part) in GetBodyPartChildren(rootPartUid, rootPart)) + { + RaiseLocalEvent(partId, ref ev); + foreach (var (organId, organ) in GetPartOrgans(partId, part)) + { + RaiseLocalEvent(organId, ref ev); + } + } + Dirty(body); } private void OnBodyCanDrag(Entity ent, ref CanDragEvent args) @@ -223,6 +241,20 @@ public IEnumerable GetBodyContainers( } } + + public bool TryGetRootBodyPart(EntityUid target, [NotNullWhen(true)] out Entity? rootPart, + BodyComponent? bodyComp = null, bool logMissingBody = false) + { + rootPart = null; + if (!Resolve(target, ref bodyComp, logMissingBody)) + return false; + var foundEnt = ((ContainerSlot)Containers.GetContainer(target, bodyComp.RootPartSlot)).ContainedEntity; + if (foundEnt == null || !TryComp(foundEnt, out var rootPartComp)) + return false; + rootPart = new Entity(foundEnt.Value, rootPartComp); + return true; + } + /// /// Gets all child body parts of this entity, including the root entity. /// diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs index efabebfc858bfe..af07f79f9d996b 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs @@ -10,36 +10,39 @@ namespace Content.Shared.Body.Systems; public partial class SharedBodySystem { private void AddOrgan( - Entity organEnt, + Entity organ, EntityUid bodyUid, EntityUid parentPartUid) { - organEnt.Comp.Body = bodyUid; - var addedEv = new OrganAddedEvent(parentPartUid); - RaiseLocalEvent(organEnt, ref addedEv); + organ.Comp.Body = bodyUid; + var ev = new OrganAddedEvent(new Entity(parentPartUid, Comp(parentPartUid))); + RaiseLocalEvent(organ,ref ev); - if (organEnt.Comp.Body is not null) + if (organ.Comp.Body != null) { - var addedInBodyEv = new OrganAddedToBodyEvent(bodyUid, parentPartUid); - RaiseLocalEvent(organEnt, ref addedInBodyEv); + var ev2 = new OrganAddedToBodyEvent( + new Entity(organ.Comp.Body.Value, Comp(organ.Comp.Body.Value)), + new Entity(parentPartUid, Comp(parentPartUid))); + RaiseLocalEvent(organ.Comp.Body.Value, ref ev2); } - - Dirty(organEnt, organEnt.Comp); + Dirty(organ); } - private void RemoveOrgan(Entity organEnt, EntityUid parentPartUid) + private void RemoveOrgan(Entity organ, EntityUid parentPartUid) { - var removedEv = new OrganRemovedEvent(parentPartUid); - RaiseLocalEvent(organEnt, ref removedEv); + var ev = new OrganRemovedEvent(new Entity(parentPartUid, Comp(parentPartUid))); + RaiseLocalEvent(organ, ref ev); - if (organEnt.Comp.Body is { Valid: true } bodyUid) + if (organ.Comp.Body is { Valid: true } bodyUid) { - var removedInBodyEv = new OrganRemovedFromBodyEvent(bodyUid, parentPartUid); - RaiseLocalEvent(organEnt, ref removedInBodyEv); + var ev2 = new OrganRemovedFromBodyEvent( + new Entity(organ.Comp.Body.Value, Comp(organ.Comp.Body.Value)), + new Entity(parentPartUid, Comp(parentPartUid))); + RaiseLocalEvent(organ.Comp.Body.Value, ref ev2); } - organEnt.Comp.Body = null; - Dirty(organEnt, organEnt.Comp); + organ.Comp.Body = null; + Dirty(organ); } /// diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index ee79faa0b8e7ab..6dd0c018fd2e15 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -61,14 +61,14 @@ private void OnBodyPartRemoved(Entity ent, ref EntRemovedFrom RemoveOrgan((removedUid, organ), ent); } - private void RecursiveBodyUpdate(Entity ent, EntityUid? bodyUid) + private void RecursiveBodyUpdate(Entity body, EntityUid? bodyUid) { - ent.Comp.Body = bodyUid; - Dirty(ent, ent.Comp); + body.Comp.Body = bodyUid; + Dirty(body); - foreach (var slotId in ent.Comp.Organs.Keys) + foreach (var slotId in body.Comp.Organs.Keys) { - if (!Containers.TryGetContainer(ent, GetOrganContainerId(slotId), out var container)) + if (!Containers.TryGetContainer(body, GetOrganContainerId(slotId), out var container)) continue; foreach (var organ in container.ContainedEntities) @@ -78,24 +78,30 @@ private void RecursiveBodyUpdate(Entity ent, EntityUid? bodyU Dirty(organ, organComp); - if (organComp.Body is { Valid: true } oldBodyUid) + if (organComp.Body != null) { - var removedEv = new OrganRemovedFromBodyEvent(oldBodyUid, ent); - RaiseLocalEvent(organ, ref removedEv); + var ev = new OrganRemovedFromBodyEvent( + (organComp.Body.Value, Comp(organComp.Body.Value)), + body); + RaiseLocalEvent(organ, ref ev); } + organComp.Body = bodyUid; - if (bodyUid is not null) + if (bodyUid != null) { - var addedEv = new OrganAddedToBodyEvent(bodyUid.Value, ent); - RaiseLocalEvent(organ, ref addedEv); + var ev = new OrganAddedToBodyEvent( + (bodyUid.Value, Comp(bodyUid.Value)), + body); + RaiseLocalEvent(organ, ref ev); } + } } - foreach (var slotId in ent.Comp.Children.Keys) + foreach (var slotId in body.Comp.Children.Keys) { - if (!Containers.TryGetContainer(ent, GetPartSlotContainerId(slotId), out var container)) + if (!Containers.TryGetContainer(body, GetPartSlotContainerId(slotId), out var container)) continue; foreach (var containedUid in container.ContainedEntities) @@ -107,33 +113,41 @@ private void RecursiveBodyUpdate(Entity ent, EntityUid? bodyU } protected virtual void AddPart( - Entity bodyEnt, - Entity partEnt, + Entity body, + Entity bodyPart, string slotId) { - Dirty(partEnt, partEnt.Comp); - partEnt.Comp.Body = bodyEnt; + Dirty(bodyPart); + bodyPart.Comp.Body = body; + + var ev = new BodyPartAddedEvent(slotId, bodyPart); + RaiseLocalEvent(body, ref ev); - var ev = new BodyPartAddedEvent(slotId, partEnt); - RaiseLocalEvent(bodyEnt, ref ev); + var bodyComp = body.Comp ?? Comp(body); - AddLeg(partEnt, bodyEnt); + var ev2 = new BodyPartAddedToBodyEvent(slotId, (body, bodyComp), bodyPart); + RaiseLocalEvent(bodyPart, ref ev2); + + AddLeg(bodyPart, body); } protected virtual void RemovePart( - Entity bodyEnt, - Entity partEnt, + Entity body, + Entity bodyPart, string slotId) { - Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false); - Dirty(partEnt, partEnt.Comp); - partEnt.Comp.Body = null; + Resolve(body, ref body.Comp, logMissing: false); + Dirty(bodyPart, bodyPart.Comp); + bodyPart.Comp.Body = null; + + var ev = new BodyPartRemovedEvent(slotId, bodyPart); + RaiseLocalEvent(body, ref ev); - var ev = new BodyPartRemovedEvent(slotId, partEnt); - RaiseLocalEvent(bodyEnt, ref ev); + var bodyComp = body.Comp ?? Comp(body); + var ev2 = new BodyPartAddedToBodyEvent(slotId, (body, bodyComp), bodyPart); + RaiseLocalEvent(bodyPart, ref ev2); - RemoveLeg(partEnt, bodyEnt); - PartRemoveDamage(bodyEnt, partEnt); + RemoveLeg(bodyPart, body); } private void AddLeg(Entity legEnt, Entity bodyEnt) @@ -167,22 +181,6 @@ private void RemoveLeg(Entity legEnt, Entity } } - private void PartRemoveDamage(Entity bodyEnt, Entity partEnt) - { - if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) - return; - - if (!_timing.ApplyingState - && partEnt.Comp.IsVital - && !GetBodyChildrenOfType(bodyEnt, partEnt.Comp.PartType, bodyEnt.Comp).Any() - ) - { - // TODO BODY SYSTEM KILL : remove this when wounding and required parts are implemented properly - var damage = new DamageSpecifier(Prototypes.Index("Bloodloss"), 300); - Damageable.TryChangeDamage(bodyEnt, damage); - } - } - /// /// Tries to get the parent body part to this if applicable. /// Doesn't validate if it's a part of body system. @@ -542,14 +540,14 @@ public IEnumerable GetPartContainers(EntityUid id, BodyPartCompon /// /// Returns all body part components for this entity including itself. /// - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyPartChildren( - EntityUid partId, - BodyPartComponent? part = null) + public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyPartChildren(EntityUid partId, BodyPartComponent? part = null, + bool inclusive = true) { if (!Resolve(partId, ref part, logMissing: false)) yield break; - yield return (partId, part); + if (inclusive) + yield return (partId, part); foreach (var slotId in part.Children.Keys) { @@ -571,6 +569,30 @@ public IEnumerable GetPartContainers(EntityUid id, BodyPartCompon } } + /// + /// Returns all body part components that are direct children of this entity. + /// + public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyPartDirectChildren(EntityUid partId, BodyPartComponent? part = null) + { + if (!Resolve(partId, ref part, false)) + yield break; + + foreach (var slotId in part.Children.Keys) + { + var containerSlotId = GetPartSlotContainerId(slotId); + + if (Containers.TryGetContainer(partId, containerSlotId, out var container)) + { + foreach (var containedEnt in container.ContainedEntities) + { + if (!TryComp(containedEnt, out BodyPartComponent? childPart)) + continue; + yield return (containedEnt, childPart); + } + } + } + } + /// /// Returns all body part slots for this entity. /// diff --git a/Content.Shared/Chemistry/Chemistry.cs b/Content.Shared/Chemistry/Chemistry.cs new file mode 100644 index 00000000000000..9d67c5dd0f99f4 --- /dev/null +++ b/Content.Shared/Chemistry/Chemistry.cs @@ -0,0 +1,29 @@ +using Content.Shared.FixedPoint; + +namespace Content.Shared.Chemistry; + +//Chemistry constants +public static class Constants +{ + //Reference value for converting Units of an unknown *liquid* reagent into Moles of *liquid* + //(This is how many moles of H2O are present in 10ml of water) + //eventually each reagent should have a molar mass and use that to calculate this value + public static float LiquidReferenceMolPerReagentUnit = 0.55508435061f; + public static FixedPoint2 ReagentUnitsToMilliLiters = 0.1f; + + public static FixedPoint2 ToMilliLitres(FixedPoint2 reagentUnits) + { + return ReagentUnitsToMilliLiters / reagentUnits; + } + + public static float LiquidMolesFromRU(FixedPoint2 reagentUnits) + { + return reagentUnits.Float()* LiquidReferenceMolPerReagentUnit; + } + + public static float LiquidRUFromMoles(float moles) + { + return moles * LiquidReferenceMolPerReagentUnit; + } + +} diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs index 4de3c369f7cce5..a62f179459214a 100644 --- a/Content.Shared/Chemistry/Components/Solution.cs +++ b/Content.Shared/Chemistry/Components/Solution.cs @@ -48,6 +48,13 @@ public sealed partial class Solution : IEnumerable, ISerializat [DataField("canReact")] public bool CanReact { get; set; } = true; + /// + /// If absorptions will be checked for when adding reagents to the container. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public bool CanBeAbsorbed = true; + /// /// Volume needed to fill this container. /// @@ -147,7 +154,7 @@ public Solution(int capacity) /// /// The prototype ID of the reagent to add. /// The quantity in milli-units. - public Solution(string prototype, FixedPoint2 quantity, ReagentData? data = null) : this() + public Solution(string prototype, FixedPoint2 quantity, ReagentDiscriminator? data = null) : this() { AddReagent(new ReagentId(prototype, data), quantity); } @@ -243,7 +250,7 @@ public bool ContainsReagent(ReagentId id) return false; } - public bool ContainsReagent(string reagentId, ReagentData? data) + public bool ContainsReagent(string reagentId, ReagentDiscriminator? data) => ContainsReagent(new(reagentId, data)); public bool TryGetReagent(ReagentId id, out ReagentQuantity quantity) @@ -404,7 +411,7 @@ public void AddReagent(ReagentQuantity reagentQuantity) /// /// The prototype of the reagent to add. /// The quantity in milli-units. - public void AddReagent(ReagentPrototype proto, FixedPoint2 quantity, float temperature, IPrototypeManager? protoMan, ReagentData? data = null) + public void AddReagent(ReagentPrototype proto, FixedPoint2 quantity, float temperature, IPrototypeManager? protoMan, ReagentDiscriminator? data = null) { if (_heatCapacityDirty) UpdateHeatCapacity(protoMan); @@ -523,7 +530,7 @@ public FixedPoint2 RemoveReagent(ReagentQuantity toRemove, bool preserveOrder = /// The prototype of the reagent to be removed. /// The amount of reagent to remove. /// How much reagent was actually removed. Zero if the reagent is not present on the solution. - public FixedPoint2 RemoveReagent(string prototype, FixedPoint2 quantity, ReagentData? data = null) + public FixedPoint2 RemoveReagent(string prototype, FixedPoint2 quantity, ReagentDiscriminator? data = null) { return RemoveReagent(new ReagentQuantity(prototype, quantity, data)); } diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs index c8e8e89ce53bbb..22a9c82167ed08 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs @@ -105,7 +105,7 @@ public void FinishMix(Entity entity) if (!_solution.TryGetFitsInDispenser(ent, out var soln, out _)) continue; - _solution.UpdateChemicals(soln.Value, true, reactionMixer); + _solution.UpdateChemicals(soln.Value, true, true, reactionMixer); } } diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index 7e00157b6ee3b9..8827abfdc46888 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -14,6 +14,8 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Text; +using Content.Shared.Chemistry.Reaction.Components; +using Content.Shared.Chemistry.Reaction.Systems; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Robust.Shared.Map; @@ -68,6 +70,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!; [Dependency] protected readonly MetaDataSystem MetaDataSys = default!; [Dependency] protected readonly INetManager NetManager = default!; + [Dependency] protected readonly ChemicalAbsorptionSystem AbsorptionSystem = default!; public override void Initialize() { @@ -299,7 +302,12 @@ public FixedPoint2 GetTotalPrototypeQuantity(EntityUid owner, string reagentId) /// /// /// - public void UpdateChemicals(Entity soln, bool needsReactionsProcessing = true, ReactionMixerComponent? mixerComponent = null) + public void UpdateChemicals( + Entity soln, + bool needsReactionsProcessing = true, + bool needsAbsorptionProcessing = false, + ReactionMixerComponent? mixerComponent = null, + Entity? absorber = null) { Dirty(soln); @@ -310,6 +318,11 @@ public void UpdateChemicals(Entity soln, bool needsReactionsP if (needsReactionsProcessing && solution.CanReact) ChemicalReactionSystem.FullyReactSolution(soln, mixerComponent); + // Process absorptions, only processed if there is an absorber + if (needsAbsorptionProcessing && solution.CanBeAbsorbed) + AbsorptionSystem.AbsorbChemicals(soln, absorber); + + var overflow = solution.Volume - solution.MaxVolume; if (overflow > FixedPoint2.Zero) { @@ -452,8 +465,8 @@ public bool TryAddReagent(Entity soln, ReagentQuantity reagen /// The amount of reagent to add. /// If all the reagent could be added. [PublicAPI] - public bool TryAddReagent(Entity soln, string prototype, FixedPoint2 quantity, float? temperature = null, ReagentData? data = null) - => TryAddReagent(soln, new ReagentQuantity(prototype, quantity, data), out _, temperature); + public bool TryAddReagent(Entity soln, string prototype, FixedPoint2 quantity, float? temperature = null, + ReagentDiscriminator? data = null) => TryAddReagent(soln, new ReagentQuantity(prototype, quantity, data), out _, temperature); /// /// Adds reagent of an Id to the container. @@ -464,7 +477,8 @@ public bool TryAddReagent(Entity soln, string prototype, Fixe /// The amount of reagent to add. /// The amount of reagent successfully added. /// If all the reagent could be added. - public bool TryAddReagent(Entity soln, string prototype, FixedPoint2 quantity, out FixedPoint2 acceptedQuantity, float? temperature = null, ReagentData? data = null) + public bool TryAddReagent(Entity soln, string prototype, FixedPoint2 quantity, + out FixedPoint2 acceptedQuantity, float? temperature = null, ReagentDiscriminator? data = null) { var reagent = new ReagentQuantity(prototype, quantity, data); return TryAddReagent(soln, reagent, out acceptedQuantity, temperature); @@ -513,7 +527,7 @@ public bool RemoveReagent(Entity soln, ReagentQuantity reagen /// The Id of the reagent to remove. /// The amount of reagent to remove. /// If the reagent to remove was found in the container. - public bool RemoveReagent(Entity soln, string prototype, FixedPoint2 quantity, ReagentData? data = null) + public bool RemoveReagent(Entity soln, string prototype, FixedPoint2 quantity, ReagentDiscriminator? data = null) { return RemoveReagent(soln, new ReagentQuantity(prototype, quantity, data)); } diff --git a/Content.Shared/Chemistry/Reaction/Components/ChemicalAbsorberComponent.cs b/Content.Shared/Chemistry/Reaction/Components/ChemicalAbsorberComponent.cs new file mode 100644 index 00000000000000..9f9b720c844d06 --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Components/ChemicalAbsorberComponent.cs @@ -0,0 +1,64 @@ +using Content.Shared.Chemistry.Reaction.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Chemistry.Reaction.Components; + +/// +/// This is used for... +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ChemicalAbsorberComponent : Component +{ + + [DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField] + public TimeSpan UpdateRate = new(0,0,0,1); + + [DataField] + public TimeSpan LastUpdate; + + [DataField(required: true), AutoNetworkedField] + public List LinkedSolutions = new(); + + /// + /// The entity that contains the solution we want to transfer our absorbed reagents to. + /// If this is null then the reagents are simply deleted. + /// + [DataField, AutoNetworkedField] + public EntityUid? TransferTargetEntity = null; + + /// + /// SolutionId for our target solution + /// + [DataField, AutoNetworkedField] + public string TransferTargetSolutionId = "bloodReagents"; + + + /// + /// List of absorption groups, these will be split into single/multi-reagent absorptions and then sorted by priortiy + /// with multi-reagent absorptions being checked FIRST. + /// And their reaction rate multipliers + /// + [DataField, AutoNetworkedField] + public Dictionary, float> AbsorptionGroups = new(); + + /// + /// A list of individual absorptions to add in addition to the ones contained in the absorption groups + /// And their reaction rate multipliers + /// + [DataField, AutoNetworkedField] + public Dictionary, float>? AdditionalAbsorptions = null; + + /// + /// Multiplier for the absorption rate of the chosen absorption (if any) + /// + [DataField, AutoNetworkedField] + public float GlobalRateMultiplier = 1.0f; + + /// + /// List of all the reagent absorption reactions in the order they should be checked + /// + [DataField] + public List CachedAbsorptionOrder = new(); +} diff --git a/Content.Shared/Chemistry/Reaction/Events/ChemicalEvents.cs b/Content.Shared/Chemistry/Reaction/Events/ChemicalEvents.cs new file mode 100644 index 00000000000000..69fb096f42daba --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Events/ChemicalEvents.cs @@ -0,0 +1,80 @@ +using Content.Shared.Chemistry.Components; +using JetBrains.Annotations; + +namespace Content.Shared.Chemistry.Reaction.Events; + +/// FOR THE LOVE OF GOD AND ALL THAT IS HOLY DO NOT ADD CHEMICALS INTO THE TRIGGERING SOLUTION OR CALL UPDATECHEMICALS +/// FROM INSIDE ANY CHEMICAL EFFECT THIS WILL CAUSE AN INFINITE LOOP AND SET FIRE TO A SMOL-PUPPY ORPHANAGE. SERIOUSLY DON'T DO IT! +/// If you need to convert chemicals, just use a reaction! + +/// +/// Base class for effects that are triggered by solutions. This should not generally be used outside of the chemistry-related +/// systems. +/// +[MeansImplicitUse] +[ByRefEvent] +[ImplicitDataDefinitionForInheritors] +public abstract partial class ChemicalEffect : HandledEntityEventArgs +{ + public Entity SolutionEntity; + + [DataField] + public List? Conditions = null; + + public void RaiseEvent(EntityManager entityMan, + EntityUid targetEntity, + Entity solutionEntity, + bool broadcast = false) + { + if (Conditions != null) + { + foreach (var condition in Conditions) + { + var check = condition.RaiseEvent(entityMan, GetTargetEntity(targetEntity), solutionEntity, broadcast); + if (!check.Valid) + return; + } + } + + var ev = CreateInstance(); + ev.SolutionEntity = solutionEntity; + entityMan.EventBus.RaiseLocalEvent(GetTargetEntity(targetEntity), ref ev, broadcast); + entityMan.EventBus.RaiseLocalEvent(solutionEntity, ref ev, broadcast); + } + + protected virtual EntityUid GetTargetEntity(EntityUid oldTarget) + { + return oldTarget; + } + + protected abstract ChemicalEffect CreateInstance(); +} + +[MeansImplicitUse] +[ByRefEvent] +[ImplicitDataDefinitionForInheritors] +public abstract partial class ChemicalCondition : HandledEntityEventArgs +{ + public Entity SolutionEntity; + + public bool Valid = false; + + public ChemicalCondition RaiseEvent(EntityManager entityMan, + EntityUid targetEntity, + Entity solutionEntity, + bool broadcast = false) + { + var ev = CreateInstance(); + ev.SolutionEntity = solutionEntity; + entityMan.EventBus.RaiseLocalEvent(GetTargetEntity(targetEntity), ref ev, broadcast); + entityMan.EventBus.RaiseLocalEvent(solutionEntity, ref ev, broadcast); + return ev; + } + + protected virtual EntityUid GetTargetEntity(EntityUid oldTarget) + { + return oldTarget; + } + + protected abstract ChemicalCondition CreateInstance(); +} diff --git a/Content.Shared/Chemistry/Reaction/Prototypes/AbsorptionGroupPrototype.cs b/Content.Shared/Chemistry/Reaction/Prototypes/AbsorptionGroupPrototype.cs new file mode 100644 index 00000000000000..6fa79ffa372219 --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Prototypes/AbsorptionGroupPrototype.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Chemistry.Reaction.Prototypes; + +/// +/// Acts as a group for absorptions, this is mainly for ease of use so that you can just add a group instead of having +/// to add all of it's reagents individual to the ChemicalAbsorberComponent +/// +[Prototype] +public sealed partial class AbsorptionGroupPrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + /// + /// List of absorptions that are part of this absorption group + /// Absorptions can be part of multiple absorption groups + /// + [DataField(required:true)] + public HashSet> Absorptions = new(); +} diff --git a/Content.Shared/Chemistry/Reaction/Prototypes/AbsorptionPrototype.cs b/Content.Shared/Chemistry/Reaction/Prototypes/AbsorptionPrototype.cs new file mode 100644 index 00000000000000..e95e3a09c7992a --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Prototypes/AbsorptionPrototype.cs @@ -0,0 +1,86 @@ +using Content.Shared.Chemistry.Reaction.Events; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry.Reaction.Prototypes; + +[Prototype] +public sealed partial class AbsorptionPrototype : BaseReactionPrototype, IPrototype +{ + + /// + /// Will this reagent be transferred to a target solution if possible, or will it always be deleted on absorption? + /// + [DataField] public bool CanTransfer = false; + + public bool IsComboAbsorption => Catalysts is {Count: > 0} || Reactants.Count > 1; + + public AbsorptionReaction GetData(float multiplier = 0f) + { + var reagentsList = new List<(ProtoId, FixedPoint2)>(); + foreach (var (key, value) in Reactants) + { + reagentsList.Add((key, value * multiplier)); + } + List<(ProtoId, FixedPoint2)>? catalystList = null; + if (Catalysts != null) + { + catalystList = new List<(ProtoId, FixedPoint2)>(); + foreach (var (key, value) in Catalysts) + { + catalystList.Add((key, value * multiplier)); + } + } + return new( + Rate, + Priority, + reagentsList, + catalystList, + Quantized, + MinTemp, + MaxTemp, + TransferHeat, + ID, + CanTransfer, + Impact, + Conditions, + Effects, + ReagentEffects, + Sound); + } +} + +[DataRecord, NetSerializable, Serializable] +public record struct AbsorptionReaction( + float Rate, + int Priority, + List<(ProtoId, FixedPoint2)> Reactants, + List<(ProtoId, FixedPoint2)>? Catalysts, + bool Quantized, + float MinTemp, + float MaxTemp, + bool TransferHeat, + string ProtoId, + bool CanTransfer, + LogImpact? Impact, + List? Conditions, + List? Effects, + List? ReagentEffects, + SoundSpecifier? Sound) : IReactionData +{ + public int CompareTo(IReactionData? other) + { + if (other == null) + return -1; + + if (Priority != other.Priority) + return other.Priority - Priority; + + return string.Compare(ProtoId, other.ProtoId, StringComparison.Ordinal); + } +} + diff --git a/Content.Shared/Chemistry/Reaction/Prototypes/BaseReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/Prototypes/BaseReactionPrototype.cs new file mode 100644 index 00000000000000..a2c18101506f01 --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Prototypes/BaseReactionPrototype.cs @@ -0,0 +1,132 @@ +using Content.Shared.Chemistry.Reaction.Events; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Chemistry.Reaction.Prototypes; + +public abstract class BaseReactionPrototype : IComparable +{ + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public string Name { get; private set; } = string.Empty; + + [DataField(required: true)] + public float Rate = 1; + + /// + /// Reactants that must be present for the reaction to take place + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> Reactants = new(); + + /// + /// Any Catalysts that must be present for the reaction to take place + /// + [DataField] + public Dictionary, FixedPoint2>? Catalysts = null; + + /// + /// If true, this reaction will only consume only integer multiples of the reactant amounts. If there are not + /// enough reactants, the reaction does not occur. Useful for spawn-entity reactions (e.g. creating cheese). + /// + [DataField] + public bool Quantized = false; + + /// + /// Determines the order in which reactions occur. This should used to ensure that (in general) descriptive / + /// pop-up generating and explosive reactions occur before things like foam/area effects. + /// + [DataField] + public int Priority = 0; + + /// + /// Audio to play when this absorption occurs. If audio is enabled + /// + [DataField("sound")] + public SoundSpecifier? Sound = null; + + + /// + /// How dangerous are the effects of absorbing these reagents? + /// If this is null, do not log anything + /// + [DataField( serverOnly: true)] + public LogImpact? Impact = null; + + /// + /// Minimum temperature the reaction will take place at + /// + [DataField] + public float MinTemp = 0; + + /// + /// Maximum temperature the reaction will take place at + /// + [DataField] + public float MaxTemp = float.PositiveInfinity; + + /// + /// Should absorbing these reagents transfer their heat to the absorbing entity. + /// In most cases this should be false because entities that care about heat tend to handle a solution's heat themselves. + /// If you enable this in that case, it will result in heat being transferred twice. Once by the system the entity's heat + /// and solutions, and again when the solution is absorbed. + /// + [DataField] + public bool TransferHeat = false; + + /// + /// What conditions are required to allow this reaction + /// + [DataField] + public List? Conditions = null; + + /// + /// What effects does absorbing this reagent have + /// + [DataField] + public List? Effects = null; + + /// + /// Effects to be triggered when the reagents are absorbed + /// + [DataField] + public List? ReagentEffects = null; + + /// + /// Comparison for creating a sorted set of reactions. Determines the order in which reactions occur. + /// + public int CompareTo(ReactionPrototype? other) + { + if (other == null) + return -1; + + if (Priority != other.Priority) + return other.Priority - Priority; + + return string.Compare(ID, other.ID, StringComparison.Ordinal); + } +} + +public interface IReactionData : IComparable +{ + float Rate { get; } + int Priority { get; } + List<(ProtoId, FixedPoint2)> Reactants { get; } + List<(ProtoId, FixedPoint2)>? Catalysts { get; } + bool Quantized { get; } + float MinTemp { get; } + float MaxTemp{ get; } + bool TransferHeat{ get; } + string ProtoId { get; } + LogImpact? Impact { get; } + List? Conditions { get; } + List? Effects { get; } + List? ReagentEffects { get; } + + SoundSpecifier? Sound { get; } +} diff --git a/Content.Shared/Chemistry/Reaction/Prototypes/RateReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/Prototypes/RateReactionPrototype.cs new file mode 100644 index 00000000000000..e873600cfead85 --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Prototypes/RateReactionPrototype.cs @@ -0,0 +1,105 @@ +using Content.Shared.Chemistry.Reaction.Events; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry.Reaction.Prototypes; + + +//Basic implementation of rate limited reaction prototype primarily for digestion/medcode code to not suck ass. + +[Prototype] +public sealed partial class RateReactionPrototype : BaseReactionPrototype, IPrototype +{ + [DataField("requiredMixerCategories")] + public List>? MixingCategories = null; + + [DataField(required: true)] + public Dictionary, FixedPoint2> Products = new(); + + /// + /// Determines whether or not this reaction creates a new chemical (false) or if it's a breakdown for existing chemicals (true) + /// Used in the chemistry guidebook to make divisions between recipes and reaction sources. + /// + /// + /// Mixing together two reagents to get a third -> false + /// Heating a reagent to break it down into 2 different ones -> true + /// + [DataField] + public bool Source; + + public RateReaction Data + { + get + { + var reactantsList = new List<(ProtoId, FixedPoint2)>(); + foreach (var (key, value) in Reactants) + { + reactantsList.Add((key, value)); + } + var productsList = new List<(ProtoId, FixedPoint2)>(); + foreach (var (key, value) in Products) + { + productsList.Add((key, value)); + } + List<(ProtoId, FixedPoint2)>? catalystList = null; + if (Catalysts != null) + { + catalystList = new List<(ProtoId, FixedPoint2)>(); + foreach (var (key, value) in Catalysts) + { + catalystList.Add((key, value)); + } + } + return new( + Rate, + Priority, + reactantsList, + productsList, + catalystList, + Quantized, + MinTemp, + MaxTemp, + TransferHeat, + ID, + Impact, + Conditions, + Effects, + ReagentEffects, + Sound); + } + } +} + +[DataRecord, NetSerializable, Serializable] +public record struct RateReaction( + float Rate, + int Priority, + List<(ProtoId, FixedPoint2)> Reactants, + List<(ProtoId, FixedPoint2)> Products, + List<(ProtoId, FixedPoint2)>? Catalysts, + bool Quantized, + float MinTemp, + float MaxTemp, + bool TransferHeat, + string ProtoId, + LogImpact? Impact, + List? Conditions, + List? Effects, + List? ReagentEffects, + SoundSpecifier? Sound) : IReactionData +{ + public int CompareTo(IReactionData? other) + { + if (other == null) + return -1; + + if (Priority != other.Priority) + return other.Priority - Priority; + + return string.Compare(ProtoId, other.ProtoId, StringComparison.Ordinal); + } +} diff --git a/Content.Shared/Chemistry/Reaction/Systems/ChemicalAbsorptionSystem.cs b/Content.Shared/Chemistry/Reaction/Systems/ChemicalAbsorptionSystem.cs new file mode 100644 index 00000000000000..f489cef6618b5f --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Systems/ChemicalAbsorptionSystem.cs @@ -0,0 +1,351 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.Administration.Logs; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reaction.Components; +using Content.Shared.Chemistry.Reaction.Events; +using Content.Shared.Chemistry.Reaction.Prototypes; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Chemistry.Reaction.Systems; + +/// +/// This handles... +/// +public sealed class ChemicalAbsorptionSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly ChemicalRateReactionSystem _rateReactionSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + /// + public override void Initialize() + { + SubscribeLocalEvent(OnAbsorberInit); + SubscribeLocalEvent(OnAbsorberMapInit); + } + + private void OnAbsorberInit(EntityUid uid, ChemicalAbsorberComponent component, ref ComponentInit args) + { + UpdateAbsorptionCache((uid, component)); + component.LastUpdate = _timing.CurTime; + } + + private void OnAbsorberMapInit(EntityUid uid, ChemicalAbsorberComponent component, ref MapInitEvent args) + { + if (!TryComp(uid, out var solMan)) + { + Log.Error($"{ToPrettyString(uid)} has an absorberComponent but not solutionManager!"); + return; + } + AbsorbChemicals((uid, component, solMan), false); + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var absorber, out var solMan)) + { + if (_timing.CurTime < absorber.LastUpdate + absorber.UpdateRate) + continue; + AbsorbChemicals((uid,absorber, solMan), false); + } + } + + + private void UpdateAbsorptionCache(Entity absorber) + { + absorber.Comp.CachedAbsorptionOrder.Clear(); + HashSet addedAbsorptions = new(); + SortedList absorptions = new(); + foreach (var (absorptionGroupId, multiplier) in absorber.Comp.AbsorptionGroups) + { + var absorptionGroup = _protoManager.Index(absorptionGroupId); + foreach (var absorptionId in absorptionGroup.Absorptions) + { + var absorption = _protoManager.Index(absorptionId); + if (!addedAbsorptions.Add(absorption)) //prevent dupes + continue; + absorptions.Add(absorption.Priority, absorption.GetData(multiplier)); + } + } + if (absorber.Comp.AdditionalAbsorptions != null) + { + foreach (var (absorptionId, multiplier) in absorber.Comp.AdditionalAbsorptions) + { + var absorption = _protoManager.Index(absorptionId); + if (!addedAbsorptions.Add(absorption)) //prevent dupes + continue; + absorptions.Add(absorption.Priority, absorption.GetData(multiplier)); + } + } + absorber.Comp.CachedAbsorptionOrder = absorptions.Values.ToList(); + Dirty(absorber); + } + + public bool TryGetCachedAbsorption(Entity absorber, + ProtoId absorptionProto, + [NotNullWhen(true)] out AbsorptionReaction? foundCachedData) + { + foundCachedData = null; + foreach (var cachedData in absorber.Comp.CachedAbsorptionOrder) + { + if (absorptionProto.Id == cachedData.ProtoId) + { + foundCachedData = cachedData; + return true; + } + } + return false; + } + + + public void AbsorbChemicals(Entity solutionEntity, + Entity? absorber = null) + { + Entity foundAbsorber; + if (absorber == null) + { + var parentEnt = _transformSystem.GetParentUid(solutionEntity); + if (!TryComp(parentEnt, out var absorberComponent)) + return; + foundAbsorber = (parentEnt, absorberComponent); + } + else + { + foundAbsorber = absorber.Value; + } + + AbsorbChemicals(solutionEntity, foundAbsorber, false); + } + + private void AbsorbChemicals(Entity absorber, bool ignoreTimeScaling) + { + foreach (var solutionName in absorber.Comp1.LinkedSolutions) + { + if (!_solutionSystem.TryGetSolution((absorber, absorber.Comp2), solutionName, out var solutionEnt)) + { + Log.Error($"Could not find solution with name: {solutionName} on {ToPrettyString(absorber)}"); + return; + } + AbsorbChemicals(solutionEnt.Value, (absorber, absorber.Comp1), ignoreTimeScaling); + } + } + + private void AbsorbChemicals(Entity solutionEntity, Entity absorber, + bool ignoreTimeScaling) + { + foreach (var absorptionData in absorber.Comp.CachedAbsorptionOrder) + { + AbsorbChemical(absorber, solutionEntity, absorptionData, ignoreTimeScaling); + } + } + + private void AbsorbChemical( + Entity absorber, + Entity solutionEntity, + AbsorptionReaction cachedAbsorption, + bool ignoreTimeScaling) + { + var unitAbsorptions = GetReactionRate(absorber, solutionEntity, cachedAbsorption, ignoreTimeScaling); + if (unitAbsorptions <= 0) + return; + var solution = solutionEntity.Comp.Solution; + + var targetSolutionUpdated = false; + Entity? targetSolution = null; + + if (cachedAbsorption.CanTransfer && TryGetTargetSolution(absorber, out targetSolution)) + { + foreach (var (reactantName, volume) in cachedAbsorption.Reactants) + { + var amountToRemove = unitAbsorptions * volume; + //TODO: this might run into issues with reagentData + targetSolution.Value.Comp.Solution.AddReagent(reactantName,solution.RemoveReagent(reactantName, amountToRemove)); + targetSolutionUpdated = true; + } + } + else + { + foreach (var (reactantName, volume) in cachedAbsorption.Reactants) + { + var amountToRemove = unitAbsorptions * volume; + //TODO: this might run into issues with reagentData + solution.RemoveReagent(reactantName, amountToRemove); + } + } + + if (cachedAbsorption.TransferHeat) + { + var thermalEnergy = solution.GetThermalEnergy(_protoManager); + //TODO: actually apply the thermal energy to the absorber entity. Can't do that from shared... + // Because for some fucking reason temperatureSystem is server only. Why! Temperature should be predicted! + } + if (cachedAbsorption.Impact != null) + { + var posFound = _transformSystem.TryGetMapOrGridCoordinates(solutionEntity, out var gridPos); + _adminLogger.Add(LogType.ChemicalReaction, + cachedAbsorption.Impact.Value, + $"Chemical absorption {cachedAbsorption.ProtoId} occurred {unitAbsorptions} times on entity {ToPrettyString(solutionEntity)} " + + $"at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}"); + } + + if (cachedAbsorption.Effects != null) + { + foreach (var solutionEffect in cachedAbsorption.Effects) + { + solutionEffect.RaiseEvent(EntityManager, absorber, solutionEntity); + } + } + + if (cachedAbsorption.ReagentEffects != null) + { + //TODO refactor this when reagentEffects get rewritten to not fucking hardcode organs + //TODO: Also remove this once all ReagentEffects are converted to ReagentEvents + var args = new ReagentEffectArgs(solutionEntity, + null, + solutionEntity.Comp.Solution, + null, + unitAbsorptions, + EntityManager, + null, + 1f); + foreach (var effect in cachedAbsorption.ReagentEffects) + { + if (!effect.ShouldApply(args)) + continue; + + if (effect.ShouldLog) + { + var posFound = _transformSystem.TryGetMapOrGridCoordinates(solutionEntity, out var gridPos); + var entity = args.SolutionEntity; + _adminLogger.Add(LogType.ReagentEffect, + effect.LogImpact, + $"Absorption effect {effect.GetType().Name:effect} of absorption " + + $"{cachedAbsorption.ProtoId} applied on entity {ToPrettyString(entity):entity} at Pos:" + + $"{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}"); + } + + effect.Effect(args); + } + } + if (targetSolutionUpdated) // Dirty/Update the solutions, do this last so absorptions resolve before reactions + _solutionSystem.UpdateChemicals(targetSolution!.Value, true, false); + _solutionSystem.UpdateChemicals(solutionEntity, true, false); + _audioSystem.PlayPvs(cachedAbsorption.Sound, solutionEntity); + } + + public bool CanAbsorb( + Entity absorber, + Entity solutionEntity, + ProtoId absorptionId) + { + if (!TryGetCachedAbsorption(absorber, absorptionId, out var foundData)) + return false; + return GetReactionRate(absorber, solutionEntity, foundData.Value, false) > 0; + } + + private float GetReactionRate( + Entity absorber, + Entity solutionEntity, + AbsorptionReaction absorption, + bool ignoreTimeScaling) + { + var rate = _rateReactionSystem.GetReactionRate(solutionEntity, + absorption, + absorber.Comp.LastUpdate, + absorber.Comp.GlobalRateMultiplier, + ignoreTimeScaling); + if (rate == 0) + return 0; + + if (absorption.Conditions == null) + return rate; + foreach (var condition in absorption.Conditions) + { + var ev = condition.RaiseEvent(EntityManager, absorber, solutionEntity); + if (!ev.Valid) + return 0; + } + return rate; + } + + /// + /// Adjusts the reaction rate calculation by the amount of time that has passed since the last update + /// This makes sure that reaction rates are always consistent and don't change based on the number of times you + /// call the update function + /// + /// + /// + /// + /// + /// + private FixedPoint2 AdjustReactionRateByTime(FixedPoint2 reactionRate, TimeSpan lastUpdate, FixedPoint2 multiplier, + bool ignoreTimeScaling) + { + if (ignoreTimeScaling) + return reactionRate * multiplier; + var duration =_timing.CurTime.TotalSeconds - lastUpdate.TotalSeconds; + //if any of these are negative something has fucked up + DebugTools.Assert(duration < 0 || multiplier < 0 || reactionRate < 0); + if (reactionRate == 0) //let's not throw an exception shall we + return 0; + return duration / reactionRate * multiplier; + } + + + /// + /// Updates the reaction rate if our new calculated rate is lower than the existing one + /// + /// + /// + /// + /// + /// + /// + /// + private void UpdateReactionRateFromReactants( + ref FixedPoint2 reactionRate, + FixedPoint2 quantity, + FixedPoint2 requiredVolume, + TimeSpan lastUpdate, + FixedPoint2 multiplier, + bool quantized, + bool ignoreTimeScaling) + { + var unitReactions = AdjustReactionRateByTime(quantity / requiredVolume, lastUpdate, + multiplier, ignoreTimeScaling); + if (quantized) + unitReactions = MathF.Floor(unitReactions.Float()); + + if (unitReactions < reactionRate) + { + reactionRate = unitReactions; + } + } + + public bool TryGetTargetSolution(Entity absorber, + [NotNullWhen(true)] out Entity? solution) + { + solution = null; + if (absorber.Comp.TransferTargetEntity == null) + return false; + if (_solutionSystem.TryGetSolution((absorber.Comp.TransferTargetEntity.Value, null), + absorber.Comp.TransferTargetSolutionId, out solution)) + return false; + return solution != null; + + } + +} diff --git a/Content.Shared/Chemistry/Reaction/Systems/ChemicalRateReactionSystem.cs b/Content.Shared/Chemistry/Reaction/Systems/ChemicalRateReactionSystem.cs new file mode 100644 index 00000000000000..0f87f85911bac9 --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/Systems/ChemicalRateReactionSystem.cs @@ -0,0 +1,229 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reaction.Prototypes; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Chemistry.Reaction.Systems; + +public sealed class ChemicalRateReactionSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + + public float GetReactionRate( + Entity targetSolution, + T reaction, + TimeSpan lastUpdate, + float multiplier = 1.0f, + bool ignoreTimeScaling = false) where T: struct, IReactionData + { + //TODO: Move this over to a shared method in chemSystem after chem gets refactored to use ReactionRates + //The logic for this will basically be the same as for reactions after reaction rates are implemented + var solution = targetSolution.Comp.Solution; + //(when not forced) Prevent running reactions multiple times in the same tickif we end up calling this + //function multiple times This prevents the burning orphanage scenario + //(still don't mess with solutions from within solution effects) + if (solution.Temperature > reaction.MaxTemp + || solution.Temperature < reaction.MinTemp + //(when not forced) Prevent running reactions multiple times in the same tickif we end up calling this + //function multiple times This prevents the burning orphanage scenario + //(still don't mess with solutions from within solution effects) + || !ignoreTimeScaling && lastUpdate == _timing.CurTime + ) + return 0; + + var unitReactions = AdjustReactionRateByTime(reaction.Rate, + lastUpdate, + multiplier, + ignoreTimeScaling); + if (unitReactions == 0 || reaction.Quantized && unitReactions < 1) + return 0; + if (unitReactions > 1) + unitReactions = 1; + + if (reaction.Catalysts != null) + { + foreach (var (reagentName, requiredVolume) in reaction.Catalysts) + { + var reactantQuantity = solution.GetTotalPrototypeQuantity(reagentName) * multiplier; + if (reactantQuantity == FixedPoint2.Zero || reaction.Quantized && reactantQuantity < requiredVolume) + return 0; + //Limit reaction rate by catalysts, technically catalysts would allow you to accelerate a reaction rate past + //it's normal rate but that's functionality that someone else can add later. For now, we are assuming that + //the rate specified on the reaction/absorption is the maximum catalyzed rate if catalysts are specified. + UpdateReactionRateFromReactants(ref unitReactions, + reactantQuantity, + requiredVolume, + lastUpdate, + multiplier, + reaction.Quantized, + ignoreTimeScaling); + } + } + + foreach (var (reagentName, requiredVolume) in reaction.Reactants) + { + var reactantQuantity = solution.GetTotalPrototypeQuantity(reagentName); + if (reactantQuantity <= FixedPoint2.Zero) + return 0; + UpdateReactionRateFromReactants(ref unitReactions, + reactantQuantity, + requiredVolume, + lastUpdate, + multiplier, + reaction.Quantized, + ignoreTimeScaling); + } + return unitReactions; + } + + /// + /// Updates the reaction rate if our new calculated rate is lower than the existing one + /// + /// + /// + /// + /// + /// + /// + /// + private void UpdateReactionRateFromReactants( + ref float reactionRate, + FixedPoint2 quantity, + FixedPoint2 requiredVolume, + TimeSpan lastUpdate, + float multiplier, + bool quantized, + bool ignoreTimeScaling) + { + var unitReactions = AdjustReactionRateByTime(quantity.Float() / requiredVolume.Float(), + lastUpdate, + multiplier, + ignoreTimeScaling); + if (quantized) + unitReactions = MathF.Floor(unitReactions); + + if (unitReactions < reactionRate) + { + reactionRate = unitReactions; + } + } + + /// + /// Adjusts the reaction rate calculation by the amount of time that has passed since the last update + /// This makes sure that reaction rates are always consistent and don't change based on the number of times you + /// call the update function + /// + /// + /// + /// + /// + /// + private float AdjustReactionRateByTime(float reactionRate, + TimeSpan lastUpdate, + float multiplier, + bool ignoreTimeScaling) + { + if (ignoreTimeScaling) + return reactionRate * multiplier; + var duration =(float)(_timing.CurTime.TotalSeconds - lastUpdate.TotalSeconds); + //if any of these are negative something has fucked up + DebugTools.Assert(duration < 0 || multiplier < 0 || reactionRate < 0); + if (reactionRate == 0) //let's not throw an exception shall we + return 0; + return duration / reactionRate * multiplier; + } + + public void RunReaction( + EntityUid target, + Entity targetSolution, + RateReaction reaction, + TimeSpan lastUpdate, + float multiplier = 1.0f, + bool ignoreTimeScaling = false) + { + var unitAbsorptions = GetReactionRate(targetSolution, reaction, lastUpdate, multiplier, ignoreTimeScaling); + if (unitAbsorptions <= 0) + return; + + foreach (var (reactantName, volume) in reaction.Reactants) + { + var amountToRemove = unitAbsorptions * volume; + targetSolution.Comp.Solution.RemoveReagent(reactantName, amountToRemove); + } + foreach (var (reactantName, volume) in reaction.Products) + { + var amountToAdd = unitAbsorptions * volume; + targetSolution.Comp.Solution.AddReagent(reactantName, amountToAdd); + } + + if (reaction.TransferHeat) + { + var thermalEnergy = targetSolution.Comp.Solution.GetThermalEnergy(_protoManager); + //TODO: actually apply the thermal energy to the reaction entity. Can't do that from shared... + // Because for some fucking reason temperatureSystem is server only. Why! Temperature should be predicted! + } + if (reaction.Impact != null) + { + var posFound = _transformSystem.TryGetMapOrGridCoordinates(targetSolution, out var gridPos); + _adminLogger.Add(LogType.ChemicalReaction, + reaction.Impact.Value, + $"Chemical absorption {reaction.ProtoId} occurred {unitAbsorptions} times on entity {ToPrettyString(targetSolution)} " + + $"at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}"); + } + + if (reaction.Effects != null) + { + foreach (var solutionEffect in reaction.Effects) + { + solutionEffect.RaiseEvent(EntityManager, target, targetSolution); + } + } + + if (reaction.ReagentEffects != null) + { + //TODO refactor this when reagentEffects get rewritten to not fucking hardcode organs + //TODO: Also remove this once all ReagentEffects are converted to ReagentEvents + var args = new ReagentEffectArgs(targetSolution, + null, + targetSolution.Comp.Solution, + null, + unitAbsorptions, + EntityManager, + null, + 1f); + foreach (var effect in reaction.ReagentEffects) + { + if (!effect.ShouldApply(args)) + continue; + + if (effect.ShouldLog) + { + var posFound = _transformSystem.TryGetMapOrGridCoordinates(targetSolution, out var gridPos); + var entity = args.SolutionEntity; + _adminLogger.Add(LogType.ReagentEffect, + effect.LogImpact, + $"Absorption effect {effect.GetType().Name:effect} of absorption " + + $"{reaction.ProtoId} applied on entity {ToPrettyString(entity):entity} at Pos:" + + $"{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}"); + } + + effect.Effect(args); + } + } + _solutionSystem.UpdateChemicals(targetSolution, true, false); + _audioSystem.PlayPvs(reaction.Sound, targetSolution); + } +} diff --git a/Content.Shared/Chemistry/Reagent/ReagentData.cs b/Content.Shared/Chemistry/Reagent/ReagentDiscriminator.cs similarity index 81% rename from Content.Shared/Chemistry/Reagent/ReagentData.cs rename to Content.Shared/Chemistry/Reagent/ReagentDiscriminator.cs index a4a77f552e32db..db4884b9f28b03 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentData.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentDiscriminator.cs @@ -4,7 +4,7 @@ namespace Content.Shared.Chemistry.Reagent; [ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] -public abstract partial class ReagentData : IEquatable +public abstract partial class ReagentDiscriminator : IEquatable { /// /// Convert to a string representation. This if for logging & debugging. This is not localized and should not be @@ -24,7 +24,7 @@ public virtual string ToString(string prototype) return $"{prototype}:{GetType().Name}"; } - public abstract bool Equals(ReagentData? other); + public abstract bool Equals(ReagentDiscriminator? other); public override bool Equals(object? obj) { @@ -35,10 +35,10 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((ReagentData) obj); + return Equals((ReagentDiscriminator) obj); } public abstract override int GetHashCode(); - public abstract ReagentData Clone(); + public abstract ReagentDiscriminator Clone(); } diff --git a/Content.Shared/Chemistry/Reagent/ReagentId.cs b/Content.Shared/Chemistry/Reagent/ReagentId.cs index 07a420021971e2..7b1851b2f4d5fc 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentId.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentId.cs @@ -20,12 +20,12 @@ public partial struct ReagentId : IEquatable /// Any additional data that is unique to this reagent type. E.g., for blood this could be DNA data. /// [DataField("data")] - public ReagentData? Data { get; private set; } + public ReagentDiscriminator? Discriminator { get; private set; } - public ReagentId(string prototype, ReagentData? data) + public ReagentId(string prototype, ReagentDiscriminator? discriminator) { Prototype = prototype; - Data = data; + Discriminator = discriminator; } public ReagentId() @@ -38,27 +38,27 @@ public bool Equals(ReagentId other) if (Prototype != other.Prototype) return false; - if (Data == null) - return other.Data == null; + if (Discriminator == null) + return other.Discriminator == null; - if (other.Data == null) + if (other.Discriminator == null) return false; - if (Data.GetType() != other.Data.GetType()) + if (Discriminator.GetType() != other.Discriminator.GetType()) return false; - return Data.Equals(other.Data); + return Discriminator.Equals(other.Discriminator); } - public bool Equals(string prototype, ReagentData? otherData = null) + public bool Equals(string prototype, ReagentDiscriminator? otherData = null) { if (Prototype != prototype) return false; - if (Data == null) + if (Discriminator == null) return otherData == null; - return Data.Equals(otherData); + return Discriminator.Equals(otherData); } public override bool Equals(object? obj) @@ -68,17 +68,17 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(Prototype, Data); + return HashCode.Combine(Prototype, Discriminator); } public string ToString(FixedPoint2 quantity) { - return Data?.ToString(Prototype, quantity) ?? $"{Prototype}:{quantity}"; + return Discriminator?.ToString(Prototype, quantity) ?? $"{Prototype}:{quantity}"; } public override string ToString() { - return Data?.ToString(Prototype) ?? Prototype; + return Discriminator?.ToString(Prototype) ?? Prototype; } public static bool operator ==(ReagentId left, ReagentId right) diff --git a/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs b/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs index 9644f919f748a7..62aeae95ea6822 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs @@ -17,7 +17,7 @@ public partial struct ReagentQuantity : IEquatable [ViewVariables] public ReagentId Reagent { get; private set; } - public ReagentQuantity(string reagentId, FixedPoint2 quantity, ReagentData? data) + public ReagentQuantity(string reagentId, FixedPoint2 quantity, ReagentDiscriminator? data) : this(new ReagentId(reagentId, data), quantity) { } @@ -37,11 +37,11 @@ public override string ToString() return Reagent.ToString(Quantity); } - public void Deconstruct(out string prototype, out FixedPoint2 quantity, out ReagentData? data) + public void Deconstruct(out string prototype, out FixedPoint2 quantity, out ReagentDiscriminator? data) { prototype = Reagent.Prototype; quantity = Quantity; - data = Reagent.Data; + data = Reagent.Discriminator; } public void Deconstruct(out ReagentId id, out FixedPoint2 quantity) diff --git a/Content.Shared/Gibbing/Systems/GibbingSystem.cs b/Content.Shared/Gibbing/Systems/GibbingSystem.cs index f3d982977a75c2..66904a6ae04617 100644 --- a/Content.Shared/Gibbing/Systems/GibbingSystem.cs +++ b/Content.Shared/Gibbing/Systems/GibbingSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.Gibbing.Components; using Content.Shared.Gibbing.Events; @@ -93,6 +93,10 @@ public bool TryGibEntityWithRef( List? excludedContainers = null, bool logMissingGibable = false) { + //TODO: Placeholder for testing! Replace with proper bodypart gibbing implementation! + if (!HasComp(outerEntity)) + return false; + if (!Resolve(gibbable, ref gibbable.Comp, logMissing: false)) { DropEntity(gibbable, Transform(outerEntity), randomSpreadMod, ref droppedEntities, diff --git a/Content.Shared/Medical/Blood/Components/BloodstreamComponent.cs b/Content.Shared/Medical/Blood/Components/BloodstreamComponent.cs new file mode 100644 index 00000000000000..af9e04b905790a --- /dev/null +++ b/Content.Shared/Medical/Blood/Components/BloodstreamComponent.cs @@ -0,0 +1,101 @@ +using Content.Shared.Alert; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Medical.Blood.Components; + +/// +/// This is a very simplified bleeding system that is intended for non-medically simulated entities. +/// It does not track blood-pressure, pulse, or have any blood type logic. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class BloodstreamComponent : Component +{ + /// + /// The next time that bleeds will be checked. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate; + + [DataField, AutoNetworkedField] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1.0f); + + /// + /// How much blood is currently being lost by bleed per tick, this number must be positive + /// + [DataField, AutoNetworkedField] + public FixedPoint2 Bloodloss = 0; + + [DataField, AutoNetworkedField] + public FixedPoint2 BleedPuddleThreshold = 1.0f; + + /// + /// How much blood is regenerated per tick, this number must be positive + /// + [DataField, AutoNetworkedField] + public FixedPoint2 Regen = 0; + + /// + /// What volume should we cut off blood regeneration at + /// if this is negative use maxVolume + /// + [DataField, AutoNetworkedField] + public FixedPoint2 RegenCutoffVolume = -1; + + /// + /// Cached/starting volume for this bloodstream + /// If this starts as negative it will use MaxVolume + /// + [DataField, AutoNetworkedField] + public FixedPoint2 Volume = -1; + + /// + /// The maximum volume for this bloodstream + /// + [DataField, AutoNetworkedField] //TODO: Required + public FixedPoint2 MaxVolume = 200; + + public const string BloodSolutionId = "bloodstream"; + + public const string DissolvedReagentSolutionId = "bloodReagents"; + + public const string SpillSolutionId = "bloodSpill"; + + /// + /// The reagent prototypeId that this entity uses for blood + /// + [DataField, AutoNetworkedField] //TODO: required + public string? BloodReagent = "Blood"; + + /// + /// This is the primary blood reagent in this bloodstream + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public ReagentId BloodReagentId = default; + + /// + /// The bloodstream solution + /// + [DataField, AutoNetworkedField] + public EntityUid? BloodSolutionEnt = null; + + /// + /// The bloodstream solution + /// + [DataField, AutoNetworkedField] + public EntityUid? BloodRegentsSolutionEnt = null; + + /// + /// The solution to "drip" onto the ground or soak into clothing + /// + [DataField, AutoNetworkedField] + public EntityUid? SpillSolutionEnt = null; + + // TODO do alert + [DataField] + public ProtoId BleedingAlert = "Bleed"; +} diff --git a/Content.Shared/Medical/Blood/Components/VascularSystemComponent.cs b/Content.Shared/Medical/Blood/Components/VascularSystemComponent.cs new file mode 100644 index 00000000000000..6892a72658e9ef --- /dev/null +++ b/Content.Shared/Medical/Blood/Components/VascularSystemComponent.cs @@ -0,0 +1,119 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Prototypes; +using Content.Shared.Medical.Blood.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Blood.Components; + + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class VascularSystemComponent : Component +{ + + #region Simulation + + /// + /// The healthy volume for this bloodstream. + /// + [DataField, AutoNetworkedField] //TODO: Required + public FixedPoint2 HealthyVolume = 250; + + /// + /// Healthy blood pressure values. + /// The first value is: high (systolic) pressure. This is the blood pressure at the moment blood is pumped. + /// The second value is: low (diastolic) pressure. This is the blood pressure in between pumps. + /// + [DataField, AutoNetworkedField] //TODO: Required + public BloodPressure HealthyBloodPressure = (120,0); + + /// + /// The current blood pressure. With the first value being high and the second being low + /// If this value is null at mapinit, it will be populated. + /// This value will become null if there are no circulationEntities or if there is no cardiac output (aka heart is fucked) + /// + [DataField, AutoNetworkedField] + public BloodPressure? CurrentBloodPressure = null; + + /// + /// The current cached pulse rate, this is the fastest rate from any of the circulation entities + /// This value will become null if there are no circulationEntities or if there is no cardiac output (aka heart is fucked) + /// + [DataField, AutoNetworkedField] + public FixedPoint2? Pulse = null; + + /// + /// Vascular resistance, aka how much your blood vessels resist the flow of blood + /// Used as a multiplier for VascularConstant to calculate blood pressure, expressed as a percentage from 0-1 + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public FixedPoint2 VascularResistance = 1; + + /// + /// Entities that are pumping this blood + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public List CirculationEntities ; + + /// + /// Cached optimal CardiacOutput value, combined value from all the circulation entities + /// This is mainly used for re-calculating vascular constants + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public FixedPoint2 OptimalCardiacOutput; + + /// + /// Constant used to calculate blood diastolic blood pressure + /// This is calculated based on the healthy low pressure and + /// multiplied by VascularResistance before being used + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public float LowPressureVascularConstant; + + /// + /// Constant used to calculate blood diastolic blood pressure + /// This is calculated based on the healthy high pressure and + /// multiplied by VascularResistance before being used + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public float HighPressureVascularConstant; + + #endregion + + #region BloodGroups + + /// + /// This prototype defines the blood that is used in this bloodstream, specifically which reagents are used and + /// the linked defined bloodtypes. It also provides the list of possible bloodtypes and the likelihood of + /// spawning with a particular one. + /// + [DataField, AutoNetworkedField] //TODO: Required + public string BloodDefinition = string.Empty; //TODO: convert back to protoID + + /// + /// If this is defined, it will set this bloodstream to use the specified bloodtype. + /// Make sure the bloodtype is in the listed types in bloodDefinition if EnforceBloodTypes is true or + /// immediate blood poisoning will occur (maybe you want that I'm not judging). + /// If this is null, the bloodtype will be randomly selected from the bloodtypes defined in the blood definition + /// according to their probabilities. + /// + [DataField, AutoNetworkedField] + public string? BloodType = null; //TODO: convert back to protoID + + /// + /// If set to true, check blood transfusions to make sure that they do not contain antibodies that are + /// not on the allowed list. If set to false, skip antibody and blood toxicity checks + /// + [DataField, AutoNetworkedField] + public bool EnforceBloodTypes = true; + + /// + /// Which antigens are allowed in this bloodstream. If an antigen is not on this list + /// it will cause a blood toxicity response (and that is very bad). This value is populated + /// when a bloodtype is selected. Note: this functionality is disabled when EnforceBloodTypes is false! + /// + [DataField, AutoNetworkedField] + public HashSet> AllowedAntigens = new(); + + #endregion +} diff --git a/Content.Shared/Medical/Blood/Events/BloodstreamEvents.cs b/Content.Shared/Medical/Blood/Events/BloodstreamEvents.cs new file mode 100644 index 00000000000000..dfd8f503da5373 --- /dev/null +++ b/Content.Shared/Medical/Blood/Events/BloodstreamEvents.cs @@ -0,0 +1,8 @@ +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Medical.Blood.Components; + +namespace Content.Shared.Medical.Blood.Events; + + +[ByRefEvent] +public record struct BloodstreamUpdatedEvent(Entity Bloodstream); diff --git a/Content.Shared/Medical/Blood/Prototypes/BloodAntigenPrototype.cs b/Content.Shared/Medical/Blood/Prototypes/BloodAntigenPrototype.cs new file mode 100644 index 00000000000000..98094acda44c07 --- /dev/null +++ b/Content.Shared/Medical/Blood/Prototypes/BloodAntigenPrototype.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Blood.Prototypes; + +/// +/// This is a prototype for blood antigens, this effectively acts as an enum for bloodtypes to use. +/// +[Prototype()] +public sealed partial class BloodAntigenPrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; +} diff --git a/Content.Shared/Medical/Blood/Prototypes/BloodDefinitionPrototype.cs b/Content.Shared/Medical/Blood/Prototypes/BloodDefinitionPrototype.cs new file mode 100644 index 00000000000000..2a73afcc4853dd --- /dev/null +++ b/Content.Shared/Medical/Blood/Prototypes/BloodDefinitionPrototype.cs @@ -0,0 +1,25 @@ + +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Blood.Prototypes; + +/// +/// This is a prototype for defining blood in a circulatory system. +/// +[Prototype()] +public sealed partial class BloodDefinitionPrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + /// + /// A dictionary containing all the bloodtypes supported by this blood definition and their chance of being + /// selected when initially creating a bloodstream + /// + [DataField(required:true)] + public Dictionary, FixedPoint2> BloodTypeDistribution = new(); + + public ICollection> BloodTypes => BloodTypeDistribution.Keys; +} diff --git a/Content.Shared/Medical/Blood/Prototypes/BloodTypePrototype.cs b/Content.Shared/Medical/Blood/Prototypes/BloodTypePrototype.cs new file mode 100644 index 00000000000000..320378393c0e79 --- /dev/null +++ b/Content.Shared/Medical/Blood/Prototypes/BloodTypePrototype.cs @@ -0,0 +1,54 @@ +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Blood.Prototypes; + +/// +/// This is a prototype for defining blood groups (O, A, B, AB, etc.) +/// +[Prototype] +public sealed partial class BloodTypePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + /// + /// What percentage of the blood is plasma, the rest are bloodCells + /// + [DataField] + public float PlasmaPercentage = 0.55f; + + /// + /// Which antigens are present in this blood type's blood cells + /// + [DataField] + public List> BloodCellAntigens = new(); + + /// + /// Which antigens are present in this blood type's blood plasma + /// + [DataField] + public List> PlasmaAntigens = new(); + + /// + /// The reagent that represents the combination of both bloodcells and plasma. + /// This is the reagent used as blood in bloodstream. + /// + [DataField] + public ProtoId WholeBloodReagent = "Blood"; + + /// + /// The reagent used for blood cells in this blood definition, this may hold any number of antibodies. + /// This is used for blood donations or when filtering. + /// + [DataField] + public ProtoId BloodCellsReagent; + + /// + /// The reagent used for blood plasma in this blood definition, this may hold any number of antibodies. + /// This is used for plasma donations or when filtering. + /// + [DataField] + public ProtoId BloodPlasmaReagent; +} diff --git a/Content.Shared/Medical/Blood/Systems/BloodTypeDiscriminator.cs b/Content.Shared/Medical/Blood/Systems/BloodTypeDiscriminator.cs new file mode 100644 index 00000000000000..0e0f290de7ce35 --- /dev/null +++ b/Content.Shared/Medical/Blood/Systems/BloodTypeDiscriminator.cs @@ -0,0 +1,83 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Medical.Blood.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Blood.Systems; + +[Serializable, NetSerializable] +public sealed partial class BloodTypeDiscriminator : ReagentDiscriminator +{ + [DataField(required: true)] + public HashSet> BloodAntigens; + + [DataField(required: true)] + public HashSet> PlasmaAntigens; + + [DataField] + public ProtoId? BloodTypeId; + + //TODO: DNA + + public BloodTypeDiscriminator(BloodTypePrototype bloodType) : + this(bloodType.BloodCellAntigens, bloodType.PlasmaAntigens, bloodType.ID) + { + } + + public BloodTypeDiscriminator(IEnumerable> bloodAntigens, + IEnumerable> plasmaAntigens, + ProtoId? bloodTypeId = null) + { + BloodAntigens = []; + PlasmaAntigens = []; + foreach (var antigen in bloodAntigens) + { + BloodAntigens.Add(antigen); + } + foreach (var antigen in plasmaAntigens) + { + PlasmaAntigens.Add(antigen); + } + BloodTypeId = bloodTypeId; + } + + public BloodTypeDiscriminator(HashSet> bloodAntigens, + HashSet> plasmaAntigens, + ProtoId? bloodTypeId = null) + { + BloodAntigens = [..bloodAntigens]; + PlasmaAntigens = [..plasmaAntigens]; + BloodTypeId = bloodTypeId; + } + + public override bool Equals(ReagentDiscriminator? other) + { + var test = other as BloodTypeDiscriminator; + return test != null + && BloodAntigens.SetEquals(test.BloodAntigens) + && PlasmaAntigens.SetEquals(test.PlasmaAntigens) + && BloodTypeId == test.BloodTypeId; + } + + public override int GetHashCode() + { + return (BloodAntigens, PlasmaAntigens, BloodTypeId).GetHashCode(); + } + + public override ReagentDiscriminator Clone() + { + return new BloodTypeDiscriminator(BloodAntigens, PlasmaAntigens, BloodTypeId); + } + + public static implicit operator BloodTypeDiscriminator(BloodTypePrototype bloodType) + { + return new BloodTypeDiscriminator(bloodType); + } +} + +public enum BloodAntigenPolicy : byte +{ + Overwrite, + KeepOriginal, + Merge +} diff --git a/Content.Shared/Medical/Blood/Systems/BloodstreamSystem.cs b/Content.Shared/Medical/Blood/Systems/BloodstreamSystem.cs new file mode 100644 index 00000000000000..60302371109e12 --- /dev/null +++ b/Content.Shared/Medical/Blood/Systems/BloodstreamSystem.cs @@ -0,0 +1,225 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Events; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Medical.Blood.Systems; + +public sealed class BloodstreamSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly VascularSystem _vascularSystem = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly INetManager _netManager = default!; + + public static int BloodstreamVolumeTEMP = 250; //TODO: unhardcode this shit + + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnBloodstreamMapInit, + after: [typeof(SharedSolutionContainerSystem)]); + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var bloodstreamComp, out var solMan)) + { + if (_gameTiming.CurTime < bloodstreamComp.NextUpdate) + continue; + bloodstreamComp.NextUpdate += bloodstreamComp.UpdateInterval; + ApplyBleeds((uid, bloodstreamComp, solMan)); + RegenBlood((uid, bloodstreamComp, solMan)); + + var ev = new BloodstreamUpdatedEvent((uid, bloodstreamComp, solMan)); + RaiseLocalEvent(uid, ref ev); + } + } + + public void ApplyBleed(Entity bloodstream, FixedPoint2 bleedToAdd) + { + if (bleedToAdd == 0 || !Resolve(bloodstream, ref bloodstream.Comp)) + return; + bloodstream.Comp.Bloodloss += bleedToAdd; + if (bloodstream.Comp.Bloodloss < 0) + bloodstream.Comp.Bloodloss = 0; + Dirty(bloodstream); + } + + public void ApplyRegen(Entity bloodstream, FixedPoint2 regenToAdd) + { + if (regenToAdd == 0 || !Resolve(bloodstream, ref bloodstream.Comp)) + return; + bloodstream.Comp.Regen += regenToAdd; + if (bloodstream.Comp.Regen < 0) + bloodstream.Comp.Regen = 0; + Dirty(bloodstream); + } + + private void OnBloodstreamMapInit(EntityUid bloodstreamEnt, BloodstreamComponent bloodstream, ref MapInitEvent args) + { + if (_netManager.IsClient) + return; + + if (!_solutionSystem.EnsureSolutionEntity((bloodstreamEnt, null), + BloodstreamComponent.BloodSolutionId, + out var bloodSolution, + bloodstream.MaxVolume) + || !_solutionSystem.EnsureSolutionEntity((bloodstreamEnt, null), + BloodstreamComponent.SpillSolutionId, + out var spillSolution, + FixedPoint2.MaxValue) + || !_solutionSystem.EnsureSolutionEntity((bloodstreamEnt, null), + BloodstreamComponent.DissolvedReagentSolutionId, + out var bloodReagentSolution, + FixedPoint2.MaxValue)) + return;//this will never get called because ensureSolution only returns false on client and MapInit only runs on server, but this fixes nullables + + _solutionSystem.SetCapacity((bloodSolution.Value, bloodSolution), bloodstream.MaxVolume); + _solutionSystem.SetCapacity((bloodReagentSolution.Value, bloodReagentSolution), FixedPoint2.MaxValue); + _solutionSystem.SetCapacity((spillSolution.Value, spillSolution), FixedPoint2.MaxValue); + + var volume = bloodstream.Volume > 0 ? bloodstream.Volume : bloodstream.MaxVolume; + + if (bloodstream.RegenCutoffVolume < 0) + bloodstream.RegenCutoffVolume = bloodstream.MaxVolume; + + bloodstream.BloodSolutionEnt = bloodSolution; + bloodstream.SpillSolutionEnt = spillSolution; + bloodstream.BloodRegentsSolutionEnt = bloodReagentSolution; + + Dirty(bloodstreamEnt, bloodstream); + + //If we have a circulation comp, call the setup method on bloodCirculationSystem + if (TryComp(bloodstreamEnt, out var bloodCircComp)) + { + _vascularSystem.SetupCirculation(bloodstreamEnt, bloodstream, bloodCircComp, volume, bloodSolution.Value); + } + else + { + if (bloodstream.BloodReagent == null) + { + throw new Exception($"Blood reagent is not defined for {ToPrettyString(bloodstreamEnt)}"); + } + _solutionSystem.AddSolution((bloodSolution.Value, bloodSolution), + new Solution(bloodstream.BloodReagent, volume)); + bloodstream.BloodReagentId = new ReagentId(bloodstream.BloodReagent, null); + bloodstream.Volume = volume; + } + } + + private void RegenBlood(Entity bloodstream) + { + //Don't passively regenerate blood if we are over the "healthy" volume + if (bloodstream.Comp1.Regen == 0 + || bloodstream.Comp1.Volume >= bloodstream.Comp1.RegenCutoffVolume + || !_solutionSystem.TryGetSolution((bloodstream, bloodstream), + BloodstreamComponent.BloodSolutionId, out var bloodSolution, true)) + return; + + bloodSolution.Value.Comp.Solution.Volume += bloodstream.Comp1.Regen; + _solutionSystem.UpdateChemicals(bloodSolution.Value); + + //Update the cached blood volume + bloodstream.Comp1.Volume = bloodSolution.Value.Comp.Solution.Volume; + Dirty(bloodstream); + } + + private void ApplyBleeds(Entity bloodstream) + { + if (bloodstream.Comp1.Bloodloss == 0 || !TryGetBloodStreamSolutions(bloodstream, + out var bloodSolution, + out var reagentSolution, + out var spillSolution)) + return; + + var bleedSol = _solutionSystem.SplitSolution(bloodSolution.Value, bloodstream.Comp1.Bloodloss); + //Get the disolved reagent loss amount by getting the bleed percentage and then multiplying it by the disolved reagent volume + var reagentLossAmount = reagentSolution.Value.Comp.Solution.Volume * (bloodstream.Comp1.Bloodloss / bloodSolution.Value.Comp.Solution.MaxVolume); + + //Not sure if it's the best idea to just spill the dissolved reagents straight into the blood puddle but we don't have + //functionality for a reagent holding other dissolved reagents + var lostDissolvedReagents = _solutionSystem.SplitSolution(reagentSolution.Value, reagentLossAmount); + spillSolution.Value.Comp.Solution.AddSolution(lostDissolvedReagents, _protoManager); + + spillSolution.Value.Comp.Solution.AddSolution(bleedSol, _protoManager); + if (spillSolution.Value.Comp.Solution.Volume > bloodstream.Comp1.BleedPuddleThreshold) + { + CreateBloodPuddle(bloodstream, spillSolution.Value.Comp.Solution); + } + _solutionSystem.RemoveAllSolution(spillSolution.Value); + //Update the cached blood volume + bloodstream.Comp1.Volume = bloodSolution.Value.Comp.Solution.Volume; + Dirty(bloodstream); + } + + //TODO: protected abstract this + private void CreateBloodPuddle(Entity bloodstream, + Solution spillSolution) + { + //TODO: placeholder, clear the reagent to prevent mispredicts + spillSolution.RemoveAllSolution(); + //Puddle spill implementation is serverside only so this will be abstract and only implemented on the server + //TODO: Make sure to transfer DNA as well (serverside only too) + Log.Debug($"PLACEHOLDER: A blood puddle should have been spawned for {ToPrettyString(bloodstream)}!"); + } + + + public bool TryGetBloodStreamSolutions(Entity bloodstream, + [NotNullWhen(true)]out Entity? bloodSolution, + [NotNullWhen(true)]out Entity? bloodReagentSolution, + [NotNullWhen(true)] out Entity? spillSolution) + { + bloodSolution = null; + spillSolution = null; + bloodReagentSolution = null; + return _solutionSystem.TryGetSolution((bloodstream, bloodstream), + BloodstreamComponent.BloodSolutionId, + out bloodSolution, true) + && _solutionSystem.TryGetSolution((bloodstream, bloodstream), + BloodstreamComponent.DissolvedReagentSolutionId, + out bloodReagentSolution, true) + && _solutionSystem.TryGetSolution((bloodstream, bloodstream), + BloodstreamComponent.SpillSolutionId, + out spillSolution, true); + } + + public bool TryGetBloodSolution(Entity bloodstream, + [NotNullWhen(true)]out Entity? bloodSolution) + { + return _solutionSystem.TryGetSolution((bloodstream, bloodstream), + BloodstreamComponent.BloodSolutionId, out bloodSolution, true); + } + + public Entity GetBloodSolution( + Entity bloodstream) + { + if (bloodstream.Comp.BloodSolutionEnt == null) + throw new Exception($"{ToPrettyString(bloodstream)} is missing a linked Blood Solution!"); + return (bloodstream.Comp.BloodSolutionEnt.Value, Comp(bloodstream.Comp.BloodSolutionEnt.Value)); + } + public Entity GetSpillSolution( + Entity bloodstream) + { + if (bloodstream.Comp.SpillSolutionEnt == null) + throw new Exception($"{ToPrettyString(bloodstream)} is missing a linked Spill Solution!"); + return (bloodstream.Comp.SpillSolutionEnt.Value, Comp(bloodstream.Comp.SpillSolutionEnt.Value)); + } + public Entity GetDissolvedSolution( + Entity bloodstream) + { + if (bloodstream.Comp.BloodRegentsSolutionEnt == null) + throw new Exception($"{ToPrettyString(bloodstream)} is missing a linked DissolvedReagent Solution!"); + return (bloodstream.Comp.BloodRegentsSolutionEnt.Value, Comp(bloodstream.Comp.BloodRegentsSolutionEnt.Value)); + } +} diff --git a/Content.Shared/Medical/Blood/Systems/VascularSystem.BloodTypes.cs b/Content.Shared/Medical/Blood/Systems/VascularSystem.BloodTypes.cs new file mode 100644 index 00000000000000..67b597483b4e99 --- /dev/null +++ b/Content.Shared/Medical/Blood/Systems/VascularSystem.BloodTypes.cs @@ -0,0 +1,184 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Blood.Systems; + +public sealed partial class VascularSystem +{ + public void ChangeBloodType( + Entity bloodCirc, + BloodTypePrototype newBloodType, + BloodAntigenPolicy bloodAntigenPolicy = BloodAntigenPolicy.Overwrite) + { + if (bloodCirc.Comp2.BloodType! == newBloodType.ID + || !Resolve(bloodCirc, ref bloodCirc.Comp3) + || !_solutionSystem.TryGetSolution((bloodCirc.Owner, bloodCirc.Comp3), + BloodstreamComponent.BloodSolutionId, out var bloodSolution, true)) + return; + var solution = bloodSolution.Value.Comp.Solution; + var oldReagent = solution.GetReagent(bloodCirc.Comp1.BloodReagentId); + + solution.RemoveReagent(oldReagent); + oldReagent = new ReagentQuantity(newBloodType.WholeBloodReagent, oldReagent.Quantity, new BloodTypeDiscriminator(newBloodType)); + solution.AddReagent(oldReagent); + bloodCirc.Comp2.BloodType = newBloodType.ID; + + UpdateAllowedAntigens((bloodCirc, bloodCirc), GetAntigensForBloodType(newBloodType), bloodAntigenPolicy); + _solutionSystem.UpdateChemicals(bloodSolution.Value); + } + + #region SolutionCreation + + public Solution CreateBloodSolution(BloodTypePrototype bloodType, FixedPoint2 volume) + { + return new Solution(bloodType.WholeBloodReagent, volume, new BloodTypeDiscriminator(bloodType)); + } + public Solution CreateBloodCellSolution(BloodTypePrototype bloodType, FixedPoint2 volume) + { + return new Solution(bloodType.BloodCellsReagent, volume, new BloodTypeDiscriminator(bloodType)); + } + + public Solution CreatePlasmaSolution(BloodTypePrototype bloodType, FixedPoint2 volume) + { + return new Solution(bloodType.BloodPlasmaReagent, volume, new BloodTypeDiscriminator(bloodType)); + } + + #endregion + + #region AntigenLogic + + public IEnumerable> GetBloodCellAntigensForBloodType(BloodTypePrototype bloodType) + { + foreach (var antigen in bloodType.BloodCellAntigens) + { + yield return antigen.Id; + } + } + + public IEnumerable> GetPlasmaAntigensForBloodType(BloodTypePrototype bloodType) + { + foreach (var antigen in bloodType.PlasmaAntigens) + { + yield return antigen.Id; + } + } + + public IEnumerable> GetAntigensForBloodType(BloodTypePrototype bloodType) + { + foreach (var value in GetBloodCellAntigensForBloodType(bloodType)) + { + yield return value; + } + foreach (var value in GetPlasmaAntigensForBloodType(bloodType)) + { + yield return value; + } + } + + public void UpdateAllowedAntigens(Entity bloodCirc, + IEnumerable> antigens, + BloodAntigenPolicy antigenPolicy = BloodAntigenPolicy.Overwrite) + { + switch (antigenPolicy) + { + case BloodAntigenPolicy.KeepOriginal: + { + return; + } + case BloodAntigenPolicy.Overwrite: + { + bloodCirc.Comp.AllowedAntigens.Clear(); + break; + } + } + foreach (var antigen in antigens) + { + bloodCirc.Comp.AllowedAntigens.Add(antigen); + } + Dirty(bloodCirc); + } + + + #endregion + + #region Setup + + private BloodTypePrototype GetInitialBloodType(Entity bloodCirc, BloodDefinitionPrototype bloodDef) + { + return bloodCirc.Comp.BloodType == null + ? SelectRandomizedBloodType(bloodDef) + : _protoManager.Index(bloodCirc.Comp.BloodType); + } + + public BloodTypePrototype SelectRandomizedBloodType(ProtoId bloodDefProto) + { + return SelectRandomizedBloodType(_protoManager.Index(bloodDefProto)); + } + + public BloodTypePrototype SelectRandomizedBloodType(BloodDefinitionPrototype bloodDefProto) + { + var total = 0f; + List> items = new(); + foreach (var (bloodTypeId, chance) in bloodDefProto.BloodTypeDistribution) + { + total += chance.Float(); + items.Add(bloodTypeId); + } + var perItemIncrease = total / items.Count; + var random = _random.NextFloat(0, total); + var foundProtoId = items[(int) MathF.Floor(random / perItemIncrease)]; + return _protoManager.Index(foundProtoId); + } + + #endregion + + #region SolutionCreationOverloads + + public Solution CreateBloodSolution(Entity bloodCirc, + FixedPoint2 volume) + { + //We ignore the nullable here because the variable always gets filled during MapInit and you shouldn't be calling + //this before mapInit runs + return CreateBloodSolution(_protoManager.Index(bloodCirc.Comp.BloodType!), volume); + } + + public Solution CreateBloodSolution(ProtoId bloodType, FixedPoint2 volume) + { + return CreateBloodSolution(_protoManager.Index(bloodType), volume); + } + + + + public Solution CreatePlasmaSolution(Entity bloodCirc, + FixedPoint2 volume) + { + //We ignore the nullable here because the variable always gets filled during MapInit and you shouldn't be calling + //this before mapInit runs + return CreatePlasmaSolution(_protoManager.Index(bloodCirc.Comp.BloodType!), volume); + } + + public Solution CreatePlasmaSolution(ProtoId bloodType, FixedPoint2 volume) + { + return CreatePlasmaSolution(_protoManager.Index(bloodType), volume); + } + + public Solution CreateBloodCellSolution(Entity bloodCirc, + FixedPoint2 volume) + { + //We ignore the nullable here because the variable always gets filled during MapInit and you shouldn't be calling + //this before mapInit runs + return CreateBloodCellSolution(_protoManager.Index(bloodCirc.Comp.BloodType!), volume); + } + + public Solution CreateBloodCellSolution(ProtoId bloodType, FixedPoint2 volume) + { + return CreateBloodCellSolution(_protoManager.Index(bloodType), volume); + } + + #endregion +} diff --git a/Content.Shared/Medical/Blood/Systems/VascularSystem.Simulation.cs b/Content.Shared/Medical/Blood/Systems/VascularSystem.Simulation.cs new file mode 100644 index 00000000000000..c5b0800495db89 --- /dev/null +++ b/Content.Shared/Medical/Blood/Systems/VascularSystem.Simulation.cs @@ -0,0 +1,96 @@ +using Content.Shared.Body.Organ; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Organs.Components; + +namespace Content.Shared.Medical.Blood.Systems; + +public sealed partial class VascularSystem +{ + public void SetHealthyBloodPressure(Entity vascularEntity, BloodPressure healthyPressure) + { + + if (healthyPressure.High < 0 || healthyPressure.Low < 0) + { + Log.Error("Neither blood pressure value can be negative!"); + return; + } + + if (healthyPressure.High < healthyPressure.Low) + { + Log.Warning("HealthyHighPressure pressure must be equal to or above HealthyLowPressure! Clamping value!"); + healthyPressure.High = healthyPressure.Low; + } + + vascularEntity.Comp.HealthyBloodPressure = healthyPressure; + vascularEntity.Comp.HighPressureVascularConstant = + CalculateVascularConstant(healthyPressure.High, vascularEntity.Comp.OptimalCardiacOutput); + vascularEntity.Comp.LowPressureVascularConstant = + CalculateVascularConstant(healthyPressure.Low, vascularEntity.Comp.OptimalCardiacOutput); + Dirty(vascularEntity); + } + + private void VascularSystemUpdate(Entity vascularEntity) + { + FixedPoint2 cardiacOutput = 0; + + foreach (var entity in vascularEntity.Comp1.CirculationEntities) + { + FixedPoint2 efficiency = 1; + if (!TryComp(entity, out var heartComp)) + continue; + if (TryComp(entity, out var organComp)) + efficiency = organComp.Efficiency; + cardiacOutput += _cardioSystem.GetCurrentCardiacOutput((entity, heartComp), + GetVolumeRatio((vascularEntity.Owner, vascularEntity.Comp2)), + efficiency); + } + + var high = CalculateBloodPressure(cardiacOutput, vascularEntity.Comp1.VascularResistance, + vascularEntity.Comp1.HighPressureVascularConstant); + var low = CalculateBloodPressure(cardiacOutput, vascularEntity.Comp1.VascularResistance, + vascularEntity.Comp1.LowPressureVascularConstant); + vascularEntity.Comp1.CurrentBloodPressure = (high, low); + vascularEntity.Comp1.Pulse = GetHighestPulse(vascularEntity.Comp1); + Dirty(vascularEntity, vascularEntity.Comp1); + } + + #region UtilityMethods + + public float GetVolumeRatio(Entity bloodstream) + { + return Math.Clamp(bloodstream.Comp.MaxVolume.Float() / bloodstream.Comp.Volume.Float(), 0f, 1f); + } + + private float CalculateVascularConstant( + FixedPoint2 targetPressure, + FixedPoint2 cardiacOutput) + { + return (targetPressure / cardiacOutput).Float(); + } + + private FixedPoint2 CalculateBloodPressure( + FixedPoint2 cardiacOutput, + FixedPoint2 vascularResistance, + FixedPoint2 vascularConstant + ) + { + return cardiacOutput * (vascularResistance * vascularConstant); + } + + private FixedPoint2? GetHighestPulse(VascularSystemComponent vascularSystemComp) + { + FixedPoint2? pulse = null; + foreach (var circEnt in vascularSystemComp.CirculationEntities) + { + if (!TryComp(circEnt, out var heart)) + continue; + if (pulse == null || pulse < heart.CurrentRate) + pulse = heart.CurrentRate; + } + return pulse; + } + +#endregion + +} diff --git a/Content.Shared/Medical/Blood/Systems/VascularSystem.cs b/Content.Shared/Medical/Blood/Systems/VascularSystem.cs new file mode 100644 index 00000000000000..28f00c467d187e --- /dev/null +++ b/Content.Shared/Medical/Blood/Systems/VascularSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Components; +using Content.Shared.Medical.Blood.Events; +using Content.Shared.Medical.Blood.Prototypes; +using Content.Shared.Medical.Organs.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Blood.Systems; + +public sealed partial class VascularSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private readonly CardioSystem _cardioSystem = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnVascularMapInit); + SubscribeLocalEvent(OnBloodstreamUpdate); + } + + private void OnVascularMapInit(EntityUid uid, VascularSystemComponent vascularSystem, ref MapInitEvent args) + { + vascularSystem.CurrentBloodPressure ??= vascularSystem.HealthyBloodPressure; + Dirty(uid, vascularSystem); + } + + private void OnBloodstreamUpdate(EntityUid uid, VascularSystemComponent vascularSystem, ref BloodstreamUpdatedEvent args) + { + VascularSystemUpdate((uid, vascularSystem, args.Bloodstream)); + } + + public void SetupCirculation(EntityUid uid, BloodstreamComponent bloodstreamComp, VascularSystemComponent vascularSystemComp, + FixedPoint2 initialVolume, Entity bloodSolution) + { + + var bloodDef = _protoManager.Index(vascularSystemComp.BloodDefinition); + var bloodType = GetInitialBloodType((uid, vascularSystemComp), bloodDef); + + vascularSystemComp.BloodType = bloodType.ID; + bloodstreamComp.Volume = initialVolume; + bloodstreamComp.BloodReagentId = new ReagentId(bloodstreamComp.BloodReagent!, new BloodTypeDiscriminator(bloodType)); + + _solutionSystem.AddSolution((bloodSolution, bloodSolution), + new Solution(new []{new ReagentQuantity(bloodstreamComp.BloodReagentId, initialVolume)})); + UpdateAllowedAntigens((uid, vascularSystemComp), GetAntigensForBloodType(bloodType)); + vascularSystemComp.Pulse = GetHighestPulse(vascularSystemComp); + + Dirty(uid, vascularSystemComp); + } +} + +[DataRecord, Serializable, NetSerializable] +public record struct BloodPressure(FixedPoint2 High, FixedPoint2 Low) +{ + public static implicit operator (FixedPoint2, FixedPoint2)(BloodPressure p) => (p.High, p.Low); + public static implicit operator BloodPressure((FixedPoint2,FixedPoint2) p) => new(p.Item1, p.Item2); +} + diff --git a/Content.Shared/Medical/Common/SeverityHelper.cs b/Content.Shared/Medical/Common/SeverityHelper.cs new file mode 100644 index 00000000000000..ad1a7e8275098b --- /dev/null +++ b/Content.Shared/Medical/Common/SeverityHelper.cs @@ -0,0 +1,58 @@ +using Content.Shared.FixedPoint; + +namespace Content.Shared.Medical.Common; + +public static class SeverityHelper +{ + private static string[] _physicalStrings = new[] + { + "Critical Damage", + "Severe Damage", + "Bad Damage", + "Damaged", + "Minor Damage", + "Slight Damage", + "Pristine" + }; + + private static string[] _severityStrings = new[] + { + "Critical", + "Extreme", + "Major", + "Moderate", + "Minor", + "Trivial" + }; + + private static string[] _visibleConditionStrings = new[] + { + "Critical", + "Looking Bad", + "Not Great", + "Looks OK", + "Healthy", + "Pristine" + }; + + public static string GetSeverityString(FixedPoint2 severityPercentage) + { + severityPercentage = FixedPoint2.Clamp(severityPercentage, 0, 100)/100; + FixedPoint2 percentage = 1 / (float)(_severityStrings.Length-1); + return _severityStrings[(severityPercentage / percentage).Int()]; + } + + public static string GetPhysicalString(FixedPoint2 physicalPercentage) + { + physicalPercentage = FixedPoint2.Clamp(physicalPercentage, 0, 1); + FixedPoint2 percentage = 1 / (float)(_severityStrings.Length-1); + return _severityStrings[(physicalPercentage / percentage).Int()]; + } + + public static string GetVisibleConditionString(FixedPoint2 severityPercentage) + { + severityPercentage = FixedPoint2.Clamp(severityPercentage, 0, 1); + FixedPoint2 percentage = 1 / (float)(_severityStrings.Length-1); + return _severityStrings[(severityPercentage / percentage).Int()]; + } +} diff --git a/Content.Shared/Medical/Consciousness/Components/ConsciousnessComponent.cs b/Content.Shared/Medical/Consciousness/Components/ConsciousnessComponent.cs new file mode 100644 index 00000000000000..56313b493d866b --- /dev/null +++ b/Content.Shared/Medical/Consciousness/Components/ConsciousnessComponent.cs @@ -0,0 +1,71 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Consciousness.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Consciousness.Components; + + + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(ConsciousnessSystem))] +public sealed partial class ConsciousnessComponent : Component +{ + /// + /// Unconsciousness threshold, ie: when does this entity pass-out/enter-crit + /// + [DataField("threshold",required: true), AutoNetworkedField] + public FixedPoint2 RawThreshold = 30; + + /// + /// The current unmodified consciousness value, if this is below the threshold the entity is in crit and + /// when this value reaches 0 the entity is dead. + /// + [DataField("consciousness"), AutoNetworkedField] + public FixedPoint2 RawValue = MaxConsciousness; + + /// + /// The current multiplier for consciousness. + /// This value is used to multiple RawConsciousness before modifiers are applied. + /// + [DataField, AutoNetworkedField] + public FixedPoint2 Multiplier = 1.0; + + /// + /// The current modifier for consciousness. + /// This value is added after raw consciousness is multiplied by the multiplier. + /// + [DataField, AutoNetworkedField] + public FixedPoint2 Modifier = 0; + + /// + /// The current maximum consciousness value, consciousness is clamped with this value. + /// + [DataField("Cap"), AutoNetworkedField] + public FixedPoint2 RawCap = MaxConsciousness; + + /// + /// Is consciousness being prevented from changing mobstate + /// + [DataField,AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public bool OverridenByMobstate = false; + + /// + /// Is this entity currently conscious + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public bool IsConscious = true; + + /// + /// How many consciousness providers do we expect to be fully functioning, + /// each removed provider will decrease consciousness by 1/ExpectedProviderCount * 100 + /// + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public int ExpectedProviderCount = 1; + + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public List LinkedProviders = new(); + + public FixedPoint2 Consciousness => FixedPoint2.Clamp(RawValue * Multiplier + Modifier, 0, Cap); + public FixedPoint2 Cap => FixedPoint2.Clamp(RawCap, 0, MaxConsciousness); + + public const float MaxConsciousness = 100f; +} diff --git a/Content.Shared/Medical/Consciousness/Components/ConsciousnessProviderComponent.cs b/Content.Shared/Medical/Consciousness/Components/ConsciousnessProviderComponent.cs new file mode 100644 index 00000000000000..571a78aa437f52 --- /dev/null +++ b/Content.Shared/Medical/Consciousness/Components/ConsciousnessProviderComponent.cs @@ -0,0 +1,11 @@ +using Content.Shared.Medical.Consciousness.Systems; + +namespace Content.Shared.Medical.Consciousness.Components; + + +[RegisterComponent, Access(typeof(ConsciousnessSystem)), AutoGenerateComponentState] +public sealed partial class ConsciousnessProviderComponent : Component +{ + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public EntityUid? LinkedConsciousness; +} diff --git a/Content.Shared/Medical/Consciousness/Events/ConsciousnessEvents.cs b/Content.Shared/Medical/Consciousness/Events/ConsciousnessEvents.cs new file mode 100644 index 00000000000000..1a36bf3435f119 --- /dev/null +++ b/Content.Shared/Medical/Consciousness/Events/ConsciousnessEvents.cs @@ -0,0 +1,29 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Consciousness.Components; + +namespace Content.Shared.Medical.Consciousness.Events; + + +[ByRefEvent] +public record struct ChangeConsciousnessAttemptEvent(Entity TargetConsciousness, FixedPoint2 PossibleDelta, bool Canceled = false); + +[ByRefEvent] +public record struct ConsciousnessChangedEvent(Entity TargetConsciousness, FixedPoint2 ConsciousnessDelta); + +[ByRefEvent] +public record struct EntityPassOutAttemptEvent(Entity TargetConsciousness, bool Canceled = false); + +[ByRefEvent] +public record struct EntityWakeUpAttemptEvent(Entity TargetConsciousness, bool Canceled = false); + +[ByRefEvent] +public record struct EntityPassOutEvent(Entity TargetConsciousness); + +[ByRefEvent] +public record struct EntityWakeUpEvent(Entity TargetConsciousness); + +[ByRefEvent] +public record struct EntityConsciousnessKillAttemptEvent(Entity TargetConsciousness, bool Canceled = false); + +[ByRefEvent] +public record struct EntityConsciousnessKillEvent(Entity TargetConsciousness); diff --git a/Content.Shared/Medical/Consciousness/Systems/ConsciousnessSystem.cs b/Content.Shared/Medical/Consciousness/Systems/ConsciousnessSystem.cs new file mode 100644 index 00000000000000..7b06d25b027130 --- /dev/null +++ b/Content.Shared/Medical/Consciousness/Systems/ConsciousnessSystem.cs @@ -0,0 +1,285 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Events; +using Content.Shared.Body.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Consciousness.Components; +using Content.Shared.Medical.Consciousness.Events; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Shared.Network; + +namespace Content.Shared.Medical.Consciousness.Systems; + +public sealed class ConsciousnessSystem : EntitySystem +{ + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly INetManager _netManager = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnMobstateChanged); + SubscribeLocalEvent(ConsciousnessInit, after: [typeof(SharedBodySystem)]); + //Prevent providers from being added twice (once on client/server). Eventually replace with proper overrides + if (!_netManager.IsClient) + { // TODO: this might be causing the spawn dead problem + SubscribeLocalEvent(OnProviderRemoved); + SubscribeLocalEvent(OnProviderAdded); + } + } + + private void OnProviderAdded(EntityUid providerUid, ConsciousnessProviderComponent provider, ref OrganAddedToBodyEvent args) + { + if (!TryComp(args.Body, out var consciousness)) + return; + AddConsciousnessProvider( + new Entity(args.Body, consciousness), + new Entity(providerUid, provider), args.Body.Comp.BodyInitialized); + } + + private void OnProviderRemoved(EntityUid providerUid, ConsciousnessProviderComponent provider, ref OrganRemovedFromBodyEvent args) + { + if (!TryComp(args.OldBody, out var consciousness) + || consciousness.LinkedProviders.Count != 0 + || !consciousness.LinkedProviders.Remove(providerUid)) + return; + RemoveConsciousnessProvider( + new Entity(args.OldBody, consciousness), + new Entity(providerUid, provider), args.OldBody.Comp.BodyInitialized); + } + + private void ConsciousnessInit(EntityUid uid, ConsciousnessComponent consciousness, MapInitEvent args) + { + UpdateConsciousnessState( + new Entity(uid, consciousness, null)); + } + + private void OnMobstateChanged(EntityUid uid, ConsciousnessComponent consciousness, ref UpdateMobStateEvent args) + { + //Do nothing if mobstate handling is set to be overriden or if we are conscious + if (consciousness.OverridenByMobstate || consciousness.Consciousness > 0) + return; + args.State = MobState.Dead; + var ev = new EntityConsciousnessKillEvent(new Entity(uid, consciousness)); + RaiseConsciousnessEvent(uid, ref ev); + } + + public void ChangeConsciousness(Entity conscious, FixedPoint2 consciousnessDelta, + bool updateState = true) + { + if (consciousnessDelta == 0 || !Resolve(conscious, ref conscious.Comp1, ref conscious.Comp2)) + return; + var delta = FixedPoint2.Clamp( + conscious.Comp1.RawValue+consciousnessDelta * conscious.Comp1.Multiplier + conscious.Comp1.Modifier, + 0, + conscious.Comp1.Cap) - conscious.Comp1.Consciousness; + var attemptEv = new ChangeConsciousnessAttemptEvent(new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref attemptEv); + if (attemptEv.Canceled) + return; + + conscious.Comp1.RawValue += consciousnessDelta; + Dirty(conscious.Owner, conscious.Comp1); + if (delta == 0) + return; + var ev = new ConsciousnessChangedEvent(new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref ev); + if (updateState) + UpdateConsciousnessState(conscious, conscious.Comp1, conscious.Comp2); + Dirty(conscious, conscious.Comp1); + } + + public void ChangeConsciousnessModifier(Entity conscious, + FixedPoint2 modifierDelta, bool updateState = true) + { + if (modifierDelta == 0 || !Resolve(conscious, ref conscious.Comp1, ref conscious.Comp2)) + return; + var delta = FixedPoint2.Clamp( + conscious.Comp1.RawValue * conscious.Comp1.Multiplier + conscious.Comp1.Modifier+modifierDelta, + 0, + conscious.Comp1.Cap) - conscious.Comp1.Consciousness; + var attemptEv = new ChangeConsciousnessAttemptEvent(new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref attemptEv); + if (attemptEv.Canceled) + return; + + conscious.Comp1.Modifier += modifierDelta; + Dirty(conscious.Owner, conscious.Comp1); + if (delta == 0) + return; + var ev = new ConsciousnessChangedEvent(new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref ev); + + if (updateState) + UpdateConsciousnessState(conscious, conscious.Comp1, conscious.Comp2); + Dirty(conscious, conscious.Comp1); + } + + public void ChangeConsciousnessMultiplier(Entity conscious, + FixedPoint2 multiplierDelta, bool updateState = true) + { + if (multiplierDelta == 0 || !Resolve(conscious, ref conscious.Comp1, ref conscious.Comp2)) + return; + var delta = FixedPoint2.Clamp( + conscious.Comp1.RawValue * (conscious.Comp1.Multiplier+multiplierDelta) + conscious.Comp1.Modifier, + 0, + conscious.Comp1.Cap) - conscious.Comp1.Consciousness; + var attemptEv = new ChangeConsciousnessAttemptEvent( + new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref attemptEv); + if (attemptEv.Canceled) + return; + + conscious.Comp1.Multiplier += multiplierDelta; + Dirty(conscious.Owner, conscious.Comp1); + if (delta == 0) + return; + var ev = new ConsciousnessChangedEvent( + new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref ev); + + if (updateState) + UpdateConsciousnessState(conscious, conscious.Comp1, conscious.Comp2); + Dirty(conscious, conscious.Comp1); + } + + public void ChangeConsciousnessCap(Entity conscious, FixedPoint2 capDelta, + bool updateState = true) + { + if (capDelta == 0 || !Resolve(conscious, ref conscious.Comp1, ref conscious.Comp2)) + return; + var newCap = FixedPoint2.Clamp( + capDelta + conscious.Comp1.Modifier, 0, ConsciousnessComponent.MaxConsciousness); + + var delta = FixedPoint2.Clamp( + conscious.Comp1.RawValue * (conscious.Comp1.Multiplier) + conscious.Comp1.Modifier, + 0, + conscious.Comp1.Cap+capDelta) - conscious.Comp1.Consciousness; + var attemptEv = new ChangeConsciousnessAttemptEvent( + new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref attemptEv); + if (attemptEv.Canceled) + return; + + conscious.Comp1.RawCap = newCap; + Dirty(conscious.Owner, conscious.Comp1); + if (delta == 0) + return; + var ev = new ConsciousnessChangedEvent( + new Entity(conscious, conscious.Comp1), delta); + RaiseConsciousnessEvent(conscious, ref ev); + + if (updateState) + UpdateConsciousnessState(conscious, conscious.Comp1, conscious.Comp2); + Dirty(conscious, conscious.Comp1); + } + + public void SetMobStateOverride(Entity conscious, bool mobStateOverrideEnabled) + { + if (!Resolve(conscious, ref conscious.Comp1, ref conscious.Comp2)) + return; + SetMobStateOverride(conscious, conscious.Comp1, conscious.Comp2, mobStateOverrideEnabled); + } + + private void SetMobStateOverride(EntityUid consciousEntity, ConsciousnessComponent consciousness, MobStateComponent mobstate ,bool overrideEnabled) + { + consciousness.OverridenByMobstate = overrideEnabled; + UpdateConsciousnessState(consciousEntity, consciousness, mobstate); + Dirty(consciousEntity,consciousness); + } + + private void RaiseConsciousnessEvent(EntityUid target, ref T eventToRaise) where T : struct + { + RaiseLocalEvent(target, ref eventToRaise); + } + + public void UpdateConsciousnessState(Entity conscious) + { + if (!Resolve(conscious, ref conscious.Comp1, ref conscious.Comp2)) + return; + UpdateConsciousnessState(conscious, conscious.Comp1, conscious.Comp2); + } + + public void AddConsciousnessProvider(Entity consciousness, Entity provider, + bool initializing = false) + { + if (consciousness.Comp.LinkedProviders.Count < consciousness.Comp.ExpectedProviderCount) + { + consciousness.Comp.LinkedProviders.Add(provider); + provider.Comp.LinkedConsciousness = consciousness.Owner; + Dirty(consciousness, consciousness.Comp); + if (initializing) + return; //Do not change consciousness if we are still initializing + ChangeConsciousness( + new Entity(consciousness, consciousness, null), + 1/(float)consciousness.Comp.ExpectedProviderCount * ConsciousnessComponent.MaxConsciousness); + return; + } + Log.Error($"Tried to add consciousness provider to {ToPrettyString(consciousness)} " + + $"which already has maximum number of providers!"); + } + + public void RemoveConsciousnessProvider(Entity consciousness, Entity provider, + bool initializing = false) + { + if (consciousness.Comp.LinkedProviders.Count > 0 && consciousness.Comp.LinkedProviders.Remove(provider.Owner)) + { + provider.Comp.LinkedConsciousness = null; + Dirty(consciousness); + + //Do not change consciousness if we are still initializing, we shouldn't normally be removing parts during + //init but just in case we want to check this. + if (!initializing) + return; + ChangeConsciousness( + new Entity(consciousness, consciousness, null), + -1/(float)consciousness.Comp.ExpectedProviderCount * ConsciousnessComponent.MaxConsciousness); + return; + } + Log.Error($"Tried to remove consciousness provider from {ToPrettyString(consciousness)} " + + $"which was not found in it's provider list, or the list was empty!"); + } + private void UpdateConsciousnessState(EntityUid consciousEnt, ConsciousnessComponent consciousness, MobStateComponent mobState) + { + var consciousnessValue = consciousness.Consciousness; + SetConscious(consciousEnt, consciousness, consciousnessValue <= consciousness.RawThreshold); + if (consciousness.OverridenByMobstate) + return; //prevent any mobstate updates when override is enabled + + var attemptEv = new EntityConsciousnessKillAttemptEvent(new Entity(consciousEnt, consciousness)); + RaiseConsciousnessEvent(consciousEnt, ref attemptEv); + if (!attemptEv.Canceled && consciousnessValue <= 0 && mobState.CurrentState != MobState.Dead) + { + _mobStateSystem.UpdateMobState(consciousEnt, mobState); + } + } + + private void SetConscious(EntityUid consciousEnt,ConsciousnessComponent consciousness, bool isConscious) + { + if (consciousness.IsConscious == isConscious) + return; + var consciousPair = new Entity(consciousEnt, consciousness); + if (isConscious) + { + var attemptEv = new EntityWakeUpAttemptEvent(consciousPair); + RaiseConsciousnessEvent(consciousEnt,ref attemptEv); + if (attemptEv.Canceled) + return; + consciousness.IsConscious = true; + var ev = new EntityWakeUpEvent(consciousPair); + RaiseConsciousnessEvent(consciousEnt,ref ev); + Dirty(consciousPair); + } + else + { + var attemptEv = new EntityPassOutAttemptEvent(consciousPair); + RaiseConsciousnessEvent(consciousEnt,ref attemptEv); + if (attemptEv.Canceled) + return; + consciousness.IsConscious = false; + var ev = new EntityPassOutEvent(consciousPair); + RaiseConsciousnessEvent(consciousEnt,ref ev); + Dirty(consciousPair); + } + } +} diff --git a/Content.Shared/Medical/Digestion/Components/DigestionComponent.cs b/Content.Shared/Medical/Digestion/Components/DigestionComponent.cs new file mode 100644 index 00000000000000..50d0c3c7326e96 --- /dev/null +++ b/Content.Shared/Medical/Digestion/Components/DigestionComponent.cs @@ -0,0 +1,63 @@ +using Content.Shared.Chemistry.Reaction.Prototypes; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Digestion.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Medical.Digestion.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class DigestionComponent : Component +{ + [DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField] + public TimeSpan UpdateRate = new(0,0,0,1); + + [DataField] + public TimeSpan LastUpdate; + + public const string ContainedEntitiesContainerId = "digestion-contained-entities"; + + public const string DigestingSolutionId = "digesting"; + + [DataField(required: true)] + public List> SupportedDigestionTypes = new(); + + [DataField(required: false)] + public List>? PassThroughDigestionTypes = null; + + [DataField, AutoNetworkedField] + public List ContainedEntities = new(); + + [DataField, AutoNetworkedField] + public FixedPoint2 MaxVolume = 1000; + + [DataField] + public bool UseBodySolution = true; + + [DataField] + public string AbsorberSolutionId = "bloodReagents"; + + + [DataField] + public ProtoId? DissolvingReagent = null; + + [DataField] + public float DissolverConcentration = 0; + + [DataField] + public ReagentId? CachedDissolverReagent = null; + + [DataField, AutoNetworkedField] + public float CachedDissolverConc = 0; + + [DataField, AutoNetworkedField] + public EntityUid CachedDigestionSolution; + + [DataField, AutoNetworkedField] + public EntityUid CachedAbsorberSolution; + + [DataField] + public List CachedDigestionReactions = new(); +} diff --git a/Content.Shared/Medical/Digestion/Components/EdibleComponent.cs b/Content.Shared/Medical/Digestion/Components/EdibleComponent.cs new file mode 100644 index 00000000000000..bd9430dcb04f09 --- /dev/null +++ b/Content.Shared/Medical/Digestion/Components/EdibleComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Digestion.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Digestion.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class EdibleComponent : Component +{ + [DataField(required:true)] + public ProtoId DigestionType; + + [DataField] + public bool EatWhole = false; + + /// + /// How many ReagentUnits per second should be transferred to the dissolving solution + /// + [DataField] + public float DigestionRate = 1; + + [DataField] + public string DigestionSolutionId = "food"; + + [DataField, AutoNetworkedField] + public EntityUid? CachedDigestionSolution = null; + +} diff --git a/Content.Shared/Medical/Digestion/Prototypes/DigestionTypePrototype.cs b/Content.Shared/Medical/Digestion/Prototypes/DigestionTypePrototype.cs new file mode 100644 index 00000000000000..8e86de1e93f412 --- /dev/null +++ b/Content.Shared/Medical/Digestion/Prototypes/DigestionTypePrototype.cs @@ -0,0 +1,15 @@ +using Content.Shared.Chemistry.Reaction.Prototypes; +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Digestion.Prototypes; + +[Prototype] +public sealed partial class DigestionTypePrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public List> DigestionReactions = new(); +} diff --git a/Content.Shared/Medical/Digestion/Systems/DigestionSystem.RateReactions.cs b/Content.Shared/Medical/Digestion/Systems/DigestionSystem.RateReactions.cs new file mode 100644 index 00000000000000..6b9397892eb6bd --- /dev/null +++ b/Content.Shared/Medical/Digestion/Systems/DigestionSystem.RateReactions.cs @@ -0,0 +1,40 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Medical.Digestion.Components; + +namespace Content.Shared.Medical.Digestion.Systems; + +public sealed partial class DigestionSystem +{ + + private void UpdateReactions(Entity digester, Entity solution) + { + RunDigestionReactions(digester, solution); + } + + private void UpdateCachedPrototypes(Entity digester) + { + foreach (var digestionTypeId in digester.Comp.SupportedDigestionTypes) + { + var digestionType = _protoManager.Index(digestionTypeId); + foreach (var digestionReactionId in digestionType.DigestionReactions) + { + var digestionReaction = _protoManager.Index(digestionReactionId); + digester.Comp.CachedDigestionReactions.Add(digestionReaction.Data); + } + digester.Comp.CachedDigestionReactions.Sort(); + } + } + + private void RunDigestionReactions(Entity digester, Entity solution) + { + if (solution.Comp.Solution.Volume == 0) + return; + foreach (var reaction in digester.Comp.CachedDigestionReactions) + { + var reactionRate = _rateReactionSystem.GetReactionRate(solution, reaction, digester.Comp.LastUpdate); + if (reactionRate == 0) + continue; + _rateReactionSystem.RunReaction(digester, solution, reaction, digester.Comp.LastUpdate); + } + } +} diff --git a/Content.Shared/Medical/Digestion/Systems/DigestionSystem.cs b/Content.Shared/Medical/Digestion/Systems/DigestionSystem.cs new file mode 100644 index 00000000000000..2cdcc4ad7624c1 --- /dev/null +++ b/Content.Shared/Medical/Digestion/Systems/DigestionSystem.cs @@ -0,0 +1,93 @@ +using Content.Shared.Body.Events; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reaction.Systems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Digestion.Components; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Medical.Digestion.Systems; + +public sealed partial class DigestionSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly ChemicalRateReactionSystem _rateReactionSystem = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnCompInit); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnBodyInit); + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var digester, out var solMan)) + { + if (_timing.CurTime < digester.LastUpdate + digester.UpdateRate) + continue; + UpdateReactions((uid, digester), + (digester.CachedDigestionSolution, + Comp(digester.CachedDigestionSolution))); + digester.LastUpdate = _timing.CurTime; + } + } + +private void OnBodyInit(Entity digester, ref BodyInitializedEvent args) + { + if (!digester.Comp.UseBodySolution) + return; + UpdateCachedSolutions(digester, args.Body); + } + + private void OnCompInit(Entity digester, ref ComponentInit args) + { + if (digester.Comp.DissolvingReagent != null) + { + digester.Comp.CachedDissolverReagent = new ReagentId(digester.Comp.DissolvingReagent.Value, null); + } + UpdateCachedPrototypes(digester); + } + + private void OnMapInit(Entity digester, ref MapInitEvent args) + { + if (_netManager.IsClient) + return; + if (digester.Comp.UseBodySolution) + return; + UpdateCachedSolutions(digester, null); + } + + + private void UpdateCachedSolutions(Entity digester, EntityUid? absorbSolutionOwner) + { + absorbSolutionOwner ??= digester; + if (!_solutionSystem.EnsureSolutionEntity((digester, null), + DigestionComponent.DigestingSolutionId, + out var digestionSolEnt, + FixedPoint2.MaxValue)) + { + Log.Error($"Could not ensure solution {DigestionComponent.DigestingSolutionId} on {ToPrettyString(digester)}." + + $"If this is being run on the client make sure it runs AFTER mapInit!"); + return; + } + digester.Comp.CachedDigestionSolution = digestionSolEnt.Value; + if (!_solutionSystem.TryGetSolution((absorbSolutionOwner.Value, null), + digester.Comp.AbsorberSolutionId, + out var absorbSolEnt, + true)) + return; + digester.Comp.CachedDigestionSolution = absorbSolEnt.Value; + Dirty(digester); + } + + +} diff --git a/Content.Shared/Medical/HealthConditions/Components/HealthConditionComponent.cs b/Content.Shared/Medical/HealthConditions/Components/HealthConditionComponent.cs new file mode 100644 index 00000000000000..907626c057ad25 --- /dev/null +++ b/Content.Shared/Medical/HealthConditions/Components/HealthConditionComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.HealthConditions.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HealthConditionComponent : Component +{ + [DataField, AutoNetworkedField] + public FixedPoint2 RawSeverity = 0; + + public FixedPoint2 SeverityAsMultiplier => RawSeverity / SeverityMax; + + [DataField, AutoNetworkedField] + public EntityUid ConditionManager = EntityUid.Invalid; + + public FixedPoint2 Severity => FixedPoint2.Clamp(RawSeverity, 0, SeverityMax); + + public const float SeverityMax = 100f; + +} diff --git a/Content.Shared/Medical/HealthConditions/Components/HealthConditionManagerComponent.cs b/Content.Shared/Medical/HealthConditions/Components/HealthConditionManagerComponent.cs new file mode 100644 index 00000000000000..1e889aaee1a181 --- /dev/null +++ b/Content.Shared/Medical/HealthConditions/Components/HealthConditionManagerComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.HealthConditions.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HealthConditionManagerComponent : Component +{ + public const string ContainerId = "Afflictions"; + + [DataField, AutoNetworkedField] + public Dictionary ContainedConditionEntities = new (); +} diff --git a/Content.Shared/Medical/HealthConditions/Event/HealthConditionEvents.cs b/Content.Shared/Medical/HealthConditions/Event/HealthConditionEvents.cs new file mode 100644 index 00000000000000..9b75b1e0e31d8c --- /dev/null +++ b/Content.Shared/Medical/HealthConditions/Event/HealthConditionEvents.cs @@ -0,0 +1,28 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.HealthConditions.Components; + +namespace Content.Shared.Medical.HealthConditions.Event; + +[ByRefEvent] +public record struct HealthConditionAddAttemptEvent(Entity PossibleCondition, bool Canceled = false); + +[ByRefEvent] +public record struct HealthConditionAddedEvent(Entity NewCondition); + +[ByRefEvent] +public record struct HealthConditionRemoveAttemptEvent(Entity Condition, bool Canceled = false); + +[ByRefEvent] +public record struct HealthConditionRemovedEvent(Entity Condition); + +[ByRefEvent] +public record struct HealthConditionSeverityChangeAttemptEvent(Entity TargetCondition, FixedPoint2 SeverityDelta, bool Canceled = false); + +[ByRefEvent] +public record struct HealthConditionSeverityChangedEvent(Entity TargetCondition, FixedPoint2 SeverityDelta); + +[ByRefEvent] +public record struct HealthConditionSeveritySetAttemptEvent(Entity TargetCondition, FixedPoint2 NewSeverity, bool Canceled = false); + +[ByRefEvent] +public record struct HealthConditionSeveritySetEvent(Entity TargetCondition, FixedPoint2 OldSeverity); diff --git a/Content.Shared/Medical/HealthConditions/Systems/HealthConditionSystem.cs b/Content.Shared/Medical/HealthConditions/Systems/HealthConditionSystem.cs new file mode 100644 index 00000000000000..6f8910b3dc71bb --- /dev/null +++ b/Content.Shared/Medical/HealthConditions/Systems/HealthConditionSystem.cs @@ -0,0 +1,295 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.HealthConditions.Components; +using Content.Shared.Medical.HealthConditions.Event; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.HealthConditions.Systems; + +public sealed class HealthConditionSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + + + public override void Initialize() + { + SubscribeLocalEvent(OnConditionManagerInit); + SubscribeLocalEvent(OnConditionAddedToMan); + SubscribeLocalEvent(OnConditionRemovedFromMan); + } + + /// + /// Try to add a condition to a condition manager or try to get one if it already exists + /// + /// Target conditionManager entity/component + /// Entity Prototype ID for the desired condition + /// Condition entity/component pair that was created/found + /// Severity value to set after creating the condition + /// Should we ignore event cancellations + /// Log a Warning if this condition is already present + /// True if condition was added or already exists, false if condition could not be added + public bool TryAddCondition( + Entity conditionManager, + EntProtoId conditionId, + [NotNullWhen(true)] out Entity? newCondition, + FixedPoint2? severityOverride = null, + bool force = false, + bool warnIfPresent = false) + { + newCondition = null; + if (!Resolve(conditionManager, ref conditionManager.Comp)) + return false; + + if (conditionManager.Comp.ContainedConditionEntities.TryGetValue(conditionId, out var existingCondition)) + { + if (warnIfPresent) + Log.Warning($"Condition of type {conditionId} already exists on {ToPrettyString(conditionManager)}"); + newCondition = new Entity(existingCondition, Comp(existingCondition)); + if (severityOverride != null) + SetConditionSeverity_Internal(newCondition.Value, severityOverride.Value); + return true; + } + + if (!TrySpawnInContainer(conditionId, conditionManager, HealthConditionManagerComponent.ContainerId, out var conditionEnt) + || !TryComp(conditionEnt, out var conditionComp)) + { + return false; + } + + newCondition = new Entity(conditionEnt.Value, conditionComp); + var attemptEv = new HealthConditionAddAttemptEvent(newCondition.Value); + RaiseLocalEvent(conditionManager, ref attemptEv); + + if (!force && attemptEv.Canceled) + { + Del(conditionEnt); + return false; + } + + if (severityOverride != null) + SetConditionSeverity_Internal(newCondition.Value, severityOverride.Value); + return true; + } + + + /// + /// Try to remove a condition from an entity + /// + /// Target conditionManager entity/component + /// Condition entity/component pair to remove + /// Should we ignore event cancellations + /// True if successfully remove, false if not + public bool TryRemoveCondition( + Entity conditionManager, + Entity condition, + bool force =false) + { + var conditionMeta = MetaData(condition); + if (!Resolve(conditionManager, ref conditionManager.Comp) + || !Resolve(condition, ref condition.Comp)) + return false; + + var validCondition = new Entity(condition, condition.Comp); + var attemptEv = new HealthConditionRemoveAttemptEvent(validCondition); + RaiseLocalEvent(conditionManager, ref attemptEv); + + return force + && !attemptEv.Canceled + && _containerSystem.RemoveEntity(conditionManager, condition) + && conditionMeta.EntityPrototype != null; + } + + + /// + /// Try to remove a condition from an entity + /// + /// Target conditionManager entity/component + /// Entity Prototype ID for the desired condition + /// Should we ignore event cancellations + /// True if successfully remove, false if not + public bool TryRemoveCondition( + Entity conditionManager, + EntProtoId conditionId, + bool force = false) + { + if (!Resolve(conditionManager, ref conditionManager.Comp) + || conditionManager.Comp.ContainedConditionEntities.TryGetValue(conditionId, out var conditionEnt)) + return false; + return TryRemoveCondition(conditionManager, new Entity(conditionEnt, null),force); + } + + + /// + /// Tries to get the condition entity associated with a conditionId if it is present + /// + /// Target conditionManager entity/component + /// Entity Prototype ID for the desired condition + /// The found Condition Entity/Comp pair + /// True if condition entity is found, false if not + public bool TryGetCondition(Entity conditionManager, EntProtoId conditionId, + [NotNullWhen(true)] out Entity? condition) + { + condition = null; + if (!Resolve(conditionManager, ref conditionManager.Comp) + || conditionManager.Comp.ContainedConditionEntities.TryGetValue(conditionId, out var conditionEnt)) + return false; + condition = new Entity(conditionEnt, Comp(conditionEnt)); + return true; + } + + /// + /// Attempt to add severity to a condition. (This may be negative to subtract from severity) + /// + /// Target Condition Entity/Comp + /// Severity to add to the condition + /// Should we ignore event cancellations + /// True if Severity was added, false if not + public bool TryAddConditionSeverity(Entity condition, FixedPoint2 severityToAdd, bool force = false) + { + if (!Resolve(condition, ref condition.Comp) + || severityToAdd == 0) + return false; + var validCondition = new Entity(condition, condition.Comp); + var attemptEv = new HealthConditionSeverityChangeAttemptEvent( + new Entity(condition, condition.Comp), severityToAdd); + RaiseConditionEvent(validCondition, ref attemptEv); + if (!force && attemptEv.Canceled) + return false; + SetConditionSeverity_Internal(validCondition, condition.Comp.RawSeverity+ severityToAdd); + return true; + } + + /// + /// Tries to set a condition's severity, try to use TryAddConditionSeverity instead because it doesn't override the + /// existing severity value and risk causing the severity value to desync in other systems + /// + /// Target Condition Entity/Comp + /// The new severity value + /// Should we ignore event cancellations + /// True if Severity was set to a new value, false if not + public bool TrySetConditionSeverity(Entity condition, FixedPoint2 newSeverity, bool force = false) + { + if (!Resolve(condition, ref condition.Comp) + || newSeverity == condition.Comp.RawSeverity) + return false; + var validCondition = new Entity(condition, condition.Comp); + var attempt2Ev = new HealthConditionSeveritySetAttemptEvent( + new Entity(condition, condition.Comp), newSeverity); + var attempt1Ev = new HealthConditionSeverityChangeAttemptEvent( + new Entity(condition, condition.Comp), newSeverity-condition.Comp.RawSeverity); + RaiseConditionEvent(validCondition, ref attempt1Ev); + RaiseConditionEvent(validCondition, ref attempt2Ev); + if (!force && (attempt1Ev.Canceled || attempt2Ev.Canceled)) + return false; + SetConditionSeverity_Internal(validCondition, condition.Comp.RawSeverity+ newSeverity); + return true; + } + + #region InternalUse/Helpers + + /// + /// Internal use only. Sets a condition's severity and raises the appropriate events. + /// + /// Target Condition Entity/Comp + /// Severity we are setting + private void SetConditionSeverity_Internal(Entity condition, FixedPoint2 newSeverity) + { + if (newSeverity == condition.Comp.RawSeverity) + return; + var oldSeverity = condition.Comp.RawSeverity; + condition.Comp.RawSeverity = newSeverity; + var clampedDelta = FixedPoint2.Clamp(newSeverity, 0, HealthConditionComponent.SeverityMax) + - FixedPoint2.Clamp( oldSeverity, 0, HealthConditionComponent.SeverityMax) ; + if (clampedDelta == 0) + return; //if the clamped delta is 0 don't do anything! + var ev = new HealthConditionSeverityChangedEvent(condition, clampedDelta); + RaiseConditionEvent(condition, ref ev); + var ev2 = new HealthConditionSeveritySetEvent(condition, oldSeverity); + RaiseConditionEvent(condition, ref ev2); + Dirty(condition); + } + + /// + /// Raises a health condition event on both the Condition entity and Managing entity + /// + /// Target Condition Entity/Comp + /// Event being raised + /// Event Type + private void RaiseConditionEvent(Entity condition, ref T conditionEvent) + where T : struct + { + RaiseLocalEvent(condition, ref conditionEvent); + RaiseLocalEvent(condition.Comp.ConditionManager, ref conditionEvent); + } + + #endregion + + #region EventHandlers + + private void OnConditionRemovedFromMan(EntityUid uid, HealthConditionManagerComponent man, ref EntRemovedFromContainerMessage args) + { + var conditionMeta = MetaData(args.Entity); + if (conditionMeta.EntityPrototype == null) + { + Log.Error($"Entity without a prototype removed from inside Condition container on {ToPrettyString(uid)} This should never happen!"); + Del(args.Entity); + return; + } + + if (!TryComp(args.Entity, out var condition)) + { + Log.Error($"Entity without an condition component removed from inside Condition container on {ToPrettyString(uid)} This should never happen!"); + Del(args.Entity); + return; + } + + if (!man.ContainedConditionEntities.Remove(conditionMeta.EntityPrototype.ID)) + { + Log.Error($"Condition of type {conditionMeta.EntityPrototype.ID} not found in {ToPrettyString(uid)}"); + return; + } + var ev = new HealthConditionRemovedEvent(new Entity(args.Entity, condition)); + RaiseLocalEvent(uid, ref ev); + Del(args.Entity); + Dirty(uid, man); + } + + private void OnConditionAddedToMan(EntityUid uid, HealthConditionManagerComponent man, ref EntInsertedIntoContainerMessage args) + { + var conditionMeta = MetaData(args.Entity); + if (conditionMeta.EntityPrototype == null) + { + Log.Warning($"Entity without a prototype inserted into Condition container on {ToPrettyString(uid)}!"); + return; + } + + if (!TryComp(args.Entity, out var condition)) + { + Log.Error($"Entity without an condition component added to Condition container on {ToPrettyString(uid)} This should never happen!"); + Del(args.Entity); + return; + } + + if (!man.ContainedConditionEntities.TryAdd(conditionMeta.EntityPrototype.ID, args.Entity)) + { + Log.Error($"Condition of type {conditionMeta.EntityPrototype.ID} already exists on {ToPrettyString(uid)}"); + Del(args.Entity); + return; + } + + condition.ConditionManager = uid; + var ev = new HealthConditionAddedEvent(new Entity(args.Entity, Comp(args.Entity))); + RaiseLocalEvent(uid, ref ev); + Dirty(uid, man); + } + + private void OnConditionManagerInit(EntityUid uid, HealthConditionManagerComponent component, ref ComponentInit args) + { + _containerSystem.EnsureContainer(uid, HealthConditionManagerComponent.ContainerId); + } + + + #endregion + +} diff --git a/Content.Shared/Medical/Metabolism/Components/MetabolismComponent.cs b/Content.Shared/Medical/Metabolism/Components/MetabolismComponent.cs new file mode 100644 index 00000000000000..b04fb1cf4a7852 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Components/MetabolismComponent.cs @@ -0,0 +1,70 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Damage; +using Content.Shared.Medical.Metabolism.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Medical.Metabolism.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MetabolismComponent : Component +{ + [DataField(required: true), AutoNetworkedField] + public ProtoId MetabolismType; + + [DataField(required: true)] + public float BaseMultiplier = 1; + + /// + /// Short-term stored energy in KiloCalories (KCal) + /// This simulates the body's glycogen storage, and serves as a buffer before long-term storage is used. + /// if set to -1, it will use the maxFastStorageValue + /// + [DataField("initialCalorieBuffer"), AutoNetworkedField] + public float CalorieBuffer = -1; + + /// + /// Maximum amount of KiloCalories that can be stored in fast storage. Once this is reached, all future KCal will be + /// stored in longterm storage + /// + [DataField("calorieBuffer", required: true), AutoNetworkedField] + public float CalorieBufferCap = 1500; + + /// + /// Longer term stored energy in KiloCalories (KCal) + /// This simulates the body's fat/adipose storage, and serves as a long term fall back if all glycogen is used up. + /// If this is completely used up, metabolism stops and organs start dying + /// + [DataField(required: true), AutoNetworkedField] + public float CalorieStorage = 80000; + + /// + /// Should this fetch the solution from the body or the owner of this component? + /// + [DataField, AutoNetworkedField] + public bool UsesBodySolution = true; + /// + /// What is the solution ID we are using? + /// + [DataField, AutoNetworkedField] + public string AbsorbSolutionId = "bloodReagents"; + + + [DataField] public float CachedReagentTarget; + [DataField] public EntityUid CachedSolutionEnt = EntityUid.Invalid; + [DataField] public ReagentId? CachedEnergyReagent = null; + [DataField] public float CachedKCalPerReagent; + [DataField] public DamageSpecifier CachedDeprivationDamage = new(); + + //TODO: implement sideeffects/medical conditions for low/high blood glucose and starvation. Also hook up hunger. + + /// + /// The next time that reagents will be metabolized. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate; + + [DataField, AutoNetworkedField] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1f); +} diff --git a/Content.Shared/Medical/Metabolism/Components/MetabolizerComponent.cs b/Content.Shared/Medical/Metabolism/Components/MetabolizerComponent.cs new file mode 100644 index 00000000000000..082d42b587f9d8 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Components/MetabolizerComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Damage; +using Content.Shared.Medical.Metabolism.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Medical.Metabolism.Components; + +/// +/// This component is used to implement metabolic reactions on entities (organs/bodyparts) +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MetabolizerComponent : Component +{ + [DataField(required: true), AutoNetworkedField] + public ProtoId MetabolismType; + [DataField(required: true)] + public float BaseMultiplier = 1; + + [DataField, AutoNetworkedField] + public DamageSpecifier CachedDeprivationDamage = new(); + + [DataField, AutoNetworkedField] public bool UsesBodySolution = true; + [DataField, AutoNetworkedField] public string AbsorbSolutionId = "bloodReagents"; + [DataField, AutoNetworkedField] public string WasteSolutionId = "bloodReagents"; + + [DataField, AutoNetworkedField] public EntityUid CachedAbsorbSolutionEnt = EntityUid.Invalid; + [DataField, AutoNetworkedField] public EntityUid CachedWasteSolutionEnt = EntityUid.Invalid; + [DataField, AutoNetworkedField] public List CachedAbsorbedReagents = new(); + [DataField, AutoNetworkedField] public List CachedWasteReagents = new(); + [DataField, AutoNetworkedField] public ReagentId? CachedEnergyReagent = null; + [DataField, AutoNetworkedField] public float CachedKCalPerReagent = 0; + /// + /// The next time that reagents will be metabolized. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate; + + [DataField, AutoNetworkedField] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1f); +} diff --git a/Content.Shared/Medical/Metabolism/Events/MetabolismEvents.cs b/Content.Shared/Medical/Metabolism/Events/MetabolismEvents.cs new file mode 100644 index 00000000000000..17a11787553229 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Events/MetabolismEvents.cs @@ -0,0 +1,2 @@ +namespace Content.Shared.Medical.Metabolism.Events; + diff --git a/Content.Shared/Medical/Metabolism/Prototypes/MetabolismTypePrototype.cs b/Content.Shared/Medical/Metabolism/Prototypes/MetabolismTypePrototype.cs new file mode 100644 index 00000000000000..9d956d7f0498b4 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Prototypes/MetabolismTypePrototype.cs @@ -0,0 +1,67 @@ +using Content.Shared.Atmos.Prototypes; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Metabolism.Prototypes; + +[Prototype] +public sealed partial class MetabolismTypePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + /// + /// Reagents that are required for a metabolic reaction to take place + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> RequiredReagents = new(); + + /// + /// Reagent byproducts created by metabolic reactions + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> WasteReagents = new(); + + /// + /// Gases that should be absorbed into the bloodstream + /// + [DataField(required: true)] + public Dictionary, GasMetabolismData> AbsorbedGases = new(); + + /// + /// Gases that should be scrubbed into the lung gasmixture and exhaled. + /// + [DataField(required: true)] + public Dictionary, GasMetabolismData> WasteGases = new(); + + /// + /// Reagent used to transport energy in the bloodstream. If this is null, energy will not be used in metabolism. + /// + [DataField] + public ProtoId? EnergyReagent = null; + + /// + /// What concentration should we try to keep the energy reagent at + /// + [DataField] + public float TargetEnergyReagentConcentration = 0; + + /// + /// How many KiloCalories are there in each reagent unit of the Energy Reagent + /// + [DataField] + public float KCalPerEnergyReagent = 0; + + /// + /// What type of damage should be applied when metabolism fails. + /// + [DataField(required: true)] + public DamageSpecifier DeprivationDamage = new(); +} + +[DataRecord, Serializable, NetSerializable] +public record struct GasMetabolismData(float LowThreshold = 0.95f, float HighThreshold = 1f); diff --git a/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.BodyMetabolism.cs b/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.BodyMetabolism.cs new file mode 100644 index 00000000000000..beba3040045526 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.BodyMetabolism.cs @@ -0,0 +1,200 @@ +using Content.Shared.Body.Events; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Metabolism.Components; + +namespace Content.Shared.Medical.Metabolism.Systems; + +public sealed partial class MetabolismSystem +{ + private void BodyMetabolismInit() + { + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnBodyInit); + SubscribeLocalEvent(OnCompInit); + } + + private void OnCompInit(EntityUid uid, MetabolismComponent metabolism, ComponentInit args) + { + var metabolismType = _protoManager.Index(metabolism.MetabolismType); + if (metabolismType.EnergyReagent != null) + metabolism.CachedEnergyReagent = new ReagentId(metabolismType.EnergyReagent.Value, null); + metabolism.CachedKCalPerReagent = metabolismType.KCalPerEnergyReagent; + metabolism.CachedDeprivationDamage = metabolismType.DeprivationDamage; + metabolism.CachedReagentTarget = metabolismType.TargetEnergyReagentConcentration; + if (metabolism.CalorieBuffer < 0) + metabolism.CalorieBuffer = metabolism.CalorieBufferCap; + } + + private void OnBodyInit(EntityUid uid, MetabolismComponent metabolism, BodyInitializedEvent args) + { + if (!metabolism.UsesBodySolution) + return; + UpdateCachedBodyMetabolismSolutions((uid, metabolism), args.Body); + InitializeBodyMetabolism((uid, metabolism)); + } + + private void OnMapInit(EntityUid uid, MetabolismComponent metabolism, MapInitEvent args) + { + if (_netManager.IsClient) + return; + + if (metabolism.UsesBodySolution) + return; + UpdateCachedBodyMetabolismSolutions((uid, metabolism), null); + InitializeBodyMetabolism((uid, metabolism)); + } + + private void InitializeBodyMetabolism(Entity metabolism) + { + Entity solEnt = (metabolism.Comp.CachedSolutionEnt,Comp(metabolism.Comp.CachedSolutionEnt)); + if (metabolism.Comp.CachedEnergyReagent == null) + return; + solEnt.Comp.Solution.AddReagent(metabolism.Comp.CachedEnergyReagent.Value, + metabolism.Comp.CachedReagentTarget * BloodstreamSystem.BloodstreamVolumeTEMP); + } + + + private void UpdateBodyMetabolism(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var metabolism)) + { + if (_gameTiming.CurTime < metabolism.NextUpdate) + continue; + + metabolism.NextUpdate += metabolism.UpdateInterval; + UpdateEnergy((uid, metabolism)); + } + } + + private void UpdateEnergy(Entity metabolism) + { + if (!TryComp(metabolism.Comp.CachedSolutionEnt, out var comp)) + return; + Entity solEnt = (metabolism.Comp.CachedSolutionEnt, comp); + //Entity solEnt = (metabolism.Comp.CachedSolutionEnt,Comp(metabolism.Comp.CachedSolutionEnt)); + if (metabolism.Comp.CachedEnergyReagent == null + || !TryGetBloodEnergyConc(metabolism, solEnt, BloodstreamSystem.BloodstreamVolumeTEMP,out var concentration)) + return; + var reagentDelta = (concentration - metabolism.Comp.CachedReagentTarget)* BloodstreamSystem.BloodstreamVolumeTEMP; + if (reagentDelta == 0) + return; + //invert the delta because we're trying to make up for it + if (ChangeStoredEnergy(metabolism, solEnt, reagentDelta)) + return; + Log.Debug($"{ToPrettyString(metabolism)} is starving!"); + //TODO: add starving effects herews + } + + private bool ChangeStoredEnergy(Entity metabolism, + Entity solEnt, + FixedPoint2 reagentDelta) + { + if (metabolism.Comp.CachedEnergyReagent == null) + return false; + if (reagentDelta == 0) + return true; + var calorieDelta = reagentDelta.Float() * metabolism.Comp.CachedKCalPerReagent; + if (calorieDelta > 0) + { + if (metabolism.Comp.CalorieBuffer == 0) + { + Log.Debug($"{ToPrettyString(metabolism)} has started filling it's calorie buffer"); + } + if (metabolism.Comp.CalorieBuffer < metabolism.Comp.CalorieBufferCap) + { + metabolism.Comp.CalorieBuffer += calorieDelta; + var overflow = metabolism.Comp.CalorieBufferCap - metabolism.Comp.CalorieBuffer; + if (overflow < 0) + { + metabolism.Comp.CalorieBuffer = metabolism.Comp.CalorieBufferCap; + Log.Debug($"{ToPrettyString(metabolism)} has overflowed it's calorie buffer and is now adding to storage."); + metabolism.Comp.CalorieStorage -= overflow; + Dirty(metabolism); + solEnt.Comp.Solution.RemoveReagent(metabolism.Comp.CachedEnergyReagent.Value, reagentDelta); + _solutionContainerSystem.UpdateChemicals(solEnt, true, false); + return true; + } + Dirty(metabolism); + solEnt.Comp.Solution.RemoveReagent(metabolism.Comp.CachedEnergyReagent.Value, reagentDelta); + _solutionContainerSystem.UpdateChemicals(solEnt, true, false); + return true; + } + metabolism.Comp.CalorieStorage += calorieDelta; + Dirty(metabolism); + solEnt.Comp.Solution.RemoveReagent(metabolism.Comp.CachedEnergyReagent.Value, reagentDelta); + _solutionContainerSystem.UpdateChemicals(solEnt, true, false); + return true; + } + + var underflow = 0f; + //Negative calorie delta so operations should be inverted!! + + if (metabolism.Comp.CalorieStorage == 0 || metabolism.Comp.CalorieBuffer == 0) + return false; + if (metabolism.Comp.CalorieBuffer < 0) + metabolism.Comp.CalorieBuffer = 0; + + underflow = metabolism.Comp.CalorieBuffer += calorieDelta; + if (underflow >= 0) + { + Dirty(metabolism); + solEnt.Comp.Solution.AddReagent(metabolism.Comp.CachedEnergyReagent.Value, -reagentDelta); + _solutionContainerSystem.UpdateChemicals(solEnt, true, false); + return true; + } + + if (metabolism.Comp.CalorieBuffer == 0) + { + Log.Debug($"{ToPrettyString(metabolism)} calorie buffer is empty!"); + } + + + metabolism.Comp.CalorieStorage += underflow; + if (metabolism.Comp.CalorieStorage < 0) + { + metabolism.Comp.CalorieStorage = 0; + Dirty(metabolism); + solEnt.Comp.Solution.AddReagent(metabolism.Comp.CachedEnergyReagent.Value, -reagentDelta); + _solutionContainerSystem.UpdateChemicals(solEnt, true, false); + return false; + } + Dirty(metabolism); + solEnt.Comp.Solution.AddReagent(metabolism.Comp.CachedEnergyReagent.Value, -reagentDelta); + _solutionContainerSystem.UpdateChemicals(solEnt, true, false); + return true; + } + + private bool TryGetBloodEnergyConc(Entity metabolism, + Entity solution, + FixedPoint2 volume, + out float concentration) + { + concentration = 0; + if (metabolism.Comp.CachedEnergyReagent == null) + return false; + concentration = _solutionContainerSystem.GetReagentConcentration(solution, + volume, + metabolism.Comp.CachedEnergyReagent.Value); + return true; + } + + + private void UpdateCachedBodyMetabolismSolutions(Entity metabolism, + EntityUid? target) + { + target ??= metabolism.Owner; + + var dirty = false; + if (_solutionContainerSystem.TryGetSolution((target.Value, null), metabolism.Comp.AbsorbSolutionId, out var absorbSol, true)) + { + metabolism.Comp.CachedSolutionEnt = absorbSol.Value.Owner; + dirty = true; + } + if (dirty) + Dirty(metabolism); + } +} diff --git a/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.Metabolizers.cs b/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.Metabolizers.cs new file mode 100644 index 00000000000000..b0cdd51dfa50c9 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.Metabolizers.cs @@ -0,0 +1,136 @@ +using Content.Shared.Body.Events; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Medical.Metabolism.Components; + +namespace Content.Shared.Medical.Metabolism.Systems; + +public sealed partial class MetabolismSystem +{ + private void MetabolizerInit() + { + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnBodyInit); + SubscribeLocalEvent(OnCompInit); + } + + //TODO: differ starting metabolizer ticking until after body initializes fully + private void MetabolizerUpdate(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var metabolism)) + { + if (_gameTiming.CurTime < metabolism.NextUpdate) + continue; + + metabolism.NextUpdate += metabolism.UpdateInterval; + if (!DoMetabolicReaction((uid,metabolism))) + { + _damageSystem.TryChangeDamage(uid, + metabolism.CachedDeprivationDamage, + true, + false); + //TODO: block entity healing + } + } + } + + private void OnCompInit(EntityUid uid, MetabolizerComponent metabolizer, ComponentInit args) + { + var metabolismType = _protoManager.Index(metabolizer.MetabolismType); + + metabolizer.CachedDeprivationDamage = metabolismType.DeprivationDamage; + metabolizer.CachedKCalPerReagent = metabolismType.KCalPerEnergyReagent; + if (metabolismType.EnergyReagent != null) + metabolizer.CachedEnergyReagent = new ReagentId(metabolismType.EnergyReagent.Value, null); + + //prevent accidents, yaml should not be setting values in these but just in case... + metabolizer.CachedAbsorbedReagents.Clear(); + metabolizer.CachedWasteReagents.Clear(); + foreach (var (reagentId, minAmt) in metabolismType.RequiredReagents) + { + metabolizer.CachedAbsorbedReagents.Add(new ReagentQuantity(reagentId, minAmt, null)); + } + foreach (var (reagentId, minAmt) in metabolismType.WasteReagents) + { + metabolizer.CachedWasteReagents.Add(new ReagentQuantity(reagentId, minAmt, null)); + } + Dirty(uid, metabolizer); + } + + private void OnMapInit(EntityUid uid, MetabolizerComponent metabolizer, MapInitEvent args) + { + if (_netManager.IsClient) + return; + if (metabolizer.UsesBodySolution) //if we are using a solution on body, differ initializing cached solutions + return; + + UpdateCachedMetabolizerSolutions((uid, metabolizer), uid); + } + + private void OnBodyInit(EntityUid uid, MetabolizerComponent metabolizer, BodyInitializedEvent args) + { + if (!metabolizer.UsesBodySolution) + return; + UpdateCachedMetabolizerSolutions((uid, metabolizer), args.Body); + } + + + private bool DoMetabolicReaction(Entity metabolizer) + { + if (metabolizer.Comp.CachedAbsorbSolutionEnt == EntityUid.Invalid + || metabolizer.Comp.CachedWasteSolutionEnt == EntityUid.Invalid) + return false; + var absorbSol = Comp(metabolizer.Comp.CachedAbsorbSolutionEnt); + var wasteSol = Comp(metabolizer.Comp.CachedWasteSolutionEnt); + + + //TODO: some of this can be simplified if/when there is a common unit reactions calculation method in chem + foreach (var (reagentId, quantity) in metabolizer.Comp.CachedAbsorbedReagents) + { + var reqQuant = quantity * metabolizer.Comp.BaseMultiplier; + if (!absorbSol.Solution.TryGetReagentQuantity(reagentId, out var solQuant) || solQuant < reqQuant) + { + return false; + } + } + + //actually remove the reagents now + foreach (var (reagentId, quantity) in metabolizer.Comp.CachedAbsorbedReagents) + { + var reqQuant = quantity*metabolizer.Comp.BaseMultiplier; + absorbSol.Solution.RemoveReagent(reagentId, reqQuant); + } + Dirty(metabolizer.Comp.CachedAbsorbSolutionEnt, absorbSol); + + if (metabolizer.Comp.CachedWasteReagents.Count == 0) + return true; //if there are no waste products then exit out + foreach (var (reagentId, quantity) in metabolizer.Comp.CachedWasteReagents) + { + var reqQuant = quantity*metabolizer.Comp.BaseMultiplier; + absorbSol.Solution.AddReagent(reagentId, reqQuant); + } + _solutionContainerSystem.UpdateChemicals((metabolizer.Comp.CachedWasteSolutionEnt,wasteSol)); + return true; + } + + private void UpdateCachedMetabolizerSolutions(Entity metabolizer, + EntityUid? target) + { + target ??= metabolizer.Owner; + + var dirty = false; + if (_solutionContainerSystem.TryGetSolution((target.Value, null), metabolizer.Comp.AbsorbSolutionId, out var absorbSol, true)) + { + metabolizer.Comp.CachedAbsorbSolutionEnt = absorbSol.Value.Owner; + dirty = true; + } + if (_solutionContainerSystem.TryGetSolution((target.Value, null), metabolizer.Comp.WasteSolutionId, out var wasteSol, true)) + { + metabolizer.Comp.CachedWasteSolutionEnt = wasteSol.Value.Owner; + dirty = true; + } + if (dirty) + Dirty(metabolizer); + } +} diff --git a/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.cs b/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.cs new file mode 100644 index 00000000000000..cb837efad7b092 --- /dev/null +++ b/Content.Shared/Medical/Metabolism/Systems/MetabolismSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Medical.Metabolism.Systems; + + +/// +/// Handles metabolic reactions, primarily cellular respiration which energizes organs by converting Oxygen/Glucose into Co2/Energy +/// Also... The mitochondria is the powerhouse of the cell. +/// +public sealed partial class MetabolismSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly DamageableSystem _damageSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly INetManager _netManager = default!; + + public override void Initialize() + { + MetabolizerInit(); + BodyMetabolismInit(); + } + + public override void Update(float frameTime) + { + MetabolizerUpdate(frameTime); + UpdateBodyMetabolism(frameTime); + } +} diff --git a/Content.Shared/Medical/Organs/Components/BrainComponent.cs b/Content.Shared/Medical/Organs/Components/BrainComponent.cs new file mode 100644 index 00000000000000..a1c29ebc3fc6a3 --- /dev/null +++ b/Content.Shared/Medical/Organs/Components/BrainComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Organs.Components +{ + //todo fix friends: Access(typeof(BrainSystem)) + [RegisterComponent, NetworkedComponent] + public sealed partial class BrainComponent : Component + { + } +} diff --git a/Content.Shared/Medical/Organs/Components/HeartComponent.cs b/Content.Shared/Medical/Organs/Components/HeartComponent.cs new file mode 100644 index 00000000000000..0be6038a662eb0 --- /dev/null +++ b/Content.Shared/Medical/Organs/Components/HeartComponent.cs @@ -0,0 +1,49 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Organs.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HeartComponent : Component +{ + + //TODO: implement fibrillation :^) + + /// + /// Is this heart currently beating? + /// For mapped organs this should be set to false, unless you want the heart to beat while + /// it's sitting on the ground. Fun fact: the heart contains its own neurons and will keep beating even after death or + /// removal from the body as long as it is supplied with oxygen. + /// + [DataField, AutoNetworkedField] + public bool IsBeating = true; + + + /// + /// How much blood does this heart pump per cycle + /// + [DataField, AutoNetworkedField]//TODO: Required + public FixedPoint2 PumpVolume = 30; + + /// + /// The resting heartrate for this organ + /// + [DataField, AutoNetworkedField] //TODO: required + public FixedPoint2 RestingRate = 90; + + /// + /// The current heart rate. If this is less than 0, it will be set to the resting rate on map init. + /// + [DataField, AutoNetworkedField] + public FixedPoint2 CurrentRate = -1; + + /// + /// The maximum rate that this heart will safely beat at. + /// + [DataField(required:true), AutoNetworkedField] + public FixedPoint2 MaximumRate = 190; + + public FixedPoint2 RestingRateSeconds => RestingRate/60; + public FixedPoint2 CurrentRateSeconds => CurrentRate/60; + public FixedPoint2 MaximumRateSeconds => MaximumRate/60; +} diff --git a/Content.Shared/Medical/Organs/Systems/CardioSystem.cs b/Content.Shared/Medical/Organs/Systems/CardioSystem.cs new file mode 100644 index 00000000000000..96d30a40a0db3f --- /dev/null +++ b/Content.Shared/Medical/Organs/Systems/CardioSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Organs.Components; + +namespace Content.Shared.Medical.Organs.Systems; + +public sealed class CardioSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnHeartMapInit); + } + + private void OnHeartMapInit(EntityUid uid, HeartComponent heart, ref MapInitEvent args) + { + if (!heart.IsBeating) + { + heart.CurrentRate = 0; + } + if (heart.CurrentRate < 0) + heart.CurrentRate = heart.RestingRate; + Dirty(uid, heart); + } + + + /// + /// Get the OPTIMAL possible cardiac output for the specified organ NOT taking into account efficiency OR blood volume + /// + /// Target heart organ + /// Cardiac Output Value in reagentUnits per second + public FixedPoint2 GetOptimalCardiacOutput(Entity heart) + { + return heart.Comp.PumpVolume * heart.Comp.CurrentRateSeconds; + } + + /// + /// Get the CURRENT cardiac for the specified organ taking into account efficiency OR blood volume + /// + /// Target heart organ + /// Ratio of current to max blood volume (0-1) + /// Efficiency of the heart organ + /// Cardiac Output Value in reagentUnits per second + public FixedPoint2 GetCurrentCardiacOutput(Entity heart, FixedPoint2 bloodVolumeRatio, FixedPoint2 efficiency) + { + if (!heart.Comp.IsBeating) + return 0; //If the heart ain't beating, we ain't pumpin! + + return GetOptimalCardiacOutput(heart) * bloodVolumeRatio * efficiency; + } + + + +} diff --git a/Content.Shared/Medical/Pain/Components/NervesComponent.cs b/Content.Shared/Medical/Pain/Components/NervesComponent.cs new file mode 100644 index 00000000000000..6e5c0dd41d54d3 --- /dev/null +++ b/Content.Shared/Medical/Pain/Components/NervesComponent.cs @@ -0,0 +1,41 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Pain.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class NervesComponent : Component +{ + + [DataField, AutoNetworkedField] + public FixedPoint2 RawPain = FixedPoint2.Zero; + + [DataField, AutoNetworkedField] + public FixedPoint2 Multiplier = 1f; + + [DataField, AutoNetworkedField] + public Dictionary ConditionThresholds = new(); + + [DataField, AutoNetworkedField] + public FixedPoint2 RawPainCap = 100; + + [DataField, AutoNetworkedField] + public FixedPoint2 MaxPain = 100; + + public FixedPoint2 MitigatedPercentage => FixedPoint2.Clamp(RawMitigatedPercentage, 0, 100); + + [DataField, AutoNetworkedField] + public FixedPoint2 RawMitigatedPercentage = 0; + + public FixedPoint2 PainCap => FixedPoint2.Clamp(RawPainCap, 0, MaxPain); + + public FixedPoint2 MitigatedPain => RawPain * MitigatedPercentage / 100; + + public FixedPoint2 Pain => FixedPoint2.Clamp(RawPain* Multiplier - MitigatedPain, 0 , PainCap) ; + + [NetSerializable, Serializable] + [DataRecord] + public record struct MedicalConditionThreshold(EntProtoId ConditionId, bool Applied); +} diff --git a/Content.Shared/Medical/Pain/Events/PainEvents.cs b/Content.Shared/Medical/Pain/Events/PainEvents.cs new file mode 100644 index 00000000000000..fca0624ea4d00c --- /dev/null +++ b/Content.Shared/Medical/Pain/Events/PainEvents.cs @@ -0,0 +1,8 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Pain.Components; + +namespace Content.Shared.Medical.Pain.Events; + + +[ByRefEvent] +public record struct PainChangedEvent(Entity NervousSystem, FixedPoint2 PainDelta); diff --git a/Content.Shared/Medical/Pain/Systems/PainSystem.cs b/Content.Shared/Medical/Pain/Systems/PainSystem.cs new file mode 100644 index 00000000000000..92653cd2741292 --- /dev/null +++ b/Content.Shared/Medical/Pain/Systems/PainSystem.cs @@ -0,0 +1,114 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.HealthConditions.Components; +using Content.Shared.Medical.HealthConditions.Systems; +using Content.Shared.Medical.Pain.Components; +using Content.Shared.Medical.Pain.Events; + +namespace Content.Shared.Medical.Pain.Systems; + + +public sealed class PainSystem : EntitySystem +{ + [Dependency] private readonly HealthConditionSystem _healthConditionSystem = default!; + + public void CheckPainThresholds(Entity nervousSystem) + { + if (!Resolve(nervousSystem,ref nervousSystem.Comp)) + return; + var pain = nervousSystem.Comp.Pain; + var changedThresholds = new List<(FixedPoint2, NervesComponent.MedicalConditionThreshold)>(); + var conditionManager = new Entity(nervousSystem, null); + foreach (var (painPercent, conditionData) in nervousSystem.Comp.ConditionThresholds) + { + var requiredPain = painPercent / 100 * nervousSystem.Comp.MaxPain; + requiredPain = CalculateAdjustedPain(new(nervousSystem, nervousSystem.Comp), requiredPain); + if (requiredPain < pain) + { + if (conditionData.Applied) + { + _healthConditionSystem.TryRemoveCondition(conditionManager, conditionData.ConditionId); + changedThresholds.Add((painPercent, conditionData with {Applied = false})); + } + continue; //move next + } + if (!conditionData.Applied) + continue; + + _healthConditionSystem.TryAddCondition(conditionManager, conditionData.ConditionId, + out _, 100); + changedThresholds.Add((painPercent, conditionData with {Applied = true})); + } + + foreach (var (painThreshold, newThresholdData) in changedThresholds) + { + nervousSystem.Comp.ConditionThresholds[painThreshold] = newThresholdData; + } + Dirty(nervousSystem, nervousSystem.Comp); + } + + public FixedPoint2 CalculateAdjustedPain(Entity nervousSystem, FixedPoint2 rawPain) + { + return FixedPoint2.Clamp(rawPain * nervousSystem.Comp.Multiplier - nervousSystem.Comp.MitigatedPain, 0, + nervousSystem.Comp.PainCap); + } + + public void ChangePain(Entity nervousSystem, FixedPoint2 painDelta) + { + if (painDelta == 0 || !Resolve(nervousSystem,ref nervousSystem.Comp)) + return; + var oldPain = nervousSystem.Comp.Pain; + nervousSystem.Comp.RawPain += painDelta; + var newPain = nervousSystem.Comp.Pain; + Dirty(nervousSystem); + if (oldPain == newPain) + return; + var ev = new PainChangedEvent(new(nervousSystem, nervousSystem.Comp), newPain - oldPain); + RaiseLocalEvent(nervousSystem, ref ev); + CheckPainThresholds(nervousSystem); + } + + public void ChangeCap(Entity nervousSystem, FixedPoint2 painCapDelta) + { + if (painCapDelta == 0 || !Resolve(nervousSystem,ref nervousSystem.Comp)) + return; + var oldPain = nervousSystem.Comp.Pain; + nervousSystem.Comp.RawPainCap += painCapDelta; + var newPain = nervousSystem.Comp.Pain; + Dirty(nervousSystem); + if (oldPain == newPain) + return; + var ev = new PainChangedEvent(new(nervousSystem, nervousSystem.Comp), newPain - oldPain); + RaiseLocalEvent(nervousSystem, ref ev); + CheckPainThresholds(nervousSystem); + } + + public void ChangeMultiplier(Entity nervousSystem, FixedPoint2 painMultDelta) + { + if (painMultDelta == 0 || !Resolve(nervousSystem,ref nervousSystem.Comp)) + return; + var oldPain = nervousSystem.Comp.Pain; + nervousSystem.Comp.Multiplier += painMultDelta; + var newPain = nervousSystem.Comp.Pain; + Dirty(nervousSystem); + if (oldPain == newPain) + return; + var ev = new PainChangedEvent(new(nervousSystem, nervousSystem.Comp), newPain - oldPain); + RaiseLocalEvent(nervousSystem, ref ev); + CheckPainThresholds(nervousSystem); + } + + public void ChangeMitigation(Entity nervousSystem, FixedPoint2 mitigationPercentDelta) + { + if (mitigationPercentDelta == 0 || !Resolve(nervousSystem,ref nervousSystem.Comp)) + return; + var oldPain = nervousSystem.Comp.Pain; + nervousSystem.Comp.RawMitigatedPercentage += mitigationPercentDelta; + var newPain = nervousSystem.Comp.Pain; + Dirty(nervousSystem); + if (oldPain == newPain) + return; + var ev = new PainChangedEvent(new(nervousSystem, nervousSystem.Comp), newPain - oldPain); + RaiseLocalEvent(nervousSystem, ref ev); + CheckPainThresholds(nervousSystem); + } +} diff --git a/Content.Shared/Medical/Respiration/Components/LungsComponent.cs b/Content.Shared/Medical/Respiration/Components/LungsComponent.cs new file mode 100644 index 00000000000000..2361d75f805a74 --- /dev/null +++ b/Content.Shared/Medical/Respiration/Components/LungsComponent.cs @@ -0,0 +1,182 @@ +using Content.Shared.Alert; +using Content.Shared.Atmos; +using Content.Shared.Medical.Metabolism.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Medical.Respiration.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LungsComponent : Component +{ + /// + /// The time it takes to perform an inhalation + /// + [DataField, AutoNetworkedField] + public TimeSpan InhaleTime = TimeSpan.FromSeconds(1.5); + + /// + /// The time it takes to perform an exhalation + /// + [DataField, AutoNetworkedField] + public TimeSpan ExhaleTime = TimeSpan.FromSeconds(1); + + /// + /// Maximum time that breath can be held for. This does not work in a vacuum/low pressure, + /// attempting it will result in a full exhale. + /// + [DataField, AutoNetworkedField] + public TimeSpan MaxHoldTime = TimeSpan.FromSeconds(90); + + + /// + /// The interval between updates. CycleTime (Inhale/exhale time) is added on top of this + /// + [DataField, AutoNetworkedField] + public TimeSpan PauseTime = TimeSpan.FromSeconds(1); + + public TimeSpan NextPhaseDelay => + Phase switch + { + BreathingPhase.Inhale => InhaleTime, + BreathingPhase.Exhale => ExhaleTime, + BreathingPhase.Pause => PauseTime, + BreathingPhase.Hold => MaxHoldTime, + BreathingPhase.Suffocating => InhaleTime, + _ => throw new Exception("Unknown phase of breathing cycle. This should not happen!") + }; + + public float TargetVolume + => Phase switch + { + BreathingPhase.Inhale => float.Lerp(NormalInhaleVolume, + MaxVolume, + BreathEffort), + BreathingPhase.Exhale => float.Lerp(MinVolume, + NormalExhaleVolume, + 1-BreathEffort), + //We only care about target volume when inhaling or exhaling + _ => ContainedGas.Volume + }; + + /// + /// Can these lungs breathe? + /// + public bool CanBreathe => Phase != BreathingPhase.Suffocating; + + [DataField, AutoNetworkedField] + public BreathingPhase Phase = BreathingPhase.Pause; + + /// + /// The maximum volume that these lungs can expand to. + /// + [DataField, AutoNetworkedField] + public float MaxVolume = 8; + + /// + /// The minimum volume that these lungs can compress to. This is bypassed and volume is set to 0 in low pressure environments. + /// + [DataField, AutoNetworkedField] + public float MinVolume = 1.5f; + + /// + /// Lung volume during a normal inhale + /// + [DataField, AutoNetworkedField] + public float NormalInhaleVolume = 4f; + + /// + /// Lung volume during a normal exhale + /// + [DataField, AutoNetworkedField] + public float NormalExhaleVolume = 3.5f; + + /// + /// How much extra *work* is being put into breathing, this is used to lerp between volume range and it's respective max value + /// + [DataField, AutoNetworkedField] + public float BreathEffort = 0f; + + /// + /// How quickly does breathing effort change based on if we are outside the target range for an absorbed reagent + /// + [DataField, AutoNetworkedField] + public float EffortSensitivity = 0.05f; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public GasMixture ContainedGas = new(); + + /// + /// What type of respiration does this respirator use + /// + [DataField(required: true), AutoNetworkedField] + public ProtoId MetabolismType; + + /// + /// Should we look for our solution on the body or this entity + /// + [DataField, AutoNetworkedField] + public bool UsesBodySolutions = true; + + /// + /// What solutionId to put absorbed reagents into + /// + [DataField, AutoNetworkedField] + public string TargetSolutionId = "bloodReagents"; + + /// + /// Cached solution owner entity. + /// + [DataField, AutoNetworkedField] + public EntityUid SolutionOwnerEntity = EntityUid.Invalid; + + [DataField, AutoNetworkedField] + public EntityUid CachedTargetSolutionEnt = EntityUid.Invalid; + + /// + /// cached data for absorbed gases + /// + [DataField, AutoNetworkedField] + public List<(Gas gas, string reagent, GasMetabolismData)> CachedAbsorbedGasData = new(); + + /// + /// cached data for waste gases + /// + [DataField, AutoNetworkedField] + public List<(Gas gas, string reagent, GasMetabolismData)> CachedWasteGasData = new(); + + // TODO do alert + /// + /// The type of gas this lung needs. Used only for the breathing alerts, not actual metabolism. + /// + [DataField] + public ProtoId Alert = "LowOxygen"; +} + + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LungsTickingComponent : Component +{ + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextPhasedUpdate; + + /// + /// Rate that reagents are absorbed from the contained gas, and when low-pressure is checked + /// + [DataField, AutoNetworkedField] + public TimeSpan UpdateRate = TimeSpan.FromSeconds(0.5f); +} + + +public enum BreathingPhase : byte +{ + Inhale, + Exhale, + Pause, + Hold, + Suffocating +} diff --git a/Content.Shared/Medical/Respiration/Events/RespirationEvents.cs b/Content.Shared/Medical/Respiration/Events/RespirationEvents.cs new file mode 100644 index 00000000000000..fe2945995514f7 --- /dev/null +++ b/Content.Shared/Medical/Respiration/Events/RespirationEvents.cs @@ -0,0 +1,20 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Medical.Respiration.Components; + +namespace Content.Shared.Medical.Respiration.Events; + + + +[ByRefEvent] +public record struct BreathAttemptEvent( + Entity Lungs, + bool Canceled = false); + +[ByRefEvent] +public record struct BreatheEvent( + Entity Lungs, + Entity AbsorptionSolutionEnt, + Solution AbsorptionSolution, + Entity WasteSolutionEnt, + Solution WasteSolution); diff --git a/Content.Shared/Medical/Respiration/Systems/SharedLungsSystem.cs b/Content.Shared/Medical/Respiration/Systems/SharedLungsSystem.cs new file mode 100644 index 00000000000000..f2a0a559cc4ebc --- /dev/null +++ b/Content.Shared/Medical/Respiration/Systems/SharedLungsSystem.cs @@ -0,0 +1,330 @@ +using Content.Shared.Atmos; +using Content.Shared.Body.Events; +using Content.Shared.Body.Systems; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Medical.Blood.Systems; +using Content.Shared.Medical.Respiration.Components; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Medical.Respiration.Systems; + + +/// +/// +/// +[Virtual] +public abstract class SharedLungsSystem : EntitySystem //Never forget the communal-lung incident of 2023 +{ + [Dependency] protected readonly IPrototypeManager ProtoManager = default!; + [Dependency] protected readonly IGameTiming GameTiming = default!; + [Dependency] protected SharedSolutionContainerSystem SolutionContainerSystem = default!; + [Dependency] protected BloodstreamSystem BloodstreamSystem = default!; + [Dependency] protected INetManager NetManager = default!; + + private int solutionVolume = 0; //TODO: unhardcode this shit + + public override void Initialize() + { + solutionVolume = BloodstreamSystem.BloodstreamVolumeTEMP; + SubscribeLocalEvent(OnLungsMapInit, after:[typeof(SharedBodySystem), typeof(BloodstreamSystem)]); + SubscribeLocalEvent(OnBodyInitialized, after: [typeof(BloodstreamSystem)]); + base.Initialize(); + } + + private void OnBodyInitialized(EntityUid uid, LungsComponent lungsComp, ref BodyInitializedEvent args) + { + if (!lungsComp.UsesBodySolutions) + return; + SetupSolution((uid, lungsComp), args.Body, null); + lungsComp.SolutionOwnerEntity = args.Body; //the owner is the body we are initialized in + SetLungTicking(uid, true); + Dirty(uid, lungsComp); + } + + protected void BreathCycle(Entity lungs) + { + switch (lungs.Comp.Phase) + { + case BreathingPhase.Inhale: + case BreathingPhase.Exhale: + { + UpdateLungVolume(lungs); + break; + } + case BreathingPhase.Suffocating: + { + Log.Debug($"{ToPrettyString(lungs.Comp.SolutionOwnerEntity)} is suffocating!"); + return; + } + } + EqualizeLungPressure(lungs); + } + + protected void UpdateBreathability(Entity lungs) + { + if (HasBreathableAtmosphere(lungs, GetBreathingAtmosphere(lungs))) + { + if (lungs.Comp.CanBreathe) + return; + Log.Debug($"{ToPrettyString(lungs.Comp.SolutionOwnerEntity)} is breathing again!"); + lungs.Comp.Phase = BreathingPhase.Inhale; + UpdateLungVolume(lungs); + return; + } + if (!lungs.Comp.CanBreathe) + return; + + Log.Debug($"{ToPrettyString(lungs.Comp.SolutionOwnerEntity)} started suffocating!"); + lungs.Comp.Phase = BreathingPhase.Suffocating; + EmptyLungs(lungs); + + } + + private void OnLungsMapInit(EntityUid uid, LungsComponent lungsComp, ref MapInitEvent args) + { + if (NetManager.IsClient) + return; + + var targetEnt = uid; + if (!lungsComp.UsesBodySolutions) + { + if (!SolutionContainerSystem.EnsureSolutionEntity((uid, null), + lungsComp.TargetSolutionId, + out var solEnt)) + return; //this will only ever return false on client and map init only runs on the server. + SetupSolution((targetEnt, lungsComp), targetEnt, solEnt); + lungsComp.SolutionOwnerEntity = uid;//the owner is ourself + } + + var respType = ProtoManager.Index(lungsComp.MetabolismType); + foreach (var (gasProto, gasSettings) in respType.AbsorbedGases) + { + var gas = ProtoManager.Index(gasProto); + if (gas.Reagent == null) + { + Log.Error($"Gas:{gas.Name} : {gas.ID} does not have an assigned reagent. This is required to be absorbable"); + continue; + } + lungsComp.CachedAbsorbedGasData.Add(((Gas)sbyte.Parse(gas.ID), gas.Reagent, gasSettings)); + } + foreach (var (gasProto, maxAbsorption) in respType.WasteGases) + { + var gas = ProtoManager.Index(gasProto); + if (gas.Reagent == null) + { + Log.Error($"Gas:{gas.Name} : {gas.ID} does not have an assigned reagent. This is required to be absorbable"); + continue; + } + lungsComp.CachedWasteGasData.Add(((Gas)sbyte.Parse(gas.ID), gas.Reagent, maxAbsorption)); + } + lungsComp.Phase = BreathingPhase.Exhale; + lungsComp.ContainedGas.Volume = lungsComp.NormalExhaleVolume; + if (!lungsComp.UsesBodySolutions) + SetLungTicking(uid, true); + Dirty(uid, lungsComp); + } + private void SetupSolution(Entity lungs, EntityUid targetEnt, Entity? solutionComp) + { + var targetSolEnt = solutionComp; + if (targetSolEnt == null && !SolutionContainerSystem.TryGetSolution((targetEnt, null), + lungs.Comp.TargetSolutionId, + out targetSolEnt, + out var targetSol, + true)) + return; + + targetSol = targetSolEnt.Value.Comp.Solution; + + //set up the solution with the initial starting concentration of absorbed gases + var metabolism = ProtoManager.Index(lungs.Comp.MetabolismType); + foreach (var (gasId, (lowThreshold, highThreshold)) in metabolism.AbsorbedGases) + { + var gasProto = ProtoManager.Index(gasId); + if (gasProto.Reagent == null) + continue; + targetSol.AddReagent(gasProto.Reagent, highThreshold * solutionVolume); + } + SolutionContainerSystem.UpdateChemicals(targetSolEnt.Value); + + //cache solutionEntities because they should never be removed + lungs.Comp.CachedTargetSolutionEnt = targetSolEnt.Value; + Dirty(lungs); + } + + protected void SetNextPhaseDelay(Entity lungs) + { + lungs.Comp1.Phase = lungs.Comp1.Phase switch + { + BreathingPhase.Inhale => BreathingPhase.Pause, + BreathingPhase.Pause => BreathingPhase.Exhale, + BreathingPhase.Exhale => BreathingPhase.Inhale, + BreathingPhase.Hold => BreathingPhase.Inhale, + BreathingPhase.Suffocating => BreathingPhase.Suffocating, + _ => lungs.Comp1.Phase + }; + lungs.Comp2.NextPhasedUpdate = GameTiming.CurTime + lungs.Comp1.NextPhaseDelay; + Dirty(lungs); + } + + protected void UpdateLungVolume(Entity lungs) + { + lungs.Comp.ContainedGas.Volume = lungs.Comp.TargetVolume; + Dirty(lungs); + } + + private bool IsBreathableGasMix(GasMixture? gasMixture) + { + return gasMixture != null && gasMixture.Pressure >= Atmospherics.HazardLowPressure; + } + + //TODO: internals :) + public bool HasBreathableAtmosphere(EntityUid uid, GasMixture? gasMixture) + { + return IsBreathableGasMix(gasMixture); + } + + public bool HasBreathableAtmosphere(Entity lungs) + { + if (!Resolve(lungs, ref lungs.Comp)) + return true; //if we have no lungs anything is breathable + var extGas = GetBreathingAtmosphere((lungs, lungs.Comp)); + return HasBreathableAtmosphere(lungs, extGas); + } + + protected virtual void EmptyLungs(Entity lungs) + { + lungs.Comp.ContainedGas = new() { Volume = 0 }; + Dirty(lungs); + } + + protected void AbsorbGases(Entity lungs) + { + //Do not try to absorb gases if there are none there, or if there are no solution to absorb into + if (!lungs.Comp.CanBreathe + || lungs.Comp.ContainedGas.Volume == 0 + || lungs.Comp.CachedTargetSolutionEnt == EntityUid.Invalid + ) + return; + + var effortMult = 0;//0 is in-range, +1 is low, -1 is high + + var targetSolEnt = + new Entity(lungs.Comp.CachedTargetSolutionEnt, Comp(lungs.Comp.CachedTargetSolutionEnt)); + var targetSolution = targetSolEnt.Comp.Solution; + + foreach (var (gas, reagent, (lowConc, highConc)) in lungs.Comp.CachedAbsorbedGasData) + { + var gasMols = lungs.Comp.ContainedGas[(int) gas]; + if (gasMols <= 0) + continue; + var reagentConc = SolutionContainerSystem.GetReagentConcentration(targetSolEnt, + solutionVolume, + new ReagentId(reagent, null)); + + if (reagentConc < lowConc && effortMult <= 0) + effortMult = 1; + if (reagentConc > highConc && effortMult == 0) + { + effortMult = -1; + continue; + } + + var concentrationDelta = highConc - reagentConc; + if (concentrationDelta == 0) + continue; + + var maxAddedReagent = concentrationDelta * solutionVolume; + var reagentToAdd = GetReagentUnitsFromMol(gasMols, reagent, lungs.Comp.ContainedGas); + if (maxAddedReagent < reagentToAdd.Quantity) + reagentToAdd = new (reagentToAdd.Reagent, maxAddedReagent); + targetSolution.AddReagent(reagentToAdd); + lungs.Comp.ContainedGas.SetMoles(gas, gasMols-( gasMols * concentrationDelta)); + } + + foreach (var (gas, reagent, (lowConc, highConc)) in lungs.Comp.CachedWasteGasData) + { + var reagentConc = SolutionContainerSystem.GetReagentConcentration(targetSolEnt, solutionVolume, new ReagentId(reagent, null)); + if (reagentConc < lowConc && effortMult != 1) + { + effortMult = -1; + continue; + } + if (reagentConc > highConc && effortMult == 0) + { + effortMult = 1; + } + + var reagentDelta = reagentConc - lowConc; + if (reagentDelta == 0) + continue; + + var reagentToRemove = new ReagentQuantity(new (reagent, null), + reagentDelta * solutionVolume); + var molsToAdd = GetMolsOfReagent(targetSolution, reagent, lungs.Comp.ContainedGas); + var maxMolsToAdd = lungs.Comp.ContainedGas.TotalMoles * reagentDelta; + if (maxMolsToAdd == 0) + continue; + if (maxMolsToAdd < molsToAdd) + { + molsToAdd = maxMolsToAdd; + reagentToRemove = GetReagentUnitsFromMol(molsToAdd, reagent, lungs.Comp.ContainedGas); + } + lungs.Comp.ContainedGas.AdjustMoles(gas, molsToAdd); + targetSolution.RemoveReagent(reagentToRemove); + } + + SolutionContainerSystem.UpdateChemicals(targetSolEnt, false, false); + Dirty(lungs); + + if (effortMult == 0) + return; + lungs.Comp.BreathEffort = + Math.Clamp(lungs.Comp.BreathEffort + effortMult * lungs.Comp.EffortSensitivity, 0, 1); + } + + + protected ReagentQuantity GetReagentUnitsFromMol(float gasMols, string reagentId, GasMixture gasMixture) + { + return new(reagentId, + Atmospherics.MolsToVolume(gasMols, + gasMixture.Pressure, + gasMixture.Temperature), + null); + } + + protected float GetMolsOfReagent(Solution solution, string reagentId, GasMixture gasMixture) + { + var reagentVolume = solution.GetReagent(new (reagentId, null)).Quantity; + return Atmospherics.VolumeToMols(reagentVolume.Float(), gasMixture.Pressure, gasMixture.Temperature); + } + + public void SetLungTicking(Entity lungs, bool shouldTick) + { + if (shouldTick) + { + EnsureComp(lungs); + return; + } + RemComp(lungs); + } + + + //TODO: internals :) + protected virtual GasMixture? GetBreathingAtmosphere(Entity lungs) + { + return null; + } + + //these are stub implementations for the client, all atmos handling is handled serverside + #region stubs + + protected virtual void EqualizeLungPressure(Entity lungs) + { + } + #endregion + +} diff --git a/Content.Shared/Medical/Trauma/Components/ConsciousnessTraumaComp.cs b/Content.Shared/Medical/Trauma/Components/ConsciousnessTraumaComp.cs new file mode 100644 index 00000000000000..6a12b2301c6e8b --- /dev/null +++ b/Content.Shared/Medical/Trauma/Components/ConsciousnessTraumaComp.cs @@ -0,0 +1,32 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Trauma.Components; + +[RegisterComponent, AutoGenerateComponentState, NetworkedComponent] +public sealed partial class ConsciousnessTraumaComponent : Component +{ + /// + /// How much are we decreasing our consciousness cap + /// + [DataField, AutoNetworkedField] + public FixedPoint2 CapDecrease= 0; + + /// + /// How much are we decreasing our consciousness + /// + [DataField, AutoNetworkedField] + public FixedPoint2 Decrease = 0; + + /// + /// How much we should change the consciousness multiplier by + /// + [DataField, AutoNetworkedField] + public FixedPoint2 MultiplierDecrease = 0; + + /// + /// How much we should change the consciousness multiplier by + /// + [DataField, AutoNetworkedField] + public FixedPoint2 ModifierDecrease = 0; +} diff --git a/Content.Shared/Medical/Trauma/Components/HealthTraumaComp.cs b/Content.Shared/Medical/Trauma/Components/HealthTraumaComp.cs new file mode 100644 index 00000000000000..e80d19b8925955 --- /dev/null +++ b/Content.Shared/Medical/Trauma/Components/HealthTraumaComp.cs @@ -0,0 +1,14 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Trauma.Components; + +[RegisterComponent, AutoGenerateComponentState, NetworkedComponent] +public sealed partial class HealthTraumaComponent : Component +{ + /// + /// How much are we decreasing our woundables health cap, expressed as a percentage (/100) of maximum + /// + [DataField, AutoNetworkedField] + public FixedPoint2 HealthCapDecrease = 0; +} diff --git a/Content.Shared/Medical/Trauma/Components/IntegrityTraumaComp.cs b/Content.Shared/Medical/Trauma/Components/IntegrityTraumaComp.cs new file mode 100644 index 00000000000000..06c0866f62db6c --- /dev/null +++ b/Content.Shared/Medical/Trauma/Components/IntegrityTraumaComp.cs @@ -0,0 +1,20 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Trauma.Components; + +[RegisterComponent, AutoGenerateComponentState, NetworkedComponent] +public sealed partial class IntegrityTraumaComponent : Component +{ + /// + /// How much are we decreasing our woundables integrity cap, expressed as a percentage (/100) of maximum + /// + [DataField, AutoNetworkedField] + public FixedPoint2 IntegrityCapDecrease = 0; + + /// + /// How much damage are we applying to integrity, expressed as a percentage (/100) of maximum integrity + /// + [DataField, AutoNetworkedField] + public FixedPoint2 IntegrityDecrease = 0; +} diff --git a/Content.Shared/Medical/Trauma/Components/PainTraumaCorp.cs b/Content.Shared/Medical/Trauma/Components/PainTraumaCorp.cs new file mode 100644 index 00000000000000..f4874ed7c86660 --- /dev/null +++ b/Content.Shared/Medical/Trauma/Components/PainTraumaCorp.cs @@ -0,0 +1,20 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Trauma.Components; + +[RegisterComponent, AutoGenerateComponentState, NetworkedComponent] +public sealed partial class PainTraumaComponent : Component +{ + [DataField, AutoNetworkedField] + public FixedPoint2 PainDecrease = 0; + + [DataField, AutoNetworkedField] + public FixedPoint2 PainCapDecrease = 0; + + [DataField, AutoNetworkedField] + public FixedPoint2 PainMultiplierDecrease = 0; + + [DataField, AutoNetworkedField] + public FixedPoint2 PainMitigationDecrease = 0; +} diff --git a/Content.Shared/Medical/Trauma/Systems/TraumaSystem.cs b/Content.Shared/Medical/Trauma/Systems/TraumaSystem.cs new file mode 100644 index 00000000000000..52855cace38e29 --- /dev/null +++ b/Content.Shared/Medical/Trauma/Systems/TraumaSystem.cs @@ -0,0 +1,136 @@ +using Content.Shared.Medical.Consciousness.Components; +using Content.Shared.Medical.Consciousness.Systems; +using Content.Shared.Medical.Pain.Components; +using Content.Shared.Medical.Pain.Systems; +using Content.Shared.Medical.Trauma.Components; +using Content.Shared.Medical.Wounding.Events; +using Content.Shared.Medical.Wounding.Systems; +using Content.Shared.Mobs.Components; + +namespace Content.Shared.Medical.Trauma.Systems; + +public sealed partial class TraumaSystem : EntitySystem +{ + + [Dependency] private readonly ConsciousnessSystem _consciousnessSystem = default!; + [Dependency] private readonly PainSystem _painSystem = default!; + [Dependency] private readonly WoundSystem _woundSystem = default!; + + public override void Initialize() + { + SubscribeTraumaWoundEvents(ApplyHealthTrauma, RemoveHealthTrauma); + SubscribeTraumaWoundEvents(ApplyIntegrityTrauma, RemoveIntegrityTrauma); + + SubscribeTraumaBodyEvents(ApplyConsciousnessTrauma, RemoveConsciousnessTrauma); + SubscribeTraumaBodyEvents(ApplyPainTrauma, RemovePainTrauma); + } + + private void ApplyPainTrauma(EntityUid uid, PainTraumaComponent pain, ref WoundAppliedToBody args) + { + _painSystem.ChangePain(new Entity(uid, null), + -pain.PainDecrease); + _painSystem.ChangeCap(new Entity(uid, null), + -pain.PainCapDecrease); + _painSystem.ChangeMultiplier(new Entity(uid, null), + -pain.PainMultiplierDecrease); + _painSystem.ChangeMitigation(new Entity(uid, null), + -pain.PainMultiplierDecrease); + } + + private void RemovePainTrauma(EntityUid uid, PainTraumaComponent pain, ref WoundRemovedFromBody args) + { + _painSystem.ChangePain(new Entity(uid, null), + pain.PainDecrease); + _painSystem.ChangeCap(new Entity(uid, null), + pain.PainCapDecrease); + _painSystem.ChangeMultiplier(new Entity(uid, null), + pain.PainMultiplierDecrease); + _painSystem.ChangeMitigation(new Entity(uid, null), + pain.PainMultiplierDecrease); + } + + + private void ApplyConsciousnessTrauma(EntityUid uid, ConsciousnessTraumaComponent trauma, ref WoundAppliedToBody args) + { + _consciousnessSystem.ChangeConsciousness( + new Entity(args.Body.Owner, null, null), + -trauma.Decrease + ); + _consciousnessSystem.ChangeConsciousnessCap( + new Entity(args.Body.Owner, null, null), + -trauma.CapDecrease); + + _consciousnessSystem.ChangeConsciousnessMultiplier( + new Entity(args.Body.Owner, null, null), + -trauma.MultiplierDecrease + ); + _consciousnessSystem.ChangeConsciousnessModifier( + new Entity(args.Body.Owner, null, null), + -trauma.ModifierDecrease + ); + } + private void RemoveConsciousnessTrauma(EntityUid uid, ConsciousnessTraumaComponent trauma, ref WoundRemovedFromBody args) + { + _consciousnessSystem.ChangeConsciousness( + new Entity(args.Body.Owner, null, null), + trauma.Decrease + ); + _consciousnessSystem.ChangeConsciousnessCap( + new Entity(args.Body.Owner, null, null), + trauma.CapDecrease); + + _consciousnessSystem.ChangeConsciousnessMultiplier( + new Entity(args.Body.Owner, null, null), + trauma.MultiplierDecrease + ); + _consciousnessSystem.ChangeConsciousnessModifier( + new Entity(args.Body.Owner, null, null), + trauma.ModifierDecrease + ); + } + + private void ApplyIntegrityTrauma(EntityUid uid, IntegrityTraumaComponent trauma, ref WoundCreatedEvent args) + { + _woundSystem.ChangeIntegrity(args.ParentWoundable, -trauma.IntegrityDecrease); + _woundSystem.ChangeIntegrityCap(args.ParentWoundable, -trauma.IntegrityCapDecrease); + } + + private void RemoveIntegrityTrauma(EntityUid uid, IntegrityTraumaComponent trauma, ref WoundDestroyedEvent args) + { + _woundSystem.ChangeIntegrity(args.ParentWoundable, trauma.IntegrityDecrease); + _woundSystem.ChangeIntegrityCap(args.ParentWoundable, trauma.IntegrityCapDecrease); + } + + private void ApplyHealthTrauma(EntityUid uid, HealthTraumaComponent trauma, ref WoundCreatedEvent args) + { + _woundSystem.ChangeHealthCap(args.ParentWoundable, -trauma.HealthCapDecrease); + } + + private void RemoveHealthTrauma(EntityUid uid, HealthTraumaComponent trauma, ref WoundDestroyedEvent args) + { + _woundSystem.ChangeHealthCap(args.ParentWoundable, trauma.HealthCapDecrease); + } + + #region Helpers + + protected void SubscribeTraumaWoundEvents( + ComponentEventRefHandler woundCreated,ComponentEventRefHandler woundDestroyed) where T1: Component, new() + { + SubscribeLocalEvent(woundCreated); + SubscribeLocalEvent(woundDestroyed); + } + + protected void SubscribeTraumaBodyEvents( + ComponentEventRefHandler attachedToBody,ComponentEventRefHandler detachedFromBody) where T1: Component, new() + { + SubscribeLocalEvent(attachedToBody); + SubscribeLocalEvent(detachedFromBody); + } + #endregion + + +} diff --git a/Content.Shared/Medical/Wounding/Components/HealableComponent.cs b/Content.Shared/Medical/Wounding/Components/HealableComponent.cs new file mode 100644 index 00000000000000..8313a221f2d49a --- /dev/null +++ b/Content.Shared/Medical/Wounding/Components/HealableComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Wounding.Components; + + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(WoundSystem))] +public sealed partial class HealableComponent : Component +{ + + [AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public TimeSpan NextUpdate = default; + + [DataField, AutoNetworkedField] + public FixedPoint2 Modifier = 1.0; + +} diff --git a/Content.Shared/Medical/Wounding/Components/MedicalDataComponent.cs b/Content.Shared/Medical/Wounding/Components/MedicalDataComponent.cs new file mode 100644 index 00000000000000..b2b8d2472f30b9 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Components/MedicalDataComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Medical.Wounding.Components; + +[RegisterComponent] +public sealed partial class MedicalDataComponent : Component +{ +} diff --git a/Content.Shared/Medical/Wounding/Components/WoundComponent.cs b/Content.Shared/Medical/Wounding/Components/WoundComponent.cs new file mode 100644 index 00000000000000..ae1d1aafa76c76 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Components/WoundComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Wounding.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(WoundSystem))] +public sealed partial class WoundComponent : Component +{ + /// + /// This is the body we are attached to, if we are attached to one + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public EntityUid? Body; + + /// + /// Root woundable for our parent, this will always be valid + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public EntityUid RootEntity; + + /// + /// Current parentWoundable, this will always be valid + /// + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] + public EntityUid ParentWoundable; + + /// + /// The current severity of the wound expressed as a percentage (/100). + /// This is used to modify multiple values. + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 Severity = 100; + + /// + /// Whether multiple wounds originating from the same prototype can exist on a woundable. + /// + [DataField] + public bool Unique; +} diff --git a/Content.Shared/Medical/Wounding/Components/WoundableComponent.cs b/Content.Shared/Medical/Wounding/Components/WoundableComponent.cs new file mode 100644 index 00000000000000..625e76e9f0e729 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Components/WoundableComponent.cs @@ -0,0 +1,90 @@ +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Prototypes; +using Content.Shared.Medical.Wounding.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; + +namespace Content.Shared.Medical.Wounding.Components; + + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(WoundSystem))] +public sealed partial class WoundableComponent : Component +{ + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public EntityUid? Body; + + public const string WoundableContainerId = "Wounds"; + + /// + /// Should we spread damage to child and parent woundables when we gib this part + /// + [DataField, AutoNetworkedField] + public bool SplatterDamageOnDestroy = true; + + [DataField, AutoNetworkedField]//TODO: Change back to EntProtoId + public string? AmputationWoundProto = null; + + [DataField(required:true, customTypeSerializer: typeof(PrototypeIdDictionarySerializer)), AutoNetworkedField] + public Dictionary Config = new(); + + /// + /// What percentage of CURRENT HEALTH should be healed each healing update. + /// This is only used if a healableComponent is also present. + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 HealPercentage = 1.5; + + + /// + /// This woundable's current health, this is tracked separately from damagable's health and will differ! + /// Health will slowly regenerate overtime. + /// When health reaches 0, all damage will be taken as integrity, which does not heal natural. + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 Health = -1; //this is set during comp init or overriden when defined + + /// + /// The current cap of health. + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 HealthCap = -1; //this is set during comp init + + /// + /// The absolute maximum possible health + /// + [DataField(required: true),ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 MaxHealth = 50; + + /// + /// This woundable's current integrity, if integrity reaches 0, this entity is gibbed/destroyed! + /// Integrity does NOT heal naturally and must be treated to heal. + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 Integrity = -1; //this is set during comp init or overriden when defined + + /// + /// The current cap of integrity + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 IntegrityCap = -1; //this is set during comp init + + /// + /// The absolute maximum possible integrity + /// + [DataField(required: true),ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 MaxIntegrity = 10; + + + /// + /// Helper property for getting Health and Integrity together as a hitpoint pool. + /// Don't show this to players as we want to avoid presenting absolute numbers for health/medical status. + /// + public FixedPoint2 HitPoints => Health + Integrity; + + public FixedPoint2 MaxHitPoints => MaxHealth + MaxIntegrity; + + public FixedPoint2 HitPointPercent => HitPoints / MaxHitPoints; + +} diff --git a/Content.Shared/Medical/Wounding/Events/WoundEvents.cs b/Content.Shared/Medical/Wounding/Events/WoundEvents.cs new file mode 100644 index 00000000000000..c04c4b7ed04fd3 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Events/WoundEvents.cs @@ -0,0 +1,52 @@ +using Content.Shared.Body.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Components; + + +namespace Content.Shared.Medical.Wounding.Events; + +[ByRefEvent] +public record struct CreateWoundAttemptEvent( + Entity TargetWoundable, + Entity PossibleWound, + bool Canceled = false); + +[ByRefEvent] +public record struct WoundCreatedEvent( + Entity ParentWoundable, + Entity Wound); + + +[ByRefEvent] +public record struct DestroyWoundAttemptEvent( + Entity TargetWoundable, + Entity WoundToRemove, + bool CancelRemove = false); + +[ByRefEvent] +public record struct WoundDestroyedEvent( + Entity ParentWoundable, + Entity Wound); + +[ByRefEvent] +public record struct SetWoundSeverityAttemptEvent( + Entity TargetWound, + FixedPoint2 NewSeverity, + bool Cancel = false); + +[ByRefEvent] +public record struct WoundSeverityChangedEvent( + Entity TargetWound, + FixedPoint2 PreviousSeverity); + +[ByRefEvent] +public record struct WoundAppliedToBody( + Entity Body, + Entity Woundable, + Entity Wound); + +[ByRefEvent] +public record struct WoundRemovedFromBody( + Entity Body, + Entity Woundable, + Entity Wound); diff --git a/Content.Shared/Medical/Wounding/Events/WoundableEvents.cs b/Content.Shared/Medical/Wounding/Events/WoundableEvents.cs new file mode 100644 index 00000000000000..b45414b95a6ea7 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Events/WoundableEvents.cs @@ -0,0 +1,13 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Components; + +namespace Content.Shared.Medical.Wounding.Events; + +[ByRefEvent] +public record struct WoundableHealAttemptEvent(Entity TargetWoundable, bool Canceled = false); + +[ByRefEvent] +public record struct WoundableHealthChangedEvent(Entity TargetWoundable, FixedPoint2 HealthDelta); + +[ByRefEvent] +public record struct WoundableIntegrityChangedEvent(Entity TargetWoundable, FixedPoint2 HealthDelta); diff --git a/Content.Shared/Medical/Wounding/Prototypes/WoundPoolPrototype.cs b/Content.Shared/Medical/Wounding/Prototypes/WoundPoolPrototype.cs new file mode 100644 index 00000000000000..686c5f91d6dc19 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Prototypes/WoundPoolPrototype.cs @@ -0,0 +1,42 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Definition; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; + +namespace Content.Shared.Medical.Wounding.Prototypes; + +[Prototype] +public sealed class WoundPoolPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// A list of possible wounds and the percentage of damage taken that is needed to apply them + /// + [DataField(required:true,customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer))] + public SortedDictionary Wounds = new(); +} + +[Serializable, NetSerializable, DataDefinition] +public sealed partial class WoundingMetadata +{ + /// + /// The uppermost damage for the wound pool + /// + [DataField(required:true)] + public FixedPoint2 DamageMax = 200; + + /// + /// How much to scale incoming damage by + /// + [DataField] + public FixedPoint2 Scaling = 1; + + /// + /// Prototype id for the woundpool we are using for this damage + /// + [DataField(required:true)] + public ProtoId WoundPool; +} diff --git a/Content.Shared/Medical/Wounding/Systems/WoundSystem.Body.cs b/Content.Shared/Medical/Wounding/Systems/WoundSystem.Body.cs new file mode 100644 index 00000000000000..448244bc3617bb --- /dev/null +++ b/Content.Shared/Medical/Wounding/Systems/WoundSystem.Body.cs @@ -0,0 +1,48 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Medical.Wounding.Components; +using Content.Shared.Medical.Wounding.Events; +using Robust.Shared.Containers; + +namespace Content.Shared.Medical.Wounding.Systems; + +public sealed partial class WoundSystem +{ + + private void InitBodyListeners() + { + SubscribeLocalEvent(OnWoundableAddedToBody); + SubscribeLocalEvent(OnWoundableRemovedFromBody); + } + + private void OnWoundableRemovedFromBody(EntityUid woundableEnt, WoundableComponent woundableComp, ref BodyPartRemovedFromBodyEvent args) + { + var woundable = new Entity(woundableEnt, woundableComp); + //We don't need an insure where because we don't remove bodyparts during mapInit + //(If we do in the future for some reason, add an ensure in here or things will break) + woundableComp.Body = null; + foreach (var wound in GetAllWounds(woundable)) + { + var ev = new WoundRemovedFromBody(args.OldBody, woundable, wound); + RaiseLocalEvent(wound, ref ev); + RaiseLocalEvent(args.OldBody, ref ev); + } + Dirty(woundableEnt, woundableComp); + } + + private void OnWoundableAddedToBody(EntityUid woundableEnt, WoundableComponent woundableComp, ref BodyPartAddedToBodyEvent args) + { + var woundable = new Entity(woundableEnt, woundableComp); + //the ensure is needed because this gets called by partAddedToBodyEvent which is raised from bodypart's mapInit + _containerSystem.EnsureContainer(woundableEnt, WoundableComponent.WoundableContainerId); + + woundableComp.Body = args.Body; + foreach (var wound in GetAllWounds(woundable)) + { + var ev = new WoundAppliedToBody(args.Body, woundable, wound); + RaiseLocalEvent(wound, ref ev); + RaiseLocalEvent(args.Body, ref ev); + } + Dirty(woundableEnt, woundableComp); + } +} diff --git a/Content.Shared/Medical/Wounding/Systems/WoundSystem.Damage.cs b/Content.Shared/Medical/Wounding/Systems/WoundSystem.Damage.cs new file mode 100644 index 00000000000000..30eac21444eb75 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Systems/WoundSystem.Damage.cs @@ -0,0 +1,141 @@ +using System.Linq; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Damage; +using Content.Shared.Medical.Wounding.Components; + +namespace Content.Shared.Medical.Wounding.Systems; + +public sealed partial class WoundSystem +{ + private void InitDamage() + { + SubscribeLocalEvent(OnWoundableDamaged); + SubscribeLocalEvent(OnBodyDamaged); + } + + + private void OnBodyDamaged(EntityUid bodyEnt, BodyComponent body, ref DamageChangedEvent args) + { + //TODO: Make this method not MEGA ASS, because jesus christ I'm giving myself terminal space aids by doing this. + //This is all placeholder and terrible, rewrite asap + + //Do not handle damage if it is being set instead of being changed. + //We will handle that with another listener + if (args.DamageDelta == null) + return; + if (!_bodySystem.TryGetRootBodyPart(bodyEnt, out var rootPart, body)) + return; + + //TODO: This is a quick hack to prevent asphyxiation/bloodloss from damaging bodyparts + //Once proper body/organ simulation is implemented these can be removed + args.DamageDelta.DamageDict.Remove("Asphyxiation"); + args.DamageDelta.DamageDict.Remove("Bloodloss"); + args.DamageDelta.DamageDict.Remove("Structural"); + if (args.DamageDelta.Empty) + return; + + DamageableComponent? damagableComp; + + if (_random.NextFloat(0f, 1f) > NonCoreDamageChance) + { + var heads = _bodySystem.GetBodyChildrenOfType(bodyEnt, BodyPartType.Head, body).ToList(); + if (_random.NextFloat(0f, 1f) <= HeadDamageChance && heads.Count > 0) + { + + var (headId, _) = heads[_random.Next(heads.Count)]; + if (TryComp(headId, out damagableComp)) + { + _damageableSystem.TryChangeDamage(headId, args.DamageDelta, damageable: damagableComp); + return; + } + } + if (TryComp(rootPart, out damagableComp)) + { + _damageableSystem.TryChangeDamage(rootPart.Value, args.DamageDelta, damageable: damagableComp); + return; + } + } + var children = _bodySystem.GetBodyPartDirectChildren(rootPart.Value, rootPart.Value.Comp).ToArray(); + Entity foundTarget = children[_random.Next(0, children.Length)]; + while (_random.NextFloat(0, 1f) > ChanceForPartSelection) + { + children = _bodySystem.GetBodyPartDirectChildren(foundTarget, foundTarget.Comp).ToArray(); + if (children.Length == 0) + break; + foundTarget = children[_random.Next(0, children.Length)]; + } + _damageableSystem.TryChangeDamage(foundTarget, args.DamageDelta); + } + + private void OnWoundableDamaged(EntityUid owner, WoundableComponent woundableComp, ref DamageChangedEvent args) + { + //Do not handle damage if it is being set instead of being changed. + //We will handle that with another listener + if (args.DamageDelta == null) + return; + CreateWoundsFromDamage(new Entity(owner, woundableComp), args.DamageDelta); + ApplyDamageToWoundable(new Entity(owner, woundableComp), args.DamageDelta); + } + + private void ApplyDamageToWoundable(Entity woundable, DamageSpecifier damageSpec) + { + woundable.Comp.Health -= damageSpec.GetTotal(); + Dirty(woundable); + if (woundable.Comp.Health > woundable.Comp.MaxHealth) + { + woundable.Comp.Health = woundable.Comp.MaxHealth; + return; + } + ValidateWoundable(woundable); + } + + private void ValidateWoundable(Entity woundable) + { + var dirty = false; + + if (woundable.Comp.Health > woundable.Comp.MaxHealth) + { + woundable.Comp.Health = woundable.Comp.MaxHealth; + dirty = true; + } + + if (woundable.Comp.Health < 0) + { + woundable.Comp.Integrity += woundable.Comp.Health; + woundable.Comp.Health = 0; + dirty = true; + } + + if (woundable.Comp.IntegrityCap > woundable.Comp.MaxIntegrity) + { + woundable.Comp.IntegrityCap = woundable.Comp.MaxIntegrity; + dirty = true; + } + + if (woundable.Comp.Integrity > woundable.Comp.IntegrityCap) + { + woundable.Comp.Integrity = woundable.Comp.IntegrityCap; + dirty = true; + } + if (dirty) + Dirty(woundable); + if (_netManager.IsClient) + return; + if (woundable.Comp.Integrity <= 0) + TryGibWoundable(woundable); + } + + + private bool TryGibWoundable(Entity woundable) + { + + if (woundable.Comp.Integrity > 0) + return false; + + //TODO: gib woundable. Setting int to 0 is placeholder until partloss is implemented + woundable.Comp.Integrity = 0; + Log.Debug($"{ToPrettyString(woundable.Owner)} is at 0 integrity and should have been destroyed (Part Gibbing not implemented yet)."); + return true; + } +} diff --git a/Content.Shared/Medical/Wounding/Systems/WoundSystem.Wounding.cs b/Content.Shared/Medical/Wounding/Systems/WoundSystem.Wounding.cs new file mode 100644 index 00000000000000..0d153325ffed6f --- /dev/null +++ b/Content.Shared/Medical/Wounding/Systems/WoundSystem.Wounding.cs @@ -0,0 +1,389 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Body.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Medical.Wounding.Components; +using Content.Shared.Medical.Wounding.Events; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Wounding.Systems; + +public sealed partial class WoundSystem +{ + private const float DefaultSeverity = 100; + private const float SplatterDamageMult = 1.0f; + private const float NonCoreDamageChance = 0.5f; + private const float HeadDamageChance = 0.2f; + private const float ChanceForPartSelection = 0.50f; + + private void InitWounding() + { + SubscribeLocalEvent(OnWoundInsertAttempt); + SubscribeLocalEvent(OnWoundRemoved); + } + + #region Utility + + public IEnumerable> GetAllWounds(Entity woundable) + { + if (!_containerSystem.TryGetContainer(woundable, WoundableComponent.WoundableContainerId, out var container)) + { + Log.Error($"Wound container could not be found for {ToPrettyString(woundable)}! This should never happen!"); + yield break; + } + foreach (var entId in container.ContainedEntities) + { + yield return new Entity(entId, Comp(entId)); + } + } + + /// + /// Checks whether adding a wound to a woundable entity would conflict with another wound it currently has. + /// + /// The wound to check against. + /// The woundable entity + /// The conflicting wound if found. + /// Returns true if the wound conflicts. + private bool ConflictsWithUniqueWound(Entity wound, Entity woundable, + [NotNullWhen(true)] out Entity? conflictingWound) + { + conflictingWound = null; + + if (wound.Comp.Unique) + return false; + + if (!TryComp(wound, out var meta) || meta.EntityPrototype == null) + return false; + + return ConflictsWithUniqueWound(meta.EntityPrototype.ID, woundable, out conflictingWound); + } + + /// + /// Checks whether adding a wound to a woundable entity would conflict with another wound it currently has. + /// + /// The name of the wound prototype to check against. + /// The woundable entity + /// The conflicting unique wound if found. + /// Returns true if the wound conflicts. + private bool ConflictsWithUniqueWound(string woundProto, + Entity woundable, + [NotNullWhen(true)] out Entity? conflictingWound) + { + conflictingWound = null; + var allWounds = GetAllWounds(woundable); + + foreach (var otherWound in allWounds) + { + // Assumption: Since this wound is unique, then any other wound we are looking for is also unique. + // So we don't need to try to get the meta component for other wounds that are not unique. + if (otherWound.Comp.Unique && + TryComp(otherWound.Owner, out var otherMeta) && + otherMeta.EntityPrototype?.ID == woundProto) + { + conflictingWound = otherWound; + return true; + } + } + + return false; + } + + #endregion + + + #region WoundDestruction + public void RemoveWound(Entity wound, Entity? woundableParent) + { + if (_netManager.IsClient) + return; //TempHack to prevent removal from running on the client, wounds are updated from the network + + if (woundableParent != null) + { + if (wound.Comp.ParentWoundable != woundableParent.Value.Owner) + { + Log.Error($"{ToPrettyString(woundableParent.Value.Owner)} does not match the parent woundable on {ToPrettyString(wound.Owner)}"); + return; + } + } + else + { + if (!TryComp(wound.Comp.ParentWoundable, out var woundable)) + { + Log.Error($"{ToPrettyString(wound.Comp.ParentWoundable)} Does not have a woundable component this should never happen!"); + return; + } + + woundableParent = new(wound.Comp.ParentWoundable, woundable); + } + + if (!_containerSystem.TryGetContainer(woundableParent.Value, WoundableComponent.WoundableContainerId, + out var container) + || !_containerSystem.Remove(new(wound, null, null), container, reparent: false)) + { + Log.Error($"Failed to remove wound from {ToPrettyString(wound.Comp.ParentWoundable)}, this should never happen!"); + } + } + #endregion + + #region WoundCreation + + public bool CreateWoundOnWoundable(Entity woundable, EntProtoId woundProtoId) + { + return CreateWoundOnWoundable(woundable, woundProtoId, 100); + } + + public bool CreateWoundOnWoundable(Entity woundable, EntProtoId woundProtoId, + FixedPoint2 severity) + { + if (!Resolve(woundable, ref woundable.Comp)) + return false; + AddWound(new Entity(woundable, woundable.Comp), woundProtoId, severity, + out var wound); + return wound != null; + } + + public void CreateWoundsFromDamage(Entity woundable, DamageSpecifier damageSpec) + { + if (!Resolve(woundable, ref woundable.Comp)) + return; + var validWoundable = new Entity(woundable, woundable.Comp); + foreach (var (damageTypeId, damage) in damageSpec.DamageDict) + { + //If damage is negative (healing) skip because wound healing is handled with internal logic. + if (damage < 0) + continue; + + var gotWound = TryGetWoundProtoFromDamage(validWoundable, new(damageTypeId), damage, + out var protoId, out var overflow, out var conflicting); + + if (conflicting != null) + SetWoundSeverity(conflicting.Value, 1, true); + + if (!gotWound) + return; + + AddWound(validWoundable, protoId!, DefaultSeverity); + } + } + + private void AddWound(Entity woundable, EntProtoId woundProtoId, FixedPoint2 severity) + { + AddWound(woundable, woundProtoId, severity, out _); + } + + private void AddWound(Entity woundable, EntProtoId woundProtoId, FixedPoint2 severity, out Entity? newWound) + { + newWound = null; + if (_netManager.IsClient) + return; //TempHack to prevent addition from running on the client, wounds are updated from the network + + newWound = CreateWound(woundProtoId, severity); + var attempt = new CreateWoundAttemptEvent(woundable, newWound.Value); + RaiseLocalEvent(woundable, ref attempt); + if (woundable.Comp.Body != null) + RaiseLocalEvent(woundable.Comp.Body.Value, ref attempt); + if (attempt.Canceled) + { + //if we aren't adding this wound, nuke it because it's not being attached to anything. + QueueDel(newWound); + newWound = null; + return; + } + if (!_containerSystem.TryGetContainer(woundable, WoundableComponent.WoundableContainerId, out var container) + || !_containerSystem.Insert(new(newWound.Value.Owner, null, null, null), container) + ) + { + Log.Error($"{ToPrettyString(woundable.Owner)} does not have a woundable container, or insertion is not possible! This should never happen!"); + newWound = null; + return; + } + } + + /// + /// Create a new wound in nullspace from a wound entity prototype id. + /// + /// ProtoId of our wound + /// The severity the wound will start with + /// + private Entity CreateWound(EntProtoId woundProtoId, FixedPoint2 severity) + { + var newEnt = Spawn(woundProtoId); + var newWound = new Entity(newEnt, Comp(newEnt)); + //Do not raise a severity changed event because we will handle that when the wound gets attached + SetWoundSeverity(newWound, severity, false); + return newWound; + } + + /// + /// Tries to get the appropriate wound for the specified damage type and damage amount + /// + /// Woundable Entity/comp + /// Damage type to check + /// Damage being applied + /// Found WoundProtoId + /// The amount of damage exceeding the max cap + /// A conflicting wound if found. This will not be null when the wound that would + /// otherwise be returned was skipped because it would conflict with this wound. + /// True if a woundProto is found, false if not + public bool TryGetWoundProtoFromDamage(Entity woundable,ProtoId damageType, FixedPoint2 damage, + [NotNullWhen(true)] out string? woundProtoId, out FixedPoint2 damageOverflow, out Entity? conflictingWound) + { + damageOverflow = 0; + woundProtoId = null; + conflictingWound = null; + if (!woundable.Comp.Config.TryGetValue(damageType, out var metadata)) + return false; + //scale the incoming damage and calculate overflows + var adjDamage = damage * metadata.Scaling; + if (adjDamage > metadata.DamageMax) + { + damageOverflow = adjDamage - metadata.DamageMax; + adjDamage = metadata.DamageMax; + } + var percentageOfMax = adjDamage / metadata.DamageMax *100; + var woundPool = _prototypeManager.Index(metadata.WoundPool); + Entity? conflicting = null; + var retConflicting = false; + foreach (var (percentage, lastWoundProtoId) in woundPool.Wounds) + { + if (percentage >= percentageOfMax) + break; + + if (ConflictsWithUniqueWound(lastWoundProtoId, woundable, out conflicting)) + { + retConflicting = true; + continue; + } + + woundProtoId = lastWoundProtoId; + retConflicting = false; + } + + if (retConflicting) + conflictingWound = conflicting; + + return woundProtoId != null; + } + + #endregion + + #region Severity + + + /// + /// Forcibly set a wound's severity, this does NOT raise a cancellable event, and should only be used internally. + /// This exists for performance reasons to reduce the amount of events being raised. + /// Or in situations where cancelling should not be allowed. + /// + /// Target wound + /// New Severity + /// Should we raise a severity changed event + private void SetWoundSeverity(Entity wound, FixedPoint2 severity, bool raiseEvent) + { + var oldSev = severity; + wound.Comp.Severity = severity; + if (!raiseEvent) + return; + var ev = new WoundSeverityChangedEvent(wound, oldSev); + RaiseLocalEvent(wound, ref ev); + Dirty(wound); + } + + /// + /// Sets a wound's severity in a cancellable way. Generally avoid using this when you can and use AddWoundSeverity instead! + /// AddWoundSeverity DOES NOT raise a cancellable event, only set does to prevent systems from overwriting each other! + /// + /// Target wound + /// New severity value + /// True if the severity was successfully set, false if not + public bool TrySetWoundSeverity(Entity wound, FixedPoint2 severity) + { + var attempt = new SetWoundSeverityAttemptEvent(wound, severity); + RaiseLocalEvent(wound, ref attempt); + if (attempt.Cancel) + return false; + SetWoundSeverity(wound, severity, true); + return true; + } + + /// + /// Add/Remove from a wounds severity value. Note: Severity is clamped between 0 and 100. + /// + /// Target wound + /// Severity value we are adding/removing + public void ChangeWoundSeverity(Entity wound, FixedPoint2 severityDelta) + { + SetWoundSeverity(wound, FixedPoint2.Clamp(wound.Comp.Severity+severityDelta, 0 , 100), true); + } + + #endregion + + #region ContainerEvents + + private void OnWoundInsertAttempt(EntityUid woundEnt, WoundComponent wound, ref ContainerGettingInsertedAttemptEvent args) + { + if (args.Container.ID != WoundableComponent.WoundableContainerId) + { + Log.Error("Tried to add wound to a container that is NOT woundableContainer"); + args.Cancel(); + return; + } + if (!TryComp(args.Container.Owner, out var woundable)) + { + Log.Error("Tried to add a wound to an entity without a woundable!"); + args.Cancel(); + return; + } + + if (ConflictsWithUniqueWound(new Entity(woundEnt, wound), + new Entity(args.Container.Owner, woundable), out _)) + { + Log.Error("Tried to add a wound to entity which would conflict with a unique wound!"); + args.Cancel(); + return; + } + + wound.ParentWoundable = args.Container.Owner; + var ev = new WoundCreatedEvent( + new (args.Container.Owner,woundable), + new(woundEnt, wound)); + RaiseLocalEvent(args.Container.Owner, ref ev); + RaiseLocalEvent(woundEnt, ref ev); + if (woundable.Body != null) + { + var ev2 = new WoundAppliedToBody( + new (woundable.Body.Value, Comp(woundable.Body.Value)), + new (args.Container.Owner,woundable), + new(woundEnt, wound)); + RaiseLocalEvent(woundable.Body.Value ,ref ev2); + RaiseLocalEvent(woundEnt ,ref ev2); + } + Dirty(woundEnt, wound); + } + + private void OnWoundRemoved(EntityUid woundEnt, WoundComponent wound, ref EntGotRemovedFromContainerMessage args) + { + var woundable = Comp(args.Container.Owner); + var ev = new WoundDestroyedEvent( + new(args.Container.Owner, woundable), + new (woundEnt, wound) + ); + RaiseLocalEvent(woundEnt, ref ev); + RaiseLocalEvent(args.Container.Owner, ref ev); + if (woundable.Body != null) + { + var ev2 = new WoundRemovedFromBody( + new (woundable.Body.Value, Comp(woundable.Body.Value)), + new (args.Container.Owner,woundable), + new(woundEnt, wound)); + RaiseLocalEvent(woundable.Body.Value ,ref ev2); + RaiseLocalEvent(woundEnt ,ref ev2); + } + if (!_netManager.IsClient) + QueueDel(woundEnt); //Wounds should never exist outside of a container + } + #endregion +} diff --git a/Content.Shared/Medical/Wounding/Systems/WoundSystem.cs b/Content.Shared/Medical/Wounding/Systems/WoundSystem.cs new file mode 100644 index 00000000000000..6d2ab105332959 --- /dev/null +++ b/Content.Shared/Medical/Wounding/Systems/WoundSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Body.Systems; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Content.Shared.Gibbing.Systems; +using Content.Shared.Medical.Wounding.Components; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Medical.Wounding.Systems; + +public sealed partial class WoundSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly GibbingSystem _gibbingSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedBodySystem _bodySystem = default!; + [Dependency] private readonly INetManager _netManager = default!; + + + private TimeSpan _healingUpdateRate = new TimeSpan(0,0,1); + + public override void Initialize() + { + if (!_netManager.IsClient) + SubscribeLocalEvent(WoundableInit); + InitWounding(); + InitBodyListeners(); + InitDamage(); + } + + public override void Update(float frameTime) + { + } + + private void WoundableInit(EntityUid owner, WoundableComponent woundable, ref MapInitEvent args) + { + woundable.HealthCap = woundable.MaxHealth; + woundable.IntegrityCap = woundable.MaxIntegrity; + if (woundable.Health < 0) + woundable.Health = woundable.HealthCap; + if (woundable.Integrity <= 0) + woundable.Integrity = woundable.IntegrityCap; + _containerSystem.EnsureContainer(owner, WoundableComponent.WoundableContainerId); + Dirty(owner,woundable); + } + + public void ChangeIntegrity(Entity woundable, FixedPoint2 deltaIntegrity) + { + woundable.Comp.Integrity += deltaIntegrity; + ValidateWoundable(woundable); + } + + public void ChangeIntegrityCap(Entity woundable, FixedPoint2 deltaIntegrityCap) + { + woundable.Comp.IntegrityCap += deltaIntegrityCap; + ValidateWoundable(woundable); + } + + public void ChangeHealthCap(Entity woundable, FixedPoint2 deltaHealthCap) + { + woundable.Comp.HealthCap += deltaHealthCap; + ValidateWoundable(woundable); + } + +} diff --git a/Resources/Prototypes/Body/Organs/Animal/animal.yml b/Resources/Prototypes/Body/Organs/Animal/animal.yml index 2f50821df353a3..9cb07f3fbb11da 100644 --- a/Resources/Prototypes/Body/Organs/Animal/animal.yml +++ b/Resources/Prototypes/Body/Organs/Animal/animal.yml @@ -41,15 +41,17 @@ - state: lung-l - state: lung-r - type: Organ - - type: Lung - - type: Metabolizer - removeEmpty: true - solutionOnBody: false - solution: "Lung" - metabolizerTypes: [ Animal ] - groups: - - id: Gas - rateModifier: 100.0 + # TODO Lungs: reimplement +# - type: Lung + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# removeEmpty: true +# solutionOnBody: false +# solution: "Lung" +# metabolizerTypes: [ Animal ] +# groups: +# - id: Gas +# rateModifier: 100.0 - type: SolutionContainerManager solutions: Lung: @@ -79,13 +81,15 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 5 - - type: Stomach - - type: Metabolizer - maxReagents: 3 - metabolizerTypes: [ Animal ] - groups: - - id: Food - - id: Drink +#TODO Stomach: Reimplement this +# - type: Stomach + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 3 +# metabolizerTypes: [ Animal ] +# groups: +# - id: Food +# - id: Drink - type: entity id: OrganMouseStomach @@ -107,12 +111,13 @@ - type: Sprite state: liver - type: Organ - - type: Metabolizer - maxReagents: 1 - metabolizerTypes: [ Animal ] - groups: - - id: Alcohol - rateModifier: 0.1 + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 1 +# metabolizerTypes: [ Animal ] +# groups: +# - id: Alcohol +# rateModifier: 0.1 - type: entity id: OrganAnimalHeart @@ -123,13 +128,14 @@ - type: Sprite state: heart-on - type: Organ - - type: Metabolizer - maxReagents: 2 - metabolizerTypes: [ Animal ] - groups: - - id: Medicine - - id: Poison - - id: Narcotic +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 2 +# metabolizerTypes: [ Animal ] +# groups: +# - id: Medicine +# - id: Poison +# - id: Narcotic - type: entity id: OrganAnimalKidneys @@ -142,7 +148,8 @@ - state: kidney-l - state: kidney-r - type: Organ - - type: Metabolizer - maxReagents: 5 - metabolizerTypes: [ Animal ] - removeEmpty: true +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 5 +# metabolizerTypes: [ Animal ] +# removeEmpty: true diff --git a/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml b/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml index 8a1afc37bb151e..a67faf6b5312d8 100644 --- a/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml +++ b/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml @@ -3,24 +3,27 @@ parent: OrganAnimalStomach name: stomach noSpawn: true - components: - - type: Metabolizer - metabolizerTypes: [ Bloodsucker ] +# components: + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [ Bloodsucker ] - type: entity id: OrganBloodsuckerLiver parent: OrganAnimalLiver name: liver noSpawn: true - components: - - type: Metabolizer - metabolizerTypes: [ Bloodsucker ] +# components: + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [ Bloodsucker ] - type: entity id: OrganBloodsuckerHeart parent: OrganAnimalHeart name: heart noSpawn: true - components: - - type: Metabolizer - metabolizerTypes: [ Bloodsucker ] +# components: + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [ Bloodsucker ] diff --git a/Resources/Prototypes/Body/Organs/Animal/slimes.yml b/Resources/Prototypes/Body/Organs/Animal/slimes.yml index f1a3d47e6673a9..7ce48cb1a14a4d 100644 --- a/Resources/Prototypes/Body/Organs/Animal/slimes.yml +++ b/Resources/Prototypes/Body/Organs/Animal/slimes.yml @@ -7,19 +7,21 @@ - type: Sprite sprite: Mobs/Species/Slime/organs.rsi state: brain-slime - - type: Stomach - - type: Metabolizer - maxReagents: 3 - metabolizerTypes: [ Slime ] - removeEmpty: true - groups: - - id: Food - - id: Drink - - id: Medicine - - id: Poison - - id: Narcotic - - id: Alcohol - rateModifier: 0.2 + #TODO Stomach: Reimplement this +# - type: Stomach + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 3 +# metabolizerTypes: [ Slime ] +# removeEmpty: true +# groups: +# - id: Food +# - id: Drink +# - id: Medicine +# - id: Poison +# - id: Narcotic +# - id: Alcohol +# rateModifier: 0.2 - type: SolutionContainerManager solutions: stomach: @@ -36,16 +38,18 @@ layers: - state: lung-l-slime - state: lung-r-slime - - type: Lung - alert: LowNitrogen - - type: Metabolizer - removeEmpty: true - solutionOnBody: false - solution: "Lung" - metabolizerTypes: [ Slime ] - groups: - - id: Gas - rateModifier: 100.0 + # TODO Lungs: reimplement +# - type: Lung +# alert: LowNitrogen + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# removeEmpty: true +# solutionOnBody: false +# solution: "Lung" +# metabolizerTypes: [ Slime ] +# groups: +# - id: Gas +# rateModifier: 100.0 - type: SolutionContainerManager solutions: organ: diff --git a/Resources/Prototypes/Body/Organs/arachnid.yml b/Resources/Prototypes/Body/Organs/arachnid.yml index c1e199e1121eaa..226c2270e743c8 100644 --- a/Resources/Prototypes/Body/Organs/arachnid.yml +++ b/Resources/Prototypes/Body/Organs/arachnid.yml @@ -34,8 +34,9 @@ - type: Sprite sprite: Mobs/Species/Arachnid/organs.rsi state: stomach - - type: Stomach - updateInterval: 1.5 + #TODO Stomach: Reimplement this +# - type: Stomach +# updateInterval: 1.5 - type: SolutionContainerManager solutions: stomach: @@ -45,8 +46,9 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 5 - - type: Metabolizer - updateInterval: 1.5 +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# updateInterval: 1.5 - type: entity id: OrganArachnidLungs @@ -58,30 +60,32 @@ layers: - state: lung-l - state: lung-r - - type: Lung - - type: Metabolizer - updateInterval: 1.5 - removeEmpty: true - solutionOnBody: false - solution: "Lung" - metabolizerTypes: [ Human ] - groups: - - id: Gas - rateModifier: 100.0 - - type: SolutionContainerManager - solutions: - organ: - reagents: - - ReagentId: Nutriment - Quantity: 10 - Lung: - maxVol: 100.0 - canReact: false - food: - maxVol: 5 - reagents: - - ReagentId: UncookedAnimalProteins - Quantity: 5 +# TODO Lungs: reimplement +# - type: Lung +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# updateInterval: 1.5 +# removeEmpty: true +# solutionOnBody: false +# solution: "Lung" +# metabolizerTypes: [ Human ] +# groups: +# - id: Gas +# rateModifier: 100.0 +# - type: SolutionContainerManager +# solutions: +# organ: +# reagents: +# - ReagentId: Nutriment +# Quantity: 10 +# Lung: +# maxVol: 100.0 +# canReact: false +# food: +# maxVol: 5 +# reagents: +# - ReagentId: UncookedAnimalProteins +# Quantity: 5 - type: entity id: OrganArachnidHeart @@ -91,14 +95,15 @@ components: - type: Sprite state: heart-on - - type: Metabolizer - updateInterval: 1.5 - maxReagents: 2 - metabolizerTypes: [Arachnid] - groups: - - id: Medicine - - id: Poison - - id: Narcotic +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# updateInterval: 1.5 +# maxReagents: 2 +# metabolizerTypes: [Arachnid] +# groups: +# - id: Medicine +# - id: Poison +# - id: Narcotic - type: entity id: OrganArachnidLiver @@ -109,13 +114,14 @@ components: - type: Sprite state: liver - - type: Metabolizer # The liver metabolizes certain chemicals only, like alcohol. - updateInterval: 1.5 - maxReagents: 1 - metabolizerTypes: [Animal] - groups: - - id: Alcohol - rateModifier: 0.1 # removes alcohol very slowly along with the stomach removing it as a drink + #TODO Metabolism: Reimplement this +# - type: Metabolizer # The liver metabolizes certain chemicals only, like alcohol. +# updateInterval: 1.5 +# maxReagents: 1 +# metabolizerTypes: [Animal] +# groups: +# - id: Alcohol +# rateModifier: 0.1 # removes alcohol very slowly along with the stomach removing it as a drink - type: entity id: OrganArachnidKidneys @@ -129,11 +135,12 @@ - state: kidney-l - state: kidney-r # The kidneys just remove anything that doesn't currently have any metabolisms, as a stopgap. - - type: Metabolizer - updateInterval: 1.5 - maxReagents: 5 - metabolizerTypes: [Animal] - removeEmpty: true +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# updateInterval: 1.5 +# maxReagents: 5 +# metabolizerTypes: [Animal] +# removeEmpty: true - type: entity id: OrganArachnidEyes diff --git a/Resources/Prototypes/Body/Organs/base.yml b/Resources/Prototypes/Body/Organs/base.yml new file mode 100644 index 00000000000000..dd6740e3cb424a --- /dev/null +++ b/Resources/Prototypes/Body/Organs/base.yml @@ -0,0 +1,53 @@ +- type: entity + id: BaseOrganUnGibbable + parent: BaseItem + abstract: true + components: + - type: Sprite + sprite: Mobs/Species/Human/organs.rsi + - type: Organ + - type: Food + - type: Extractable + grindableSolutionName: organ + - type: SolutionContainerManager + solutions: + organ: + reagents: + - ReagentId: Nutriment + Quantity: 10 + food: + maxVol: 5 + reagents: + - ReagentId: UncookedAnimalProteins + Quantity: 5 + - type: FlavorProfile + flavors: + - people + - type: Tag + tags: + - Meat + - type: Metabolizer + metabolismType: Human + baseMultiplier: 1 + - type: Damageable + damageContainer: Biological + - type: Woundable + maxHealth: 20 + maxIntegrity: 5 + config: + Piercing: + damageMax: 25 + woundPool: CommonPiercing + Blunt: + damageMax: 25 + woundPool: CommonBlunt + Slash: + damageMax: 25 + woundPool: CommonSlash + +- type: entity + id: BaseOrgan + parent: BaseOrganUnGibbable + abstract: true + components: + - type: Gibbable diff --git a/Resources/Prototypes/Body/Organs/diona.yml b/Resources/Prototypes/Body/Organs/diona.yml index 69fc630b9e4d5b..29e5353d3c7db8 100644 --- a/Resources/Prototypes/Body/Organs/diona.yml +++ b/Resources/Prototypes/Body/Organs/diona.yml @@ -77,19 +77,21 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 5 - - type: Stomach - - type: Metabolizer - maxReagents: 6 - metabolizerTypes: [ Plant ] - removeEmpty: true - groups: - - id: Food - - id: Drink - - id: Medicine - - id: Poison - - id: Narcotic - - id: Alcohol - rateModifier: 0.1 +#TODO Stomach: Reimplement this +# - type: Stomach +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 6 +# metabolizerTypes: [ Plant ] +# removeEmpty: true +# groups: +# - id: Food +# - id: Drink +# - id: Medicine +# - id: Poison +# - id: Narcotic +# - id: Alcohol +# rateModifier: 0.1 - type: entity id: OrganDionaLungs @@ -102,15 +104,17 @@ layers: - state: lung-l - state: lung-r - - type: Lung - - type: Metabolizer - removeEmpty: true - solutionOnBody: false - solution: "Lung" - metabolizerTypes: [ Plant ] - groups: - - id: Gas - rateModifier: 100.0 + # TODO Lungs: reimplement +# - type: Lung + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# removeEmpty: true +# solutionOnBody: false +# solution: "Lung" +# metabolizerTypes: [ Plant ] +# groups: +# - id: Gas +# rateModifier: 100.0 - type: SolutionContainerManager solutions: organ: @@ -131,7 +135,7 @@ description: "The source of incredible, unending intelligence. Honk." components: - type: Brain - - type: Nymph # This will make the organs turn into a nymph when they're removed. + - type: Nymph # This will make the organs turn into a nymph when they're removed. entityPrototype: OrganDionaNymphBrain transferMind: true @@ -170,11 +174,11 @@ - type: entity id: OrganDionaNymphStomach - parent: MobDionaNymphAccent + parent: MobDionaNymphAccent noSpawn: true name: diona nymph suffix: Stomach - description: Contains the stomach of a formerly fully-formed Diona. It doesn't taste any better for it. + description: Contains the stomach of a formerly fully-formed Diona. It doesn't taste any better for it. components: - type: IsDeadIC - type: Body @@ -186,7 +190,7 @@ noSpawn: true name: diona nymph suffix: Lungs - description: Contains the lungs of a formerly fully-formed Diona. Breathtaking. + description: Contains the lungs of a formerly fully-formed Diona. Breathtaking. components: - type: IsDeadIC - type: Body diff --git a/Resources/Prototypes/Body/Organs/dwarf.yml b/Resources/Prototypes/Body/Organs/dwarf.yml index 8da0cb1666f92f..0c6bc61d6b4600 100644 --- a/Resources/Prototypes/Body/Organs/dwarf.yml +++ b/Resources/Prototypes/Body/Organs/dwarf.yml @@ -2,17 +2,19 @@ id: OrganDwarfHeart parent: OrganHumanHeart name: dwarf heart - components: - - type: Metabolizer - metabolizerTypes: [Dwarf] +# components: +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [Dwarf] - type: entity id: OrganDwarfLiver parent: OrganHumanLiver name: dwarf liver - components: - - type: Metabolizer - metabolizerTypes: [Dwarf] +# components: +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [Dwarf] - type: entity id: OrganDwarfStomach @@ -31,8 +33,10 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 5 - - type: Stomach - - type: Metabolizer - # mm very yummy - maxReagents: 5 - metabolizerTypes: [Dwarf] + #TODO Stomach: Reimplement this +# - type: Stomach + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# # mm very yummy +# maxReagents: 5 +# metabolizerTypes: [Dwarf] diff --git a/Resources/Prototypes/Body/Organs/human.yml b/Resources/Prototypes/Body/Organs/human.yml index 69081020ce0bcd..5da57125a81ce5 100644 --- a/Resources/Prototypes/Body/Organs/human.yml +++ b/Resources/Prototypes/Body/Organs/human.yml @@ -1,14 +1,10 @@ - type: entity id: BaseHumanOrganUnGibbable - parent: BaseItem + parent: BaseOrganUnGibbable abstract: true components: - type: Sprite sprite: Mobs/Species/Human/organs.rsi - - type: Organ - - type: Food - - type: Extractable - grindableSolutionName: organ - type: SolutionContainerManager solutions: organ: @@ -29,10 +25,8 @@ - type: entity id: BaseHumanOrgan - parent: BaseHumanOrganUnGibbable + parent: BaseOrgan abstract: true - components: - - type: Gibbable - type: entity id: OrganHumanBrain @@ -40,6 +34,7 @@ name: brain description: "The source of incredible, unending intelligence. Honk." components: + - type: ConsciousnessProvider - type: Sprite state: brain - type: Organ @@ -67,7 +62,7 @@ - type: FlavorProfile flavors: - people - + - type: entity id: OrganHumanEyes parent: BaseHumanOrgan @@ -118,15 +113,19 @@ layers: - state: lung-l - state: lung-r - - type: Lung - - type: Metabolizer - removeEmpty: true - solutionOnBody: false - solution: "Lung" - metabolizerTypes: [ Human ] - groups: - - id: Gas - rateModifier: 100.0 + - type: Lungs + metabolismType: Human + # TODO Lungs: reimplement +# - type: Lung +# TODO Metabolism: Reimplement this +# - type: Metabolizer +# removeEmpty: true +# solutionOnBody: false +# solution: "Lung" +# metabolizerTypes: [ Human ] +# groups: +# - id: Gas +# rateModifier: 100.0 - type: SolutionContainerManager solutions: organ: @@ -153,13 +152,14 @@ # The heart 'metabolizes' medicines and poisons that aren't filtered out by other organs. # This is done because these chemicals need to have some effect even if they aren't being filtered out of your body. # You're technically 'immune to poison' without a heart, but.. uhh, you'll have bigger problems on your hands. - - type: Metabolizer - maxReagents: 2 - metabolizerTypes: [Human] - groups: - - id: Medicine - - id: Poison - - id: Narcotic +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 2 +# metabolizerTypes: [Human] +# groups: +# - id: Medicine +# - id: Poison +# - id: Narcotic - type: entity id: OrganHumanStomach @@ -178,17 +178,19 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 5 - - type: Stomach + #TODO Stomach: Reimplement this +# - type: Stomach # The stomach metabolizes stuff like foods and drinks. # TODO: Have it work off of the ent's solution container, and move this # to intestines instead. - - type: Metabolizer - # mm yummy - maxReagents: 3 - metabolizerTypes: [Human] - groups: - - id: Food - - id: Drink +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# # mm yummy +# maxReagents: 3 +# metabolizerTypes: [Human] +# groups: +# - id: Food +# - id: Drink - type: entity id: OrganHumanLiver @@ -198,12 +200,13 @@ components: - type: Sprite state: liver - - type: Metabolizer # The liver metabolizes certain chemicals only, like alcohol. - maxReagents: 1 - metabolizerTypes: [Human] - groups: - - id: Alcohol - rateModifier: 0.1 # removes alcohol very slowly along with the stomach removing it as a drink + #TODO Metabolism: Reimplement this +# - type: Metabolizer # The liver metabolizes certain chemicals only, like alcohol. +# maxReagents: 1 +# metabolizerTypes: [Human] +# groups: +# - id: Alcohol +# rateModifier: 0.1 # removes alcohol very slowly along with the stomach removing it as a drink - type: entity id: OrganHumanKidneys @@ -216,7 +219,8 @@ - state: kidney-l - state: kidney-r # The kidneys just remove anything that doesn't currently have any metabolisms, as a stopgap. - - type: Metabolizer - maxReagents: 5 - metabolizerTypes: [Human] - removeEmpty: true +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 5 +# metabolizerTypes: [Human] +# removeEmpty: true diff --git a/Resources/Prototypes/Body/Organs/moth.yml b/Resources/Prototypes/Body/Organs/moth.yml index aef5576048d224..cbcad71f9ae6df 100644 --- a/Resources/Prototypes/Body/Organs/moth.yml +++ b/Resources/Prototypes/Body/Organs/moth.yml @@ -3,11 +3,12 @@ parent: [OrganAnimalStomach, OrganHumanStomach] noSpawn: true components: - - type: Stomach - specialDigestible: - tags: - - ClothMade - - Paper +#TODO Stomach: Reimplement this +# - type: Stomach +# specialDigestible: +# tags: +# - ClothMade +# - Paper - type: SolutionContainerManager solutions: stomach: @@ -17,10 +18,11 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 5 - - type: Metabolizer - maxReagents: 3 - metabolizerTypes: [ Moth ] - removeEmpty: true - groups: - - id: Food - - id: Drink +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 3 +# metabolizerTypes: [ Moth ] +# removeEmpty: true +# groups: +# - id: Food +# - id: Drink diff --git a/Resources/Prototypes/Body/Organs/rat.yml b/Resources/Prototypes/Body/Organs/rat.yml index 9d1352c72f5e9b..bd26eddfacf941 100644 --- a/Resources/Prototypes/Body/Organs/rat.yml +++ b/Resources/Prototypes/Body/Organs/rat.yml @@ -2,9 +2,10 @@ id: OrganRatLungs parent: OrganHumanLungs suffix: "rat" - components: - - type: Metabolizer - metabolizerTypes: [ Rat ] +# components: +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [ Rat ] - type: entity id: OrganRatStomach diff --git a/Resources/Prototypes/Body/Organs/reptilian.yml b/Resources/Prototypes/Body/Organs/reptilian.yml index f8423582cc2868..5495886214ab12 100644 --- a/Resources/Prototypes/Body/Organs/reptilian.yml +++ b/Resources/Prototypes/Body/Organs/reptilian.yml @@ -1,17 +1,18 @@ - type: entity id: OrganReptilianStomach - parent: OrganAnimalStomach + parent: OrganHumanStomach noSpawn: true components: - - type: Stomach - specialDigestible: - tags: - - Fruit - - ReptilianFood - - Meat - - Pill - - Crayon - - Paper + #TODO Stomach: Reimplement this +# - type: Stomach +# specialDigestible: +# tags: +# - Fruit +# - ReptilianFood +# - Meat +# - Pill +# - Crayon +# - Paper - type: SolutionContainerManager solutions: stomach: diff --git a/Resources/Prototypes/Body/Organs/slime.yml b/Resources/Prototypes/Body/Organs/slime.yml index 3da76c5d4aacc4..21ee7f82e547f9 100644 --- a/Resources/Prototypes/Body/Organs/slime.yml +++ b/Resources/Prototypes/Body/Organs/slime.yml @@ -7,34 +7,36 @@ - type: Sprite sprite: Mobs/Species/Slime/organs.rsi state: brain-slime - - type: Stomach - - type: Metabolizer - maxReagents: 6 - metabolizerTypes: [ Slime ] - removeEmpty: true - groups: - - id: Food - - id: Drink - - id: Medicine - - id: Poison - - id: Narcotic - - id: Alcohol - rateModifier: 0.25 - - type: SolutionContainerManager - solutions: - stomach: - maxVol: 50.0 - food: - maxVol: 5 - reagents: - - ReagentId: GreyMatter - Quantity: 5 - organ: - reagents: - - ReagentId: Slime - Quantity: 10 +#TODO Stomach: Reimplement this +# - type: Stomach +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# maxReagents: 6 +# metabolizerTypes: [ Slime ] +# removeEmpty: true +# groups: +# - id: Food +# - id: Drink +# - id: Medicine +# - id: Poison +# - id: Narcotic +# - id: Alcohol +# rateModifier: 0.25 +# - type: SolutionContainerManager +# solutions: +# stomach: +# maxVol: 50.0 +# food: +# maxVol: 5 +# reagents: +# - ReagentId: GreyMatter +# Quantity: 5 +# organ: +# reagents: +# - ReagentId: Slime +# Quantity: 10 + - - type: entity id: OrganSlimeLungs parent: BaseHumanOrgan @@ -46,16 +48,18 @@ layers: - state: lung-l-slime - state: lung-r-slime - - type: Lung - alert: LowNitrogen - - type: Metabolizer - removeEmpty: true - solutionOnBody: false - solution: "Lung" - metabolizerTypes: [ Slime ] - groups: - - id: Gas - rateModifier: 100.0 +# TODO Lungs: reimplement +# - type: Lung +# alert: LowNitrogen + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# removeEmpty: true +# solutionOnBody: false +# solution: "Lung" +# metabolizerTypes: [ Slime ] +# groups: +# - id: Gas +# rateModifier: 100.0 - type: SolutionContainerManager solutions: organ: diff --git a/Resources/Prototypes/Body/Organs/vox.yml b/Resources/Prototypes/Body/Organs/vox.yml index 1b4d12116f86f0..bbd007098e307c 100644 --- a/Resources/Prototypes/Body/Organs/vox.yml +++ b/Resources/Prototypes/Body/Organs/vox.yml @@ -2,8 +2,10 @@ id: OrganVoxLungs parent: OrganHumanLungs suffix: "vox" - components: - - type: Metabolizer - metabolizerTypes: [ Vox ] - - type: Lung - alert: LowNitrogen +# components: +#TODO Metabolism: Reimplement this +# - type: Metabolizer +# metabolizerTypes: [ Vox ] +# TODO Lungs: reimplement +# - type: Lung +# alert: LowNitrogen diff --git a/Resources/Prototypes/Body/Parts/base.yml b/Resources/Prototypes/Body/Parts/base.yml index 836d0f140afe89..33e70c2b360d5d 100644 --- a/Resources/Prototypes/Body/Parts/base.yml +++ b/Resources/Prototypes/Body/Parts/base.yml @@ -6,9 +6,23 @@ name: "body part" abstract: true components: + - type: BodyPart - type: Damageable damageContainer: Biological - - type: BodyPart + - type: Woundable + maxHealth: 50 + maxIntegrity: 10 + config: + Piercing: + damageMax: 60 + woundPool: CommonPiercing + Blunt: + damageMax: 60 + woundPool: CommonBlunt + Slash: + damageMax: 60 + woundPool: CommonSlash + - type: Gibbable - type: ContainerContainer containers: diff --git a/Resources/Prototypes/Body/Prototypes/reptilian.yml b/Resources/Prototypes/Body/Prototypes/reptilian.yml index 1e9ebd54a48a8b..b1d650196ee72e 100644 --- a/Resources/Prototypes/Body/Prototypes/reptilian.yml +++ b/Resources/Prototypes/Body/Prototypes/reptilian.yml @@ -13,10 +13,10 @@ torso: part: TorsoReptilian organs: - heart: OrganAnimalHeart + heart: OrganHumanHeart lungs: OrganHumanLungs stomach: OrganReptilianStomach - liver: OrganAnimalLiver + liver: OrganHumanLiver kidneys: OrganHumanKidneys connections: - right arm diff --git a/Resources/Prototypes/Chemistry/metabolizer_types.yml b/Resources/Prototypes/Chemistry/metabolizer_types.yml index 3f7bf05b35e150..47cc0c9c4b318f 100644 --- a/Resources/Prototypes/Chemistry/metabolizer_types.yml +++ b/Resources/Prototypes/Chemistry/metabolizer_types.yml @@ -1,6 +1,7 @@ # If your species wants to metabolize stuff differently, # you'll likely have to tag its metabolizers with something other than Human. +#TODO Metabolism: Remove these and replace with absorptions - type: metabolizerType id: Animal name: metabolizer-type-animal diff --git a/Resources/Prototypes/Damage/types.yml b/Resources/Prototypes/Damage/types.yml index 0107da24823b8d..cd6d01a1313a6c 100644 --- a/Resources/Prototypes/Damage/types.yml +++ b/Resources/Prototypes/Damage/types.yml @@ -21,19 +21,19 @@ name: damage-type-blunt armorCoefficientPrice: 2 armorFlatPrice: 10 - + - type: damageType id: Cellular name: damage-type-cellular armorCoefficientPrice: 5 armorFlatPrice: 30 - + - type: damageType id: Caustic name: damage-type-caustic armorCoefficientPrice: 5 armorFlatPrice: 30 - + - type: damageType id: Cold name: damage-type-cold diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index a79fbfbf246f06..2c070d1e3637fb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -48,7 +48,8 @@ - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: ReplacementAccent accent: mouse - type: MeleeWeapon @@ -110,7 +111,8 @@ - Bee - Trash - type: Bloodstream - bloodMaxVolume: 0.1 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 0.1 - type: MobPrice price: 50 - type: NPCRetaliation @@ -221,7 +223,8 @@ interactSuccessSound: path: /Audio/Animals/chicken_cluck_happy.ogg - type: Bloodstream - bloodMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 100 - type: EggLayer eggSpawn: - id: FoodEgg @@ -344,8 +347,9 @@ Dead: Base: cockroach_dead - type: Bloodstream - bloodReagent: InsectBlood - bloodMaxVolume: 20 + #TODO Bloodstream: re-implement this +# bloodReagent: InsectBlood +# bloodMaxVolume: 20 - type: Food - type: Hunger baseDecayRate: 0.25 @@ -485,14 +489,16 @@ damageContainer: Biological damageModifierSet: Moth - type: Bloodstream - bloodReagent: InsectBlood - - type: Respirator - damage: - types: - Asphyxiation: 0.5 - damageRecovery: - types: - Asphyxiation: -0.5 + #TODO Bloodstream: re-implement this +# bloodReagent: InsectBlood + #TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.5 +# damageRecovery: +# types: +# Asphyxiation: -0.5 - type: CombatMode - type: Butcherable spawned: @@ -599,7 +605,8 @@ interactSuccessSound: path: /Audio/Animals/duck_quack_happy.ogg - type: Bloodstream - bloodMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 100 - type: EggLayer eggSpawn: - id: FoodEgg @@ -704,7 +711,8 @@ Dead: Base: dead - type: Bloodstream - bloodMaxVolume: 0.1 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 0.1 - type: MobPrice price: 50 @@ -844,8 +852,9 @@ - type: ReplacementAccent accent: crab - type: Bloodstream - bloodMaxVolume: 50 - bloodReagent: CopperBlood + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 +# bloodReagent: CopperBlood - type: Tag tags: - VimPilot @@ -987,7 +996,8 @@ interactSuccessSound: path: /Audio/Animals/goose_honk.ogg - type: Bloodstream - bloodMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 100 - type: NpcFactionMember factions: - Passive @@ -1029,7 +1039,8 @@ - id: FoodMeat amount: 4 - type: Bloodstream - bloodMaxVolume: 300 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 300 # if you fuck with the gorilla he will harambe you - type: MeleeWeapon soundHit: @@ -1611,6 +1622,14 @@ reagents: - ReagentId: UncookedAnimalProteins Quantity: 3 + bloodstream: + maxVol: 50 + bloodReagents: + maxVol: 50 + bloodSpill: + maxVol: 9999 + - type: Bloodstream + maxVolume: 50 - type: Butcherable spawned: - id: FoodMeatRat @@ -1624,13 +1643,14 @@ - ChefPilot - Mouse - Meat - - type: Respirator - damage: - types: - Asphyxiation: 0.25 - damageRecovery: - types: - Asphyxiation: -0.25 + #TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.25 +# damageRecovery: +# types: +# Asphyxiation: -0.25 - type: Barotrauma damage: types: @@ -1644,8 +1664,8 @@ # TODO: Remove CombatMode when Prototype Composition is added - type: CombatMode combatToggleAction: ActionCombatModeToggleOff - - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: CanEscapeInventory - type: MobPrice price: 50 @@ -1821,7 +1841,8 @@ interactSuccessSound: path: /Audio/Animals/lizard_happy.ogg - type: Bloodstream - bloodMaxVolume: 150 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 150 - type: Damageable damageContainer: Biological damageModifierSet: Scale @@ -1873,7 +1894,8 @@ interactFailureString: petting-failure-generic interactSuccessSpawn: EffectHearts - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: entity name: frog @@ -1927,7 +1949,8 @@ interactSuccessSound: path: /Audio/Animals/frog_ribbit.ogg - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: Tag tags: - VimPilot @@ -1984,7 +2007,8 @@ interactSuccessSound: path: /Audio/Animals/parrot_raught.ogg - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: entity name: penguin @@ -2155,7 +2179,8 @@ interactFailureString: petting-failure-generic interactSuccessSpawn: EffectHearts - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: Damageable damageContainer: Biological damageModifierSet: Scale @@ -2245,8 +2270,9 @@ - type: Spider - type: IgnoreSpiderWeb - type: Bloodstream - bloodMaxVolume: 150 - bloodReagent: CopperBlood + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 150 +# bloodReagent: CopperBlood - type: Speech speechVerb: Arachnid speechSounds: Arachnid @@ -2345,8 +2371,9 @@ - type: Speech speechVerb: Cluwne - type: Bloodstream - bloodMaxVolume: 150 - bloodReagent: Laughter + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 150 +# bloodReagent: Laughter - type: entity name: wizard spider @@ -2535,7 +2562,8 @@ attributes: gender: epicene - type: Bloodstream - bloodMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 100 - type: MeleeWeapon angle: 0 animation: WeaponArcBite @@ -2657,7 +2685,8 @@ attributes: gender: epicene - type: Bloodstream - bloodReagent: DemonsBlood + #TODO Bloodstream: re-implement this +# bloodReagent: DemonsBlood - type: Damageable damageContainer: Biological damageModifierSet: Infernal @@ -2862,8 +2891,9 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/cat_meow.ogg - - type: Respirator #It just works? - minSaturation: 5.0 +#TODO respiration: reimplement this +# - type: Respirator #It just works? +# minSaturation: 5.0 - type: entity name: caracal cat @@ -3124,13 +3154,14 @@ - Trash - Hamster - Meat - - type: Respirator - damage: - types: - Asphyxiation: 0.25 - damageRecovery: - types: - Asphyxiation: -0.25 +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.25 +# damageRecovery: +# types: +# Asphyxiation: -0.25 - type: Barotrauma damage: types: @@ -3152,7 +3183,8 @@ interactSuccessSound: path: /Audio/Animals/fox_squeak.ogg - type: Bloodstream - bloodMaxVolume: 60 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 60 - type: CanEscapeInventory baseResistTime: 3 - type: MobPrice @@ -3256,8 +3288,9 @@ speciesId: cat templateId: pet - type: Bloodstream - bloodReagent: Sap - bloodMaxVolume: 60 + #TODO Bloodstream: re-implement this +# bloodReagent: Water +# bloodMaxVolume: 60 - type: DamageStateVisuals states: Alive: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 3b6c4e8ed92081..76c1a2021fc69f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -18,16 +18,17 @@ - type: ReplacementAccent accent: xeno - type: Bloodstream - bloodReagent: FerrochromicAcid - bloodMaxVolume: 75 #we don't want the map to become pools of blood - bloodlossDamage: - types: - Bloodloss: - 0.5 - bloodlossHealDamage: - types: - Bloodloss: - -1 + #TODO Bloodstream: re-implment this +# bloodReagent: FerrochromicAcid +# bloodMaxVolume: 75 #we don't want the map to become pools of blood +# bloodlossDamage: +# types: +# Bloodloss: +# 0.5 +# bloodlossHealDamage: +# types: +# Bloodloss: +# -1 - type: Insulated - type: CombatMode - type: MeleeWeapon diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml index 6f9935d351d39e..b5e7a0663d75a1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml @@ -85,13 +85,14 @@ thresholds: 0: Alive 500: Dead - - type: Metabolizer - solutionOnBody: false - updateInterval: 0.25 - metabolizerTypes: [ Dragon ] - groups: - - id: Medicine - - id: Poison + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# solutionOnBody: false +# updateInterval: 0.25 +# metabolizerTypes: [ Dragon ] +# groups: +# - id: Medicine +# - id: Poison - type: Butcherable spawned: - id: MaterialBananium1 @@ -116,8 +117,9 @@ - type: Input context: "human" - type: Bloodstream - bloodMaxVolume: 300 - bloodReagent: Laughter + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 300 +# bloodReagent: Laughter - type: entity name: behonker diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index ba21ca4da23edc..f8209cc1a2e3b7 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -261,8 +261,9 @@ speedModifierThresholds: 50: 0.4 - type: Bloodstream - bloodReagent: Water - chemicalMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodReagent: Water +# chemicalMaxVolume: 100 - type: StatusEffects allowed: - SlowedDown @@ -331,7 +332,8 @@ suffix: Beer components: - type: Bloodstream - bloodReagent: Beer + #TODO Bloodstream: re-implement this +# bloodReagent: Beer - type: PointLight color: "#cfa85f" - type: Sprite @@ -368,7 +370,8 @@ suffix: Nocturine components: - type: Bloodstream - bloodReagent: Nocturine + #TODO Bloodstream: re-implement this +# bloodReagent: Nocturine - type: PointLight color: "#128e80" - type: Sprite @@ -388,7 +391,8 @@ suffix: THC components: - type: Bloodstream - bloodReagent: THC + #TODO Bloodstream: re-implement this +# bloodReagent: THC - type: PointLight color: "#808080" - type: Sprite @@ -405,7 +409,8 @@ suffix: Bicaridine components: - type: Bloodstream - bloodReagent: Bicaridine + #TODO Bloodstream: re-implement this +# bloodReagent: Bicaridine - type: PointLight color: "#ffaa00" - type: Sprite @@ -422,7 +427,8 @@ suffix: Toxin components: - type: Bloodstream - bloodReagent: Toxin + #TODO Bloodstream: re-implement this +# bloodReagent: Toxin - type: PointLight color: "#cf3600" - type: Sprite @@ -439,7 +445,8 @@ suffix: Napalm components: - type: Bloodstream - bloodReagent: Napalm + #TODO Bloodstream: re-implement this +# bloodReagent: Napalm - type: PointLight color: "#FA00AF" - type: Sprite @@ -456,7 +463,8 @@ suffix: Omnizine components: - type: Bloodstream - bloodReagent: Omnizine + #TODO Bloodstream: re-implement this +# bloodReagent: Omnizine - type: PointLight color: "#fcf7f9" - type: Sprite @@ -473,7 +481,8 @@ suffix: Mute Toxin components: - type: Bloodstream - bloodReagent: MuteToxin + #TODO Bloodstream: re-implement this +# bloodReagent: MuteToxin - type: PointLight color: "#0f0f0f" - type: Sprite @@ -490,7 +499,8 @@ suffix: Norepinephric Acid components: - type: Bloodstream - bloodReagent: NorepinephricAcid + #TODO Bloodstream: re-implement this +# bloodReagent: NorepinephricAcid - type: PointLight color: "#96a8b5" - type: Sprite @@ -507,7 +517,8 @@ suffix: Ephedrine components: - type: Bloodstream - bloodReagent: Ephedrine + #TODO Bloodstream: re-implement this +# bloodReagent: Ephedrine - type: PointLight color: "#D2FFFA" - type: Sprite @@ -524,7 +535,8 @@ suffix: Robust Harvest components: - type: Bloodstream - bloodReagent: RobustHarvest + #TODO Bloodstream: re-implement this +# bloodReagent: RobustHarvest - type: PointLight color: "#3e901c" - type: Sprite diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml index 06ab02dedc906c..8267b8152e30a7 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml @@ -42,7 +42,8 @@ - id: FoodMeat amount: 1 - type: Bloodstream - bloodMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 100 - type: CombatMode - type: MeleeWeapon soundHit: @@ -240,7 +241,8 @@ - id: FoodMeat amount: 1 - type: Bloodstream - bloodMaxVolume: 100 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 100 - type: CombatMode - type: MeleeWeapon soundHit: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 718c4236cff579..23432b419a15a2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -718,10 +718,11 @@ - type: Damageable damageModifierSet: SlimePet - type: Bloodstream - bloodlossHealDamage: - types: - Bloodloss: - -0.8 + #TODO Bloodstream: re-implement this +# bloodlossHealDamage: +# types: +# Bloodloss: +# -0.8 - type: Temperature heatDamageThreshold: 800 coldDamageThreshold: 0 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 4936d883e578c1..bf75bd77c67816 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -3,6 +3,7 @@ parent: - BaseMob - MobDamageable + - MobDamageThresholds - MobAtmosExposed id: BaseSimpleMob suffix: AI @@ -37,7 +38,7 @@ parent: - BaseSimpleMob - MobCombat - - MobBloodstream + - MobSimpleBloodstream - MobFlammable id: SimpleSpaceMobBase # Mob without barotrauma, freezing and asphyxiation (for space carps!?) suffix: AI @@ -68,6 +69,7 @@ parent: - MobRespirator - MobAtmosStandard + - MobSimpleBloodstream - SimpleSpaceMobBase id: SimpleMobBase # for air breathers suffix: AI @@ -101,8 +103,6 @@ - Pacified - StaminaModifier - Flashed - - type: Bloodstream - bloodMaxVolume: 150 - type: MobPrice price: 150 - type: FloatingVisuals diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index d68415992ad18c..0ea14e5c8a6ad2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -43,27 +43,29 @@ spawned: - id: FoodMeatSlime amount: 2 - - type: Respirator - damage: - types: - Asphyxiation: 0.2 - damageRecovery: - types: - Asphyxiation: -1.0 - maxSaturation: 15 +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.2 +# damageRecovery: +# types: +# Asphyxiation: -1.0 +# maxSaturation: 15 - type: Damageable damageContainer: Biological damageModifierSet: Slime - type: Bloodstream - bloodReagent: Slime - bloodlossDamage: - types: - Bloodloss: - 0.5 - bloodlossHealDamage: - types: - Bloodloss: - -0.25 + #TODO Bloodstream: re-implement this +# bloodReagent: Slime +# bloodlossDamage: +# types: +# Bloodloss: +# 0.5 +# bloodlossHealDamage: +# types: +# Bloodloss: +# -0.25 - type: Barotrauma damage: types: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 91f5e952e92087..1340e2a2151e0c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -39,8 +39,9 @@ critThreshold: 150 - type: MovementAlwaysTouching - type: Bloodstream - bloodMaxVolume: 300 - bloodReagent: Cryoxadone + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 300 +# bloodReagent: Cryoxadone - type: CombatMode - type: Temperature heatDamageThreshold: 500 @@ -211,8 +212,9 @@ amount: 1 prob: 0.5 - type: Bloodstream - bloodMaxVolume: 250 - bloodReagent: Cryoxadone + #TODO Bloodstream: re-implement this + #bloodMaxVolume: 250 + #bloodReagent: Cryoxadone - type: Fixtures fixtures: fix1: @@ -314,8 +316,9 @@ amount: 1 prob: 0.3 - type: Bloodstream - bloodMaxVolume: 200 - bloodReagent: Cryoxadone + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 200 +# bloodReagent: Cryoxadone - type: Fixtures fixtures: fix1: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml index 0a2b4f80bbcd34..8400482612a9de 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml @@ -56,7 +56,8 @@ - id: FoodMeatXeno amount: 1 - type: Bloodstream - bloodMaxVolume: 50 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 50 - type: CombatMode - type: MeleeWeapon soundHit: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 9fb0be02aec9a5..b98a366678134b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -67,8 +67,9 @@ - type: Stamina critThreshold: 200 - type: Bloodstream - bloodReagent: FluorosulfuricAcid - bloodMaxVolume: 650 + #TODO Bloodstream: re-implement this +# bloodReagent: FluorosulfuricAcid +# bloodMaxVolume: 650 - type: MeleeWeapon altDisarm: false angle: 0 diff --git a/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml index d9dea3c18d9880..9718210473a681 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml @@ -3,11 +3,12 @@ name: Urist McWeb parent: BaseMobArachnid id: MobArachnid - components: - - type: Respirator - damage: - types: - Asphyxiation: 1.5 # This makes space and crit more lethal to arachnids. - damageRecovery: - types: - Asphyxiation: -0.5 + #components: + #TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 1.5 # This makes space and crit more lethal to arachnids. +# damageRecovery: +# types: +# Asphyxiation: -0.5 diff --git a/Resources/Prototypes/Entities/Mobs/Player/diona.yml b/Resources/Prototypes/Entities/Mobs/Player/diona.yml index 4153250bbf9b8a..996d1e20927822 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/diona.yml @@ -3,14 +3,15 @@ name: Urist McPlants parent: BaseMobDiona id: MobDiona - components: - - type: Respirator - damage: - types: - Asphyxiation: 0.5 - damageRecovery: - types: - Asphyxiation: -1.0 +# components: +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.5 +# damageRecovery: +# types: +# Asphyxiation: -1.0 # Reformed Diona - type: entity @@ -20,4 +21,4 @@ name: Reformed Diona components: - type: IsDeadIC - - type: RandomHumanoidAppearance \ No newline at end of file + - type: RandomHumanoidAppearance diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 8bf9bfab41e2f4..929d1bcf1744c3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -7,7 +7,8 @@ abstract: true components: - type: Bloodstream - bloodMaxVolume: 650 + #TODO Bloodstream: re-implement this +# bloodMaxVolume: 650 - type: GhostRole allowMovement: true allowSpeech: true @@ -92,19 +93,28 @@ speedModifierThresholds: 250: 0.7 400: 0.5 + #TODO Metabolism: Reimplement this +# - type: Metabolizer +# solutionOnBody: false +# updateInterval: 0.25 +# metabolizerTypes: [ Dragon ] +# groups: +# - id: Medicine +# - id: Poison # disable taking damage from fire, since its a fire breathing dragon - type: Flammable damage: types: {} - type: Temperature heatDamageThreshold: 800 - - type: Metabolizer - solutionOnBody: false - updateInterval: 0.25 - metabolizerTypes: [ Dragon ] - groups: - - id: Medicine - - id: Poison +# TODO metabolism: fix this +# - type: Metabolizer +# solutionOnBody: false +# updateInterval: 0.25 +# metabolizerTypes: [ Dragon ] +# groups: +# - id: Medicine +# - id: Poison - type: Butcherable spawned: - id: FoodMeatDragon diff --git a/Resources/Prototypes/Entities/Mobs/Player/gingerbread.yml b/Resources/Prototypes/Entities/Mobs/Player/gingerbread.yml index 18ff8381d4fb85..c5991b3067e5a9 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/gingerbread.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/gingerbread.yml @@ -3,11 +3,12 @@ name: Urist McCookie parent: BaseMobGingerbread id: MobGingerbread - components: - - type: Respirator - damage: - types: - Asphyxiation: 0.5 - damageRecovery: - types: - Asphyxiation: -1.0 +# components: + #TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.5 +# damageRecovery: +# types: +# Asphyxiation: -1.0 diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml index 8b3c66d5dd18f4..44c145729303ee 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml @@ -44,7 +44,8 @@ - !type:WashCreamPieReaction # Damage (Self) - type: Bloodstream - bloodReagent: CopperBlood + #TODO Bloodstream: re-implement this +# bloodReagent: CopperBlood # Damage (Others) - type: MeleeWeapon animation: WeaponArcBite diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index d956f1871d78de..430cbfaafeb39a 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -3,6 +3,10 @@ parent: - BaseMob - MobDamageable + - MobConsciousness + - MobWoundable + - MobMetabolism + - MobPain - MobCombat - StripableInventoryBase id: BaseMobSpecies @@ -275,13 +279,14 @@ spawned: - id: FoodMeat amount: 5 - - type: Respirator - damage: - types: - Asphyxiation: 1.0 - damageRecovery: - types: - Asphyxiation: -1.0 +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 1.0 +# damageRecovery: +# types: +# Asphyxiation: -1.0 - type: FireVisuals alternateState: Standing diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 7edb3a19dbad6f..46c0ed2a4e48ff 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -31,7 +31,8 @@ - id: FoodMeatPlant amount: 5 - type: Bloodstream - bloodReagent: Sap + #TODO Bloodstream: re-implement this +# bloodReagent: Water - type: Reactive groups: Flammable: [ Touch ] diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index 5a54b56c48e0f3..f19fc243a2d799 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -10,13 +10,14 @@ - type: Icon sprite: Mobs/Species/Slime/parts.rsi # It was like this beforehand, no idea why. state: full - - type: Respirator - damage: - types: - Asphyxiation: 2 - damageRecovery: - types: - Asphyxiation: -1.0 + #TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 2 +# damageRecovery: +# types: +# Asphyxiation: -1.0 - type: Sprite noRot: true drawdepth: Mobs diff --git a/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml b/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml index c514a6f1a050bf..95492e487e25dd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml @@ -27,7 +27,8 @@ - id: FoodBakedCookie #should be replaced with gingerbread sheets or something... provided you're willing to make a full spriteset of those. amount: 5 - type: Bloodstream - bloodReagent: Sugar + #TODO Bloodstream: re-implement this +# bloodReagent: Sugar - type: Fixtures fixtures: fix1: diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index f6fde849efebee..8b0cca06327eaf 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -33,7 +33,8 @@ - id: FoodMeat amount: 5 - type: Bloodstream - bloodReagent: InsectBlood + #TODO Bloodstream: re-implement this +# bloodReagent: InsectBlood - type: DamageVisuals damageOverlayGroups: Brute: diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index caa3690e5d20af..adb049977f3b21 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -69,7 +69,8 @@ sprite: Mobs/Effects/brute_damage.rsi color: "#2cf274" - type: Bloodstream - bloodReagent: Slime # TODO Color slime blood based on their slime color or smth + #TODO Bloodstream: re-implement this +# bloodReagent: Slime # TODO Color slime blood based on their slime color or smth - type: Barotrauma damage: types: @@ -102,14 +103,15 @@ spawned: - id: FoodMeatSlime amount: 5 - - type: Respirator - damage: - types: - Asphyxiation: 0.2 - damageRecovery: - types: - Asphyxiation: -1.0 - maxSaturation: 15 +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.2 +# damageRecovery: +# types: +# Asphyxiation: -1.0 +# maxSaturation: 15 - type: entity parent: MobHumanDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/vox.yml b/Resources/Prototypes/Entities/Mobs/Species/vox.yml index ec8035563b725d..f0c09d981f8f49 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/vox.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/vox.yml @@ -50,7 +50,8 @@ sprite: Mobs/Effects/brute_damage.rsi color: "#7a8bf2" - type: Bloodstream - bloodReagent: AmmoniaBlood + #TODO Bloodstream: re-implement this +# bloodReagent: AmmoniaBlood - type: MeleeWeapon soundHit: collection: AlienClaw diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index fae47113107d8c..c4b65c2d6ab1b5 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -46,6 +46,50 @@ - type: RequireProjectileTarget active: False +- type: entity + save: false + id: MobPain + abstract: true + components: + - type: Nerves + +- type: entity + save: false + id: MobMetabolism + abstract: true + components: + - type: Metabolism + metabolismType: Human + baseMultiplier: 1 + calorieBuffer: 1500 + calorieStorage: 80000 + +- type: entity + save: false + id: MobConsciousness + abstract: true + components: + - type: Consciousness + threshold: 30 + +- type: entity + save: false + id: MobDamageThresholds + abstract: true + components: + - type: MobThresholds + thresholds: + 0: Alive + 100: Critical + 200: Dead + +- type: entity + save: false + id: MobWoundable + abstract: true + components: + - type: MedicalData #TODO: move this to it's own prototype category + # Used for mobs that have health and can take damage. - type: entity save: false @@ -80,11 +124,6 @@ - type: RadiationReceiver - type: Stamina - type: MobState - - type: MobThresholds - thresholds: - 0: Alive - 100: Critical - 200: Dead - type: MobStateActions actions: Critical: @@ -206,27 +245,43 @@ abstract: true components: - type: Internals - - type: Respirator - damage: - types: - Asphyxiation: 2 - damageRecovery: - types: - Asphyxiation: -1.0 +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 2 +# damageRecovery: +# types: +# Asphyxiation: -1.0 # Used for mobs that have a bloodstream - type: entity save: false id: MobBloodstream + parent: MobSimpleBloodstream + abstract: true + #TODO vascular/bloodstream: reimplment this +# components: +# - type: VascularSystem +# bloodDefinition: HumanBlood + +# Used for mobs that have a simplified bloodstream +# this does not track blood types or have logic for pulse/pressure +# use this for mobs that do not use body system or do not use advanced body/organ simulation +- type: entity + save: false + id: MobSimpleBloodstream abstract: true components: - type: SolutionContainerManager + solutions: + bloodstream: + maxVol: 250 + bloodReagents: + maxVol: 250 + bloodSpill: + maxVol: 9999 - type: InjectableSolution - solution: chemicals + solution: bloodstream - type: Bloodstream - bloodlossDamage: - types: - Bloodloss: 0.5 - bloodlossHealDamage: - types: - Bloodloss: -1 + maxVolume: 250 diff --git a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml index ca56ef5acbb78e..a1c5a9721bb00c 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml @@ -249,13 +249,14 @@ reagents: - ReagentId: Protein Quantity: 2 - - type: Respirator - damage: - types: - Asphyxiation: 0.25 - damageRecovery: - types: - Asphyxiation: -0.25 +#TODO respiration: reimplement this +# - type: Respirator +# damage: +# types: +# Asphyxiation: 0.25 +# damageRecovery: +# types: +# Asphyxiation: -0.25 - type: entity name: dark haze diff --git a/Resources/Prototypes/Medical/Blood/BloodAntigen/human.yml b/Resources/Prototypes/Medical/Blood/BloodAntigen/human.yml new file mode 100644 index 00000000000000..f3b308128cb875 --- /dev/null +++ b/Resources/Prototypes/Medical/Blood/BloodAntigen/human.yml @@ -0,0 +1,13 @@ +# There are a fuck-ton more antigens than these but we only really care about ABO + RH factor +# other blood-types are generally to rare to bother caring about. + +- type: bloodAntigen + id: HumanA + +- type: bloodAntigen + id: HumanB + +# I know RH factor technically isn't an antigen, it's a protein but there is no reason to implement a +# separate system just for RH factor +- type: bloodAntigen + id: HumanD diff --git a/Resources/Prototypes/Medical/Blood/BloodType/human.yml b/Resources/Prototypes/Medical/Blood/BloodType/human.yml new file mode 100644 index 00000000000000..5335df07a94bc3 --- /dev/null +++ b/Resources/Prototypes/Medical/Blood/BloodType/human.yml @@ -0,0 +1,81 @@ +# bloodtypes are labeled based on the antigens present on the bloodcells or O for none, +# and Pos or Neg for RHFactor (AKA whether the D antigen is present or not on bloodcells) +# any antigens that are not present on the bloodcells are found in the blood plasma +# EXCEPT for the D antigen, this is only ever on bloodcells if it is present at all (This simulates RH factor). + + + +- type: bloodType + id: HumanBloodType + abstract: true + wholeBloodReagent: Blood + bloodCellsReagent: Blood #TODO Bloodstream: bloodcells + bloodPlasmaReagent: Water #TODO Bloodstream: bloodPlasma + +- type: bloodType + id: HumanAPos + parent: HumanBloodType + bloodCellAntigens: + - HumanA + - HumanD + plasmaAntigens: + - HumanB + +- type: bloodType + id: HumanANeg + parent: HumanBloodType + bloodCellAntigens: + - HumanA + plasmaAntigens: + - HumanB + +- type: bloodType + id: HumanBPos + parent: HumanBloodType + bloodCellAntigens: + - HumanB + - HumanD + plasmaAntigens: + - HumanA + +- type: bloodType + id: HumanBNeg + parent: HumanBloodType + bloodCellAntigens: + - HumanB + plasmaAntigens: + - HumanA + +- type: bloodType + id: HumanABPos + parent: HumanBloodType + bloodCellAntigens: + - HumanB + - HumanA + - HumanD + plasmaAntigens: [] + +- type: bloodType + id: HumanABNeg + parent: HumanBloodType + bloodCellAntigens: + - HumanB + - HumanA + plasmaAntigens: [] + +- type: bloodType + id: HumanOPos + parent: HumanBloodType + bloodCellAntigens: + - HumanD + plasmaAntigens: + - HumanB + - HumanA + +- type: bloodType + id: HumanONeg + parent: HumanBloodType + bloodCellAntigens: [] + plasmaAntigens: + - HumanB + - HumanA diff --git a/Resources/Prototypes/Medical/Blood/bloodDefinitions.yml b/Resources/Prototypes/Medical/Blood/bloodDefinitions.yml new file mode 100644 index 00000000000000..b7425a9ffd03a8 --- /dev/null +++ b/Resources/Prototypes/Medical/Blood/bloodDefinitions.yml @@ -0,0 +1,12 @@ +- type: bloodDefinition + id: HumanBlood + bloodTypeDistribution: + HumanOPos: 37.4 + HumanONeg: 6.6 + HumanAPos: 35.7 + HumanANeg: 6.3 + HumanBPos: 8.5 + HumanBNeg: 1.5 + HumanABPos: 3.4 + HumanABNeg: 0.6 +#percentages sourced from: https://stanfordbloodcenter.org/donate-blood/blood-donation-facts/blood-types/ diff --git a/Resources/Prototypes/Medical/Metabolism/MetabolismTypes.yml b/Resources/Prototypes/Medical/Metabolism/MetabolismTypes.yml new file mode 100644 index 00000000000000..663053846271a5 --- /dev/null +++ b/Resources/Prototypes/Medical/Metabolism/MetabolismTypes.yml @@ -0,0 +1,27 @@ +- type: metabolismType + id: Human + absorbedGases: + "0" : + lowThreshold: 0.90 + highThreshold: 1.0 + wasteGases: + "2" : + lowThreshold: 0.05 + highThreshold: 0.30 + requiredReagents: + "Oxygen": 0.06 + "Sugar": 0.01 + wasteReagents: + "CarbonDioxide": 0.06 + deprivationDamage: + types: + Cellular: 0.05 + energyReagent: "Sugar" #TODO metabolism: add glucose and swap this to use it + #Average human blood sugar concentration is 0.055 mol/RU, converted this is 6.3514%. I rounded up to 6.5 because it's nicer. + #This makes the "healthy" glucose level for humans at 16.25 RU, if humans have 250 RU of blood. + targetEnergyReagentConcentration: 0.065 + + #Molar volume of glucose is 115.48ml/mole with a Caloric density of 686 KCal (https://www.sciencedirect.com/topics/nursing-and-health-professions/caloric-density) + #SS14 uses DeciLitres for units so 115.48ml is 1.1548 RU. 686/1.1548 = 594.0422584 which gives us RU per KCal, + #rounding it up to 595 to keep the numbers nicer + kCalPerEnergyReagent: 595 diff --git a/Resources/Prototypes/Medical/Wounds/Common/abrasions.yml b/Resources/Prototypes/Medical/Wounds/Common/abrasions.yml new file mode 100644 index 00000000000000..4dcaedb918e08a --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/abrasions.yml @@ -0,0 +1,18 @@ + +- type: entity + id: MinorAbrasion + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Abrasion + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereAbrasion + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/avulsions.yml b/Resources/Prototypes/Medical/Wounds/Common/avulsions.yml new file mode 100644 index 00000000000000..eacc630742b787 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/avulsions.yml @@ -0,0 +1,28 @@ +- type: entity + id: SurfaceAvulsion + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: MinorTissueAvulsion + parent: BaseHealingWound + components: + - type: Wound +- type: entity + id: TissueAvulsion + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereTissueAvulsion + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Amputation + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/base.yml b/Resources/Prototypes/Medical/Wounds/Common/base.yml new file mode 100644 index 00000000000000..960fd8aa317624 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/base.yml @@ -0,0 +1,16 @@ +- type: entity + id: BaseWound + abstract: true + components: + - type: Wound + +- type: entity + id: BaseHealingWound + parent: BaseWound + abstract: true + components: + - type: Healable + + + + diff --git a/Resources/Prototypes/Medical/Wounds/Common/blisters.yml b/Resources/Prototypes/Medical/Wounds/Common/blisters.yml new file mode 100644 index 00000000000000..ccb66e73693bac --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/blisters.yml @@ -0,0 +1,17 @@ +- type: entity + id: MinorBlister + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Blister + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereBlister + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/burns.yml b/Resources/Prototypes/Medical/Wounds/Common/burns.yml new file mode 100644 index 00000000000000..8cb71ab39a5c81 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/burns.yml @@ -0,0 +1,116 @@ +#base burn wounds +- type: entity + id: FirstDegreeBurnBase + parent: BaseHealingWound + abstract: true + components: + - type: Wound + +- type: entity + id: SecondDegreeBurnBase + parent: BaseHealingWound + abstract: true + components: + - type: Wound + +- type: entity + id: ThirdDegreeBurnBase + parent: BaseHealingWound + abstract: true + components: + - type: Wound + +# === Thermal === +- type: entity + id: FirstDegreeThermalBurn + parent: FirstDegreeBurnBase + components: + - type: Wound + +- type: entity + id: SecondDegreeThermalBurn + parent: SecondDegreeBurnBase + components: + - type: Wound + +- type: entity + id: ThirdDegreeThermalBurn + parent: ThirdDegreeBurnBase + components: + - type: Wound + +# === Frostbite === +- type: entity + id: FirstDegreeFrostbite + parent: FirstDegreeBurnBase + components: + - type: Wound + +- type: entity + id: SecondDegreeFrostbite + parent: SecondDegreeBurnBase + components: + - type: Wound + +- type: entity + id: ThirdDegreeFrostbite + parent: ThirdDegreeBurnBase + components: + - type: Wound + +# === Chemical === +- type: entity + id: FirstDegreeChemBurn + parent: FirstDegreeBurnBase + components: + - type: Wound + +- type: entity + id: SecondDegreeChemBurn + parent: SecondDegreeBurnBase + components: + - type: Wound + +- type: entity + id: ThirdDegreeChemBurn + parent: ThirdDegreeBurnBase + components: + - type: Wound + +# === Electrical === +- type: entity + id: FirstDegreeElectricalBurn + parent: FirstDegreeBurnBase + components: + - type: Wound + +- type: entity + id: SecondDegreeElectricalBurn + parent: SecondDegreeBurnBase + components: + - type: Wound + +- type: entity + id: ThirdDegreeElectricalBurn + parent: ThirdDegreeBurnBase + components: + - type: Wound + +# === Radiation === +- type: entity + id: FirstDegreeRadiationBurn + parent: FirstDegreeBurnBase + components: + - type: Wound + +- type: entity + id: SecondDegreeRadiationBurn + parent: SecondDegreeBurnBase + components: + - type: Wound + +- type: entity + id: ThirdDegreeRadiationBurn + parent: ThirdDegreeBurnBase + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/contusions.yml b/Resources/Prototypes/Medical/Wounds/Common/contusions.yml new file mode 100644 index 00000000000000..851117c491c948 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/contusions.yml @@ -0,0 +1,36 @@ + +- type: entity + id: MinorBruise + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Bruise + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereBruise + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: CriticalBruise + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Crush + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereCrush + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/incisions.yml b/Resources/Prototypes/Medical/Wounds/Common/incisions.yml new file mode 100644 index 00000000000000..bf49e91b78acdb --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/incisions.yml @@ -0,0 +1,17 @@ +- type: entity + id: MinorIncision + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Incision + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: LargeIncision + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/lacerations.yml b/Resources/Prototypes/Medical/Wounds/Common/lacerations.yml new file mode 100644 index 00000000000000..69b775d3ff6459 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/lacerations.yml @@ -0,0 +1,18 @@ +- type: entity + id: Cut + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: Laceration + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereLaceration + parent: BaseHealingWound + components: + - type: Wound + diff --git a/Resources/Prototypes/Medical/Wounds/Common/penetrations.yml b/Resources/Prototypes/Medical/Wounds/Common/penetrations.yml new file mode 100644 index 00000000000000..5a8b5105c652c1 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/penetrations.yml @@ -0,0 +1,17 @@ +- type: entity + id: MinorPenetratingWound + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: PenetratingWound + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SeverePenetratingWound + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/punctures.yml b/Resources/Prototypes/Medical/Wounds/Common/punctures.yml new file mode 100644 index 00000000000000..da3528c8b2bc4c --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/punctures.yml @@ -0,0 +1,17 @@ +- type: entity + id: MinorPunctureWound + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: PunctureWound + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SeverePunctureWound + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Common/velocity_wounds.yml b/Resources/Prototypes/Medical/Wounds/Common/velocity_wounds.yml new file mode 100644 index 00000000000000..b57dd57dc43aef --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Common/velocity_wounds.yml @@ -0,0 +1,17 @@ +- type: entity + id: MinorVelocityWound + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: VelocityWound + parent: BaseHealingWound + components: + - type: Wound + +- type: entity + id: SevereVelocityWound + parent: BaseHealingWound + components: + - type: Wound diff --git a/Resources/Prototypes/Medical/Wounds/Pools/Common/blunt.yml b/Resources/Prototypes/Medical/Wounds/Pools/Common/blunt.yml new file mode 100644 index 00000000000000..940a2b1c99d64b --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Pools/Common/blunt.yml @@ -0,0 +1,9 @@ +- type: woundPool + id: CommonBlunt + wounds: + 2: MinorBruise + 5: Bruise + 10: SevereBruise + 25: CriticalBruise + 75: Crush + 90: SevereCrush diff --git a/Resources/Prototypes/Medical/Wounds/Pools/Common/burn.yml b/Resources/Prototypes/Medical/Wounds/Pools/Common/burn.yml new file mode 100644 index 00000000000000..8b137891791fe9 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Pools/Common/burn.yml @@ -0,0 +1 @@ + diff --git a/Resources/Prototypes/Medical/Wounds/Pools/Common/piercing.yml b/Resources/Prototypes/Medical/Wounds/Pools/Common/piercing.yml new file mode 100644 index 00000000000000..3292f93981d76c --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Pools/Common/piercing.yml @@ -0,0 +1,15 @@ +- type: woundPool + id: CommonPiercing + wounds: + 2: MinorBruise + 4: Bruise + 6: SevereBruise + 8: CriticalBruise + 10: MinorPenetratingWound + 15: MinorPunctureWound + 20: PunctureWound + 35: PenetratingWound + 50: SeverePunctureWound + 65: PenetratingWound + 80: SeverePenetratingWound + 90: SevereTissueAvulsion diff --git a/Resources/Prototypes/Medical/Wounds/Pools/Common/slash.yml b/Resources/Prototypes/Medical/Wounds/Pools/Common/slash.yml new file mode 100644 index 00000000000000..8bde256d43de70 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/Pools/Common/slash.yml @@ -0,0 +1,13 @@ +- type: woundPool + id: CommonSlash + wounds: + 2: MinorAbrasion + 5: Abrasion + 10: SevereAbrasion + 25: Cut + 40: Laceration + 50: SevereLaceration + 60: SurfaceAvulsion + 70: MinorTissueAvulsion + 80: TissueAvulsion + 90: SevereTissueAvulsion diff --git a/Resources/Prototypes/Medical/Wounds/debug.yml b/Resources/Prototypes/Medical/Wounds/debug.yml new file mode 100644 index 00000000000000..f8d420bceed259 --- /dev/null +++ b/Resources/Prototypes/Medical/Wounds/debug.yml @@ -0,0 +1,48 @@ +- type: entity + id: InstaGibWound + parent: BaseWound + components: + - type: Wound + - type: IntegrityTrauma + integrityDecrease: 9999 #this will destroy any part + +- type: entity + id: PainWound + parent: BaseWound + components: + - type: Wound + - type: PainTrauma + painDecrease: 2 + + +- type: entity + id: MassivePainWound + parent: BaseWound + components: + - type: Wound + - type: PainTrauma + painDecrease: 50 + +- type: entity + id: MinorConsciousWound + parent: BaseWound + components: + - type: Wound + - type: ConsciousnessTrauma + decrease: 5 + +- type: entity + id: ConsciousCritWound + parent: BaseWound + components: + - type: Wound + - type: ConsciousnessTrauma + decrease: 70 + +- type: entity + id: ConsciousKillWound + parent: BaseWound + components: + - type: Wound + - type: ConsciousnessTrauma + decrease: 100