diff --git a/Content.Client/Backmen/Body/Components/BrainComponent.cs b/Content.Client/Backmen/Body/Components/BrainComponent.cs new file mode 100644 index 00000000000..0af31ab2679 --- /dev/null +++ b/Content.Client/Backmen/Body/Components/BrainComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Client.Backmen.Body.Components; + +[RegisterComponent] +public sealed partial class BrainComponent : Component; diff --git a/Content.Client/Backmen/Body/Components/LungComponent.cs b/Content.Client/Backmen/Body/Components/LungComponent.cs new file mode 100644 index 00000000000..7ce80e6ccb5 --- /dev/null +++ b/Content.Client/Backmen/Body/Components/LungComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Client.Backmen.Body.Components; + +[RegisterComponent] +public sealed partial class LungComponent : Component; diff --git a/Content.Client/Backmen/Body/Components/StomachComponent.cs b/Content.Client/Backmen/Body/Components/StomachComponent.cs new file mode 100644 index 00000000000..f791a71726f --- /dev/null +++ b/Content.Client/Backmen/Body/Components/StomachComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Client.Backmen.Body.Components; + +[RegisterComponent] +public sealed partial class StomachComponent : Component; diff --git a/Content.Client/Backmen/Surgery/SurgeryBui.cs b/Content.Client/Backmen/Surgery/SurgeryBui.cs new file mode 100644 index 00000000000..625bb00e0f3 --- /dev/null +++ b/Content.Client/Backmen/Surgery/SurgeryBui.cs @@ -0,0 +1,435 @@ +using Content.Client.Xenonids.UI; +using Content.Client.Administration.UI.CustomControls; +using Content.Shared.Backmen.Surgery; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Rotation; +using Content.Shared.Standing; +using Content.Client.Hands.Systems; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using Robust.Shared.Timing; +using Robust.Client.Timing; +using static Robust.Client.UserInterface.Control; + +namespace Content.Client.Backmen.Surgery; + +[UsedImplicitly] +public sealed class SurgeryBui : BoundUserInterface +{ + [Dependency] private readonly IEntityManager _entities = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + [Dependency] private readonly IClientGameTiming _gameTiming = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private readonly SurgerySystem _system; + private readonly HandsSystem _hands; + [ViewVariables] + private SurgeryWindow? _window; + + private EntityUid? _part; + private bool _isBody = false; + private (EntityUid Ent, EntProtoId Proto)? _surgery; + private readonly List _previousSurgeries = new(); + private DateTime _lastRefresh = DateTime.UtcNow; + private (string handName, EntityUid item) _throttling = ("", new EntityUid()); + public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + _system = _entities.System(); + _hands = _entities.System(); + + _system.OnStep += RefreshUI; + _hands.OnPlayerItemAdded += OnPlayerItemAdded; + } + + private void OnPlayerItemAdded(string handName, EntityUid item) + { + if (_throttling.handName.Equals(handName) + && _throttling.item.Equals(item) + && DateTime.UtcNow - _lastRefresh < TimeSpan.FromSeconds(0.2) + || !_timing.IsFirstTimePredicted + || _window == null + || !_window.IsOpen) + return; + + _throttling = (handName, item); + _lastRefresh = DateTime.UtcNow; + RefreshUI(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + if (state is SurgeryBuiState s) + Update(s); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _window?.Dispose(); + + _system.OnStep -= RefreshUI; + } + + private void Update(SurgeryBuiState state) + { + //Logger.Debug($"Attempting to update surgerybuistate with {state}, {_player.LocalEntity}, first predicted? {_timing.IsFirstTimePredicted}, surgeryTargetComp? {_entities.TryGetComponent(_player.LocalEntity, out var surgeryTargetComp2)} {surgeryTargetComp2?.CanOperate}"); + if (!_entities.TryGetComponent(_player.LocalEntity, out var surgeryTargetComp) + || !surgeryTargetComp.CanOperate) + return; + + if (_window == null) + { + _window = new SurgeryWindow(); + _window.OnClose += Close; + _window.Title = Loc.GetString("surgery-ui-window-title"); + + _window.PartsButton.OnPressed += _ => + { + _part = null; + _isBody = false; + _surgery = null; + _previousSurgeries.Clear(); + View(ViewType.Parts); + }; + + _window.SurgeriesButton.OnPressed += _ => + { + _surgery = null; + _previousSurgeries.Clear(); + + if (!_entities.TryGetNetEntity(_part, out var netPart) || + State is not SurgeryBuiState s || + !s.Choices.TryGetValue(netPart.Value, out var surgeries)) + { + return; + } + + OnPartPressed(netPart.Value, surgeries); + }; + + _window.StepsButton.OnPressed += _ => + { + if (!_entities.TryGetNetEntity(_part, out var netPart) || + _previousSurgeries.Count == 0) + { + return; + } + + var last = _previousSurgeries[^1]; + _previousSurgeries.RemoveAt(_previousSurgeries.Count - 1); + + if (_system.GetSingleton(last) is not { } previousId || + !_entities.TryGetComponent(previousId, out SurgeryComponent? previous)) + { + return; + } + + OnSurgeryPressed((previousId, previous), netPart.Value, last); + }; + } + + _window.Surgeries.DisposeAllChildren(); + _window.Steps.DisposeAllChildren(); + _window.Parts.DisposeAllChildren(); + View(ViewType.Parts); + + var oldSurgery = _surgery; + var oldPart = _part; + _part = null; + _surgery = null; + + var options = new List<(NetEntity netEntity, EntityUid entity, string Name, BodyPartType? PartType)>(); + foreach (var choice in state.Choices.Keys) + { + if (_entities.TryGetEntity(choice, out var ent)) + { + if (_entities.TryGetComponent(ent, out BodyPartComponent? part)) + options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, part.PartType)); + else if (_entities.TryGetComponent(ent, out BodyComponent? body)) + options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, null)); + } + } + + options.Sort((a, b) => + { + int GetScore(BodyPartType? partType) + { + return partType switch + { + BodyPartType.Head => 1, + BodyPartType.Torso => 2, + BodyPartType.Arm => 3, + BodyPartType.Hand => 4, + BodyPartType.Leg => 5, + BodyPartType.Foot => 6, + // BodyPartType.Tail => 7, No tails yet! + BodyPartType.Other => 8, + _ => 9 + }; + } + + return GetScore(a.PartType) - GetScore(b.PartType); + }); + + foreach (var (netEntity, entity, partName, _) in options) + { + //var netPart = _entities.GetNetEntity(part.Owner); + var surgeries = state.Choices[netEntity]; + var partButton = new XenoChoiceControl(); + + partButton.Set(partName, null); + partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries); + + _window.Parts.AddChild(partButton); + + foreach (var surgeryId in surgeries) + { + if (_system.GetSingleton(surgeryId) is not { } surgery || + !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp)) + continue; + + if (oldPart == entity && oldSurgery?.Proto == surgeryId) + OnSurgeryPressed((surgery, surgeryComp), netEntity, surgeryId); + } + + if (oldPart == entity && oldSurgery == null) + OnPartPressed(netEntity, surgeries); + } + + + if (!_window.IsOpen) + //Logger.Debug("Attempting to open"); + _window.OpenCentered(); + } + + private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId) + { + if (_window == null || + _system.GetSingleton(stepId) is not { } step) + { + return; + } + var stepName = new FormattedMessage(); + stepName.AddText(_entities.GetComponent(step).EntityName); + var stepButton = new SurgeryStepButton { Step = step }; + stepButton.Button.OnPressed += _ => SendMessage(new SurgeryStepChosenBuiMsg(netPart, surgeryId, stepId, _isBody)); + + _window.Steps.AddChild(stepButton); + } + + private void OnSurgeryPressed(Entity surgery, NetEntity netPart, EntProtoId surgeryId) + { + if (_window == null) + return; + + _part = _entities.GetEntity(netPart); + _isBody = _entities.HasComponent(_part); + _surgery = (surgery, surgeryId); + + _window.Steps.DisposeAllChildren(); + + // This apparently does not consider if theres multiple surgery requirements in one surgery. Maybe thats fine. + if (surgery.Comp.Requirement is { } requirementId && _system.GetSingleton(requirementId) is { } requirement) + { + var label = new XenoChoiceControl(); + label.Button.OnPressed += _ => + { + _previousSurgeries.Add(surgeryId); + + if (_entities.TryGetComponent(requirement, out SurgeryComponent? requirementComp)) + OnSurgeryPressed((requirement, requirementComp), netPart, requirementId); + }; + + var msg = new FormattedMessage(); + var surgeryName = _entities.GetComponent(requirement).EntityName; + msg.AddMarkup($"[bold]{Loc.GetString("surgery-ui-window-require")}: {surgeryName}[/bold]"); + label.Set(msg, null); + + _window.Steps.AddChild(label); + _window.Steps.AddChild(new HSeparator { Margin = new Thickness(0, 0, 0, 1) }); + } + foreach (var stepId in surgery.Comp.Steps) + { + AddStep(stepId, netPart, surgeryId); + } + + View(ViewType.Steps); + RefreshUI(); + } + + private void OnPartPressed(NetEntity netPart, List surgeryIds) + { + if (_window == null) + return; + + _part = _entities.GetEntity(netPart); + _isBody = _entities.HasComponent(_part); + _window.Surgeries.DisposeAllChildren(); + + var surgeries = new List<(Entity Ent, EntProtoId Id, string Name)>(); + foreach (var surgeryId in surgeryIds) + { + if (_system.GetSingleton(surgeryId) is not { } surgery || + !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp)) + { + continue; + } + + var name = _entities.GetComponent(surgery).EntityName; + surgeries.Add(((surgery, surgeryComp), surgeryId, name)); + } + + surgeries.Sort((a, b) => + { + var priority = a.Ent.Comp.Priority.CompareTo(b.Ent.Comp.Priority); + if (priority != 0) + return priority; + + return string.Compare(a.Name, b.Name, StringComparison.Ordinal); + }); + + foreach (var surgery in surgeries) + { + var surgeryButton = new XenoChoiceControl(); + surgeryButton.Set(surgery.Name, null); + + surgeryButton.Button.OnPressed += _ => OnSurgeryPressed(surgery.Ent, netPart, surgery.Id); + _window.Surgeries.AddChild(surgeryButton); + } + + RefreshUI(); + View(ViewType.Surgeries); + } + + private void RefreshUI() + { + if (_window == null + || !_window.IsOpen + || _part == null + || !_entities.HasComponent(_surgery?.Ent) + || !_entities.TryGetComponent(_player.LocalEntity ?? EntityUid.Invalid, out var surgeryComp) + || !surgeryComp.CanOperate) + { + return; + } + Logger.Debug($"Running RefreshUI on {Owner}"); + var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent); + var i = 0; + foreach (var child in _window.Steps.Children) + { + if (child is not SurgeryStepButton stepButton) + continue; + + var status = StepStatus.Incomplete; + if (next == null) + { + status = StepStatus.Complete; + } + else if (next.Value.Surgery.Owner != _surgery.Value.Ent) + { + status = StepStatus.Incomplete; + } + else if (next.Value.Step == i) + { + status = StepStatus.Next; + } + else if (i < next.Value.Step) + { + status = StepStatus.Complete; + } + + stepButton.Button.Disabled = status != StepStatus.Next; + + var stepName = new FormattedMessage(); + stepName.AddText(_entities.GetComponent(stepButton.Step).EntityName); + + if (status == StepStatus.Complete) + { + stepButton.Button.Modulate = Color.Green; + } + else + { + stepButton.Button.Modulate = Color.White; + if (_player.LocalEntity is { } player + && status == StepStatus.Next + && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _)) + { + stepButton.ToolTip = popup; + stepButton.Button.Disabled = true; + + switch (reason) + { + case StepInvalidReason.MissingSkills: + stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-skills")}[/color]"); + break; + case StepInvalidReason.NeedsOperatingTable: + stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-table")}[/color]"); + break; + case StepInvalidReason.Armor: + stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-armor")}[/color]"); + break; + case StepInvalidReason.MissingTool: + stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-tools")}[/color]"); + break; + } + } + } + + var texture = _entities.GetComponentOrNull(stepButton.Step)?.Icon?.Default; + stepButton.Set(stepName, texture); + i++; + } + } + + private void View(ViewType type) + { + if (_window == null) + return; + + _window.PartsButton.Parent!.Margin = new Thickness(0, 0, 0, 10); + + _window.Parts.Visible = type == ViewType.Parts; + _window.PartsButton.Disabled = type == ViewType.Parts; + + _window.Surgeries.Visible = type == ViewType.Surgeries; + _window.SurgeriesButton.Disabled = type != ViewType.Steps; + + _window.Steps.Visible = type == ViewType.Steps; + _window.StepsButton.Disabled = type != ViewType.Steps || _previousSurgeries.Count == 0; + + if (_entities.TryGetComponent(_part, out MetaDataComponent? partMeta) && + _entities.TryGetComponent(_surgery?.Ent, out MetaDataComponent? surgeryMeta)) + { + _window.Title = $"Surgery - {partMeta.EntityName}, {surgeryMeta.EntityName}"; + } + else if (partMeta != null) + { + _window.Title = $"Surgery - {partMeta.EntityName}"; + } + else + { + _window.Title = "Surgery"; + } + } + + private enum ViewType + { + Parts, + Surgeries, + Steps + } + + private enum StepStatus + { + Next, + Complete, + Incomplete + } +} diff --git a/Content.Client/Backmen/Surgery/SurgeryStepButton.xaml b/Content.Client/Backmen/Surgery/SurgeryStepButton.xaml new file mode 100644 index 00000000000..50d4733e68e --- /dev/null +++ b/Content.Client/Backmen/Surgery/SurgeryStepButton.xaml @@ -0,0 +1,4 @@ + + diff --git a/Content.Client/Backmen/Surgery/SurgeryStepButton.xaml.cs b/Content.Client/Backmen/Surgery/SurgeryStepButton.xaml.cs new file mode 100644 index 00000000000..0e18e15421a --- /dev/null +++ b/Content.Client/Backmen/Surgery/SurgeryStepButton.xaml.cs @@ -0,0 +1,16 @@ +using Content.Client.Xenonids.UI; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Backmen.Surgery; + +[GenerateTypedNameReferences] +public sealed partial class SurgeryStepButton : XenoChoiceControl +{ + public EntityUid Step { get; set; } + + public SurgeryStepButton() + { + RobustXamlLoader.Load(this); + } +} diff --git a/Content.Client/Backmen/Surgery/SurgerySystem.cs b/Content.Client/Backmen/Surgery/SurgerySystem.cs new file mode 100644 index 00000000000..aeb3358dea3 --- /dev/null +++ b/Content.Client/Backmen/Surgery/SurgerySystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Backmen.Surgery; +using Content.Shared.Medical.Surgery; + +namespace Content.Client.Backmen.Surgery; + +public sealed class SurgerySystem : SharedSurgerySystem +{ + public event Action? OnStep; + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnRefresh); + } + + private void OnRefresh(SurgeryUiRefreshEvent ev) + { + OnStep?.Invoke(); + } +} diff --git a/Content.Client/Backmen/Surgery/SurgeryWindow.xaml b/Content.Client/Backmen/Surgery/SurgeryWindow.xaml new file mode 100644 index 00000000000..b6b7ac9a2b7 --- /dev/null +++ b/Content.Client/Backmen/Surgery/SurgeryWindow.xaml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs new file mode 100644 index 00000000000..7a463ca0f20 --- /dev/null +++ b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs @@ -0,0 +1,29 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Utility; + +namespace Content.Client.Xenonids.UI; + +[GenerateTypedNameReferences] +[Virtual] +public partial class XenoChoiceControl : Control +{ + public XenoChoiceControl() + { + RobustXamlLoader.Load(this); + } + + public void Set(string name, Texture? texture) + { + NameLabel.SetMessage(name); + Texture.Texture = texture; + } + + public void Set(FormattedMessage msg, Texture? texture) + { + NameLabel.SetMessage(msg); + Texture.Texture = texture; + } +} \ No newline at end of file diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs index ec508790ba8..d6cf61d3061 100644 --- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs +++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs @@ -237,8 +237,7 @@ public override void Update(float frameTime) if (pressure <= Atmospherics.HazardLowPressure) { // Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear. - _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * Atmospherics.LowPressureDamage, true, false); - + _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * Atmospherics.LowPressureDamage, true, false, canSever: false); // backmen if (!barotrauma.TakingDamage) { barotrauma.TakingDamage = true; @@ -252,7 +251,7 @@ public override void Update(float frameTime) var damageScale = MathF.Min(((pressure / Atmospherics.HazardHighPressure) - 1) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage); // Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear. - _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false); + _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false, canSever: false); // backmen if (!barotrauma.TakingDamage) { diff --git a/Content.Server/Backmen/Surgery/SurgerySystem.cs b/Content.Server/Backmen/Surgery/SurgerySystem.cs new file mode 100644 index 00000000000..02046afaf71 --- /dev/null +++ b/Content.Server/Backmen/Surgery/SurgerySystem.cs @@ -0,0 +1,227 @@ +using Content.Shared.Bed.Sleep; +using Content.Shared.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.Body.Organ; +using Content.Shared.Body.Part; +using Content.Server.Chat.Systems; +using Content.Server.Popups; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Medical.Surgery.Steps; +using Content.Shared.Medical.Surgery.Conditions; +using Content.Shared.Medical.Surgery.Effects.Step; +using Content.Server.Atmos.Rotting; +using Content.Shared.Eye.Blinding.Components; +using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Prototypes; +using Robust.Server.GameObjects; +using Robust.Shared.Configuration; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using System.Linq; +using Content.Shared.Backmen.Surgery; +using Content.Shared.Backmen.Surgery.Tools; + +namespace Content.Server.Backmen.Surgery; + +public sealed class SurgerySystem : SharedSurgerySystem +{ + [Dependency] private readonly BodySystem _body = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly RottingSystem _rot = default!; + [Dependency] private readonly BlindableSystem _blindableSystem = default!; + + private readonly List _surgeries = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToolAfterInteract); + SubscribeLocalEvent(OnSurgeryStepDamage); + SubscribeLocalEvent(OnSurgeryDamageChange); + SubscribeLocalEvent(OnSurgerySpecialDamageChange); + SubscribeLocalEvent(OnStepAffixPartComplete); + SubscribeLocalEvent(OnStepScreamComplete); + SubscribeLocalEvent(OnStepSpawnComplete); + + SubscribeLocalEvent(OnPrototypesReloaded); + + LoadPrototypes(); + } + + protected override void RefreshUI(EntityUid body) + { + var surgeries = new Dictionary>(); + foreach (var surgery in _surgeries) + { + if (GetSingleton(surgery) is not { } surgeryEnt) + continue; + + foreach (var part in _body.GetBodyChildren(body)) + { + var ev = new SurgeryValidEvent(body, part.Id); + RaiseLocalEvent(surgeryEnt, ref ev); + + if (ev.Cancelled) + continue; + + surgeries.GetOrNew(GetNetEntity(part.Id)).Add(surgery); + } + + } + //Logger.Debug($"Setting UI state with {surgeries}, {body} and {SurgeryUIKey.Key}"); + _ui.SetUiState(body, SurgeryUIKey.Key, new SurgeryBuiState(surgeries)); + /* + Reason we do this is because when applying a BUI State, it rolls back the state on the entity temporarily, + which just so happens to occur right as we're checking for step completion, so we end up with the UI + not updating at all until you change tools or reopen the window. + */ + + var actors = _ui.GetActors(body, SurgeryUIKey.Key).ToArray(); + if (actors.Length == 0) + return; + + var filter = Filter.Entities(actors); + RaiseNetworkEvent(new Shared.Medical.Surgery.SurgeryUiRefreshEvent(GetNetEntity(body)), filter); + } + + private void SetDamage(EntityUid body, + DamageSpecifier damage, + float partMultiplier, + EntityUid user, + EntityUid part) + { + var changed = _damageableSystem.TryChangeDamage(body, damage, true, origin: user, canSever: false, partMultiplier: partMultiplier); + if (changed != null + && changed.GetTotal() == 0 + && damage.GetTotal() < 0 + && TryComp(part, out var partComp)) + { + var targetPart = _body.GetTargetBodyPart(partComp.PartType, partComp.Symmetry); + _body.TryChangeIntegrity((part, partComp), damage.GetTotal().Float(), false, targetPart, out var _); + } + } + + private void OnToolAfterInteract(Entity ent, ref AfterInteractEvent args) + { + var user = args.User; + if (args.Handled + || !args.CanReach + || args.Target == null + || !TryComp(args.User, out var surgery) + || !surgery.CanOperate) + { + return; + } + + if (user == args.Target && !_config.GetCVar(Shared.Corvax.CCCVars.CCCVars.CanOperateOnSelf)) + { + _popup.PopupEntity(Loc.GetString("surgery-error-self-surgery"), user, user); + return; + } + + args.Handled = true; + _ui.OpenUi(args.Target.Value, SurgeryUIKey.Key, user); + //Logger.Debug("UI opened"); + RefreshUI(args.Target.Value); + } + + private void OnSurgeryStepDamage(Entity ent, ref SurgeryStepDamageEvent args) + { + SetDamage(args.Body, args.Damage, args.PartMultiplier, args.User, args.Part); + } + + private void OnSurgeryDamageChange(Entity ent, ref SurgeryStepEvent args) + { + // This unintentionally punishes the user if they have an organ in another hand that is already used. + // Imo surgery shouldnt let you automatically pick tools on both hands anyway, it should only use the one you've got in your selected hand. + if (ent.Comp.IsConsumable) + { + if (args.Tools.Where(tool => TryComp(tool, out var organComp) + && !_body.TrySetOrganUsed(tool, true, organComp)).Any()) + return; + } + + var damageChange = ent.Comp.Damage; + if (HasComp(args.Body)) + damageChange = damageChange * ent.Comp.SleepModifier; + + SetDamage(args.Body, damageChange, 0.5f, args.User, args.Part); + } + + private void OnSurgerySpecialDamageChange(Entity ent, ref SurgeryStepEvent args) + { + if (ent.Comp.IsConsumable) + { + if (args.Tools.Where(tool => TryComp(tool, out var organComp) + && !_body.TrySetOrganUsed(tool, true, organComp)).Any()) + return; + } + + if (ent.Comp.DamageType == "Rot") + { + _rot.ReduceAccumulator(args.Body, TimeSpan.FromSeconds(2147483648)); + } + else if (ent.Comp.DamageType == "Eye") + { + if (TryComp(args.Body, out BlindableComponent? blindComp) + && blindComp.EyeDamage > 0) + _blindableSystem.AdjustEyeDamage((args.Body, blindComp), -blindComp!.EyeDamage); + } + } + + private void OnStepAffixPartComplete(Entity ent, ref SurgeryStepEvent args) + { + if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp)) + return; + + var targetPart = _body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).FirstOrDefault(); + + if (targetPart != default) + { + var ev = new BodyPartEnableChangedEvent(true); + RaiseLocalEvent(targetPart.Id, ref ev); + // This is basically an equalizer, severing a part will badly damage it. + // and affixing it will heal it a bit if it's not too badly damaged. + _body.TryChangeIntegrity(targetPart, targetPart.Component.Integrity - BodyPartComponent.IntegrityAffixPart, false, + _body.GetTargetBodyPart(targetPart.Component.PartType, targetPart.Component.Symmetry), out _); + } + + } + + private void OnStepScreamComplete(Entity ent, ref SurgeryStepEvent args) + { + if (!HasComp(args.Body)) + _chat.TryEmoteWithChat(args.Body, ent.Comp.Emote); + } + private void OnStepSpawnComplete(Entity ent, ref SurgeryStepEvent args) + { + if (TryComp(args.Body, out TransformComponent? xform)) + SpawnAtPosition(ent.Comp.Entity, xform.Coordinates); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (args.WasModified()) + LoadPrototypes(); + } + + private void LoadPrototypes() + { + _surgeries.Clear(); + foreach (var entity in _prototypes.EnumeratePrototypes()) + { + if (entity.HasComponent()) + _surgeries.Add(new EntProtoId(entity.ID)); + } + } +} diff --git a/Content.Server/Backmen/Targeting/TargetingSystem.cs b/Content.Server/Backmen/Targeting/TargetingSystem.cs new file mode 100644 index 00000000000..247a9bf6b9e --- /dev/null +++ b/Content.Server/Backmen/Targeting/TargetingSystem.cs @@ -0,0 +1,51 @@ +using Content.Shared.Backmen.Targeting; +using Content.Shared.Body.Systems; +using Content.Shared.Mobs; + +namespace Content.Server.Backmen.Targeting; +public sealed class TargetingSystem : SharedTargetingSystem +{ + [Dependency] private readonly SharedBodySystem _bodySystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(OnTargetChange); + SubscribeLocalEvent(OnMobStateChange); + } + + private void OnTargetChange(TargetChangeEvent message, EntitySessionEventArgs args) + { + if (!TryComp(GetEntity(message.Uid), out var target)) + return; + + target.Target = message.BodyPart; + Dirty(GetEntity(message.Uid), target); + } + + private void OnMobStateChange(EntityUid uid, TargetingComponent component, MobStateChangedEvent args) + { + // Revival is handled by the server, so we're keeping all of this here. + var changed = false; + + if (args.NewMobState == MobState.Dead) + { + foreach (TargetBodyPart part in Enum.GetValues(typeof(TargetBodyPart))) + { + component.BodyStatus[part] = TargetIntegrity.Dead; + changed = true; + } + } + else if (args.OldMobState == MobState.Dead && (args.NewMobState == MobState.Alive || args.NewMobState == MobState.Critical)) + { + component.BodyStatus = _bodySystem.GetBodyPartStatus(uid); + changed = true; + } + + if (changed) + { + Dirty(uid, component); + RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(uid)), uid); + } + } +} diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 4279f3ed2b8..a4202deec29 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Body.Systems; +using Content.Shared.Damage; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; @@ -20,6 +21,7 @@ public sealed class BodySystem : SharedBodySystem [Dependency] private readonly GhostSystem _ghostSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; @@ -95,6 +97,7 @@ protected override void RemovePart( var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value); _humanoidSystem.SetLayersVisibility( bodyEnt, layers, visible: false, permanent: true, humanoid); + _appearance.SetData(bodyEnt, layer, true); } public override HashSet GibBody( @@ -105,8 +108,7 @@ public override HashSet GibBody( Vector2? splatDirection = null, float splatModifier = 1, Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null - ) + SoundSpecifier? gibSoundOverride = null) { if (!Resolve(bodyId, ref body, logMissing: false) || TerminatingOrDeleted(bodyId) @@ -120,7 +122,7 @@ public override HashSet GibBody( return new HashSet(); var gibs = base.GibBody(bodyId, gibOrgans, body, launchGibs: launchGibs, - splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone); + splatDirection: splatDirection, splatModifier: splatModifier, splatCone: splatCone); var ev = new BeingGibbedEvent(gibs); RaiseLocalEvent(bodyId, ref ev); @@ -129,4 +131,40 @@ public override HashSet GibBody( return gibs; } + + public override HashSet GibPart( + EntityUid partId, + BodyPartComponent? part = null, + bool launchGibs = true, + Vector2? splatDirection = null, + float splatModifier = 1, + Angle splatCone = default, + SoundSpecifier? gibSoundOverride = null) + { + if (!Resolve(partId, ref part, logMissing: false) + || TerminatingOrDeleted(partId) + || EntityManager.IsQueuedForDeletion(partId)) + { + return new HashSet(); + } + + var xform = Transform(partId); + if (xform.MapUid is null) + return new HashSet(); + + var gibs = base.GibPart(partId, part, launchGibs: launchGibs, + splatDirection: splatDirection, splatModifier: splatModifier, splatCone: splatCone); + + var ev = new BeingGibbedEvent(gibs); + RaiseLocalEvent(partId, ref ev); + + QueueDel(partId); + + return gibs; + } + + protected override void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component) + { + return; + } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs new file mode 100644 index 00000000000..95f3a319bfe --- /dev/null +++ b/Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs @@ -0,0 +1,19 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using JetBrains.Annotations; + +namespace Content.Server.Destructible.Thresholds.Behaviors +{ + [UsedImplicitly] + [DataDefinition] + public sealed partial class GibPartBehavior : IThresholdBehavior + { + public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) + { + if (system.EntityManager.TryGetComponent(owner, out BodyPartComponent? part)) + { + system.BodySystem.GibPart(owner, part); + } + } + } +} diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index 903fd1ff3f7..8102ca66b04 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -2,6 +2,8 @@ using Content.Server.Inventory; using Content.Server.Stack; using Content.Server.Stunnable; +using Content.Shared.Body.Systems; +using Content.Shared.Body.Events; using Content.Shared.ActionBlocker; using Content.Shared.Body.Part; using Content.Shared.CombatMode; @@ -36,7 +38,7 @@ public sealed class HandsSystem : SharedHandsSystem [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly PullingSystem _pullingSystem = default!; [Dependency] private readonly ThrowingSystem _throwingSystem = default!; - + [Dependency] private readonly SharedBodySystem _bodySystem = default!; public override void Initialize() { base.Initialize(); @@ -52,6 +54,8 @@ public override void Initialize() SubscribeLocalEvent(GetComponentState); SubscribeLocalEvent(OnExploded); + SubscribeLocalEvent(HandleBodyPartEnabled); + SubscribeLocalEvent(HandleBodyPartDisabled); CommandBinds.Builder .Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem)) @@ -101,32 +105,57 @@ private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent a args.Handled = true; // no shove/stun. } - private void HandleBodyPartAdded(EntityUid uid, HandsComponent component, ref BodyPartAddedEvent args) + private void TryAddHand(EntityUid uid, HandsComponent component, Entity part, string slot) { - if (args.Part.Comp.PartType != BodyPartType.Hand) + if (part.Comp is null + || part.Comp.PartType != BodyPartType.Hand) return; // If this annoys you, which it should. // Ping Smugleaf. - var location = args.Part.Comp.Symmetry switch + var location = part.Comp.Symmetry switch { BodyPartSymmetry.None => HandLocation.Middle, BodyPartSymmetry.Left => HandLocation.Left, BodyPartSymmetry.Right => HandLocation.Right, - _ => throw new ArgumentOutOfRangeException(nameof(args.Part.Comp.Symmetry)) + _ => throw new ArgumentOutOfRangeException(nameof(part.Comp.Symmetry)) }; - AddHand(uid, args.Slot, location); + if (part.Comp.Enabled + && _bodySystem.TryGetParentBodyPart(part, out var _, out var parentPartComp) + && parentPartComp.Enabled) + AddHand(uid, slot, location); + } + + private void HandleBodyPartAdded(EntityUid uid, HandsComponent component, ref BodyPartAddedEvent args) + { + TryAddHand(uid, component, args.Part, args.Slot); } private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args) { - if (args.Part.Comp.PartType != BodyPartType.Hand) + if (args.Part.Comp is null + || args.Part.Comp.PartType != BodyPartType.Hand) return; - RemoveHand(uid, args.Slot); } + private void HandleBodyPartEnabled(EntityUid uid, HandsComponent component, ref BodyPartEnabledEvent args) + { + TryAddHand(uid, component, args.Part, SharedBodySystem.GetPartSlotContainerId(args.Part.Comp.ParentSlot?.Id ?? string.Empty)); + } + + private void HandleBodyPartDisabled(EntityUid uid, HandsComponent component, ref BodyPartDisabledEvent args) + { + if (TerminatingOrDeleted(uid) + || args.Part.Comp is null + || args.Part.Comp.PartType != BodyPartType.Hand) + return; + + RemoveHand(uid, SharedBodySystem.GetPartSlotContainerId(args.Part.Comp.ParentSlot?.Id ?? string.Empty)); + } + + #region pulling private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args) @@ -195,7 +224,7 @@ hands.ActiveHandEntity is not { } throwEnt || { var splitStack = _stackSystem.Split(throwEnt, 1, EntityManager.GetComponent(player).Coordinates, stack); - if (splitStack is not {Valid: true}) + if (splitStack is not { Valid: true }) return false; throwEnt = splitStack.Value; diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 15fe2a69cf9..50fd66d6745 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -202,6 +202,7 @@ private void OnActivateUI(Entity entity, ref AfterActivatableU : 0, null, null, + null, null )); } diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs index cf5869d1cbb..dced200bf1c 100644 --- a/Content.Server/Medical/HealingSystem.cs +++ b/Content.Server/Medical/HealingSystem.cs @@ -1,14 +1,18 @@ using Content.Server.Administration.Logs; using Content.Server.Body.Components; using Content.Server.Body.Systems; +using Content.Shared.Body.Part; +using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Medical.Components; using Content.Server.Popups; using Content.Server.Stack; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Audio; +using Content.Shared.Body.Systems; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.DoAfter; +using Content.Shared.Body.Components; using Content.Shared.FixedPoint; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -21,6 +25,7 @@ using Content.Shared.Stacks; using Robust.Shared.Audio.Systems; using Robust.Shared.Random; +using System.Linq; namespace Content.Server.Medical; @@ -36,6 +41,7 @@ public sealed class HealingSystem : EntitySystem [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SharedBodySystem _bodySystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; public override void Initialize() @@ -87,6 +93,21 @@ entity.Comp.DamageContainerID is not null && var total = healed?.GetTotal() ?? FixedPoint2.Zero; + /* This is rather shitcodey. Problem is that right now damage is coupled to integrity. + If the body is fully healed, all of the checks on TryChangeDamage stop us from actually healing. + So in this case we add a special check to heal anyway if TryChangeDamage returns null. + */ + if (total == 0) + { + var parts = _bodySystem.GetBodyChildren(args.Target).ToList(); + // We fetch the most damaged body part + var mostDamaged = parts.MinBy(x => x.Component.Integrity); + var targetBodyPart = _bodySystem.GetTargetBodyPart(mostDamaged); + + if (targetBodyPart != null) + _bodySystem.TryChangeIntegrity(mostDamaged, healing.Damage.GetTotal().Float(), false, targetBodyPart.Value, out _); + } + // Re-verify that we can heal the damage. if (TryComp(args.Used.Value, out var stackComp)) @@ -115,7 +136,7 @@ entity.Comp.DamageContainerID is not null && _audio.PlayPvs(healing.HealingEndSound, entity.Owner, AudioHelpers.WithVariation(0.125f, _random).WithVolume(1f)); // Logic to determine the whether or not to repeat the healing action - args.Repeat = (HasDamage(entity.Comp, healing) && !dontRepeat); + args.Repeat = HasDamage(entity.Comp, healing) && !dontRepeat || ArePartsDamaged(entity); if (!args.Repeat && !dontRepeat) _popupSystem.PopupEntity(Loc.GetString("medical-item-finished-using", ("item", args.Used)), entity.Owner, args.User); args.Handled = true; @@ -136,6 +157,19 @@ private bool HasDamage(DamageableComponent component, HealingComponent healing) return false; } + private bool ArePartsDamaged(EntityUid target) + { + if (!TryComp(target, out var body)) + return false; + + foreach (var part in _bodySystem.GetBodyChildren(target, body)) + { + if (part.Component.Integrity < BodyPartComponent.MaxIntegrity) + return true; + } + return false; + } + private void OnHealingUse(Entity entity, ref UseInHandEvent args) { if (args.Handled) @@ -174,6 +208,7 @@ targetDamage.DamageContainerID is not null && var anythingToDo = HasDamage(targetDamage, component) || + ArePartsDamaged(target) || 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) diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 90646725bb7..19bfa8b82d7 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -3,6 +3,7 @@ using Content.Server.PowerCell; using Content.Server.Temperature.Components; using Content.Server.Traits.Assorted; +using Content.Shared.Backmen.Targeting; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Damage; using Content.Shared.DoAfter; @@ -51,7 +52,7 @@ public override void Update(float frameTime) if (component.NextUpdate > _timing.CurTime) continue; - if (component.ScannedEntity is not {} patient) + if (component.ScannedEntity is not { } patient) continue; if (Deleted(patient)) @@ -210,13 +211,20 @@ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool s if (HasComp(target)) unrevivable = true; + // start-backmen: surgery + Dictionary? body = null; + if (TryComp(target, out var targetingComponent)) + body = targetingComponent.BodyStatus; + // end-backmen: surgery + _uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage( GetNetEntity(target), bodyTemperature, bloodAmount, scanMode, bleeding, - unrevivable + unrevivable, + body // backmen: surgery )); } } diff --git a/Content.Shared/Backmen/Surgery/Body/AmputateAttemptEvent.cs b/Content.Shared/Backmen/Surgery/Body/AmputateAttemptEvent.cs new file mode 100644 index 00000000000..b71a0407bf2 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Body/AmputateAttemptEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Body.Events; + +/// +/// Raised on an entity when attempting to remove a body part. +/// +[ByRefEvent] +public readonly record struct AmputateAttemptEvent(EntityUid Part); diff --git a/Content.Shared/Backmen/Surgery/Body/EyesComponent.cs b/Content.Shared/Backmen/Surgery/Body/EyesComponent.cs new file mode 100644 index 00000000000..55be5f1a9c4 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Body/EyesComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Body.Organ; + +[RegisterComponent] +public sealed partial class EyesComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Body/HeartComponent.cs b/Content.Shared/Backmen/Surgery/Body/HeartComponent.cs new file mode 100644 index 00000000000..fc4def945eb --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Body/HeartComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Body.Organ; + +[RegisterComponent] +public sealed partial class HeartComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Body/LiverComponent.cs b/Content.Shared/Backmen/Surgery/Body/LiverComponent.cs new file mode 100644 index 00000000000..23021bea319 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Body/LiverComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Body.Organ; + +[RegisterComponent] +public sealed partial class LiverComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Body/SharedBodySystem.Integrity.cs b/Content.Shared/Backmen/Surgery/Body/SharedBodySystem.Integrity.cs new file mode 100644 index 00000000000..1bef8f86115 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Body/SharedBodySystem.Integrity.cs @@ -0,0 +1,353 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Damage; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Standing; +using Content.Shared.Backmen.Targeting; +using Robust.Shared.CPUJob.JobQueues; +using Robust.Shared.CPUJob.JobQueues.Queues; +using Robust.Shared.Network; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Content.Shared.Backmen.Surgery.Steps.Parts; + +// ReSharper disable once CheckNamespace +namespace Content.Shared.Body.Systems; + +public partial class SharedBodySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly IRobustRandom _random = default!; + private readonly string[] _severingDamageTypes = { "Slash", "Pierce", "Blunt" }; + private const double IntegrityJobTime = 0.005; + private readonly JobQueue _integrityJobQueue = new(IntegrityJobTime); + + public sealed class IntegrityJob : Job + { + private readonly SharedBodySystem _self; + private readonly Entity _ent; + public IntegrityJob(SharedBodySystem self, Entity ent, double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation) + { + _self = self; + _ent = ent; + } + + public IntegrityJob(SharedBodySystem self, Entity ent, double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default) : base(maxTime, stopwatch, cancellation) + { + _self = self; + _ent = ent; + } + + protected override Task Process() + { + _self.ProcessIntegrityTick(_ent); + + return Task.FromResult(null); + } + } + + private EntityQuery _queryTargeting; + private void InitializeBkm() + { + _queryTargeting = GetEntityQuery(); + } + + private void ProcessIntegrityTick(Entity entity) + { + if (entity.Comp is { Body: { } body, Integrity: > BodyPartComponent.MaxIntegrity / 2 and < BodyPartComponent.MaxIntegrity } + && _queryTargeting.HasComp(body) + && !_mobState.IsDead(body)) + { + var healing = entity.Comp.SelfHealingAmount; + if (healing + entity.Comp.Integrity > BodyPartComponent.MaxIntegrity) + healing = entity.Comp.Integrity - BodyPartComponent.MaxIntegrity; + + TryChangeIntegrity(entity, + healing, + false, + GetTargetBodyPart(entity), + out _); + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + _integrityJobQueue.Process(); + + if (!_timing.IsFirstTimePredicted) + return; + + using var query = EntityQueryEnumerator(); + while (query.MoveNext(out var ent, out var part)) + { + part.HealingTimer += frameTime; + + if (part.HealingTimer >= part.HealingTime) + { + part.HealingTimer = 0; + _integrityJobQueue.EnqueueJob(new IntegrityJob(this, (ent, part), IntegrityJobTime)); + } + } + } + + + /// + /// Propagates damage to the specified parts of the entity. + /// + private void ApplyPartDamage( + Entity partEnt, + DamageSpecifier damage, + BodyPartType targetType, + TargetBodyPart targetPart, + bool canSever, + float partMultiplier) + { + if ( + partEnt.Comp.Body is not { } body || + !TryComp(body, out var mobState)) + return; + + foreach (var (damageType, damageValue) in damage.DamageDict) + { + if (damageValue.Float() == 0 + || TryEvadeDamage((body, mobState), GetEvadeChance(targetType))) + continue; + + var modifier = GetDamageModifier(damageType); + var partModifier = GetPartDamageModifier(targetType); + var integrityDamage = damageValue.Float() * modifier * partModifier * partMultiplier; + TryChangeIntegrity(partEnt, + integrityDamage, + canSever && _severingDamageTypes.Contains(damageType), + targetPart, + out var severed); + + if (severed) + break; + } + } + + public void TryChangeIntegrity(Entity partEnt, + float integrity, + bool canSever, + TargetBodyPart? targetPart, + out bool severed) + { + severed = false; + + if (!_timing.IsFirstTimePredicted || !_queryTargeting.HasComp(partEnt.Comp.Body)) + return; + + var partIdSlot = GetParentPartAndSlotOrNull(partEnt)?.Slot; + var originalIntegrity = partEnt.Comp.Integrity; + partEnt.Comp.Integrity = Math.Min(BodyPartComponent.MaxIntegrity, partEnt.Comp.Integrity - integrity); + + // This will also prevent the torso from being removed. + if (canSever + && !HasComp(partEnt) + && !partEnt.Comp.Enabled + && partEnt.Comp.Integrity <= 0 + && partIdSlot is not null) + severed = true; + + if (partEnt.Comp.Enabled + && partEnt.Comp.Integrity <= BodyPartComponent.CritIntegrity) + { + var ev = new BodyPartEnableChangedEvent(false); + RaiseLocalEvent(partEnt, ref ev); + } + else if (!partEnt.Comp.Enabled + && partEnt.Comp.Integrity >= BodyPartComponent.SomewhatIntegrity) + { + var ev = new BodyPartEnableChangedEvent(true); + RaiseLocalEvent(partEnt, ref ev); + } + + if (Math.Abs(partEnt.Comp.Integrity - originalIntegrity) > 0.01 + && _queryTargeting.TryComp(partEnt.Comp.Body, out var targeting) + && HasComp(partEnt.Comp.Body)) + { + var newIntegrity = GetIntegrityThreshold(partEnt.Comp.Integrity, severed, partEnt.Comp.Enabled); + // We need to check if the part is dead to prevent the UI from showing dead parts as alive. + if (targetPart is not null && targeting.BodyStatus[targetPart.Value] != TargetIntegrity.Dead) + { + targeting.BodyStatus[targetPart.Value] = newIntegrity; + Dirty(partEnt.Comp.Body.Value, targeting); + } + + // Revival events are handled by the server, so ends up being locked to a network event. + if (_net.IsServer) + RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(partEnt.Comp.Body.Value)), partEnt.Comp.Body.Value); + } + + if (severed && partIdSlot is not null) + DropPart(partEnt); + + Dirty(partEnt, partEnt.Comp); + } + + /// + /// Gets the integrity of all body parts in the entity. + /// + public Dictionary GetBodyPartStatus(EntityUid entityUid) + { + var result = new Dictionary(); + + if (!TryComp(entityUid, out var body)) + return result; + + foreach (TargetBodyPart part in Enum.GetValues(typeof(TargetBodyPart))) + { + result[part] = TargetIntegrity.Severed; + } + + foreach (var partComponent in GetBodyChildren(entityUid, body)) + { + var targetBodyPart = GetTargetBodyPart(partComponent.Component.PartType, partComponent.Component.Symmetry); + + if (targetBodyPart != null) + { + result[targetBodyPart.Value] = GetIntegrityThreshold(partComponent.Component.Integrity, false, partComponent.Component.Enabled); + } + } + + return result; + } + + public TargetBodyPart? GetTargetBodyPart(Entity part) => GetTargetBodyPart(part.Comp.PartType, part.Comp.Symmetry); + public TargetBodyPart? GetTargetBodyPart(BodyPartComponent part) => GetTargetBodyPart(part.PartType, part.Symmetry); + /// + /// Converts Enums from BodyPartType to their Targeting system equivalent. + /// + public TargetBodyPart? GetTargetBodyPart(BodyPartType type, BodyPartSymmetry symmetry) + { + return (type, symmetry) switch + { + (BodyPartType.Head, _) => TargetBodyPart.Head, + (BodyPartType.Torso, _) => TargetBodyPart.Torso, + (BodyPartType.Arm, BodyPartSymmetry.Left) => TargetBodyPart.LeftArm, + (BodyPartType.Arm, BodyPartSymmetry.Right) => TargetBodyPart.RightArm, + (BodyPartType.Leg, BodyPartSymmetry.Left) => TargetBodyPart.LeftLeg, + (BodyPartType.Leg, BodyPartSymmetry.Right) => TargetBodyPart.RightLeg, + _ => null + }; + } + + /// + /// Converts Enums from Targeting system to their BodyPartType equivalent. + /// + public (BodyPartType Type, BodyPartSymmetry Symmetry) ConvertTargetBodyPart(TargetBodyPart targetPart) + { + return targetPart switch + { + TargetBodyPart.Head => (BodyPartType.Head, BodyPartSymmetry.None), + TargetBodyPart.Torso => (BodyPartType.Torso, BodyPartSymmetry.None), + TargetBodyPart.LeftArm => (BodyPartType.Arm, BodyPartSymmetry.Left), + TargetBodyPart.RightArm => (BodyPartType.Arm, BodyPartSymmetry.Right), + TargetBodyPart.LeftLeg => (BodyPartType.Leg, BodyPartSymmetry.Left), + TargetBodyPart.RightLeg => (BodyPartType.Leg, BodyPartSymmetry.Right), + _ => (BodyPartType.Torso, BodyPartSymmetry.None) + }; + + } + + /// + /// Fetches the damage multiplier for part integrity based on damage types. + /// + public float GetDamageModifier(string damageType) + { + return damageType switch + { + "Blunt" => 0.8f, + "Slash" => 1.2f, + "Pierce" => 0.5f, + "Heat" => 1.0f, + "Cold" => 1.0f, + "Shock" => 0.8f, + "Poison" => 0.8f, + "Radiation" => 0.8f, + "Cellular" => 0.8f, + _ => 0.5f + }; + } + + /// + /// Fetches the damage multiplier for part integrity based on part types. + /// + public float GetPartDamageModifier(BodyPartType partType) + { + return partType switch + { + BodyPartType.Head => 0.5f, // 50% damage, necks are hard to cut + BodyPartType.Torso => 1.0f, // 100% damage + BodyPartType.Arm => 0.7f, // 70% damage + BodyPartType.Leg => 0.7f, // 70% damage + _ => 0.5f + }; + } + + /// + /// Fetches the TargetIntegrity equivalent of the current integrity value for the body part. + /// + public TargetIntegrity GetIntegrityThreshold(float integrity, bool severed, bool enabled) + { + if (severed) + return TargetIntegrity.Severed; + + if (!enabled) + return TargetIntegrity.Disabled; + + return integrity switch + { + <= BodyPartComponent.CritIntegrity => TargetIntegrity.CriticallyWounded, + <= BodyPartComponent.HeavyIntegrity => TargetIntegrity.HeavilyWounded, + <= BodyPartComponent.MedIntegrity => TargetIntegrity.ModeratelyWounded, + <= BodyPartComponent.SomewhatIntegrity => TargetIntegrity.SomewhatWounded, + <= BodyPartComponent.LightIntegrity => TargetIntegrity.LightlyWounded, + _ => TargetIntegrity.Healthy + }; + } + + /// + /// Fetches the chance to evade integrity damage for a body part. + /// Used when the entity is not dead, laying down, or incapacitated. + /// + public float GetEvadeChance(BodyPartType partType) + { + return partType switch + { + BodyPartType.Head => 0.70f, // 70% chance to evade + BodyPartType.Arm => 0.20f, // 20% chance to evade + BodyPartType.Leg => 0.20f, // 20% chance to evade + BodyPartType.Torso => 0f, // 0% chance to evade + _ => 0f + }; + } + + public bool CanEvadeDamage(Entity uid) + { + if (!Resolve(uid, ref uid.Comp)) + return false; + + return TryComp(uid, out var standingState) + && !_mobState.IsCritical(uid, uid) + && !_mobState.IsDead(uid, uid); + } + + public bool TryEvadeDamage(Entity uid, float evadeChance) + { + if (!Resolve(uid, ref uid.Comp)) + return false; + + if (!CanEvadeDamage(uid)) + return false; + + return _random.NextFloat() < evadeChance; + } +} diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs new file mode 100644 index 00000000000..bab7e405ad5 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryCloseIncisionConditionComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryLarvaConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryLarvaConditionComponent.cs new file mode 100644 index 00000000000..3aac5951c6f --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryLarvaConditionComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryLarvaConditionComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs new file mode 100644 index 00000000000..0c43549e669 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryOperatingTableConditionComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryOrganConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryOrganConditionComponent.cs new file mode 100644 index 00000000000..19c8df14910 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryOrganConditionComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Body.Organ; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryOrganConditionComponent : Component +{ + [DataField] + public ComponentRegistry? Organ; + + [DataField] + public bool Inverse = false; + + [DataField] + public bool Reattaching = false; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartConditionComponent.cs new file mode 100644 index 00000000000..5941e2fba3a --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartConditionComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Body.Part; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryPartConditionComponent : Component +{ + [DataField] + public BodyPartType Part; + + [DataField] + public BodyPartSymmetry? Symmetry; + + [DataField] + public bool Inverse = false; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartPresentCondition.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartPresentCondition.cs new file mode 100644 index 00000000000..608f90ba4cb --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartPresentCondition.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryPartPresentConditionComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs new file mode 100644 index 00000000000..fb51ab5b060 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Body.Part; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryPartRemovedConditionComponent : Component +{ + [DataField] + public BodyPartType Part; + + [DataField] + public BodyPartSymmetry? Symmetry; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryValidEvent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryValidEvent.cs new file mode 100644 index 00000000000..da769a457ac --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryValidEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Body.Part; + +namespace Content.Shared.Medical.Surgery.Conditions; + +/// +/// Raised on the entity that is receiving surgery. +/// +[ByRefEvent] +public record struct SurgeryValidEvent(EntityUid Body, EntityUid Part, bool Cancelled = false, BodyPartType PartType = default, BodyPartSymmetry? Symmetry = default); \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Conditions/SurgeryWoundedConditionComponent.cs b/Content.Shared/Backmen/Surgery/Conditions/SurgeryWoundedConditionComponent.cs new file mode 100644 index 00000000000..2279fcd0440 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Conditions/SurgeryWoundedConditionComponent.cs @@ -0,0 +1,7 @@ +using Content.Shared.Body.Part; +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Conditions; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryWoundedConditionComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Complete/SurgeryCompletedEvent.cs b/Content.Shared/Backmen/Surgery/Effects/Complete/SurgeryCompletedEvent.cs new file mode 100644 index 00000000000..a0e040fbe7a --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Complete/SurgeryCompletedEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Medical.Surgery.Effects.Complete; + +/// +/// Raised on the entity that received the surgery. +/// +[ByRefEvent] +public record struct SurgeryCompletedEvent; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs new file mode 100644 index 00000000000..2077dfa53b8 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Effects.Complete; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryRemoveLarvaComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs new file mode 100644 index 00000000000..646276bbeb4 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Damage; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryDamageChangeEffectComponent : Component +{ + [DataField] + public DamageSpecifier Damage = default!; + + [DataField] + public float SleepModifier = 0.5f; + + [DataField] + public bool IsConsumable = false; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs new file mode 100644 index 00000000000..823db704472 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Damage; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgerySpecialDamageChangeEffectComponent : Component +{ + [DataField] + public string DamageType = ""; + + [DataField] + public bool IsConsumable = false; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepAffixPartEffectComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepAffixPartEffectComponent.cs new file mode 100644 index 00000000000..c41534ca833 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepAffixPartEffectComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryStepAffixPartEffectComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs new file mode 100644 index 00000000000..61300425a7d --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryStepCavityEffectComponent : Component +{ + [DataField] + public string Action = "Insert"; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs new file mode 100644 index 00000000000..02e8b749eed --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs @@ -0,0 +1,12 @@ +using Content.Shared.Chat.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SurgeryStepEmoteEffectComponent : Component +{ + [DataField, AutoNetworkedField] + public ProtoId Emote = "Scream"; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs new file mode 100644 index 00000000000..766713c6f68 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs @@ -0,0 +1,13 @@ +using Content.Shared.Chat.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using System.ComponentModel.DataAnnotations; + +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SurgeryStepSpawnEffectComponent : Component +{ + [DataField(required: true), AutoNetworkedField] + public EntProtoId Entity; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs new file mode 100644 index 00000000000..58db1422d8f --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs @@ -0,0 +1,20 @@ +using Content.Shared.Damage; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +namespace Content.Shared.Medical.Surgery.Effects.Step; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SurgeryTendWoundsEffectComponent : Component +{ + [DataField, AutoNetworkedField] + public string MainGroup = "Brute"; + + [DataField, AutoNetworkedField] + public bool IsAutoRepeatable = true; + + [DataField, AutoNetworkedField] + public DamageSpecifier Damage = default!; + + [DataField, AutoNetworkedField] + public float HealMultiplier = 0.07f; +} \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/OperatingTableComponent.cs b/Content.Shared/Backmen/Surgery/OperatingTableComponent.cs new file mode 100644 index 00000000000..05e2060f24b --- /dev/null +++ b/Content.Shared/Backmen/Surgery/OperatingTableComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery; + +[RegisterComponent, NetworkedComponent] +public sealed partial class OperatingTableComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Backmen/Surgery/SharedSurgerySystem.Steps.cs new file mode 100644 index 00000000000..4efc9459934 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SharedSurgerySystem.Steps.cs @@ -0,0 +1,584 @@ +using Content.Shared.Medical.Surgery.Conditions; +using Content.Shared.Medical.Surgery.Effects.Step; +using Content.Shared.Medical.Surgery.Steps; +using Content.Shared.Body.Part; +using Content.Shared.Body.Organ; +using Content.Shared.Body.Events; +using Content.Shared.Buckle.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Inventory; +using Content.Shared.Item; +using Content.Shared.DoAfter; +using Content.Shared.Popups; +using Robust.Shared.Prototypes; +using System.Linq; +using Content.Shared.Backmen.Surgery.Steps; +using Content.Shared.Backmen.Surgery.Tools; +using Content.Shared.Containers.ItemSlots; + +namespace Content.Shared.Backmen.Surgery; + +public abstract partial class SharedSurgerySystem +{ + private static readonly string[] BruteDamageTypes = { "Slash", "Blunt", "Piercing" }; + private static readonly string[] BurnDamageTypes = { "Heat", "Shock", "Cold", "Caustic" }; + private void InitializeSteps() + { + SubscribeLocalEvent(OnToolStep); + SubscribeLocalEvent(OnToolCheck); + SubscribeLocalEvent(OnToolCanPerform); + + //SubSurgery(OnCutLarvaRootsStep, OnCutLarvaRootsCheck); + SubSurgery(OnTendWoundsStep, OnTendWoundsCheck); + SubSurgery(OnCavityStep, OnCavityCheck); + SubSurgery(OnAddPartStep, OnAddPartCheck); + SubSurgery(OnRemovePartStep, OnRemovePartCheck); + SubSurgery(OnAddOrganStep, OnAddOrganCheck); + SubSurgery(OnRemoveOrganStep, OnRemoveOrganCheck); + Subs.BuiEvents(SurgeryUIKey.Key, + subs => + { + subs.Event(OnSurgeryTargetStepChosen); + }); + } + + private void SubSurgery(EntityEventRefHandler onStep, + EntityEventRefHandler onComplete) where TComp : IComponent + { + SubscribeLocalEvent(onStep); + SubscribeLocalEvent(onComplete); + } + + private void OnToolStep(Entity ent, ref SurgeryStepEvent args) + { + if (ent.Comp.Tool != null) + { + foreach (var reg in ent.Comp.Tool.Values) + { + if (!AnyHaveComp(args.Tools, reg.Component, out var tool)) + return; + + if (_net.IsServer && + TryComp(tool, out SurgeryToolComponent? toolComp) && + toolComp.EndSound != null) + { + _audio.PlayEntity(toolComp.EndSound, args.User, tool); + } + } + } + + if (ent.Comp.Add != null) + { + foreach (var reg in ent.Comp.Add.Values) + { + var compType = reg.Component.GetType(); + if (HasComp(args.Part, compType)) + continue; + AddComp(args.Part, _compFactory.GetComponent(compType)); + } + } + + if (ent.Comp.Remove != null) + { + foreach (var reg in ent.Comp.Remove.Values) + { + RemComp(args.Part, reg.Component.GetType()); + } + } + + if (ent.Comp.BodyRemove != null) + { + foreach (var reg in ent.Comp.BodyRemove.Values) + { + RemComp(args.Body, reg.Component.GetType()); + } + } + + if (!_inventory.TryGetSlotEntity(args.User, "gloves", out var gloves) + || !_inventory.TryGetSlotEntity(args.User, "mask", out var mask)) + { + var sepsis = new DamageSpecifier(_prototypes.Index("Poison"), 5); + var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, sepsis, 0.5f); + RaiseLocalEvent(args.Body, ref ev); + } + } + + private void OnToolCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + if (ent.Comp.Add != null) + { + foreach (var reg in ent.Comp.Add.Values) + { + if (!HasComp(args.Part, reg.Component.GetType())) + { + args.Cancelled = true; + return; + } + } + } + + if (ent.Comp.Remove != null) + { + foreach (var reg in ent.Comp.Remove.Values) + { + if (HasComp(args.Part, reg.Component.GetType())) + { + args.Cancelled = true; + return; + } + } + } + + if (ent.Comp.BodyRemove != null) + { + foreach (var reg in ent.Comp.BodyRemove.Values) + { + if (HasComp(args.Body, reg.Component.GetType())) + { + args.Cancelled = true; + return; + } + } + } + } + + private void OnToolCanPerform(Entity ent, ref SurgeryCanPerformStepEvent args) + { + if (HasComp(ent)) + { + if (!TryComp(args.Body, out BuckleComponent? buckle) || + !HasComp(buckle.BuckledTo)) + { + args.Invalid = StepInvalidReason.NeedsOperatingTable; + return; + } + } + + RaiseLocalEvent(args.Body, ref args); + + if (args.Invalid != StepInvalidReason.None) + return; + + if (ent.Comp.Tool != null) + { + args.ValidTools ??= new HashSet(); + + foreach (var reg in ent.Comp.Tool.Values) + { + if (!AnyHaveComp(args.Tools, reg.Component, out var withComp)) + { + args.Invalid = StepInvalidReason.MissingTool; + + if (reg.Component is ISurgeryToolComponent tool) + args.Popup = $"You need {tool.ToolName} to perform this step!"; + + return; + } + + args.ValidTools.Add(withComp); + } + } + } + + private EntProtoId? GetProtoId(EntityUid entityUid) + { + if (!TryComp(entityUid, out var metaData)) + return null; + + return metaData.EntityPrototype?.ID; + } + + private void OnTendWoundsStep(Entity ent, ref SurgeryStepEvent args) + { + var group = ent.Comp.MainGroup == "Brute" ? BruteDamageTypes : BurnDamageTypes; + + if (!TryComp(args.Body, out DamageableComponent? damageable) + || !group.Any(damageType => damageable.Damage.DamageDict.TryGetValue(damageType, out var value) + && value > 0) + && (!TryComp(args.Part, out BodyPartComponent? bodyPart) + || bodyPart.Integrity == BodyPartComponent.MaxIntegrity)) + return; + + var bonus = ent.Comp.HealMultiplier * damageable.DamagePerGroup[ent.Comp.MainGroup]; + if (_mobState.IsDead(args.Body)) + bonus *= 0.2; + + var adjustedDamage = new DamageSpecifier(ent.Comp.Damage); + var bonusPerType = bonus / group.Length; + + foreach (var type in group) + { + adjustedDamage.DamageDict[type] -= bonusPerType; + } + + var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, adjustedDamage, 0.5f); + RaiseLocalEvent(args.Body, ref ev); + } + + private void OnTendWoundsCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + var group = ent.Comp.MainGroup == "Brute" ? BruteDamageTypes : BurnDamageTypes; + + if (!TryComp(args.Body, out DamageableComponent? damageable) + || group.Any(damageType => damageable.Damage.DamageDict.TryGetValue(damageType, out var value) + && value > 0) + || !TryComp(args.Part, out BodyPartComponent? bodyPart) + || bodyPart.Integrity < BodyPartComponent.MaxIntegrity) + args.Cancelled = true; + } + + /*private void OnCutLarvaRootsStep(Entity ent, ref SurgeryStepEvent args) + { + if (TryComp(args.Body, out VictimInfectedComponent? infected) && + infected.BurstAt > _timing.CurTime && + infected.SpawnedLarva == null) + { + infected.RootsCut = true; + } + } + + private void OnCutLarvaRootsCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + if (!TryComp(args.Body, out VictimInfectedComponent? infected) || !infected.RootsCut) + args.Cancelled = true; + + // The larva has fully developed and surgery is now impossible + // TODO: Surgery should still be possible, but the fully developed larva should escape while also saving the hosts life + if (infected != null && infected.SpawnedLarva != null) + args.Cancelled = true; + }*/ + + private void OnCavityStep(Entity ent, ref SurgeryStepEvent args) + { + if (!TryComp(args.Part, out BodyPartComponent? partComp) || partComp.PartType != BodyPartType.Torso) + return; + + var activeHandEntity = _hands.EnumerateHeld(args.User).FirstOrDefault(); + if (activeHandEntity != default + && ent.Comp.Action == "Insert" + && TryComp(activeHandEntity, out ItemComponent? itemComp) + && (itemComp.Size.Id == "Tiny" + || itemComp.Size.Id == "Small")) + _itemSlotsSystem.TryInsert(ent, partComp.ItemInsertionSlot, activeHandEntity, args.User); + else if (ent.Comp.Action == "Remove") + _itemSlotsSystem.TryEjectToHands(ent, partComp.ItemInsertionSlot, args.User); + } + + private void OnCavityCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + // Normally this check would simply be partComp.ItemInsertionSlot.HasItem, but as mentioned before, + // For whatever reason it's not instantiating the field on the clientside after the wizmerge. + if (!TryComp(args.Part, out BodyPartComponent? partComp) + || !TryComp(args.Part, out ItemSlotsComponent? itemComp) + || ent.Comp.Action == "Insert" + && !itemComp.Slots[partComp.ContainerName].HasItem + || ent.Comp.Action == "Remove" + && itemComp.Slots[partComp.ContainerName].HasItem) + args.Cancelled = true; + } + + private void OnAddPartStep(Entity ent, ref SurgeryStepEvent args) + { + if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp)) + return; + + foreach (var tool in args.Tools) + { + if (TryComp(tool, out BodyPartComponent? partComp) + && partComp.PartType == removedComp.Part + && (removedComp.Symmetry == null || partComp.Symmetry == removedComp.Symmetry)) + { + var slotName = removedComp.Symmetry != null + ? $"{removedComp.Symmetry?.ToString().ToLower()} {removedComp.Part.ToString().ToLower()}" + : removedComp.Part.ToString().ToLower(); + _body.TryCreatePartSlot(args.Part, slotName, partComp.PartType, out var _); + _body.AttachPart(args.Part, slotName, tool); + _body.ChangeSlotState((tool, partComp), false); + } + } + } + + private void OnAddPartCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp) + || !_body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).Any()) + args.Cancelled = true; + } + + private void OnRemovePartStep(Entity ent, ref SurgeryStepEvent args) + { + if (!TryComp(args.Part, out BodyPartComponent? partComp) + || partComp.Body != args.Body) + return; + + var ev = new AmputateAttemptEvent(args.Part); + RaiseLocalEvent(args.Part, ref ev); + _hands.TryPickupAnyHand(args.User, args.Part); + } + + private void OnRemovePartCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + if (!TryComp(args.Part, out BodyPartComponent? partComp) + || partComp.Body == args.Body) + args.Cancelled = true; + } + + private void OnAddOrganStep(Entity ent, ref SurgeryStepEvent args) + { + if (!TryComp(args.Part, out BodyPartComponent? partComp) + || partComp.Body != args.Body + || !TryComp(args.Surgery, out SurgeryOrganConditionComponent? organComp) + || organComp.Organ == null) + return; + + // Adding organs is generally done for a single one at a time, so we only need to check for the first. + var firstOrgan = organComp.Organ.Values.FirstOrDefault(); + if (firstOrgan == default) + return; + + foreach (var tool in args.Tools) + { + if (HasComp(tool, firstOrgan.Component.GetType()) + && TryComp(tool, out var insertedOrgan) + && _body.InsertOrgan(args.Part, tool, insertedOrgan.SlotId, partComp, insertedOrgan)) + break; + } + } + + private void OnAddOrganCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + if (!TryComp(args.Surgery, out var organComp) + || organComp.Organ is null + || !TryComp(args.Part, out BodyPartComponent? partComp) + || partComp.Body != args.Body) + return; + + // For now we naively assume that every entity will only have one of each organ type. + // that we do surgery on, but in the future we'll need to reference their prototype somehow + // to know if they need 2 hearts, 2 lungs, etc. + foreach (var reg in organComp.Organ.Values) + { + if (!_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var _)) + { + args.Cancelled = true; + } + } + } + + private void OnRemoveOrganStep(Entity ent, ref SurgeryStepEvent args) + { + if (!TryComp(args.Surgery, out var organComp) + || organComp.Organ == null) + return; + + foreach (var reg in organComp.Organ.Values) + { + _body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs); + if (organs != null && organs.Count > 0) + { + _body.RemoveOrgan(organs[0].Id, organs[0].Organ); + _hands.TryPickupAnyHand(args.User, organs[0].Id); + } + } + } + + private void OnRemoveOrganCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) + { + if (!TryComp(args.Surgery, out var organComp) + || organComp.Organ == null + || !TryComp(args.Part, out BodyPartComponent? partComp) + || partComp.Body != args.Body) + return; + + foreach (var reg in organComp.Organ.Values) + { + if (_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs) + && organs != null + && organs.Count > 0) + { + args.Cancelled = true; + return; + } + } + } + + private void OnSurgeryTargetStepChosen(Entity ent, ref SurgeryStepChosenBuiMsg args) + { + var user = args.Actor; + if (GetEntity(args.Entity) is not { Valid: true } body || + GetEntity(args.Part) is not { Valid: true } targetPart || + !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, user, out var surgery, out var part, out var step)) + { + return; + } + + if (!PreviousStepsComplete(body, part, surgery, args.Step) || + IsStepComplete(body, part, args.Step, surgery)) + return; + + if (!CanPerformStep(user, body, part, step, true, out _, out _, out var validTools)) + return; + + if (_net.IsServer && validTools?.Count > 0) + { + foreach (var tool in validTools) + { + if (TryComp(tool, out SurgeryToolComponent? toolComp) && + toolComp.EndSound != null) + { + _audio.PlayEntity(toolComp.StartSound, user, tool); + } + } + } + + if (TryComp(body, out TransformComponent? xform)) + _rotateToFace.TryFaceCoordinates(user, _transform.GetMapCoordinates(body, xform).Position); + + var ev = new SurgeryDoAfterEvent(args.Surgery, args.Step); + // TODO: Make this serialized on a per surgery step basis, and also add penalties based on ghetto tools. + var duration = 2f; + if (TryComp(user, out SurgerySpeedModifierComponent? surgerySpeedMod) + && surgerySpeedMod is not null) + duration = duration / surgerySpeedMod.SpeedModifier; + + var doAfter = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(duration), ev, body, part) + { + BreakOnMove = true, + CancelDuplicate = true, + DuplicateCondition = DuplicateConditions.SameEvent, + NeedHand = true, + BreakOnHandChange = true, + }; + + _doAfter.TryStartDoAfter(doAfter); + } + + private (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity surgery, List requirements) + { + if (!Resolve(surgery, ref surgery.Comp)) + return null; + + if (requirements.Contains(surgery)) + throw new ArgumentException($"Surgery {surgery} has a requirement loop: {string.Join(", ", requirements)}"); + + requirements.Add(surgery); + + if (surgery.Comp.Requirement is { } requirementId && + GetSingleton(requirementId) is { } requirement && + GetNextStep(body, part, requirement, requirements) is { } requiredNext) + { + return requiredNext; + } + + for (var i = 0; i < surgery.Comp.Steps.Count; i++) + { + var surgeryStep = surgery.Comp.Steps[i]; + if (!IsStepComplete(body, part, surgeryStep, surgery)) + return ((surgery, surgery.Comp), i); + } + + return null; + } + + public (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery) + { + return GetNextStep(body, part, surgery, new List()); + } + + public bool PreviousStepsComplete(EntityUid body, EntityUid part, Entity surgery, EntProtoId step) + { + // TODO RMC14 use index instead of the prototype id + if (surgery.Comp.Requirement is { } requirement) + { + if (GetSingleton(requirement) is not { } requiredEnt || + !TryComp(requiredEnt, out SurgeryComponent? requiredComp) || + !PreviousStepsComplete(body, part, (requiredEnt, requiredComp), step)) + { + return false; + } + } + + foreach (var surgeryStep in surgery.Comp.Steps) + { + if (surgeryStep == step) + break; + + if (!IsStepComplete(body, part, surgeryStep, surgery)) + return false; + } + + return true; + } + + public bool CanPerformStep(EntityUid user, EntityUid body, EntityUid part, + EntityUid step, bool doPopup, out string? popup, out StepInvalidReason reason, + out HashSet? validTools) + { + var type = BodyPartType.Other; + if (TryComp(part, out BodyPartComponent? partComp)) + { + type = partComp.PartType; + } + + var slot = type switch + { + BodyPartType.Head => SlotFlags.HEAD, + BodyPartType.Torso => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING, + BodyPartType.Arm => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING, + BodyPartType.Hand => SlotFlags.GLOVES, + BodyPartType.Leg => SlotFlags.OUTERCLOTHING | SlotFlags.LEGS, + BodyPartType.Foot => SlotFlags.FEET, + BodyPartType.Tail => SlotFlags.NONE, + BodyPartType.Other => SlotFlags.NONE, + _ => SlotFlags.NONE + }; + + var check = new SurgeryCanPerformStepEvent(user, body, GetTools(user), slot); + RaiseLocalEvent(step, ref check); + popup = check.Popup; + validTools = check.ValidTools; + + if (check.Invalid != StepInvalidReason.None) + { + if (doPopup && check.Popup != null) + _popup.PopupEntity(check.Popup, user, PopupType.SmallCaution); + + reason = check.Invalid; + return false; + } + + reason = default; + return true; + } + + public bool CanPerformStep(EntityUid user, EntityUid body, EntityUid part, EntityUid step, bool doPopup) + { + return CanPerformStep(user, body, part, step, doPopup, out _, out _, out _); + } + + public bool IsStepComplete(EntityUid body, EntityUid part, EntProtoId step, EntityUid surgery) + { + if (GetSingleton(step) is not { } stepEnt) + return false; + + var ev = new SurgeryStepCompleteCheckEvent(body, part, surgery); + RaiseLocalEvent(stepEnt, ref ev); + return !ev.Cancelled; + } + + private bool AnyHaveComp(List tools, IComponent component, out EntityUid withComp) + { + foreach (var tool in tools) + { + if (HasComp(tool, component.GetType())) + { + withComp = tool; + return true; + } + } + + withComp = default; + return false; + } +} diff --git a/Content.Shared/Backmen/Surgery/SharedSurgerySystem.cs b/Content.Shared/Backmen/Surgery/SharedSurgerySystem.cs new file mode 100644 index 00000000000..10ce5b6b810 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SharedSurgerySystem.cs @@ -0,0 +1,265 @@ +using System.Linq; +using Content.Shared.Backmen.Surgery.Steps.Parts; +using Content.Shared.Medical.Surgery.Conditions; +using Content.Shared.Body.Systems; +using Content.Shared.Medical.Surgery.Steps; +using Content.Shared.Body.Part; +using Content.Shared.Damage; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Body.Components; +using Content.Shared.Buckle.Components; +using Content.Shared.DoAfter; +using Content.Shared.Mobs.Systems; +using Content.Shared.GameTicking; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Popups; +using Content.Shared.Standing; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Backmen.Surgery; + +public abstract partial class SharedSurgerySystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedBodySystem _body = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly RotateToFaceSystem _rotateToFace = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private readonly Dictionary _surgeries = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRoundRestartCleanup); + + SubscribeLocalEvent(OnTargetDoAfter); + SubscribeLocalEvent(OnCloseIncisionValid); + //SubscribeLocalEvent(OnLarvaValid); + SubscribeLocalEvent(OnPartConditionValid); + SubscribeLocalEvent(OnOrganConditionValid); + SubscribeLocalEvent(OnWoundedValid); + SubscribeLocalEvent(OnPartRemovedConditionValid); + SubscribeLocalEvent(OnPartPresentConditionValid); + + //SubscribeLocalEvent(OnRemoveLarva); + + InitializeSteps(); + } + + private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) + { + _surgeries.Clear(); + } + + private void OnTargetDoAfter(Entity ent, ref SurgeryDoAfterEvent args) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (args.Cancelled || + args.Handled || + args.Target is not { } target || + !IsSurgeryValid(ent, target, args.Surgery, args.Step, args.User, out var surgery, out var part, out var step) || + !PreviousStepsComplete(ent, part, surgery, args.Step) || + !CanPerformStep(args.User, ent, part, step, false)) + { + Log.Warning($"{ToPrettyString(args.User)} tried to start invalid surgery."); + return; + } + + args.Repeat = HasComp(step); + var ev = new SurgeryStepEvent(args.User, ent, part, GetTools(args.User), surgery); + RaiseLocalEvent(step, ref ev); + RefreshUI(ent); + } + + private void OnCloseIncisionValid(Entity ent, ref SurgeryValidEvent args) + { + if (!HasComp(args.Part) || + !HasComp(args.Part) || + !HasComp(args.Part) || + !HasComp(args.Part) || + !HasComp(args.Part)) + { + args.Cancelled = true; + } + } + + private void OnWoundedValid(Entity ent, ref SurgeryValidEvent args) + { + if (!TryComp(args.Body, out DamageableComponent? damageable) + || !TryComp(args.Part, out BodyPartComponent? bodyPart) + || damageable.TotalDamage <= 0 + && bodyPart.Integrity == BodyPartComponent.MaxIntegrity + && !HasComp(args.Part)) + args.Cancelled = true; + } + + /*private void OnLarvaValid(Entity ent, ref SurgeryValidEvent args) + { + if (!TryComp(args.Body, out VictimInfectedComponent? infected)) + args.Cancelled = true; + + // The larva has fully developed and surgery is now impossible + if (infected != null && infected.SpawnedLarva != null) + args.Cancelled = true; + }*/ + private void OnPartConditionValid(Entity ent, ref SurgeryValidEvent args) + { + if (!TryComp(args.Part, out var part)) + { + args.Cancelled = true; + return; + } + + var typeMatch = part.PartType == ent.Comp.Part; + var symmetryMatch = ent.Comp.Symmetry == null || part.Symmetry == ent.Comp.Symmetry; + var valid = typeMatch && symmetryMatch; + + if (ent.Comp.Inverse ? valid : !valid) + args.Cancelled = true; + } + + private void OnOrganConditionValid(Entity ent, ref SurgeryValidEvent args) + { + if (!TryComp(args.Part, out var partComp) + || partComp.Body != args.Body + || ent.Comp.Organ == null) + { + args.Cancelled = true; + return; + } + + foreach (var reg in ent.Comp.Organ.Values) + { + if (_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs) + && organs != null + && organs.Count > 0) + { + if (ent.Comp.Inverse + && (!ent.Comp.Reattaching + || ent.Comp.Reattaching + && !HasComp(args.Part))) + args.Cancelled = true; + } + else if (!ent.Comp.Inverse) + args.Cancelled = true; + } + } + + private void OnPartRemovedConditionValid(Entity ent, ref SurgeryValidEvent args) + { + if (!TryComp(args.Part, out _) + || _body.GetBodyChildrenOfType(args.Body, ent.Comp.Part, symmetry: ent.Comp.Symmetry).Any() + && !HasComp(args.Part)) + args.Cancelled = true; + } + + private void OnPartPresentConditionValid(Entity ent, ref SurgeryValidEvent args) + { + if (args.Part == EntityUid.Invalid + || !HasComp(args.Part)) + args.Cancelled = true; + } + + /*private void OnRemoveLarva(Entity ent, ref SurgeryCompletedEvent args) + { + RemCompDeferred(ent); + }*/ + + protected bool IsSurgeryValid(EntityUid body, EntityUid targetPart, EntProtoId surgery, EntProtoId stepId, + EntityUid user, out Entity surgeryEnt, out EntityUid part, out EntityUid step) + { + surgeryEnt = default; + part = default; + step = default; + + if (!HasComp(body) || + !IsLyingDown(body, user) || + GetSingleton(surgery) is not { } surgeryEntId || + !TryComp(surgeryEntId, out SurgeryComponent? surgeryComp) || + !surgeryComp.Steps.Contains(stepId) || + GetSingleton(stepId) is not { } stepEnt + || !HasComp(targetPart) + && !HasComp(targetPart)) + return false; + + + var ev = new SurgeryValidEvent(body, targetPart); + if (_timing.IsFirstTimePredicted) + { + RaiseLocalEvent(stepEnt, ref ev); + RaiseLocalEvent(surgeryEntId, ref ev); + } + + if (ev.Cancelled) + return false; + + surgeryEnt = (surgeryEntId, surgeryComp); + part = targetPart; + step = stepEnt; + return true; + } + + public EntityUid? GetSingleton(EntProtoId surgeryOrStep) + { + if (!_prototypes.HasIndex(surgeryOrStep)) + return null; + + // This (for now) assumes that surgery entity data remains unchanged between client + // and server + // if it does not you get the bullet + if (!_surgeries.TryGetValue(surgeryOrStep, out var ent) || TerminatingOrDeleted(ent)) + { + ent = Spawn(surgeryOrStep, MapCoordinates.Nullspace); + _surgeries[surgeryOrStep] = ent; + } + + return ent; + } + + private List GetTools(EntityUid surgeon) + { + return _hands.EnumerateHeld(surgeon).ToList(); + } + + public bool IsLyingDown(EntityUid entity, EntityUid user) + { + if (_standing.IsDown(entity)) + return true; + + if (TryComp(entity, out BuckleComponent? buckle) && + TryComp(buckle.BuckledTo, out StrapComponent? strap)) + { + var rotation = strap.Rotation; + if (rotation.GetCardinalDir() is Direction.West or Direction.East) + return true; + } + + _popup.PopupEntity(Loc.GetString("surgery-error-laying"), user, user); + + return false; + } + + protected virtual void RefreshUI(EntityUid body) + { + } +} diff --git a/Content.Shared/Backmen/Surgery/StepInvalidReason.cs b/Content.Shared/Backmen/Surgery/StepInvalidReason.cs new file mode 100644 index 00000000000..d0d0709f812 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/StepInvalidReason.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Backmen.Surgery; + +public enum StepInvalidReason +{ + None, + MissingSkills, + NeedsOperatingTable, + Armor, + MissingTool, +} diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/BleedersClampedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/BleedersClampedComponent.cs new file mode 100644 index 00000000000..12978f06aa8 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/BleedersClampedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BleedersClampedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/BodyPartReattachedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/BodyPartReattachedComponent.cs new file mode 100644 index 00000000000..a8fd0e93d23 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/BodyPartReattachedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BodyPartReattachedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/BodyPartSawedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/BodyPartSawedComponent.cs new file mode 100644 index 00000000000..ba22b042b9b --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/BodyPartSawedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BodyPartSawedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/IncisionOpenComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/IncisionOpenComponent.cs new file mode 100644 index 00000000000..c7da946df7f --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/IncisionOpenComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class IncisionOpenComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs new file mode 100644 index 00000000000..4167a836040 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class InternalBleedersClampedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/OrganReattachedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/OrganReattachedComponent.cs new file mode 100644 index 00000000000..fe446dc3c9c --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/OrganReattachedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class OrganReattachedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/PartRemovedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/PartRemovedComponent.cs new file mode 100644 index 00000000000..bf3c5c722d3 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/PartRemovedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class PartsRemovedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/RibcageOpenComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/RibcageOpenComponent.cs new file mode 100644 index 00000000000..c159c9b52a8 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/RibcageOpenComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class RibcageOpenComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/RibcageSawedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/RibcageSawedComponent.cs new file mode 100644 index 00000000000..0927d8a9335 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/RibcageSawedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class RibcageSawedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/Parts/SkinRetractedComponent.cs b/Content.Shared/Backmen/Surgery/Steps/Parts/SkinRetractedComponent.cs new file mode 100644 index 00000000000..dd5247e05a0 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/Parts/SkinRetractedComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps.Parts; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SkinRetractedComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryAddOrganStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryAddOrganStepComponent.cs new file mode 100644 index 00000000000..e67bad1b623 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryAddOrganStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryAddOrganStepComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryAddPartStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryAddPartStepComponent.cs new file mode 100644 index 00000000000..0229552ae8a --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryAddPartStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryAddPartStepComponent : Component; \ No newline at end of file diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryCanPerformStepEvent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryCanPerformStepEvent.cs new file mode 100644 index 00000000000..e89a05efac9 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryCanPerformStepEvent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Backmen.Surgery.Steps; + +[ByRefEvent] +public record struct SurgeryCanPerformStepEvent( + EntityUid User, + EntityUid Body, + List Tools, + SlotFlags TargetSlots, + string? Popup = null, + StepInvalidReason Invalid = StepInvalidReason.None, + HashSet? ValidTools = null +) : IInventoryRelayEvent; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs new file mode 100644 index 00000000000..75972676c29 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryCutLarvaRootsStepComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs new file mode 100644 index 00000000000..3f66533a08f --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryRemoveOrganStepComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryRemovePartStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryRemovePartStepComponent.cs new file mode 100644 index 00000000000..c58582d2d38 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryRemovePartStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryRemovePartStepComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryRepeatableStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryRepeatableStepComponent.cs new file mode 100644 index 00000000000..14010b7e962 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryRepeatableStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryRepeatableStepComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs new file mode 100644 index 00000000000..ce6f67011ef --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared.Backmen.Surgery.Steps; + +[ByRefEvent] +public record struct SurgeryStepCompleteCheckEvent(EntityUid Body, EntityUid Part, EntityUid Surgery, bool Cancelled = false); diff --git a/Content.Shared/Backmen/Surgery/Steps/SurgeryStepComponent.cs b/Content.Shared/Backmen/Surgery/Steps/SurgeryStepComponent.cs new file mode 100644 index 00000000000..b9356bf01cc --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Steps/SurgeryStepComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Backmen.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +[Prototype("SurgerySteps")] +public sealed partial class SurgeryStepComponent : Component +{ + + [DataField] + public ComponentRegistry? Tool; + + [DataField] + public ComponentRegistry? Add; + + [DataField] + public ComponentRegistry? Remove; + + [DataField] + public ComponentRegistry? BodyRemove; +} diff --git a/Content.Shared/Backmen/Surgery/SurgeryComponent.cs b/Content.Shared/Backmen/Surgery/SurgeryComponent.cs new file mode 100644 index 00000000000..1d13731ff88 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Backmen.Surgery; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Prototype("Surgeries")] +public sealed partial class SurgeryComponent : Component +{ + [DataField, AutoNetworkedField] + public int Priority; + + [DataField, AutoNetworkedField] + public EntProtoId? Requirement; + + [DataField(required: true), AutoNetworkedField] + public List Steps = new(); +} diff --git a/Content.Shared/Backmen/Surgery/SurgeryDoAfterEvent.cs b/Content.Shared/Backmen/Surgery/SurgeryDoAfterEvent.cs new file mode 100644 index 00000000000..0b27ed8c548 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryDoAfterEvent.cs @@ -0,0 +1,18 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Surgery; + +[Serializable, NetSerializable] +public sealed partial class SurgeryDoAfterEvent : SimpleDoAfterEvent +{ + public readonly EntProtoId Surgery; + public readonly EntProtoId Step; + + public SurgeryDoAfterEvent(EntProtoId surgery, EntProtoId step) + { + Surgery = surgery; + Step = step; + } +} diff --git a/Content.Shared/Backmen/Surgery/SurgerySpeedModifierComponent.cs b/Content.Shared/Backmen/Surgery/SurgerySpeedModifierComponent.cs new file mode 100644 index 00000000000..4f8702c920c --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgerySpeedModifierComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Backmen.Surgery; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgerySpeedModifierComponent : Component +{ + [DataField] + public float SpeedModifier = 1.5f; +} diff --git a/Content.Shared/Backmen/Surgery/SurgeryStepDamageEvent.cs b/Content.Shared/Backmen/Surgery/SurgeryStepDamageEvent.cs new file mode 100644 index 00000000000..c8864acec89 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryStepDamageEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Damage; + +namespace Content.Shared.Backmen.Surgery; + +/// +/// Raised on the target entity. +/// +[ByRefEvent] +public record struct SurgeryStepDamageEvent(EntityUid User, EntityUid Body, EntityUid Part, EntityUid Surgery, DamageSpecifier Damage, float PartMultiplier); diff --git a/Content.Shared/Backmen/Surgery/SurgeryStepEvent.cs b/Content.Shared/Backmen/Surgery/SurgeryStepEvent.cs new file mode 100644 index 00000000000..bb986f683f4 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryStepEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Backmen.Surgery; + +/// +/// Raised on the step entity. +/// +[ByRefEvent] +public record struct SurgeryStepEvent(EntityUid User, EntityUid Body, EntityUid Part, List Tools, EntityUid Surgery); diff --git a/Content.Shared/Backmen/Surgery/SurgeryTargetComponent.cs b/Content.Shared/Backmen/Surgery/SurgeryTargetComponent.cs new file mode 100644 index 00000000000..bdfa6a11829 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryTargetComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryTargetComponent : Component +{ + [DataField] + public bool CanOperate = true; +} diff --git a/Content.Shared/Backmen/Surgery/SurgeryUI.cs b/Content.Shared/Backmen/Surgery/SurgeryUI.cs new file mode 100644 index 00000000000..1e8320522a1 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryUI.cs @@ -0,0 +1,27 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Surgery; + +[Serializable, NetSerializable] +public enum SurgeryUIKey +{ + Key +} + +[Serializable, NetSerializable] +public sealed class SurgeryBuiState(Dictionary> choices) : BoundUserInterfaceState +{ + public readonly Dictionary> Choices = choices; +} + +[Serializable, NetSerializable] +public sealed class SurgeryStepChosenBuiMsg(NetEntity part, EntProtoId surgery, EntProtoId step, bool isBody) : BoundUserInterfaceMessage +{ + public readonly NetEntity Part = part; + public readonly EntProtoId Surgery = surgery; + public readonly EntProtoId Step = step; + + // Used as a marker for whether or not we're hijacking surgery by applying it on the body itself. + public readonly bool IsBody = isBody; +} diff --git a/Content.Shared/Backmen/Surgery/SurgeryUiRefreshEvent.cs b/Content.Shared/Backmen/Surgery/SurgeryUiRefreshEvent.cs new file mode 100644 index 00000000000..9d41401d7f8 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/SurgeryUiRefreshEvent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Surgery; + +[Serializable, NetSerializable] +public sealed class SurgeryUiRefreshEvent : EntityEventArgs +{ + public NetEntity Uid { get; } + + public SurgeryUiRefreshEvent(NetEntity uid) + { + Uid = uid; + } +} diff --git a/Content.Shared/Backmen/Surgery/Tools/BoneGelComponent.cs b/Content.Shared/Backmen/Surgery/Tools/BoneGelComponent.cs new file mode 100644 index 00000000000..0348852ad15 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/BoneGelComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BoneGelComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "bone gel"; + + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/BoneSawComponent.cs b/Content.Shared/Backmen/Surgery/Tools/BoneSawComponent.cs new file mode 100644 index 00000000000..17e9ab149c5 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/BoneSawComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BoneSawComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "a bone saw"; + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/BoneSetterComponent.cs b/Content.Shared/Backmen/Surgery/Tools/BoneSetterComponent.cs new file mode 100644 index 00000000000..7c71ff19100 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/BoneSetterComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BoneSetterComponent : Component; diff --git a/Content.Shared/Backmen/Surgery/Tools/CauteryComponent.cs b/Content.Shared/Backmen/Surgery/Tools/CauteryComponent.cs new file mode 100644 index 00000000000..a4dc6ade3b5 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/CauteryComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class CauteryComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "a cautery"; + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/HemostatComponent.cs b/Content.Shared/Backmen/Surgery/Tools/HemostatComponent.cs new file mode 100644 index 00000000000..c83c2bde510 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/HemostatComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class HemostatComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "a hemostat"; + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/ISurgeryToolComponent.cs b/Content.Shared/Backmen/Surgery/Tools/ISurgeryToolComponent.cs new file mode 100644 index 00000000000..c61236dd303 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/ISurgeryToolComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Backmen.Surgery.Tools; + +public interface ISurgeryToolComponent +{ + [DataField] + public string ToolName { get; } + + // Mostly intended for discardable or non-reusable tools. + [DataField] + public bool? Used { get; set; } +} diff --git a/Content.Shared/Backmen/Surgery/Tools/RetractorComponent.cs b/Content.Shared/Backmen/Surgery/Tools/RetractorComponent.cs new file mode 100644 index 00000000000..49d917253dd --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/RetractorComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class RetractorComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "a retractor"; + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/ScalpelComponent.cs b/Content.Shared/Backmen/Surgery/Tools/ScalpelComponent.cs new file mode 100644 index 00000000000..5d9e95c78fb --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/ScalpelComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class ScalpelComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "a scalpel"; + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/SurgeryToolComponent.cs b/Content.Shared/Backmen/Surgery/Tools/SurgeryToolComponent.cs new file mode 100644 index 00000000000..3a4f65de1dc --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/SurgeryToolComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SurgeryToolComponent : Component +{ + + [DataField, AutoNetworkedField] + public SoundSpecifier? StartSound; + + [DataField, AutoNetworkedField] + public SoundSpecifier? EndSound; +} diff --git a/Content.Shared/Backmen/Surgery/Tools/SurgicalDrillComponent.cs b/Content.Shared/Backmen/Surgery/Tools/SurgicalDrillComponent.cs new file mode 100644 index 00000000000..75feb380326 --- /dev/null +++ b/Content.Shared/Backmen/Surgery/Tools/SurgicalDrillComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Surgery.Tools; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgicalDrillComponent : Component, ISurgeryToolComponent +{ + public string ToolName => "a surgical drill"; + public bool? Used { get; set; } = null; +} diff --git a/Content.Shared/Backmen/Targeting/Events.cs b/Content.Shared/Backmen/Targeting/Events.cs new file mode 100644 index 00000000000..dc4ad54619b --- /dev/null +++ b/Content.Shared/Backmen/Targeting/Events.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Targeting; + +[Serializable, NetSerializable] +public sealed class TargetChangeEvent : EntityEventArgs +{ + public NetEntity Uid { get; } + public TargetBodyPart BodyPart { get; } + public TargetChangeEvent(NetEntity uid, TargetBodyPart bodyPart) + { + Uid = uid; + BodyPart = bodyPart; + } +} + +[Serializable, NetSerializable] +public sealed class TargetIntegrityChangeEvent : EntityEventArgs +{ + public NetEntity Uid { get; } + public bool RefreshUi { get; } + public TargetIntegrityChangeEvent(NetEntity uid, bool refreshUi = true) + { + Uid = uid; + RefreshUi = refreshUi; + } +} + +public sealed class RefreshInventorySlotsEvent : EntityEventArgs +{ + public string SlotName { get; } + + public RefreshInventorySlotsEvent(string slotName) + { + SlotName = slotName; + } +} diff --git a/Content.Shared/Backmen/Targeting/SharedTargetingSystem.cs b/Content.Shared/Backmen/Targeting/SharedTargetingSystem.cs new file mode 100644 index 00000000000..b0d7356ad77 --- /dev/null +++ b/Content.Shared/Backmen/Targeting/SharedTargetingSystem.cs @@ -0,0 +1,3 @@ +namespace Content.Shared.Backmen.Targeting; + +public abstract class SharedTargetingSystem : EntitySystem; diff --git a/Content.Shared/Backmen/Targeting/TargetBodyPart.cs b/Content.Shared/Backmen/Targeting/TargetBodyPart.cs new file mode 100644 index 00000000000..303da22b06a --- /dev/null +++ b/Content.Shared/Backmen/Targeting/TargetBodyPart.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Backmen.Targeting; +public enum TargetBodyPart +{ + Head, + Torso, + LeftArm, + RightArm, + LeftLeg, + RightLeg +} diff --git a/Content.Shared/Backmen/Targeting/TargetIntegrity.cs b/Content.Shared/Backmen/Targeting/TargetIntegrity.cs new file mode 100644 index 00000000000..b87720da06e --- /dev/null +++ b/Content.Shared/Backmen/Targeting/TargetIntegrity.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.Backmen.Targeting; +public enum TargetIntegrity +{ + Healthy = 0, + LightlyWounded = 1, + SomewhatWounded = 2, + ModeratelyWounded = 3, + HeavilyWounded = 4, + CriticallyWounded = 5, + Severed = 6, + Dead = 7, + Disabled = 8, +} diff --git a/Content.Shared/Backmen/Targeting/TargetingComponent.cs b/Content.Shared/Backmen/Targeting/TargetingComponent.cs new file mode 100644 index 00000000000..66dadf1a9af --- /dev/null +++ b/Content.Shared/Backmen/Targeting/TargetingComponent.cs @@ -0,0 +1,47 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Targeting; + +/// +/// Controls entity limb targeting for actions. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TargetingComponent : Component +{ + [ViewVariables, AutoNetworkedField] + public TargetBodyPart Target = TargetBodyPart.Torso; + + /// + /// What odds does the entity have of targeting each body part? + /// + [DataField] + public Dictionary TargetOdds = new() + { + { TargetBodyPart.Head, 0.1f }, + { TargetBodyPart.Torso, 0.4f }, + { TargetBodyPart.LeftArm, 0.125f }, + { TargetBodyPart.RightArm, 0.125f }, + { TargetBodyPart.LeftLeg, 0.125f }, + { TargetBodyPart.RightLeg, 0.125f } + }; + + /// + /// What is the current integrity of each body part? + /// + [ViewVariables, AutoNetworkedField] + public Dictionary BodyStatus = new() + { + { TargetBodyPart.Head, TargetIntegrity.Healthy }, + { TargetBodyPart.Torso, TargetIntegrity.Healthy }, + { TargetBodyPart.LeftArm, TargetIntegrity.Healthy }, + { TargetBodyPart.RightArm, TargetIntegrity.Healthy }, + { TargetBodyPart.LeftLeg, TargetIntegrity.Healthy }, + { TargetBodyPart.RightLeg, TargetIntegrity.Healthy } + }; + + /// + /// What noise does the entity play when swapping targets? + /// + [DataField] + public string SwapSound = "/Audio/Effects/toggleoncombat.ogg"; +} diff --git a/Content.Shared/Body/Organ/OrganComponent.cs b/Content.Shared/Body/Organ/OrganComponent.cs index 3048927b5fb..67442312cd3 100644 --- a/Content.Shared/Body/Organ/OrganComponent.cs +++ b/Content.Shared/Body/Organ/OrganComponent.cs @@ -1,16 +1,33 @@ +using Content.Shared.Backmen.Surgery.Tools; using Content.Shared.Body.Systems; -using Robust.Shared.Containers; using Robust.Shared.GameStates; namespace Content.Shared.Body.Organ; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedBodySystem))] -public sealed partial class OrganComponent : Component +public sealed partial class OrganComponent : Component, ISurgeryToolComponent { /// /// Relevant body this organ is attached to. /// [DataField, AutoNetworkedField] public EntityUid? Body; + + /// + /// Shitcodey solution to not being able to know what name corresponds to each organ's slot ID + /// without referencing the prototype or hardcoding. + /// + + [DataField] + public string SlotId = ""; + + [DataField] + public string ToolName { get; set; } = "An organ"; + + /// + /// If true, the organ will not heal an entity when transplanted into them. + /// + [DataField, AutoNetworkedField] + public bool? Used { get; set; } = false; } diff --git a/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs new file mode 100644 index 00000000000..5aa5d533554 --- /dev/null +++ b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Humanoid.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body.Part; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class BodyPartAppearanceComponent : Component +{ + + /// + /// ID of this custom base layer. Must be a . + /// + [IdDataField, AutoNetworkedField] + public ProtoId? ID { get; set; } + + /// + /// Color of this custom base layer. Null implies skin colour if the corresponding is set to match skin. + /// + [DataField, AutoNetworkedField] + public Color? Color { get; set; } + + [DataField, AutoNetworkedField] + public EntityUid? OriginalBody { get; set; } + + //TODO add other custom variables such as species and markings - in case someone decides to attach a lizard arm to a human for example +} diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs index c4e65c06a3f..d8ce075e526 100644 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ b/Content.Shared/Body/Part/BodyPartComponent.cs @@ -1,5 +1,8 @@ -using Content.Shared.Body.Components; +using Content.Shared.Backmen.Surgery.Tools; +using Content.Shared.Containers.ItemSlots; +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; @@ -8,7 +11,7 @@ namespace Content.Shared.Body.Part; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedBodySystem))] -public sealed partial class BodyPartComponent : Component +public sealed partial class BodyPartComponent : Component, ISurgeryToolComponent { // Need to set this on container changes as it may be several transform parents up the hierarchy. /// @@ -17,6 +20,12 @@ public sealed partial class BodyPartComponent : Component [DataField, AutoNetworkedField] public EntityUid? Body; + [DataField, AutoNetworkedField] + public EntityUid? OriginalBody; + + [DataField, AutoNetworkedField] + public BodyPartSlot? ParentSlot; + [DataField, AutoNetworkedField] public BodyPartType PartType = BodyPartType.Other; @@ -28,9 +37,22 @@ public sealed partial class BodyPartComponent : Component [DataField("vital"), AutoNetworkedField] public bool IsVital; + /// + /// Amount of damage to deal when the part gets removed. + /// Only works if IsVital is true. + /// + [DataField, AutoNetworkedField] + public FixedPoint2 VitalDamage = MaxIntegrity; + [DataField, AutoNetworkedField] public BodyPartSymmetry Symmetry = BodyPartSymmetry.None; + [DataField] + public string ToolName { get; set; } = "A body part"; + + [DataField, AutoNetworkedField] + public bool? Used { get; set; } = null; + /// /// Child body parts attached to this body part. /// @@ -43,6 +65,49 @@ public sealed partial class BodyPartComponent : Component [DataField, AutoNetworkedField] public Dictionary Organs = new(); + /// + /// How much health the body part has until it pops out. + /// + [DataField, AutoNetworkedField] + public float Integrity = MaxIntegrity; + + public const float MaxIntegrity = 70; + public const float LightIntegrity = 56; + public const float SomewhatIntegrity = 42; + public const float MedIntegrity = 28; + public const float HeavyIntegrity = 17.5f; + public const float CritIntegrity = 7; + public const float IntegrityAffixPart = 7; + + /// + /// Whether this body part is enabled or not. + /// + [DataField, AutoNetworkedField] + public bool Enabled = true; + + /// + /// How long it takes to run another self heal tick on the body part. + /// + [DataField("healingTime")] + public float HealingTime = 30; + + /// + /// How long it has been since the last self heal tick on the body part. + /// + public float HealingTimer = 0; + + /// + /// How much health to heal on the body part per tick. + /// + [DataField("selfHealingAmount")] + public float SelfHealingAmount = 5; + + [DataField] + public string ContainerName { get; set; } = "part_slot"; + + [DataField, AutoNetworkedField] + public ItemSlot ItemInsertionSlot = new(); + /// /// These are only for VV/Debug do not use these for gameplay/systems /// diff --git a/Content.Shared/Body/Part/BodyPartEvents.cs b/Content.Shared/Body/Part/BodyPartEvents.cs index 0d8d2c8a268..f4444121e1d 100644 --- a/Content.Shared/Body/Part/BodyPartEvents.cs +++ b/Content.Shared/Body/Part/BodyPartEvents.cs @@ -5,3 +5,12 @@ namespace Content.Shared.Body.Part; [ByRefEvent] public readonly record struct BodyPartRemovedEvent(string Slot, Entity Part); + +[ByRefEvent] +public readonly record struct BodyPartEnableChangedEvent(bool Enabled); + +[ByRefEvent] +public readonly record struct BodyPartEnabledEvent(Entity Part); + +[ByRefEvent] +public readonly record struct BodyPartDisabledEvent(Entity Part); diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 250f90db8f3..ccd705af147 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -4,17 +4,22 @@ using Content.Shared.Body.Organ; using Content.Shared.Body.Part; using Content.Shared.Body.Prototypes; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; using Content.Shared.DragDrop; using Content.Shared.Gibbing.Components; using Content.Shared.Gibbing.Events; using Content.Shared.Gibbing.Systems; +using Content.Shared.Humanoid; using Content.Shared.Inventory; +using Content.Shared.Rejuvenate; +using Content.Shared.Standing; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Utility; - +using Robust.Shared.Timing; namespace Content.Shared.Body.Systems; public partial class SharedBodySystem @@ -27,9 +32,10 @@ public partial class SharedBodySystem */ [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; [Dependency] private readonly GibbingSystem _gibbingSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - + [Dependency] private readonly IGameTiming _gameTiming = default!; private const float GibletLaunchImpulse = 8; private const float GibletLaunchImpulseVariance = 3; @@ -42,6 +48,9 @@ private void InitializeBody() SubscribeLocalEvent(OnBodyInit); SubscribeLocalEvent(OnBodyMapInit); SubscribeLocalEvent(OnBodyCanDrag); + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnStandAttempt); + SubscribeLocalEvent(OnRejuvenate); } private void OnBodyInserted(Entity ent, ref EntInsertedIntoContainerMessage args) @@ -116,7 +125,6 @@ private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype) var rootPart = Comp(rootPartUid); rootPart.Body = bodyEntity; Dirty(rootPartUid, rootPart); - // Setup the rest of the body entities. SetupOrgans((rootPartUid, rootPart), protoRoot.Organs); MapInitParts(rootPartUid, prototype); @@ -127,6 +135,41 @@ private void OnBodyCanDrag(Entity ent, ref CanDragEvent args) args.Handled = true; } + private void OnStandAttempt(Entity ent, ref StandAttemptEvent args) + { + if (ent.Comp.LegEntities.Count == 0) + args.Cancel(); + } + + private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) + { + if (!_gameTiming.IsFirstTimePredicted) + return; + + DebugTools.Assert(ent.Comp is not null); + if (args.PartMultiplier == 0 + || args.TargetPart is null + || args.DamageDelta is null + || args is { DamageIncreased: false, DamageDecreased: false }) + return; + + var (targetType, targetSymmetry) = ConvertTargetBodyPart(args.TargetPart.Value); + Log.Debug($"Applying damage to {ToPrettyString(ent)} with {ent.Comp} and {args} {targetType} {targetSymmetry}"); + foreach (var part in GetBodyChildrenOfType(ent, targetType, ent.Comp) + .Where(part => part.Component.Symmetry == targetSymmetry)) + { + ApplyPartDamage(part, args.DamageDelta, targetType, args.TargetPart.Value, args.CanSever, args.PartMultiplier); + } + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + foreach (var part in GetBodyChildren(ent, ent.Comp)) + { + TryChangeIntegrity(part, part.Component.Integrity - BodyPartComponent.MaxIntegrity, false, GetTargetBodyPart(part), out _); + } + } + /// /// Sets up all of the relevant body parts for a particular body entity and root part. /// @@ -168,6 +211,7 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) var childPartComponent = Comp(childPart); var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent); + childPartComponent.ParentSlot = partSlot; var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection)); if (partSlot is null || !Containers.Insert(childPart, cont)) @@ -177,6 +221,20 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) continue; } + if (TryComp(parentPartComponent.Body, out HumanoidAppearanceComponent? bodyAppearance)) + { + var appearance = AddComp(childPart); + appearance.OriginalBody = childPartComponent.OriginalBody; + appearance.Color = bodyAppearance.SkinColor; + + var symmetry = ((BodyPartSymmetry) childPartComponent.Symmetry).ToString(); + if (symmetry == "None") + symmetry = ""; + appearance.ID = "removed" + symmetry + ((BodyPartType) childPartComponent.PartType).ToString(); + + Dirty(childPart, appearance); + } + // Add organs SetupOrgans((childPart, childPartComponent), connectionSlot.Organs); @@ -233,11 +291,11 @@ public IEnumerable GetBodyContainers( { if (id is null || !Resolve(id.Value, ref body, logMissing: false) + || body is null + || body.RootContainer == default || body.RootContainer.ContainedEntity is null || !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart)) - { yield break; - } foreach (var child in GetBodyPartChildren(body.RootContainer.ContainedEntity.Value, rootPart)) { @@ -308,9 +366,9 @@ public virtual HashSet GibBody( foreach (var part in parts) { - _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Skip, ref gibs, - playAudio: false, launchGibs:true, launchDirection:splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier, - launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone); + _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Drop, ref gibs, + playAudio: false, launchGibs: true, launchDirection: splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier, + launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone); if (!gibOrgans) continue; @@ -319,7 +377,7 @@ public virtual HashSet GibBody( { _gibbingSystem.TryGibEntityWithRef(bodyId, organ.Id, GibType.Drop, GibContentsOption.Skip, ref gibs, playAudio: false, launchImpulse: GibletLaunchImpulse * splatModifier, - launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone); + launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone); } } @@ -335,4 +393,41 @@ public virtual HashSet GibBody( _audioSystem.PlayPredicted(gibSoundOverride, bodyTransform.Coordinates, null); return gibs; } + + public virtual HashSet GibPart( + EntityUid partId, + BodyPartComponent? part = null, + bool launchGibs = true, + Vector2? splatDirection = null, + float splatModifier = 1, + Angle splatCone = default, + SoundSpecifier? gibSoundOverride = null) + { + var gibs = new HashSet(); + + _gibbingSystem.TryGibEntityWithRef( + partId, + partId, + GibType.Gib, + GibContentsOption.Drop, + ref gibs, + playAudio: true, + launchGibs: true, + launchDirection: splatDirection, + launchImpulse: GibletLaunchImpulse * splatModifier, + launchImpulseVariance: GibletLaunchImpulseVariance, + launchCone: splatCone); + + if (TryComp(partId, out var inventory)) + { + foreach (var item in _inventory.GetHandOrInventoryEntities((partId, null, inventory))) + { + SharedTransform.AttachToGridOrMap(item); + gibs.Add(item); + } + } + + _audioSystem.PlayPredicted(gibSoundOverride, Transform(partId).Coordinates, null); + return gibs; + } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs index f83dd50c998..7511536de45 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs @@ -209,4 +209,15 @@ public bool TryGetBodyOrganEntityComps( comps = null; return false; } + + public bool TrySetOrganUsed(EntityUid organId, bool used, OrganComponent? organ = null) + { + if (!Resolve(organId, ref organ) + || organ.Used == true) + return false; + + organ.Used = true; + Dirty(organId, organ); + return true; + } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs new file mode 100644 index 00000000000..930da554379 --- /dev/null +++ b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs @@ -0,0 +1,83 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Body.Events; +using Robust.Shared.GameStates; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; + +namespace Content.Shared.Body.Systems; +// Code shamelessly stolen from MS14. +public partial class SharedBodySystem +{ + private void InitializePartAppearances() + { + base.Initialize(); + + SubscribeLocalEvent(OnPartAppearanceInit); + SubscribeLocalEvent(OnPartAddedToBody); + } + + private static readonly Dictionary<(BodyPartType, BodyPartSymmetry), HumanoidVisualLayers> BodyPartVisualLayers + = new Dictionary<(BodyPartType, BodyPartSymmetry), HumanoidVisualLayers> + { + { (BodyPartType.Head,BodyPartSymmetry.None), HumanoidVisualLayers.Head }, + { (BodyPartType.Tail,BodyPartSymmetry.None), HumanoidVisualLayers.Tail }, + { (BodyPartType.Torso,BodyPartSymmetry.None), HumanoidVisualLayers.Chest }, + { (BodyPartType.Arm,BodyPartSymmetry.Right), HumanoidVisualLayers.RArm }, + { (BodyPartType.Arm,BodyPartSymmetry.Left), HumanoidVisualLayers.LArm }, + { (BodyPartType.Hand,BodyPartSymmetry.Right), HumanoidVisualLayers.RHand }, + { (BodyPartType.Hand,BodyPartSymmetry.Left), HumanoidVisualLayers.LHand }, + { (BodyPartType.Leg,BodyPartSymmetry.Right), HumanoidVisualLayers.RLeg }, + { (BodyPartType.Leg,BodyPartSymmetry.Left), HumanoidVisualLayers.LLeg }, + { (BodyPartType.Foot,BodyPartSymmetry.Right), HumanoidVisualLayers.RLeg }, + { (BodyPartType.Foot,BodyPartSymmetry.Left), HumanoidVisualLayers.LLeg } + }; + + private void OnPartAppearanceInit(EntityUid uid, BodyPartAppearanceComponent component, ComponentInit args) + { + + if (TryComp(uid, out BodyPartComponent? part) && part.OriginalBody != null && + TryComp(part.OriginalBody.Value, out HumanoidAppearanceComponent? bodyAppearance)) + { + var customLayers = bodyAppearance.CustomBaseLayers; + var spriteLayers = bodyAppearance.BaseLayers; + var visualLayer = BodyPartVisualLayers[(part.PartType, part.Symmetry)]; + + component.OriginalBody = part.OriginalBody.Value; + + if (customLayers.ContainsKey(visualLayer)) + { + component.ID = customLayers[visualLayer].Id; + component.Color = customLayers[visualLayer].Color; + } + else if (spriteLayers.ContainsKey(visualLayer)) + { + component.ID = spriteLayers[visualLayer].ID; + component.Color = bodyAppearance.SkinColor; + } + else + { + var symmetry = ((BodyPartSymmetry) part.Symmetry).ToString(); + if (symmetry == "None") + symmetry = ""; + component.ID = "removed" + symmetry + ((BodyPartType) part.PartType).ToString(); + component.Color = bodyAppearance.SkinColor; + } + } + Dirty(uid, component); + UpdateAppearance(uid, component); + } + + public void OnPartAddedToBody(EntityUid uid, BodyPartAppearanceComponent component, ref BodyPartAddedEvent args) + { + if (TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance)) + { + var part = args.Part; + var customLayers = bodyAppearance.CustomBaseLayers; + var visualLayer = BodyPartVisualLayers[(part.Comp.PartType, part.Comp.Symmetry)]; + customLayers[visualLayer] = new CustomBaseLayerInfo(component.ID, customLayers[visualLayer].Color); + } + } + + protected abstract void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component); +} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index 0917197e29f..32316043409 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -1,28 +1,58 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Shared.Body.Components; using Content.Shared.Body.Events; using Content.Shared.Body.Organ; using Content.Shared.Body.Part; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Humanoid; +using Content.Shared.Inventory; using Content.Shared.Movement.Components; +using Content.Shared.Random; +using Content.Shared.Backmen.Targeting; using Robust.Shared.Containers; using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Content.Shared.Body.Systems; public partial class SharedBodySystem { + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly RandomHelperSystem _randomHelper = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + private void InitializeParts() { // TODO: This doesn't handle comp removal on child ents. // If you modify this also see the Body partial for root parts. + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnBodyPartRemove); SubscribeLocalEvent(OnBodyPartInserted); SubscribeLocalEvent(OnBodyPartRemoved); + SubscribeLocalEvent(OnAmputateAttempt); + SubscribeLocalEvent(OnPartEnableChanged); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (ent.Comp.PartType == BodyPartType.Torso) + { + _slots.AddItemSlot(ent, ent.Comp.ContainerName, ent.Comp.ItemInsertionSlot); + Dirty(ent, ent.Comp); + } } + private void OnBodyPartRemove(Entity ent, ref ComponentRemove args) + { + if (ent.Comp.PartType == BodyPartType.Torso) + { + _slots.RemoveItemSlot(ent, ent.Comp.ItemInsertionSlot); + } + } private void OnBodyPartInserted(Entity ent, ref EntInsertedIntoContainerMessage args) { // Body part inserted into another body part. @@ -47,7 +77,6 @@ private void OnBodyPartRemoved(Entity ent, ref EntRemovedFrom // Body part removed from another body part. var removedUid = args.Entity; var slotId = args.Container.ID; - DebugTools.Assert(!TryComp(removedUid, out BodyPartComponent? b) || b.Body == ent.Comp.Body); DebugTools.Assert(!TryComp(removedUid, out OrganComponent? o) || o.Body == ent.Comp.Body); @@ -93,6 +122,8 @@ private void RecursiveBodyUpdate(Entity ent, EntityUid? bodyU } } + // The code for RemovePartEffect() should live here, because it literally is the point of this recursive function. + // But the debug asserts at the top plus existing tests need refactoring for this. So we'll be lazy. foreach (var slotId in ent.Comp.Children.Keys) { if (!Containers.TryGetContainer(ent, GetPartSlotContainerId(slotId), out var container)) @@ -127,15 +158,60 @@ protected virtual void RemovePart( { Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false); Dirty(partEnt, partEnt.Comp); - partEnt.Comp.Body = null; + partEnt.Comp.ParentSlot = null; + partEnt.Comp.OriginalBody = partEnt.Comp.Body; var ev = new BodyPartRemovedEvent(slotId, partEnt); RaiseLocalEvent(bodyEnt, ref ev); - RemoveLeg(partEnt, bodyEnt); + RemovePartEffect(partEnt, bodyEnt); PartRemoveDamage(bodyEnt, partEnt); } + protected virtual void DropPart(Entity partEnt) + { + ChangeSlotState(partEnt, true); + + // We then detach the part, which will kickstart EntRemovedFromContainer events. + if (TryComp(partEnt, out TransformComponent? transform) && _gameTiming.IsFirstTimePredicted) + { + var ev = new BodyPartEnableChangedEvent(false); + RaiseLocalEvent(partEnt, ref ev); + SharedTransform.AttachToGridOrMap(partEnt, transform); + _randomHelper.RandomOffset(partEnt, 0.5f); + } + + } + + + /// + /// This function handles disabling or enabling equipment slots when an entity is + /// missing all of a given part type, or they get one added to them. + /// It is called right before dropping a part, or right after adding one. + /// + public void ChangeSlotState(Entity partEnt, bool disable) + { + if (partEnt.Comp.Body is not null) + Log.Debug($"Attempting to change slot state to {disable} for {partEnt.Comp.PartType}. Number of parts: {GetBodyPartCount(partEnt.Comp.Body.Value, partEnt.Comp.PartType)}"); + if (partEnt.Comp.Body is not null + && GetBodyPartCount(partEnt.Comp.Body.Value, partEnt.Comp.PartType) == 1 + && TryGetPartSlotContainerName(partEnt.Comp.PartType, out var containerNames)) + { + Log.Debug($"Found container names {containerNames}, with a number of {containerNames.Count}"); + foreach (var containerName in containerNames) + { + Log.Debug($"Setting slot state to {disable} for {containerName}"); + _inventorySystem.SetSlotStatus(partEnt.Comp.Body.Value, containerName, disable); + var ev = new RefreshInventorySlotsEvent(containerName); + RaiseLocalEvent(partEnt.Comp.Body.Value, ev); + } + } + } + + private void OnAmputateAttempt(Entity partEnt, ref AmputateAttemptEvent args) + { + DropPart(partEnt); + } private void AddLeg(Entity legEnt, Entity bodyEnt) { if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) @@ -159,12 +235,33 @@ private void RemoveLeg(Entity legEnt, Entity bodyEnt.Comp.LegEntities.Remove(legEnt); UpdateMovementSpeed(bodyEnt); Dirty(bodyEnt, bodyEnt.Comp); + Standing.Down(bodyEnt); + } + } + + // TODO: Refactor this crap. + private void RemovePartEffect(Entity partEnt, Entity bodyEnt) + { + if (TerminatingOrDeleted(bodyEnt) || !Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) + return; - if (!bodyEnt.Comp.LegEntities.Any()) + if (partEnt.Comp.Children.Any()) + { + foreach (var slotId in partEnt.Comp.Children.Keys) { - Standing.Down(bodyEnt); + if (Containers.TryGetContainer(partEnt, GetPartSlotContainerId(slotId), out var container) && + container is ContainerSlot slot && + slot.ContainedEntity is { } childEntity && + TryComp(childEntity, out BodyPartComponent? childPart)) + { + var ev = new BodyPartEnableChangedEvent(false); + RaiseLocalEvent(childEntity, ref ev); + DropPart((childEntity, childPart)); + } } + Dirty(bodyEnt, bodyEnt.Comp); } + } private void PartRemoveDamage(Entity bodyEnt, Entity partEnt) @@ -177,9 +274,74 @@ private void PartRemoveDamage(Entity bodyEnt, Entity("Bloodloss"), 300); - Damageable.TryChangeDamage(bodyEnt, damage); + var damage = new DamageSpecifier(Prototypes.Index("Bloodloss"), partEnt.Comp.VitalDamage); + Damageable.TryChangeDamage(bodyEnt, damage, partMultiplier: 0f); + } + } + + private void OnPartEnableChanged(Entity partEnt, ref BodyPartEnableChangedEvent args) + { + partEnt.Comp.Enabled = args.Enabled; + Dirty(partEnt, partEnt.Comp); + + if (args.Enabled) + EnablePart(partEnt); + else + DisablePart(partEnt); + } + + private void EnablePart(Entity partEnt) + { + if (!TryComp(partEnt.Comp.Body, out BodyComponent? body)) + return; + + // I hate having to hardcode these checks so much. + if (partEnt.Comp.PartType == BodyPartType.Leg) + { + AddLeg(partEnt, (partEnt.Comp.Body.Value, body)); + } + + if (partEnt.Comp.PartType == BodyPartType.Arm) + { + var hand = GetBodyChildrenOfType(partEnt.Comp.Body.Value, BodyPartType.Hand, symmetry: partEnt.Comp.Symmetry).FirstOrDefault(); + if (hand != default) + { + var ev = new BodyPartEnabledEvent(hand); + RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev); + } + } + + if (partEnt.Comp.PartType == BodyPartType.Hand) + { + var ev = new BodyPartEnabledEvent(partEnt); + RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev); + } + } + + private void DisablePart(Entity partEnt) + { + if (!TryComp(partEnt.Comp.Body, out BodyComponent? body)) + return; + + if (partEnt.Comp.PartType == BodyPartType.Leg) + { + RemoveLeg(partEnt, (partEnt.Comp.Body.Value, body)); + } + + if (partEnt.Comp.PartType == BodyPartType.Arm) + { + var hand = GetBodyChildrenOfType(partEnt.Comp.Body.Value, BodyPartType.Hand, symmetry: partEnt.Comp.Symmetry).FirstOrDefault(); + if (hand != default) + { + var ev = new BodyPartDisabledEvent(hand); + RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev); + } + } + + if (partEnt.Comp.PartType == BodyPartType.Hand) + { + var ev = new BodyPartDisabledEvent(partEnt); + RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev); } } @@ -438,12 +600,24 @@ public bool AttachPart( return false; } + if (!Containers.TryGetContainer(parentPartId, GetPartSlotContainerId(slot.Id), out var container)) { DebugTools.Assert($"Unable to find body slot {slot.Id} for {ToPrettyString(parentPartId)}"); return false; } + part.ParentSlot = slot; + + if (TryComp(part.Body, out HumanoidAppearanceComponent? bodyAppearance) + && !HasComp(partId)) + { + var appearance = AddComp(partId); + appearance.OriginalBody = part.Body; + appearance.Color = bodyAppearance.SkinColor; + UpdateAppearance(partId, appearance); + } + return Containers.Insert(partId, container); } @@ -656,11 +830,12 @@ public bool BodyHasChild( public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildrenOfType( EntityUid bodyId, BodyPartType type, - BodyComponent? body = null) + BodyComponent? body = null, + BodyPartSymmetry? symmetry = null) { foreach (var part in GetBodyChildren(bodyId, body)) { - if (part.Component.PartType == type) + if (part.Component.PartType == type && (symmetry == null || part.Component.Symmetry == symmetry)) yield return part; } } @@ -722,6 +897,48 @@ public bool TryGetBodyPartOrganComponents( return false; } + /// + /// Tries to get a list of ValueTuples of EntityUid and OrganComponent on each organ + /// in the given part. + /// + /// The part entity id to check on. + /// The type of component to check for. + /// The part to check for organs on. + /// Whether any were found. + /// + /// This method is somewhat of a copout to the fact that we can't use reflection to generically + /// get the type of a component on runtime due to sandboxing. So we simply do a HasComp check for each organ. + /// + public bool TryGetBodyPartOrgans( + EntityUid uid, + Type type, + [NotNullWhen(true)] out List<(EntityUid Id, OrganComponent Organ)>? organs, + BodyPartComponent? part = null) + { + if (!Resolve(uid, ref part)) + { + organs = null; + return false; + } + + var list = new List<(EntityUid Id, OrganComponent Organ)>(); + + foreach (var organ in GetPartOrgans(uid, part)) + { + if (HasComp(organ.Id, type)) + list.Add((organ.Id, organ.Component)); + } + + if (list.Count != 0) + { + organs = list; + return true; + } + + organs = null; + return false; + } + /// /// Gets the parent body part and all immediate child body parts for the partId. /// @@ -790,5 +1007,31 @@ public bool TryGetBodyPartAdjacentPartsComponents( return false; } + private bool TryGetPartSlotContainerName(BodyPartType partType, out HashSet containerNames) + { + containerNames = partType switch + { + BodyPartType.Arm => new() { "gloves" }, + BodyPartType.Leg => new() { "shoes" }, + BodyPartType.Head => new() { "eyes", "ears", "head", "mask" }, + _ => new() + }; + return containerNames.Count > 0; + } + + public int GetBodyPartCount(EntityUid bodyId, BodyPartType partType, BodyComponent? body = null) + { + if (!Resolve(bodyId, ref body, logMissing: false)) + return 0; + + int count = 0; + foreach (var part in GetBodyChildren(bodyId, body)) + { + if (part.Component.PartType == partType) + count++; + } + return count; + } + #endregion } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs index a45966fcc37..a00fb0ba4df 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.cs @@ -28,7 +28,7 @@ public abstract partial class SharedBodySystem : EntitySystem /// public const string OrganSlotContainerIdPrefix = "body_organ_slot_"; - [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] protected readonly IPrototypeManager Prototypes = default!; [Dependency] protected readonly DamageableSystem Damageable = default!; [Dependency] protected readonly MovementSpeedModifierSystem Movement = default!; @@ -42,6 +42,7 @@ public override void Initialize() InitializeBody(); InitializeParts(); + InitializeBkm(); // backmen } /// diff --git a/Content.Shared/Corvax/CCCVars/CCCVars.cs b/Content.Shared/Corvax/CCCVars/CCCVars.cs index 2808da945af..306d5b3845e 100644 --- a/Content.Shared/Corvax/CCCVars/CCCVars.cs +++ b/Content.Shared/Corvax/CCCVars/CCCVars.cs @@ -77,4 +77,11 @@ public sealed class CCCVars /// public static readonly CVarDef PeacefulRoundEnd = CVarDef.Create("game.peaceful_end", true, CVar.SERVERONLY); + + #region Surgery + + public static readonly CVarDef CanOperateOnSelf = + CVarDef.Create("surgery.can_operate_on_self", false, CVar.SERVERONLY); + + #endregion } diff --git a/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs b/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs index cc3b3f6d5d9..1bbc2d8080b 100644 --- a/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs +++ b/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs @@ -1,6 +1,8 @@ using Content.Shared.Administration.Logs; +using Content.Shared.Backmen.Targeting; using Content.Shared.Damage.Components; using Content.Shared.Database; +using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Popups; @@ -58,8 +60,23 @@ private void OnHandInteract(Entity entity, ref Intera totalDamage = DamageSpecifier.ApplyModifierSet(totalDamage, protectiveEntity.Comp.DamageProtection); } } + // start-backmen: surgery - totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target); + TargetBodyPart? targetPart = null; + var hands = CompOrNull(args.User); + if (hands is { ActiveHand: not null }) + { + targetPart = hands.ActiveHand.Location switch + { + HandLocation.Left => TargetBodyPart.LeftArm, + HandLocation.Right => TargetBodyPart.RightArm, + _ => null + }; + } + + // end-backmen: surgery + + totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target, targetPart: targetPart); if (totalDamage != null && totalDamage.AnyPositive()) { diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index 3c3e1b736df..5723b27668d 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -7,9 +7,11 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Radiation.Events; using Content.Shared.Rejuvenate; +using Content.Shared.Backmen.Targeting; using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Shared.Damage @@ -20,10 +22,11 @@ public sealed class DamageableSystem : EntitySystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; - + [Dependency] private readonly IRobustRandom _random = default!; private EntityQuery _appearanceQuery; private EntityQuery _damageableQuery; private EntityQuery _mindContainerQuery; + private EntityQuery _targetingQuery; public override void Initialize() { @@ -36,6 +39,7 @@ public override void Initialize() _appearanceQuery = GetEntityQuery(); _damageableQuery = GetEntityQuery(); _mindContainerQuery = GetEntityQuery(); + _targetingQuery = GetEntityQuery(); } /// @@ -97,10 +101,31 @@ public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpeci /// The damage changed event is used by other systems, such as damage thresholds. /// public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null, - bool interruptsDoAfters = true, EntityUid? origin = null) + bool interruptsDoAfters = true, EntityUid? origin = null, bool canSever = true, float partMultiplier = 1.00f, + TargetBodyPart? targetPart = null) { + ; component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup); component.TotalDamage = component.Damage.GetTotal(); + // If our target has a TargetingComponent, that means they will take limb damage + // And if their attacker also has one, then we use that part. + if (_targetingQuery.TryComp(uid, out var target)) + { + if (targetPart != null) + { + // keep from args + } + else if (origin.HasValue && _targetingQuery.TryComp(origin.Value, out var targeter)) + { + targetPart = targeter.Target; + } + else + { + targetPart = GetRandomBodyPart(uid, target); + } + } + + Dirty(uid, component); if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null) @@ -108,7 +133,7 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList()); _appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance); } - RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin)); + RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin, targetPart, canSever, partMultiplier)); } /// @@ -124,7 +149,8 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp /// null if the user had no applicable components that can take damage. /// public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false, - bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null) + bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null, + bool? canSever = true, float? partMultiplier = 1.00f, TargetBodyPart? targetPart = null) { if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false)) { @@ -137,7 +163,7 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp return damage; } - var before = new BeforeDamageChangedEvent(damage, origin); + var before = new BeforeDamageChangedEvent(damage, origin, targetPart); RaiseLocalEvent(uid.Value, ref before); if (before.Cancelled) @@ -153,8 +179,7 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp // use a local private field instead of creating a new dictionary here.. damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); } - - var ev = new DamageModifyEvent(damage, origin); + var ev = new DamageModifyEvent(damage, origin, targetPart); RaiseLocalEvent(uid.Value, ev); damage = ev.Damage; @@ -186,7 +211,7 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp } if (delta.DamageDict.Count > 0) - DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin); + DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin, canSever ?? true, partMultiplier ?? 1.00f, targetPart); return delta; } @@ -280,13 +305,35 @@ private void DamageableHandleState(EntityUid uid, DamageableComponent component, DamageChanged(uid, component, delta); } } + + public TargetBodyPart? GetRandomBodyPart(EntityUid uid, TargetingComponent? target = null) + { + if (!Resolve(uid, ref target)) + return null; + + var totalWeight = target.TargetOdds.Values.Sum(); + var randomValue = _random.NextFloat() * totalWeight; + + foreach (var (part, weight) in target.TargetOdds) + { + if (randomValue <= weight) + return part; + randomValue -= weight; + } + + return TargetBodyPart.Torso; // Default to torso if something goes wrong + } } /// /// Raised before damage is done, so stuff can cancel it if necessary. /// [ByRefEvent] - public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false); + public record struct BeforeDamageChangedEvent( + DamageSpecifier Damage, + EntityUid? Origin = null, + TargetBodyPart? targetPart = null, + bool Cancelled = false); /// /// Raised on an entity when damage is about to be dealt, @@ -303,12 +350,14 @@ public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent public readonly DamageSpecifier OriginalDamage; public DamageSpecifier Damage; public EntityUid? Origin; + public readonly TargetBodyPart? TargetPart; - public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null) + public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null, TargetBodyPart? targetPart = null) { OriginalDamage = damage; Damage = damage; Origin = origin; + TargetPart = targetPart; } } @@ -331,10 +380,15 @@ public sealed class DamageChangedEvent : EntityEventArgs public readonly DamageSpecifier? DamageDelta; /// - /// Was any of the damage change dealing damage, or was it all healing? + /// Was any of the change dealing damage? /// public readonly bool DamageIncreased; + /// + /// Was any of the change healing? + /// + public readonly bool DamageDecreased; + /// /// Does this event interrupt DoAfters? /// Note: As provided in the constructor, this *does not* account for DamageIncreased. @@ -347,12 +401,30 @@ public sealed class DamageChangedEvent : EntityEventArgs /// public readonly EntityUid? Origin; - public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin) + /// + /// How much do we multiply this damage by for part damage? + /// + public readonly float PartMultiplier; + + /// + /// Can this damage event sever parts? + /// + public readonly bool CanSever; + + /// + /// What part of this entity is going to take damage? + /// + public readonly TargetBodyPart? TargetPart; + + public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, + EntityUid? origin, TargetBodyPart? targetPart = null, bool canSever = true, float partMultiplier = 1.00f) { Damageable = damageable; DamageDelta = damageDelta; Origin = origin; - + TargetPart = targetPart; + CanSever = canSever; + PartMultiplier = partMultiplier; if (DamageDelta == null) return; @@ -363,6 +435,11 @@ public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damag DamageIncreased = true; break; } + else if (damageChange < 0) + { + DamageDecreased = true; + break; + } } InterruptsDoAfters = interruptsDoAfters && DamageIncreased; } diff --git a/Content.Shared/Hands/Components/HandsComponent.cs b/Content.Shared/Hands/Components/HandsComponent.cs index b3cb51ae359..3147606a71d 100644 --- a/Content.Shared/Hands/Components/HandsComponent.cs +++ b/Content.Shared/Hands/Components/HandsComponent.cs @@ -30,6 +30,7 @@ public sealed partial class HandsComponent : Component /// /// List of hand-names. These are keys for . The order of this list determines the order in which hands are iterated over. /// + [ViewVariables] public List SortedHands = new(); /// diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 863d9da970f..46f8afe4cd5 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -59,6 +59,12 @@ public static class ContentKeyFunctions public static readonly BoundKeyFunction ZoomOut = "ZoomOut"; public static readonly BoundKeyFunction ZoomIn = "ZoomIn"; public static readonly BoundKeyFunction ResetZoom = "ResetZoom"; + public static readonly BoundKeyFunction TargetHead = "TargetHead"; + public static readonly BoundKeyFunction TargetTorso = "TargetTorso"; + public static readonly BoundKeyFunction TargetLeftArm = "TargetLeftArm"; + public static readonly BoundKeyFunction TargetRightArm = "TargetRightArm"; + public static readonly BoundKeyFunction TargetLeftLeg = "TargetLeftLeg"; + public static readonly BoundKeyFunction TargetRightLeg = "TargetRightLeg"; public static readonly BoundKeyFunction ArcadeUp = "ArcadeUp"; public static readonly BoundKeyFunction ArcadeDown = "ArcadeDown"; diff --git a/Content.Shared/Inventory/InventoryComponent.cs b/Content.Shared/Inventory/InventoryComponent.cs index 629cf1169c4..db761ad693b 100644 --- a/Content.Shared/Inventory/InventoryComponent.cs +++ b/Content.Shared/Inventory/InventoryComponent.cs @@ -15,6 +15,7 @@ public sealed partial class InventoryComponent : Component [DataField("speciesId")] public string? SpeciesId { get; set; } public SlotDefinition[] Slots = Array.Empty(); + public ContainerSlot[] Containers = Array.Empty(); [DataField] diff --git a/Content.Shared/Inventory/InventorySystem.Slots.cs b/Content.Shared/Inventory/InventorySystem.Slots.cs index 2522dd5d0a3..dbaea2a5ec3 100644 --- a/Content.Shared/Inventory/InventorySystem.Slots.cs +++ b/Content.Shared/Inventory/InventorySystem.Slots.cs @@ -1,17 +1,19 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Inventory.Events; using Content.Shared.Storage; +using Content.Shared.Random; using Robust.Shared.Containers; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Content.Shared.Inventory; - public partial class InventorySystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IViewVariablesManager _vvm = default!; - + [Dependency] private readonly RandomHelperSystem _randomHelper = default!; + [Dependency] private readonly ISerializationManager _serializationManager = default!; private void InitializeSlots() { SubscribeLocalEvent(OnInit); @@ -57,7 +59,8 @@ protected virtual void OnInit(EntityUid uid, InventoryComponent component, Compo if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate)) return; - component.Slots = invTemplate.Slots; + _serializationManager.CopyTo(invTemplate.Slots, ref component.Slots, notNullableOverride: true); + component.Containers = new ContainerSlot[component.Slots.Length]; for (var i = 0; i < component.Containers.Length; i++) { @@ -115,7 +118,7 @@ public bool TryGetSlot(EntityUid uid, string slot, [NotNullWhen(true)] out SlotD foreach (var slotDef in inventory.Slots) { - if (!slotDef.Name.Equals(slot)) + if (!slotDef.Name.Equals(slot) || slotDef.Disabled) continue; slotDefinition = slotDef; return true; @@ -170,6 +173,39 @@ private IEnumerable ListViewVariablesSlots(EntityUid uid, InventoryCompo } } + public void SetSlotStatus(EntityUid uid, string slotName, bool isDisabled, InventoryComponent? inventory = null) + { + if (!Resolve(uid, ref inventory)) + return; + + foreach (var slot in inventory.Slots) + { + if (slot.Name != slotName) + continue; + + + if (!TryGetSlotContainer(uid, slotName, out var container, out _, inventory)) + break; + + if (isDisabled) + { + if (container.ContainedEntity is { } entityUid && TryComp(entityUid, out TransformComponent? transform) && _gameTiming.IsFirstTimePredicted) + { + _transform.AttachToGridOrMap(entityUid, transform); + _randomHelper.RandomOffset(entityUid, 0.5f); + } + _containerSystem.ShutdownContainer(container); + } + else + _containerSystem.EnsureContainer(uid, slotName); + + slot.Disabled = isDisabled; + break; + } + + Dirty(uid, inventory); + } + /// /// Enumerator for iterating over an inventory's slot containers. Also has methods that skip empty containers. /// It should be safe to add or remove items while enumerating. @@ -203,7 +239,7 @@ public bool MoveNext([NotNullWhen(true)] out ContainerSlot? container) var i = _nextIdx++; var slot = _slots[i]; - if ((slot.SlotFlags & _flags) == 0) + if ((slot.SlotFlags & _flags) == 0 || slot.Disabled) continue; container = _containers[i]; @@ -221,7 +257,7 @@ public bool NextItem(out EntityUid item) var i = _nextIdx++; var slot = _slots[i]; - if ((slot.SlotFlags & _flags) == 0) + if ((slot.SlotFlags & _flags) == 0 || slot.Disabled) continue; var container = _containers[i]; diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs index a4d77767e37..cc70a842314 100644 --- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs +++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs @@ -55,4 +55,9 @@ public sealed partial class SlotDefinition /// Entity blacklist for CanEquip checks. /// [DataField("blacklist")] public EntityWhitelist? Blacklist = null; + + /// + /// Is this slot disabled? Could be due to severing or other reasons. + /// + [DataField("disabled")] public bool Disabled = false; } diff --git a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs index 08af1a36a7b..b993f0727eb 100644 --- a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs +++ b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs @@ -1,3 +1,5 @@ +using Content.Shared.Backmen.Targeting; +using Content.Shared.Body.Components; using Robust.Shared.Serialization; namespace Content.Shared.MedicalScanner; @@ -14,8 +16,9 @@ public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage public bool? ScanMode; public bool? Bleeding; public bool? Unrevivable; + public Dictionary? Body; // backmen: surgery - public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable) + public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable, Dictionary? body) { TargetEntity = targetEntity; Temperature = temperature; @@ -23,6 +26,7 @@ public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperatu ScanMode = scanMode; Bleeding = bleeding; Unrevivable = unrevivable; + Body = body; // backmen: surgery } } diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index 95abea63db0..bb83bd3caee 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -44,7 +44,7 @@ public override void Update(float frameTime) continue; } - foreach (var state in status.ActiveEffects) + foreach (var state in status.ActiveEffects.ToArray()) { if (curTime > state.Value.Cooldown.Item2) TryRemoveStatusEffect(uid, state.Key, status); diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs index 212c03475cf..df158d52c3f 100644 --- a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs +++ b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs @@ -90,6 +90,12 @@ public sealed partial class MeleeWeaponComponent : Component [ViewVariables(VVAccess.ReadWrite), DataField] public FixedPoint2 ClickDamageModifier = FixedPoint2.New(1); + /// + /// Part damage is multiplied by this amount for single-target attacks + /// + [DataField, AutoNetworkedField] + public float ClickPartDamageMultiplier = 1.00f; + // TODO: Temporarily 1.5 until interactionoutline is adjusted to use melee, then probably drop to 1.2 /// /// Nearest edge range to hit an entity. @@ -97,6 +103,24 @@ public sealed partial class MeleeWeaponComponent : Component [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] public float Range = 1.5f; + /// + /// Attack range for heavy swings + /// + [DataField, AutoNetworkedField] + public float HeavyRangeModifier = 1f; + + /// + /// Weapon damage is multiplied by this amount for heavy swings + /// + [DataField, AutoNetworkedField] + public float HeavyDamageBaseModifier = 1.2f; + + /// + /// Part damage is multiplied by this amount for heavy swings + /// + [DataField, AutoNetworkedField] + public float HeavyPartDamageMultiplier = 0.5f; + /// /// Total width of the angle for wide attacks. /// diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 767b5c4ef62..fd6c5215c60 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -511,7 +511,7 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity RaiseLocalEvent(target.Value, attackedEvent); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList); - var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user, ignoreResistances:resistanceBypass); + var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin: user, ignoreResistances:resistanceBypass, partMultiplier: component.ClickPartDamageMultiplier); if (damageResult is {Empty: false}) { @@ -666,7 +666,7 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU RaiseLocalEvent(entity, attackedEvent); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList); - var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin:user); + var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin: user, partMultiplier: component.HeavyPartDamageMultiplier); if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero) { diff --git a/Resources/Audio/Medical/Surgery/attributions.yml b/Resources/Audio/Medical/Surgery/attributions.yml new file mode 100644 index 00000000000..c88a3e0b70f --- /dev/null +++ b/Resources/Audio/Medical/Surgery/attributions.yml @@ -0,0 +1,49 @@ +- files: ["cautery1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/cautery1.ogg" + +- files: ["cautery2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/cautery2.ogg" + +- files: ["hemostat.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/hemostat.ogg" + +- files: ["organ1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/organ1.ogg" + +- files: ["organ2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/organ2.ogg" + +- files: ["retractor1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/retractor1.ogg" + +- files: ["retractor2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/retractor2.ogg" + +- files: ["saw.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/saw.ogg" + +- files: ["scalpel1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/scalpel1.ogg" + +- files: ["scalpel2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from cmss13" + source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/scalpel2.ogg" \ No newline at end of file diff --git a/Resources/Audio/Medical/Surgery/cautery1.ogg b/Resources/Audio/Medical/Surgery/cautery1.ogg new file mode 100644 index 00000000000..fbd9f2b4d86 Binary files /dev/null and b/Resources/Audio/Medical/Surgery/cautery1.ogg differ diff --git a/Resources/Audio/Medical/Surgery/cautery2.ogg b/Resources/Audio/Medical/Surgery/cautery2.ogg new file mode 100644 index 00000000000..cc91e9f3ce6 Binary files /dev/null and b/Resources/Audio/Medical/Surgery/cautery2.ogg differ diff --git a/Resources/Audio/Medical/Surgery/hemostat1.ogg b/Resources/Audio/Medical/Surgery/hemostat1.ogg new file mode 100644 index 00000000000..e624bafafbf Binary files /dev/null and b/Resources/Audio/Medical/Surgery/hemostat1.ogg differ diff --git a/Resources/Audio/Medical/Surgery/organ1.ogg b/Resources/Audio/Medical/Surgery/organ1.ogg new file mode 100644 index 00000000000..37eaffc1a34 Binary files /dev/null and b/Resources/Audio/Medical/Surgery/organ1.ogg differ diff --git a/Resources/Audio/Medical/Surgery/organ2.ogg b/Resources/Audio/Medical/Surgery/organ2.ogg new file mode 100644 index 00000000000..43b22f8354c Binary files /dev/null and b/Resources/Audio/Medical/Surgery/organ2.ogg differ diff --git a/Resources/Audio/Medical/Surgery/retractor1.ogg b/Resources/Audio/Medical/Surgery/retractor1.ogg new file mode 100644 index 00000000000..70625c961cf Binary files /dev/null and b/Resources/Audio/Medical/Surgery/retractor1.ogg differ diff --git a/Resources/Audio/Medical/Surgery/retractor2.ogg b/Resources/Audio/Medical/Surgery/retractor2.ogg new file mode 100644 index 00000000000..94548ec2504 Binary files /dev/null and b/Resources/Audio/Medical/Surgery/retractor2.ogg differ diff --git a/Resources/Audio/Medical/Surgery/saw.ogg b/Resources/Audio/Medical/Surgery/saw.ogg new file mode 100644 index 00000000000..62623f6aa3a Binary files /dev/null and b/Resources/Audio/Medical/Surgery/saw.ogg differ diff --git a/Resources/Audio/Medical/Surgery/scalpel1.ogg b/Resources/Audio/Medical/Surgery/scalpel1.ogg new file mode 100644 index 00000000000..f292d1024da Binary files /dev/null and b/Resources/Audio/Medical/Surgery/scalpel1.ogg differ diff --git a/Resources/Audio/Medical/Surgery/scalpel2.ogg b/Resources/Audio/Medical/Surgery/scalpel2.ogg new file mode 100644 index 00000000000..7335f3d9cef Binary files /dev/null and b/Resources/Audio/Medical/Surgery/scalpel2.ogg differ diff --git a/Resources/Locale/en-US/backmen/guidebook/guides.ftl b/Resources/Locale/en-US/backmen/guidebook/guides.ftl new file mode 100644 index 00000000000..217d0bfacc7 --- /dev/null +++ b/Resources/Locale/en-US/backmen/guidebook/guides.ftl @@ -0,0 +1,4 @@ +guide-entry-surgery = Surgery +guide-entry-partmanipulation = Part Manipulation +guide-entry-organmanipulation = Organ Manipulation +guide-entry-utilitysurgeries = Utility Surgeries diff --git a/Resources/Locale/en-US/backmen/surgery/surgery-ui.ftl b/Resources/Locale/en-US/backmen/surgery/surgery-ui.ftl new file mode 100644 index 00000000000..fd1c45fcb47 --- /dev/null +++ b/Resources/Locale/en-US/backmen/surgery/surgery-ui.ftl @@ -0,0 +1,11 @@ +surgery-ui-window-title = Surgery +surgery-ui-window-require = Requires +surgery-ui-window-parts = < Parts +surgery-ui-window-surgeries = < Surgeries +surgery-ui-window-steps = < Steps +surgery-ui-window-steps-error-skills = You have no surgical skills. +surgery-ui-window-steps-error-table = You need an operating table for this. +surgery-ui-window-steps-error-armor = You need to remove their armor! +surgery-ui-window-steps-error-tools = You're missing tools for this surgery. +surgery-error-laying = They need to be laying down! +surgery-error-self-surgery = You can't perform surgery on yourself! diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index bcc5a994b8b..95ec42409c3 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -110,6 +110,7 @@ ui-options-header-camera = Camera ui-options-header-interaction-basic = Basic Interaction ui-options-header-interaction-adv = Advanced Interaction ui-options-header-ui = User Interface +ui-options-header-targeting = Targeting ui-options-header-misc = Miscellaneous ui-options-header-hotbar = Hotbar ui-options-header-shuttle = Shuttle @@ -162,6 +163,13 @@ ui-options-function-rotate-object-clockwise = Rotate clockwise ui-options-function-rotate-object-counterclockwise = Rotate counterclockwise ui-options-function-flip-object = Flip +ui-options-function-target-head = Target head +ui-options-function-target-torso = Target torso +ui-options-function-target-left-arm = Target left arm +ui-options-function-target-right-arm = Target right arm +ui-options-function-target-left-leg = Target left leg +ui-options-function-target-right-leg = Target right leg + ui-options-function-focus-chat-input-window = Focus chat ui-options-function-focus-local-chat-window = Focus chat (IC) ui-options-function-focus-emote = Focus chat (Emote) diff --git a/Resources/Locale/ru-RU/backmen/guidebook/guides.ftl b/Resources/Locale/ru-RU/backmen/guidebook/guides.ftl new file mode 100644 index 00000000000..f25aefc662f --- /dev/null +++ b/Resources/Locale/ru-RU/backmen/guidebook/guides.ftl @@ -0,0 +1,4 @@ +guide-entry-surgery = Хирургия +guide-entry-partmanipulation = Манипулирование конечностями +guide-entry-organmanipulation = Манипулирование органами +guide-entry-utilitysurgeries = Особые применения diff --git a/Resources/Locale/ru-RU/backmen/surgery/surgery-items.ftl b/Resources/Locale/ru-RU/backmen/surgery/surgery-items.ftl new file mode 100644 index 00000000000..15cb777981e --- /dev/null +++ b/Resources/Locale/ru-RU/backmen/surgery/surgery-items.ftl @@ -0,0 +1,33 @@ +ent-MedicalBiofabricator = медицинский биофабрикатор + .desc = Производит искусственные органы, которые могут быть установлены пациентам. Потребляет биомассу. + +ent-BioSynthHeart = био-синтетическое сердце + .desc = Искуственное сердце, которое может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthLiver = био-синтетическая печень + .desc = Искуственная печень, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthLungs = био-синтетические лёгкие + .desc = Искуственные лёгкие, которые могут устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthEyes = био-синтетические глаза + .desc = Искуственные глаза, которые могут устанавливаться и адаптироваться к любому живому организму. + +ent-BioSynthLeftArm = био-синтетическая левая рука + .desc = Искуственная рука, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthRightArm = био-синтетическая правая рука + .desc = Искуственная рука, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthLeftHand = био-синтетическая левая ладонь + .desc = Искуственная ладонь, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthRightHand = био-синтетическая правая ладонь + .desc = Искуственная ладонь, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthLeftLeg = био-синтетическая левая нога + .desc = Искуственная нога, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthRightLeg = био-синтетическая правая нога + .desc = Искуственная нога, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthLeftFoot = био-синтетическая левая стопа + .desc = Искуственная стопа, которая может устанавливаться и адаптироваться к любому живому организму. +ent-BioSynthRightFoot = био-синтетическая правая стопа + .desc = Искуственная стопа, которая может устанавливаться и адаптироваться к любому живому организму. + +ent-MedicalBiofabMachineBoard = медицинский биофабрикатор (машинная плата) + .desc = Машинная плата, необходимая для создания медицинского биофабрикатора. +ent-BoneGel = бутылка костяного геля + .desc = Контейнер для костяного геля, нужно время от времени пополнять в специальной машине. diff --git a/Resources/Locale/ru-RU/backmen/surgery/surgery-operations.ftl b/Resources/Locale/ru-RU/backmen/surgery/surgery-operations.ftl new file mode 100644 index 00000000000..dd7bfaeee9b --- /dev/null +++ b/Resources/Locale/ru-RU/backmen/surgery/surgery-operations.ftl @@ -0,0 +1,61 @@ +ent-SurgeryStepOpenIncisionScalpel = Порезать при помощи скальпеля +ent-SurgeryStepClampBleeders = Остановить внешнее кровотечение +ent-SurgeryStepRetractSkin = Втянуть кожу +ent-SurgeryStepSawBones = Пропилить кость +ent-SurgeryStepPriseOpenBones = Вскрыть кость +ent-SurgeryStepCloseBones = Закрыть кость +ent-SurgeryStepMendRibcage = Восстановить кость +ent-SurgeryStepCloseIncision = Закрыть разрез + +ent-SurgeryStepInsertFeature = Прикрепить часть тела +ent-SurgeryStepSealWounds = Прижечь раны +ent-SurgeryStepSawFeature = Пропилить через кость +ent-SurgeryStepClampInternalBleeders = Остановить внутреннее кровотечение +ent-SurgeryStepRemoveFeature = Ампутировать часть тела + +ent-SurgeryStepCarefulIncisionScalpel = Сделать аккуратный разрез +ent-SurgeryStepRepairBruteTissue = Восстановить механически поврежденную ткань +ent-SurgeryStepRepairBurnTissue = Восстановить термически поврежденную ткань +ent-SurgeryStepSealTendWound = Прижечь кровотечение + +ent-SurgeryStepInsertItem = Вставить предмет в полость +ent-SurgeryStepRemoveItem = Достать предмет из полости + +ent-SurgeryStepRemoveOrgan = Удалить орган +ent-SurgeryStepInsertOrgan = Вставить орган +ent-SurgeryStepInsertLungs = Вставить лёгкие +ent-SurgeryStepInsertLiver = Вставить печень +ent-SurgeryStepInsertEyes = Вставить глаза +ent-SurgeryStepInsertHeart = Вставить сердце + + +ent-SurgeryOpenIncision = Открыть разрез +ent-SurgeryCloseIncision = Закрыть разрез +ent-SurgeryOpenRibcage = Открыть грудную клетку +ent-SurgeryRemovePart = Удалить часть тела + +ent-SurgeryAttachHead = Прикрепить голову +ent-SurgeryAttachLeftArm = Прикрепить левую руку +ent-SurgeryAttachRightArm = Прикрепить правую руку +ent-SurgeryAttachLeftLeg = Прикрепить левую ногу +ent-SurgeryAttachRightLeg = Прикрепить правую ногу +ent-SurgeryAttachLeftHand = Прикрепить левую ладонь +ent-SurgeryAttachRightHand = Прикрепить правую ладонь +ent-SurgeryAttachLeftFoot = Прикрепить левую стопу +ent-SurgeryAttachRightFoot = Прикрепить правую стопу + +ent-SurgeryTendWoundsBrute = Восстановить механические повреждения +ent-SurgeryTendWoundsBurn = Восстановить термические повреждения + +ent-SurgeryInsertItem = Вставить предмет + +ent-SurgeryRemoveBrain = Удалить мозг +ent-SurgeryInsertBrain = Вставить мозг +ent-SurgeryRemoveHeart = Удалить сердце +ent-SurgeryInsertHeart = Вставить сердце +ent-SurgeryRemoveLiver = Удалить печень +ent-SurgeryInsertLiver = Вставить печень +ent-SurgeryRemoveLungs = Удалить лёгкие +ent-SurgeryInsertLungs = Вставить лёгкие +ent-SurgeryRemoveEyes = Удалить глаза +ent-SurgeryInsertEyes = Вставить глаза diff --git a/Resources/Locale/ru-RU/backmen/surgery/surgery-ui.ftl b/Resources/Locale/ru-RU/backmen/surgery/surgery-ui.ftl new file mode 100644 index 00000000000..169797ccd7c --- /dev/null +++ b/Resources/Locale/ru-RU/backmen/surgery/surgery-ui.ftl @@ -0,0 +1,20 @@ +ui-options-function-target-head = Прицелиться на голову +ui-options-function-target-torso = Прицелиться на тело +ui-options-function-target-left-arm = Прицелиться на левую руку +ui-options-function-target-right-arm = Прицелиться на правую руку +ui-options-function-target-left-leg = Прицелиться на левую ногу +ui-options-function-target-right-leg = Прицелиться на правую ногу +ui-options-header-targeting = Прицеливание + +surgery-ui-window-title = Хирургия +surgery-ui-window-require = Требуется +surgery-ui-window-parts = Части +surgery-ui-window-surgeries = Операции +surgery-ui-window-steps = Шаги + +surgery-ui-window-steps-error-skills = Не хватает навыка для операции! +surgery-ui-window-steps-error-table = Требуется операционный стол! +surgery-ui-window-steps-error-armor = Нужно снять с этого броню! +surgery-ui-window-steps-error-tools = Не хватает инструментов! +surgery-error-laying = Оно должно быть в положении лёжа! +surgery-error-self-surgery = Вы не можете провести операцию на самом себе! diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/structures/furniture/tables/operating_table.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/structures/furniture/tables/operating_table.ftl index bc4fad5d91c..c6b9ca05fd1 100644 --- a/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/structures/furniture/tables/operating_table.ftl +++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/structures/furniture/tables/operating_table.ftl @@ -1,2 +1,2 @@ ent-OperatingTable = операционный стол - .desc = Специальный медицинский стол для проведения операций. Впрочем, сейчас это просто бесполезный реквизит. + .desc = Специальный медицинский стол для проведения операций. Смотря на него у вас неожиданно возникает ощущение быстрого течения времени... diff --git a/Resources/Prototypes/Body/Organs/human.yml b/Resources/Prototypes/Body/Organs/human.yml index cb1492b8a6a..6882fa8c0ad 100644 --- a/Resources/Prototypes/Body/Organs/human.yml +++ b/Resources/Prototypes/Body/Organs/human.yml @@ -3,36 +3,36 @@ 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: 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: entity id: BaseHumanOrgan parent: BaseHumanOrganUnGibbable abstract: true components: - - type: Gibbable + - type: Gibbable - type: entity id: OrganHumanBrain @@ -40,54 +40,60 @@ name: brain description: "The source of incredible, unending intelligence. Honk." components: - - type: Sprite - state: brain - - type: Organ - - type: Input - context: "ghost" - - type: Brain - - type: InputMover - - type: Examiner - - type: BlockMovement - - type: BadFood - - type: Tag - tags: - - Meat - - type: SolutionContainerManager - solutions: - organ: - reagents: - - ReagentId: Nutriment - Quantity: 10 - food: - maxVol: 5 - reagents: - - ReagentId: GreyMatter - Quantity: 5 - - type: FlavorProfile - flavors: - - people - - type: FoodSequenceElement - entries: - Burger: Brain - Taco: Brain - - type: Item - size: Small - heldPrefix: brain - + - type: Sprite + state: brain + - type: Organ + slotId: brain # backmen: surgery + - type: Input + context: "ghost" + - type: Brain + - type: InputMover + - type: Examiner + - type: BlockMovement + - type: BadFood + - type: Tag + tags: + - Meat + - type: SolutionContainerManager + solutions: + organ: + reagents: + - ReagentId: Nutriment + Quantity: 10 + food: + maxVol: 5 + reagents: + - ReagentId: GreyMatter + Quantity: 5 + - type: FlavorProfile + flavors: + - people + - type: FoodSequenceElement + entries: + Burger: Brain + Taco: Brain + - type: Item + size: Small + heldPrefix: brain + - type: entity id: OrganHumanEyes parent: BaseHumanOrgan name: eyes description: "I see you!" components: - - type: Sprite - layers: - - state: eyeball-l - - state: eyeball-r - - type: Item - size: Small - heldPrefix: eyeballs + # start-backmen: surgery + - type: Organ + slotId: eyes + - type: Eyes + # end-backmen: surgery + - type: Sprite + layers: + - state: eyeball-l + - state: eyeball-r + - type: Item + size: Small + heldPrefix: eyeballs - type: entity id: OrganHumanTongue @@ -95,19 +101,19 @@ name: tongue description: "A fleshy muscle mostly used for lying." components: - - type: Sprite - state: tongue + - type: Sprite + state: tongue - type: entity id: OrganHumanAppendix parent: BaseHumanOrgan name: appendix components: - - type: Sprite - layers: - - state: appendix - - state: appendix-inflamed - visible: false + - type: Sprite + layers: + - state: appendix + - state: appendix-inflamed + visible: false - type: entity id: OrganHumanEars @@ -115,8 +121,8 @@ name: ears description: "There are three parts to the ear. Inner, middle and outer. Only one of these parts should normally be visible." components: - - type: Sprite - state: ears + - type: Sprite + state: ears - type: entity id: OrganHumanLungs @@ -124,36 +130,38 @@ name: lungs description: "Filters oxygen from an atmosphere, which is then sent into the bloodstream to be used as an electron carrier." components: - - type: Sprite - layers: - - state: lung-l - - state: lung-r - - type: Item - size: Small - heldPrefix: lungs - - type: Lung - - type: Metabolizer - 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: Sprite + layers: + - state: lung-l + - state: lung-r + - type: Item + size: Small + heldPrefix: lungs + - type: Lung + - type: Organ # backmen: surgery + slotId: lungs + - type: Metabolizer + 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: OrganHumanHeart @@ -161,21 +169,26 @@ name: heart description: "I feel bad for the heartless bastard who lost this." components: - - type: Sprite - state: heart-on - # 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 - - type: Item - size: Small - heldPrefix: heart + # start-backmen: surgery + - type: Heart + - type: Organ + slotId: heart + # end-backmen: surgery + - type: Sprite + state: heart-on + # 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 + - type: Item + size: Small + heldPrefix: heart - type: entity id: OrganHumanStomach @@ -183,31 +196,33 @@ name: stomach description: "Gross. This is hard to stomach." components: - - type: Sprite - state: stomach - - type: Item - size: Small - heldPrefix: stomach - - type: SolutionContainerManager - solutions: - stomach: - maxVol: 50 - food: - maxVol: 5 - reagents: - - ReagentId: UncookedAnimalProteins - Quantity: 5 - - 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 + - type: Sprite + state: stomach + - type: Item + size: Small + heldPrefix: stomach + - type: SolutionContainerManager + solutions: + stomach: + maxVol: 50 + food: + maxVol: 5 + reagents: + - ReagentId: UncookedAnimalProteins + Quantity: 5 + - type: Stomach + - type: Organ # backmen: surgery + slotId: 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 - type: entity id: OrganHumanLiver @@ -215,17 +230,22 @@ name: liver description: "Pairing suggestion: chianti and fava beans." components: - - type: Sprite - state: liver - - type: Item - size: Small - heldPrefix: 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 + # start-backmen: surgery + - type: Liver + - type: Organ + slotId: liver + # end-backmen: surgery + - type: Sprite + state: liver + - type: Item + size: Small + heldPrefix: 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 - type: entity id: OrganHumanKidneys @@ -233,15 +253,15 @@ name: kidneys description: "Filters toxins from the bloodstream." components: - - type: Sprite - layers: - - state: kidney-l - - state: kidney-r - - type: Item - size: Small - heldPrefix: kidneys - # The kidneys just remove anything that doesn't currently have any metabolisms, as a stopgap. - - type: Metabolizer - maxReagents: 5 - metabolizerTypes: [Human] - removeEmpty: true + - type: Sprite + layers: + - state: kidney-l + - state: kidney-r + - type: Item + size: Small + heldPrefix: kidneys + # The kidneys just remove anything that doesn't currently have any metabolisms, as a stopgap. + - type: Metabolizer + maxReagents: 5 + metabolizerTypes: [Human] + removeEmpty: true diff --git a/Resources/Prototypes/Body/Parts/base.yml b/Resources/Prototypes/Body/Parts/base.yml index 836d0f140af..bb5bdcca27f 100644 --- a/Resources/Prototypes/Body/Parts/base.yml +++ b/Resources/Prototypes/Body/Parts/base.yml @@ -9,6 +9,13 @@ - type: Damageable damageContainer: Biological - type: BodyPart + # start-backmen: surgery + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/organ1.ogg + endSound: + path: /Audio/Medical/Surgery/organ2.ogg + # end-backmen: surgery - type: Gibbable - type: ContainerContainer containers: @@ -19,6 +26,37 @@ - type: Tag tags: - Trash + # start-backmen: surgery + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Blunt + damage: 50 + behaviors: + - !type:GibPartBehavior { } + - trigger: + !type:DamageTypeTrigger + damageType: Slash + damage: 100 + behaviors: + - !type:GibPartBehavior { } + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 200 + behaviors: + - !type:SpawnEntitiesBehavior + spawnInContainer: true + spawn: + Ash: + min: 1 + max: 1 + - !type:BurnBodyBehavior { } + - !type:PlaySoundBehavior + sound: + collection: MeatLaserImpact + # end-backmen: surgery - type: entity id: BaseTorso @@ -28,6 +66,13 @@ components: - type: BodyPart partType: Torso + # start-backmen: surgery + toolName: "a torso" + containerName: "torso_slot" + - type: ContainerContainer + containers: + torso_slot: !type:ContainerSlot {} + # end-backmen: surgery - type: entity id: BaseHead @@ -37,6 +82,7 @@ components: - type: BodyPart partType: Head + toolName: "a head" # backmen: surgery vital: true - type: Input context: "ghost" @@ -53,6 +99,7 @@ - type: BodyPart partType: Arm symmetry: Left + toolName: "a left arm" # backmen: surgery - type: entity id: BaseRightArm @@ -63,6 +110,7 @@ - type: BodyPart partType: Arm symmetry: Right + toolName: "a right arm" # backmen: surgery - type: entity id: BaseLeftHand @@ -73,6 +121,7 @@ - type: BodyPart partType: Hand symmetry: Left + toolName: "a left hand" # backmen: surgery - type: entity id: BaseRightHand @@ -83,6 +132,7 @@ - type: BodyPart partType: Hand symmetry: Right + toolName: "a right hand" # backmen: surgery - type: entity id: BaseLeftLeg @@ -93,6 +143,7 @@ - type: BodyPart partType: Leg symmetry: Left + toolName: "a left leg" # backmen: surgery - type: MovementBodyPart - type: entity @@ -104,6 +155,7 @@ - type: BodyPart partType: Leg symmetry: Right + toolName: "a right leg" # backmen: surgery - type: MovementBodyPart - type: entity @@ -115,6 +167,7 @@ - type: BodyPart partType: Foot symmetry: Left + toolName: "a left foot" # backmen: surgery - type: entity id: BaseRightFoot @@ -125,3 +178,4 @@ - type: BodyPart partType: Foot symmetry: Right + toolName: "a right foot" # backmen: surgery diff --git a/Resources/Prototypes/Body/Prototypes/a_ghost.yml b/Resources/Prototypes/Body/Prototypes/a_ghost.yml index 7d358a57622..a2eaef3dd58 100644 --- a/Resources/Prototypes/Body/Prototypes/a_ghost.yml +++ b/Resources/Prototypes/Body/Prototypes/a_ghost.yml @@ -6,17 +6,17 @@ torso: part: TorsoHuman connections: - - right_arm - - left_arm - right_arm: + - right arm + - left arm + right arm: part: RightArmHuman connections: - - right_hand - left_arm: + - right hand + left arm: part: LeftArmHuman connections: - - left_hand - right_hand: + - left hand + right hand: part: RightHandHuman - left_hand: + left hand: part: LeftHandHuman diff --git a/Resources/Prototypes/Body/Prototypes/human.yml b/Resources/Prototypes/Body/Prototypes/human.yml index 94c77a27d73..7a0f3bb5a7b 100644 --- a/Resources/Prototypes/Body/Prototypes/human.yml +++ b/Resources/Prototypes/Body/Prototypes/human.yml @@ -13,37 +13,37 @@ torso: part: TorsoHuman connections: - - right_arm - - left_arm - - right_leg - - left_leg + - right arm + - left arm + - right leg + - left leg organs: heart: OrganHumanHeart lungs: OrganHumanLungs stomach: OrganHumanStomach liver: OrganHumanLiver kidneys: OrganHumanKidneys - right_arm: + right arm: part: RightArmHuman connections: - - right_hand - left_arm: + - right hand + left arm: part: LeftArmHuman connections: - - left_hand - right_hand: + - left hand + right hand: part: RightHandHuman - left_hand: + left hand: part: LeftHandHuman - right_leg: + right leg: part: RightLegHuman connections: - - right_foot - left_leg: + - right foot + left leg: part: LeftLegHuman connections: - - left_foot - right_foot: + - left foot + right foot: part: RightFootHuman - left_foot: + left foot: part: LeftFootHuman diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml index 75b3a290659..b99c99321d8 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml @@ -12,6 +12,7 @@ - id: Cautery - id: Retractor - id: Scalpel + - id: BoneGel # backmen: surgery - type: entity parent: ClothingBackpackDuffelSyndicateMedicalBundle @@ -29,6 +30,7 @@ - id: ScalpelAdvanced - id: ClothingHandsGlovesNitrile - id: EmergencyRollerBedSpawnFolded + - id: BoneGel # backmen: surgery - type: entity parent: ClothingBackpackDuffelSyndicateBundle diff --git a/Resources/Prototypes/Catalog/Fills/Crates/medical.yml b/Resources/Prototypes/Catalog/Fills/Crates/medical.yml index fe04f72899a..24edb83f4f1 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/medical.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/medical.yml @@ -67,7 +67,11 @@ - id: Drill - id: Saw - id: Hemostat - - id: ClothingMaskSterile + # start-backmen: surgery + - id: BoneGel + - id: BoxLatexGloves + - id: BoxSterileMask + # end-backmen: surgery - type: entity id: CrateMedicalScrubs diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index c1631537022..9e54ea81a1c 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -268,9 +268,9 @@ - PillCanister - Radio - DiscreteHealthAnalyzer - - SurgeryTool - Dropper components: + - SurgeryTool - Hypospray - Injector - Pill diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index e2dd9ac3f30..6f3ed454218 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1227,6 +1227,9 @@ abstract: true components: - type: CombatMode + #- type: SurgeryTarget + # canOperate: false + #- type: Targeting - type: Inventory templateId: monkey speciesId: monkey @@ -1248,6 +1251,13 @@ layer: - MobLayer - type: Stripping + - type: Strippable + - type: UserInterface + interfaces: + enum.StrippingUiKey.Key: + type: StrippableBoundUserInterface + #enum.SurgeryUIKey.Key: + # type: SurgeryBui - type: Sprite drawdepth: Mobs layers: diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 100a0ced842..403a43715f6 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -187,6 +187,8 @@ type: HumanoidMarkingModifierBoundUserInterface enum.StrippingUiKey.Key: type: StrippableBoundUserInterface + enum.SurgeryUIKey.Key: # backmen: surgery + type: SurgeryBui - type: Puller - type: Speech speechSounds: Alto @@ -211,6 +213,8 @@ - FootstepSound - DoorBumpOpener - AnomalyHost + - type: Targeting # backmen: surgery + - type: SurgeryTarget # backmen: surgery - type: entity save: false @@ -223,6 +227,7 @@ id: BaseMobSpeciesOrganic abstract: true components: + - type: Targeting # backmen: surgery - type: Barotrauma damage: types: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml index 5b0c97dc837..64ff84d34cf 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml @@ -8,9 +8,10 @@ - type: Sprite - type: StaticPrice price: 20 - - type: Tag - tags: - - SurgeryTool +# - type: Tag +# tags: +# - SurgeryTool + - type: SurgeryTool # backmen: surgery # Cautery @@ -32,6 +33,14 @@ Heat: 5 soundHit: path: /Audio/Effects/lightburn.ogg + # start-backmen: surgery + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/cautery1.ogg + endSound: + path: /Audio/Medical/Surgery/cautery2.ogg + - type: Cautery + # end-backmen: surgery # Drill @@ -57,6 +66,12 @@ path: /Audio/Items/drill_hit.ogg - type: StaticPrice price: 40 + # start-backmen: surgery + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/saw.ogg + - type: SurgicalDrill + # end-backmen: surgery # Scalpel @@ -90,6 +105,15 @@ Slash: 8 soundHit: path: /Audio/Weapons/bladeslice.ogg + # start-backmen: surgery + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/scalpel1.ogg + endSound: + path: /Audio/Medical/Surgery/scalpel2.ogg + - type: Scalpel + # end-backmen: surgery + - type: entity name: shiv @@ -142,25 +166,63 @@ - type: Item sprite: Objects/Specific/Medical/Surgery/scissors.rsi storedRotation: 90 + # start-backmen: surgery + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/retractor1.ogg + endSound: + path: /Audio/Medical/Surgery/retractor2.ogg + - type: Retractor + # end-backmen: surgery - type: entity name: hemostat id: Hemostat - parent: Retractor + parent: BaseToolSurgery # backmen description: A surgical tool used to compress blood vessels to prevent bleeding. components: - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi state: hemostat - type: Item heldPrefix: hemostat + sprite: Objects/Specific/Medical/Surgery/scissors.rsi storedRotation: 90 + # start-backmen: surgery + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/retractor1.ogg + endSound: + path: /Audio/Medical/Surgery/hemostat1.ogg + - type: Hemostat + # end-backmen: surgery + +# Bone setter +- type: entity + parent: BaseToolSurgery + id: Bonesetter + name: bone setter + description: Used for setting bones back into place. + components: + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/bonesetter.rsi + state: bonesetter + - type: Item + sprite: Objects/Specific/Medical/Surgery/bonesetter.rsi + - type: BoneSetter + +# Bone Gel +- type: entity + parent: BaseToolSurgery + id: BoneGel + name: bottle of bone gel + description: A container for bone gel that often needs to be refilled from a specialized machine. + components: + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/bone_gel.rsi + state: bone-gel + - type: BoneGel - # - type: entity - # name: bone setter - # id: BoneSetter - # parent: Retractor - # description: A surgical tool used for setting bones. - # components: # Saws - type: entity @@ -183,7 +245,20 @@ - type: Tool qualities: - Sawing -# No melee for regular saw because have you ever seen someone use a band saw as a weapon? It's dumb. + speedModifier: 1.0 + # start-backmen: surgery + - type: MeleeWeapon + attackRate: 0.75 + range: 1.35 + damage: + types: + Blunt: 2.5 + Slash: 6.5 + angle: 20 + - type: BoneSaw + # end-backmen: surgery +# --No melee for regular saw because have you ever seen someone use a band saw as a weapon? It's dumb.-- +# No, I'm going to saw through your bones. - type: entity name: choppa @@ -202,8 +277,13 @@ Brute: 10 soundHit: path: /Audio/Weapons/bladeslice.ogg + # start-backmen: surgery - type: Tool + qualities: + - Sawing speedModifier: 0.5 + - type: BoneSaw + # end-backmen: surgery - type: entity name: circular saw @@ -221,8 +301,16 @@ Brute: 15 soundHit: path: /Audio/Items/drill_hit.ogg + # start-backmen: surgery - type: Tool + qualities: + - Sawing speedModifier: 1.5 + - type: SurgeryTool + startSound: + path: /Audio/Medical/Surgery/saw.ogg + - type: BoneSaw + # end-backmen: surgery - type: entity name: advanced circular saw @@ -236,5 +324,10 @@ heldPrefix: advanced - type: MeleeWeapon attackRate: 1.5 + # start-backmen: surgery - type: Tool + qualities: + - Sawing speedModifier: 2.0 + - type: BoneSaw + # end-backmen: surgery diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/operating_table.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/operating_table.yml index 75cffd91f54..38b351d770a 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/operating_table.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/operating_table.yml @@ -11,3 +11,4 @@ - type: Icon sprite: Structures/Furniture/Tables/optable.rsi state: operating_table + - type: OperatingTable # backmen diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 72b56059146..f95baefd437 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -932,6 +932,7 @@ - MedkitRadiation - MedkitCombat - Scalpel + - BoneGel - Retractor - Cautery - Drill diff --git a/Resources/Prototypes/EntityLists/Tools/surgery.yml b/Resources/Prototypes/EntityLists/Tools/surgery.yml index 20f689d272d..072a754c501 100644 --- a/Resources/Prototypes/EntityLists/Tools/surgery.yml +++ b/Resources/Prototypes/EntityLists/Tools/surgery.yml @@ -1,10 +1,14 @@ - type: entityList id: surgerytools entities: + - Scalpel - Cautery - Drill - ScalpelLaser - Retractor - Hemostat - - SawAdvanced -# - Drapes + - SawElectric + - Bonesetter + - BoneGel + - Saw +# - Drapes \ No newline at end of file diff --git a/Resources/Prototypes/Guidebook/medical.yml b/Resources/Prototypes/Guidebook/medical.yml index 0c7b1c781e4..422d7d6faab 100644 --- a/Resources/Prototypes/Guidebook/medical.yml +++ b/Resources/Prototypes/Guidebook/medical.yml @@ -7,6 +7,7 @@ - Chemist - Cloning - Cryogenics + - Surgery - type: guideEntry id: Medical Doctor @@ -50,3 +51,31 @@ id: AdvancedBrute name: guide-entry-brute text: "/ServerInfo/Guidebook/Medical/AdvancedBrute.xml" + +- type: guideEntry + id: Surgery + name: guide-entry-surgery + text: "/ServerInfo/Corvax/Guidebook/Medical/Surgery.xml" + children: + - Part Manipulation + - Organ Manipulation + - Utility Surgeries + +- type: guideEntry + id: Part Manipulation + name: guide-entry-partmanipulation + text: "/ServerInfo/Corvax/Guidebook/Medical/PartManipulation.xml" + filterEnabled: true + +- type: guideEntry + id: Organ Manipulation + name: guide-entry-organmanipulation + text: "/ServerInfo/Corvax/Guidebook/Medical/OrganManipulation.xml" + filterEnabled: true + +- type: guideEntry + id: Utility Surgeries + name: guide-entry-utilitysurgeries + text: "/ServerInfo/Corvax/Guidebook/Medical/UtilitySurgeries.xml" + filterEnabled: true + diff --git a/Resources/Prototypes/Reagents/gases.yml b/Resources/Prototypes/Reagents/gases.yml index 9ef508feead..7af730061e1 100644 --- a/Resources/Prototypes/Reagents/gases.yml +++ b/Resources/Prototypes/Reagents/gases.yml @@ -243,23 +243,13 @@ - !type:ReagentThreshold reagent: NitrousOxide min: 0.2 + max: 0.5 - !type:OrganType type: Slime shouldHave: false emote: Laugh showInChat: true probability: 0.1 - - !type:Emote - conditions: - - !type:ReagentThreshold - reagent: NitrousOxide - min: 0.2 - - !type:OrganType - type: Slime - shouldHave: false - emote: Scream - showInChat: true - probability: 0.01 - !type:PopupMessage conditions: - !type:ReagentThreshold @@ -286,13 +276,13 @@ conditions: - !type:ReagentThreshold reagent: NitrousOxide - min: 1.8 + min: 1 - !type:OrganType type: Slime shouldHave: false key: ForcedSleep component: ForcedSleeping - time: 3 + time: 6 # This reeks, but I guess it works LMAO type: Add - !type:HealthChange conditions: diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml index cf9a10cd23c..54fc72191b9 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml @@ -14,6 +14,14 @@ - Medical - Chemistry - Maintenance + special: + - !type:AddComponentSpecial + components: + #start-backmen: surgery + #- type: CPRTraining + - type: SurgerySpeedModifier + SpeedModifier: 1.75 + #end-backmen: surgery - type: startingGear id: ChemistGear diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index fa6ddec09c2..e46d8d30c55 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -32,11 +32,13 @@ - Brig - Cryogenics special: + #start-backmen: surgery + #- type: CPRTraining + - type: SurgerySpeedModifier + SpeedModifier: 2.5 + #end-backmen: surgery - !type:AddImplantSpecial implants: [ MindShieldImplant ] - - !type:AddComponentSpecial - components: - - type: CommandStaff - type: startingGear id: CMOGear diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml index 42c388f9b74..207c8f102cc 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml @@ -15,6 +15,14 @@ - Maintenance extendedAccess: - Chemistry + special: + - !type:AddComponentSpecial + components: + #start-backmen: surgery + #- type: CPRTraining + - type: SurgerySpeedModifier + SpeedModifier: 1.75 + #end-backmen: surgery - type: startingGear id: DoctorGear diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml index 75496e43f42..296801769fe 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml @@ -15,6 +15,14 @@ access: - Medical - Maintenance + special: + - !type:AddComponentSpecial + components: + #start-backmen: surgery + #- type: CPRTraining + - type: SurgerySpeedModifier + SpeedModifier: 1.5 + #end-backmen: surgery - type: startingGear id: MedicalInternGear diff --git a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml index 166f9ac42b7..da668777f56 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml @@ -17,6 +17,14 @@ - Maintenance extendedAccess: - Chemistry + special: + - !type:AddComponentSpecial + components: + #start-backmen: surgery + #- type: CPRTraining + - type: SurgerySpeedModifier + SpeedModifier: 1.75 + #end-backmen: surgery - type: startingGear id: ParamedicGear diff --git a/Resources/Prototypes/_Backmen/Entities/Objects/Devices/Circuitboards/production.yml b/Resources/Prototypes/_Backmen/Entities/Objects/Devices/Circuitboards/production.yml new file mode 100644 index 00000000000..547eaa54001 --- /dev/null +++ b/Resources/Prototypes/_Backmen/Entities/Objects/Devices/Circuitboards/production.yml @@ -0,0 +1,13 @@ +- type: entity + id: MedicalBiofabMachineBoard + parent: BaseMachineCircuitboard + name: medical biofab machine board + description: A machine printed circuit board for a medical biofab. + components: + - type: Sprite + state: medical + - type: MachineBoard + prototype: MedicalBiofabricator +# requirements: +# MatterBin: 2 +# Manipulator: 2 diff --git a/Resources/Prototypes/_Backmen/Entities/Objects/Specific/Medical/surgery.yml b/Resources/Prototypes/_Backmen/Entities/Objects/Specific/Medical/surgery.yml new file mode 100644 index 00000000000..002de50e50a --- /dev/null +++ b/Resources/Prototypes/_Backmen/Entities/Objects/Specific/Medical/surgery.yml @@ -0,0 +1,76 @@ +# ORGANS + +- type: entity + parent: OrganHumanHeart + id: BioSynthHeart + name: bio-synthetic heart + description: This heart can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: OrganHumanLiver + id: BioSynthLiver + name: bio-synthetic liver + description: This liver can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: OrganHumanLungs + id: BioSynthLungs + name: bio-synthetic lungs + description: These lungs can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: OrganHumanEyes + id: BioSynthEyes + name: bio-synthetic eyes + description: These eyes can be transplanted into any living organism and it will adapt to its recipient. + + +# PARTS + +- type: entity + parent: LeftArmHuman + id: BioSynthLeftArm + name: bio-synthetic left arm + description: This left arm can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: RightArmHuman + id: BioSynthRightArm + name: bio-synthetic right arm + description: This right arm can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: LeftHandHuman + id: BioSynthLeftHand + name: bio-synthetic left hand + description: This left hand can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: RightHandHuman + id: BioSynthRightHand + name: bio-synthetic right hand + description: This right hand can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: LeftLegHuman + id: BioSynthLeftLeg + name: bio-synthetic left leg + description: This left leg can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: RightLegHuman + id: BioSynthRightLeg + name: bio-synthetic right leg + description: This right leg can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: LeftFootHuman + id: BioSynthLeftFoot + name: bio-synthetic left foot + description: This left foot can be transplanted into any living organism and it will adapt to its recipient. + +- type: entity + parent: RightFootHuman + id: BioSynthRightFoot + name: bio-synthetic right foot + description: This right foot can be transplanted into any living organism and it will adapt to its recipient. diff --git a/Resources/Prototypes/_Backmen/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/_Backmen/Entities/Structures/Machines/lathe.yml new file mode 100644 index 00000000000..f6e944a67b6 --- /dev/null +++ b/Resources/Prototypes/_Backmen/Entities/Structures/Machines/lathe.yml @@ -0,0 +1,42 @@ +- type: entity + id: MedicalBiofabricator + parent: BaseLathe + name: medical biofabricator + description: Produces organs and other organic matter that can be surgically grafted onto patients with biomass. + components: + - type: Sprite + sprite: Structures/Machines/limbgrower.rsi + snapCardinals: true + layers: + - state: limbgrower_idleoff + map: ["enum.LatheVisualLayers.IsRunning"] +# - state: limbgrower_idleoff +# shader: unshaded +# map: ["enum.PowerDeviceVisualLayers.Powered"] +# - state: inserting +# map: ["enum.MaterialStorageVisualLayers.Inserting"] +# - state: panel +# map: ["enum.WiresVisualLayers.MaintenancePanel"] + - type: Machine + board: MedicalBiofabMachineBoard + - type: MaterialStorage + whitelist: + tags: + - Sheet + - RawMaterial + - type: Lathe + idleState: limbgrower_idleoff + runningState: limbgrower_idleon + staticRecipes: + - SynthLiver + - SynthHeart + - SynthLungs + - SynthEyes + - SynthLeftLeg + - SynthRightLeg + - SynthLeftFoot + - SynthRightFoot + - SynthLeftArm + - SynthRightArm + - SynthLeftHand + - SynthRightHand diff --git a/Resources/Prototypes/_Backmen/Entities/Surgery/surgeries.yml b/Resources/Prototypes/_Backmen/Entities/Surgery/surgeries.yml new file mode 100644 index 00000000000..1729cdd306c --- /dev/null +++ b/Resources/Prototypes/_Backmen/Entities/Surgery/surgeries.yml @@ -0,0 +1,471 @@ +- type: entity + id: SurgeryBase + categories: [ HideSpawnMenu ] + +- type: entity + parent: SurgeryBase + id: SurgeryOpenIncision + name: Open Incision + categories: [ HideSpawnMenu ] + components: + - type: Surgery + steps: + - SurgeryStepOpenIncisionScalpel + - SurgeryStepRetractSkin + - SurgeryStepClampBleeders + - type: SurgeryPartPresentCondition + +- type: entity + parent: SurgeryBase + id: SurgeryCloseIncision + name: Close Incision + categories: [ HideSpawnMenu ] + components: + - type: Surgery + priority: 1 + steps: + - SurgeryStepCloseBones + - SurgeryStepMendRibcage + - SurgeryStepCloseIncision + - type: SurgeryPartPresentCondition + +- type: entity + parent: SurgeryBase + id: SurgeryOpenRibcage + name: Open Ribcage + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepSawBones + - SurgeryStepPriseOpenBones + - type: SurgeryPartCondition + part: Torso + +- type: entity + parent: SurgeryBase + id: SurgeryRemovePart + name: Remove Part + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepSawFeature + - SurgeryStepClampInternalBleeders + - SurgeryStepRemoveFeature + - type: SurgeryPartCondition + part: Torso + inverse: true + +# I fucking hate hardcoding all of this shit to accomodate for surgery BUI. +# If anyone can give me pointers on how to make it better I'd be incredibly grateful. + +- type: entity + parent: SurgeryBase + id: SurgeryAttachHead + name: Attach Head + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryPartRemovedCondition + part: Head + +- type: entity + parent: SurgeryBase + id: SurgeryAttachLeftArm + name: Attach Left Arm + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryPartRemovedCondition + part: Arm + symmetry: Left + +- type: entity + parent: SurgeryBase + id: SurgeryAttachRightArm + name: Attach Right Arm + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryPartRemovedCondition + part: Arm + symmetry: Right + +- type: entity + parent: SurgeryBase + id: SurgeryAttachLeftLeg + name: Attach Left Leg + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryPartRemovedCondition + part: Leg + symmetry: Left + +- type: entity + parent: SurgeryBase + id: SurgeryAttachRightLeg + name: Attach Right Leg + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryPartRemovedCondition + part: Leg + symmetry: Right + +- type: entity + parent: SurgeryBase + id: SurgeryAttachLeftHand + name: Attach Left Han + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Arm + symmetry: Left + - type: SurgeryPartRemovedCondition + part: Hand + symmetry: Left + +- type: entity + parent: SurgeryBase + id: SurgeryAttachRightHand + name: Attach Right Hand + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Arm + symmetry: Right + - type: SurgeryPartRemovedCondition + part: Hand + symmetry: Right + +- type: entity + parent: SurgeryBase + id: SurgeryAttachLeftFoot + name: Attach Left Foot + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Leg + symmetry: Left + - type: SurgeryPartRemovedCondition + part: Foot + symmetry: Left + +- type: entity + parent: SurgeryBase + id: SurgeryAttachRightFoot + name: Attach Right Foot + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepInsertFeature + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Leg + symmetry: Right + - type: SurgeryPartRemovedCondition + part: Foot + symmetry: Right + +#- type: entity +# parent: SurgeryBase +# id: SurgeryAlienEmbryoRemoval +# name: Alien Embryo Removal +# description: Removal of an alien embryo from the body. +# categories: [ HideSpawnMenu ] +# components: +# - type: Surgery +# priority: -1 +# requirement: SurgeryOpenRibcage +# steps: +# - SurgeryStepCutLarvaRoots +# - SurgeryStepRemoveLarva +# - type: SurgeryLarvaCondition +# - type: SurgeryPartCondition +# part: Torso + +- type: entity + parent: SurgeryBase + id: SurgeryTendWoundsBrute + name: Tend Bruise Wounds + categories: [ HideSpawnMenu ] + components: + - type: Surgery + steps: + - SurgeryStepCarefulIncisionScalpel + - SurgeryStepRepairBruteTissue + - SurgeryStepSealTendWound + - type: SurgeryWoundedCondition + +- type: entity + parent: SurgeryBase + id: SurgeryTendWoundsBurn + name: Tend Burn Wounds + categories: [ HideSpawnMenu ] + components: + - type: Surgery + steps: + - SurgeryStepCarefulIncisionScalpel + - SurgeryStepRepairBurnTissue + - SurgeryStepSealTendWound + - type: SurgeryWoundedCondition + +- type: entity + parent: SurgeryBase + id: SurgeryInsertItem + name: Cavity Implant + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepInsertItem + - SurgeryStepRemoveItem + - type: SurgeryPartCondition + part: Torso + +# Note for any Organ manipulation surgeries. Most of the organs are only defined on the server. +# I added some of them to the client too, but we should probably move them to a shared +# prototype at some point. + +- type: entity + parent: SurgeryBase + id: SurgeryRemoveBrain + name: Remove Brain + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepSawBones + - SurgeryStepClampInternalBleeders + - SurgeryStepRemoveOrgan + - type: SurgeryPartCondition + part: Head + - type: SurgeryOrganCondition + organ: + - type: Brain + +- type: entity + parent: SurgeryBase + id: SurgeryInsertBrain + name: Insert Brain + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepSawBones + - SurgeryStepInsertOrgan + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Head + - type: SurgeryOrganCondition + organ: + - type: Brain + inverse: true + reattaching: true + + +- type: entity + parent: SurgeryBase + id: SurgeryRemoveHeart + name: Remove Heart + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepSawBones + - SurgeryStepClampInternalBleeders + - SurgeryStepRemoveOrgan + - type: SurgeryPartCondition + part: Torso + - type: SurgeryOrganCondition + organ: + - type: Heart + +- type: entity + parent: SurgeryBase + id: SurgeryInsertHeart + name: Insert Heart + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepSawBones + - SurgeryStepInsertHeart + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryOrganCondition + organ: + - type: Heart + inverse: true + reattaching: true + +- type: entity + parent: SurgeryBase + id: SurgeryRemoveLiver + name: Remove Liver + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepSawBones + - SurgeryStepClampInternalBleeders + - SurgeryStepRemoveOrgan + - type: SurgeryPartCondition + part: Torso + - type: SurgeryOrganCondition + organ: + - type: Liver + +- type: entity + parent: SurgeryBase + id: SurgeryInsertLiver + name: Insert Liver + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepSawBones + - SurgeryStepInsertLiver + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryOrganCondition + organ: + - type: Liver + inverse: true + reattaching: true + +- type: entity + parent: SurgeryBase + id: SurgeryRemoveLungs + name: Remove Lungs + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepSawBones + - SurgeryStepClampInternalBleeders + - SurgeryStepRemoveOrgan + - type: SurgeryPartCondition + part: Torso + - type: SurgeryOrganCondition + organ: + - type: Lung + +- type: entity + parent: SurgeryBase + id: SurgeryInsertLungs + name: Insert Lungs + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenRibcage + steps: + - SurgeryStepSawBones + - SurgeryStepInsertLungs + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Torso + - type: SurgeryOrganCondition + organ: + - type: Lung + inverse: true + reattaching: true + +- type: entity + parent: SurgeryBase + id: SurgeryRemoveEyes + name: Remove Eyes + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepSawBones + - SurgeryStepClampInternalBleeders + - SurgeryStepRemoveOrgan + - type: SurgeryPartCondition + part: Head + - type: SurgeryOrganCondition + organ: + - type: Eyes + +- type: entity + parent: SurgeryBase + id: SurgeryInsertEyes + name: Insert Eyes + categories: [ HideSpawnMenu ] + components: + - type: Surgery + requirement: SurgeryOpenIncision + steps: + - SurgeryStepSawBones + - SurgeryStepInsertEyes + - SurgeryStepSealWounds + - type: SurgeryPartCondition + part: Head + - type: SurgeryOrganCondition + organ: + - type: Eyes + inverse: true + reattaching: true diff --git a/Resources/Prototypes/_Backmen/Entities/Surgery/surgery_steps.yml b/Resources/Prototypes/_Backmen/Entities/Surgery/surgery_steps.yml new file mode 100644 index 00000000000..ede2dce9060 --- /dev/null +++ b/Resources/Prototypes/_Backmen/Entities/Surgery/surgery_steps.yml @@ -0,0 +1,472 @@ +- type: entity + id: SurgeryStepBase + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepOpenIncisionScalpel + name: Cut with a scalpel + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Scalpel + add: + - type: IncisionOpen + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scalpel.rsi + state: scalpel + - type: SurgeryDamageChangeEffect + damage: + types: + Bloodloss: 10 + sleepModifier: 0.5 + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepClampBleeders + name: Clamp the bleeders + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Hemostat + add: + - type: BleedersClamped + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: hemostat + - type: SurgeryDamageChangeEffect + damage: + types: + Bloodloss: -5 + sleepModifier: 2 + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepRetractSkin + name: Retract the skin + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Retractor + add: + - type: SkinRetracted + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: retractor + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepSawBones + name: Saw through bones + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: BoneSaw + add: + - type: RibcageSawed + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/saw.rsi + state: saw + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepPriseOpenBones + name: Prise the bones open + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Retractor + add: + - type: RibcageOpen + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: retractor + +#- type: entity +# parent: SurgeryStepBase +# id: SurgeryStepCutLarvaRoots +# name: Cut larva roots +# categories: [ HideSpawnMenu ] +# components: +# - type: SurgeryStep +# skill: 2 +# tool: +# - type: Scalpel +# - type: SurgeryCutLarvaRootsStep +# - type: Sprite +# sprite: Objects/Specific/Medical/Surgery/scalpel.rsi +# state: scalpel +# - type: SurgeryOperatingTableCondition + +#- type: entity +# parent: SurgeryStepBase +# id: SurgeryStepRemoveLarva +# name: Remove larva +# categories: [ HideSpawnMenu ] +# components: +# - type: SurgeryStep +# skill: 2 +# tool: +# - type: Hemostat +# bodyRemove: +# - type: VictimInfected +# - type: Sprite +# sprite: Objects/Specific/Medical/Surgery/scissors.rsi +# state: hemostat +# - type: SurgeryOperatingTableCondition +# - type: SurgeryStepSpawnEffect +# entity: XenoEmbryo + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepCloseBones + name: Close bones + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Retractor + remove: + - type: RibcageOpen + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: retractor + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepMendRibcage + name: Mend ribcage + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: BoneGel + remove: + - type: RibcageSawed + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/bone_gel.rsi + state: bone-gel + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepCloseIncision + name: Close incision + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Cautery + remove: + # This surgery removes a bunch of components that might be leftover from other surgeries in unintended cases. + # Essentially a bit of a fallback for endusers :) + - type: SkinRetracted + - type: BleedersClamped + - type: IncisionOpen + - type: BodyPartReattached + - type: InternalBleedersClamped + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/cautery.rsi + state: cautery + - type: SurgeryDamageChangeEffect + damage: + types: + Heat: -5 + sleepModifier: 2 + - type: SurgeryStepEmoteEffect + +# Feature Insertion + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepInsertFeature + name: Insert part + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: BodyPart + add: + - type: BodyPartReattached + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/manipulation.rsi + state: insertion + - type: SurgeryAddPartStep + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepSealWounds + name: Seal wounds + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Cautery + remove: + - type: SkinRetracted + - type: BleedersClamped + - type: IncisionOpen + - type: BodyPartReattached + - type: InternalBleedersClamped + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/cautery.rsi + state: cautery + - type: SurgeryStepEmoteEffect + - type: SurgeryStepAffixPartEffect + - type: SurgeryDamageChangeEffect + damage: + types: + Heat: -5 + sleepModifier: 2 + +# Feature Removal + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepSawFeature + name: Saw through bones + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: BoneSaw + add: + - type: BodyPartSawed + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/saw.rsi + state: saw + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepClampInternalBleeders + name: Clamp internal bleeders + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Hemostat + add: + - type: InternalBleedersClamped + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: hemostat + - type: SurgeryDamageChangeEffect + damage: + types: + Bloodloss: -5 + sleepModifier: 2 + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepRemoveFeature + name: Amputate part + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: BoneSaw + remove: + - type: SkinRetracted + - type: BleedersClamped + - type: InternalBleedersClamped + - type: IncisionOpen + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/saw.rsi + state: saw + - type: SurgeryRemovePartStep + - type: SurgeryStepEmoteEffect + +# Tend Wounds + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepCarefulIncisionScalpel + name: Make a careful incision + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Scalpel + add: + - type: IncisionOpen + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scalpel.rsi + state: scalpel + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepRepairBruteTissue + name: Repair damaged tissue + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Hemostat + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: hemostat + - type: SurgeryTendWoundsEffect + damage: + groups: + Brute: -5 + - type: SurgeryRepeatableStep + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepRepairBurnTissue + name: Repair burnt tissue + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Hemostat + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: hemostat + - type: SurgeryTendWoundsEffect + mainGroup: Burn + damage: + groups: + Burn: -5 + - type: SurgeryRepeatableStep + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepSealTendWound + name: Seal the wound + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Cautery + remove: + - type: IncisionOpen + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/cautery.rsi + state: cautery + - type: SurgeryDamageChangeEffect + damage: + types: + Heat: -5 + sleepModifier: 2 + - type: SurgeryStepEmoteEffect + +# Cavity Implanting + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepInsertItem + name: Insert item into cavity + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/manipulation.rsi + state: insertion + - type: SurgeryStepCavityEffect + action: Insert + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepRemoveItem + name: Remove item from cavity + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/manipulation.rsi + state: insertion + - type: SurgeryStepCavityEffect + action: Remove + - type: SurgeryStepEmoteEffect + +# Organ Manipulation + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepRemoveOrgan + name: Remove organ + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Hemostat + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/scissors.rsi + state: hemostat + - type: SurgeryRemoveOrganStep + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepBase + id: SurgeryStepInsertOrgan + name: Add organ + categories: [ HideSpawnMenu ] + components: + - type: SurgeryStep + tool: + - type: Organ + add: + - type: OrganReattached + - type: Sprite + sprite: Objects/Specific/Medical/Surgery/manipulation.rsi + state: insertion + - type: SurgeryAddOrganStep + - type: SurgeryStepEmoteEffect + +- type: entity + parent: SurgeryStepInsertOrgan + id: SurgeryStepInsertLungs + name: Add lungs + categories: [ HideSpawnMenu ] + components: + - type: SurgeryDamageChangeEffect + damage: + types: + Asphyxiation: -2147483648 # Literally the max 32 bit value, if your patient has gone higher than this, maybe it's time to restart the round. + sleepModifier: 1 + isConsumable: true + +- type: entity + parent: SurgeryStepInsertOrgan + id: SurgeryStepInsertLiver + name: Add liver + categories: [ HideSpawnMenu ] + components: + - type: SurgeryDamageChangeEffect + damage: + types: + Poison: -2147483648 # Literally the max 32 bit value, if your patient has gone higher than this, maybe it's time to restart the round. + sleepModifier: 1 + isConsumable: true + +- type: entity + parent: SurgeryStepInsertOrgan + id: SurgeryStepInsertEyes + name: Add eyes + categories: [ HideSpawnMenu ] + components: + - type: SurgerySpecialDamageChangeEffect + damageType: Eye + isConsumable: true + +- type: entity + parent: SurgeryStepInsertOrgan + id: SurgeryStepInsertHeart + name: Add heart + categories: [ HideSpawnMenu ] + components: + - type: SurgerySpecialDamageChangeEffect + damageType: Rot + isConsumable: true diff --git a/Resources/Prototypes/_Backmen/Recipes/Lathes/surgery.yml b/Resources/Prototypes/_Backmen/Recipes/Lathes/surgery.yml new file mode 100644 index 00000000000..75def0ca802 --- /dev/null +++ b/Resources/Prototypes/_Backmen/Recipes/Lathes/surgery.yml @@ -0,0 +1,91 @@ +- type: latheRecipe + id: BoneGel + result: BoneGel + completetime: 2 + materials: + Plastic: 200 + Plasma: 200 + +- type: latheRecipe + id: SynthHeart + result: BioSynthHeart + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthLungs + result: BioSynthLungs + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthLiver + result: BioSynthLiver + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthEyes + result: BioSynthEyes + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthLeftArm + result: BioSynthLeftArm + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthLeftHand + result: BioSynthLeftHand + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthRightArm + result: BioSynthRightArm + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthRightHand + result: BioSynthRightHand + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthLeftLeg + result: BioSynthLeftLeg + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthRightLeg + result: BioSynthRightLeg + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthLeftFoot + result: BioSynthLeftFoot + completetime: 30 + materials: + Biomass: 10 + +- type: latheRecipe + id: SynthRightFoot + result: BioSynthRightFoot + completetime: 30 + materials: + Biomass: 10 diff --git a/Resources/ServerInfo/Corvax/Guidebook/Medical/OrganManipulation.xml b/Resources/ServerInfo/Corvax/Guidebook/Medical/OrganManipulation.xml new file mode 100644 index 00000000000..7c98372b1c6 --- /dev/null +++ b/Resources/ServerInfo/Corvax/Guidebook/Medical/OrganManipulation.xml @@ -0,0 +1,50 @@ + +# Манипуляция органами +Хирургия позволяет вам заменять органы у пациентов на здоровые, что позволит вам лечить некоторые виды хронических болезней. + +## Анатомия +Обычно в теле присутствуют следующие органы: + + + + + + + + + + + + + +Некоторые виды имеют больше или меньше органов, например у Дион функции множества из этих органов объединены в одном универсальном, но в целом это общее представление о том, что вы можете встретить. +Для удаления органа, вам необходимо провести соответствующую операцию Удалить Орган на части тела, в которой этот орган находится. С помощью этой процедуры вы сможете извлечь любой орган без вреда для пациента. + +Соответственно для трансплантации органа вам нужно использовать процедуру Добавления Органа на той части тела где он был раньше. + +## Что делает каждая из трансплантаций? +- Трансплантация [color=#a4885c]мозга[/color] Позволит пациенту контролировать другое тело. Идеальная альтернатива клонированию если у вас есть свободное тело. +- Трансплантация [color=#a4885c]печени[/color] излечит пациента от нанесённого ему урона ядами. +- Трансплантация [color=#a4885c]лёгких[/color] излечит пациента от огромного количества урона удушьем. +- Трансплантация [color=#a4885c]сердца[/color] излечит пациента от гниения и генетического урона. +- Трансплантация [color=#a4885c]глаз[/color] позволит пациенту снова видеть, и излечит любой урон от глаз. + +## Где мне достать новые органы? + +Обычно когда к вам приходит пациент с целью трансплантации нового органа, им понадобится новый, поскольку старый будет либо отсутствовать, либо будет повреждён без возможности восстановления. +Используйте для этого Медицинский Биофабрикатор, который производит био-синтетические органы для всех рас за биомассу. + + + + + + + +Плата медицинского биофабрикатора есть у Главного Врача в его шкафчике. + +## Почему трансплантация органа +## не произвела никакого эффекта? + +Тело вашего пациента отвергла орган, поскольку он в плохом состоянии, попробуйте использовать свежий орган от донора, либо же создайте новый при помощи Медицинского Биофабрикатора. + + diff --git a/Resources/ServerInfo/Corvax/Guidebook/Medical/PartManipulation.xml b/Resources/ServerInfo/Corvax/Guidebook/Medical/PartManipulation.xml new file mode 100644 index 00000000000..284fa0fe230 --- /dev/null +++ b/Resources/ServerInfo/Corvax/Guidebook/Medical/PartManipulation.xml @@ -0,0 +1,51 @@ + +# Манипуляция частями тела +С этим руководством вы сможете превращать феленидов в людей, либо же наоборот если вы особенно жестоки. + +## Анатомия +Обычно тело имеет 10 основных частей. Среди них: + + + + + + + + + + + + + + + + + + +Некоторые расы могут иметь больше или меньше частей тела, такие как хвосты, крылья или дополнительные конечностей, но в целом это приближённая модель того что вы увидите. +Для удаления конечности, вам необходимо провести на ней хирурхическую операцию Удаления Конечности, которая удалит её без вреда для пациента. + +Однако если вы хотите присоединить конечность, вам необходимо провести операцию Присоединения Конечности на части тела к которой вы хотите её присоединить. +Ладони прикреплены к рукам, стопы прикреплены к ногам, всё остальное прикреплено к туловищу. + +## Где мне взять больше частей тела? + +Обычно когда к вам приходит пациент с целью установки новой конечности, им понадобится новая, поскольку старая будет либо отсутствовать, либо повреждена без возможности восстановления. +Для этого используйте Медицинский Биофабрикатор и немного биомассы. Затем произведите любую био-синтетическую часть тела, которая подойдёт любым расам. + + + + + + + +Плата медицинского биофабрикатора есть у Главного Врача в его шкафчике. + +## Я присоединил часть тела к пациенту, +## но почему она не работает? + +После прикрепления части тела к пациенту ей необходимо приживиться, для этого вам необходимо вылечить только что присоединённую часть тела. +Неповреждённая часть потребует небольших затрат для этого, но с серьёзно повреждённой конечностью могут возникнуть трудности. +Используйте хирургическую операцию для Лечения Ран, или же потратьте медикаменты для лечения нужной конечности. + + diff --git a/Resources/ServerInfo/Corvax/Guidebook/Medical/Surgery.xml b/Resources/ServerInfo/Corvax/Guidebook/Medical/Surgery.xml new file mode 100644 index 00000000000..c746bb64b03 --- /dev/null +++ b/Resources/ServerInfo/Corvax/Guidebook/Medical/Surgery.xml @@ -0,0 +1,40 @@ + +# Хирургия +Ваша еженедельная активность. С помощью проведения хирургических операций на пациентах вы можее лечить повреждения, добавлять новые части тела и органы, и так далее. + +## Основы +Для начала хирургии пациент должен быть в положении лёжа, и в идеале без сознания. + +Если пациент всё ещё в сознании, он будет чувствовать огромную боль и могут получать гораздо более серьёзные травмы из-за болевого шока. + + + + + + +Вам также потребуется надеть Перчатки и Маску для предотвращения попадания инфекции в тело пациента. + + + + + + +## Начало + +Для хирургического вмешательства, вам потребуется следующий набор инструментов. + + + + + + + + + + + + + +Как только любой хирургический инструмент есть у вас в руке, провзаимодействуйте с телом пациента для начала операции. Вы можете выбрать части тела на которых хотите провести хирургию. + + diff --git a/Resources/ServerInfo/Corvax/Guidebook/Medical/UtilitySurgeries.xml b/Resources/ServerInfo/Corvax/Guidebook/Medical/UtilitySurgeries.xml new file mode 100644 index 00000000000..16d26b4d224 --- /dev/null +++ b/Resources/ServerInfo/Corvax/Guidebook/Medical/UtilitySurgeries.xml @@ -0,0 +1,24 @@ + +# Особые применения хирургии + +## Лечение ран + +Залечить Раны - это хирургическая операция, которая позволяет лечить Механический урон или урон от Ожогов без медикаментов. +Для начала вам потребуется открыть разрез на теле пациента, а затем применять гемостат до тех пор, пока конечность не будет полностью восстановлена. +Далее закройте разрез при помощи прибора для прижигания. + + + + + + + +Эта процедура работает тем лучше, чем больше у вашего пациента повреждений, особенно если он всё ещё жив. Это позволяет вам воскрешать пациентов +с огромным количеством урона, например тупого Технического Ассистента который вошёл в горящую тритьеварку. + +## Имплантирование в полость + +Эта процедура позволяет вам имплантировать любой крошечный или маленький предмет в туловище пациента. Для начала вам необходимо открыть разрез, а затем открыть грудную клетку. +Пациент не сможет никак взаимодействовать с имплантированным предметом, однако для этого могут быть особые способы использования, например это даёт возможность ещё эффективнее спрятать диск ядерной аунтетификации. + + diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_0.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_0.png new file mode 100644 index 00000000000..f547b0bc961 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_0.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_1.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_1.png new file mode 100644 index 00000000000..0ba1a77dfd9 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_1.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_2.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_2.png new file mode 100644 index 00000000000..1cb55b4fe97 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_2.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_3.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_3.png new file mode 100644 index 00000000000..d797b40a161 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_3.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_4.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_4.png new file mode 100644 index 00000000000..fbc1678ce5d Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_4.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_5.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_5.png new file mode 100644 index 00000000000..f6caf62d405 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_5.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_6.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_6.png new file mode 100644 index 00000000000..74b58c642bb Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_6.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_7.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_7.png new file mode 100644 index 00000000000..74b58c642bb Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_7.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/head_8.png b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_8.png new file mode 100644 index 00000000000..4d024c61f7f Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/head.rsi/head_8.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/head.rsi/meta.json b/Resources/Textures/Interface/Targeting/Status/head.rsi/meta.json new file mode 100644 index 00000000000..2c34f86c28e --- /dev/null +++ b/Resources/Textures/Interface/Targeting/Status/head.rsi/meta.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c7e14784b35b1a136351c396d3a546f461ee2451", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "head_0" + }, + { + "name": "head_1" + }, + { + "name": "head_2" + }, + { + "name": "head_3" + }, + { + "name": "head_4" + }, + { + "name": "head_5" + }, + { + "name": "head_6" + }, + { + "name": "head_7" + }, + { + "name": "head_8" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_0.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_0.png new file mode 100644 index 00000000000..f2b97eeda69 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_0.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_1.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_1.png new file mode 100644 index 00000000000..98883b555e6 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_1.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_2.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_2.png new file mode 100644 index 00000000000..057cf12cafc Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_2.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_3.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_3.png new file mode 100644 index 00000000000..e225fda639d Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_3.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_4.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_4.png new file mode 100644 index 00000000000..4d250735a89 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_4.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_5.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_5.png new file mode 100644 index 00000000000..0f31b3bfccd Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_5.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_6.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_6.png new file mode 100644 index 00000000000..32cd12440e3 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_6.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_7.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_7.png new file mode 100644 index 00000000000..32cd12440e3 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_7.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_8.png b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_8.png new file mode 100644 index 00000000000..b89bb2c20ed Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_8.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/meta.json b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/meta.json new file mode 100644 index 00000000000..a4883062b15 --- /dev/null +++ b/Resources/Textures/Interface/Targeting/Status/leftarm.rsi/meta.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c7e14784b35b1a136351c396d3a546f461ee2451", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "leftarm_0" + }, + { + "name": "leftarm_1" + }, + { + "name": "leftarm_2" + }, + { + "name": "leftarm_3" + }, + { + "name": "leftarm_4" + }, + { + "name": "leftarm_5" + }, + { + "name": "leftarm_6" + }, + { + "name": "leftarm_7" + }, + { + "name": "leftarm_8" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_0.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_0.png new file mode 100644 index 00000000000..2c1ff500970 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_0.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_1.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_1.png new file mode 100644 index 00000000000..b3ea18c15af Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_1.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_2.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_2.png new file mode 100644 index 00000000000..4439c85a05f Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_2.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_3.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_3.png new file mode 100644 index 00000000000..eb0baeaedb5 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_3.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_4.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_4.png new file mode 100644 index 00000000000..8c0cc64f9b1 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_4.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_5.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_5.png new file mode 100644 index 00000000000..3db84b2482a Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_5.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_6.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_6.png new file mode 100644 index 00000000000..aafd9842ef1 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_6.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_7.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_7.png new file mode 100644 index 00000000000..aafd9842ef1 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_7.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_8.png b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_8.png new file mode 100644 index 00000000000..9652d363d4e Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_8.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/meta.json b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/meta.json new file mode 100644 index 00000000000..b94785bcb66 --- /dev/null +++ b/Resources/Textures/Interface/Targeting/Status/leftleg.rsi/meta.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c7e14784b35b1a136351c396d3a546f461ee2451", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "leftleg_0" + }, + { + "name": "leftleg_1" + }, + { + "name": "leftleg_2" + }, + { + "name": "leftleg_3" + }, + { + "name": "leftleg_4" + }, + { + "name": "leftleg_5" + }, + { + "name": "leftleg_6" + }, + { + "name": "leftleg_7" + }, + { + "name": "leftleg_8" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/meta.json b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/meta.json new file mode 100644 index 00000000000..c858492b6d8 --- /dev/null +++ b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/meta.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c7e14784b35b1a136351c396d3a546f461ee2451", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "rightarm_0" + }, + { + "name": "rightarm_1" + }, + { + "name": "rightarm_2" + }, + { + "name": "rightarm_3" + }, + { + "name": "rightarm_4" + }, + { + "name": "rightarm_5" + }, + { + "name": "rightarm_6" + }, + { + "name": "rightarm_7" + }, + { + "name": "rightarm_8" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_0.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_0.png new file mode 100644 index 00000000000..bc949f384b5 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_0.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_1.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_1.png new file mode 100644 index 00000000000..e1c0b4c09e9 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_1.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_2.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_2.png new file mode 100644 index 00000000000..5c9f675a1d3 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_2.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_3.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_3.png new file mode 100644 index 00000000000..6ccdf982379 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_3.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_4.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_4.png new file mode 100644 index 00000000000..e38f50c3439 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_4.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_5.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_5.png new file mode 100644 index 00000000000..1e0b6a768f9 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_5.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_6.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_6.png new file mode 100644 index 00000000000..90b26a759b4 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_6.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_7.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_7.png new file mode 100644 index 00000000000..90b26a759b4 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_7.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_8.png b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_8.png new file mode 100644 index 00000000000..126b5e0cd08 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_8.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/meta.json b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/meta.json new file mode 100644 index 00000000000..279743503cd --- /dev/null +++ b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/meta.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c7e14784b35b1a136351c396d3a546f461ee2451", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "rightleg_0" + }, + { + "name": "rightleg_1" + }, + { + "name": "rightleg_2" + }, + { + "name": "rightleg_3" + }, + { + "name": "rightleg_4" + }, + { + "name": "rightleg_5" + }, + { + "name": "rightleg_6" + }, + { + "name": "rightleg_7" + }, + { + "name": "rightleg_8" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_0.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_0.png new file mode 100644 index 00000000000..d3461f477d7 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_0.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_1.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_1.png new file mode 100644 index 00000000000..014fa1c7671 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_1.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_2.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_2.png new file mode 100644 index 00000000000..6e52faa7d22 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_2.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_3.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_3.png new file mode 100644 index 00000000000..e527d365684 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_3.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_4.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_4.png new file mode 100644 index 00000000000..f988d5c494f Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_4.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_5.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_5.png new file mode 100644 index 00000000000..3b06ca0bf24 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_5.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_6.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_6.png new file mode 100644 index 00000000000..88368e0b007 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_6.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_7.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_7.png new file mode 100644 index 00000000000..88368e0b007 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_7.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_8.png b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_8.png new file mode 100644 index 00000000000..2d99a1c8994 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_8.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/meta.json b/Resources/Textures/Interface/Targeting/Status/torso.rsi/meta.json new file mode 100644 index 00000000000..491f5a0dfc2 --- /dev/null +++ b/Resources/Textures/Interface/Targeting/Status/torso.rsi/meta.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c7e14784b35b1a136351c396d3a546f461ee2451", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "torso_0" + }, + { + "name": "torso_1" + }, + { + "name": "torso_2" + }, + { + "name": "torso_3" + }, + { + "name": "torso_4" + }, + { + "name": "torso_5" + }, + { + "name": "torso_6" + }, + { + "name": "torso_7" + }, + { + "name": "torso_8" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_0.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_0.png new file mode 100644 index 00000000000..0ccc014503c Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_0.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_1.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_1.png new file mode 100644 index 00000000000..4bda9828e74 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_1.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_2.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_2.png new file mode 100644 index 00000000000..d2e3d59a1c9 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_2.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_3.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_3.png new file mode 100644 index 00000000000..262e5601785 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_3.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_4.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_4.png new file mode 100644 index 00000000000..627d16dccbb Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_4.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_5.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_5.png new file mode 100644 index 00000000000..64ab60165f4 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_5.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_6.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_6.png new file mode 100644 index 00000000000..c7d377ffb2c Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_6.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_7.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_7.png new file mode 100644 index 00000000000..c7d377ffb2c Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_7.png differ diff --git a/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_8.png b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_8.png new file mode 100644 index 00000000000..bf0f5a312b1 Binary files /dev/null and b/Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_8.png differ diff --git a/Resources/Textures/Interface/Targeting/doll.png b/Resources/Textures/Interface/Targeting/doll.png new file mode 100644 index 00000000000..086021b245a Binary files /dev/null and b/Resources/Textures/Interface/Targeting/doll.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel.png new file mode 100644 index 00000000000..f66bf6cdf9e Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_0.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_0.png new file mode 100644 index 00000000000..4399c73304b Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_0.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_25.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_25.png new file mode 100644 index 00000000000..3d47302afd5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_25.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_50.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_50.png new file mode 100644 index 00000000000..744b46f1ef8 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_50.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_75.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_75.png new file mode 100644 index 00000000000..4fbd689bb4c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_75.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/meta.json new file mode 100644 index 00000000000..61ab52970b1 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from cmss13 at https://github.com/cmss13-devs/cmss13/blob/7917d83bac9cddd14c0ca0b457256b2683cc047f/icons/obj/items/surgery_tools.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "bone-gel" + }, + { + "name": "predator_bone-gel" + }, + { + "name": "bone-gel_75" + }, + { + "name": "bone-gel_50" + }, + { + "name": "bone-gel_25" + }, + { + "name": "bone-gel_0" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/predator_bone-gel.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/predator_bone-gel.png new file mode 100644 index 00000000000..4eb66f5843a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/predator_bone-gel.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/bonesetter.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/bonesetter.png new file mode 100644 index 00000000000..b876de0d28e Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/bonesetter.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/meta.json new file mode 100644 index 00000000000..227b276eda4 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from cmss13 at https://github.com/cmss13-devs/cmss13/blob/7917d83bac9cddd14c0ca0b457256b2683cc047f/icons/obj/items/surgery_tools.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "bonesetter" + }, + { + "name": "predator_bonesetter" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/predator_bonesetter.png b/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/predator_bonesetter.png new file mode 100644 index 00000000000..c77903ff77a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/predator_bonesetter.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/insertion.png b/Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/insertion.png new file mode 100644 index 00000000000..961c3c641a7 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/insertion.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/meta.json new file mode 100644 index 00000000000..af2b78c1ea8 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Aurorastation at https://github.com/Aurorastation/Aurora.3/commit/ac435b98e2d0a455ad319dca3bb9bfdc0cd8b051", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "insertion" + } + ] +} diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_fill.png b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_fill.png new file mode 100644 index 00000000000..1e45b366221 Binary files /dev/null and b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_fill.png differ diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleoff.png b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleoff.png new file mode 100644 index 00000000000..73bab51916c Binary files /dev/null and b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleoff.png differ diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleon.png b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleon.png new file mode 100644 index 00000000000..f47f93eb86a Binary files /dev/null and b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleon.png differ diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_openpanel.png b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_openpanel.png new file mode 100644 index 00000000000..cc174c7d8b9 Binary files /dev/null and b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_openpanel.png differ diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_panelopen.png b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_panelopen.png new file mode 100644 index 00000000000..d4c4de1f8c4 Binary files /dev/null and b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_panelopen.png differ diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_unfill.png b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_unfill.png new file mode 100644 index 00000000000..6aa57f247e2 Binary files /dev/null and b/Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_unfill.png differ diff --git a/Resources/Textures/Structures/Machines/limbgrower.rsi/meta.json b/Resources/Textures/Structures/Machines/limbgrower.rsi/meta.json new file mode 100644 index 00000000000..a1f2b51b28e --- /dev/null +++ b/Resources/Textures/Structures/Machines/limbgrower.rsi/meta.json @@ -0,0 +1,85 @@ +{ + "version": 1, + "license": "GNU AGPL v3", + "copyright": "Taken from /tg/station at commit 85c26c1", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "limbgrower_fill", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "limbgrower_unfill", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "limbgrower_openpanel", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "limbgrower_idleoff", + "delays": [ + [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + ] + }, + { + "name": "limbgrower_idleon", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "limbgrower_panelopen" + } + ] +} diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 3b8158b7c7a..5f0ef2753a9 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -70,21 +70,27 @@ binds: - function: CameraRotateLeft type: State key: NumpadNum7 + mod1: Control - function: CameraRotateRight type: State key: NumpadNum9 + mod1: Control - function: CameraReset type: State key: NumpadNum8 + mod1: Control - function: ZoomOut type: State key: NumpadNum4 + mod1: Control - function: ZoomIn type: State key: NumpadNum6 + mod1: Control - function: ResetZoom type: State key: NumpadNum5 + mod1: Control # Misc - function: ShowEscapeMenu type: State @@ -584,3 +590,22 @@ binds: type: State key: MouseRight canFocus: true +# Targeting +- function: TargetHead + type: State + key: NumpadNum8 +- function: TargetTorso + type: State + key: NumpadNum5 +- function: TargetLeftArm + type: State + key: NumpadNum6 +- function: TargetRightArm + type: State + key: NumpadNum4 +- function: TargetLeftLeg + type: State + key: NumpadNum3 +- function: TargetRightLeg + type: State + key: NumpadNum1