diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Patch.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Patch.cs new file mode 100644 index 0000000000..3ad6899e8f --- /dev/null +++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Patch.cs @@ -0,0 +1,175 @@ +using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Mobs.Components; +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Chemistry; +using Content.Shared.DoAfter; + +namespace Content.Server.Chemistry.EntitySystems +{ + public sealed partial class ChemistrySystem + { + [Dependency] private readonly ReactiveSystem _reactive = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + + public void InitializePatch() + { + SubscribeLocalEvent(OnPatchDoAfter); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnSolutionChange); + } + + private void OnPatchDoAfter(Entity entity, ref PatchDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + TryDoInject(entity, args.Args.Target.Value, args.Args.User); + args.Handled = true; + } + + private void PatchDoAfter(Entity patch, EntityUid target, EntityUid user) + { + var (uid, component) = patch; + + // Dont need to start DoAfter if patch is empty + if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var _, out var patchSolution) || patchSolution.Volume == 0) + { + _popup.PopupCursor(Loc.GetString("patch-component-empty-message"), user); + return; + } + + // Create a pop-up for the user + _popup.PopupEntity(Loc.GetString("patch-component-injecting-user"), target, user); + + var isTarget = user != target; + + if (isTarget) + { + // Create a pop-up for the target + var userName = Identity.Entity(user, EntityManager); + _popup.PopupEntity(Loc.GetString("injector-component-injecting-target", + ("user", userName)), user, target); + } + + var actualDelay = MathHelper.Max(patch.Comp.Delay, TimeSpan.FromSeconds(1)); + + // Injections take 0.5 seconds longer per additional 5u + actualDelay += TimeSpan.FromSeconds(patchSolution.Volume.Float() / component.Delay.TotalSeconds - 0.5f); + + _adminLogger.Add(LogType.ForceFeed, $"{_entMan.ToPrettyString(user):user} is attempting to put a patch on {_entMan.ToPrettyString(target):target}"); + + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new PatchDoAfterEvent(), patch.Owner, target: target, used: patch.Owner) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnTargetMove = true + }); + } + + /// + /// Actually difference between OnUseInHand and OnAfterInteract only in target + /// In OnUseInHand target is always = user. In OnAfterInteract target may be user or may not + /// + private void OnUseInHand(Entity entity, ref UseInHandEvent args) + { + if (args.Handled) + return; + + if (args.User is not { Valid: true } target) + return; + + PatchDoAfter(entity, target, args.User); + + args.Handled = true; + } + + private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) + { + if (!args.CanReach || args.Handled) + return; + + var (_, component) = entity; + + if (!EligibleEntity(args.Target, _entMan, component)) + return; + + if (args.Target is not { Valid: true } target) + return; + + var user = args.User; + + PatchDoAfter(entity, target, user); + args.Handled = true; + } + + private void OnSolutionChange(Entity entity, ref SolutionContainerChangedEvent args) + { + Dirty(entity); + } + + private bool TryDoInject(Entity patch, EntityUid? target, EntityUid user) + { + var (uid, component) = patch; + + string? msgFormat = null; + if (!EligibleEntity(target, _entMan, component)) + return false; + + if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var patchSoln, out var patchSolution) || patchSolution.Volume == 0) + { + // TODO: Empty patch should stop the bleeding + + _popup.PopupCursor(Loc.GetString("patch-component-empty-message"), user); + return true; + } + + if (!_solutionContainers.TryGetInjectableSolution(target.Value, out var targetSoln, out var targetSolution)) + { + _popup.PopupCursor(Loc.GetString("patch-cant-inject", ("target", Identity.Entity(target.Value, _entMan))), user); + return false; + } + + if (patchSolution.Volume > targetSolution.AvailableVolume) + { + _popup.PopupCursor(Loc.GetString("patch-cant-inject-now"), user); + return false; + } + + var removedSolution = _solutionContainers.SplitSolution(patchSoln.Value, patchSolution.Volume); + + _popup.PopupCursor(Loc.GetString(msgFormat ?? "patch-component-inject-other-message", ("other", target)), user); + + if (!targetSolution.CanAddSolution(removedSolution)) + return true; + + _reactive.DoEntityReaction(target.Value, removedSolution, ReactionMethod.Touch); + // Transfering only half of the solution via Injection method + removedSolution.ScaleSolution(0.5f); + _reactive.DoEntityReaction(target.Value, removedSolution, ReactionMethod.Injection); + _solutionContainers.TryAddSolution(targetSoln.Value, removedSolution); + QueueDel(patch); + + _adminLogger.Add(LogType.ForceFeed, $"{_entMan.ToPrettyString(user):user} put a patch on {_entMan.ToPrettyString(target.Value):target} with a solution {SolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {_entMan.ToPrettyString(uid):using}"); + + return true; + } + + static bool EligibleEntity([NotNullWhen(true)] EntityUid? entity, IEntityManager entMan, PatchComponent component) + { + // Using patch only on mobs + return component.OnlyMobs + ? entMan.HasComponent(entity) && + entMan.HasComponent(entity) + : entMan.HasComponent(entity); + } + } +} diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs index c4f22dc63a..97d1c65757 100644 --- a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs @@ -22,5 +22,6 @@ public sealed partial class ChemistrySystem : EntitySystem // Why ChemMaster duplicates reagentdispenser nobody knows. InitializeHypospray(); InitializeMixing(); + InitializePatch(); } } diff --git a/Content.Shared/Chemistry/Components/PatchComponent.cs b/Content.Shared/Chemistry/Components/PatchComponent.cs new file mode 100644 index 0000000000..da014a4f53 --- /dev/null +++ b/Content.Shared/Chemistry/Components/PatchComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry.Components; + +[Serializable, NetSerializable] +public sealed partial class PatchDoAfterEvent : SimpleDoAfterEvent +{ +} + +/// +/// Implements draw/inject behavior for droppers and syringes. +/// +/// +/// Can optionally support both +/// injection and drawing or just injection. Can inject/draw reagents from solution +/// containers, and can directly inject into a mobs bloodstream. +/// + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class PatchComponent : Component +{ + + [ViewVariables, AutoNetworkedField] + public FixedPoint2 CurrentVolume; + + [ViewVariables, AutoNetworkedField] + public FixedPoint2 TotalVolume; + + [DataField("solutionName")] + public string SolutionName = "patch"; + + [DataField("onlyMobs")] + public bool OnlyMobs = true; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("delay")] + public TimeSpan Delay = TimeSpan.FromSeconds(5); + +} diff --git a/Resources/Locale/en-US/chemistry/components/patch-component.ftl b/Resources/Locale/en-US/chemistry/components/patch-component.ftl new file mode 100644 index 0000000000..f62ab45a35 --- /dev/null +++ b/Resources/Locale/en-US/chemistry/components/patch-component.ftl @@ -0,0 +1,10 @@ +## Entity + +patch-component-target-getting-injected = Someone trying to put a patch on you +patch-component-injecting-user = You are trying to put a patch on {$target}. +patch-component-inject-other-message = You put a patch on {$other}. +patch-component-inject-self-message = You put a patch on yourself. +patch-component-empty-message = It's empty! +patch-component-feel-prick-message = You feel a little sting +patch-cant-inject = Can't put a patch on {$target}! +patch-cant-inject-now = Can't put a patch now diff --git a/Resources/Locale/ru-RU/chemistry/components/patch-component.ftl b/Resources/Locale/ru-RU/chemistry/components/patch-component.ftl new file mode 100644 index 0000000000..86c4a12e47 --- /dev/null +++ b/Resources/Locale/ru-RU/chemistry/components/patch-component.ftl @@ -0,0 +1,13 @@ +## Entity + +patch-component-injecting-user = Вы приклеиваете пластырь. +patch-component-inject-other-message = Вы приклеили пластырь к {$other}. +patch-component-inject-self-message = Вы наложили на себя пластырь. +patch-component-empty-message = Пластырь пуст! +patch-component-feel-prick-message = Вы чувствуете небольшое жжение. +patch-cant-inject = Вы не моежете прикрепить пластырь к {$target}! +patch-cant-inject-now = Вы не можете наложить пластырь сейчас + +## mob-inject doafter messages + +injector-component-injecting-target = {CAPITALIZE(THE($user))} пытается приклеить пластырь на вас! diff --git a/Resources/Locale/ru-RU/locales-new/autotranslate-54.ftl b/Resources/Locale/ru-RU/locales-new/autotranslate-54.ftl index b744a0df1c..eabd40d46d 100644 --- a/Resources/Locale/ru-RU/locales-new/autotranslate-54.ftl +++ b/Resources/Locale/ru-RU/locales-new/autotranslate-54.ftl @@ -18,3 +18,9 @@ ent-BaseLightTube = { ent-BaseItem } .desc = { ent-BaseItem.desc } ent-HandheldHealthAnalyzerEmpty = { ent-HandheldHealthAnalyzer } .desc = { ent-HandheldHealthAnalyzer.desc } +ent-Patch = пластырь + .desc = Медицинский пластырь, доставляющий реагенты сквозь кожу прямо в кровь. +ent-BurnPatch = пластырь от ожогов + .desc = Инновационный пластырь для ускоренного лечения мелких ожогов. +ent-BrutePatch = пластырь от ушибов + .desc = Компоненты пластыря непрерывно поступают к источнику боли, заживляя мелкие порезы и царапины. diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml index df23526d30..bfcf9b970f 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml @@ -12,5 +12,8 @@ SyringeBicaridine: 1 SyringeDermaline: 1 Eftpos: 2 + Patch: 20 + BrutePatch: 10 + BurnPatch: 10 emaggedInventory: SyringeEphedrine: 1 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/patch.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/patch.yml new file mode 100644 index 0000000000..1f25e5c06e --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/patch.yml @@ -0,0 +1,68 @@ +- type: entity + parent: BaseItem + id: BasePatch + abstract: true + components: + - type: Sprite + sprite: Objects/Specific/Medical/patch.rsi + layers: + - state: patch + - state: patch1 + map: [ "enum.SolutionContainerLayers.Fill" ] + visible: false + - type: Item + sprite: Objects/Specific/Medical/patch.rsi + - type: SolutionContainerManager + solutions: + patch: + maxVol: 40 + - type: RefillableSolution + solution: patch + - type: ExaminableSolution + solution: patch + - type: Appearance + - type: SolutionContainerVisuals + - type: Patch + OnlyMobs: false + - type: StaticPrice + price: 30 + +- type: entity + name: patch + parent: BasePatch + description: a medicated patch that can deliver drugs directly into the bloodstream through the layers of the skin. + id: Patch + components: + - type: StaticPrice + price: 10 + - type: SolutionContainerVisuals + maxFillLevels: 4 + fillBaseName: patch + +- type: entity + name: brute patch + parent: Patch + description: This innovative medical patch utilizes advanced biomimetic technology to significantly accelerate the healing of minor cuts and scrapes. + id: BrutePatch + components: + - type: SolutionContainerManager + solutions: + patch: + maxVol: 40 + reagents: + - ReagentId: Bicaridine + Quantity: 10 + +- type: entity + name: burn patch + parent: Patch + description: This type of patch contains a drug reservoir made of a substance that heals burns. + id: BurnPatch + components: + - type: SolutionContainerManager + solutions: + patch: + maxVol: 40 + reagents: + - ReagentId: Dermaline + Quantity: 10 diff --git a/Resources/Textures/Objects/Specific/Medical/patch.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/patch.rsi/meta.json new file mode 100644 index 0000000000..d0f83381d3 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/patch.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-4.0", + "copyright": "Copyright (c) 2024 RinKeeper", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "patch" + }, + { + "name": "patch1" + }, + { + "name": "patch2" + }, + { + "name": "patch3" + }, + { + "name": "patch4" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch.png b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch.png new file mode 100644 index 0000000000..ea0b594374 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch1.png b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch1.png new file mode 100644 index 0000000000..2cd18e6b5b Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch1.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch2.png b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch2.png new file mode 100644 index 0000000000..6d5b141b0d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch2.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch3.png b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch3.png new file mode 100644 index 0000000000..d643e1edc9 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch3.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch4.png b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch4.png new file mode 100644 index 0000000000..7c2f289160 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/patch.rsi/patch4.png differ