From caf18f4bc6a42964033390eaccc91e23b439509a Mon Sep 17 00:00:00 2001 From: mhamster <81412348+mhamsterr@users.noreply.github.com> Date: Fri, 30 May 2025 07:06:03 +0700 Subject: [PATCH] Chameleon clothes + EMP behaviour (#30924) * resolving conflicts?? * Controlled clothes changing + time stuff + EmpChangeIntensity * Single clothes change + EmpContinious + moved random pick logic into GetRandomValidPrototype * Changes from reviews Co-Authored-By: Nemanja <98561806+emogarbage404@users.noreply.github.com> * Update ChameleonClothingComponent.cs * repairing irreparable damage i failed, did i? * damaging repaired irreparable uh??? * 2025 FUN ALLOWED!!!! * Minor changes from reviews Co-Authored-By: beck-thompson <107373427+beck-thompson@users.noreply.github.com> * Fix merge conflicts * Fix that last bug * cleanup * Remove VV attr. * AutoPausedField on emp time change --------- Co-authored-by: Nemanja <98561806+emogarbage404@users.noreply.github.com> Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Co-authored-by: beck-thompson Co-authored-by: EmoGarbage404 --- .../Systems/ChameleonClothingSystem.cs | 55 -------------- .../UI/ChameleonBoundUserInterface.cs | 2 +- .../Clothing/UI/ChameleonMenu.xaml.cs | 8 +- .../Systems/ChameleonClothingSystem.cs | 53 +++++++++++++ .../Components/ChameleonClothingComponent.cs | 33 ++++++++- .../SharedChameleonClothingSystem.cs | 74 +++++++++++++++++++ 6 files changed, 162 insertions(+), 63 deletions(-) diff --git a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs index bde6a4b99a..74fcf48849 100644 --- a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs @@ -13,16 +13,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem { [Dependency] private readonly IPrototypeManager _proto = default!; - private static readonly SlotFlags[] IgnoredSlots = - { - SlotFlags.All, - SlotFlags.PREVENTEQUIP, - SlotFlags.NONE - }; - private static readonly SlotFlags[] Slots = Enum.GetValues().Except(IgnoredSlots).ToArray(); - - private readonly Dictionary> _data = new(); - public override void Initialize() { base.Initialize(); @@ -61,49 +51,4 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem borderColor.AccentVColor = otherBorderColor.AccentVColor; } } - - /// - /// Get a list of valid chameleon targets for these slots. - /// - public IEnumerable GetValidTargets(SlotFlags slot) - { - var set = new HashSet(); - foreach (var availableSlot in _data.Keys) - { - if (slot.HasFlag(availableSlot)) - { - set.UnionWith(_data[availableSlot]); - } - } - return set; - } - - private void PrepareAllVariants() - { - _data.Clear(); - var prototypes = _proto.EnumeratePrototypes(); - - foreach (var proto in prototypes) - { - // check if this is valid clothing - if (!IsValidTarget(proto)) - continue; - if (!proto.TryGetComponent(out ClothingComponent? item, Factory)) - continue; - - // sort item by their slot flags - // one item can be placed in several buckets - foreach (var slot in Slots) - { - if (!item.Slots.HasFlag(slot)) - continue; - - if (!_data.ContainsKey(slot)) - { - _data.Add(slot, new List()); - } - _data[slot].Add(proto.ID); - } - } - } } diff --git a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs index bd86ffbec0..876f300e50 100644 --- a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs +++ b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs @@ -42,7 +42,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface var targets = _chameleon.GetValidTargets(st.Slot); if (st.RequiredTag != null) { - var newTargets = new List(); + var newTargets = new List(); foreach (var target in targets) { if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto)) diff --git a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs index bd45be4510..c6dce10776 100644 --- a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs +++ b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs @@ -19,8 +19,8 @@ public sealed partial class ChameleonMenu : DefaultWindow private readonly SpriteSystem _sprite; public event Action? OnIdSelected; - private IEnumerable _possibleIds = Enumerable.Empty(); - private string? _selectedId; + private IEnumerable _possibleIds = []; + private EntProtoId? _selectedId; private string _searchFilter = ""; public ChameleonMenu() @@ -32,7 +32,7 @@ public sealed partial class ChameleonMenu : DefaultWindow Search.OnTextChanged += OnSearchEntered; } - public void UpdateState(IEnumerable possibleIds, string? selectedId) + public void UpdateState(IEnumerable possibleIds, string? selectedId) { _possibleIds = possibleIds; _selectedId = selectedId; @@ -57,7 +57,7 @@ public sealed partial class ChameleonMenu : DefaultWindow if (!_prototypeManager.TryIndex(id, out EntityPrototype? proto)) continue; - var lowId = id.ToLowerInvariant(); + var lowId = id.Id.ToLowerInvariant(); var lowName = proto.Name.ToLowerInvariant(); if (!lowId.Contains(searchFilterLow) && !lowName.Contains(_searchFilter)) continue; diff --git a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs index f3c7e316ca..75850e6d74 100644 --- a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs @@ -1,9 +1,14 @@ +using System.Linq; +using Content.Server.Emp; using Content.Server.IdentityManagement; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Emp; using Content.Shared.IdentityManagement.Components; +using Content.Shared.Inventory; using Content.Shared.Prototypes; using Robust.Shared.Prototypes; +using Robust.Shared.Random; namespace Content.Server.Clothing.Systems; @@ -11,12 +16,16 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IdentitySystem _identity = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnSelected); + + SubscribeLocalEvent(OnEmpPulse); } private void OnMapInit(EntityUid uid, ChameleonClothingComponent component, MapInitEvent args) @@ -29,6 +38,21 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem SetSelectedPrototype(uid, args.SelectedId, component: component); } + private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args) + { + if (!component.AffectedByEmp) + return; + + if (component.EmpContinuous) + component.NextEmpChange = _timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity); + + var pick = GetRandomValidPrototype(component.Slot, component.RequireTag); + SetSelectedPrototype(uid, pick, component: component); + + args.Affected = true; + args.Disabled = true; + } + private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = null) { if (!Resolve(uid, ref component)) @@ -65,6 +89,35 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem Dirty(uid, component); } + /// + /// Get a random prototype for a given slot. + /// + public string GetRandomValidPrototype(SlotFlags slot, string? tag = null) + { + return _random.Pick(GetValidTargets(slot, tag).ToList()); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + // Randomize EMP-affected clothing + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var chameleon)) + { + if (!chameleon.EmpContinuous) + continue; + + if (_timing.CurTime < chameleon.NextEmpChange) + continue; + + // randomly pick cloth element from available and apply it + var pick = GetRandomValidPrototype(chameleon.Slot, chameleon.RequireTag); + SetSelectedPrototype(uid, pick, component: chameleon); + + chameleon.NextEmpChange += TimeSpan.FromSeconds(1f / chameleon.EmpChangeIntensity); + } + } + private void UpdateIdentityBlocker(EntityUid uid, ChameleonClothingComponent component, EntityPrototype proto) { if (proto.HasComponent(Factory)) diff --git a/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs b/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs index 8fa2f19fa2..5caddda18b 100644 --- a/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs +++ b/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs @@ -3,27 +3,26 @@ using Content.Shared.Inventory; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Clothing.Components; /// /// Allow players to change clothing sprite to any other clothing prototype. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] [Access(typeof(SharedChameleonClothingSystem))] public sealed partial class ChameleonClothingComponent : Component { /// /// Filter possible chameleon options by their slot flag. /// - [ViewVariables(VVAccess.ReadOnly)] [DataField(required: true)] public SlotFlags Slot; /// /// EntityPrototype id that chameleon item is trying to mimic. /// - [ViewVariables(VVAccess.ReadOnly)] [DataField(required: true), AutoNetworkedField] public EntProtoId? Default; @@ -38,6 +37,34 @@ public sealed partial class ChameleonClothingComponent : Component /// [DataField] public string? RequireTag; + + /// + /// Will component owner be affected by EMP pulses? + /// + [DataField] + public bool AffectedByEmp = true; + + /// + /// Intensity of clothes change on EMP. + /// Can be interpreted as "How many times clothes will change every second?". + /// Useless without set to true. + /// + [DataField] + public int EmpChangeIntensity = 7; + + /// + /// Should the EMP-change happen continuously, or only once? + /// (False = once, True = continuously) + /// Useless without + /// + [DataField] + public bool EmpContinuous = true; + + /// + /// When should next EMP-caused appearance change happen? + /// + [AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextEmpChange = TimeSpan.Zero; } [Serializable, NetSerializable] diff --git a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs index 6c5fbdfb0b..233a71acee 100644 --- a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Access.Components; using Content.Shared.Clothing.Components; using Content.Shared.Contraband; @@ -7,6 +8,7 @@ using Content.Shared.Item; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Clothing.EntitySystems; @@ -20,6 +22,19 @@ public abstract class SharedChameleonClothingSystem : EntitySystem [Dependency] private readonly SharedItemSystem _itemSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] protected readonly IGameTiming _timing = default!; + + private static readonly SlotFlags[] IgnoredSlots = + { + SlotFlags.All, + SlotFlags.PREVENTEQUIP, + SlotFlags.NONE + }; + private static readonly SlotFlags[] Slots = Enum.GetValues().Except(IgnoredSlots).ToArray(); + + private readonly Dictionary> _data = new(); + + public readonly Dictionary> ValidVariants = new(); [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; private static readonly ProtoId WhitelistChameleonTag = "WhitelistChameleon"; @@ -30,6 +45,14 @@ public abstract class SharedChameleonClothingSystem : EntitySystem SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); SubscribeLocalEvent>(OnVerb); + + SubscribeLocalEvent(OnPrototypeReload); + PrepareAllVariants(); + } + + private void OnPrototypeReload(EntityUid uid, ChameleonClothingComponent component, PrototypesReloadedEventArgs args) + { + PrepareAllVariants(); } private void OnGotEquipped(EntityUid uid, ChameleonClothingComponent component, GotEquippedEvent args) @@ -139,4 +162,55 @@ public abstract class SharedChameleonClothingSystem : EntitySystem return true; } + + /// + /// Get a list of valid chameleon targets for these slots. + /// + public IEnumerable GetValidTargets(SlotFlags slot, string? tag = null) + { + var validTargets = new List(); + if (tag != null) + { + foreach (var proto in _data[slot]) + { + if (IsValidTarget(_proto.Index(proto), slot, tag)) + validTargets.Add(proto); + } + } + else + { + validTargets = _data[slot]; + } + + return validTargets; + } + + protected void PrepareAllVariants() + { + _data.Clear(); + var prototypes = _proto.EnumeratePrototypes(); + + foreach (var proto in prototypes) + { + // check if this is valid clothing + if (!IsValidTarget(proto)) + continue; + if (!proto.TryGetComponent(out ClothingComponent? item, Factory)) + continue; + + // sort item by their slot flags + // one item can be placed in several buckets + foreach (var slot in Slots) + { + if (!item.Slots.HasFlag(slot)) + continue; + + if (!_data.ContainsKey(slot)) + { + _data.Add(slot, new List()); + } + _data[slot].Add(proto.ID); + } + } + } }