Predict vending machine UI (#33412)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Advertise.Systems;
|
||||
|
||||
namespace Content.Client.Advertise.Systems;
|
||||
|
||||
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
|
||||
@@ -96,9 +96,12 @@ public class ListContainer : Control
|
||||
{
|
||||
ListContainerButton control = new(data[0], 0);
|
||||
GenerateItem?.Invoke(data[0], control);
|
||||
// Yes this AddChild is necessary for reasons (get proper style or whatever?)
|
||||
// without it the DesiredSize may be different to the final DesiredSize.
|
||||
AddChild(control);
|
||||
control.Measure(Vector2Helpers.Infinity);
|
||||
_itemHeight = control.DesiredSize.Y;
|
||||
control.Dispose();
|
||||
control.Orphan();
|
||||
}
|
||||
|
||||
// Ensure buttons are re-generated.
|
||||
@@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
|
||||
|
||||
public ListContainerButton(ListData data, int index)
|
||||
{
|
||||
AddStyleClass(StyleClassButton);
|
||||
Data = data;
|
||||
Index = index;
|
||||
// AddChild(Background = new PanelContainer
|
||||
|
||||
@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
|
||||
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Robust.Client.AutoGenerated;
|
||||
@@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
|
||||
private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
|
||||
private readonly Dictionary<EntProtoId, uint> _amounts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vending machine is able to be interacted with or not.
|
||||
/// </summary>
|
||||
private bool _enabled;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
||||
|
||||
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
|
||||
|
||||
public VendingMachineMenu()
|
||||
{
|
||||
MinSize = SetSize = new Vector2(250, 150);
|
||||
@@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
|
||||
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
|
||||
return;
|
||||
|
||||
button.AddChild(new VendingMachineItem(protoID, text));
|
||||
|
||||
button.ToolTip = text;
|
||||
button.StyleBoxOverride = _styleBox;
|
||||
var item = new VendingMachineItem(protoID, text);
|
||||
_listItems[protoID] = (button, item);
|
||||
button.AddChild(item);
|
||||
button.AddStyleClass("ButtonSquare");
|
||||
button.Disabled = !_enabled || _amounts[protoID] == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the list of available items on the vending machine interface
|
||||
/// and sets icons based on their prototypes
|
||||
/// </summary>
|
||||
public void Populate(List<VendingMachineInventoryEntry> inventory)
|
||||
public void Populate(List<VendingMachineInventoryEntry> inventory, bool enabled)
|
||||
{
|
||||
_enabled = enabled;
|
||||
_listItems.Clear();
|
||||
_amounts.Clear();
|
||||
|
||||
if (inventory.Count == 0 && VendingContents.Visible)
|
||||
{
|
||||
SearchBar.Visible = false;
|
||||
@@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
|
||||
var entry = inventory[i];
|
||||
|
||||
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
|
||||
{
|
||||
_amounts[entry.ID] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_dummies.TryGetValue(entry.ID, out var dummy))
|
||||
{
|
||||
@@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
|
||||
|
||||
var itemName = Identity.Name(dummy, _entityManager);
|
||||
var itemText = $"{itemName} [{entry.Amount}]";
|
||||
_amounts[entry.ID] = entry.Amount;
|
||||
|
||||
if (itemText.Length > longestEntry.Length)
|
||||
longestEntry = itemText;
|
||||
|
||||
listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
|
||||
listData.Add(new VendorItemsListData(prototype.ID, i)
|
||||
{
|
||||
ItemText = itemText,
|
||||
});
|
||||
}
|
||||
|
||||
VendingContents.PopulateList(listData);
|
||||
@@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
|
||||
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates text entries for vending data in place without modifying the list controls.
|
||||
/// </summary>
|
||||
public void UpdateAmounts(List<VendingMachineInventoryEntry> cachedInventory, bool enabled)
|
||||
{
|
||||
_enabled = enabled;
|
||||
|
||||
foreach (var proto in _dummies.Keys)
|
||||
{
|
||||
if (!_listItems.TryGetValue(proto, out var button))
|
||||
continue;
|
||||
|
||||
var dummy = _dummies[proto];
|
||||
var amount = cachedInventory.First(o => o.ID == proto).Amount;
|
||||
// Could be better? Problem is all inventory entries get squashed.
|
||||
var text = GetItemText(dummy, amount);
|
||||
|
||||
button.Item.SetText(text);
|
||||
button.Button.Disabled = !enabled || amount == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetItemText(EntityUid dummy, uint amount)
|
||||
{
|
||||
var itemName = Identity.Name(dummy, _entityManager);
|
||||
return $"{itemName} [{amount}]";
|
||||
}
|
||||
|
||||
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
|
||||
{
|
||||
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
|
||||
Math.Clamp(contentCount * 50, 150, 350));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
|
||||
public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
|
||||
{
|
||||
public string ItemText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
|
||||
|
||||
var system = EntMan.System<VendingMachineSystem>();
|
||||
_cachedInventory = system.GetAllInventory(Owner);
|
||||
|
||||
_menu?.Populate(_cachedInventory);
|
||||
_menu?.Populate(_cachedInventory, enabled);
|
||||
}
|
||||
|
||||
public void UpdateAmounts()
|
||||
{
|
||||
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
|
||||
|
||||
var system = EntMan.System<VendingMachineSystem>();
|
||||
_cachedInventory = system.GetAllInventory(Owner);
|
||||
_menu?.UpdateAmounts(_cachedInventory, enabled);
|
||||
}
|
||||
|
||||
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
|
||||
@@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
|
||||
if (selectedItem == null)
|
||||
return;
|
||||
|
||||
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
|
||||
SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.VendingMachines;
|
||||
|
||||
@@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -16,15 +17,70 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
|
||||
SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
|
||||
}
|
||||
|
||||
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
|
||||
private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
|
||||
{
|
||||
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
|
||||
if (args.Current is not VendingMachineComponentState state)
|
||||
return;
|
||||
|
||||
var uid = entity.Owner;
|
||||
var component = entity.Comp;
|
||||
|
||||
component.Contraband = state.Contraband;
|
||||
component.EjectEnd = state.EjectEnd;
|
||||
component.DenyEnd = state.DenyEnd;
|
||||
component.DispenseOnHitEnd = state.DispenseOnHitEnd;
|
||||
|
||||
// If all we did was update amounts then we can leave BUI buttons in place.
|
||||
var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
|
||||
!component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
|
||||
!component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
|
||||
|
||||
component.Inventory.Clear();
|
||||
component.EmaggedInventory.Clear();
|
||||
component.ContrabandInventory.Clear();
|
||||
|
||||
foreach (var entry in state.Inventory)
|
||||
{
|
||||
component.Inventory.Add(entry.Key, new(entry.Value));
|
||||
}
|
||||
|
||||
foreach (var entry in state.EmaggedInventory)
|
||||
{
|
||||
component.EmaggedInventory.Add(entry.Key, new(entry.Value));
|
||||
}
|
||||
|
||||
foreach (var entry in state.ContrabandInventory)
|
||||
{
|
||||
component.ContrabandInventory.Add(entry.Key, new(entry.Value));
|
||||
}
|
||||
|
||||
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
|
||||
{
|
||||
if (fullUiUpdate)
|
||||
{
|
||||
bui.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
bui.UpdateAmounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(entity.Owner,
|
||||
VendingMachineUiKey.Key,
|
||||
out var bui))
|
||||
{
|
||||
bui.UpdateAmounts();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(EntityUid uid, VendingMachineComponent component, AnimationCompletedEvent args)
|
||||
@@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
||||
if (component.LoopDenyAnimation)
|
||||
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
|
||||
else
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
|
||||
|
||||
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
||||
break;
|
||||
|
||||
case VendingMachineVisualState.Eject:
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
|
||||
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Advertise.Systems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
|
||||
|
||||
namespace Content.Server.Advertise;
|
||||
namespace Content.Server.Advertise.EntitySystems;
|
||||
|
||||
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
||||
public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
@@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
||||
entity.Comp.Flag = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return false;
|
||||
|
||||
entity.Comp.Flag = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Advertise.EntitySystems;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Arcade;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Arcade.BlockGame;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Advertise.EntitySystems;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Arcade;
|
||||
using Content.Shared.Power;
|
||||
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
@@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
||||
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
|
||||
}
|
||||
|
||||
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
|
||||
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
|
||||
{
|
||||
if (component.Game == null)
|
||||
return;
|
||||
@@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
||||
|
||||
switch (msg.PlayerAction)
|
||||
{
|
||||
case PlayerAction.Attack:
|
||||
case PlayerAction.Heal:
|
||||
case PlayerAction.Recharge:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
|
||||
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
|
||||
// Any sort of gameplay action counts
|
||||
if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
|
||||
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
|
||||
break;
|
||||
case PlayerAction.NewGame:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
|
||||
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
|
||||
|
||||
component.Game = new SpaceVillainGame(uid, component, this);
|
||||
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
break;
|
||||
case PlayerAction.RequestData:
|
||||
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
|
||||
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
|
||||
return;
|
||||
|
||||
_uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
|
||||
_uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.Emp;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Emp;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
@@ -21,7 +14,6 @@ using Content.Shared.Throwing;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Content.Shared.Wall;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
@@ -32,14 +24,9 @@ namespace Content.Server.VendingMachines
|
||||
public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly PricingSystem _pricing = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _light = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
private const float WallVendEjectDistanceFromWall = 1f;
|
||||
|
||||
@@ -55,11 +42,6 @@ namespace Content.Server.VendingMachines
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
|
||||
|
||||
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
|
||||
{
|
||||
subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
|
||||
});
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
|
||||
@@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines
|
||||
|
||||
if (HasComp<ApcPowerReceiverComponent>(uid))
|
||||
{
|
||||
TryUpdateVisualState(uid, component);
|
||||
TryUpdateVisualState((uid, component));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,26 +83,15 @@ namespace Content.Server.VendingMachines
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
if (args.Actor is not { Valid: true } entity || Deleted(entity))
|
||||
return;
|
||||
|
||||
AuthorizedVend(uid, entity, args.Type, args.ID, component);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
TryUpdateVisualState(uid, component);
|
||||
TryUpdateVisualState((uid, component));
|
||||
}
|
||||
|
||||
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
|
||||
{
|
||||
vendComponent.Broken = true;
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
}
|
||||
|
||||
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
|
||||
@@ -128,7 +99,7 @@ namespace Content.Server.VendingMachines
|
||||
if (!args.DamageIncreased && component.Broken)
|
||||
{
|
||||
component.Broken = false;
|
||||
TryUpdateVisualState(uid, component);
|
||||
TryUpdateVisualState((uid, component));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,8 +110,11 @@ namespace Content.Server.VendingMachines
|
||||
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
|
||||
_random.Prob(component.DispenseOnHitChance.Value))
|
||||
{
|
||||
if (component.DispenseOnHitCooldown > 0f)
|
||||
component.DispenseOnHitCoolingDown = true;
|
||||
if (component.DispenseOnHitCooldown != null)
|
||||
{
|
||||
component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
|
||||
}
|
||||
|
||||
EjectRandom(uid, throwItem: true, forceEject: true, component);
|
||||
}
|
||||
}
|
||||
@@ -199,145 +173,6 @@ namespace Content.Server.VendingMachines
|
||||
Dirty(uid, component);
|
||||
}
|
||||
|
||||
public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
if (vendComponent.Denying)
|
||||
return;
|
||||
|
||||
vendComponent.Denying = true;
|
||||
Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f));
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user is authorized to use this vending machine
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="sender">Entity trying to use the vending machine</param>
|
||||
/// <param name="vendComponent"></param>
|
||||
public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return false;
|
||||
|
||||
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
|
||||
return true;
|
||||
|
||||
if (_accessReader.IsAllowed(sender, uid, accessReader))
|
||||
return true;
|
||||
|
||||
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
|
||||
Deny(uid, vendComponent);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
|
||||
/// or the item doesn't exist in its inventory.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="type">The type of inventory the item is from</param>
|
||||
/// <param name="itemId">The prototype ID of the item</param>
|
||||
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
|
||||
/// <param name="vendComponent"></param>
|
||||
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = GetEntry(uid, itemId, type, vendComponent);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
|
||||
Deny(uid, vendComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.Amount <= 0)
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
|
||||
Deny(uid, vendComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(entry.ID))
|
||||
return;
|
||||
|
||||
|
||||
// Start Ejecting, and prevent users from ordering while anim playing
|
||||
vendComponent.Ejecting = true;
|
||||
vendComponent.NextItemToEject = entry.ID;
|
||||
vendComponent.ThrowNextItem = throwItem;
|
||||
|
||||
if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
|
||||
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
|
||||
|
||||
entry.Amount--;
|
||||
Dirty(uid, vendComponent);
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
Audio.PlayPvs(vendComponent.SoundVend, uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="sender">Entity that is trying to use the vending machine</param>
|
||||
/// <param name="type">The type of inventory the item is from</param>
|
||||
/// <param name="itemId">The prototype ID of the item</param>
|
||||
/// <param name="component"></param>
|
||||
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
|
||||
{
|
||||
if (IsAuthorized(uid, sender, component))
|
||||
{
|
||||
TryEjectVendorItem(uid, type, itemId, component.CanShoot, component);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to update the visuals of the component based on its current state.
|
||||
/// </summary>
|
||||
public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
var finalState = VendingMachineVisualState.Normal;
|
||||
if (vendComponent.Broken)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Broken;
|
||||
}
|
||||
else if (vendComponent.Ejecting)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Eject;
|
||||
}
|
||||
else if (vendComponent.Denying)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Deny;
|
||||
}
|
||||
else if (!this.IsPowered(uid, EntityManager))
|
||||
{
|
||||
finalState = VendingMachineVisualState.Off;
|
||||
}
|
||||
|
||||
if (_light.TryGetLight(uid, out var pointlight))
|
||||
{
|
||||
var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
|
||||
_light.SetEnabled(uid, lightState, pointlight);
|
||||
}
|
||||
|
||||
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
|
||||
/// </summary>
|
||||
@@ -367,18 +202,18 @@ namespace Content.Server.VendingMachines
|
||||
}
|
||||
else
|
||||
{
|
||||
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
|
||||
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
|
||||
protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
// No need to update the visual state because we never changed it during a forced eject
|
||||
if (!forceEject)
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
|
||||
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
|
||||
{
|
||||
@@ -411,68 +246,17 @@ namespace Content.Server.VendingMachines
|
||||
vendComponent.ThrowNextItem = false;
|
||||
}
|
||||
|
||||
private VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return null;
|
||||
|
||||
if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return component.EmaggedInventory.GetValueOrDefault(entryId);
|
||||
|
||||
if (type == InventoryType.Contraband && component.Contraband)
|
||||
return component.ContrabandInventory.GetValueOrDefault(entryId);
|
||||
|
||||
return component.Inventory.GetValueOrDefault(entryId);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<VendingMachineComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (comp.Ejecting)
|
||||
{
|
||||
comp.EjectAccumulator += frameTime;
|
||||
if (comp.EjectAccumulator >= comp.EjectDelay)
|
||||
{
|
||||
comp.EjectAccumulator = 0f;
|
||||
comp.Ejecting = false;
|
||||
|
||||
EjectItem(uid, comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.Denying)
|
||||
{
|
||||
comp.DenyAccumulator += frameTime;
|
||||
if (comp.DenyAccumulator >= comp.DenyDelay)
|
||||
{
|
||||
comp.DenyAccumulator = 0f;
|
||||
comp.Denying = false;
|
||||
|
||||
TryUpdateVisualState(uid, comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.DispenseOnHitCoolingDown)
|
||||
{
|
||||
comp.DispenseOnHitAccumulator += frameTime;
|
||||
if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
|
||||
{
|
||||
comp.DispenseOnHitAccumulator = 0f;
|
||||
comp.DispenseOnHitCoolingDown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>();
|
||||
while (disabled.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
if (comp.NextEmpEject < _timing.CurTime)
|
||||
{
|
||||
EjectRandom(uid, true, false, comp);
|
||||
comp.NextEmpEject += TimeSpan.FromSeconds(5 * comp.EjectDelay);
|
||||
comp.NextEmpEject += (5 * comp.EjectDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,7 +269,7 @@ namespace Content.Server.VendingMachines
|
||||
RestockInventoryFromPrototype(uid, vendComponent);
|
||||
|
||||
Dirty(uid, vendComponent);
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
}
|
||||
|
||||
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using Content.Shared.Advertise.Systems;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Advertise.Components;
|
||||
namespace Content.Shared.Advertise.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally
|
||||
/// requiring that a Flag be set as well.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))]
|
||||
public sealed partial class SpeakOnUIClosedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
@@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component
|
||||
/// <summary>
|
||||
/// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Flag;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using SpeakOnUIClosedComponent = Content.Shared.Advertise.Components.SpeakOnUIClosedComponent;
|
||||
|
||||
namespace Content.Shared.Advertise.Systems;
|
||||
|
||||
public abstract class SharedSpeakOnUIClosedSystem : EntitySystem
|
||||
{
|
||||
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return false;
|
||||
|
||||
entity.Comp.Flag = value;
|
||||
Dirty(entity);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public abstract partial class SharedVendingMachineSystem
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float) component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target,
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float)component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target,
|
||||
target: target, used: uid)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
|
||||
@@ -1,34 +1,143 @@
|
||||
using Content.Shared.Emag.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Advertise.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.VendingMachines;
|
||||
|
||||
public abstract partial class SharedVendingMachineSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] protected readonly SharedAudioSystem Audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] protected readonly SharedPointLightSystem Light = default!;
|
||||
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
|
||||
[Dependency] protected readonly SharedPopupSystem Popup = default!;
|
||||
[Dependency] private readonly SharedSpeakOnUIClosedSystem _speakOn = default!;
|
||||
[Dependency] protected readonly SharedUserInterfaceSystem UISystem = default!;
|
||||
[Dependency] protected readonly IRobustRandom Randomizer = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
|
||||
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
|
||||
|
||||
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
|
||||
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
|
||||
{
|
||||
subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnVendingGetState(Entity<VendingMachineComponent> entity, ref ComponentGetState args)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
|
||||
var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
|
||||
var emaggedInventory = new Dictionary<string, VendingMachineInventoryEntry>();
|
||||
var contrabandInventory = new Dictionary<string, VendingMachineInventoryEntry>();
|
||||
|
||||
foreach (var weh in component.Inventory)
|
||||
{
|
||||
inventory[weh.Key] = new(weh.Value);
|
||||
}
|
||||
|
||||
foreach (var weh in component.EmaggedInventory)
|
||||
{
|
||||
emaggedInventory[weh.Key] = new(weh.Value);
|
||||
}
|
||||
|
||||
foreach (var weh in component.ContrabandInventory)
|
||||
{
|
||||
contrabandInventory[weh.Key] = new(weh.Value);
|
||||
}
|
||||
|
||||
args.State = new VendingMachineComponentState()
|
||||
{
|
||||
Inventory = inventory,
|
||||
EmaggedInventory = emaggedInventory,
|
||||
ContrabandInventory = contrabandInventory,
|
||||
Contraband = component.Contraband,
|
||||
EjectEnd = component.EjectEnd,
|
||||
DenyEnd = component.DenyEnd,
|
||||
DispenseOnHitEnd = component.DispenseOnHitEnd,
|
||||
};
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<VendingMachineComponent>();
|
||||
var curTime = Timing.CurTime;
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (comp.Ejecting)
|
||||
{
|
||||
if (curTime > comp.EjectEnd)
|
||||
{
|
||||
comp.EjectEnd = null;
|
||||
Dirty(uid, comp);
|
||||
|
||||
EjectItem(uid, comp);
|
||||
UpdateUI((uid, comp));
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.Denying)
|
||||
{
|
||||
if (curTime > comp.DenyEnd)
|
||||
{
|
||||
comp.DenyEnd = null;
|
||||
Dirty(uid, comp);
|
||||
|
||||
TryUpdateVisualState((uid, comp));
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.DispenseOnHitCoolingDown)
|
||||
{
|
||||
if (curTime > comp.DispenseOnHitEnd)
|
||||
{
|
||||
comp.DispenseOnHitEnd = null;
|
||||
Dirty(uid, comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInventoryEjectMessage(Entity<VendingMachineComponent> entity, ref VendingMachineEjectMessage args)
|
||||
{
|
||||
if (!_receiver.IsPowered(entity.Owner) || Deleted(entity))
|
||||
return;
|
||||
|
||||
if (args.Actor is not { Valid: true } actor)
|
||||
return;
|
||||
|
||||
AuthorizedVend(entity.Owner, actor, args.Type, args.ID, entity.Comp);
|
||||
}
|
||||
|
||||
protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
|
||||
@@ -36,6 +145,162 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
|
||||
RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
|
||||
}
|
||||
|
||||
protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user is authorized to use this vending machine
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="sender">Entity trying to use the vending machine</param>
|
||||
/// <param name="vendComponent"></param>
|
||||
public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return false;
|
||||
|
||||
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
|
||||
return true;
|
||||
|
||||
if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp<EmaggedComponent>(uid))
|
||||
return true;
|
||||
|
||||
Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid, sender);
|
||||
Deny((uid, vendComponent), sender);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return null;
|
||||
|
||||
if (type == InventoryType.Emagged && HasComp<EmaggedComponent>(uid))
|
||||
return component.EmaggedInventory.GetValueOrDefault(entryId);
|
||||
|
||||
if (type == InventoryType.Contraband && component.Contraband)
|
||||
return component.ContrabandInventory.GetValueOrDefault(entryId);
|
||||
|
||||
return component.Inventory.GetValueOrDefault(entryId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
|
||||
/// or the item doesn't exist in its inventory.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="type">The type of inventory the item is from</param>
|
||||
/// <param name="itemId">The prototype ID of the item</param>
|
||||
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
|
||||
/// <param name="vendComponent"></param>
|
||||
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
if (vendComponent.Ejecting || vendComponent.Broken || !_receiver.IsPowered(uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = GetEntry(uid, itemId, type, vendComponent);
|
||||
|
||||
if (string.IsNullOrEmpty(entry?.ID))
|
||||
{
|
||||
Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
|
||||
Deny((uid, vendComponent));
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.Amount <= 0)
|
||||
{
|
||||
Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
|
||||
Deny((uid, vendComponent));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start Ejecting, and prevent users from ordering while anim playing
|
||||
vendComponent.EjectEnd = Timing.CurTime + vendComponent.EjectDelay;
|
||||
vendComponent.NextItemToEject = entry.ID;
|
||||
vendComponent.ThrowNextItem = throwItem;
|
||||
|
||||
if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
|
||||
_speakOn.TrySetFlag((uid, speakComponent));
|
||||
|
||||
entry.Amount--;
|
||||
Dirty(uid, vendComponent);
|
||||
UpdateUI((uid, vendComponent));
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
Audio.PlayPredicted(vendComponent.SoundVend, uid, user);
|
||||
}
|
||||
|
||||
public void Deny(Entity<VendingMachineComponent?> entity, EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref entity.Comp))
|
||||
return;
|
||||
|
||||
if (entity.Comp.Denying)
|
||||
return;
|
||||
|
||||
entity.Comp.DenyEnd = Timing.CurTime + entity.Comp.DenyDelay;
|
||||
Audio.PlayPredicted(entity.Comp.SoundDeny, entity.Owner, user, AudioParams.Default.WithVolume(-2f));
|
||||
TryUpdateVisualState(entity);
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
protected virtual void UpdateUI(Entity<VendingMachineComponent?> entity) { }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to update the visuals of the component based on its current state.
|
||||
/// </summary>
|
||||
public void TryUpdateVisualState(Entity<VendingMachineComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref entity.Comp))
|
||||
return;
|
||||
|
||||
var finalState = VendingMachineVisualState.Normal;
|
||||
if (entity.Comp.Broken)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Broken;
|
||||
}
|
||||
else if (entity.Comp.Ejecting)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Eject;
|
||||
}
|
||||
else if (entity.Comp.Denying)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Deny;
|
||||
}
|
||||
else if (!_receiver.IsPowered(entity.Owner))
|
||||
{
|
||||
finalState = VendingMachineVisualState.Off;
|
||||
}
|
||||
|
||||
// TODO: You know this should really live on the client with netsync off because client knows the state.
|
||||
if (Light.TryGetLight(entity.Owner, out var pointlight))
|
||||
{
|
||||
var lightEnabled = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
|
||||
Light.SetEnabled(entity.Owner, lightEnabled, pointlight);
|
||||
}
|
||||
|
||||
_appearanceSystem.SetData(entity.Owner, VendingMachineVisuals.VisualState, finalState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="sender">Entity that is trying to use the vending machine</param>
|
||||
/// <param name="type">The type of inventory the item is from</param>
|
||||
/// <param name="itemId">The prototype ID of the item</param>
|
||||
/// <param name="component"></param>
|
||||
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
|
||||
{
|
||||
if (IsAuthorized(uid, sender, component))
|
||||
{
|
||||
TryEjectVendorItem(uid, type, itemId, component.CanShoot, sender, component);
|
||||
}
|
||||
}
|
||||
|
||||
public void RestockInventoryFromPrototype(EntityUid uid,
|
||||
VendingMachineComponent? component = null, float restockQuality = 1f)
|
||||
{
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.VendingMachines
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class VendingMachineComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/>
|
||||
/// </summary>
|
||||
// Okay so not using ProtoId here is load-bearing because the ProtoId serializer will log errors if the prototype doesn't exist.
|
||||
[DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<VendingMachineInventoryPrototype>), required: true)]
|
||||
public string PackPrototypeId = string.Empty;
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Content.Shared.VendingMachines
|
||||
/// Used by the client to determine how long the deny animation should be played.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float DenyDelay = 2.0f;
|
||||
public TimeSpan DenyDelay = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// Used by the server to determine how long the vending machine stays in the "Eject" state.
|
||||
@@ -30,23 +30,40 @@ namespace Content.Shared.VendingMachines
|
||||
/// Used by the client to determine how long the deny animation should be played.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float EjectDelay = 1.2f;
|
||||
public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2);
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
[DataField]
|
||||
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
[DataField]
|
||||
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
[DataField]
|
||||
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
/// <summary>
|
||||
/// If true then unlocks the <see cref="ContrabandInventory"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Contraband;
|
||||
|
||||
public bool Ejecting;
|
||||
public bool Denying;
|
||||
public bool DispenseOnHitCoolingDown;
|
||||
[ViewVariables]
|
||||
public bool Ejecting => EjectEnd != null;
|
||||
|
||||
[ViewVariables]
|
||||
public bool Denying => DenyEnd != null;
|
||||
|
||||
[ViewVariables]
|
||||
public bool DispenseOnHitCoolingDown => DispenseOnHitEnd != null;
|
||||
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan? EjectEnd;
|
||||
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan? DenyEnd;
|
||||
|
||||
[DataField]
|
||||
public TimeSpan? DispenseOnHitEnd;
|
||||
|
||||
public string? NextItemToEject;
|
||||
|
||||
@@ -55,7 +72,7 @@ namespace Content.Shared.VendingMachines
|
||||
/// <summary>
|
||||
/// When true, will forcefully throw any object it dispenses
|
||||
/// </summary>
|
||||
[DataField("speedLimiter")]
|
||||
[DataField]
|
||||
public bool CanShoot = false;
|
||||
|
||||
public bool ThrowNextItem = false;
|
||||
@@ -64,14 +81,14 @@ namespace Content.Shared.VendingMachines
|
||||
/// The chance that a vending machine will randomly dispense an item on hit.
|
||||
/// Chance is 0 if null.
|
||||
/// </summary>
|
||||
[DataField("dispenseOnHitChance")]
|
||||
[DataField]
|
||||
public float? DispenseOnHitChance;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of damage that must be done per hit to have a chance
|
||||
/// of dispensing an item.
|
||||
/// </summary>
|
||||
[DataField("dispenseOnHitThreshold")]
|
||||
[DataField]
|
||||
public float? DispenseOnHitThreshold;
|
||||
|
||||
/// <summary>
|
||||
@@ -80,13 +97,13 @@ namespace Content.Shared.VendingMachines
|
||||
/// 0 for a vending machine for legitimate reasons (no desired delay/no eject animation)
|
||||
/// and can be circumvented with forced ejections.
|
||||
/// </summary>
|
||||
[DataField("dispenseOnHitCooldown")]
|
||||
public float? DispenseOnHitCooldown = 1.0f;
|
||||
[DataField]
|
||||
public TimeSpan? DispenseOnHitCooldown = TimeSpan.FromSeconds(1.0);
|
||||
|
||||
/// <summary>
|
||||
/// Sound that plays when ejecting an item
|
||||
/// </summary>
|
||||
[DataField("soundVend")]
|
||||
[DataField]
|
||||
// Grabbed from: https://github.com/tgstation/tgstation/blob/d34047a5ae911735e35cd44a210953c9563caa22/sound/machines/machine_vend.ogg
|
||||
public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg")
|
||||
{
|
||||
@@ -100,7 +117,7 @@ namespace Content.Shared.VendingMachines
|
||||
/// <summary>
|
||||
/// Sound that plays when an item can't be ejected
|
||||
/// </summary>
|
||||
[DataField("soundDeny")]
|
||||
[DataField]
|
||||
// Yoinked from: https://github.com/discordia-space/CEV-Eris/blob/35bbad6764b14e15c03a816e3e89aa1751660ba9/sound/machines/Custom_deny.ogg
|
||||
public SoundSpecifier SoundDeny = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
|
||||
|
||||
@@ -108,10 +125,6 @@ namespace Content.Shared.VendingMachines
|
||||
|
||||
public float NonLimitedEjectRange = 5f;
|
||||
|
||||
public float EjectAccumulator = 0f;
|
||||
public float DenyAccumulator = 0f;
|
||||
public float DispenseOnHitAccumulator = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// The quality of the stock in the vending machine on spawn.
|
||||
/// Represents the percentage chance (0.0f = 0%, 1.0f = 100%) each set of items in the machine is fully-stocked.
|
||||
@@ -123,7 +136,7 @@ namespace Content.Shared.VendingMachines
|
||||
/// <summary>
|
||||
/// While disabled by EMP it randomly ejects items
|
||||
/// </summary>
|
||||
[DataField("nextEmpEject", customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextEmpEject = TimeSpan.Zero;
|
||||
|
||||
#region Client Visuals
|
||||
@@ -131,28 +144,28 @@ namespace Content.Shared.VendingMachines
|
||||
/// RSI state for when the vending machine is unpowered.
|
||||
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
|
||||
/// </summary>
|
||||
[DataField("offState")]
|
||||
[DataField]
|
||||
public string? OffState;
|
||||
|
||||
/// <summary>
|
||||
/// RSI state for the screen of the vending machine
|
||||
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Screen"/>
|
||||
/// </summary>
|
||||
[DataField("screenState")]
|
||||
[DataField]
|
||||
public string? ScreenState;
|
||||
|
||||
/// <summary>
|
||||
/// RSI state for the vending machine's normal state. Usually a looping animation.
|
||||
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
|
||||
/// </summary>
|
||||
[DataField("normalState")]
|
||||
[DataField]
|
||||
public string? NormalState;
|
||||
|
||||
/// <summary>
|
||||
/// RSI state for the vending machine's eject animation.
|
||||
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
|
||||
/// </summary>
|
||||
[DataField("ejectState")]
|
||||
[DataField]
|
||||
public string? EjectState;
|
||||
|
||||
/// <summary>
|
||||
@@ -160,14 +173,14 @@ namespace Content.Shared.VendingMachines
|
||||
/// or looped depending on how <see cref="LoopDenyAnimation"/> is set.
|
||||
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
|
||||
/// </summary>
|
||||
[DataField("denyState")]
|
||||
[DataField]
|
||||
public string? DenyState;
|
||||
|
||||
/// <summary>
|
||||
/// RSI state for when the vending machine is unpowered.
|
||||
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
|
||||
/// </summary>
|
||||
[DataField("brokenState")]
|
||||
[DataField]
|
||||
public string? BrokenState;
|
||||
|
||||
/// <summary>
|
||||
@@ -195,6 +208,13 @@ namespace Content.Shared.VendingMachines
|
||||
ID = id;
|
||||
Amount = amount;
|
||||
}
|
||||
|
||||
public VendingMachineInventoryEntry(VendingMachineInventoryEntry entry)
|
||||
{
|
||||
Type = entry.Type;
|
||||
ID = entry.ID;
|
||||
Amount = entry.Amount;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
@@ -206,13 +226,13 @@ namespace Content.Shared.VendingMachines
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum VendingMachineVisuals
|
||||
public enum VendingMachineVisuals : byte
|
||||
{
|
||||
VisualState
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum VendingMachineVisualState
|
||||
public enum VendingMachineVisualState : byte
|
||||
{
|
||||
Normal,
|
||||
Off,
|
||||
@@ -254,4 +274,22 @@ namespace Content.Shared.VendingMachines
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class VendingMachineComponentState : ComponentState
|
||||
{
|
||||
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
|
||||
|
||||
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
|
||||
|
||||
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
|
||||
|
||||
public bool Contraband;
|
||||
|
||||
public TimeSpan? EjectEnd;
|
||||
|
||||
public TimeSpan? DenyEnd;
|
||||
|
||||
public TimeSpan? DispenseOnHitEnd;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user