diff --git a/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs b/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs new file mode 100644 index 0000000000..60d8ea413a --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs @@ -0,0 +1,43 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.SmartFridge; +using Robust.Client.UserInterface; +using Robust.Shared.Input; + +namespace Content.Client.SmartFridge; + +public sealed class SmartFridgeBoundUserInterface : BoundUserInterface +{ + private SmartFridgeMenu? _menu; + + public SmartFridgeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _menu = this.CreateWindow(); + _menu.OnItemSelected += OnItemSelected; + Refresh(); + } + + public void Refresh() + { + if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge)) + return; + + menu.SetFlavorText(Loc.GetString(fridge.FlavorText)); + menu.Populate((Owner, fridge)); + } + + private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + if (data is not SmartFridgeListData entry) + return; + SendPredictedMessage(new SmartFridgeDispenseItemMessage(entry.Entry)); + } +} diff --git a/Content.Client/SmartFridge/SmartFridgeItem.xaml b/Content.Client/SmartFridge/SmartFridgeItem.xaml new file mode 100644 index 0000000000..3960d7ce42 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeItem.xaml @@ -0,0 +1,16 @@ + + + diff --git a/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs b/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs new file mode 100644 index 0000000000..c69d2e7de2 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs @@ -0,0 +1,18 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client.SmartFridge; + +[GenerateTypedNameReferences] +public sealed partial class SmartFridgeItem : BoxContainer +{ + public SmartFridgeItem(EntityUid uid, string text) + { + RobustXamlLoader.Load(this); + + EntityView.SetEntity(uid); + NameLabel.Text = text; + } +} diff --git a/Content.Client/SmartFridge/SmartFridgeMenu.xaml b/Content.Client/SmartFridge/SmartFridgeMenu.xaml new file mode 100644 index 0000000000..73cb0f3925 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeMenu.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs b/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs new file mode 100644 index 0000000000..c896e7fada --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Numerics; +using Content.Client.UserInterface.Controls; +using Content.Shared.SmartFridge; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; + +namespace Content.Client.SmartFridge; + +public record SmartFridgeListData(EntityUid Representative, SmartFridgeEntry Entry, int Amount) : ListData; + +[GenerateTypedNameReferences] +public sealed partial class SmartFridgeMenu : FancyWindow +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + + public event Action? OnItemSelected; + + private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) }; + + public SmartFridgeMenu() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + VendingContents.SearchBar = SearchBar; + VendingContents.DataFilterCondition += DataFilterCondition; + VendingContents.GenerateItem += GenerateButton; + VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data); + } + + private bool DataFilterCondition(string filter, ListData data) + { + if (data is not SmartFridgeListData entry) + return false; + + if (string.IsNullOrEmpty(filter)) + return true; + + return entry.Entry.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase); + } + + private void GenerateButton(ListData data, ListContainerButton button) + { + if (data is not SmartFridgeListData entry) + return; + + var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount)); + button.AddChild(new SmartFridgeItem(entry.Representative, label)); + + button.ToolTip = label; + button.StyleBoxOverride = _styleBox; + } + + public void Populate(Entity ent) + { + var listData = new List(); + + foreach (var item in ent.Comp.Entries) + { + if (!ent.Comp.ContainedEntries.TryGetValue(item, out var items) || items.Count == 0) + { + listData.Add(new SmartFridgeListData(EntityUid.Invalid, item, 0)); + } + else + { + var representative = _entityManager.GetEntity(items.First()); + listData.Add(new SmartFridgeListData(representative, item, items.Count)); + } + } + + VendingContents.PopulateList(listData); + } + + public void SetFlavorText(string flavor) + { + LeftFlavorLabel.Text = flavor; + } +} diff --git a/Content.Client/SmartFridge/SmartFridgeUISystem.cs b/Content.Client/SmartFridge/SmartFridgeUISystem.cs new file mode 100644 index 0000000000..4068c27e05 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeUISystem.cs @@ -0,0 +1,24 @@ +using Content.Shared.SmartFridge; +using Robust.Shared.Analyzers; + +namespace Content.Client.SmartFridge; + +public sealed class SmartFridgeUISystem : EntitySystem +{ + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSmartFridgeAfterState); + } + + private void OnSmartFridgeAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (!_uiSystem.TryGetOpenUi(ent.Owner, SmartFridgeUiKey.Key, out var bui)) + return; + + bui.Refresh(); + } +} diff --git a/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs b/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs new file mode 100644 index 0000000000..aa2ebef606 --- /dev/null +++ b/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs @@ -0,0 +1,111 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.SmartFridge; + +namespace Content.IntegrationTests.Tests.SmartFridge; + +public sealed class SmartFridgeInteractionTest : InteractionTest +{ + private const string SmartFridgeProtoId = "SmartFridge"; + private const string SampleItemProtoId = "FoodAmbrosiaVulgaris"; + private const string SampleDumpableAndInsertableId = "PillCanisterSomething"; + private const int SampleDumpableCount = 5; + private const string SampleDumpableId = "ChemBagSomething"; + + [TestPrototypes] + private const string TestPrototypes = $@" +- type: entity + parent: PillCanister + id: {SampleDumpableAndInsertableId} + components: + - type: StorageFill + contents: + - id: PillCopper + amount: 5 + +- type: entity + parent: ChemBag + id: {SampleDumpableId} + components: + - type: StorageFill + contents: + - id: PillCopper + amount: 5 +"; + + [Test] + public async Task InsertAndDispenseItemTest() + { + await PlaceInHands(SampleItemProtoId); + + await SpawnTarget(SmartFridgeProtoId); + var fridge = SEntMan.GetEntity(Target.Value); + var component = SEntMan.GetComponent(fridge); + + await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords)); + await RunTicks(1); + + // smartfridge spawns with nothing + Assert.That(component.Entries, Is.Empty); + await InteractUsing(SampleItemProtoId); + + // smartfridge now has items + Assert.That(component.Entries, Is.Not.Empty); + Assert.That(component.ContainedEntries[component.Entries[0]], Is.Not.Empty); + + // open the UI + await Activate(); + Assert.That(IsUiOpen(SmartFridgeUiKey.Key)); + + // dispense an item + await SendBui(SmartFridgeUiKey.Key, new SmartFridgeDispenseItemMessage(component.Entries[0])); + + // assert that the listing is still there + Assert.That(component.Entries, Is.Not.Empty); + // but empty + Assert.That(component.ContainedEntries[component.Entries[0]], Is.Empty); + + // and that the thing we dispensed is actually around + await AssertEntityLookup( + ("APCBasic", 1), + (SampleItemProtoId, 1) + ); + } + + [Test] + public async Task InsertDumpableInsertableItemTest() + { + await PlaceInHands(SampleItemProtoId); + + await SpawnTarget(SmartFridgeProtoId); + var fridge = SEntMan.GetEntity(Target.Value); + var component = SEntMan.GetComponent(fridge); + + await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords)); + await RunTicks(1); + + await InteractUsing(SampleDumpableAndInsertableId); + + // smartfridge now has one item only + Assert.That(component.Entries, Is.Not.Empty); + Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(1)); + } + + [Test] + public async Task InsertDumpableItemTest() + { + await PlaceInHands(SampleItemProtoId); + + await SpawnTarget(SmartFridgeProtoId); + var fridge = SEntMan.GetEntity(Target.Value); + var component = SEntMan.GetComponent(fridge); + + await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords)); + await RunTicks(1); + + await InteractUsing(SampleDumpableId); + + // smartfridge now has N items + Assert.That(component.Entries, Is.Not.Empty); + Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(SampleDumpableCount)); + } +} diff --git a/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs b/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs index e92552ef6d..bdf8b5ba07 100644 --- a/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs +++ b/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs @@ -19,6 +19,7 @@ using Content.Shared.Movement.Events; using Content.Shared.Popups; using Content.Shared.Power; using Content.Shared.Power.EntitySystems; +using Content.Shared.Storage.Components; using Content.Shared.Throwing; using Content.Shared.Verbs; using Content.Shared.Whitelist; @@ -90,6 +91,9 @@ public abstract class SharedDisposalUnitSystem : EntitySystem SubscribeLocalEvent(OnAfterInteractUsing); SubscribeLocalEvent(OnDragDropOn); SubscribeLocalEvent(OnMovement); + + SubscribeLocalEvent(OnGetDumpableVerb); + SubscribeLocalEvent(OnDump); } private void AddDisposalAltVerbs(Entity ent, ref GetVerbsEvent args) @@ -785,4 +789,23 @@ public abstract class SharedDisposalUnitSystem : EntitySystem // See also, medical scanner. Also maybe add verbs for entering lockers/body bags? args.Verbs.Add(verb); } + + private void OnGetDumpableVerb(Entity ent, ref GetDumpableVerbEvent args) + { + args.Verb = Loc.GetString("dump-disposal-verb-name", ("unit", ent)); + } + + private void OnDump(Entity ent, ref DumpEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + args.PlaySound = true; + + foreach (var entity in args.DumpQueue) + { + DoInsertDisposalUnit(ent, entity, args.User); + } + } } diff --git a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs index c332064ea3..251043a60d 100644 --- a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs +++ b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs @@ -3,11 +3,13 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Storage; using Content.Shared.Storage.Components; +using Robust.Shared.Random; namespace Content.Shared.Placeable; public sealed class PlaceableSurfaceSystem : EntitySystem { + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; @@ -19,6 +21,8 @@ public sealed class PlaceableSurfaceSystem : EntitySystem SubscribeLocalEvent(OnStorageInteractUsingAttempt); SubscribeLocalEvent(OnStorageAfterOpen); SubscribeLocalEvent(OnStorageAfterClose); + SubscribeLocalEvent(OnGetDumpableVerb); + SubscribeLocalEvent(OnDump); } public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceComponent? surface = null) @@ -87,4 +91,25 @@ public sealed class PlaceableSurfaceSystem : EntitySystem { SetPlaceable(ent.Owner, false, ent.Comp); } + + private void OnGetDumpableVerb(Entity ent, ref GetDumpableVerbEvent args) + { + args.Verb = Loc.GetString("dump-placeable-verb-name", ("surface", ent)); + } + + private void OnDump(Entity ent, ref DumpEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + args.PlaySound = true; + + var (targetPos, targetRot) = _transformSystem.GetWorldPositionRotation(ent); + + foreach (var entity in args.DumpQueue) + { + _transformSystem.SetWorldPositionRotation(entity, targetPos + _random.NextVector2Box() / 4, targetRot); + } + } } diff --git a/Content.Shared/SmartFridge/SmartFridgeComponent.cs b/Content.Shared/SmartFridge/SmartFridgeComponent.cs new file mode 100644 index 0000000000..db132c586d --- /dev/null +++ b/Content.Shared/SmartFridge/SmartFridgeComponent.cs @@ -0,0 +1,99 @@ +using Content.Shared.Whitelist; +using Robust.Shared.Analyzers; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.SmartFridge; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SmartFridgeSystem))] +public sealed partial class SmartFridgeComponent : Component +{ + /// + /// The container ID that this SmartFridge stores its inventory in + /// + [DataField] + public string Container = "smart_fridge_inventory"; + + /// + /// Whitelist for what entities can be inserted + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Blacklist for what entities can be inserted + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// The sound played on inserting an item into the fridge + /// + [DataField] + public SoundSpecifier? InsertSound = new SoundCollectionSpecifier("MachineInsert"); + + /// + /// A list of entries to display in the UI + /// + [DataField, AutoNetworkedField] + public List Entries = new(); + + /// + /// A mapping of smart fridge entries to the actual contained contents + /// + [DataField, AutoNetworkedField] + [Access(typeof(SmartFridgeSystem), Other = AccessPermissions.ReadExecute)] + public Dictionary> ContainedEntries = new(); + + /// + /// The flavour text displayed at the bottom of the SmartFridge's UI + /// + [DataField] + public LocId FlavorText = "smart-fridge-request-generic"; + + /// + /// Sound that plays when ejecting an item + /// + [DataField] + public SoundSpecifier SoundVend = new SoundCollectionSpecifier("VendingDispense") + { + Params = new AudioParams + { + Volume = -4f, + Variation = 0.15f + } + }; + + /// + /// Sound that plays when an item can't be ejected + /// + [DataField] + public SoundSpecifier SoundDeny = new SoundCollectionSpecifier("VendingDeny"); +} + +[Serializable, NetSerializable, DataRecord] +public record struct SmartFridgeEntry +{ + public string Name; + + public SmartFridgeEntry(string name) + { + Name = name; + } +} + +[Serializable, NetSerializable] +public enum SmartFridgeUiKey : byte +{ + Key, +} + +[Serializable, NetSerializable] +public sealed class SmartFridgeDispenseItemMessage(SmartFridgeEntry entry) : BoundUserInterfaceMessage +{ + public SmartFridgeEntry Entry = entry; +} diff --git a/Content.Shared/SmartFridge/SmartFridgeSystem.cs b/Content.Shared/SmartFridge/SmartFridgeSystem.cs new file mode 100644 index 0000000000..1341f6cd03 --- /dev/null +++ b/Content.Shared/SmartFridge/SmartFridgeSystem.cs @@ -0,0 +1,157 @@ +using Content.Shared.Access.Components; +using Content.Shared.Access.Systems; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Storage.Components; +using Content.Shared.Whitelist; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Timing; + +namespace Content.Shared.SmartFridge; + +public sealed class SmartFridgeSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessReader = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnItemRemoved); + + SubscribeLocalEvent(OnGetDumpableVerb); + SubscribeLocalEvent(OnDump); + + Subs.BuiEvents(SmartFridgeUiKey.Key, + sub => + { + sub.Event(OnDispenseItem); + }); + } + + private bool DoInsert(Entity ent, EntityUid user, IEnumerable usedItems, bool playSound) + { + if (!_container.TryGetContainer(ent, ent.Comp.Container, out var container)) + return false; + + if (!Allowed(ent, user)) + return true; + + bool anyInserted = false; + foreach (var used in usedItems) + { + if (!_whitelist.CheckBoth(used, ent.Comp.Blacklist, ent.Comp.Whitelist)) + continue; + anyInserted = true; + + _container.Insert(used, container); + var key = new SmartFridgeEntry(Identity.Name(used, EntityManager)); + if (!ent.Comp.Entries.Contains(key)) + ent.Comp.Entries.Add(key); + + ent.Comp.ContainedEntries.TryAdd(key, new()); + var entries = ent.Comp.ContainedEntries[key]; + if (!entries.Contains(GetNetEntity(used))) + entries.Add(GetNetEntity(used)); + + Dirty(ent); + } + + if (anyInserted && playSound) + { + _audio.PlayPredicted(ent.Comp.InsertSound, ent, user); + } + + return anyInserted; + } + + private void OnInteractUsing(Entity ent, ref InteractUsingEvent args) + { + if (!_hands.CanDrop(args.User, args.Used)) + return; + + args.Handled = DoInsert(ent, args.User, [args.Used], true); + } + + private void OnItemRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + var key = new SmartFridgeEntry(Identity.Name(args.Entity, EntityManager)); + + if (ent.Comp.ContainedEntries.TryGetValue(key, out var contained)) + { + contained.Remove(GetNetEntity(args.Entity)); + } + + Dirty(ent); + } + + private bool Allowed(Entity machine, EntityUid user) + { + if (_accessReader.IsAllowed(user, machine)) + return true; + + _popup.PopupPredicted(Loc.GetString("smart-fridge-component-try-eject-access-denied"), machine, user); + _audio.PlayPredicted(machine.Comp.SoundDeny, machine, user); + return false; + } + + private void OnDispenseItem(Entity ent, ref SmartFridgeDispenseItemMessage args) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (!Allowed(ent, args.Actor)) + return; + + if (!ent.Comp.ContainedEntries.TryGetValue(args.Entry, out var contained)) + { + _audio.PlayPredicted(ent.Comp.SoundDeny, ent, args.Actor); + _popup.PopupPredicted(Loc.GetString("smart-fridge-component-try-eject-unknown-entry"), ent, args.Actor); + return; + } + + foreach (var item in contained) + { + if (!_container.TryRemoveFromContainer(GetEntity(item))) + continue; + + _audio.PlayPredicted(ent.Comp.SoundVend, ent, args.Actor); + contained.Remove(item); + Dirty(ent); + return; + } + + _audio.PlayPredicted(ent.Comp.SoundDeny, ent, args.Actor); + _popup.PopupPredicted(Loc.GetString("smart-fridge-component-try-eject-out-of-stock"), ent, args.Actor); + } + + private void OnGetDumpableVerb(Entity ent, ref GetDumpableVerbEvent args) + { + if (_accessReader.IsAllowed(args.User, ent)) + { + args.Verb = Loc.GetString("dump-smartfridge-verb-name", ("unit", ent)); + } + } + + private void OnDump(Entity ent, ref DumpEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + args.PlaySound = true; + + DoInsert(ent, args.User, args.DumpQueue, false); + } +} diff --git a/Content.Shared/Storage/Components/DumpableComponent.cs b/Content.Shared/Storage/Components/DumpableComponent.cs index ecf5f17ea9..0c751df2a3 100644 --- a/Content.Shared/Storage/Components/DumpableComponent.cs +++ b/Content.Shared/Storage/Components/DumpableComponent.cs @@ -33,3 +33,15 @@ public sealed partial class DumpableComponent : Component [DataField("multiplier"), AutoNetworkedField] public float Multiplier = 1.0f; } + +/// +/// Event raised on Dumpable entities to get the verb for dumping +/// +[ByRefEvent] +public record struct GetDumpableVerbEvent(EntityUid User, string? Verb); + +/// +/// Event raised on Dumpable entities to complete the dump +/// +[ByRefEvent] +public record struct DumpEvent(Queue DumpQueue, EntityUid User, bool PlaySound, bool Handled); diff --git a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs index d0ad27eee5..6c0cc2d656 100644 --- a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs +++ b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs @@ -1,11 +1,7 @@ using System.Linq; -using Content.Shared.Disposal; -using Content.Shared.Disposal.Components; -using Content.Shared.Disposal.Unit; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Item; -using Content.Shared.Placeable; using Content.Shared.Storage.Components; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; @@ -21,7 +17,6 @@ public sealed class DumpableSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedDisposalUnitSystem _disposalUnitSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; @@ -39,10 +34,12 @@ public sealed class DumpableSystem : EntitySystem private void OnAfterInteract(EntityUid uid, DumpableComponent component, AfterInteractEvent args) { - if (!args.CanReach || args.Handled) + if (!args.CanReach || args.Handled || args.Target is not { } target) return; - if (!HasComp(args.Target) && !HasComp(args.Target)) + var evt = new GetDumpableVerbEvent(args.User, null); + RaiseLocalEvent(target, ref evt); + if (evt.Verb is null) return; if (!TryComp(uid, out var storage)) @@ -51,7 +48,7 @@ public sealed class DumpableSystem : EntitySystem if (!storage.Container.ContainedEntities.Any()) return; - StartDoAfter(uid, args.Target.Value, args.User, component); + StartDoAfter(uid, target, args.User, component); args.Handled = true; } @@ -83,33 +80,22 @@ public sealed class DumpableSystem : EntitySystem if (!TryComp(uid, out var storage) || !storage.Container.ContainedEntities.Any()) return; - if (HasComp(args.Target)) - { - UtilityVerb verb = new() - { - Act = () => - { - StartDoAfter(uid, args.Target, args.User, dumpable); - }, - Text = Loc.GetString("dump-disposal-verb-name", ("unit", args.Target)), - IconEntity = GetNetEntity(uid) - }; - args.Verbs.Add(verb); - } + var evt = new GetDumpableVerbEvent(args.User, null); + RaiseLocalEvent(args.Target, ref evt); - if (HasComp(args.Target)) + if (evt.Verb is not { } verbText) + return; + + UtilityVerb verb = new() { - UtilityVerb verb = new() + Act = () => { - Act = () => - { - StartDoAfter(uid, args.Target, args.User, dumpable); - }, - Text = Loc.GetString("dump-placeable-verb-name", ("surface", args.Target)), - IconEntity = GetNetEntity(uid) - }; - args.Verbs.Add(verb); - } + StartDoAfter(uid, args.Target, args.User, dumpable); + }, + Text = verbText, + IconEntity = GetNetEntity(uid) + }; + args.Verbs.Add(verb); } private void StartDoAfter(EntityUid storageUid, EntityUid targetUid, EntityUid userUid, DumpableComponent dumpable) @@ -141,34 +127,15 @@ public sealed class DumpableSystem : EntitySystem private void OnDoAfter(EntityUid uid, DumpableComponent component, DumpableDoAfterEvent args) { - if (args.Handled || args.Cancelled || !TryComp(uid, out var storage) || storage.Container.ContainedEntities.Count == 0) + if (args.Handled || args.Cancelled || !TryComp(uid, out var storage) || storage.Container.ContainedEntities.Count == 0 || args.Args.Target is not { } target) return; var dumpQueue = new Queue(storage.Container.ContainedEntities); - var dumped = false; + var evt = new DumpEvent(dumpQueue, args.Args.User, false, false); + RaiseLocalEvent(target, ref evt); - if (HasComp(args.Args.Target)) - { - dumped = true; - - foreach (var entity in dumpQueue) - { - _disposalUnitSystem.DoInsertDisposalUnit(args.Args.Target.Value, entity, args.Args.User); - } - } - else if (HasComp(args.Args.Target)) - { - dumped = true; - - var (targetPos, targetRot) = _transformSystem.GetWorldPositionRotation(args.Args.Target.Value); - - foreach (var entity in dumpQueue) - { - _transformSystem.SetWorldPositionRotation(entity, targetPos + _random.NextVector2Box() / 4, targetRot); - } - } - else + if (!evt.Handled) { var targetPos = _transformSystem.GetWorldPosition(uid); @@ -177,9 +144,11 @@ public sealed class DumpableSystem : EntitySystem var transform = Transform(entity); _transformSystem.SetWorldPositionRotation(entity, targetPos + _random.NextVector2Box() / 4, _random.NextAngle(), transform); } + + return; } - if (dumped) + if (evt.PlaySound) { _audio.PlayPredicted(component.DumpSound, uid, args.User); } diff --git a/Resources/Locale/en-US/smartfridge/smartfridge.ftl b/Resources/Locale/en-US/smartfridge/smartfridge.ftl new file mode 100644 index 0000000000..e7960367b9 --- /dev/null +++ b/Resources/Locale/en-US/smartfridge/smartfridge.ftl @@ -0,0 +1,8 @@ +smart-fridge-component-try-eject-unknown-entry = Invalid selection! +smart-fridge-component-try-eject-out-of-stock = Out of stock! +smart-fridge-component-try-eject-access-denied = Access denied! +smart-fridge-component-search-filter = Search... +smart-fridge-component-title = SmartFridge +smart-fridge-list-item = {$item} [{$amount}] +smart-fridge-request-generic = All sales final +smart-fridge-request-chemistry = Request refills from chemistry diff --git a/Resources/Locale/en-US/storage/components/dumpable-component.ftl b/Resources/Locale/en-US/storage/components/dumpable-component.ftl index 732a45a3b6..867b461f1f 100644 --- a/Resources/Locale/en-US/storage/components/dumpable-component.ftl +++ b/Resources/Locale/en-US/storage/components/dumpable-component.ftl @@ -1,3 +1,4 @@ dump-verb-name = Dump out on ground dump-disposal-verb-name = Dump out into {$unit} dump-placeable-verb-name = Dump out onto {$surface} +dump-smartfridge-verb-name = Restock into {$unit} diff --git a/Resources/Prototypes/Entities/Structures/Machines/smartfridge.yml b/Resources/Prototypes/Entities/Structures/Machines/smartfridge.yml index 8ea6fee938..788febc014 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/smartfridge.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/smartfridge.yml @@ -21,27 +21,37 @@ - state: smartfridge_door map: ["enum.StorageVisualLayers.Door"] shader: unshaded - - type: EntityStorageVisuals - stateBaseClosed: smartfridge - stateDoorOpen: smartfridge_open - stateDoorClosed: smartfridge_door - type: PointLight radius: 1.5 energy: 1.6 color: "#9dc5c9" - - type: EntityStorage - isCollidableWhenOpen: true - closeSound: - path: /Audio/Machines/windoor_open.ogg - params: - volume: -3 - openSound: - path: /Audio/Machines/windoor_open.ogg - params: - volume: -3 - type: ContainerContainer containers: - entity_storage: !type:Container + smart_fridge_inventory: !type:Container + - type: LitOnPowered + - type: ApcPowerReceiver + powerLoad: 200 + - type: ExtensionCableReceiver + - type: SmartFridge + whitelist: + components: + - FitsInDispenser + - Pill + - Produce + - Seed + tags: + - PillCanister + - Bottle + - Syringe + - ChemDispensable + - type: ActivatableUI + key: enum.SmartFridgeUiKey.Key + - type: ActivatableUIRequiresPower + - type: UserInterface + interfaces: + enum.SmartFridgeUiKey.Key: + type: SmartFridgeBoundUserInterface + - type: AccessReader - type: UseDelay delay: 1 - type: AntiRottingContainer @@ -105,3 +115,15 @@ Blunt: 5 soundHit: collection: MetalThud + - type: ExplosionResistance + damageCoefficient: 0.1 + +- type: entity + parent: SmartFridge + id: SmartFridgeMedical + suffix: Medical + components: + - type: SmartFridge + flavorText: smart-fridge-request-chemistry + - type: AccessReader + access: [["Medical"]] diff --git a/Resources/Prototypes/SoundCollections/machines.yml b/Resources/Prototypes/SoundCollections/machines.yml index 3c567ab7b3..45768bfa88 100644 --- a/Resources/Prototypes/SoundCollections/machines.yml +++ b/Resources/Prototypes/SoundCollections/machines.yml @@ -22,3 +22,18 @@ id: CargoBeep files: - /Audio/Effects/Cargo/beep.ogg + +- type: soundCollection + id: VendingDispense + files: + - /Audio/Machines/machine_vend.ogg + +- type: soundCollection + id: VendingDeny + files: + - /Audio/Machines/custom_deny.ogg + +- type: soundCollection + id: MachineInsert + files: + - /Audio/Weapons/Guns/MagIn/revolver_magin.ogg