diff --git a/Content.Client/Stories/ChameleonStamp/ChameleonStampSystem.cs b/Content.Client/Stories/ChameleonStamp/ChameleonStampSystem.cs new file mode 100644 index 0000000000..ea7b263346 --- /dev/null +++ b/Content.Client/Stories/ChameleonStamp/ChameleonStampSystem.cs @@ -0,0 +1,84 @@ +using System.Linq; +using Content.Shared.Stories.ChameleonStamp; +using Content.Shared.Inventory; +using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Log; +using Content.Shared.Paper; + +namespace Content.Client.Stories.ChameleonStamp +{ + public sealed class ChameleonStampSystem : SharedChameleonStampSystem + { + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IComponentFactory _factory = default!; + + private readonly List _data = new List(); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(HandleState); + PrepareAllVariants(); + SubscribeLocalEvent(OnProtoReloaded); + } + + private void OnProtoReloaded(PrototypesReloadedEventArgs args) + { + if (args.WasModified()) + { + PrepareAllVariants(); + } + } + + private void HandleState(EntityUid uid, ChameleonStampComponent component, ref AfterAutoHandleStateEvent args) + { + Logger.Info($"Обработка состояния для сущности с UID: {uid}"); + UpdateVisuals(uid, component); + } + + protected override void UpdateSprite(EntityUid uid, EntityPrototype proto) + { + base.UpdateSprite(uid, proto); + + if (TryComp(uid, out SpriteComponent? sprite) + && proto.TryGetComponent(out SpriteComponent? otherSprite, _factory)) + { + sprite.CopyFrom(otherSprite); + } + } + + public IEnumerable GetValidTargets() + { + var set = new HashSet(); + + foreach (var proto in _data) + { + Logger.Info($"Добавление прототипа {proto} в список"); + set.UnionWith(_data); + } + return set; + } + + private void PrepareAllVariants() + { + _data.Clear(); + var prototypes = _proto.EnumeratePrototypes(); + + foreach (var proto in prototypes) + { + // проверка, является ли это допустимой одеждой + if (!IsValidTarget(proto)) + { + continue; + } + if (!proto.TryGetComponent(out StampComponent? item, _factory)) + { + continue; + } + _data.Add(proto.ID); + Logger.Info($"Добавлен прототип {proto.ID}"); + } + } + } +} diff --git a/Content.Client/Stories/ChameleonStamp/UI/ChameleonStampBoundUserInterface.cs b/Content.Client/Stories/ChameleonStamp/UI/ChameleonStampBoundUserInterface.cs new file mode 100644 index 0000000000..395e3cace5 --- /dev/null +++ b/Content.Client/Stories/ChameleonStamp/UI/ChameleonStampBoundUserInterface.cs @@ -0,0 +1,48 @@ +using Content.Client.Clothing.Systems; +using Content.Shared.Clothing.Components; +using Content.Client.Clothing.UI; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Content.Shared.Stories.ChameleonStamp; +namespace Content.Client.Stories.ChameleonStamp.UI; + +[UsedImplicitly] +public sealed class ChameleonStampBoundUserInterface : BoundUserInterface +{ + private readonly ChameleonStampSystem _chameleon; + + [ViewVariables] + private ChameleonMenu? _menu; + + public ChameleonStampBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + _chameleon = EntMan.System(); + } + + protected override void Open() + { + base.Open(); + + _menu = this.CreateWindow(); + _menu.OnIdSelected += OnIdSelected; + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + if (state is not ChameleonStampBoundUserInterfaceState st) + { + Logger.Info($"Проверка стейтов не пройдена."); + return; + } + Logger.Info($"Проверка стейтов пройдена."); + var targets = _chameleon.GetValidTargets(); + _menu?.UpdateState(targets, st.SelectedId); + } + + private void OnIdSelected(string selectedId) + { + SendMessage(new ChameleonStampPrototypeSelectedMessage(selectedId)); + } +} diff --git a/Content.Server/Stories/ChameleonStamp/ChameleonStampSystem.cs b/Content.Server/Stories/ChameleonStamp/ChameleonStampSystem.cs new file mode 100644 index 0000000000..eda35b1d6c --- /dev/null +++ b/Content.Server/Stories/ChameleonStamp/ChameleonStampSystem.cs @@ -0,0 +1,93 @@ +using Content.Server.IdentityManagement; +using Content.Shared.IdentityManagement.Components; +using Content.Shared.Prototypes; +using Content.Shared.Verbs; +using Robust.Server.GameObjects; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using Content.Shared.Stories.ChameleonStamp; + +namespace Content.Server.Stories.ChameleonStamp; + +public sealed class ChameleonStampSystem : SharedChameleonStampSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly IdentitySystem _identity = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent>(OnVerb); + SubscribeLocalEvent(OnSelected); + } + + private void OnMapInit(EntityUid uid, ChameleonStampComponent component, MapInitEvent args) + { + SetSelectedPrototype(uid, component.Default, true, component); + } + + private void OnVerb(EntityUid uid, ChameleonStampComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || component.User != args.User) + return; + + args.Verbs.Add(new InteractionVerb() + { + Text = Loc.GetString("chameleon-component-verb-text"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), + Act = () => TryOpenUi(uid, args.User, component) + }); + } + + private void OnSelected(EntityUid uid, ChameleonStampComponent component, ChameleonStampPrototypeSelectedMessage args) + { + SetSelectedPrototype(uid, args.SelectedId, component: component); + } + + private void TryOpenUi(EntityUid uid, EntityUid user, ChameleonStampComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + if (!TryComp(user, out ActorComponent? actor)) + return; + _uiSystem.TryToggleUi(uid, ChameleonUiKey.Key, actor.PlayerSession); + } + + private void UpdateUi(EntityUid uid, ChameleonStampComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + var state = new ChameleonStampBoundUserInterfaceState(component.Default); + _uiSystem.SetUiState(uid, ChameleonUiKey.Key, state); + } + + /// + /// Change chameleon items name, description and sprite to mimic other entity prototype. + /// + public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false, + ChameleonStampComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; + + // check that wasn't already selected + // forceUpdate on component init ignores this check + if (component.Default == protoId && !forceUpdate) + return; + + // make sure that it is valid change + if (string.IsNullOrEmpty(protoId) || !_proto.TryIndex(protoId, out EntityPrototype? proto)) + return; + if (!IsValidTarget(proto)) + return; + component.Default = protoId; + UpdateVisuals(uid, component); + UpdateUi(uid, component); + Dirty(uid, component); + } +} diff --git a/Content.Shared/Stories/ChameleonStamp/ChameleonStampComponent.cs b/Content.Shared/Stories/ChameleonStamp/ChameleonStampComponent.cs new file mode 100644 index 0000000000..3d4b1c1068 --- /dev/null +++ b/Content.Shared/Stories/ChameleonStamp/ChameleonStampComponent.cs @@ -0,0 +1,56 @@ +using Content.Shared.Stories.ChameleonStamp; +using Content.Shared.Inventory; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Stories.ChameleonStamp; + +/// +/// Allow players to change clothing sprite to any other clothing prototype. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedChameleonStampSystem))] +public sealed partial class ChameleonStampComponent : Component +{ + /// + /// EntityPrototype id that chameleon item is trying to mimic. + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField(required: true), AutoNetworkedField] + public EntProtoId? Default; + + /// + /// Current user that wears chameleon clothing. + /// + [ViewVariables] + public EntityUid? User; +} + +[Serializable, NetSerializable] +public sealed class ChameleonStampBoundUserInterfaceState : BoundUserInterfaceState +{ + public readonly string? SelectedId; + + public ChameleonStampBoundUserInterfaceState(string? selectedId) + { + SelectedId = selectedId; + } +} + +[Serializable, NetSerializable] +public sealed class ChameleonStampPrototypeSelectedMessage : BoundUserInterfaceMessage +{ + public readonly string SelectedId; + + public ChameleonStampPrototypeSelectedMessage(string selectedId) + { + SelectedId = selectedId; + } +} + +[Serializable, NetSerializable] +public enum ChameleonUiKey : byte +{ + Key +} diff --git a/Content.Shared/Stories/ChameleonStamp/SharedChameleonStampSystem.cs b/Content.Shared/Stories/ChameleonStamp/SharedChameleonStampSystem.cs new file mode 100644 index 0000000000..3da8f726d3 --- /dev/null +++ b/Content.Shared/Stories/ChameleonStamp/SharedChameleonStampSystem.cs @@ -0,0 +1,93 @@ +using Content.Shared.Access.Components; +using Content.Shared.Stories.ChameleonStamp; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Item; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; +using Content.Shared.Paper; + +namespace Content.Shared.Stories.ChameleonStamp; + +public abstract class SharedChameleonStampSystem : EntitySystem +{ + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedItemSystem _itemSystem = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly SharedItemSystem _itemSys = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnGotUnequipped); + } + + private void OnGotEquipped(EntityUid uid, ChameleonStampComponent component, GotEquippedEvent args) + { + component.User = args.Equipee; + } + + private void OnGotUnequipped(EntityUid uid, ChameleonStampComponent component, GotUnequippedEvent args) + { + component.User = null; + } + + public void CopyVisuals(EntityUid uid, StampComponent otherStamp, StampComponent? stamp = null) + { + if (!Resolve(uid, ref stamp)) + return; + + stamp.StampedColor = otherStamp.StampedColor; + stamp.StampedName = otherStamp.StampedName; + stamp.StampState = otherStamp.StampState; + + _itemSys.VisualsChanged(uid); + Dirty(uid, stamp); + } + + // Updates chameleon visuals and meta information. + // This function is called on a server after user selected new outfit. + // And after that on a client after state was updated. + // This 100% makes sure that server and client have exactly same data. + protected void UpdateVisuals(EntityUid uid, ChameleonStampComponent component) + { + if (string.IsNullOrEmpty(component.Default) || + !_proto.TryIndex(component.Default, out EntityPrototype? proto)) + return; + + // world sprite icon + UpdateSprite(uid, proto); + + // sta sprite logic + if (TryComp(uid, out StampComponent? stamp) && + proto.TryGetComponent("Stamp", out StampComponent? otherStamp)) + { + CopyVisuals(uid, otherStamp, stamp); + } + } + + protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { } + + /// + /// Check if this entity prototype is valid target for chameleon item. + /// + public bool IsValidTarget(EntityPrototype proto) + { + // check if entity is valid + if (proto.Abstract || proto.HideSpawnMenu) + return false; + + // check if it is marked as valid chameleon target + if (!proto.TryGetComponent(out TagComponent? tag, _factory) || !_tag.HasTag(tag, "WhitelistChameleon")) + return false; + + // check if it's valid clothing + if (!proto.TryGetComponent("Stamp", out StampComponent? stamp)) + return false; + + return true; + } +} diff --git a/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml b/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml index 66ff215f30..b53cc370b6 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml @@ -21,6 +21,9 @@ size: Tiny - type: StealTarget stealGroup: Stamp + - type: Tag + tags: + - WhitelistChameleon - type: entity name: alternate rubber stamp diff --git a/Resources/Prototypes/Stories/Entities/Objects/stamps.yml b/Resources/Prototypes/Stories/Entities/Objects/stamps.yml index fdd9d1bcc9..8d272e43f8 100644 --- a/Resources/Prototypes/Stories/Entities/Objects/stamps.yml +++ b/Resources/Prototypes/Stories/Entities/Objects/stamps.yml @@ -12,3 +12,22 @@ - type: Sprite sprite: Objects/Misc/bureaucracy.rsi state: stamp-hosp + - type: Tag + tags: + - WhitelistChameleon + +- type: entity + name: печать мима + description: Печать из резины, для проставления печатей на важных документах. + parent: RubberStampBase + id: ChameleonRubberStamp + suffix: DO NOT MAP + components: + - type: ChameleonStamp + default: RubberStampBase + - type: UserInterface + interfaces: + enum.ChameleonUiKey.Key: + type: ChameleonStampBoundUserInterface + - type: Tag + tags: [] \ No newline at end of file