diff --git a/Content.Server/White/SelfHeal/SelfHealSystem.cs b/Content.Server/White/SelfHeal/SelfHealSystem.cs new file mode 100644 index 0000000000..84a301da53 --- /dev/null +++ b/Content.Server/White/SelfHeal/SelfHealSystem.cs @@ -0,0 +1,180 @@ +using Content.Server.Administration.Logs; +using Content.Server.Nutrition.Components; +using Content.Server.Popups; +using Content.Shared.Actions; +using Content.Shared.Damage; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Humanoid; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.White.SelfHeal; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; + +namespace Content.Server.White.SelfHeal; + +public sealed class SelfHealSystem: EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnHealingAfterInteract); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnInit(EntityUid uid, SelfHealComponent component, ComponentInit args) + { + _actionsSystem.AddAction(uid, ref component.ActionEntity, component.Action); + } + + private void OnHealingAfterInteract(EntityUid uid, SelfHealComponent component, SelfHealEvent args) + { + if (args.Handled) + return; + + TryHeal(args.Performer, args.Target, component); + args.Handled = true; + } + + private void OnDoAfter(EntityUid uid, DamageableComponent component, SelfHealDoAfterEvent args) + { + var dontRepeat = false; + + if (!TryComp(args.User, out SelfHealComponent? healing) || args.Target == null) + return; + + if (args.Handled || args.Cancelled) + return; + + if (healing.DamageContainers is not null && + component.DamageContainerID is not null && + !healing.DamageContainers.Contains(component.DamageContainerID)) + { + return; + } + + if (!CanHeal(args.User, args.Target.Value, healing)) + return; + + Heal(args.Target.Value, healing, args); + + // Logic to determine the whether or not to repeat the healing action + args.Repeat = (HasDamage(component, healing) && !dontRepeat); + if (!args.Repeat && !dontRepeat) + _popupSystem.PopupEntity(Loc.GetString("self-heal-finished-using", ("verb", Loc.GetString("self-heal-lick")), ("name", uid)), uid, args.User); + + args.Handled = true; + + } + + private void Heal(EntityUid uid, SelfHealComponent component, SelfHealDoAfterEvent args) + { + var healed = _damageable.TryChangeDamage(uid, component.Damage, true, origin: args.Args.User); + + if (healed == null) + return; + + var total = healed.GetTotal(); + var userString = EntityManager.ToPrettyString(args.User); + var targetString = EntityManager.ToPrettyString(uid); + + var healMessage = uid != args.User + ? $"{userString:user} healed {targetString:target} for {total:damage} with {Loc.GetString("self-heal-lick")}" + : $"{userString:user} healed themselves for {total:damage} with {Loc.GetString("self-heal-lick")}"; + _adminLogger.Add(LogType.Healed, $"{healMessage}"); + + if (TryComp(args.User, out var selfHealComponent)) + { + var audio = selfHealComponent.HealingSound; + var audioParams = new AudioParams().WithVariation(2f).WithVolume(-5f); + + _audio.PlayPvs(audio, args.User, audioParams); + _popupSystem.PopupEntity(Loc.GetString("self-heal-using-other", ("name", uid), ("verb", Loc.GetString("self-heal-lick"))), uid); + } + } + + private bool CanHeal(EntityUid user, EntityUid target, SelfHealComponent component) + { + if (!TryComp(target, out var targetDamage) || !HasComp(target)) + return false; + + if (user != target && !_interactionSystem.InRangeUnobstructed(user, target, popup: true)) + return false; + + if (!HasDamage(targetDamage, component)) + { + var popup = Loc.GetString("self-heal-cant-use", ("verb", Loc.GetString("self-heal-lick")), + ("name", target)); + _popupSystem.PopupEntity(popup, user, user); + return false; + } + + if (component.DisallowedClothingUser != null) + { + foreach (var clothing in component.DisallowedClothingUser) + { + if (_inventorySystem.TryGetSlotEntity(user, clothing, out var blockedClothing) && + EntityManager.TryGetComponent(blockedClothing, out var blocker) && + blocker.Enabled) + { + var popup = Loc.GetString("self-heal-cant-use-clothing", ("verb", Loc.GetString("self-heal-lick")), ("clothing", blockedClothing)); + _popupSystem.PopupEntity(popup, user, user); + return false; + } + } + } + + if (component.DisallowedClothingTarget != null) + { + foreach (var clothing in component.DisallowedClothingTarget) + { + if (_inventorySystem.TryGetSlotEntity(target, clothing, out var blockedClothing)) + { + var popup = Loc.GetString("self-heal-cant-use-clothing-other", ("verb", Loc.GetString("self-heal-lick")), ("name", target), ("clothing", blockedClothing)); + _popupSystem.PopupEntity(popup, user, user); + return false; + } + } + } + + return true; + } + + private void TryHeal(EntityUid user, EntityUid target, SelfHealComponent component) + { + if (!CanHeal(user, target, component)) + return; + + var doAfterEventArgs = + new DoAfterArgs(EntityManager, user, component.Delay, new SelfHealDoAfterEvent(), target, target) + { + BreakOnUserMove = true, + BreakOnTargetMove = true, + }; + _doAfter.TryStartDoAfter(doAfterEventArgs); + } + + private bool HasDamage(DamageableComponent component, SelfHealComponent healing) + { + var damageableDict = component.Damage.DamageDict; + var healingDict = healing.Damage.DamageDict; + foreach (var type in healingDict) + { + if (damageableDict[type.Key].Value > 0) + return true; + } + + return false; + } +} diff --git a/Content.Shared/White/SelfHeal/SelfHealComponent.cs b/Content.Shared/White/SelfHeal/SelfHealComponent.cs new file mode 100644 index 0000000000..f2db22c61f --- /dev/null +++ b/Content.Shared/White/SelfHeal/SelfHealComponent.cs @@ -0,0 +1,40 @@ +using Content.Shared.Actions; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.White.SelfHeal; + +[RegisterComponent] +public sealed partial class SelfHealComponent: Component +{ + [ViewVariables(VVAccess.ReadWrite), DataField("delay")] + public float Delay = 3f; + + [ViewVariables(VVAccess.ReadWrite), DataField("healingSound")] + public SoundSpecifier? HealingSound; + + [ViewVariables(VVAccess.ReadWrite), DataField("damage", required: true)] + public DamageSpecifier Damage = default!; + + [ViewVariables(VVAccess.ReadWrite), DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List? DamageContainers; + + [ViewVariables(VVAccess.ReadWrite),DataField("disallowedClothingUser")] + public List? DisallowedClothingUser; + + [ViewVariables(VVAccess.ReadWrite), DataField("disallowedClothingTarget")] + public List? DisallowedClothingTarget; + + [DataField] + public EntProtoId Action = "SelfHealAction"; + + [DataField] + public EntityUid? ActionEntity; +} + +public sealed partial class SelfHealEvent : EntityTargetActionEvent +{ +} diff --git a/Content.Shared/White/SelfHeal/SelfHealDoAfterEvent.cs b/Content.Shared/White/SelfHeal/SelfHealDoAfterEvent.cs new file mode 100644 index 0000000000..9207bc6ab2 --- /dev/null +++ b/Content.Shared/White/SelfHeal/SelfHealDoAfterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.White.SelfHeal; + +[Serializable, NetSerializable] +public sealed partial class SelfHealDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Resources/Audio/White/Felinid/lick.ogg b/Resources/Audio/White/Felinid/lick.ogg new file mode 100644 index 0000000000..f988ed0238 Binary files /dev/null and b/Resources/Audio/White/Felinid/lick.ogg differ diff --git a/Resources/Locale/en-US/medical/components/self-healing-component.ftl b/Resources/Locale/en-US/medical/components/self-healing-component.ftl new file mode 100644 index 0000000000..e654d44cbf --- /dev/null +++ b/Resources/Locale/en-US/medical/components/self-healing-component.ftl @@ -0,0 +1,8 @@ +self-heal-finished-using = You have finished {$verb}ing all {$name}`s wounds +self-heal-cant-use = There is no damage you can heal by {$verb}ing {$name} +self-heal-stop-bleeding = They have stopped bleeding +self-heal-lick = lick +self-heal-cant-use-clothing = You cant {$verb}ing yourself while wearing a {$clothing} +self-heal-cant-use-clothing-other = You cant {$verb}ing {$name} while {$name} is wearing a {$clothing} +self-heal-action = Lick the wounds +self-heal-using-other = {$name} have {$verb}ed some of {$name} wounds diff --git a/Resources/Locale/ru-RU/medical/components/self-healing-component.ftl b/Resources/Locale/ru-RU/medical/components/self-healing-component.ftl new file mode 100644 index 0000000000..ec2db16b58 --- /dev/null +++ b/Resources/Locale/ru-RU/medical/components/self-healing-component.ftl @@ -0,0 +1,9 @@ +self-heal-finished-using = Вы закончили {$verb} все раны {$name}! +self-heal-cant-use = {$name} не имеет ран, которые вы могли бы {$verb} +self-heal-stop-bleeding = Оно перестало кровоточить +self-heal-lick = вылизывать +self-heal-cant-use-clothing = Вы не можете {$verb}, пока на вас {$clothing} +self-heal-cant-use-clothing-other = Вы не можете {$verb} {$name}, пока {$name} носит {$clothing} +self-heal-action = Зализать раны +self-heal-using-other = {$name} закончил {$verb} часть {$name} ран +ent-SelfHealAction = Зализать раны diff --git a/Resources/Prototypes/White/Mobs/Player/felinid.yml b/Resources/Prototypes/White/Mobs/Player/felinid.yml index 2286b850c9..61cb484f76 100644 --- a/Resources/Prototypes/White/Mobs/Player/felinid.yml +++ b/Resources/Prototypes/White/Mobs/Player/felinid.yml @@ -37,3 +37,18 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others + - type: SelfHeal + damageContainers: + - Biological + damage: + types: + Slash: -0.4 + Piercing: -0.4 + disallowedClothingUser: + - mask + disallowedClothingTarget: + - jumpsuit + - outerClothing + healingSound: + path: "/Audio/White/Felinid/lick.ogg" + diff --git a/Resources/Prototypes/White/Species/actions.yml b/Resources/Prototypes/White/Species/actions.yml index 8a305c922e..9a2fe9306d 100644 --- a/Resources/Prototypes/White/Species/actions.yml +++ b/Resources/Prototypes/White/Species/actions.yml @@ -21,3 +21,12 @@ event: !type:HairballActionEvent charges: 1 useDelay: 30 + +- type: entity + id: SelfHealAction + name: Lick wounds. + noSpawn: true + components: + - type: EntityTargetAction + event: !type:SelfHealEvent + icon: White/Icons/tongue.png diff --git a/Resources/Textures/White/Icons/tongue.png b/Resources/Textures/White/Icons/tongue.png new file mode 100644 index 0000000000..5cffd4e0e3 Binary files /dev/null and b/Resources/Textures/White/Icons/tongue.png differ