diff --git a/Content.Client/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs index 847f8d2d5a2..44b4f757c9f 100644 --- a/Content.Client/Humanoid/MarkingPicker.xaml.cs +++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs @@ -1,11 +1,13 @@ // SPDX-License-Identifier: MIT using System.Linq; +using Content.Shared._Arcane.DiscordRoles; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; @@ -21,6 +23,10 @@ public sealed partial class MarkingPicker : Control [Dependency] private readonly MarkingManager _markingManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + // Arcane-Start + [Dependency] private readonly ISharedDiscordRoleManager _discordRoles = default!; + [Dependency] private readonly IPlayerManager _player = default!; + // Arcane-End private readonly SpriteSystem _sprite; @@ -231,6 +237,10 @@ public void Populate(string filter) var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", _sprite.Frame0(marking.Sprites[0])); item.Metadata = marking; + + // Arcane-Start + SetMarkingUnavailableIfNeeded(item, marking); + // Arcane-End } CMarkingPoints.Visible = _currentMarkings.PointsLeft(_selectedMarkingCategory) != -1; @@ -499,6 +509,15 @@ private void MarkingAdd() } var marking = (MarkingPrototype) _selectedUnusedMarking.Metadata!; + + // Arcane-Start + if (!marking.CanUse(_discordRoles, _player.LocalSession, out _)) + { + _selectedUnusedMarking = null; + return; + } + // Arcane-End + var markingObject = marking.AsMarking(); // We need add hair markings in cloned set manually because _currentMarkings doesn't have it @@ -571,9 +590,24 @@ private void MarkingRemove() { var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", _sprite.Frame0(marking.Sprites[0])); item.Metadata = marking; + + // Arcane-Start + SetMarkingUnavailableIfNeeded(item, marking); + // Arcane-End } _selectedMarking = null; CMarkingColors.Visible = false; OnMarkingRemoved?.Invoke(_currentMarkings); } -} \ No newline at end of file + + // Arcane-Start + private void SetMarkingUnavailableIfNeeded(ItemList.Item item, MarkingPrototype marking) + { + if (!marking.CanUse(_discordRoles, _player.LocalSession, out var reason)) + { + item.Disabled = true; + item.TooltipText = reason?.ToString(); + } + } + // Arcane-End +} diff --git a/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs b/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs index b1511c1af4f..024d5ff2e49 100644 --- a/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs +++ b/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs @@ -1,9 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later using System.Linq; +using Content.Shared._Arcane.DiscordRoles; using Content.Shared.Humanoid.Markings; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; +using Robust.Client.Player; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; @@ -14,6 +16,10 @@ public sealed partial class SingleMarkingPicker : BoxContainer { [Dependency] private readonly MarkingManager _markingManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + // Arcane-Start + [Dependency] private readonly ISharedDiscordRoleManager _discordRoles = default!; + [Dependency] private readonly IPlayerManager _player = default!; + // Arcane-End private readonly SpriteSystem _sprite; @@ -177,7 +183,9 @@ public void PopulateList(string filter) throw new ArgumentException("Tried to populate marking list without a set species!"); } + // Arcane-Start _markingPrototypeCache ??= _markingManager.MarkingsByCategoryAndSpecies(Category, _species); + // Arcane-End MarkingSelectorContainer.Visible = _markings != null && _markings.Count != 0; if (_markings == null || _markings.Count == 0) @@ -197,7 +205,16 @@ public void PopulateList(string filter) var item = MarkingList.AddItem(Loc.GetString($"marking-{id}"), _sprite.Frame0(marking.Sprites[0])); item.Metadata = marking.ID; - if (_markings[Slot].MarkingId == id) + // Arcane-Start + var isCurrent = _markings![Slot].MarkingId == id; + if (!isCurrent && !marking.CanUse(_discordRoles, _player.LocalSession, out var reason)) + { + item.Disabled = true; + item.TooltipText = reason?.ToString(); + } + // Arcane-End + + if (isCurrent) { _ignoreItemSelected = true; item.Selected = true; @@ -302,4 +319,4 @@ private string GetMarkingName(MarkingPrototype marking) { return Loc.GetString($"marking-{marking.ID}"); } -} \ No newline at end of file +} diff --git a/Content.Server/_Arcane/DiscordRoles/DiscordRoleIds.cs b/Content.Server/_Arcane/DiscordRoles/DiscordRoleIds.cs index df7208bc56e..2a8a0388162 100644 --- a/Content.Server/_Arcane/DiscordRoles/DiscordRoleIds.cs +++ b/Content.Server/_Arcane/DiscordRoles/DiscordRoleIds.cs @@ -13,6 +13,7 @@ public static class DiscordRoleIds [DiscordRole.SponsorTier1] = 1510991486399942707, [DiscordRole.SponsorTier2] = 1510991694785675397, [DiscordRole.AdminBenefit] = 1545943229143195658, + [DiscordRole.ArtLead] = 1509175991241670696, [DiscordRole.UnlockRoles] = 1533054913318223872, }; } diff --git a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs index b0f8eeebd4a..8bf9e7d70b7 100644 --- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs @@ -2,8 +2,10 @@ using System.Linq; using System.Numerics; +using Content.Shared._Arcane.DiscordRoles; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization; @@ -236,7 +238,8 @@ public static Color ClampColor(Color color) return new(color.RByte, color.GByte, color.BByte); } - public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearance appearance, string species, Sex sex) + public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearance appearance, string species, Sex sex, + ISharedDiscordRoleManager? discordRoles = null, ICommonSession? session = null) // Arcane { var hairStyleId = appearance.HairStyleId; var facialHairStyleId = appearance.FacialHairStyleId; @@ -264,6 +267,7 @@ public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearanc { markingSet = new MarkingSet(appearance.Markings, speciesProto.MarkingPoints, markingManager, proto); markingSet.EnsureValid(markingManager); + markingSet.EnsureEffects(discordRoles, session, markingManager); // Arcane var strategy = proto.Index(speciesProto.SkinColoration).Strategy; skinColor = strategy.EnsureVerified(skinColor); diff --git a/Content.Shared/Humanoid/Markings/Effects/MarkingEffect.cs b/Content.Shared/Humanoid/Markings/Effects/MarkingEffect.cs new file mode 100644 index 00000000000..8fe6b7fce9b --- /dev/null +++ b/Content.Shared/Humanoid/Markings/Effects/MarkingEffect.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +using System.Diagnostics.CodeAnalysis; +using Content.Shared._Arcane.DiscordRoles; +using Robust.Shared.Player; +using Robust.Shared.Utility; + +namespace Content.Shared.Humanoid.Markings.Effects; + +/// +/// Base class for effects that gate whether a marking may be used by a given session. +/// +[ImplicitDataDefinitionForInheritors] +public abstract partial class MarkingEffect +{ + /// + /// Tries to validate whether the marking is available to the session. + /// + public abstract bool Validate( + MarkingPrototype marking, + ICommonSession? session, + ISharedDiscordRoleManager? discordRoles, + [NotNullWhen(false)] out FormattedMessage? reason); +} diff --git a/Content.Shared/Humanoid/Markings/MarkingManager.cs b/Content.Shared/Humanoid/Markings/MarkingManager.cs index 95b2846fe3e..0a501e35d42 100644 --- a/Content.Shared/Humanoid/Markings/MarkingManager.cs +++ b/Content.Shared/Humanoid/Markings/MarkingManager.cs @@ -50,7 +50,7 @@ public FrozenDictionary MarkingsByCategory(MarkingCate } /// - /// Markings by category and species. + /// Filters and colors markings based on species and it's restrictions in the marking's prototype from this marking set. /// /// /// diff --git a/Content.Shared/Humanoid/Markings/MarkingPrototype.cs b/Content.Shared/Humanoid/Markings/MarkingPrototype.cs index 4f031d25478..26ef208d814 100644 --- a/Content.Shared/Humanoid/Markings/MarkingPrototype.cs +++ b/Content.Shared/Humanoid/Markings/MarkingPrototype.cs @@ -1,5 +1,8 @@ // SPDX-License-Identifier: MIT +using Content.Shared._Arcane.DiscordRoles; +using Content.Shared.Humanoid.Markings.Effects; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -44,6 +47,33 @@ public sealed partial class MarkingPrototype : IPrototype [DataField("sprites", required: true)] public List Sprites { get; private set; } = default!; + // Arcane-Start + /// + /// Effects that gate whether this marking may be used by a given session. Empty means no restriction. + /// + [DataField("effects")] + public List Effects { get; private set; } = new(); + + public bool CanUse(ISharedDiscordRoleManager? discordRoles, ICommonSession? session) + => CanUse(discordRoles, session, out _); + + public bool CanUse(ISharedDiscordRoleManager? discordRoles, ICommonSession? session, out FormattedMessage? reason) + { + reason = null; + + if (discordRoles == null || session == null) + return true; + + foreach (var effect in Effects) + { + if (!effect.Validate(this, session, discordRoles, out reason)) + return false; + } + + return true; + } + // Arcane-End + /// Impstation start [DataField] public string? Shader { get; private set; } = null; diff --git a/Content.Shared/Humanoid/Markings/MarkingsSet.cs b/Content.Shared/Humanoid/Markings/MarkingsSet.cs index 440a28c2be9..31d329e11f7 100644 --- a/Content.Shared/Humanoid/Markings/MarkingsSet.cs +++ b/Content.Shared/Humanoid/Markings/MarkingsSet.cs @@ -3,7 +3,9 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Shared._Arcane.DiscordRoles; using Content.Shared.Humanoid.Prototypes; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -234,6 +236,42 @@ public void EnsureSexes(Sex sex, MarkingManager? markingManager = null) } } + // Arcane-Start + /// + /// Removes markings whose effects reject the session. + /// + /// Discord role manager, used by sponsor effects. + /// The session to check, or null to skip filtering. + /// Marking manager. + public void EnsureEffects(ISharedDiscordRoleManager? discordRoles, ICommonSession? session, + MarkingManager? markingManager = null) + { + IoCManager.Resolve(ref markingManager); + + var toRemove = new List<(MarkingCategories category, string id)>(); + + foreach (var (category, list) in Markings) + { + foreach (var marking in list) + { + if (!markingManager.TryGetMarking(marking, out var prototype)) + { + continue; + } + + if (!prototype.CanUse(discordRoles, session)) + { + toRemove.Add((category, marking.MarkingId)); + } + } + } + + foreach (var remove in toRemove) + { + Remove(remove.category, remove.id); + } + } + /// /// Ensures that all markings in this set are valid. /// diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index 55a87ff56fd..27b370be5b6 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -61,6 +61,7 @@ using System.Linq; using System.Text.RegularExpressions; using Content.Shared._Arcane.CCVars; +using Content.Shared._Arcane.DiscordRoles; using Content.Shared._Arcane.TTS; using Content.Shared.CCVar; using Content.Shared.GameTicking; @@ -1107,7 +1108,8 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection width = Math.Clamp(Width, speciesPrototype.MinWidth, speciesPrototype.MaxWidth); // end Goobstation: port EE height/width sliders - var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex); + var discordRoles = collection.Resolve(); // Arcane + var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex, discordRoles, session); // Arcane-Edit var prefsUnavailableMode = PreferenceUnavailable switch { diff --git a/Content.Shared/_Arcane/DiscordRoles/DiscordRole.cs b/Content.Shared/_Arcane/DiscordRoles/DiscordRole.cs index cb203ee1cac..704d900a974 100644 --- a/Content.Shared/_Arcane/DiscordRoles/DiscordRole.cs +++ b/Content.Shared/_Arcane/DiscordRoles/DiscordRole.cs @@ -8,6 +8,7 @@ public enum DiscordRole : byte SponsorTier1, SponsorTier2, AdminBenefit, + ArtLead, UnlockRoles, } diff --git a/Content.Shared/_Arcane/Humanoid/Markings/Effects/SponsorRequirementMarkingEffect.cs b/Content.Shared/_Arcane/Humanoid/Markings/Effects/SponsorRequirementMarkingEffect.cs new file mode 100644 index 00000000000..17233222bb4 --- /dev/null +++ b/Content.Shared/_Arcane/Humanoid/Markings/Effects/SponsorRequirementMarkingEffect.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared._Arcane.DiscordRoles; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Markings.Effects; +using Robust.Shared.Player; +using Robust.Shared.Utility; + +namespace Content.Shared._Arcane.Humanoid.Markings.Effects; + +/// +/// Verifies possession of a specific Discord role that unlocks the sponsor marking +/// +public sealed partial class SponsorRequirementMarkingEffect : MarkingEffect +{ + [DataField(required: true)] + public HashSet Roles; + + public override bool Validate( + MarkingPrototype marking, + ICommonSession? session, + ISharedDiscordRoleManager? discordRoles, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = FormattedMessage.FromUnformatted(Loc.GetString("marking-sponsor-requirement")); + + if (session == null || discordRoles == null) + return true; + + return Roles.Any(role => discordRoles.HasRole(session, role)); + } +} diff --git a/Content.Shared/_Arcane/Sponsor/SponsorRoleBenefits.cs b/Content.Shared/_Arcane/Sponsor/SponsorRoleBenefits.cs index 778e0d3f816..17070a45b63 100644 --- a/Content.Shared/_Arcane/Sponsor/SponsorRoleBenefits.cs +++ b/Content.Shared/_Arcane/Sponsor/SponsorRoleBenefits.cs @@ -17,10 +17,12 @@ public static class SponsorRoleBenefits [DiscordRole.SponsorTier1] = new("Tier1", "#7d25a8", 2, -1), [DiscordRole.SponsorTier2] = new("Tier2", "#d8aa2d", 3, -2), [DiscordRole.AdminBenefit] = new("AdminBenefit", "#78ecf5", 5, -3), + [DiscordRole.ArtLead] = new("ArtLead", "#ffdaf9", 6, -4) }; private static readonly DiscordRole[] Priority = [ + DiscordRole.ArtLead, DiscordRole.AdminBenefit, DiscordRole.SponsorTier2, DiscordRole.SponsorTier1, diff --git a/Resources/Locale/ru-RU/_Arcane/markings/humanoid_xeno.ftl b/Resources/Locale/ru-RU/_Arcane/markings/humanoid_xeno.ftl index 844113d2609..8282dc2cbf7 100644 --- a/Resources/Locale/ru-RU/_Arcane/markings/humanoid_xeno.ftl +++ b/Resources/Locale/ru-RU/_Arcane/markings/humanoid_xeno.ftl @@ -21,6 +21,7 @@ marking-HumanoidXenoTailOpress = Хвост угнетателя marking-HumanoidXenoTailValkyr = Хвост валькирии marking-HumanoidXenoTailDefender = Хвост защитника marking-HumanoidXenoTailWarrior = Хвост воина +marking-HumanoidXenoTailRen = Хвост Рэн marking-HumanoidXenoHeadDrone = Голова дрона marking-HumanoidXenoHeadSpitter = Голова плевателя marking-HumanoidXenoHeadPretor = Голова претора @@ -44,6 +45,7 @@ marking-HumanoidXenoHeadBurrower = Голова бурильщика marking-HumanoidXenoHeadDefender = Голова защитника marking-HumanoidXenoHeadValkyr = Голова валькирии marking-HumanoidXenoHeadWarrior = Голова воина +marking-HumanoidXenoHeadRen = Голова Рэн marking-HumanoidXenoChestPredalien = Грудь предалиена marking-HumanoidXenoChestSpitter = Грудь плевателя marking-HumanoidXenoChestMuscles = "Мышцы" @@ -63,6 +65,7 @@ marking-HumanoidXenoTracheasCougar = Трахеи пумы marking-HumanoidXenoTracheasCrocodile = Трахеи крокодила marking-HumanoidXenoTracheasBers = Трахеи берсерка marking-HumanoidXenoTracheasWarrior = Трахеи воина +marking-HumanoidXenoTracheasRen = Трахеи Рэн marking-HumanoidXenoLegsPredalien = Ноги предалиена marking-HumanoidXenoLegsMuscles = "Мышцы" marking-HumanoidXenoLegsCougar = Ноги пумы diff --git a/Resources/Locale/ru-RU/_Arcane/sponsor/sponsor.ftl b/Resources/Locale/ru-RU/_Arcane/sponsor/sponsor.ftl index 39543c56ad7..80d94103692 100644 --- a/Resources/Locale/ru-RU/_Arcane/sponsor/sponsor.ftl +++ b/Resources/Locale/ru-RU/_Arcane/sponsor/sponsor.ftl @@ -5,6 +5,8 @@ not-patron-name = Отсутствует Tier1-patron-name = [color=#8b00d1]Тир 1[/color] Tier2-patron-name = [color=#ecad00]Тир 2[/color] AdminBenefit-patron-name = [color=#ecad00]Хорошая Вахта[/color] +ArtLead-patron-name = [color=#ffdaf9]Арт-Лид[/color] loadout-sponsor-requirement = Этот предмет доступен только для спонсоров! +marking-sponsor-requirement = Этот маркинг доступен только для спонсоров! loadout-group-patron = Спонсор diff --git a/Resources/Prototypes/_Arcane/Entities/Mobs/Customization/Markings/humanoid_xeno.yml b/Resources/Prototypes/_Arcane/Entities/Mobs/Customization/Markings/humanoid_xeno.yml index d143f71019f..60a52e24032 100644 --- a/Resources/Prototypes/_Arcane/Entities/Mobs/Customization/Markings/humanoid_xeno.yml +++ b/Resources/Prototypes/_Arcane/Entities/Mobs/Customization/Markings/humanoid_xeno.yml @@ -263,6 +263,20 @@ - sprite: _Arcane/Mobs/Customization/HumanoidXeno/tail.rsi state: warrior_tail_thin_grad +- type: marking + id: HumanoidXenoTailRen + bodyPart: Tail + markingCategory: Tail + speciesRestriction: [HumanoidXeno] + effects: + - !type:SponsorRequirementMarkingEffect + roles: [ArtLead] + sprites: + - sprite: _Arcane/Mobs/Customization/HumanoidXeno/tail.rsi + state: ren_tail + - sprite: _Arcane/Mobs/Customization/HumanoidXeno/tail.rsi + state: ren_tail_grad + # Head @@ -566,6 +580,18 @@ - sprite: _Arcane/Mobs/Customization/HumanoidXeno/head.rsi state: warrior_head_shine +- type: marking + id: HumanoidXenoHeadRen + bodyPart: Head + markingCategory: Head + speciesRestriction: [HumanoidXeno] + effects: + - !type:SponsorRequirementMarkingEffect + roles: [ArtLead] + sprites: + - sprite: _Arcane/Mobs/Customization/HumanoidXeno/head.rsi + state: ren_head + # Chest - type: marking @@ -743,6 +769,18 @@ - sprite: _Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi state: warrior_tubes +- type: marking + id: HumanoidXenoTracheasRen + bodyPart: Chest + markingCategory: Tracheas + speciesRestriction: [HumanoidXeno] + effects: + - !type:SponsorRequirementMarkingEffect + roles: [ArtLead] + sprites: + - sprite: _Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi + state: ren_tubes + # Legs - type: marking diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/meta.json b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/meta.json index 6662091b1f5..3122047c50e 100644 --- a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/meta.json +++ b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/meta.json @@ -286,6 +286,10 @@ { "name": "warrior_teeth", "directions": 4 + }, + { + "name": "ren_head", + "directions": 4 } ] } diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/ren_head.png b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/ren_head.png new file mode 100644 index 00000000000..c7dd57d04e7 Binary files /dev/null and b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/head.rsi/ren_head.png differ diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/meta.json b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/meta.json index 310ef8936f5..fea88158a15 100644 --- a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/meta.json +++ b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/meta.json @@ -98,6 +98,14 @@ { "name": "warrior_tail_thin", "directions": 4 + }, + { + "name": "ren_tail", + "directions": 4 + }, + { + "name": "ren_tail_grad", + "directions": 4 } ] } diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/ren_tail.png b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/ren_tail.png new file mode 100644 index 00000000000..2a594fbf0b1 Binary files /dev/null and b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/ren_tail.png differ diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/ren_tail_grad.png b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/ren_tail_grad.png new file mode 100644 index 00000000000..2fa6c79eb32 Binary files /dev/null and b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tail.rsi/ren_tail_grad.png differ diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/meta.json b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/meta.json index 96be503100e..72642618c6f 100644 --- a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/meta.json +++ b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/meta.json @@ -58,6 +58,10 @@ { "name": "warrior_tubes", "directions": 4 + }, + { + "name": "ren_tubes", + "directions": 4 } ] } diff --git a/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/ren_tubes.png b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/ren_tubes.png new file mode 100644 index 00000000000..d1996719480 Binary files /dev/null and b/Resources/Textures/_Arcane/Mobs/Customization/HumanoidXeno/tracheas.rsi/ren_tubes.png differ