diff --git a/Content.Client/Body/Systems/BodySystem.cs b/Content.Client/Body/Systems/BodySystem.cs index bab785525b0dc7..bee569c1011fa6 100644 --- a/Content.Client/Body/Systems/BodySystem.cs +++ b/Content.Client/Body/Systems/BodySystem.cs @@ -1,7 +1,83 @@ using Content.Shared.Body.Systems; +using Content.Shared.Body.Part; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.GameObjects; +using Robust.Shared.Utility; +using Content.Shared.Body.Components; namespace Content.Client.Body.Systems; public sealed class BodySystem : SharedBodySystem { + [Dependency] private readonly MarkingManager _markingManager = default!; + + private void ApplyMarkingToPart(MarkingPrototype markingPrototype, + IReadOnlyList? colors, + bool visible, + SpriteComponent sprite) + { + for (var j = 0; j < markingPrototype.Sprites.Count; j++) + { + var markingSprite = markingPrototype.Sprites[j]; + + if (markingSprite is not SpriteSpecifier.Rsi rsi) + { + continue; + } + + var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; + + if (!sprite.LayerMapTryGet(layerId, out _)) + { + var layer = sprite.AddLayer(markingSprite, j + 1); + sprite.LayerMapSet(layerId, layer); + sprite.LayerSetSprite(layerId, rsi); + } + + sprite.LayerSetVisible(layerId, visible); + + if (!visible) + { + continue; + } + + // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid + // and we need to check the index is correct. + // So if that happens just default to white? + if (colors != null && j < colors.Count) + { + sprite.LayerSetColor(layerId, colors[j]); + } + else + { + sprite.LayerSetColor(layerId, Color.White); + } + } + } + + protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component) + { + if (!TryComp(target, out SpriteComponent? sprite)) + return; + + if (component.Color != null) + sprite.Color = component.Color.Value; + + foreach (var (visualLayer, markingList) in component.Markings) + { + foreach (var marking in markingList) + { + if (!_markingManager.TryGetMarking(marking, out var markingPrototype)) + continue; + + ApplyMarkingToPart(markingPrototype, marking.MarkingColors, marking.Visible, sprite); + } + } + } + + protected override void RemovePartMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance) + { + return; + } } diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index e889a7b64a51a9..c75747a265e685 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -6,6 +6,7 @@ using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.Targeting; using Content.Shared.FixedPoint; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; @@ -36,6 +37,12 @@ public sealed partial class HealthAnalyzerWindow : FancyWindow private readonly IPrototypeManager _prototypes; private readonly IResourceCache _cache; + // Start-Shitmed + private EntityUid _spriteViewEntity; + + [ValidatePrototypeId] + private readonly EntProtoId _bodyView = "AlertSpriteView"; + // End-Shitmed public HealthAnalyzerWindow() { RobustXamlLoader.Load(this); @@ -72,7 +79,7 @@ public void Populate(HealthAnalyzerScannedUserMessage msg) // Patient Information - SpriteView.SetEntity(target.Value); + SpriteView.SetEntity(SetupIcon(msg.Body) ?? target.Value); // Shitmed SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value; NoDataTex.Visible = !SpriteView.Visible; @@ -249,5 +256,36 @@ private BoxContainer CreateDiagnosticGroupTitle(string text, string id) return rootContainer; } + + // Start-Shitmed + /// + /// Sets up the Body Doll using Alert Entity to use in Health Analyzer. + /// + private EntityUid? SetupIcon(Dictionary? body) + { + if (body is null) + return null; + if (!_entityManager.Deleted(_spriteViewEntity)) + _entityManager.QueueDeleteEntity(_spriteViewEntity); + _spriteViewEntity = _entityManager.Spawn(_bodyView); + if (!_entityManager.TryGetComponent(_spriteViewEntity, out var sprite)) + return null; + int layer = 0; + foreach (var (bodyPart, integrity) in body) + { + // TODO: Fix this way PartStatusUIController and make it use layers instead of TextureRects + string enumName = Enum.GetName(typeof(TargetBodyPart), bodyPart) ?? "Unknown"; + int enumValue = (int) integrity; + var rsi = new SpriteSpecifier.Rsi(new ResPath($"/Textures/Interface/Targeting/Status/{enumName.ToLowerInvariant()}.rsi"), $"{enumName.ToLowerInvariant()}_{enumValue}"); + // It's probably shitcode but im lazy to get into sprite stuff - It is shitcode :) + if (!sprite.TryGetLayer(layer, out _)) + sprite.AddLayer(_spriteSystem.Frame0(rsi)); + else + sprite.LayerSetTexture(layer, _spriteSystem.Frame0(rsi)); + layer++; + } + return _spriteViewEntity; + } + // End-Shitmed } } diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 867dcbc2692106..b05a16b6d4f920 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -65,7 +65,8 @@ private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent foreach (var (key, info) in component.CustomBaseLayers) { oldLayers.Remove(key); - SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color); + // Shitmed modification: For whatever reason these weren't actually ignoring the skin color as advertised. + SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color, overrideSkin: true); } // hide old layers @@ -83,7 +84,8 @@ private void SetLayerData( HumanoidVisualLayers key, string? protoId, bool sexMorph = false, - Color? color = null) + Color? color = null, + bool overrideSkin = false) { var layerIndex = sprite.LayerMapReserveBlank(key); var layer = sprite[layerIndex]; @@ -101,7 +103,7 @@ private void SetLayerData( var proto = _prototypeManager.Index(protoId); component.BaseLayers[key] = proto; - if (proto.MatchSkin) + if (proto.MatchSkin && !overrideSkin) layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha); if (proto.BaseSprite != null) diff --git a/Content.Client/Medical/Surgery/SurgeryBui.cs b/Content.Client/Medical/Surgery/SurgeryBui.cs index ddfbf561f2b02d..b084c8c731aca6 100644 --- a/Content.Client/Medical/Surgery/SurgeryBui.cs +++ b/Content.Client/Medical/Surgery/SurgeryBui.cs @@ -3,15 +3,16 @@ using Content.Shared.Medical.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; +using OpenToolkit.GraphicsLibraryFramework; namespace Content.Client.Medical.Surgery; @@ -21,10 +22,10 @@ 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; - [ViewVariables] private SurgeryWindow? _window; @@ -32,22 +33,20 @@ public sealed class SurgeryBui : BoundUserInterface 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(); } - protected override void Open() + protected override void ReceiveMessage(BoundUserInterfaceMessage message) { - _system.OnRefresh += () => - { - UpdateDisabledPanel(); - RefreshUI(); - }; + if (_window == null) + return; - if (State is SurgeryBuiState s) - Update(s); + if (message is SurgeryBuiRefreshMessage) + RefreshUI(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -59,11 +58,8 @@ protected override void UpdateState(BoundUserInterfaceState state) protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (disposing) _window?.Dispose(); - - _system.OnRefresh -= RefreshUI; } private void Update(SurgeryBuiState state) @@ -71,6 +67,7 @@ private void Update(SurgeryBuiState state) if (!_entities.TryGetComponent(_player.LocalEntity, out var surgeryTargetComp) || !surgeryTargetComp.CanOperate) return; + if (_window == null) { _window = new SurgeryWindow(); @@ -125,7 +122,6 @@ State is not SurgeryBuiState s || _window.Surgeries.DisposeAllChildren(); _window.Steps.DisposeAllChildren(); _window.Parts.DisposeAllChildren(); - View(ViewType.Parts); var oldSurgery = _surgery; @@ -194,11 +190,6 @@ int GetScore(BodyPartType? partType) if (!_window.IsOpen) _window.OpenCentered(); - else - { - RefreshUI(); - UpdateDisabledPanel(); - } } private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId) @@ -303,7 +294,6 @@ private void OnPartPressed(NetEntity netPart, List surgeryIds) private void RefreshUI() { if (_window == null - || !_timing.IsFirstTimePredicted || !_window.IsOpen || _part == null || !_entities.HasComponent(_surgery?.Ent) @@ -312,7 +302,6 @@ private void RefreshUI() { return; } - var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent); var i = 0; foreach (var child in _window.Steps.Children) @@ -350,60 +339,16 @@ private void RefreshUI() else { stepButton.Button.Modulate = Color.White; - // GOD THIS NEEDS A REWRITE SO BADLY, IT UPDATES ON EVERY SINGLE TICK - // THEN RUNS CANPERFORMSTEP WHICH CALLS A SHITLOAD OF EVENTS - // DID THEY NOT FUCKING PLAYTEST THIS??? 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++; } - - UpdateDisabledPanel(); - } - - private void UpdateDisabledPanel() - { - if (_window == null) - return; - - if (_system.IsLyingDown(Owner)) - { - _window.DisabledPanel.Visible = false; - _window.DisabledPanel.MouseFilter = MouseFilterMode.Ignore; - return; - } - - _window.DisabledPanel.Visible = true; - - var text = new FormattedMessage(); - text.AddMarkup($"[color=red][font size=16]{Loc.GetString("surgery-ui-window-steps-error-laying")}[/font][/color]"); - _window.DisabledLabel.SetMessage(text); - _window.DisabledPanel.MouseFilter = MouseFilterMode.Stop; } private void View(ViewType type) diff --git a/Content.Client/Medical/Surgery/SurgerySystem.cs b/Content.Client/Medical/Surgery/SurgerySystem.cs index 19c8fac5fce399..cbf1aeee4836ae 100644 --- a/Content.Client/Medical/Surgery/SurgerySystem.cs +++ b/Content.Client/Medical/Surgery/SurgerySystem.cs @@ -4,10 +4,8 @@ namespace Content.Client.Medical.Surgery; public sealed class SurgerySystem : SharedSurgerySystem { - public event Action? OnRefresh; - - public override void Update(float frameTime) + public override void Initialize() { - OnRefresh?.Invoke(); + base.Initialize(); } -} \ No newline at end of file +} diff --git a/Content.Client/Medical/Surgery/SurgeryWindow.xaml b/Content.Client/Medical/Surgery/SurgeryWindow.xaml index fc66e0cd71be2b..bba801a8a58e9a 100644 --- a/Content.Client/Medical/Surgery/SurgeryWindow.xaml +++ b/Content.Client/Medical/Surgery/SurgeryWindow.xaml @@ -20,11 +20,4 @@ - - - - - - diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 441dc560a0681a..6a2538df8e1e0b 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Body.Part; using Content.Shared.Body.Systems; using Content.Shared.Damage; +using Content.Shared.Gibbing.Events; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; @@ -25,7 +26,6 @@ public sealed class BodySystem : SharedBodySystem [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; public override void Initialize() @@ -111,7 +111,9 @@ public override HashSet GibBody( Vector2? splatDirection = null, float splatModifier = 1, Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null) + SoundSpecifier? gibSoundOverride = null, + GibType gib = GibType.Gib, + GibContentsOption contents = GibContentsOption.Drop) { if (!Resolve(bodyId, ref body, logMissing: false) || TerminatingOrDeleted(bodyId) @@ -125,7 +127,8 @@ 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, + gib: gib, contents: contents); var ev = new BeingGibbedEvent(gibs); RaiseLocalEvent(bodyId, ref ev); @@ -165,4 +168,22 @@ public override HashSet GibPart( return gibs; } + + protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component) + { + return; + } + + protected override void RemovePartMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance) + { + foreach (var (visualLayer, markingList) in partAppearance.Markings) + { + foreach (var marking in markingList) + { + _humanoidSystem.RemoveMarking(target, marking.MarkingId, sync: false, humanoid: bodyAppearance); + } + } + + Dirty(target, bodyAppearance); + } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs index c83fed1906962b..da054e24ac35ac 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs @@ -1,4 +1,5 @@ using Content.Shared.Body.Components; +using Content.Shared.Gibbing.Events; using JetBrains.Annotations; namespace Content.Server.Destructible.Thresholds.Behaviors @@ -7,13 +8,15 @@ namespace Content.Server.Destructible.Thresholds.Behaviors [DataDefinition] public sealed partial class GibBehavior : IThresholdBehavior { + [DataField] public GibType GibType = GibType.Gib; + [DataField] public GibContentsOption GibContents = GibContentsOption.Drop; [DataField("recursive")] private bool _recursive = true; public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) { if (system.EntityManager.TryGetComponent(owner, out BodyComponent? body)) { - system.BodySystem.GibBody(owner, _recursive, body); + system.BodySystem.GibBody(owner, _recursive, body, gib: GibType, contents: GibContents); } } } diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 023a2e4083f866..caae6c5f17e55e 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -207,6 +207,7 @@ private void OnActivateUI(Entity entity, ref AfterActivatableU : 0, null, null, + null, null )); } diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 76d09c42f08018..b35c8f21d13153 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Temperature.Components; using Content.Server.Traits.Assorted; using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Targeting; using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.Interaction; @@ -201,13 +202,21 @@ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool s /*if (HasComp(target)) Somehow we dont have unrevivable??? unrevivable = true; */ + + // Start-Shitmed + Dictionary? body = null; + if (TryComp(target, out var targetingComponent)) + body = targetingComponent.BodyStatus; + // End-Shitmed + _uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage( GetNetEntity(target), bodyTemperature, bloodAmount, scanMode, bleeding, - unrevivable + unrevivable, + body // Shitmed )); } } diff --git a/Content.Server/Medical/Surgery/SurgerySystem.cs b/Content.Server/Medical/Surgery/SurgerySystem.cs index e96bc3229a665f..92ea39b718b31a 100644 --- a/Content.Server/Medical/Surgery/SurgerySystem.cs +++ b/Content.Server/Medical/Surgery/SurgerySystem.cs @@ -54,9 +54,7 @@ public override void Initialize() SubscribeLocalEvent(OnStepAffixPartComplete); SubscribeLocalEvent(OnStepScreamComplete); SubscribeLocalEvent(OnStepSpawnComplete); - SubscribeLocalEvent(OnPrototypesReloaded); - LoadPrototypes(); } @@ -81,8 +79,13 @@ protected override void RefreshUI(EntityUid body) } _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. I love shitcode. + */ + _ui.ServerSendUiMessage(body, SurgeryUIKey.Key, new SurgeryBuiRefreshMessage()); } - private void SetDamage(EntityUid body, DamageSpecifier damage, float partMultiplier, EntityUid user, EntityUid part) { @@ -103,13 +106,15 @@ private void OnToolAfterInteract(Entity ent, ref AfterInte if (args.Handled || !args.CanReach || args.Target == null + || !HasComp(args.Target) || !TryComp(args.User, out var surgery) - || !surgery.CanOperate) + || !surgery.CanOperate + || !IsLyingDown(args.Target.Value, args.User)) { return; } - if (user == args.Target && !_config.GetCVar(CCVars.CanOperateOnSelf)) + if (user == args.Target && _config.GetCVar(CCVars.CanOperateOnSelf)) { _popup.PopupEntity(Loc.GetString("surgery-error-self-surgery"), user, user); return; @@ -176,7 +181,7 @@ private void OnStepAffixPartComplete(Entity 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 its not too badly damaged. + // and affixing it will heal it a bit if it's not too badly damaged. _body.TryChangeIntegrity(targetPart, targetPart.Component.Integrity - 20, false, _body.GetTargetBodyPart(targetPart.Component.PartType, targetPart.Component.Symmetry), out _); } diff --git a/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs new file mode 100644 index 00000000000000..53cf40b9b5f6ba --- /dev/null +++ b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs @@ -0,0 +1,46 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Humanoid.Markings; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.GameStates; + +namespace Content.Shared.Body.Part; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +public sealed partial class BodyPartAppearanceComponent : Component +{ + /// + /// HumanoidVisualLayer type for this body part. + /// + [DataField, AutoNetworkedField] + public HumanoidVisualLayers Type { get; set; } + + /// + /// Relevant markings for this body part that will be applied on attachment. + /// + [DataField, AutoNetworkedField] + public Dictionary> Markings = new(); + + /// + /// ID of this custom base layer. Must be a . + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField] + public string? 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; } + + /// + /// Color of this custom base eye layer. Null implies eye colour if the corresponding is set to match skin. + /// + [DataField, AutoNetworkedField] + public Color? EyeColor { 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 62b455b511310f..b71907da53e169 100644 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ b/Content.Shared/Body/Part/BodyPartComponent.cs @@ -1,11 +1,14 @@ -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Medical.Surgery.Tools; -using Content.Shared.Body.Components; +using Content.Shared.Body.Components; using Content.Shared.Body.Systems; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Medical.Surgery.Tools; using Content.Shared.FixedPoint; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Content.Shared.Humanoid; namespace Content.Shared.Body.Part; @@ -107,6 +110,19 @@ public sealed partial class BodyPartComponent : Component, ISurgeryToolComponent [DataField, AutoNetworkedField] public ItemSlot ItemInsertionSlot = new(); + + /// + /// Current species. Dictates things like body part sprites. + /// + [DataField, AutoNetworkedField] + public string Species { get; set; } = ""; + + /// + /// Do not make a stupid joke do not make a stupid joke do not make a stupid joke. + /// + [DataField, AutoNetworkedField] + public Sex Sex { get; set; } = Sex.Male; + /// /// 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 f4444121e1d4c5..9872b09200254d 100644 --- a/Content.Shared/Body/Part/BodyPartEvents.cs +++ b/Content.Shared/Body/Part/BodyPartEvents.cs @@ -1,11 +1,22 @@ +using Content.Shared.Humanoid; + namespace Content.Shared.Body.Part; [ByRefEvent] public readonly record struct BodyPartAddedEvent(string Slot, Entity Part); +// Kind of a clone of the above for surgical reattachment specifically. +[ByRefEvent] +public readonly record struct BodyPartAttachedEvent(Entity Part); + [ByRefEvent] public readonly record struct BodyPartRemovedEvent(string Slot, Entity Part); +// Kind of a clone of the above for any instances where we call DropPart(), reasoning being that RemovedEvent fires off +// a lot more often than what I'd like due to PVS. +[ByRefEvent] +public readonly record struct BodyPartDroppedEvent(Entity Part); + [ByRefEvent] public readonly record struct BodyPartEnableChangedEvent(bool Enabled); diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 558a190f1ca506..ad12544f33bf72 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -10,6 +10,9 @@ using Content.Shared.Gibbing.Components; using Content.Shared.Gibbing.Events; using Content.Shared.Gibbing.Systems; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Events; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Inventory; using Content.Shared.Rejuvenate; using Content.Shared.Standing; @@ -19,7 +22,6 @@ using Robust.Shared.Map; using Robust.Shared.Utility; using Robust.Shared.Timing; - namespace Content.Shared.Body.Systems; public partial class SharedBodySystem @@ -51,6 +53,7 @@ private void InitializeBody() SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnStandAttempt); SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnProfileLoadFinished); } private void OnBodyInserted(Entity ent, ref EntInsertedIntoContainerMessage args) @@ -124,10 +127,11 @@ private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype) var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId); var rootPart = Comp(rootPartUid); rootPart.Body = bodyEntity; + rootPart.OriginalBody = bodyEntity; Dirty(rootPartUid, rootPart); // Setup the rest of the body entities. SetupOrgans((rootPartUid, rootPart), protoRoot.Organs); - MapInitParts(rootPartUid, prototype); + MapInitParts(rootPartUid, rootPart, prototype); } private void OnBodyCanDrag(Entity ent, ref CanDragEvent args) @@ -171,7 +175,7 @@ private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) /// /// Sets up all of the relevant body parts for a particular body entity and root part. /// - private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) + private void MapInitParts(EntityUid rootPartId, BodyPartComponent rootPart, BodyPrototype prototype) { // Start at the root part and traverse the body graph, setting up parts as we go. // Basic BFS pathfind. @@ -210,6 +214,8 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) var childPartComponent = Comp(childPart); var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent); childPartComponent.ParentSlot = partSlot; + childPartComponent.OriginalBody = rootPart.Body; + Dirty(childPart, childPartComponent); var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection)); if (partSlot is null || !Containers.Insert(childPart, cont)) @@ -333,7 +339,9 @@ public virtual HashSet GibBody( Vector2? splatDirection = null, float splatModifier = 1, Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null) + SoundSpecifier? gibSoundOverride = null, + GibType gib = GibType.Gib, + GibContentsOption contents = GibContentsOption.Drop) { var gibs = new HashSet(); @@ -350,7 +358,7 @@ public virtual HashSet GibBody( foreach (var part in parts) { - _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Drop, ref gibs, + _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, gib, contents, ref gibs, playAudio: false, launchGibs: true, launchDirection: splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier, launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone); @@ -402,4 +410,16 @@ public virtual HashSet GibPart( _audioSystem.PlayPredicted(gibSoundOverride, Transform(partId).Coordinates, null); return gibs; } + + private void OnProfileLoadFinished(EntityUid uid, BodyComponent component, ProfileLoadFinishedEvent args) + { + if (!TryComp(uid, out var appearance) + || TerminatingOrDeleted(uid)) + return; + + foreach (var part in GetBodyChildren(uid, component)) + { + EnsureComp(part.Id); + } + } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs b/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs index 415295561a312e..d7d6e7749758a4 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs @@ -134,12 +134,15 @@ public void TryChangeIntegrity(Entity partEnt, 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 @@ -147,7 +150,6 @@ public void TryChangeIntegrity(Entity partEnt, && partIdSlot is not null) severed = true; - // This will also prevent the torso from being removed. if (partEnt.Comp.Enabled && partEnt.Comp.Integrity <= 15.0f) { diff --git a/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs new file mode 100644 index 00000000000000..dd5c40fe129285 --- /dev/null +++ b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs @@ -0,0 +1,150 @@ +using System.Linq; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; + +namespace Content.Shared.Body.Systems; +public partial class SharedBodySystem +{ + [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!; + private void InitializePartAppearances() + { + base.Initialize(); + + SubscribeLocalEvent(OnPartAppearanceStartup); + SubscribeLocalEvent(HandleState); + SubscribeLocalEvent(OnPartAttachedToBody); + SubscribeLocalEvent(OnPartDroppedFromBody); + } + + private void OnPartAppearanceStartup(EntityUid uid, BodyPartAppearanceComponent component, ComponentStartup args) + { + + // God this function reeks, it needs some cleanup BADLY. Help is appreciated as always. + + if (!TryComp(uid, out BodyPartComponent? part) + || part.OriginalBody == null + || TerminatingOrDeleted(part.OriginalBody.Value) + || !TryComp(part.OriginalBody.Value, out HumanoidAppearanceComponent? bodyAppearance) + || HumanoidVisualLayersExtension.ToHumanoidLayers(part) is not { } relevantLayer) + return; + + var customLayers = bodyAppearance.CustomBaseLayers; + var spriteLayers = bodyAppearance.BaseLayers; + component.Type = relevantLayer; + component.OriginalBody = part.OriginalBody.Value; + part.Sex = bodyAppearance.Sex; + + // Thanks Rane for the human reskin :) + if (bodyAppearance.Species.ToString() == "Felinid") + part.Species = "Human"; + else + part.Species = bodyAppearance.Species; + + if (customLayers.ContainsKey(component.Type)) + { + component.ID = customLayers[component.Type].Id; + component.Color = customLayers[component.Type].Color; + } + else if (spriteLayers.ContainsKey(component.Type)) + { + component.ID = spriteLayers[component.Type].ID; + component.Color = bodyAppearance.SkinColor; + } + else + { + component.Color = bodyAppearance.SkinColor; + var symmetryPrefix = part.Symmetry switch + { + BodyPartSymmetry.Left => "L", + BodyPartSymmetry.Right => "R", + _ => "" + }; + + var genderSuffix = ""; + + if (part.PartType == BodyPartType.Torso || part.PartType == BodyPartType.Head) + genderSuffix = part.Sex.ToString(); + + component.ID = $"Mob{part.Species}{symmetryPrefix}{part.PartType}{genderSuffix}"; + } + + // I HATE HARDCODED CHECKS I HATE HARDCODED CHECKS I HATE HARDCODED CHECKS + if (part.PartType == BodyPartType.Head) + component.EyeColor = bodyAppearance.EyeColor; + + var markingsByLayer = new Dictionary>(); + + foreach (var layer in HumanoidVisualLayersExtension.Sublayers(relevantLayer)) + { + var category = MarkingCategoriesConversion.FromHumanoidVisualLayers(layer); + if (bodyAppearance.MarkingSet.Markings.TryGetValue(category, out var markingList)) + markingsByLayer[layer] = markingList.Select(m => new Marking(m.MarkingId, m.MarkingColors.ToList())).ToList(); + } + + component.Markings = markingsByLayer; + } + private void HandleState(EntityUid uid, BodyPartAppearanceComponent component, ref AfterAutoHandleStateEvent args) + { + ApplyPartMarkings(uid, component); + } + private void OnPartAttachedToBody(EntityUid uid, BodyComponent component, ref BodyPartAttachedEvent args) + { + if (!TryComp(args.Part, out BodyPartAppearanceComponent? partAppearance) + || !TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance)) + return; + + _humanoid.SetBaseLayerId(uid, partAppearance.Type, partAppearance.ID, sync: true, bodyAppearance); + UpdateAppearance(uid, partAppearance); + } + + private void OnPartDroppedFromBody(EntityUid uid, BodyComponent component, ref BodyPartDroppedEvent args) + { + if (TerminatingOrDeleted(uid) + || !TryComp(args.Part, out BodyPartAppearanceComponent? appearance)) + return; + + RemoveAppearance(uid, appearance, args.Part); + } + + protected void UpdateAppearance(EntityUid target, + BodyPartAppearanceComponent component) + { + if (!TryComp(target, out HumanoidAppearanceComponent? bodyAppearance)) + return; + + if (component.EyeColor != null) + bodyAppearance.EyeColor = component.EyeColor.Value; + + if (component.Color != null) + _humanoid.SetBaseLayerColor(target, component.Type, component.Color, true, bodyAppearance); + + _humanoid.SetLayerVisibility(target, component.Type, true, true, bodyAppearance); + foreach (var (visualLayer, markingList) in component.Markings) + { + _humanoid.SetLayerVisibility(target, visualLayer, true, true, bodyAppearance); + foreach (var marking in markingList) + { + _humanoid.AddMarking(target, marking.MarkingId, marking.MarkingColors, false, true, bodyAppearance); + } + } + Dirty(target, bodyAppearance); + } + + protected void RemoveAppearance(EntityUid entity, BodyPartAppearanceComponent component, EntityUid partEntity) + { + if (!TryComp(entity, out HumanoidAppearanceComponent? bodyAppearance)) + return; + + foreach (var (visualLayer, markingList) in component.Markings) + { + _humanoid.SetLayerVisibility(entity, visualLayer, false, true, bodyAppearance); + } + RemovePartMarkings(entity, component, bodyAppearance); + } + + protected abstract void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component); + + protected abstract void RemovePartMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance); +} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index 06acd3d855218b..77f8f67ebb785d 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -4,6 +4,7 @@ using Content.Shared.Body.Part; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Random; @@ -29,6 +30,7 @@ private void InitializeParts() SubscribeLocalEvent(OnBodyPartInserted); SubscribeLocalEvent(OnBodyPartRemoved); SubscribeLocalEvent(OnAmputateAttempt); + SubscribeLocalEvent(OnPartEnableChanged); } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -167,14 +169,25 @@ protected virtual void RemovePart( protected virtual void DropPart(Entity partEnt) { ChangeSlotState(partEnt, true); - + // I don't know if this can cause issues, since any part that's being detached HAS to have a Body. + // though I really just want the compiler to shut the fuck up. + var body = partEnt.Comp.Body.GetValueOrDefault(); // 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); + var enableEvent = new BodyPartEnableChangedEvent(false); + RaiseLocalEvent(partEnt, ref enableEvent); + + if (TryComp(body, out HumanoidAppearanceComponent? bodyAppearance) + && !HasComp(partEnt) + && !TerminatingOrDeleted(body) + && !TerminatingOrDeleted(partEnt)) + EnsureComp(partEnt); + SharedTransform.AttachToGridOrMap(partEnt, transform); _randomHelper.RandomOffset(partEnt, 0.5f); + var droppedEvent = new BodyPartDroppedEvent(partEnt); + RaiseLocalEvent(body, ref droppedEvent); } } @@ -601,6 +614,13 @@ public bool AttachPart( } part.ParentSlot = slot; + + if (TryComp(part.Body, out HumanoidAppearanceComponent? bodyAppearance) + && !HasComp(partId) + && !TerminatingOrDeleted(parentPartId) + && !TerminatingOrDeleted(partId)) // Saw some exceptions involving these due to the spawn menu. + EnsureComp(partId); + return Containers.Insert(partId, container); } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs index 1d9aae69976aff..013e302633b648 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.cs @@ -44,6 +44,7 @@ public override void Initialize() InitializeParts(); // To try and mitigate the server load due to integrity checks, we set up a Job Queue. InitializeIntegrityQueue(); + InitializePartAppearances(); } /// diff --git a/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs b/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs new file mode 100644 index 00000000000000..fc894854dcf1d1 --- /dev/null +++ b/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Humanoid.Events; + +/// +/// Raised on an entity when their profile has finished being loaded +/// +public sealed class ProfileLoadFinishedEvent : EntityEventArgs +{ +} + diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index a1e8bec2cd80a3..f33c65b59152f1 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Examine; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Humanoid.Events; using Content.Shared.IdentityManagement; using Content.Shared.Preferences; using Content.Shared.HeightAdjust; @@ -440,6 +441,7 @@ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned Dirty(humanoid); + RaiseLocalEvent(uid, new ProfileLoadFinishedEvent()); } /// diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs index 04f046bf816778..fed6be01a24ca0 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs @@ -4,7 +4,6 @@ using Content.Shared.Medical.Surgery.Tools; //using Content.Shared._RMC14.Xenonids.Parasite; using Content.Shared.Body.Part; -using Content.Shared.Body.Systems; using Content.Shared.Body.Organ; using Content.Shared.Bed.Sleep; using Content.Shared.Body.Events; @@ -78,7 +77,6 @@ private void OnToolStep(Entity ent, ref SurgeryStepEvent a var compType = reg.Component.GetType(); if (HasComp(args.Part, compType)) continue; - AddComp(args.Part, _compFactory.GetComponent(compType)); } } @@ -162,6 +160,19 @@ private void OnToolCanPerform(Entity ent, ref SurgeryCanPe } } + if (_inventory.TryGetContainerSlotEnumerator(args.Body, out var containerSlotEnumerator, args.TargetSlots)) + { + while (containerSlotEnumerator.MoveNext(out var containerSlot)) + { + if (!containerSlot.ContainedEntity.HasValue) + continue; + + args.Invalid = StepInvalidReason.Armor; + args.Popup = Loc.GetString("surgery-ui-window-steps-error-armor"); + return; + } + } + RaiseLocalEvent(args.Body, ref args); if (args.Invalid != StepInvalidReason.None) @@ -302,6 +313,8 @@ private void OnAddPartStep(Entity ent, ref SurgeryS _body.TryCreatePartSlot(args.Part, slotName, partComp.PartType, out var _); _body.AttachPart(args.Part, slotName, tool); _body.ChangeSlotState((tool, partComp), false); + var ev = new BodyPartAttachedEvent((tool, partComp)); + RaiseLocalEvent(args.Body, ref ev); } } } @@ -414,7 +427,7 @@ private void OnSurgeryTargetStepChosen(Entity ent, ref S 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, out var surgery, out var part, out var step)) + !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, user, out var surgery, out var part, out var step)) { return; } @@ -571,7 +584,6 @@ public bool IsStepComplete(EntityUid body, EntityUid part, EntProtoId step, Enti var ev = new SurgeryStepCompleteCheckEvent(body, part, surgery); RaiseLocalEvent(stepEnt, ref ev); - return !ev.Cancelled; } diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs index 1f9e1748109b5e..9b1dfab1f0c2fc 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs @@ -73,16 +73,20 @@ private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) 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, out var surgery, out var part, out var step) || + !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); @@ -184,21 +188,22 @@ private void OnPartPresentConditionValid(Entity surgeryEnt, out EntityUid part, out EntityUid step) + EntityUid user, out Entity surgeryEnt, out EntityUid part, out EntityUid step) { surgeryEnt = default; part = default; step = default; + if (!HasComp(body) || - !IsLyingDown(body) || + !IsLyingDown(body, user) || GetSingleton(surgery) is not { } surgeryEntId || !TryComp(surgeryEntId, out SurgeryComponent? surgeryComp) || !surgeryComp.Steps.Contains(stepId) || - GetSingleton(stepId) is not { } stepEnt) + GetSingleton(stepId) is not { } stepEnt + || !HasComp(targetPart) + && !HasComp(targetPart)) return false; - if (!HasComp(targetPart) && !HasComp(targetPart)) - return false; var ev = new SurgeryValidEvent(body, targetPart); if (_timing.IsFirstTimePredicted) @@ -238,7 +243,7 @@ private List GetTools(EntityUid surgeon) return _hands.EnumerateHeld(surgeon).ToList(); } - public bool IsLyingDown(EntityUid entity) + public bool IsLyingDown(EntityUid entity, EntityUid user) { if (_standing.IsDown(entity)) return true; @@ -251,6 +256,8 @@ public bool IsLyingDown(EntityUid entity) return true; } + _popup.PopupEntity(Loc.GetString("surgery-error-laying"), user, user); + return false; } diff --git a/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs b/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs index 5ea663c0a4a073..d2d7f8d4620848 100644 --- a/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs +++ b/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs @@ -7,4 +7,4 @@ public sealed partial class SurgeryTargetComponent : Component { [DataField] public bool CanOperate = true; -} \ No newline at end of file +} diff --git a/Content.Shared/Medical/Surgery/SurgeryUI.cs b/Content.Shared/Medical/Surgery/SurgeryUI.cs index cffa9cdc4954a1..2572aaca65a72e 100644 --- a/Content.Shared/Medical/Surgery/SurgeryUI.cs +++ b/Content.Shared/Medical/Surgery/SurgeryUI.cs @@ -15,6 +15,11 @@ public sealed class SurgeryBuiState(Dictionary> choi public readonly Dictionary> Choices = choices; } +[Serializable, NetSerializable] +public sealed class SurgeryBuiRefreshMessage : BoundUserInterfaceMessage +{ +} + [Serializable, NetSerializable] public sealed class SurgeryStepChosenBuiMsg(NetEntity part, EntProtoId surgery, EntProtoId step, bool isBody) : BoundUserInterfaceMessage { @@ -24,4 +29,4 @@ public sealed class SurgeryStepChosenBuiMsg(NetEntity part, EntProtoId surgery, // Used as a marker for whether or not we're hijacking surgery by applying it on the body itself. public readonly bool IsBody = isBody; -} \ No newline at end of file +} diff --git a/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs b/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs new file mode 100644 index 00000000000000..9d41401d7f8ad9 --- /dev/null +++ b/Content.Shared/Medical/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/MedicalScanner/HealthAnalyzerScannedUserMessage.cs b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs index 08af1a36a7b9ac..857ad6b83686b9 100644 --- a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs +++ b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs @@ -1,3 +1,5 @@ +using Content.Shared.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; // Shitmed - 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; // Shitmed } } diff --git a/Resources/Locale/en-US/surgery/surgery-ui.ftl b/Resources/Locale/en-US/surgery/surgery-ui.ftl index 51fd9392c45b47..cf58da3977be55 100644 --- a/Resources/Locale/en-US/surgery/surgery-ui.ftl +++ b/Resources/Locale/en-US/surgery/surgery-ui.ftl @@ -6,6 +6,6 @@ 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-ui-window-steps-error-laying = They need to be laying down! +surgery-ui-window-steps-error-tools = Missing tools. +surgery-error-laying = They need to be laying down! surgery-error-self-surgery = You can't perform surgery on yourself! diff --git a/Resources/Prototypes/Body/Parts/harpy.yml b/Resources/Prototypes/Body/Parts/harpy.yml index 9e51334406fa04..8b463f953fc326 100644 --- a/Resources/Prototypes/Body/Parts/harpy.yml +++ b/Resources/Prototypes/Body/Parts/harpy.yml @@ -6,6 +6,8 @@ components: - type: Damageable damageContainer: Biological + - type: Gibbable + - type: SurgeryTool - type: BodyPart - type: ContainerContainer containers: @@ -16,6 +18,36 @@ - type: Tag tags: - Trash + - 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 + - type: entity id: TorsoHarpy @@ -31,6 +63,11 @@ state: "torso_m" - type: BodyPart partType: Torso + toolName: "a torso" + containerName: "torso_slot" + - type: ContainerContainer + containers: + torso_slot: !type:ContainerSlot {} - type: entity id: HeadHarpy @@ -46,6 +83,7 @@ state: "head_m" - type: BodyPart partType: Head + toolName: "a head" vital: true - type: Input context: "ghost" @@ -70,6 +108,7 @@ - type: BodyPart partType: Arm symmetry: Left + toolName: "a left arm" - type: entity id: RightArmHarpy @@ -86,6 +125,7 @@ - type: BodyPart partType: Arm symmetry: Right + toolName: "a right arm" - type: entity id: LeftHandHarpy @@ -102,6 +142,7 @@ - type: BodyPart partType: Hand symmetry: Left + toolName: "a left hand" - type: entity id: RightHandHarpy @@ -118,6 +159,7 @@ - type: BodyPart partType: Hand symmetry: Right + toolName: "a right hand" - type: entity id: LeftLegHarpy @@ -134,6 +176,7 @@ - type: BodyPart partType: Leg symmetry: Left + toolName: "a left leg" - type: MovementBodyPart - type: entity @@ -151,6 +194,7 @@ - type: BodyPart partType: Leg symmetry: Right + toolName: "a right leg" - type: MovementBodyPart - type: entity @@ -168,6 +212,7 @@ - type: BodyPart partType: Foot symmetry: Left + toolName: "a left foot" - type: entity id: RightFootHarpy @@ -184,3 +229,5 @@ - type: BodyPart partType: Foot symmetry: Right + toolName: "a right foot" + diff --git a/Resources/Prototypes/Body/Parts/skeleton.yml b/Resources/Prototypes/Body/Parts/skeleton.yml index ffba0c7c44aa85..e2200f7194eeb2 100644 --- a/Resources/Prototypes/Body/Parts/skeleton.yml +++ b/Resources/Prototypes/Body/Parts/skeleton.yml @@ -18,6 +18,35 @@ - type: Tag tags: - Trash + - 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 - type: entity id: TorsoSkeleton diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 7078f93fe70be4..7ba4b3cc7b987b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -397,7 +397,8 @@ damageType: Blunt damage: 10 behaviors: - - !type:GibBehavior { } + - !type:GibBehavior + gibContents: Skip - type: NonSpreaderZombie - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml index a8772a41f8ad1f..35dc3c243051d4 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml @@ -79,7 +79,7 @@ - trigger: !type:DamageTrigger damage: 500 behaviors: - - !type:GibBehavior {} + - !type:GibBehavior { } - type: Icon sprite: Mobs/Species/IPC/parts.rsi state: full diff --git a/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml b/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml index 2d34c87cfb4f89..cf3aa6b1caa784 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml @@ -40,7 +40,8 @@ !type:DamageTrigger damage: 150 behaviors: - - !type:GibBehavior { } + - !type:GibBehavior + gibContents: Skip - type: SlowOnDamage #modified speeds because they're so weak speedModifierThresholds: 60: 0.9 diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 56b8d3f35bc782..5512cbf5ec04e7 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -1012,6 +1012,7 @@ - MedkitRadiation - MedkitCombat - Scalpel + - BoneGel - Retractor - Cautery - Drill @@ -1540,4 +1541,4 @@ - SynthLeftArm - SynthRightArm - SynthLeftHand - - SynthRightHand \ No newline at end of file + - SynthRightHand diff --git a/Resources/Prototypes/Entities/Surgery/surgeries.yml b/Resources/Prototypes/Entities/Surgery/surgeries.yml index 1729cdd306c9d8..5b0e7c83abf8bc 100644 --- a/Resources/Prototypes/Entities/Surgery/surgeries.yml +++ b/Resources/Prototypes/Entities/Surgery/surgeries.yml @@ -69,7 +69,7 @@ categories: [ HideSpawnMenu ] components: - type: Surgery - requirement: SurgeryOpenIncision + #requirement: SurgeryOpenIncision steps: - SurgeryStepInsertFeature - SurgeryStepSealWounds @@ -149,7 +149,7 @@ - type: entity parent: SurgeryBase id: SurgeryAttachLeftHand - name: Attach Left Han + name: Attach Left Hand categories: [ HideSpawnMenu ] components: - type: Surgery diff --git a/Resources/Prototypes/Mood/genericNegativeEffects.yml b/Resources/Prototypes/Mood/genericNegativeEffects.yml index 482cddb376c280..d5b57f09454560 100644 --- a/Resources/Prototypes/Mood/genericNegativeEffects.yml +++ b/Resources/Prototypes/Mood/genericNegativeEffects.yml @@ -47,4 +47,5 @@ # Surgery - type: moodEffect id: SurgeryPain - moodChange: -10 \ No newline at end of file + moodChange: -10 + timeout: 360 diff --git a/Resources/Prototypes/Recipes/Lathes/medical.yml b/Resources/Prototypes/Recipes/Lathes/medical.yml index 10658252a4bada..bec71ce3d13e96 100644 --- a/Resources/Prototypes/Recipes/Lathes/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/medical.yml @@ -73,7 +73,7 @@ - type: latheRecipe id: BoneGel result: BoneGel - completetime: 10 + completetime: 2 materials: Plastic: 200 Plasma: 200