diff --git a/Content.Server/_White/BodyArmor/ArmorPlates/ArmorPlateComponent.cs b/Content.Server/_White/BodyArmor/ArmorPlates/ArmorPlateComponent.cs new file mode 100644 index 00000000000..e255f7a06e3 --- /dev/null +++ b/Content.Server/_White/BodyArmor/ArmorPlates/ArmorPlateComponent.cs @@ -0,0 +1,31 @@ +using Content.Shared.FixedPoint; + +namespace Content.Server._White.BodyArmor.ArmorPlates; + +[RegisterComponent] +public sealed partial class ArmorPlateComponent : Component +{ + [DataField] + public int AllowedDamage = 100; + + [DataField] + public int ReceivedDamage; + + [DataField("tier")] + public PlateTier PlateTier = 0; + + [DataField] + public Dictionary DamageOfTier = new() + { + { PlateTier.TierOne, 7 }, + { PlateTier.TierTwo, 12 }, + { PlateTier.TierThree, 14 } + }; +} + +public enum PlateTier : byte +{ + TierOne = 0, + TierTwo = 1, + TierThree = 2, +} diff --git a/Content.Server/_White/BodyArmor/ArmorPlates/ArmorPlateSystem.cs b/Content.Server/_White/BodyArmor/ArmorPlates/ArmorPlateSystem.cs new file mode 100644 index 00000000000..8593f8f4e98 --- /dev/null +++ b/Content.Server/_White/BodyArmor/ArmorPlates/ArmorPlateSystem.cs @@ -0,0 +1,97 @@ +using Content.Server._White.BodyArmor.PlateCarrier; +using Content.Server.DoAfter; +using Content.Shared._White.BodyArmor; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Robust.Server.Containers; +using Robust.Shared.Containers; + +namespace Content.Server._White.BodyArmor.ArmorPlates; + +public sealed class ArmorPlateSystem : EntitySystem +{ + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly ContainerSystem _containerSystem = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent(OnPutPlateDoAfter); + SubscribeLocalEvent(OnExamined); + } + + private void OnExamined(EntityUid uid, ArmorPlateComponent component, ExaminedEvent args) + { + var hasDamage = component.ReceivedDamage > 0 ? "имеются визуальные повреждения." : "визуальные повреждения отсутствуют."; + Dictionary tierList = new() + { + { PlateTier.TierOne, "первый уровень броне-защиты" }, + { PlateTier.TierTwo, "второй уровень броне-защиты" }, + { PlateTier.TierThree, "третий уровень броне-защиты" } + }; + + using (args.PushGroup(nameof(ArmorPlateComponent))) + { + args.PushMarkup(Loc.GetString("armorplate-tier", ("tier", tierList[component.PlateTier]))); + args.PushMarkup(Loc.GetString("armorplate-damage", ("hasdamage", hasDamage))); + } + } + + private void OnInteract(EntityUid uid, ArmorPlateComponent component, AfterInteractEvent args) + { + var platecarrier = args.Target; + + if(!TryComp(platecarrier, out var plateCarrierComponent)) + return; + + if(plateCarrierComponent.PlateIsClosed) + return; + + if(plateCarrierComponent.HasPlate) + return; + + var doAfterEventArgs = new DoAfterArgs(EntityManager, + args.User, + plateCarrierComponent.TimeToPutPlate, + new PutPlateDoAfterEvent(), + uid, + used: uid, + target: platecarrier) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + if(!_doAfterSystem.TryStartDoAfter(doAfterEventArgs)) + return; + + args.Handled = true; + } + + private void OnPutPlateDoAfter(EntityUid uid, ArmorPlateComponent component, PutPlateDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + if(args.Target == null || args.Used == null) + return; + + var platecarrier = (EntityUid)args.Target; + + if (!TryComp(platecarrier, out var plateCarrierComponent)) + return; + + var armorplate = (EntityUid)args.Used; + + var armorPlateContainer = + _containerSystem.EnsureContainer(platecarrier, PlateCarrierComponent.ArmorPlateContainer); + + _containerSystem.Insert(armorplate, armorPlateContainer); + plateCarrierComponent.HasPlate = true; + + args.Handled = true; + } +} diff --git a/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierComponent.cs b/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierComponent.cs new file mode 100644 index 00000000000..0e41186de02 --- /dev/null +++ b/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierComponent.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Audio; + +namespace Content.Server._White.BodyArmor.PlateCarrier; + +[RegisterComponent] +public sealed partial class PlateCarrierComponent : Component +{ + public static string ArmorPlateContainer = "armor_plate"; + + [DataField] + public bool PlateIsClosed = true; // true - застегнут отдел для бронеплит + + [DataField] + public bool HasPlate = false; + + [DataField] + public string PlateCarrierSlot = "outerClothing"; + + [DataField] + public bool IsBreak = false; + + [DataField] + public float ChanceOfBreak = 0.05F; + + [DataField] + [ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier BreakSound = new SoundPathSpecifier("/Audio/White/BodyArmor/PlateCarrier/break.ogg"); + + [DataField] + public int PlateCarrierDamage = 0; + + [DataField] + [ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/White/BodyArmor/PlateCarrier/open.ogg"); + + [DataField] + [ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier CloseSound = new SoundPathSpecifier("/Audio/White/BodyArmor/PlateCarrier/close.ogg"); + + [DataField] + public TimeSpan TimeToPutPlate = TimeSpan.FromSeconds(3); +} diff --git a/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierOnUserComponent.cs b/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierOnUserComponent.cs new file mode 100644 index 00000000000..d9d572a4661 --- /dev/null +++ b/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierOnUserComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server._White.BodyArmor.PlateCarrier; + +[RegisterComponent] +public sealed partial class PlateCarrierOnUserComponent : Component +{ + [DataField] + public EntityUid? PlateCarrier; +} diff --git a/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierSystem.cs b/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierSystem.cs new file mode 100644 index 00000000000..bdaf771ea12 --- /dev/null +++ b/Content.Server/_White/BodyArmor/PlateCarrier/PlateCarrierSystem.cs @@ -0,0 +1,257 @@ +using System.Linq; +using Content.Server._White.BodyArmor.ArmorPlates; +using Content.Server.DoAfter; +using Content.Server.Hands.Systems; +using Content.Shared._White.BodyArmor; +using Content.Shared.Coordinates; +using Content.Shared.Damage; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.FixedPoint; +using Content.Shared.Hands.Components; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Ranged.Components; +using Robust.Server.Audio; +using Robust.Server.Containers; +using Robust.Shared.Containers; +using Robust.Shared.Random; + +namespace Content.Server._White.BodyArmor.PlateCarrier; + +public sealed class PlateCarrierSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly ContainerSystem _containerSystem = default!; + [Dependency] private readonly HandsSystem _handsSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnAltVerb); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnGetPlateDoAfter); + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnUnequipped); + SubscribeLocalEvent(OnUserGetDamage); + SubscribeLocalEvent(BeforeEquipPlateCarrier); + } + + private void OnExamined(EntityUid uid, PlateCarrierComponent component, ExaminedEvent args) + { + var hasPlate = component.HasPlate ? "установлена." : "не установлена."; + var hasDamage = component.PlateCarrierDamage > 0 ? "имеются визуальные повреждения." : "визуальные повреждения отсутствуют."; + var isBreak = component.IsBreak; + + using (args.PushGroup(nameof(PlateCarrierComponent))) + { + args.PushMarkup(Loc.GetString("armorplate-place", ("hasplate", hasPlate))); + args.PushMarkup(Loc.GetString("platecarrier-damage", ("hasdamage", hasDamage))); + args.PushMarkup(isBreak ? Loc.GetString("platecarrier-break") : Loc.GetString("platecarrier-nobreak")); + } + } + + private void OnAltVerb(GetVerbsEvent args) + { + var platecarrier = args.Target; + if(!TryComp(platecarrier, out var plateCarrierComponent)) + return; + + if (plateCarrierComponent is { HasPlate: true, PlateIsClosed: false }) + { + AlternativeVerb plateVerb = new() + { + Act = () => + { + GetArmorPlate(args.User, args.Target, plateCarrierComponent); + }, + Disabled = false, + Priority = 1, + Text = Loc.GetString("getplate"), + }; + args.Verbs.Add(plateVerb); + } + + AlternativeVerb verb = new() + { + Act = () => + { + SetPlateCarrierClosed(args.Target, plateCarrierComponent); + }, + Disabled = false, + Priority = 3, + Text = Loc.GetString("platecarrierclosed", ("closed", (plateCarrierComponent.PlateIsClosed ? "Расстегнуть" : "Застегнуть"))), + }; + + args.Verbs.Add(verb); + } + + private void BeforeEquipPlateCarrier(EntityUid uid, PlateCarrierComponent component, BeingEquippedAttemptEvent args) + { + if (component.IsBreak) + { + args.Cancel(); + } + } + + private void OnEquipped(EntityUid uid, PlateCarrierComponent component, GotEquippedEvent args) + { + if(HasComp(args.Equipee)) + return; + + var userComp = EnsureComp(args.Equipee); + userComp.PlateCarrier = args.Equipment; + } + + private void OnUnequipped(EntityUid uid, PlateCarrierComponent component, GotUnequippedEvent args) + { + UnequipHelper(args.Equipee); + } + + private void OnUserGetDamage(EntityUid uid, PlateCarrierOnUserComponent component, DamageModifyEvent args) + { + if(args.Origin == null) + return; + + var attacker = args.Origin; + + if (!_handsSystem.TryGetActiveHand((Entity) attacker, out var activeHand)) + return; + + if(activeHand.Container == null) + return; + + if(!HasComp(activeHand.Container.ContainedEntities[0])) + return; + + if(!TryComp(component.PlateCarrier, out var plateCarrierComponent)) + return; + + var intDamage = (int)args.OriginalDamage.DamageDict.First().Value; + + if (!plateCarrierComponent.HasPlate) + { + plateCarrierComponent.PlateCarrierDamage += intDamage; + return; + } + + plateCarrierComponent.PlateCarrierDamage += (intDamage / 2); + var armorPlate = GetArmorPlateInContainer((EntityUid)component.PlateCarrier, plateCarrierComponent); + + if(!TryComp(armorPlate, out var armorPlateComponent)) + return; + + armorPlateComponent.ReceivedDamage += (intDamage / 2); + + var newDamageSpecifier = new DamageSpecifier(); + + foreach (var damage in args.OriginalDamage.DamageDict) + { + newDamageSpecifier.DamageDict.Add(damage.Key, (damage.Value - ApplyDamage(uid, armorPlateComponent, plateCarrierComponent))); + } + + args.Damage = newDamageSpecifier; + } + + private void SetPlateCarrierClosed(EntityUid platecarrier, PlateCarrierComponent plateCarrierComponent) + { + _audioSystem.PlayPvs((plateCarrierComponent.PlateIsClosed ? plateCarrierComponent.OpenSound : plateCarrierComponent.CloseSound), platecarrier.ToCoordinates()); + plateCarrierComponent.PlateIsClosed = !plateCarrierComponent.PlateIsClosed; + } + + private void GetArmorPlate(EntityUid uid, EntityUid platecarrier, PlateCarrierComponent plateCarrierComponent) + { + var doAfterEventArgs = new DoAfterArgs(EntityManager, + uid, + plateCarrierComponent.TimeToPutPlate, + new GetPlateDoAfterEvent(), + platecarrier, + target: platecarrier) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + _doAfterSystem.TryStartDoAfter(doAfterEventArgs); + } + + private void OnGetPlateDoAfter(EntityUid uid, PlateCarrierComponent component, GetPlateDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + if(!component.HasPlate) + return; + + if(args.Target == null) + return; + + var platecarrier = (EntityUid)args.Target; + + var armorPlateContainer = + _containerSystem.EnsureContainer(platecarrier, PlateCarrierComponent.ArmorPlateContainer); + + var plates = armorPlateContainer.ContainedEntities.ToList(); + + _containerSystem.EmptyContainer(armorPlateContainer, true); + foreach (var plate in plates) + { + _handsSystem.PickupOrDrop(args.User, plate); + } + component.HasPlate = false; + + args.Handled = true; + } + + private EntityUid? GetArmorPlateInContainer(EntityUid platecarrier, PlateCarrierComponent component) + { + if(!component.HasPlate) + return null; + + var container = + _containerSystem.EnsureContainer(platecarrier, PlateCarrierComponent.ArmorPlateContainer); + + return container.ContainedEntities[0]; + } + + private FixedPoint2 ApplyDamage(EntityUid target, ArmorPlateComponent armorPlateComponent, PlateCarrierComponent plateCarrierComponent) + { + if (armorPlateComponent.ReceivedDamage >= armorPlateComponent.AllowedDamage) + { + if(!plateCarrierComponent.IsBreak) + ChanceOfBreak(target, plateCarrierComponent); + + return 0; + } + + if (armorPlateComponent.ReceivedDamage >= (armorPlateComponent.AllowedDamage / 2)) + return (armorPlateComponent.DamageOfTier[armorPlateComponent.PlateTier] / 2); + + return armorPlateComponent.DamageOfTier[armorPlateComponent.PlateTier]; + } + + private void ChanceOfBreak(EntityUid target, PlateCarrierComponent plateCarrierComponent) + { + var isBreak = _robustRandom.Prob(plateCarrierComponent.ChanceOfBreak); + + if(!isBreak) + return; + + _audioSystem.PlayPvs(plateCarrierComponent.BreakSound, target); + _inventorySystem.TryUnequip(target, plateCarrierComponent.PlateCarrierSlot, true, true, true); + plateCarrierComponent.IsBreak = true; + } + + private void UnequipHelper(EntityUid user) + { + if(!HasComp(user)) + return; + + RemComp(user); + } +} diff --git a/Content.Shared/_White/BodyArmor/BodyArmorEvents.cs b/Content.Shared/_White/BodyArmor/BodyArmorEvents.cs new file mode 100644 index 00000000000..13bd9701109 --- /dev/null +++ b/Content.Shared/_White/BodyArmor/BodyArmorEvents.cs @@ -0,0 +1,14 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared._White.BodyArmor; + +[Serializable, NetSerializable] +public sealed partial class PutPlateDoAfterEvent : SimpleDoAfterEvent +{ +} + +[Serializable, NetSerializable] +public sealed partial class GetPlateDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Resources/Audio/White/BodyArmor/PlateCarrier/break.ogg b/Resources/Audio/White/BodyArmor/PlateCarrier/break.ogg new file mode 100644 index 00000000000..4e8d1aa6cf2 Binary files /dev/null and b/Resources/Audio/White/BodyArmor/PlateCarrier/break.ogg differ diff --git a/Resources/Audio/White/BodyArmor/PlateCarrier/close.ogg b/Resources/Audio/White/BodyArmor/PlateCarrier/close.ogg new file mode 100644 index 00000000000..55d720d7d54 Binary files /dev/null and b/Resources/Audio/White/BodyArmor/PlateCarrier/close.ogg differ diff --git a/Resources/Audio/White/BodyArmor/PlateCarrier/open.ogg b/Resources/Audio/White/BodyArmor/PlateCarrier/open.ogg new file mode 100644 index 00000000000..a8736d8708c Binary files /dev/null and b/Resources/Audio/White/BodyArmor/PlateCarrier/open.ogg differ diff --git a/Resources/Locale/ru-RU/_white/bodyarmor/armorplates/armorplate.ftl b/Resources/Locale/ru-RU/_white/bodyarmor/armorplates/armorplate.ftl new file mode 100644 index 00000000000..4e4a9d6bda1 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/bodyarmor/armorplates/armorplate.ftl @@ -0,0 +1,2 @@ +armorplate-damage = Визуальный осмотр: { $hasdamage } +armorplate-tier = Уровень защиты: { $tier } \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_white/bodyarmor/platecarrier/platecarrier.ftl b/Resources/Locale/ru-RU/_white/bodyarmor/platecarrier/platecarrier.ftl new file mode 100644 index 00000000000..96bd6c351f6 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/bodyarmor/platecarrier/platecarrier.ftl @@ -0,0 +1,6 @@ +platecarrierclosed = { CAPITALIZE($closed) } плитник +getplate = Достать бронеплиту +armorplate-place = Бронеплита: [color=red]{ $hasplate }[/color] +platecarrier-damage = Визуальный осмотр: { $hasdamage } +platecarrier-break = Состояние: [color=red]разорвана держащая лямка.[/color] +platecarrier-nobreak = Состояние: [color=green]держащие лямки и панели без повреждений.[/color] \ No newline at end of file diff --git a/Resources/Locale/ru-RU/locales-new/autotranslate-5.ftl b/Resources/Locale/ru-RU/locales-new/autotranslate-5.ftl index ef21ca4eb83..aac000b3d0e 100644 --- a/Resources/Locale/ru-RU/locales-new/autotranslate-5.ftl +++ b/Resources/Locale/ru-RU/locales-new/autotranslate-5.ftl @@ -28,8 +28,8 @@ ent-ClothingNeckScarfStripedSyndieRed = красный шарф синдикат .desc = Стильный полосатый красный шарф синдиката. Идеальный зимний аксессуар для тех, кто хорошо разбирается в моде, и тех, у кого есть настроение что-нибудь украсть. ent-ClothingNeckScarfStripedCentcom = полосатый шарф ЦентКом .desc = Стильный полосатый шарф цвета ЦК. Идеальный зимний аксессуар для тех, кто хорошо разбирается в моде, а также для тех, кому приходится заниматься бумажной работой на морозе. -ent-ClothingOuterArmorBasic = бронежилет - .desc = Стандартный бронежилет типа I, который обеспечивает достойную защиту от большинства типов повреждений. +ent-ClothingOuterArmorBasic = плитник + .desc = Стандартный плитник. ent-ClothingOuterArmorBasicSlim = бронежилет .desc = Тонкий бронежилет типа I, который обеспечивает достойную защиту от большинства видов повреждений. ent-ClothingOuterCoatLabGene = лабораторный халат генетика diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml index e440671139a..9d6838725b9 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml @@ -5,22 +5,17 @@ - type: entity parent: [ClothingOuterBase, AllowSuitStorageClothing] id: ClothingOuterArmorBasic - name: armor vest - description: A standard Type I armored vest that provides decent protection against most types of damage. + name: plate carrier + description: A standard plate carrier. components: - type: Sprite sprite: White/Clothing/OuterClothing/kevlar.rsi - type: Clothing sprite: White/Clothing/OuterClothing/kevlar.rsi - - type: Armor #Based on /tg/ but slightly compensated to fit the fact that armor stacks in SS14. - modifiers: - coefficients: - Blunt: 0.70 - Slash: 0.70 - Piercing: 0.70 #Can save you, but bullets will still hurt. Will take about 10 shots from a Viper before critting, as opposed to 7 while unarmored and 16~ with a bulletproof vest. - Heat: 0.80 - - type: ExplosionResistance - damageCoefficient: 0.90 + - type: PlateCarrier + - type: ContainerContainer + containers: + armor_plate: !type:Container # WD edit sounds start - type: EmitSoundOnPickup sound: diff --git a/Resources/Prototypes/_White/BodyArmor/ArmorPlates/armorplates.yml b/Resources/Prototypes/_White/BodyArmor/ArmorPlates/armorplates.yml new file mode 100644 index 00000000000..06b5f31f3f2 --- /dev/null +++ b/Resources/Prototypes/_White/BodyArmor/ArmorPlates/armorplates.yml @@ -0,0 +1,22 @@ +- type: entity + parent: BaseItem + id: BaseArmorPlate + abstract: true + components: + - type: Sprite + sprite: White/BodyArmors/ArmorPlates/armorplates.rsi + - type: Item + sprite: White/BodyArmors/ArmorPlates/armorplates.rsi + size: Normal + - type: Appearance + +- type: entity + parent: BaseArmorPlate + id: ArmorPlateTierOne + name: Armor Plate (1 Tier) + description: One tier of armor plates + components: + - type: ArmorPlate + tier: 0 + - type: Sprite + state: onetier diff --git a/Resources/Textures/White/BodyArmors/ArmorPlates/armorplates.rsi/meta.json b/Resources/Textures/White/BodyArmors/ArmorPlates/armorplates.rsi/meta.json new file mode 100644 index 00000000000..067286e315e --- /dev/null +++ b/Resources/Textures/White/BodyArmors/ArmorPlates/armorplates.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by CaypenNow", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "onetier" + } + ] +} diff --git a/Resources/Textures/White/BodyArmors/ArmorPlates/armorplates.rsi/onetier.png b/Resources/Textures/White/BodyArmors/ArmorPlates/armorplates.rsi/onetier.png new file mode 100644 index 00000000000..6b9b5d3703b Binary files /dev/null and b/Resources/Textures/White/BodyArmors/ArmorPlates/armorplates.rsi/onetier.png differ