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 <beck314159@hotmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
This commit is contained in:
mhamster
2025-05-30 07:06:03 +07:00
committed by GitHub
parent 394aa7e7b8
commit caf18f4bc6
6 changed files with 162 additions and 63 deletions

View File

@@ -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<SlotFlags>().Except(IgnoredSlots).ToArray();
private readonly Dictionary<SlotFlags, List<string>> _data = new();
public override void Initialize()
{
base.Initialize();
@@ -61,49 +51,4 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
borderColor.AccentVColor = otherBorderColor.AccentVColor;
}
}
/// <summary>
/// Get a list of valid chameleon targets for these slots.
/// </summary>
public IEnumerable<string> GetValidTargets(SlotFlags slot)
{
var set = new HashSet<string>();
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<EntityPrototype>();
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<string>());
}
_data[slot].Add(proto.ID);
}
}
}
}

View File

@@ -42,7 +42,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
var targets = _chameleon.GetValidTargets(st.Slot);
if (st.RequiredTag != null)
{
var newTargets = new List<string>();
var newTargets = new List<EntProtoId>();
foreach (var target in targets)
{
if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))

View File

@@ -19,8 +19,8 @@ public sealed partial class ChameleonMenu : DefaultWindow
private readonly SpriteSystem _sprite;
public event Action<string>? OnIdSelected;
private IEnumerable<string> _possibleIds = Enumerable.Empty<string>();
private string? _selectedId;
private IEnumerable<EntProtoId> _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<string> possibleIds, string? selectedId)
public void UpdateState(IEnumerable<EntProtoId> 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;

View File

@@ -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<ChameleonClothingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChameleonClothingComponent, ChameleonPrototypeSelectedMessage>(OnSelected);
SubscribeLocalEvent<ChameleonClothingComponent, EmpPulseEvent>(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);
}
/// <summary>
/// Get a random prototype for a given slot.
/// </summary>
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<EmpDisabledComponent, ChameleonClothingComponent>();
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<IdentityBlockerComponent>(Factory))

View File

@@ -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;
/// <summary>
/// Allow players to change clothing sprite to any other clothing prototype.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause]
[Access(typeof(SharedChameleonClothingSystem))]
public sealed partial class ChameleonClothingComponent : Component
{
/// <summary>
/// Filter possible chameleon options by their slot flag.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField(required: true)]
public SlotFlags Slot;
/// <summary>
/// EntityPrototype id that chameleon item is trying to mimic.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField(required: true), AutoNetworkedField]
public EntProtoId? Default;
@@ -38,6 +37,34 @@ public sealed partial class ChameleonClothingComponent : Component
/// </summary>
[DataField]
public string? RequireTag;
/// <summary>
/// Will component owner be affected by EMP pulses?
/// </summary>
[DataField]
public bool AffectedByEmp = true;
/// <summary>
/// Intensity of clothes change on EMP.
/// Can be interpreted as "How many times clothes will change every second?".
/// Useless without <see cref="AffectedByEmp"/> set to true.
/// </summary>
[DataField]
public int EmpChangeIntensity = 7;
/// <summary>
/// Should the EMP-change happen continuously, or only once?
/// (False = once, True = continuously)
/// Useless without <see cref="AffectedByEmp"/>
/// </summary>
[DataField]
public bool EmpContinuous = true;
/// <summary>
/// When should next EMP-caused appearance change happen?
/// </summary>
[AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpChange = TimeSpan.Zero;
}
[Serializable, NetSerializable]

View File

@@ -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<SlotFlags>().Except(IgnoredSlots).ToArray();
private readonly Dictionary<SlotFlags, List<EntProtoId>> _data = new();
public readonly Dictionary<SlotFlags, List<string>> ValidVariants = new();
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
private static readonly ProtoId<TagPrototype> WhitelistChameleonTag = "WhitelistChameleon";
@@ -30,6 +45,14 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
SubscribeLocalEvent<ChameleonClothingComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<ChameleonClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<ChameleonClothingComponent, GetVerbsEvent<InteractionVerb>>(OnVerb);
SubscribeLocalEvent<ChameleonClothingComponent, PrototypesReloadedEventArgs>(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;
}
/// <summary>
/// Get a list of valid chameleon targets for these slots.
/// </summary>
public IEnumerable<EntProtoId> GetValidTargets(SlotFlags slot, string? tag = null)
{
var validTargets = new List<EntProtoId>();
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<EntityPrototype>();
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<EntProtoId>());
}
_data[slot].Add(proto.ID);
}
}
}
}