Generalized Store System (#10201)
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
using Content.Client.Traitor.Uplink;
|
||||
using Content.Shared.Revenant;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
|
||||
79
Content.Client/Store/Ui/StoreBoundUserInterface.cs
Normal file
79
Content.Client/Store/Ui/StoreBoundUserInterface.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Content.Shared.Store;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Store.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private StoreMenu? _menu;
|
||||
|
||||
private string _windowName = Loc.GetString("store-ui-default-title");
|
||||
|
||||
public StoreBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
_menu = new StoreMenu(_windowName);
|
||||
|
||||
_menu.OpenCentered();
|
||||
_menu.OnClose += Close;
|
||||
|
||||
_menu.OnListingButtonPressed += (_, listing) =>
|
||||
{
|
||||
if (_menu.CurrentBuyer != null)
|
||||
SendMessage(new StoreBuyListingMessage(_menu.CurrentBuyer.Value, listing));
|
||||
};
|
||||
|
||||
_menu.OnCategoryButtonPressed += (_, category) =>
|
||||
{
|
||||
_menu.CurrentCategory = category;
|
||||
if (_menu.CurrentBuyer != null)
|
||||
SendMessage(new StoreRequestUpdateInterfaceMessage(_menu.CurrentBuyer.Value));
|
||||
};
|
||||
|
||||
_menu.OnWithdrawAttempt += (_, type, amount) =>
|
||||
{
|
||||
if (_menu.CurrentBuyer != null)
|
||||
SendMessage(new StoreRequestWithdrawMessage(_menu.CurrentBuyer.Value, type, amount));
|
||||
};
|
||||
}
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (_menu == null)
|
||||
return;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case StoreUpdateState msg:
|
||||
if (msg.Buyer != null)
|
||||
_menu.CurrentBuyer = msg.Buyer;
|
||||
_menu.UpdateBalance(msg.Balance);
|
||||
_menu.PopulateStoreCategoryButtons(msg.Listings);
|
||||
_menu.UpdateListing(msg.Listings.ToList());
|
||||
break;
|
||||
case StoreInitializeState msg:
|
||||
_windowName = msg.Name;
|
||||
if (_menu != null && _menu.Window != null)
|
||||
_menu.Window.Title = msg.Name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_menu?.Close();
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
21
Content.Client/Store/Ui/StoreListingControl.xaml
Normal file
21
Content.Client/Store/Ui/StoreListingControl.xaml
Normal file
@@ -0,0 +1,21 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Margin="8,8,8,8" Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="StoreItemName" HorizontalExpand="True" />
|
||||
<Button
|
||||
Name="StoreItemBuyButton"
|
||||
MinWidth="64"
|
||||
HorizontalAlignment="Right"
|
||||
Access="Public" />
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="HighDivider" />
|
||||
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
|
||||
<TextureRect
|
||||
Name="StoreItemTexture"
|
||||
Margin="0,0,4,0"
|
||||
MinSize="48 48"
|
||||
Stretch="KeepAspectCentered" />
|
||||
<RichTextLabel Name="StoreItemDescription" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
24
Content.Client/Store/Ui/StoreListingControl.xaml.cs
Normal file
24
Content.Client/Store/Ui/StoreListingControl.xaml.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Store.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StoreListingControl : Control
|
||||
{
|
||||
public StoreListingControl(string itemName, string itemDescription,
|
||||
string price, bool canBuy, Texture? texture = null)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
StoreItemName.Text = itemName;
|
||||
StoreItemDescription.SetMessage(itemDescription);
|
||||
|
||||
StoreItemBuyButton.Text = price;
|
||||
StoreItemBuyButton.Disabled = !canBuy;
|
||||
|
||||
StoreItemTexture.Texture = texture;
|
||||
}
|
||||
}
|
||||
54
Content.Client/Store/Ui/StoreMenu.xaml
Normal file
54
Content.Client/Store/Ui/StoreMenu.xaml
Normal file
@@ -0,0 +1,54 @@
|
||||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'store-ui-default-title'}"
|
||||
MinSize="512 512"
|
||||
SetSize="512 512">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer Margin="4,4,4,4" Orientation="Horizontal">
|
||||
<RichTextLabel
|
||||
Name="BalanceInfo"
|
||||
HorizontalAlignment="Left"
|
||||
Access="Public"
|
||||
HorizontalExpand="True" />
|
||||
<Button
|
||||
Name="WithdrawButton"
|
||||
MinWidth="64"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{Loc 'store-ui-default-withdraw-text'}" />
|
||||
</BoxContainer>
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#80808005" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Name="CategoryListContainer" Orientation="Vertical">
|
||||
<!-- Category buttons are added here by code -->
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<ScrollContainer
|
||||
Name="StoreListingsScroll"
|
||||
HScrollEnabled="False"
|
||||
HorizontalExpand="True"
|
||||
MinSize="100 256"
|
||||
SizeFlagsStretchRatio="2"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Name="StoreListingsContainer"
|
||||
MinSize="100 256"
|
||||
Orientation="Vertical"
|
||||
SizeFlagsStretchRatio="2"
|
||||
VerticalExpand="True">
|
||||
<!-- Listings are added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
222
Content.Client/Store/Ui/StoreMenu.xaml.cs
Normal file
222
Content.Client/Store/Ui/StoreMenu.xaml.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.Store;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Client.Graphics;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using System.Linq;
|
||||
using Content.Shared.FixedPoint;
|
||||
|
||||
namespace Content.Client.Store.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StoreMenu : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private StoreWithdrawWindow? _withdrawWindow;
|
||||
|
||||
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
||||
|
||||
public EntityUid? CurrentBuyer;
|
||||
public Dictionary<string, FixedPoint2> Balance = new();
|
||||
public string CurrentCategory = string.Empty;
|
||||
|
||||
public StoreMenu(string name)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
||||
if (Window != null)
|
||||
Window.Title = name;
|
||||
}
|
||||
|
||||
public void UpdateBalance(Dictionary<string, FixedPoint2> balance)
|
||||
{
|
||||
Balance = balance;
|
||||
|
||||
var currency = new Dictionary<(string, FixedPoint2), CurrencyPrototype>();
|
||||
foreach (var type in balance)
|
||||
{
|
||||
currency.Add((type.Key, type.Value), _prototypeManager.Index<CurrencyPrototype>(type.Key));
|
||||
}
|
||||
|
||||
var balanceStr = string.Empty;
|
||||
foreach (var type in currency)
|
||||
{
|
||||
balanceStr += $"{Loc.GetString(type.Value.BalanceDisplay, ("amount", type.Key.Item2))}\n";
|
||||
}
|
||||
|
||||
BalanceInfo.SetMarkup(balanceStr.TrimEnd());
|
||||
|
||||
var disabled = true;
|
||||
foreach (var type in currency)
|
||||
{
|
||||
if (type.Value.CanWithdraw && type.Value.EntityId != null && type.Key.Item2 > 0)
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
WithdrawButton.Disabled = disabled;
|
||||
}
|
||||
|
||||
public void UpdateListing(List<ListingData> listings)
|
||||
{
|
||||
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
|
||||
|
||||
// should probably chunk these out instead. to-do if this clogs the internet tubes.
|
||||
// maybe read clients prototypes instead?
|
||||
ClearListings();
|
||||
foreach (var item in sorted)
|
||||
{
|
||||
AddListingGui(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
// check if window is already open
|
||||
if (_withdrawWindow != null && _withdrawWindow.IsOpen)
|
||||
{
|
||||
_withdrawWindow.MoveToFront();
|
||||
return;
|
||||
}
|
||||
|
||||
// open a new one
|
||||
_withdrawWindow = new StoreWithdrawWindow();
|
||||
_withdrawWindow.OpenCentered();
|
||||
|
||||
_withdrawWindow.CreateCurrencyButtons(Balance);
|
||||
_withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
|
||||
}
|
||||
|
||||
private void AddListingGui(ListingData listing)
|
||||
{
|
||||
if (!listing.Categories.Contains(CurrentCategory))
|
||||
return;
|
||||
|
||||
string listingName = new (listing.Name);
|
||||
string listingDesc = new (listing.Description);
|
||||
var listingPrice = listing.Cost;
|
||||
var canBuy = CanBuyListing(Balance, listingPrice);
|
||||
|
||||
var spriteSys = _entityManager.EntitySysManager.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
Texture? texture = null;
|
||||
if (listing.Icon != null)
|
||||
texture = spriteSys.Frame0(listing.Icon);
|
||||
|
||||
if (listing.ProductEntity != null)
|
||||
{
|
||||
if (texture == null)
|
||||
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
|
||||
|
||||
var proto = _prototypeManager.Index<EntityPrototype>(listing.ProductEntity);
|
||||
if (listingName == string.Empty)
|
||||
listingName = proto.Name;
|
||||
if (listingDesc == string.Empty)
|
||||
listingDesc = proto.Description;
|
||||
}
|
||||
else if (listing.ProductAction != null)
|
||||
{
|
||||
var action = _prototypeManager.Index<InstantActionPrototype>(listing.ProductAction);
|
||||
if (action.Icon != null)
|
||||
texture = spriteSys.Frame0(action.Icon);
|
||||
}
|
||||
|
||||
var newListing = new StoreListingControl(listingName, listingDesc, GetListingPriceString(listing), canBuy, texture);
|
||||
newListing.StoreItemBuyButton.OnButtonDown += args
|
||||
=> OnListingButtonPressed?.Invoke(args, listing);
|
||||
|
||||
StoreListingsContainer.AddChild(newListing);
|
||||
}
|
||||
|
||||
public bool CanBuyListing(Dictionary<string, FixedPoint2> currency, Dictionary<string, FixedPoint2> price)
|
||||
{
|
||||
foreach (var type in price)
|
||||
{
|
||||
if (!currency.ContainsKey(type.Key))
|
||||
return false;
|
||||
|
||||
if (currency[type.Key] < type.Value)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public string GetListingPriceString(ListingData listing)
|
||||
{
|
||||
var text = string.Empty;
|
||||
|
||||
foreach (var type in listing.Cost)
|
||||
{
|
||||
var currency = _prototypeManager.Index<CurrencyPrototype>(type.Key);
|
||||
text += $"{Loc.GetString(currency.PriceDisplay, ("amount", type.Value))}\n";
|
||||
}
|
||||
|
||||
if (listing.Cost.Count < 1)
|
||||
text = Loc.GetString("store-currency-free");
|
||||
|
||||
return text.TrimEnd();
|
||||
}
|
||||
|
||||
private void ClearListings()
|
||||
{
|
||||
StoreListingsContainer.Children.Clear();
|
||||
}
|
||||
|
||||
public void PopulateStoreCategoryButtons(HashSet<ListingData> listings)
|
||||
{
|
||||
var allCategories = new List<StoreCategoryPrototype>();
|
||||
foreach (var listing in listings)
|
||||
{
|
||||
foreach (var cat in listing.Categories)
|
||||
{
|
||||
var proto = _prototypeManager.Index<StoreCategoryPrototype>(cat);
|
||||
if (!allCategories.Contains(proto))
|
||||
allCategories.Add(proto);
|
||||
}
|
||||
}
|
||||
|
||||
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
|
||||
|
||||
if (CurrentCategory == string.Empty && allCategories.Count > 0)
|
||||
CurrentCategory = allCategories.First().ID;
|
||||
|
||||
if (allCategories.Count <= 1)
|
||||
return;
|
||||
|
||||
CategoryListContainer.Children.Clear();
|
||||
|
||||
foreach (var proto in allCategories)
|
||||
{
|
||||
var catButton = new StoreCategoryButton
|
||||
{
|
||||
Text = Loc.GetString(proto.Name),
|
||||
Id = proto.ID
|
||||
};
|
||||
|
||||
catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.Id);
|
||||
CategoryListContainer.AddChild(catButton);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
CurrentBuyer = null;
|
||||
_withdrawWindow?.Close();
|
||||
}
|
||||
|
||||
private sealed class StoreCategoryButton : Button
|
||||
{
|
||||
public string? Id;
|
||||
}
|
||||
}
|
||||
16
Content.Client/Store/Ui/StoreWithdrawWindow.xaml
Normal file
16
Content.Client/Store/Ui/StoreWithdrawWindow.xaml
Normal file
@@ -0,0 +1,16 @@
|
||||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'store-ui-default-withdraw-text'}"
|
||||
MinSize="256 128">
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
<SliderIntInput Name="WithdrawSlider" HorizontalExpand="True" />
|
||||
<BoxContainer
|
||||
Name="ButtonContainer"
|
||||
VerticalAlignment="Bottom"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
102
Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
Normal file
102
Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Localization;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Store;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Client.Graphics;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Store.Ui;
|
||||
|
||||
/// <summary>
|
||||
/// Window to select amount TC to withdraw from Uplink account
|
||||
/// Used as sub-window in Uplink UI
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StoreWithdrawWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private Dictionary<FixedPoint2, CurrencyPrototype> _validCurrencies = new();
|
||||
private HashSet<CurrencyWithdrawButton> _buttons = new();
|
||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
||||
|
||||
public StoreWithdrawWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public void CreateCurrencyButtons(Dictionary<string, FixedPoint2> balance)
|
||||
{
|
||||
_validCurrencies.Clear();
|
||||
foreach (var currency in balance)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<CurrencyPrototype>(currency.Key, out var proto))
|
||||
continue;
|
||||
|
||||
_validCurrencies.Add(currency.Value, proto);
|
||||
}
|
||||
|
||||
//this shouldn't ever happen but w/e
|
||||
if (_validCurrencies.Count < 1)
|
||||
return;
|
||||
|
||||
ButtonContainer.Children.Clear();
|
||||
_buttons.Clear();
|
||||
foreach (var currency in _validCurrencies)
|
||||
{
|
||||
Logger.Debug((currency.Value.PriceDisplay));
|
||||
var button = new CurrencyWithdrawButton()
|
||||
{
|
||||
Id = currency.Value.ID,
|
||||
Amount = currency.Key,
|
||||
MinHeight = 20,
|
||||
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.PriceDisplay))),
|
||||
};
|
||||
button.Disabled = false;
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
|
||||
Close();
|
||||
};
|
||||
|
||||
_buttons.Add(button);
|
||||
ButtonContainer.AddChild(button);
|
||||
}
|
||||
|
||||
var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
|
||||
|
||||
// setup withdraw slider
|
||||
WithdrawSlider.MinValue = 1;
|
||||
WithdrawSlider.MaxValue = maxWithdrawAmount;
|
||||
|
||||
WithdrawSlider.OnValueChanged += OnValueChanged;
|
||||
OnValueChanged(WithdrawSlider.Value);
|
||||
}
|
||||
|
||||
public void OnValueChanged(int i)
|
||||
{
|
||||
foreach (var button in _buttons)
|
||||
{
|
||||
button.Disabled = button.Amount < WithdrawSlider.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CurrencyWithdrawButton : Button
|
||||
{
|
||||
public string? Id;
|
||||
public FixedPoint2 Amount = FixedPoint2.Zero;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Traitor.Uplink
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class UplinkBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private UplinkMenu? _menu;
|
||||
|
||||
public UplinkBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
_menu = new UplinkMenu();
|
||||
_menu.OpenCentered();
|
||||
_menu.OnClose += Close;
|
||||
|
||||
_menu.OnListingButtonPressed += (_, listing) =>
|
||||
{
|
||||
SendMessage(new UplinkBuyListingMessage(listing.ItemId));
|
||||
};
|
||||
|
||||
_menu.OnCategoryButtonPressed += (_, category) =>
|
||||
{
|
||||
_menu.CurrentFilterCategory = category;
|
||||
SendMessage(new UplinkRequestUpdateInterfaceMessage());
|
||||
};
|
||||
|
||||
_menu.OnWithdrawAttempt += (tc) =>
|
||||
{
|
||||
SendMessage(new UplinkTryWithdrawTC(tc));
|
||||
};
|
||||
}
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (_menu == null)
|
||||
return;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case UplinkUpdateState msg:
|
||||
_menu.UpdateAccount(msg.Account);
|
||||
_menu.UpdateListing(msg.Listings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_menu?.Close();
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="8 8 8 8">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="UplinkItemName"
|
||||
HorizontalExpand="True"/>
|
||||
<Button Name="UplinkItemBuyButton"
|
||||
Access="Public"
|
||||
HorizontalAlignment="Right"
|
||||
MinWidth="64"/>
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="HighDivider" />
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<TextureRect Name="UplinkItemTexture"
|
||||
MinSize="48 48"
|
||||
Margin="0 0 4 0"
|
||||
Stretch="KeepAspectCentered"/>
|
||||
<RichTextLabel Name="UplinkItemDescription" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -1,28 +0,0 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Traitor.Uplink
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class UplinkListingControl : Control
|
||||
{
|
||||
|
||||
public UplinkListingControl(string itemName, string itemDescription,
|
||||
int itemPrice, bool canBuy, Texture? texture = null)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
UplinkItemName.Text = itemName;
|
||||
UplinkItemDescription.SetMessage(itemDescription);
|
||||
|
||||
UplinkItemBuyButton.Text = $"{itemPrice} TC";
|
||||
UplinkItemBuyButton.Disabled = !canBuy;
|
||||
|
||||
UplinkItemTexture.Texture = texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'uplink-user-interface-title'}"
|
||||
MinSize="512 512"
|
||||
SetSize="512 512">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="4 4 4 4">
|
||||
<RichTextLabel Name="BalanceInfo"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Left" />
|
||||
<Button Name="WithdrawButton"
|
||||
Text="{Loc 'uplink-user-interface-withdraw-button'}"
|
||||
HorizontalAlignment="Right"
|
||||
MinWidth="64"/>
|
||||
</BoxContainer>
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True">
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#80808005" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Name="CategoryListContainer"
|
||||
Orientation="Vertical">
|
||||
<!-- Category buttons are added here by code -->
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<ScrollContainer Name="UplinkListingsScroll"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HScrollEnabled="False"
|
||||
MinSize="100 256">
|
||||
<BoxContainer Name="UplinkListingsContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="2"
|
||||
MinSize="100 256">
|
||||
<!-- Listings are added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
@@ -1,163 +0,0 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Traitor.Uplink
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class UplinkMenu : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
private UplinkWithdrawWindow? _withdrawWindow;
|
||||
|
||||
public event Action<BaseButton.ButtonEventArgs, UplinkListingData>? OnListingButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs, UplinkCategory>? OnCategoryButtonPressed;
|
||||
public event Action<int>? OnWithdrawAttempt;
|
||||
|
||||
private UplinkCategory _currentFilter;
|
||||
private UplinkAccountData? _loggedInUplinkAccount;
|
||||
|
||||
public UplinkMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
PopulateUplinkCategoryButtons();
|
||||
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
||||
}
|
||||
|
||||
public UplinkCategory CurrentFilterCategory
|
||||
{
|
||||
get => _currentFilter;
|
||||
set
|
||||
{
|
||||
if (value.GetType() != typeof(UplinkCategory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentFilter = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAccount(UplinkAccountData account)
|
||||
{
|
||||
_loggedInUplinkAccount = account;
|
||||
|
||||
// update balance label
|
||||
var balance = account.DataBalance;
|
||||
var weightedColor = balance switch
|
||||
{
|
||||
<= 0 => "gray",
|
||||
<= 5 => "green",
|
||||
<= 20 => "yellow",
|
||||
<= 50 => "purple",
|
||||
_ => "gray"
|
||||
};
|
||||
var balanceStr = Loc.GetString("uplink-bound-user-interface-tc-balance-popup",
|
||||
("weightedColor", weightedColor),
|
||||
("balance", balance));
|
||||
BalanceInfo.SetMarkup(balanceStr);
|
||||
|
||||
// you can't withdraw if you don't have TC
|
||||
WithdrawButton.Disabled = balance <= 0;
|
||||
}
|
||||
|
||||
public void UpdateListing(UplinkListingData[] listings)
|
||||
{
|
||||
// should probably chunk these out instead. to-do if this clogs the internet tubes.
|
||||
// maybe read clients prototypes instead?
|
||||
ClearListings();
|
||||
foreach (var item in listings)
|
||||
{
|
||||
AddListingGui(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (_loggedInUplinkAccount == null)
|
||||
return;
|
||||
|
||||
// check if window is already open
|
||||
if (_withdrawWindow != null && _withdrawWindow.IsOpen)
|
||||
{
|
||||
_withdrawWindow.MoveToFront();
|
||||
return;
|
||||
}
|
||||
|
||||
// open a new one
|
||||
_withdrawWindow = new UplinkWithdrawWindow(_loggedInUplinkAccount.DataBalance);
|
||||
_withdrawWindow.OpenCentered();
|
||||
|
||||
_withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
|
||||
}
|
||||
|
||||
private void AddListingGui(UplinkListingData listing)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(listing.ItemId, out EntityPrototype? prototype) || listing.Category != CurrentFilterCategory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var listingName = listing.ListingName == string.Empty ? prototype.Name : listing.ListingName;
|
||||
var listingDesc = listing.Description == string.Empty ? prototype.Description : listing.Description;
|
||||
var listingPrice = listing.Price;
|
||||
var canBuy = _loggedInUplinkAccount?.DataBalance >= listing.Price;
|
||||
|
||||
var texture = listing.Icon?.Frame0();
|
||||
if (texture == null)
|
||||
texture = SpriteComponent.GetPrototypeIcon(prototype, _resourceCache).Default;
|
||||
|
||||
var newListing = new UplinkListingControl(listingName, listingDesc, listingPrice, canBuy, texture);
|
||||
newListing.UplinkItemBuyButton.OnButtonDown += args
|
||||
=> OnListingButtonPressed?.Invoke(args, listing);
|
||||
|
||||
UplinkListingsContainer.AddChild(newListing);
|
||||
}
|
||||
|
||||
private void ClearListings()
|
||||
{
|
||||
UplinkListingsContainer.Children.Clear();
|
||||
}
|
||||
|
||||
private void PopulateUplinkCategoryButtons()
|
||||
{
|
||||
foreach (UplinkCategory cat in Enum.GetValues(typeof(UplinkCategory)))
|
||||
{
|
||||
var catButton = new PDAUplinkCategoryButton
|
||||
{
|
||||
Text = Loc.GetString(cat.ToString()),
|
||||
ButtonCategory = cat
|
||||
};
|
||||
//It'd be neat if it could play a cool tech ping sound when you switch categories,
|
||||
//but right now there doesn't seem to be an easy way to do client-side audio without still having to round trip to the server and
|
||||
//send to a specific client INetChannel.
|
||||
catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.ButtonCategory);
|
||||
|
||||
CategoryListContainer.AddChild(catButton);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
_withdrawWindow?.Close();
|
||||
}
|
||||
|
||||
private sealed class PDAUplinkCategoryButton : Button
|
||||
{
|
||||
public UplinkCategory ButtonCategory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'uplink-user-interface-withdraw-title'}"
|
||||
MinSize="256 128">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<SliderIntInput Name="WithdrawSlider"
|
||||
HorizontalExpand="True"/>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True"
|
||||
VerticalAlignment="Bottom">
|
||||
<Button Name="ApplyButton"
|
||||
Text="{Loc 'uplink-user-interface-withdraw-withdraw-button'}"
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalExpand="True"/>
|
||||
<Button Name="CancelButton"
|
||||
Text="{Loc 'uplink-user-interface-withdraw-cancel-button'}"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
@@ -1,34 +0,0 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Client.Traitor.Uplink
|
||||
{
|
||||
/// <summary>
|
||||
/// Window to select amount TC to withdraw from Uplink account
|
||||
/// Used as sub-window in Uplink UI
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class UplinkWithdrawWindow : DefaultWindow
|
||||
{
|
||||
public event System.Action<int>? OnWithdrawAttempt;
|
||||
|
||||
public UplinkWithdrawWindow(int tcCount)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// setup withdraw slider
|
||||
WithdrawSlider.MinValue = 1;
|
||||
WithdrawSlider.MaxValue = tcCount;
|
||||
|
||||
// and buttons
|
||||
ApplyButton.OnButtonDown += _ =>
|
||||
{
|
||||
OnWithdrawAttempt?.Invoke(WithdrawSlider.Value);
|
||||
Close();
|
||||
};
|
||||
CancelButton.OnButtonDown += _ => Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Server.Traitor;
|
||||
using System.Data;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
@@ -35,6 +37,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
|
||||
private Dictionary<Mind.Mind, bool> _aliveNukeops = new();
|
||||
private bool _opsWon;
|
||||
|
||||
@@ -8,7 +8,6 @@ using Content.Server.Station.Components;
|
||||
using Content.Server.Suspicion;
|
||||
using Content.Server.Suspicion.Roles;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.EntityList;
|
||||
@@ -17,7 +16,6 @@ using Content.Shared.Maps;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Suspicion;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -48,6 +46,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
|
||||
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
|
||||
public override string Prototype => "Suspicion";
|
||||
|
||||
@@ -173,16 +172,8 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
|
||||
mind!.AddRole(traitorRole);
|
||||
traitors.Add(traitorRole);
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already, so we just need to
|
||||
// initiate uplink account.
|
||||
var uplinkAccount = new UplinkAccount(traitorStartingBalance, mind.OwnedEntity!);
|
||||
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
// try to place uplink
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
if (!_uplink.AddUplink(mind.OwnedEntity!.Value, traitorStartingBalance))
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Rules.Configurations;
|
||||
@@ -6,19 +6,18 @@ using Content.Server.Hands.Components;
|
||||
using Content.Server.PDA;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
using Content.Server.TraitorDeathMatch.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -39,6 +38,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
|
||||
public override string Prototype => "TraitorDeathMatch";
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
|
||||
|
||||
private bool _safeToEndRound = false;
|
||||
|
||||
private readonly Dictionary<UplinkAccount, string> _allOriginalNames = new();
|
||||
private readonly Dictionary<EntityUid, string> _allOriginalNames = new();
|
||||
|
||||
private const string TraitorPrototypeID = "Traitor";
|
||||
|
||||
@@ -108,15 +108,10 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
|
||||
newTmp = Spawn(BackpackPrototypeName, ownedCoords);
|
||||
_inventory.TryEquip(owned, newTmp, "back", true);
|
||||
|
||||
// Like normal traitors, they need access to a traitor account.
|
||||
var uplinkAccount = new UplinkAccount(startingBalance, owned);
|
||||
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
if (!_uplink.AddUplink(owned, startingBalance))
|
||||
return;
|
||||
|
||||
EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(owned, uplinkAccount, newPDA);
|
||||
|
||||
_allOriginalNames[uplinkAccount] = Name(owned);
|
||||
_allOriginalNames[owned] = Name(owned);
|
||||
|
||||
// The PDA needs to be marked with the correct owner.
|
||||
var pda = Comp<PDAComponent>(newPDA);
|
||||
@@ -186,14 +181,17 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
|
||||
|
||||
var lines = new List<string>();
|
||||
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
|
||||
foreach (var uplink in EntityManager.EntityQuery<UplinkComponent>(true))
|
||||
|
||||
foreach (var uplink in EntityManager.EntityQuery<StoreComponent>(true))
|
||||
{
|
||||
var uplinkAcc = uplink.UplinkAccount;
|
||||
if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc))
|
||||
var owner = uplink.AccountOwner;
|
||||
if (owner != null && _allOriginalNames.ContainsKey(owner.Value))
|
||||
{
|
||||
var tcbalance = _uplink.GetTCBalance(uplink);
|
||||
|
||||
lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
|
||||
("originalName", _allOriginalNames[uplinkAcc]),
|
||||
("tcBalance", uplinkAcc.Balance)));
|
||||
("originalName", _allOriginalNames[owner.Value]),
|
||||
("tcBalance", tcbalance)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ using Content.Server.Chat.Managers;
|
||||
using Content.Server.Objectives.Interfaces;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -28,6 +29,10 @@ public sealed class TraitorRuleSystem : GameRuleSystem
|
||||
[Dependency] private readonly IObjectivesManager _objectivesManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
|
||||
public override string Prototype => "Traitor";
|
||||
|
||||
@@ -35,6 +40,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
|
||||
public List<TraitorRole> Traitors = new();
|
||||
|
||||
private const string TraitorPrototypeID = "Traitor";
|
||||
private const string TraitorUplinkPresetId = "StorePresetUplink";
|
||||
|
||||
public int TotalTraitors => Traitors.Count;
|
||||
public string[] Codewords = new string[3];
|
||||
@@ -173,16 +179,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem
|
||||
}
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already, so we just need to
|
||||
// initiate uplink account.
|
||||
// PDA should be in place already
|
||||
DebugTools.AssertNotNull(mind.OwnedEntity);
|
||||
|
||||
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
|
||||
var uplinkAccount = new UplinkAccount(startingBalance, mind.OwnedEntity!);
|
||||
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>().AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
if (!_uplink.AddUplink(mind.OwnedEntity!.Value, startingBalance))
|
||||
return false;
|
||||
|
||||
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);
|
||||
|
||||
@@ -2,29 +2,28 @@ using Content.Server.Instruments;
|
||||
using Content.Server.Light.Components;
|
||||
using Content.Server.Light.EntitySystems;
|
||||
using Content.Server.Light.Events;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
using Content.Server.PDA.Ringer;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.PDA;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Traitor;
|
||||
|
||||
namespace Content.Server.PDA
|
||||
{
|
||||
public sealed class PDASystem : SharedPDASystem
|
||||
{
|
||||
[Dependency] private readonly UplinkSystem _uplinkSystem = default!;
|
||||
[Dependency] private readonly UplinkAccountsSystem _uplinkAccounts = default!;
|
||||
[Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
|
||||
[Dependency] private readonly RingerSystem _ringerSystem = default!;
|
||||
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly StoreSystem _storeSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -32,8 +31,8 @@ namespace Content.Server.PDA
|
||||
|
||||
SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle);
|
||||
SubscribeLocalEvent<PDAComponent, AfterActivatableUIOpenEvent>(AfterUIOpen);
|
||||
SubscribeLocalEvent<PDAComponent, UplinkInitEvent>(OnUplinkInit);
|
||||
SubscribeLocalEvent<PDAComponent, UplinkRemovedEvent>(OnUplinkRemoved);
|
||||
SubscribeLocalEvent<PDAComponent, StoreAddedEvent>(OnUplinkInit);
|
||||
SubscribeLocalEvent<PDAComponent, StoreRemovedEvent>(OnUplinkRemoved);
|
||||
SubscribeLocalEvent<PDAComponent, GridModifiedEvent>(OnGridChanged);
|
||||
}
|
||||
|
||||
@@ -74,12 +73,12 @@ namespace Content.Server.PDA
|
||||
UpdatePDAUserInterface(pda);
|
||||
}
|
||||
|
||||
private void OnUplinkInit(EntityUid uid, PDAComponent pda, UplinkInitEvent args)
|
||||
private void OnUplinkInit(EntityUid uid, PDAComponent pda, StoreAddedEvent args)
|
||||
{
|
||||
UpdatePDAUserInterface(pda);
|
||||
}
|
||||
|
||||
private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, UplinkRemovedEvent args)
|
||||
private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, StoreRemovedEvent args)
|
||||
{
|
||||
UpdatePDAUserInterface(pda);
|
||||
}
|
||||
@@ -111,7 +110,7 @@ namespace Content.Server.PDA
|
||||
// players. This should really use a sort of key-code entry system that selects an account which is not directly tied to
|
||||
// a player entity.
|
||||
|
||||
if (!HasComp<UplinkComponent>(pda.Owner))
|
||||
if (!TryComp<StoreComponent>(pda.Owner, out var storeComponent))
|
||||
return;
|
||||
|
||||
var uplinkState = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, hasInstrument);
|
||||
@@ -121,7 +120,8 @@ namespace Content.Server.PDA
|
||||
if (session.AttachedEntity is not EntityUid { Valid: true } user)
|
||||
continue;
|
||||
|
||||
if (_uplinkAccounts.HasAccount(user))
|
||||
if (storeComponent.AccountOwner == user || (TryComp<MindComponent>(session.AttachedEntity, out var mindcomp) && mindcomp.Mind != null &&
|
||||
mindcomp.Mind.HasRole<TraitorRole>()))
|
||||
ui.SetState(uplinkState, session);
|
||||
}
|
||||
}
|
||||
@@ -143,8 +143,9 @@ namespace Content.Server.PDA
|
||||
|
||||
case PDAShowUplinkMessage _:
|
||||
{
|
||||
if (EntityManager.TryGetComponent(pda.Owner, out UplinkComponent? uplink))
|
||||
_uplinkSystem.ToggleUplinkUI(uplink, msg.Session);
|
||||
if (msg.Session.AttachedEntity != null &&
|
||||
TryComp<StoreComponent>(pda.Owner, out var store))
|
||||
_storeSystem.ToggleUi(msg.Session.AttachedEntity.Value, store);
|
||||
break;
|
||||
}
|
||||
case PDAShowRingtoneMessage _:
|
||||
@@ -170,8 +171,13 @@ namespace Content.Server.PDA
|
||||
|
||||
private void AfterUIOpen(EntityUid uid, PDAComponent pda, AfterActivatableUIOpenEvent args)
|
||||
{
|
||||
//TODO: this is awful
|
||||
// A new user opened the UI --> Check if they are a traitor and should get a user specific UI state override.
|
||||
if (!HasComp<UplinkComponent>(pda.Owner) || !_uplinkAccounts.HasAccount(args.User))
|
||||
if (!TryComp<StoreComponent>(pda.Owner, out var storeComp))
|
||||
return;
|
||||
|
||||
if (storeComp.AccountOwner != args.User &&
|
||||
!(TryComp<MindComponent>(args.User, out var mindcomp) && mindcomp.Mind != null && mindcomp.Mind.HasRole<TraitorRole>()))
|
||||
return;
|
||||
|
||||
if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui))
|
||||
|
||||
22
Content.Server/Store/Components/CurrencyComponent.cs
Normal file
22
Content.Server/Store/Components/CurrencyComponent.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Store;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
|
||||
namespace Content.Server.Store.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a component that can be inserted into a store
|
||||
/// to increase its balance.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class CurrencyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The value of the currency.
|
||||
/// The string is the currency type that will be added.
|
||||
/// The FixedPoint2 is the value of each individual currency entity.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("price", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
|
||||
public Dictionary<string, FixedPoint2> Price = new();
|
||||
}
|
||||
91
Content.Server/Store/Components/StoreComponent.cs
Normal file
91
Content.Server/Store/Components/StoreComponent.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Store;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Store.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component manages a store which players can use to purchase different listings
|
||||
/// through the ui. The currency, listings, and categories are defined in yaml.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class StoreComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The default preset for the store. Is overriden by default values specified on the component.
|
||||
/// </summary>
|
||||
[DataField("preset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))]
|
||||
public string? Preset;
|
||||
|
||||
/// <summary>
|
||||
/// All the listing categories that are available on this store.
|
||||
/// The available listings are partially based on the categories.
|
||||
/// </summary>
|
||||
[DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StoreCategoryPrototype>))]
|
||||
public HashSet<string> Categories = new();
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of currency that can be used in the store.
|
||||
/// The string represents the ID of te currency prototype, where the
|
||||
/// float is that amount.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("balance", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
|
||||
public Dictionary<string, FixedPoint2> Balance = new();
|
||||
|
||||
/// <summary>
|
||||
/// The list of currencies that can be inserted into this store.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly), DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<CurrencyPrototype>))]
|
||||
public HashSet<string> CurrencyWhitelist = new();
|
||||
|
||||
/// <summary>
|
||||
/// The person who "owns" the store/account. Used if you want the listings to be fixed
|
||||
/// regardless of who activated it. I.E. role specific items for uplinks.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityUid? AccountOwner = null;
|
||||
|
||||
/// <summary>
|
||||
/// All listings, including those that aren't available to the buyer
|
||||
/// </summary>
|
||||
public HashSet<ListingData> Listings = new();
|
||||
|
||||
/// <summary>
|
||||
/// All available listings from the last time that it was checked.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HashSet<ListingData> LastAvailableListings = new();
|
||||
|
||||
/// <summary>
|
||||
/// checks whether or not the store has been opened yet.
|
||||
/// </summary>
|
||||
public bool Opened = false;
|
||||
|
||||
#region audio
|
||||
/// <summary>
|
||||
/// The sound played to the buyer when a purchase is succesfully made.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("buySuccessSound")]
|
||||
public SoundSpecifier BuySuccessSound = new SoundPathSpecifier("/Audio/Effects/kaching.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The sound played to the buyer when a purchase fails.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("insufficientFundsSound")]
|
||||
public SoundSpecifier InsufficientFundsSound = new SoundPathSpecifier("/Audio/Effects/error.ogg");
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that is broadcast when a store is added to an entity
|
||||
/// </summary>
|
||||
public sealed class StoreAddedEvent : EntityEventArgs { };
|
||||
/// <summary>
|
||||
/// Event that is broadcast when a store is removed from an entity
|
||||
/// </summary>
|
||||
public sealed class StoreRemovedEvent : EntityEventArgs { };
|
||||
63
Content.Server/Store/Conditions/BuyerAntagCondition.cs
Normal file
63
Content.Server/Store/Conditions/BuyerAntagCondition.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Shared.Store.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Allows a store entry to be filtered out based on the user's antag role.
|
||||
/// Supports both blacklists and whitelists. This is copypaste because roles
|
||||
/// are absolute shitcode. Refactor this later. -emo
|
||||
/// </summary>
|
||||
public sealed class BuyerAntagCondition : ListingCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// A whitelist of antag roles that can purchase this listing. Only one needs to be found.
|
||||
/// </summary>
|
||||
[DataField("whitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AntagPrototype>))]
|
||||
public HashSet<string>? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// A blacklist of antag roles that cannot purchase this listing. Only one needs to be found.
|
||||
/// </summary>
|
||||
[DataField("blacklist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AntagPrototype>))]
|
||||
public HashSet<string>? Blacklist;
|
||||
|
||||
public override bool Condition(ListingConditionArgs args)
|
||||
{
|
||||
var ent = args.EntityManager;
|
||||
|
||||
if (!ent.TryGetComponent<MindComponent>(args.Buyer, out var mind) || mind.Mind == null)
|
||||
return true;
|
||||
|
||||
if (Blacklist != null)
|
||||
{
|
||||
foreach (var role in mind.Mind.AllRoles)
|
||||
{
|
||||
if (role is not TraitorRole blacklistantag)
|
||||
continue;
|
||||
|
||||
if (Blacklist.Contains(blacklistantag.Prototype.ID))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Whitelist != null)
|
||||
{
|
||||
var found = false;
|
||||
foreach (var role in mind.Mind.AllRoles)
|
||||
{
|
||||
if (role is not TraitorRole antag)
|
||||
continue;
|
||||
|
||||
if (Whitelist.Contains(antag.Prototype.ID))
|
||||
found = true;
|
||||
}
|
||||
if (!found)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
63
Content.Server/Store/Conditions/BuyerJobCondition.cs
Normal file
63
Content.Server/Store/Conditions/BuyerJobCondition.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Store;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Server.Store.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Allows a store entry to be filtered out based on the user's job.
|
||||
/// Supports both blacklists and whitelists
|
||||
/// </summary>
|
||||
public sealed class BuyerJobCondition : ListingCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// A whitelist of jobs prototypes that can purchase this listing. Only one needs to be found.
|
||||
/// </summary>
|
||||
[DataField("whitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
|
||||
public HashSet<string>? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// A blacklist of job prototypes that can purchase this listing. Only one needs to be found.
|
||||
/// </summary>
|
||||
[DataField("blacklist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
|
||||
public HashSet<string>? Blacklist;
|
||||
|
||||
public override bool Condition(ListingConditionArgs args)
|
||||
{
|
||||
var ent = args.EntityManager;
|
||||
|
||||
if (!ent.TryGetComponent<MindComponent>(args.Buyer, out var mind) || mind.Mind == null)
|
||||
return true; //this is for things like surplus crate
|
||||
|
||||
if (Blacklist != null)
|
||||
{
|
||||
foreach (var role in mind.Mind.AllRoles)
|
||||
{
|
||||
if (role is not Job job)
|
||||
continue;
|
||||
|
||||
if (Blacklist.Contains(job.Prototype.ID))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Whitelist != null)
|
||||
{
|
||||
var found = false;
|
||||
foreach (var role in mind.Mind.AllRoles)
|
||||
{
|
||||
if (role is not Job job)
|
||||
continue;
|
||||
|
||||
if (Whitelist.Contains(job.Prototype.ID))
|
||||
found = true;
|
||||
}
|
||||
if (!found)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
41
Content.Server/Store/Conditions/BuyerWhitelistCondition.cs
Normal file
41
Content.Server/Store/Conditions/BuyerWhitelistCondition.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.Store.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Filters out an entry based on the components or tags on an entity.
|
||||
/// </summary>
|
||||
public sealed class BuyerWhitelistCondition : ListingCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// A whitelist of tags or components.
|
||||
/// </summary>
|
||||
[DataField("whitelist")]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// A blacklist of tags or components.
|
||||
/// </summary>
|
||||
[DataField("blacklist")]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
public override bool Condition(ListingConditionArgs args)
|
||||
{
|
||||
var ent = args.EntityManager;
|
||||
|
||||
if (Whitelist != null)
|
||||
{
|
||||
if (!Whitelist.IsValid(args.Buyer, ent))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Blacklist != null)
|
||||
{
|
||||
if (Blacklist.IsValid(args.Buyer, ent))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.Store;
|
||||
|
||||
namespace Content.Server.Store.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Only allows a listing to be purchased a certain amount of times.
|
||||
/// </summary>
|
||||
public sealed class ListingLimitedStockCondition : ListingCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of times this listing can be purchased.
|
||||
/// </summary>
|
||||
[DataField("stock", required: true)]
|
||||
public int Stock;
|
||||
|
||||
public override bool Condition(ListingConditionArgs args)
|
||||
{
|
||||
return args.Listing.PurchaseAmount < Stock;
|
||||
}
|
||||
}
|
||||
44
Content.Server/Store/Conditions/StoreWhitelistCondition.cs
Normal file
44
Content.Server/Store/Conditions/StoreWhitelistCondition.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.Store.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Filters out an entry based on the components or tags on the store itself.
|
||||
/// </summary>
|
||||
public sealed class StoreWhitelistCondition : ListingCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// A whitelist of tags or components.
|
||||
/// </summary>
|
||||
[DataField("whitelist")]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// A blacklist of tags or components.
|
||||
/// </summary>
|
||||
[DataField("blacklist")]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
public override bool Condition(ListingConditionArgs args)
|
||||
{
|
||||
if (args.StoreEntity == null)
|
||||
return false;
|
||||
|
||||
var ent = args.EntityManager;
|
||||
|
||||
if (Whitelist != null)
|
||||
{
|
||||
if (!Whitelist.IsValid(args.StoreEntity.Value, ent))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Blacklist != null)
|
||||
{
|
||||
if (Blacklist.IsValid(args.StoreEntity.Value, ent))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
127
Content.Server/Store/Systems/StoreSystem.Listings.cs
Normal file
127
Content.Server/Store/Systems/StoreSystem.Listings.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Shared.Store;
|
||||
|
||||
namespace Content.Server.Store.Systems;
|
||||
|
||||
public sealed partial class StoreSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Refreshes all listings on a store.
|
||||
/// Do not use if you don't know what you're doing.
|
||||
/// </summary>
|
||||
/// <param name="component">The store to refresh</param>
|
||||
public void RefreshAllListings(StoreComponent component)
|
||||
{
|
||||
component.Listings = GetAllListings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all listings from a prototype.
|
||||
/// </summary>
|
||||
/// <returns>All the listings</returns>
|
||||
public HashSet<ListingData> GetAllListings()
|
||||
{
|
||||
var allListings = _proto.EnumeratePrototypes<ListingPrototype>();
|
||||
|
||||
var allData = new HashSet<ListingData>();
|
||||
|
||||
foreach (var listing in allListings)
|
||||
allData.Add(listing);
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listing from an Id to a store
|
||||
/// </summary>
|
||||
/// <param name="component">The store to add the listing to</param>
|
||||
/// <param name="listingId">The id of the listing</param>
|
||||
/// <returns>Whetehr or not the listing was added successfully</returns>
|
||||
public bool TryAddListing(StoreComponent component, string listingId)
|
||||
{
|
||||
if (!_proto.TryIndex<ListingPrototype>(listingId, out var proto))
|
||||
{
|
||||
Logger.Error("Attempted to add invalid listing.");
|
||||
return false;
|
||||
}
|
||||
return TryAddListing(component, proto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listing to a store
|
||||
/// </summary>
|
||||
/// <param name="component">The store to add the listing to</param>
|
||||
/// <param name="listing">The listing</param>
|
||||
/// <returns>Whether or not the listing was add successfully</returns>
|
||||
public bool TryAddListing(StoreComponent component, ListingData listing)
|
||||
{
|
||||
return component.Listings.Add(listing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the available listings for a store
|
||||
/// </summary>
|
||||
/// <param name="user">The person getting the listings.</param>
|
||||
/// <param name="component">The store the listings are coming from.</param>
|
||||
/// <returns>The available listings.</returns>
|
||||
public IEnumerable<ListingData> GetAvailableListings(EntityUid user, StoreComponent component)
|
||||
{
|
||||
return GetAvailableListings(user, component.Listings, component.Categories, component.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the available listings for a user given an overall set of listings and categories to filter by.
|
||||
/// </summary>
|
||||
/// <param name="user">The person getting the listings.</param>
|
||||
/// <param name="listings">All of the listings that are available. If null, will just get all listings from the prototypes.</param>
|
||||
/// <param name="categories">What categories to filter by.</param>
|
||||
/// <param name="storeEntity">The physial entity of the store. Can be null.</param>
|
||||
/// <returns>The available listings.</returns>
|
||||
public IEnumerable<ListingData> GetAvailableListings(EntityUid user, HashSet<ListingData>? listings, HashSet<string> categories, EntityUid? storeEntity = null)
|
||||
{
|
||||
if (listings == null)
|
||||
listings = GetAllListings();
|
||||
|
||||
foreach (var listing in listings)
|
||||
{
|
||||
if (!ListingHasCategory(listing, categories))
|
||||
continue;
|
||||
|
||||
if (listing.Conditions != null)
|
||||
{
|
||||
var args = new ListingConditionArgs(user, storeEntity, listing, EntityManager);
|
||||
var conditionsMet = true;
|
||||
|
||||
foreach (var condition in listing.Conditions)
|
||||
{
|
||||
if (!condition.Condition(args))
|
||||
{
|
||||
conditionsMet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conditionsMet)
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return listing;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a listing appears in a list of given categories
|
||||
/// </summary>
|
||||
/// <param name="listing">The listing itself.</param>
|
||||
/// <param name="categories">The categories to check through.</param>
|
||||
/// <returns>If the listing was present in one of the categories.</returns>
|
||||
public bool ListingHasCategory(ListingData listing, HashSet<string> categories)
|
||||
{
|
||||
foreach (var cat in categories)
|
||||
{
|
||||
if (listing.Categories.Contains(cat))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
226
Content.Server/Store/Systems/StoreSystem.Ui.cs
Normal file
226
Content.Server/Store/Systems/StoreSystem.Ui.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Server.GameObjects;
|
||||
using System.Linq;
|
||||
using Content.Server.Stack;
|
||||
using Content.Shared.Prototypes;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Store.Systems;
|
||||
|
||||
public sealed partial class StoreSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _admin = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly StackSystem _stack = default!;
|
||||
|
||||
private void InitializeUi()
|
||||
{
|
||||
SubscribeLocalEvent<StoreComponent, StoreRequestUpdateInterfaceMessage>((_,c,r) => UpdateUserInterface(r.CurrentBuyer, c));
|
||||
SubscribeLocalEvent<StoreComponent, StoreBuyListingMessage>(OnBuyRequest);
|
||||
SubscribeLocalEvent<StoreComponent, StoreRequestWithdrawMessage>(OnRequestWithdraw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the store Ui open and closed
|
||||
/// </summary>
|
||||
/// <param name="user">the person doing the toggling</param>
|
||||
/// <param name="component">the store being toggled</param>
|
||||
public void ToggleUi(EntityUid user, StoreComponent component)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(user, out var actor))
|
||||
return;
|
||||
|
||||
var ui = component.Owner.GetUIOrNull(StoreUiKey.Key);
|
||||
ui?.Toggle(actor.PlayerSession);
|
||||
|
||||
UpdateUserInterface(user, component, ui);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the user interface for a store and refreshes the listings
|
||||
/// </summary>
|
||||
/// <param name="user">The person who if opening the store ui. Listings are filtered based on this.</param>
|
||||
/// <param name="component">The store component being refreshed.</param>
|
||||
/// <param name="ui"></param>
|
||||
public void UpdateUserInterface(EntityUid? user, StoreComponent component, BoundUserInterface? ui = null)
|
||||
{
|
||||
if (ui == null)
|
||||
{
|
||||
ui = component.Owner.GetUIOrNull(StoreUiKey.Key);
|
||||
if (ui == null)
|
||||
{
|
||||
Logger.Error("No Ui key.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//if we haven't opened it before, initialize the shit
|
||||
if (!component.Opened)
|
||||
{
|
||||
InitializeFromPreset(component.Preset, component);
|
||||
component.Opened = true;
|
||||
}
|
||||
|
||||
//this is the person who will be passed into logic for all listing filtering.
|
||||
var buyer = user;
|
||||
if (buyer != null) //if we have no "buyer" for this update, then don't update the listings
|
||||
{
|
||||
if (component.AccountOwner != null) //if we have one stored, then use that instead
|
||||
buyer = component.AccountOwner.Value;
|
||||
|
||||
component.LastAvailableListings = GetAvailableListings(buyer.Value, component).ToHashSet();
|
||||
}
|
||||
|
||||
//dictionary for all currencies, including 0 values for currencies on the whitelist
|
||||
Dictionary<string, FixedPoint2> allCurrency = new();
|
||||
foreach (var supported in component.CurrencyWhitelist)
|
||||
{
|
||||
allCurrency.Add(supported, FixedPoint2.Zero);
|
||||
|
||||
if (component.Balance.ContainsKey(supported))
|
||||
allCurrency[supported] = component.Balance[supported];
|
||||
}
|
||||
|
||||
var state = new StoreUpdateState(buyer, component.LastAvailableListings, allCurrency);
|
||||
ui.SetState(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles whenever a purchase was made.
|
||||
/// </summary>
|
||||
private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListingMessage msg)
|
||||
{
|
||||
ListingData? listing = component.Listings.FirstOrDefault(x => x.Equals(msg.Listing));
|
||||
if (listing == null) //make sure this listing actually exists
|
||||
{
|
||||
Logger.Debug("listing does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
//verify that we can actually buy this listing and it wasn't added
|
||||
if (!ListingHasCategory(listing, component.Categories))
|
||||
return;
|
||||
//condition checking because why not
|
||||
if (listing.Conditions != null)
|
||||
{
|
||||
var args = new ListingConditionArgs(msg.Buyer, component.Owner, listing, EntityManager);
|
||||
var conditionsMet = true;
|
||||
|
||||
foreach (var condition in listing.Conditions.Where(condition => !condition.Condition(args)))
|
||||
conditionsMet = false;
|
||||
|
||||
if (!conditionsMet)
|
||||
return;
|
||||
}
|
||||
|
||||
//check that we have enough money
|
||||
foreach (var currency in listing.Cost)
|
||||
{
|
||||
if (!component.Balance.TryGetValue(currency.Key, out var balance) || balance < currency.Value)
|
||||
{
|
||||
_audio.Play(component.InsufficientFundsSound, Filter.SinglePlayer(msg.Session), uid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//subtract the cash
|
||||
foreach (var currency in listing.Cost)
|
||||
component.Balance[currency.Key] -= currency.Value;
|
||||
|
||||
//spawn entity
|
||||
if (listing.ProductEntity != null)
|
||||
{
|
||||
var product = Spawn(listing.ProductEntity, Transform(msg.Buyer).Coordinates);
|
||||
_hands.TryPickupAnyHand(msg.Buyer, product);
|
||||
}
|
||||
|
||||
//give action
|
||||
if (listing.ProductAction != null)
|
||||
{
|
||||
var action = new InstantAction(_proto.Index<InstantActionPrototype>(listing.ProductAction));
|
||||
_actions.AddAction(msg.Buyer, action, null);
|
||||
}
|
||||
|
||||
//broadcast event
|
||||
if (listing.ProductEvent != null)
|
||||
{
|
||||
RaiseLocalEvent(listing.ProductEvent);
|
||||
}
|
||||
|
||||
//log dat shit.
|
||||
if (TryComp<MindComponent>(msg.Buyer, out var mind))
|
||||
{
|
||||
_admin.Add(LogType.StorePurchase, LogImpact.Low,
|
||||
$"{ToPrettyString(mind.Owner):player} purchased listing \"{listing.Name}\" from {ToPrettyString(uid)}");
|
||||
}
|
||||
|
||||
listing.PurchaseAmount++; //track how many times something has been purchased
|
||||
_audio.Play(component.BuySuccessSound, Filter.SinglePlayer(msg.Session), uid); //cha-ching!
|
||||
|
||||
UpdateUserInterface(msg.Buyer, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles dispensing the currency you requested to be withdrawn.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This would need to be done should a currency with decimal values need to use it.
|
||||
/// not quite sure how to handle that
|
||||
/// </remarks>
|
||||
private void OnRequestWithdraw(EntityUid uid, StoreComponent component, StoreRequestWithdrawMessage msg)
|
||||
{
|
||||
//make sure we have enough cash in the bank and we actually support this currency
|
||||
if (!component.Balance.TryGetValue(msg.Currency, out var currentAmount) || currentAmount < msg.Amount)
|
||||
return;
|
||||
|
||||
//make sure a malicious client didn't send us random shit
|
||||
if (!_proto.TryIndex<CurrencyPrototype>(msg.Currency, out var proto))
|
||||
return;
|
||||
|
||||
//we need an actually valid entity to spawn. This check has been done earlier, but just in case.
|
||||
if (proto.EntityId == null || !proto.CanWithdraw)
|
||||
return;
|
||||
|
||||
var entproto = _proto.Index<EntityPrototype>(proto.EntityId);
|
||||
|
||||
var amountRemaining = msg.Amount;
|
||||
var coordinates = Transform(msg.Buyer).Coordinates;
|
||||
if (entproto.HasComponent<StackComponent>())
|
||||
{
|
||||
while (amountRemaining > 0)
|
||||
{
|
||||
var ent = Spawn(proto.EntityId, coordinates);
|
||||
var stackComponent = Comp<StackComponent>(ent); //we already know it exists
|
||||
|
||||
var amountPerStack = Math.Min(stackComponent.MaxCount, amountRemaining);
|
||||
|
||||
_stack.SetCount(ent, amountPerStack, stackComponent);
|
||||
amountRemaining -= amountPerStack;
|
||||
_hands.TryPickupAnyHand(msg.Buyer, ent);
|
||||
}
|
||||
}
|
||||
else //please for the love of christ give your currency stack component
|
||||
{
|
||||
while (amountRemaining > 0)
|
||||
{
|
||||
var ent = Spawn(proto.EntityId, coordinates);
|
||||
_hands.TryPickupAnyHand(msg.Buyer, ent);
|
||||
amountRemaining--;
|
||||
}
|
||||
}
|
||||
|
||||
component.Balance[msg.Currency] -= msg.Amount;
|
||||
UpdateUserInterface(msg.Buyer, component);
|
||||
}
|
||||
}
|
||||
154
Content.Server/Store/Systems/StoreSystem.cs
Normal file
154
Content.Server/Store/Systems/StoreSystem.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Content.Server.Stack;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Store;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using Content.Server.UserInterface;
|
||||
|
||||
namespace Content.Server.Store.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Manages general interactions with a store and different entities,
|
||||
/// getting listings for stores, and interfacing with the store UI.
|
||||
/// </summary>
|
||||
public sealed partial class StoreSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CurrencyComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<StoreComponent, BeforeActivatableUIOpenEvent>((_,c,a) => UpdateUserInterface(a.User, c));
|
||||
|
||||
SubscribeLocalEvent<StoreComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<StoreComponent, ComponentShutdown>(OnShutdown);
|
||||
|
||||
InitializeUi();
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args)
|
||||
{
|
||||
RaiseLocalEvent(uid, new StoreAddedEvent(), true);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, StoreComponent component, ComponentShutdown args)
|
||||
{
|
||||
RaiseLocalEvent(uid, new StoreRemovedEvent(), true);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (args.Target == null || !TryComp<StoreComponent>(args.Target, out var store))
|
||||
return;
|
||||
|
||||
//if you somehow are inserting cash before the store initializes.
|
||||
if (!store.Opened)
|
||||
{
|
||||
InitializeFromPreset(store.Preset, store);
|
||||
store.Opened = true;
|
||||
}
|
||||
|
||||
args.Handled = TryAddCurrency(GetCurrencyValue(component), store);
|
||||
|
||||
if (args.Handled)
|
||||
{
|
||||
var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target));
|
||||
_popup.PopupEntity(msg, args.Target.Value, Filter.Pvs(args.Target.Value));
|
||||
QueueDel(args.Used);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value from an entity's currency component.
|
||||
/// Scales with stacks.
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
/// <returns>The value of the currency</returns>
|
||||
public Dictionary<string, FixedPoint2> GetCurrencyValue(CurrencyComponent component)
|
||||
{
|
||||
TryComp<StackComponent>(component.Owner, out var stack);
|
||||
var amount = stack?.Count ?? 1;
|
||||
|
||||
return component.Price.ToDictionary(v => v.Key, p => p.Value * amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a currency to a store's balance.
|
||||
/// </summary>
|
||||
/// <param name="component">The currency to add</param>
|
||||
/// <param name="store">The store to add it to</param>
|
||||
/// <returns>Whether or not the currency was succesfully added</returns>
|
||||
public bool TryAddCurrency(CurrencyComponent component, StoreComponent store)
|
||||
{
|
||||
return TryAddCurrency(GetCurrencyValue(component), store);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a currency to a store's balance
|
||||
/// </summary>
|
||||
/// <param name="currency">The value to add to the store</param>
|
||||
/// <param name="store">The store to add it to</param>
|
||||
/// <returns>Whether or not the currency was succesfully added</returns>
|
||||
public bool TryAddCurrency(Dictionary<string, FixedPoint2> currency, StoreComponent store)
|
||||
{
|
||||
//verify these before values are modified
|
||||
foreach (var type in currency)
|
||||
{
|
||||
if (!store.CurrencyWhitelist.Contains(type.Key))
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var type in currency)
|
||||
{
|
||||
if (!store.Balance.TryAdd(type.Key, type.Value))
|
||||
store.Balance[type.Key] += type.Value;
|
||||
}
|
||||
|
||||
UpdateUserInterface(null, store);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a store based on a preset ID
|
||||
/// </summary>
|
||||
/// <param name="preset">The ID of a store preset prototype</param>
|
||||
/// <param name="component">The store being initialized</param>
|
||||
public void InitializeFromPreset(string? preset, StoreComponent component)
|
||||
{
|
||||
if (preset == null)
|
||||
return;
|
||||
|
||||
if (!_proto.TryIndex<StorePresetPrototype>(preset, out var proto))
|
||||
return;
|
||||
|
||||
InitializeFromPreset(proto, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a store based on a given preset
|
||||
/// </summary>
|
||||
/// <param name="preset">The StorePresetPrototype</param>
|
||||
/// <param name="component">The store being initialized</param>
|
||||
public void InitializeFromPreset(StorePresetPrototype preset, StoreComponent component)
|
||||
{
|
||||
RefreshAllListings(component);
|
||||
component.Preset = preset.ID;
|
||||
component.CurrencyWhitelist.UnionWith(preset.CurrencyWhitelist);
|
||||
component.Categories.UnionWith(preset.Categories);
|
||||
if (component.Balance == new Dictionary<string, FixedPoint2>() && preset.InitialBalance != null) //if we don't have a value stored, use the preset
|
||||
TryAddCurrency(preset.InitialBalance, component);
|
||||
|
||||
var ui = component.Owner.GetUIOrNull(StoreUiKey.Key);
|
||||
ui?.SetState(new StoreInitializeState(preset.StoreName));
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes when one of the UplinkAccounts changed its TC balance
|
||||
/// </summary>
|
||||
public sealed class UplinkAccountBalanceChanged : EntityEventArgs
|
||||
{
|
||||
public readonly UplinkAccount Account;
|
||||
|
||||
/// <summary>
|
||||
/// Difference between NewBalance - OldBalance
|
||||
/// </summary>
|
||||
public readonly int Difference;
|
||||
|
||||
public readonly int NewBalance;
|
||||
public readonly int OldBalance;
|
||||
|
||||
public UplinkAccountBalanceChanged(UplinkAccount account, int difference)
|
||||
{
|
||||
Account = account;
|
||||
Difference = difference;
|
||||
|
||||
NewBalance = account.Balance;
|
||||
OldBalance = account.Balance - difference;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// Manage all registred uplink accounts and their balance
|
||||
/// </summary>
|
||||
public sealed class UplinkAccountsSystem : EntitySystem
|
||||
{
|
||||
public const string TelecrystalProtoId = "Telecrystal";
|
||||
|
||||
[Dependency]
|
||||
private readonly UplinkListingSytem _listingSystem = default!;
|
||||
[Dependency]
|
||||
private readonly SharedStackSystem _stackSystem = default!;
|
||||
|
||||
private readonly HashSet<UplinkAccount> _accounts = new();
|
||||
|
||||
public bool AddNewAccount(UplinkAccount acc)
|
||||
{
|
||||
return _accounts.Add(acc);
|
||||
}
|
||||
|
||||
public bool HasAccount(EntityUid holder) =>
|
||||
_accounts.Any(acct => acct.AccountHolder == holder);
|
||||
|
||||
/// <summary>
|
||||
/// Add TC to uplinks account balance
|
||||
/// </summary>
|
||||
public bool AddToBalance(UplinkAccount account, int toAdd)
|
||||
{
|
||||
account.Balance += toAdd;
|
||||
|
||||
RaiseLocalEvent(new UplinkAccountBalanceChanged(account, toAdd));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Charge TC from uplinks account balance
|
||||
/// </summary>
|
||||
public bool RemoveFromBalance(UplinkAccount account, int price)
|
||||
{
|
||||
if (account.Balance - price < 0)
|
||||
return false;
|
||||
|
||||
account.Balance -= price;
|
||||
|
||||
RaiseLocalEvent(new UplinkAccountBalanceChanged(account, -price));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force-set TC uplinks account balance to a new value
|
||||
/// </summary>
|
||||
public bool SetBalance(UplinkAccount account, int newBalance)
|
||||
{
|
||||
if (newBalance < 0)
|
||||
return false;
|
||||
|
||||
var dif = newBalance - account.Balance;
|
||||
account.Balance = newBalance;
|
||||
RaiseLocalEvent(new UplinkAccountBalanceChanged(account, dif));
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public bool TryPurchaseItem(UplinkAccount acc, string itemId, EntityCoordinates spawnCoords, [NotNullWhen(true)] out EntityUid? purchasedItem)
|
||||
{
|
||||
purchasedItem = null;
|
||||
|
||||
if (!_listingSystem.TryGetListing(itemId, out var listing))
|
||||
return false;
|
||||
|
||||
if (acc.Balance < listing.Price)
|
||||
return false;
|
||||
|
||||
if (!RemoveFromBalance(acc, listing.Price))
|
||||
return false;
|
||||
|
||||
purchasedItem = EntityManager.SpawnEntity(listing.ItemId, spawnCoords);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWithdrawTC(UplinkAccount acc, int tc, EntityCoordinates spawnCoords, [NotNullWhen(true)] out EntityUid? stackUid)
|
||||
{
|
||||
stackUid = null;
|
||||
|
||||
// try to charge TC from players account
|
||||
var actTC = Math.Min(tc, acc.Balance);
|
||||
if (actTC <= 0)
|
||||
return false;
|
||||
if (!RemoveFromBalance(acc, actTC))
|
||||
return false;
|
||||
|
||||
// create a stack of TCs near player
|
||||
var stackEntity = EntityManager.SpawnEntity(TelecrystalProtoId, spawnCoords);
|
||||
stackUid = stackEntity;
|
||||
|
||||
// set right amount in stack
|
||||
_stackSystem.SetCount(stackUid.Value, actTC);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
@@ -82,15 +81,10 @@ namespace Content.Server.Traitor.Uplink.Commands
|
||||
// Get TC count
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
var tcCount = configManager.GetCVar(CCVars.TraitorStartingBalance);
|
||||
|
||||
// Get account
|
||||
var uplinkAccount = new UplinkAccount(tcCount, user);
|
||||
var accounts = entityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
Logger.Debug(entityManager.ToPrettyString(user));
|
||||
// Finally add uplink
|
||||
if (!entityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(user, uplinkAccount, uplinkEntity))
|
||||
var uplinkSys = entityManager.EntitySysManager.GetEntitySystem<UplinkSystem>();
|
||||
if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using Content.Shared.Store;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink.SurplusBundle;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,4 +15,11 @@ public sealed class SurplusBundleComponent : Component
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("totalPrice")]
|
||||
public int TotalPrice = 20;
|
||||
|
||||
/// <summary>
|
||||
/// The preset that will be used to get all the listings.
|
||||
/// Currently just defaults to the basic uplink.
|
||||
/// </summary>
|
||||
[DataField("storePreset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))]
|
||||
public string StorePreset = "StorePresetUplink";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -12,23 +13,25 @@ public sealed class SurplusBundleSystem : EntitySystem
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
|
||||
private UplinkStoreListingPrototype[] _uplinks = default!;
|
||||
private ListingData[] _listings = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SurplusBundleComponent, MapInitEvent>(OnMapInit);
|
||||
|
||||
InitList();
|
||||
SubscribeLocalEvent<SurplusBundleComponent, ComponentInit>(OnInit);
|
||||
}
|
||||
|
||||
private void InitList()
|
||||
private void OnInit(EntityUid uid, SurplusBundleComponent component, ComponentInit args)
|
||||
{
|
||||
// sort data in price descending order
|
||||
_uplinks = _prototypeManager.EnumeratePrototypes<UplinkStoreListingPrototype>()
|
||||
.Where(item => item.CanSurplus).ToArray();
|
||||
Array.Sort(_uplinks, (a, b) => b.Price - a.Price);
|
||||
var storePreset = _prototypeManager.Index<StorePresetPrototype>(component.StorePreset);
|
||||
|
||||
_listings = _store.GetAvailableListings(uid, null, storePreset.Categories).ToArray();
|
||||
|
||||
Array.Sort(_listings, (a, b) => (int) (b.Cost.Values.Sum() - a.Cost.Values.Sum())); //this might get weird with multicurrency but don't think about it
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, SurplusBundleComponent component, MapInitEvent args)
|
||||
@@ -46,19 +49,19 @@ public sealed class SurplusBundleSystem : EntitySystem
|
||||
var content = GetRandomContent(component.TotalPrice);
|
||||
foreach (var item in content)
|
||||
{
|
||||
var ent = EntityManager.SpawnEntity(item.ItemId, cords);
|
||||
var ent = EntityManager.SpawnEntity(item.ProductEntity, cords);
|
||||
_entityStorage.Insert(ent, component.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
// wow, is this leetcode reference?
|
||||
private List<UplinkStoreListingPrototype> GetRandomContent(int targetCost)
|
||||
private List<ListingData> GetRandomContent(FixedPoint2 targetCost)
|
||||
{
|
||||
var ret = new List<UplinkStoreListingPrototype>();
|
||||
if (_uplinks.Length == 0)
|
||||
var ret = new List<ListingData>();
|
||||
if (_listings.Length == 0)
|
||||
return ret;
|
||||
|
||||
var totalCost = 0;
|
||||
var totalCost = FixedPoint2.Zero;
|
||||
var index = 0;
|
||||
while (totalCost < targetCost)
|
||||
{
|
||||
@@ -66,10 +69,10 @@ public sealed class SurplusBundleSystem : EntitySystem
|
||||
// Find new item with the lowest acceptable price
|
||||
// All expansive items will be before index, all acceptable after
|
||||
var remainingBudget = targetCost - totalCost;
|
||||
while (_uplinks[index].Price > remainingBudget)
|
||||
while (_listings[index].Cost.Values.Sum() > remainingBudget)
|
||||
{
|
||||
index++;
|
||||
if (index >= _uplinks.Length)
|
||||
if (index >= _listings.Length)
|
||||
{
|
||||
// Looks like no cheap items left
|
||||
// It shouldn't be case for ss14 content
|
||||
@@ -79,10 +82,10 @@ public sealed class SurplusBundleSystem : EntitySystem
|
||||
}
|
||||
|
||||
// Select random listing and add into crate
|
||||
var randomIndex = _random.Next(index, _uplinks.Length);
|
||||
var randomItem = _uplinks[randomIndex];
|
||||
var randomIndex = _random.Next(index, _listings.Length);
|
||||
var randomItem = _listings[randomIndex];
|
||||
ret.Add(randomItem);
|
||||
totalCost += randomItem.Price;
|
||||
totalCost += randomItem.Cost.Values.Sum();
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Content.Server.Traitor.Uplink.Telecrystal
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class TelecrystalComponent : Component
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Stacks;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink.Telecrystal
|
||||
{
|
||||
public sealed class TelecrystalSystem : EntitySystem
|
||||
{
|
||||
[Dependency]
|
||||
private readonly UplinkAccountsSystem _accounts = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<TelecrystalComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, TelecrystalComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (args.Target == null || !EntityManager.TryGetComponent(args.Target.Value, out UplinkComponent? uplink))
|
||||
return;
|
||||
|
||||
// TODO: when uplink will have some auth logic (like PDA ringtone code)
|
||||
// check if uplink open before adding TC
|
||||
// No metagaming by using this on every PDA around just to see if it gets used up.
|
||||
|
||||
var acc = uplink.UplinkAccount;
|
||||
if (acc == null)
|
||||
return;
|
||||
|
||||
EntityManager.TryGetComponent(uid, out SharedStackComponent? stack);
|
||||
|
||||
var tcCount = stack != null ? stack.Count : 1;
|
||||
if (!_accounts.AddToBalance(acc, tcCount))
|
||||
return;
|
||||
|
||||
var msg = Loc.GetString("telecrystal-component-sucs-inserted",
|
||||
("source", args.Used), ("target", args.Target));
|
||||
|
||||
args.User.PopupMessage(args.User, msg);
|
||||
|
||||
EntityManager.DeleteEntity(uid);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class UplinkComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("buySuccessSound")]
|
||||
public SoundSpecifier BuySuccessSound = new SoundPathSpecifier("/Audio/Effects/kaching.ogg");
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("insufficientFundsSound")]
|
||||
public SoundSpecifier InsufficientFundsSound = new SoundPathSpecifier("/Audio/Effects/error.ogg");
|
||||
|
||||
[DataField("activatesInHands")]
|
||||
public bool ActivatesInHands = false;
|
||||
|
||||
[DataField("presetInfo")]
|
||||
public PresetUplinkInfo? PresetInfo = null;
|
||||
|
||||
[ViewVariables] public UplinkAccount? UplinkAccount;
|
||||
|
||||
[ViewVariables, DataField("jobWhiteList", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
|
||||
public HashSet<string>? JobWhitelist = null;
|
||||
|
||||
[Serializable]
|
||||
[DataDefinition]
|
||||
public sealed class PresetUplinkInfo
|
||||
{
|
||||
[DataField("balance")]
|
||||
public int StartingBalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink
|
||||
{
|
||||
public sealed class UplinkInitEvent : EntityEventArgs
|
||||
{
|
||||
public UplinkComponent Uplink;
|
||||
|
||||
public UplinkInitEvent(UplinkComponent uplink)
|
||||
{
|
||||
Uplink = uplink;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UplinkRemovedEvent : EntityEventArgs
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains and controls all items in traitors uplink shop
|
||||
/// </summary>
|
||||
public sealed class UplinkListingSytem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private readonly Dictionary<string, UplinkListingData> _listings = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
foreach (var item in _prototypeManager.EnumeratePrototypes<UplinkStoreListingPrototype>())
|
||||
{
|
||||
var newListing = new UplinkListingData(item.ListingName, item.ItemId,
|
||||
item.Price, item.Category, item.Description, item.Icon, item.JobWhitelist);
|
||||
|
||||
RegisterUplinkListing(newListing);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterUplinkListing(UplinkListingData listing)
|
||||
{
|
||||
if (!ContainsListing(listing))
|
||||
{
|
||||
_listings.Add(listing.ItemId, listing);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsListing(UplinkListingData listing)
|
||||
{
|
||||
return _listings.ContainsKey(listing.ItemId);
|
||||
}
|
||||
|
||||
public bool TryGetListing(string itemID, [NotNullWhen(true)] out UplinkListingData? data)
|
||||
{
|
||||
return _listings.TryGetValue(itemID, out data);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, UplinkListingData> GetListings()
|
||||
{
|
||||
return _listings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,230 +1,41 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink
|
||||
{
|
||||
public sealed class UplinkSystem : EntitySystem
|
||||
{
|
||||
[Dependency]
|
||||
private readonly UplinkAccountsSystem _accounts = default!;
|
||||
[Dependency]
|
||||
private readonly UplinkListingSytem _listing = default!;
|
||||
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
|
||||
public override void Initialize()
|
||||
public const string TelecrystalCurrencyPrototype = "Telecrystal";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of TC on an "uplink"
|
||||
/// Mostly just here for legacy systems based on uplink.
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
/// <returns>the amount of TC</returns>
|
||||
public int GetTCBalance(StoreComponent component)
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UplinkComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<UplinkComponent, ComponentRemove>(OnRemove);
|
||||
SubscribeLocalEvent<UplinkComponent, ActivateInWorldEvent>(OnActivate);
|
||||
|
||||
// UI events
|
||||
SubscribeLocalEvent<UplinkComponent, UplinkBuyListingMessage>(OnBuy);
|
||||
SubscribeLocalEvent<UplinkComponent, UplinkRequestUpdateInterfaceMessage>(OnRequestUpdateUI);
|
||||
SubscribeLocalEvent<UplinkComponent, UplinkTryWithdrawTC>(OnWithdrawTC);
|
||||
|
||||
SubscribeLocalEvent<UplinkAccountBalanceChanged>(OnBalanceChangedBroadcast);
|
||||
FixedPoint2? tcBalance = component.Balance.GetValueOrDefault(TelecrystalCurrencyPrototype);
|
||||
return tcBalance != null ? tcBalance.Value.Int() : 0;
|
||||
}
|
||||
|
||||
public void SetAccount(UplinkComponent component, UplinkAccount account)
|
||||
{
|
||||
if (component.UplinkAccount != null)
|
||||
{
|
||||
Logger.Error("Can't init one uplink with different account!");
|
||||
return;
|
||||
}
|
||||
|
||||
component.UplinkAccount = account;
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, UplinkComponent component, ComponentInit args)
|
||||
{
|
||||
RaiseLocalEvent(uid, new UplinkInitEvent(component), true);
|
||||
|
||||
// if component has a preset info (probably spawn by admin)
|
||||
// create a new account and register it for this uplink
|
||||
if (component.PresetInfo != null)
|
||||
{
|
||||
var account = new UplinkAccount(component.PresetInfo.StartingBalance);
|
||||
_accounts.AddNewAccount(account);
|
||||
SetAccount(component, account);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemove(EntityUid uid, UplinkComponent component, ComponentRemove args)
|
||||
{
|
||||
RaiseLocalEvent(uid, new UplinkRemovedEvent(), true);
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, UplinkComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
// check if uplinks activates directly or use some proxy, like a PDA
|
||||
if (!component.ActivatesInHands)
|
||||
return;
|
||||
if (component.UplinkAccount == null)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
ToggleUplinkUI(component, actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnBalanceChangedBroadcast(UplinkAccountBalanceChanged ev)
|
||||
{
|
||||
foreach (var uplink in EntityManager.EntityQuery<UplinkComponent>())
|
||||
{
|
||||
if (uplink.UplinkAccount == ev.Account)
|
||||
{
|
||||
UpdateUserInterface(uplink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRequestUpdateUI(EntityUid uid, UplinkComponent uplink, UplinkRequestUpdateInterfaceMessage args)
|
||||
{
|
||||
UpdateUserInterface(uplink);
|
||||
}
|
||||
|
||||
private void OnBuy(EntityUid uid, UplinkComponent uplink, UplinkBuyListingMessage message)
|
||||
{
|
||||
if (message.Session.AttachedEntity is not { Valid: true } player) return;
|
||||
if (uplink.UplinkAccount == null) return;
|
||||
|
||||
if (!_accounts.TryPurchaseItem(uplink.UplinkAccount, message.ItemId,
|
||||
EntityManager.GetComponent<TransformComponent>(player).Coordinates, out var entity))
|
||||
{
|
||||
SoundSystem.Play(uplink.InsufficientFundsSound.GetSound(),
|
||||
Filter.SinglePlayer(message.Session), uplink.Owner, AudioParams.Default);
|
||||
RaiseNetworkEvent(new UplinkInsufficientFundsMessage(), message.Session.ConnectedClient);
|
||||
return;
|
||||
}
|
||||
|
||||
_handsSystem.PickupOrDrop(player, entity.Value);
|
||||
|
||||
SoundSystem.Play(uplink.BuySuccessSound.GetSound(),
|
||||
Filter.SinglePlayer(message.Session), uplink.Owner, AudioParams.Default.WithVolume(-8f));
|
||||
|
||||
RaiseNetworkEvent(new UplinkBuySuccessMessage(), message.Session.ConnectedClient);
|
||||
}
|
||||
|
||||
private void OnWithdrawTC(EntityUid uid, UplinkComponent uplink, UplinkTryWithdrawTC args)
|
||||
{
|
||||
var acc = uplink.UplinkAccount;
|
||||
if (acc == null)
|
||||
return;
|
||||
|
||||
if (args.Session.AttachedEntity is not { Valid: true } player) return;
|
||||
var cords = EntityManager.GetComponent<TransformComponent>(player).Coordinates;
|
||||
|
||||
// try to withdraw TCs from account
|
||||
if (!_accounts.TryWithdrawTC(acc, args.TC, cords, out var tcUid))
|
||||
return;
|
||||
|
||||
// try to put it into players hands
|
||||
_handsSystem.PickupOrDrop(player, tcUid.Value);
|
||||
|
||||
// play buying sound
|
||||
SoundSystem.Play(uplink.BuySuccessSound.GetSound(),
|
||||
Filter.SinglePlayer(args.Session), uplink.Owner, AudioParams.Default.WithVolume(-8f));
|
||||
|
||||
UpdateUserInterface(uplink);
|
||||
}
|
||||
|
||||
public void ToggleUplinkUI(UplinkComponent component, IPlayerSession session)
|
||||
{
|
||||
var ui = component.Owner.GetUIOrNull(UplinkUiKey.Key);
|
||||
ui?.Toggle(session);
|
||||
|
||||
UpdateUserInterface(component);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface(UplinkComponent component)
|
||||
{
|
||||
var ui = component.Owner.GetUIOrNull(UplinkUiKey.Key);
|
||||
if (ui == null)
|
||||
return;
|
||||
|
||||
var listings = _listing.GetListings().Values.ToList();
|
||||
var acc = component.UplinkAccount;
|
||||
|
||||
UplinkAccountData accData;
|
||||
if (acc != null)
|
||||
{
|
||||
// if we don't have a jobwhitelist stored, get a new one
|
||||
if (component.JobWhitelist == null &&
|
||||
acc.AccountHolder != null &&
|
||||
TryComp<MindComponent>(acc.AccountHolder, out var mind) &&
|
||||
mind.Mind != null)
|
||||
{
|
||||
HashSet<string>? jobList = new();
|
||||
foreach (var role in mind.Mind.AllRoles.ToList())
|
||||
{
|
||||
if (role.GetType() == typeof(Job))
|
||||
{
|
||||
var job = (Job) role;
|
||||
jobList.Add(job.Prototype.ID);
|
||||
}
|
||||
}
|
||||
component.JobWhitelist = jobList;
|
||||
}
|
||||
|
||||
// filter out items not on the whitelist
|
||||
for (var i = 0; i < listings.Count; i++)
|
||||
{
|
||||
var entry = listings[i];
|
||||
if (entry.JobWhitelist != null)
|
||||
{
|
||||
var found = false;
|
||||
if (component.JobWhitelist != null)
|
||||
{
|
||||
foreach (var job in component.JobWhitelist)
|
||||
{
|
||||
if (entry.JobWhitelist.Contains(job))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
listings.Remove(entry);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
accData = new UplinkAccountData(acc.AccountHolder, acc.Balance);
|
||||
}
|
||||
else
|
||||
{
|
||||
accData = new UplinkAccountData(null, 0);
|
||||
}
|
||||
|
||||
ui.SetState(new UplinkUpdateState(accData, listings.ToArray()));
|
||||
}
|
||||
|
||||
public bool AddUplink(EntityUid user, UplinkAccount account, EntityUid? uplinkEntity = null)
|
||||
/// <summary>
|
||||
/// Adds an uplink to the target
|
||||
/// </summary>
|
||||
/// <param name="user">The person who is getting the uplink</param>
|
||||
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
|
||||
/// <param name="uplinkPresetId">The id of the storepreset</param>
|
||||
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
|
||||
/// <returns>Whether or not the uplink was added successfully</returns>
|
||||
public bool AddUplink(EntityUid user, FixedPoint2? balance, string uplinkPresetId = "StorePresetUplink", EntityUid? uplinkEntity = null)
|
||||
{
|
||||
// Try to find target item
|
||||
if (uplinkEntity == null)
|
||||
@@ -234,11 +45,17 @@ namespace Content.Server.Traitor.Uplink
|
||||
return false;
|
||||
}
|
||||
|
||||
var uplink = uplinkEntity.Value.EnsureComponent<UplinkComponent>();
|
||||
SetAccount(uplink, account);
|
||||
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
|
||||
_store.InitializeFromPreset(uplinkPresetId, store);
|
||||
store.AccountOwner = user;
|
||||
store.Balance.Clear();
|
||||
|
||||
if (!HasComp<PDAComponent>(uplinkEntity.Value))
|
||||
uplink.ActivatesInHands = true;
|
||||
if (balance != null)
|
||||
{
|
||||
store.Balance.Clear();
|
||||
_store.TryAddCurrency(
|
||||
new Dictionary<string, FixedPoint2>() { { TelecrystalCurrencyPrototype, balance.Value } }, store);
|
||||
}
|
||||
|
||||
// TODO add BUI. Currently can't be done outside of yaml -_-
|
||||
|
||||
@@ -248,14 +65,13 @@ namespace Content.Server.Traitor.Uplink
|
||||
private EntityUid? FindUplinkTarget(EntityUid user)
|
||||
{
|
||||
// Try to find PDA in inventory
|
||||
|
||||
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
|
||||
{
|
||||
while (containerSlotEnumerator.MoveNext(out var pdaUid))
|
||||
{
|
||||
if (!pdaUid.ContainedEntity.HasValue) continue;
|
||||
|
||||
if (HasComp<PDAComponent>(pdaUid.ContainedEntity.Value))
|
||||
if (HasComp<PDAComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
|
||||
return pdaUid.ContainedEntity.Value;
|
||||
}
|
||||
}
|
||||
@@ -263,7 +79,7 @@ namespace Content.Server.Traitor.Uplink
|
||||
// Also check hands
|
||||
foreach (var item in _handsSystem.EnumerateHeld(user))
|
||||
{
|
||||
if (HasComp<PDAComponent>(item))
|
||||
if (HasComp<PDAComponent>(item) || HasComp<StoreComponent>(item))
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.TraitorDeathMatch.Components;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
@@ -12,8 +14,11 @@ namespace Content.Server.TraitorDeathMatch;
|
||||
public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly UplinkAccountsSystem _uplink = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
|
||||
private const string TcCurrencyPrototype = "Telecrystal";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -43,7 +48,7 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent<UplinkComponent>(args.Used, out var victimUplink))
|
||||
if (!EntityManager.TryGetComponent<StoreComponent>(args.Used, out var victimUplink))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString(
|
||||
"traitor-death-match-redemption-component-interact-using-main-message",
|
||||
@@ -72,10 +77,10 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
UplinkComponent? userUplink = null;
|
||||
StoreComponent? userUplink = null;
|
||||
|
||||
if (_inventory.TryGetSlotEntity(args.User, "id", out var pdaUid) &&
|
||||
EntityManager.TryGetComponent<UplinkComponent>(pdaUid, out var userUplinkComponent))
|
||||
EntityManager.TryGetComponent<StoreComponent>(pdaUid, out var userUplinkComponent))
|
||||
userUplink = userUplinkComponent;
|
||||
|
||||
if (userUplink == null)
|
||||
@@ -88,35 +93,13 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// We have finally determined both PDA components. FINALLY.
|
||||
|
||||
var userAccount = userUplink.UplinkAccount;
|
||||
var victimAccount = victimUplink.UplinkAccount;
|
||||
|
||||
if (userAccount == null)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString(
|
||||
"traitor-death-match-redemption-component-interact-using-main-message",
|
||||
("secondMessage",
|
||||
Loc.GetString(
|
||||
"traitor-death-match-redemption-component-interact-using-user-no-uplink-account-message"))), uid, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
if (victimAccount == null)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString(
|
||||
"traitor-death-match-redemption-component-interact-using-main-message",
|
||||
("secondMessage",
|
||||
Loc.GetString(
|
||||
"traitor-death-match-redemption-component-interact-using-victim-no-uplink-account-message"))), uid, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4 is the per-PDA bonus amount.
|
||||
var transferAmount = victimAccount.Balance + 4;
|
||||
_uplink.SetBalance(victimAccount, 0);
|
||||
_uplink.AddToBalance(userAccount, transferAmount);
|
||||
// 4 is the per-PDA bonus amount
|
||||
var transferAmount = _uplink.GetTCBalance(victimUplink) + 4;
|
||||
victimUplink.Balance.Clear();
|
||||
_store.TryAddCurrency(new Dictionary<string, FixedPoint2>() { {"Telecrystal", FixedPoint2.New(transferAmount)}}, userUplink);
|
||||
|
||||
EntityManager.DeleteEntity(victimUplink.Owner);
|
||||
|
||||
|
||||
@@ -75,4 +75,5 @@ public enum LogType
|
||||
Gib = 70,
|
||||
Identity = 71,
|
||||
CableCut = 72,
|
||||
StorePurchase = 73,
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
namespace Content.Shared.PDA
|
||||
{
|
||||
public enum UplinkCategory
|
||||
{
|
||||
Weapons,
|
||||
Ammo,
|
||||
Explosives,
|
||||
Misc,
|
||||
Bundles,
|
||||
Tools,
|
||||
Utility,
|
||||
Job,
|
||||
Armor,
|
||||
Pointless,
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.PDA
|
||||
{
|
||||
[Prototype("uplinkListing")]
|
||||
public sealed class UplinkStoreListingPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataFieldAttribute]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("itemId", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ItemId { get; } = string.Empty;
|
||||
|
||||
[DataField("price")]
|
||||
public int Price { get; } = 5;
|
||||
|
||||
[DataField("category")]
|
||||
public UplinkCategory Category { get; } = UplinkCategory.Utility;
|
||||
|
||||
[DataField("description")]
|
||||
public string Description { get; } = string.Empty;
|
||||
|
||||
[DataField("listingName")]
|
||||
public string ListingName { get; } = string.Empty;
|
||||
|
||||
[DataField("icon")]
|
||||
public SpriteSpecifier? Icon { get; } = null;
|
||||
|
||||
[DataField("jobWhitelist", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
|
||||
public HashSet<string>? JobWhitelist;
|
||||
|
||||
[DataField("surplus")]
|
||||
public bool CanSurplus = true;
|
||||
}
|
||||
}
|
||||
43
Content.Shared/Store/CurrencyPrototype.cs
Normal file
43
Content.Shared/Store/CurrencyPrototype.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
/// <summary>
|
||||
/// Prototype used to define different types of currency for generic stores.
|
||||
/// Mainly used for antags, such as traitors, nukies, and revenants
|
||||
/// This is separate to the cargo ordering system.
|
||||
/// </summary>
|
||||
[Prototype("currency")]
|
||||
[DataDefinition, Serializable, NetSerializable]
|
||||
public sealed class CurrencyPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The Loc string used for displaying the balance of a certain currency at the top of the store ui
|
||||
/// </summary>
|
||||
[DataField("balanceDisplay")]
|
||||
public string BalanceDisplay { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The Loc string used for displaying the price of listings in store UI
|
||||
/// </summary>
|
||||
[DataField("priceDisplay")]
|
||||
public string PriceDisplay { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The physical entity of the currency
|
||||
/// </summary>
|
||||
[DataField("entityId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? EntityId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId.
|
||||
/// </summary>
|
||||
[DataField("canWithdraw")]
|
||||
public bool CanWithdraw { get; } = true;
|
||||
}
|
||||
23
Content.Shared/Store/ListingCondition.cs
Normal file
23
Content.Shared/Store/ListingCondition.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
/// <summary>
|
||||
/// Used to define a complicated condition that requires C#
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
[MeansImplicitUse]
|
||||
public abstract class ListingCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether or not a certain entity can purchase a listing.
|
||||
/// </summary>
|
||||
/// <returns>Whether or not the listing can be purchased</returns>
|
||||
public abstract bool Condition(ListingConditionArgs args);
|
||||
}
|
||||
|
||||
/// <param name="Buyer">The person purchasing the listing</param>
|
||||
/// <param name="Listing">The liting itself</param>
|
||||
/// <param name="EntityManager">An entitymanager for sane coding</param>
|
||||
public readonly record struct ListingConditionArgs(EntityUid Buyer, EntityUid? StoreEntity, ListingData Listing, IEntityManager EntityManager);
|
||||
133
Content.Shared/Store/ListingPrototype.cs
Normal file
133
Content.Shared/Store/ListingPrototype.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
/// <summary>
|
||||
/// This is the data object for a store listing which is passed around in code.
|
||||
/// this allows for prices and features of listings to be dynamically changed in code
|
||||
/// without having to modify the prototypes.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Virtual, DataDefinition]
|
||||
public class ListingData : IEquatable<ListingData>
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the listing. If empty, uses the entity's name (if present)
|
||||
/// </summary>
|
||||
[DataField("name")]
|
||||
public string Name = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The description of the listing. If empty, uses the entity's description (if present)
|
||||
/// </summary>
|
||||
[DataField("description")]
|
||||
public string Description = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The categories that this listing applies to. Used for filtering a listing for a store.
|
||||
/// </summary>
|
||||
[DataField("categories", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<StoreCategoryPrototype>))]
|
||||
public List<string> Categories = new();
|
||||
|
||||
/// <summary>
|
||||
/// The cost of the listing. String represents the currency type while the FixedPoint2 represents the amount of that currency.
|
||||
/// </summary>
|
||||
[DataField("cost", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
|
||||
public Dictionary<string, FixedPoint2> Cost = new();
|
||||
|
||||
/// <summary>
|
||||
/// Specific customizeable conditions that determine whether or not the listing can be purchased.
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
[DataField("conditions", serverOnly: true)]
|
||||
public List<ListingCondition>? Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// The icon for the listing. If null, uses the icon for the entity or action.
|
||||
/// </summary>
|
||||
[DataField("icon")]
|
||||
public SpriteSpecifier? Icon;
|
||||
|
||||
/// <summary>
|
||||
/// The priority for what order the listings will show up in on the menu.
|
||||
/// </summary>
|
||||
[DataField("priority")]
|
||||
public int Priority = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The entity that is given when the listing is purchased.
|
||||
/// </summary>
|
||||
[DataField("productEntity", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? ProductEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The action that is given when the listing is purchased.
|
||||
/// </summary>
|
||||
[DataField("productAction", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
|
||||
public string? ProductAction;
|
||||
|
||||
/// <summary>
|
||||
/// The event that is broadcast when the listing is purchased.
|
||||
/// </summary>
|
||||
[DataField("productEvent")]
|
||||
public object? ProductEvent;
|
||||
|
||||
/// <summary>
|
||||
/// used internally for tracking how many times an item was purchased.
|
||||
/// </summary>
|
||||
public int PurchaseAmount = 0;
|
||||
|
||||
public bool Equals(ListingData? listing)
|
||||
{
|
||||
if (listing == null)
|
||||
return false;
|
||||
|
||||
//simple conditions
|
||||
if (Priority != listing.Priority ||
|
||||
Name != listing.Name ||
|
||||
Description != listing.Description ||
|
||||
ProductEntity != listing.ProductEntity ||
|
||||
ProductAction != listing.ProductAction ||
|
||||
ProductEvent != listing.ProductEvent)
|
||||
return false;
|
||||
|
||||
if (Icon != null && !Icon.Equals(listing.Icon))
|
||||
return false;
|
||||
|
||||
///more complicated conditions that eat perf. these don't really matter
|
||||
///as much because you will very rarely have to check these.
|
||||
if (!Categories.OrderBy(x => x).SequenceEqual(listing.Categories.OrderBy(x => x)))
|
||||
return false;
|
||||
|
||||
if (!Cost.OrderBy(x => x).SequenceEqual(listing.Cost.OrderBy(x => x)))
|
||||
return false;
|
||||
|
||||
if ((Conditions != null && listing.Conditions != null) &&
|
||||
!Conditions.OrderBy(x => x).SequenceEqual(listing.Conditions.OrderBy(x => x)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//<inheritdoc>
|
||||
/// <summary>
|
||||
/// Defines a set item listing that is available in a store
|
||||
/// </summary>
|
||||
[Prototype("listing")]
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed class ListingPrototype : ListingData, IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
}
|
||||
22
Content.Shared/Store/StoreCategoryPrototype.cs
Normal file
22
Content.Shared/Store/StoreCategoryPrototype.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
/// <summary>
|
||||
/// Used to define different categories for a store.
|
||||
/// </summary>
|
||||
[Prototype("storeCategory")]
|
||||
[Serializable, NetSerializable, DataDefinition]
|
||||
public sealed class StoreCategoryPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("name")]
|
||||
public string Name { get; } = string.Empty;
|
||||
|
||||
[DataField("priority")]
|
||||
public int Priority { get; } = 0;
|
||||
}
|
||||
41
Content.Shared/Store/StorePresetPrototype.cs
Normal file
41
Content.Shared/Store/StorePresetPrototype.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
using Content.Shared.FixedPoint;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies generic info for initializing a store.
|
||||
/// </summary>
|
||||
[Prototype("storePreset")]
|
||||
[DataDefinition]
|
||||
public sealed class StorePresetPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables] [IdDataField] public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The name displayed at the top of the store window
|
||||
/// </summary>
|
||||
[DataField("storeName", required: true)]
|
||||
public string StoreName { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The categories that this store can access
|
||||
/// </summary>
|
||||
[DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StoreCategoryPrototype>))]
|
||||
public HashSet<string> Categories { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The inital balance that the store initializes with.
|
||||
/// </summary>
|
||||
[DataField("initialBalance",
|
||||
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
|
||||
public Dictionary<string, FixedPoint2>? InitialBalance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The currencies that are accepted in the store
|
||||
/// </summary>
|
||||
[DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<CurrencyPrototype>))]
|
||||
public HashSet<string> CurrencyWhitelist { get; } = new();
|
||||
}
|
||||
84
Content.Shared/Store/StoreUi.cs
Normal file
84
Content.Shared/Store/StoreUi.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.MobState;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum StoreUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StoreUpdateState : BoundUserInterfaceState
|
||||
{
|
||||
public readonly EntityUid? Buyer;
|
||||
|
||||
public readonly HashSet<ListingData> Listings;
|
||||
|
||||
public readonly Dictionary<string, FixedPoint2> Balance;
|
||||
|
||||
public StoreUpdateState(EntityUid? buyer, HashSet<ListingData> listings, Dictionary<string, FixedPoint2> balance)
|
||||
{
|
||||
Buyer = buyer;
|
||||
Listings = listings;
|
||||
Balance = balance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// initializes miscellaneous data about the store.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StoreInitializeState : BoundUserInterfaceState
|
||||
{
|
||||
public readonly string Name;
|
||||
|
||||
public StoreInitializeState(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public EntityUid CurrentBuyer;
|
||||
|
||||
public StoreRequestUpdateInterfaceMessage(EntityUid currentBuyer)
|
||||
{
|
||||
CurrentBuyer = currentBuyer;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StoreBuyListingMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public EntityUid Buyer;
|
||||
|
||||
public ListingData Listing;
|
||||
|
||||
public StoreBuyListingMessage(EntityUid buyer, ListingData listing)
|
||||
{
|
||||
Buyer = buyer;
|
||||
Listing = listing;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StoreRequestWithdrawMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public EntityUid Buyer;
|
||||
|
||||
public string Currency;
|
||||
|
||||
public int Amount;
|
||||
|
||||
public StoreRequestWithdrawMessage(EntityUid buyer, string currency, int amount)
|
||||
{
|
||||
Buyer = buyer;
|
||||
Currency = currency;
|
||||
Amount = amount;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
public sealed class UplinkAccount
|
||||
{
|
||||
public readonly EntityUid? AccountHolder;
|
||||
public int Balance;
|
||||
|
||||
public UplinkAccount(int startingBalance, EntityUid? accountHolder = null)
|
||||
{
|
||||
AccountHolder = accountHolder;
|
||||
Balance = startingBalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkAccountData
|
||||
{
|
||||
public EntityUid? DataAccountHolder;
|
||||
public int DataBalance;
|
||||
public UplinkAccountData(EntityUid? dataAccountHolder, int dataBalance)
|
||||
{
|
||||
DataAccountHolder = dataAccountHolder;
|
||||
DataBalance = dataBalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Content.Shared.PDA;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkListingData : ComponentState, IEquatable<UplinkListingData>
|
||||
{
|
||||
public readonly string ItemId;
|
||||
public readonly int Price;
|
||||
public readonly UplinkCategory Category;
|
||||
public readonly string Description;
|
||||
public readonly string ListingName;
|
||||
public readonly SpriteSpecifier? Icon;
|
||||
public readonly HashSet<string>? JobWhitelist;
|
||||
|
||||
public UplinkListingData(string listingName, string itemId,
|
||||
int price, UplinkCategory category,
|
||||
string description, SpriteSpecifier? icon, HashSet<string>? jobWhitelist)
|
||||
{
|
||||
ListingName = listingName;
|
||||
Price = price;
|
||||
Category = category;
|
||||
Description = description;
|
||||
ItemId = itemId;
|
||||
Icon = icon;
|
||||
JobWhitelist = jobWhitelist;
|
||||
}
|
||||
|
||||
public bool Equals(UplinkListingData? other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ItemId == other.ItemId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkBuyListingMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public string ItemId;
|
||||
|
||||
public UplinkBuyListingMessage(string itemId)
|
||||
{
|
||||
ItemId = itemId;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public UplinkRequestUpdateInterfaceMessage()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkTryWithdrawTC : BoundUserInterfaceMessage
|
||||
{
|
||||
public int TC;
|
||||
|
||||
public UplinkTryWithdrawTC(int tc)
|
||||
{
|
||||
TC = tc;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkBuySuccessMessage : EntityEventArgs
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkInsufficientFundsMessage : EntityEventArgs
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UplinkUpdateState : BoundUserInterfaceState
|
||||
{
|
||||
public UplinkAccountData Account;
|
||||
public UplinkListingData[] Listings;
|
||||
|
||||
public UplinkUpdateState(UplinkAccountData account, UplinkListingData[] listings)
|
||||
{
|
||||
Account = account;
|
||||
Listings = listings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Traitor.Uplink
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum UplinkUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
}
|
||||
11
Resources/Locale/en-US/store/currency.ftl
Normal file
11
Resources/Locale/en-US/store/currency.ftl
Normal file
@@ -0,0 +1,11 @@
|
||||
store-currency-inserted = {CAPITALIZE(THE($used))} is inserted into the {THE($target)}.
|
||||
|
||||
store-currency-free = Free
|
||||
store-currency-balance-display-debugdollar = Debug Dollar: {$amount}
|
||||
store-currency-price-display-debugdollar = {$amount ->
|
||||
[one] {$amount} Debug Dollar
|
||||
*[other] {$amount} Debug Dollars
|
||||
}
|
||||
|
||||
store-currency-balance-display-telecrystal = TC: {$amount}
|
||||
store-currency-price-display-telecrystal = {$amount} TC
|
||||
4
Resources/Locale/en-US/store/store.ftl
Normal file
4
Resources/Locale/en-US/store/store.ftl
Normal file
@@ -0,0 +1,4 @@
|
||||
store-ui-default-title = Store
|
||||
store-ui-default-withdraw-text = Withdraw
|
||||
|
||||
store-withdraw-button-ui = Withdraw {$currency}
|
||||
44
Resources/Prototypes/Catalog/catalog.yml
Normal file
44
Resources/Prototypes/Catalog/catalog.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
- type: listing
|
||||
id: DebugListing
|
||||
name: debug name
|
||||
description: debug desc
|
||||
categories:
|
||||
- Debug
|
||||
cost:
|
||||
DebugDollar: 10
|
||||
Telecrystal: 10
|
||||
|
||||
- type: listing
|
||||
id: DebugListing3
|
||||
name: debug name 3
|
||||
description: debug desc 3
|
||||
categories:
|
||||
- Debug
|
||||
cost:
|
||||
DebugDollar: 10
|
||||
|
||||
- type: listing
|
||||
id: DebugListing5
|
||||
name: debug name 5
|
||||
description: debug desc 5
|
||||
categories:
|
||||
- Debug
|
||||
|
||||
- type: listing
|
||||
id: DebugListing4
|
||||
name: debug name 4
|
||||
description: debug desc 4
|
||||
productAction: Scream
|
||||
categories:
|
||||
- Debug
|
||||
cost:
|
||||
DebugDollar: 1
|
||||
|
||||
- type: listing
|
||||
id: DebugListing2
|
||||
name: debug name 2
|
||||
description: debug desc 2
|
||||
categories:
|
||||
- Debug2
|
||||
cost:
|
||||
DebugDollar: 10
|
||||
@@ -1,500 +1,641 @@
|
||||
# Guns
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkPistolViper
|
||||
category: Weapons
|
||||
itemId: WeaponPistolViper
|
||||
listingName: Viper
|
||||
name: Viper
|
||||
description: A small, easily concealable, but somewhat underpowered gun. Use pistol magazines (.35 auto).
|
||||
price: 6
|
||||
productEntity: WeaponPistolViper
|
||||
cost:
|
||||
Telecrystal: 6
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkRevolverPython
|
||||
category: Weapons
|
||||
itemId: WeaponRevolverPython
|
||||
listingName: Python
|
||||
name: Python
|
||||
description: A loud and deadly revolver. Uses .40 Magnum.
|
||||
price: 8
|
||||
productEntity: WeaponRevolverPython
|
||||
cost:
|
||||
Telecrystal: 8
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
# Inbuilt suppressor so it's sneaky + more expensive.
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkPistolCobra
|
||||
category: Weapons
|
||||
itemId: WeaponPistolCobra
|
||||
listingName: Cobra
|
||||
name: Cobra
|
||||
description: A rugged, robust operator handgun with inbuilt silencer. Use pistol magazines (.25 caseless).
|
||||
price: 8
|
||||
productEntity: WeaponPistolCobra
|
||||
cost:
|
||||
Telecrystal: 8
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
# Poor accuracy, slow to fire, cheap option
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkRifleMosin
|
||||
category: Weapons
|
||||
itemId: WeaponSniperMosin
|
||||
listingName: Surplus Rifle
|
||||
name: Surplus Rifle
|
||||
description: A bolt action service rifle that has seen many wars. Not modern by any standard, hand loaded, and terrible recoil, but it is cheap.
|
||||
price: 4
|
||||
productEntity: WeaponSniperMosin
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
#- type: uplinkListing
|
||||
# id: UplinkCrossbowEnergyMini
|
||||
# category: Weapons
|
||||
# itemId: CrossbowEnergyMini
|
||||
# price: 8
|
||||
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkEsword
|
||||
category: Weapons
|
||||
itemId: EnergySword
|
||||
listingName: Energy Sword
|
||||
name: Energy Sword
|
||||
description: A very dangerous energy sword. Can be stored in pockets when turned off. Makes a lot of noise when used or turned on.
|
||||
price: 8
|
||||
productEntity: EnergySword
|
||||
cost:
|
||||
Telecrystal: 8
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
# bug swept to make
|
||||
#- type: uplinkListing
|
||||
# id: UplinkDoubleBladedESword
|
||||
# category: Weapons
|
||||
# itemId: DoubleBladedESword
|
||||
# price: 16
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkEnergyDagger
|
||||
category: Weapons
|
||||
itemId: EnergyDagger
|
||||
listingName: Energy Dagger
|
||||
name: Energy Dagger
|
||||
description: A small energy blade conveniently disguised in the form of a pen.
|
||||
price: 2
|
||||
productEntity: EnergyDagger
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkFireAxeFlaming
|
||||
category: Weapons
|
||||
itemId: FireAxeFlaming
|
||||
listingName: Fire Axe
|
||||
name: Fire Axe
|
||||
description: A classic-style weapon infused with advanced atmos technology to allow it to set targets on fire.
|
||||
price: 10
|
||||
productEntity: FireAxeFlaming
|
||||
cost:
|
||||
Telecrystal: 10
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
|
||||
# Explosives
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkExplosiveGrenade
|
||||
category: Explosives
|
||||
itemId: ExGrenade
|
||||
price: 4
|
||||
productEntity: ExGrenade
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkExplosives
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkExplosiveGrenadeFlash
|
||||
category: Explosives
|
||||
itemId: GrenadeFlashBang
|
||||
price: 2
|
||||
productEntity: GrenadeFlashBang
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkExplosives
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSyndieMiniBomb
|
||||
category: Explosives
|
||||
itemId: SyndieMiniBomb
|
||||
price: 7
|
||||
productEntity: SyndieMiniBomb
|
||||
cost:
|
||||
Telecrystal: 7
|
||||
categories:
|
||||
- UplinkExplosives
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkGrenadePenguin
|
||||
category: Explosives
|
||||
itemId: MobGrenadePenguin
|
||||
price: 6
|
||||
surplus: false # got wrecked by penguins from surplus crate
|
||||
productEntity: MobGrenadePenguin
|
||||
cost:
|
||||
Telecrystal: 6
|
||||
categories:
|
||||
- UplinkExplosives
|
||||
conditions:
|
||||
- !type:BuyerWhitelistCondition
|
||||
blacklist:
|
||||
components:
|
||||
- SurplusBundle
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkC4
|
||||
category: Explosives
|
||||
itemId: C4
|
||||
price: 2
|
||||
description: >
|
||||
C-4 is plastic explosive of the common variety Composition C. You can use it to breach walls, airlocks or sabotage equipment.
|
||||
It can be attached to almost all objects and has a modifiable timer with a minimum setting of 10 seconds.
|
||||
productEntity: C4
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkExplosives
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkC4Bundle
|
||||
category: Explosives
|
||||
itemId: ClothingBackpackDuffelSyndicateC4tBundle
|
||||
price: 12 # 25% off
|
||||
description: Because sometimes quantity is quality. Contains 8 C-4 plastic explosives.
|
||||
productEntity: ClothingBackpackDuffelSyndicateC4tBundle
|
||||
cost:
|
||||
Telecrystal: 12
|
||||
categories:
|
||||
- UplinkExplosives
|
||||
|
||||
# Ammo
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkPistol9mmMagazine
|
||||
category: Ammo
|
||||
itemId: MagazinePistol
|
||||
price: 2
|
||||
productEntity: MagazinePistol
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkAmmo
|
||||
|
||||
# For the Mandella
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkMagazinePistolCaselessRifle
|
||||
category: Ammo
|
||||
itemId: MagazinePistolCaselessRifle
|
||||
price: 2
|
||||
productEntity: MagazinePistolCaselessRifle
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkAmmo
|
||||
|
||||
# For the Inspector
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSpeedLoaderMagnum
|
||||
category: Ammo
|
||||
itemId: SpeedLoaderMagnum
|
||||
price: 2
|
||||
icon: /Textures/Objects/Weapons/Guns/Ammunition/SpeedLoaders/Magnum/magnum_speed_loader.rsi/base.png
|
||||
productEntity: SpeedLoaderMagnum
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkAmmo
|
||||
|
||||
# For the mosin
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkMosinAmmo
|
||||
category: Ammo
|
||||
itemId: BoxMagazineLightRifle
|
||||
description: A box of cartridges for the surplus rifle.
|
||||
price: 2
|
||||
|
||||
productEntity: BoxMagazineLightRifle
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkAmmo
|
||||
|
||||
#Utility
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkHoloparaKit
|
||||
category: Utility
|
||||
itemId: BoxHoloparasite
|
||||
listingName: Holoparasite Kit
|
||||
name: Holoparasite Kit
|
||||
description: The pride and joy of Cybersun. Contains an injector that hosts a sentient metaphysical guardian made of hard light which resides in the user's body when not active. The guardian can punch rapidly and is immune to hazardous environments and bullets, but shares any damage it takes with the user.
|
||||
icon: /Textures/Objects/Misc/guardian_info.rsi/icon.png
|
||||
price: 14
|
||||
productEntity: BoxHoloparasite
|
||||
cost:
|
||||
Telecrystal: 14
|
||||
categories:
|
||||
- UplinkUtility
|
||||
conditions:
|
||||
- !type:StoreWhitelistCondition
|
||||
blacklist:
|
||||
tags:
|
||||
- NukeOpsUplink
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkHolster
|
||||
category: Utility
|
||||
itemId: ClothingBeltSyndieHolster
|
||||
listingName: Syndicate Shoulder Holster
|
||||
name: Syndicate Shoulder Holster
|
||||
description: A deep shoulder holster capable of holding many types of ballistics.
|
||||
icon: /Textures/Clothing/Belt/syndieholster.rsi/icon.png
|
||||
price: 2
|
||||
productEntity: ClothingBeltSyndieHolster
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkEmag
|
||||
category: Utility
|
||||
itemId: Emag
|
||||
name: Emag
|
||||
description: The business card of the syndicate, this sequencer is able to break open airlocks and tamper with a variety of station devices. Recharges automatically.
|
||||
icon: /Textures/Objects/Tools/emag.rsi/icon.png
|
||||
price: 8
|
||||
productEntity: Emag
|
||||
cost:
|
||||
Telecrystal: 8
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkAgentIDCard
|
||||
category: Utility
|
||||
itemId: AgentIDCard
|
||||
listingName: Agent ID Card
|
||||
name: Agent ID Card
|
||||
description: A modified ID card that can copy accesses from other cards and change its name and job title at-will.
|
||||
icon: Objects/Misc/id_cards.rsi/default.png
|
||||
price: 3
|
||||
productEntity: AgentIDCard
|
||||
cost:
|
||||
Telecrystal: 3
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkJetpack
|
||||
category: Utility
|
||||
itemId: JetpackBlack
|
||||
listingName: Black Jetpack
|
||||
name: Black Jetpack
|
||||
description: A black jetpack. It allows you to fly around in space. Additional fuel not included.
|
||||
icon: Objects/Tanks/Jetpacks/black.rsi/icon.png
|
||||
price: 5
|
||||
productEntity: JetpackBlack
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
- type: uplinkListing
|
||||
id: ReinforcementTeleporterSyndicate
|
||||
category: Utility
|
||||
itemId: ReinforcementTeleporterSyndicate
|
||||
listingName: Reinforcement Teleporter
|
||||
- type: listing
|
||||
id: UplinkReinforcementTeleporterSyndicate
|
||||
name: Reinforcement Teleporter
|
||||
description: Teleport in an agent of extremely questionable quality. No off button, buy this if you're ready to party. They have a pistol with no reserve ammo, and a knife. That's it.
|
||||
productEntity: ReinforcementTeleporterSyndicate
|
||||
icon: Objects/Devices/communication.rsi/old-radio.png
|
||||
price: 25
|
||||
cost:
|
||||
Telecrystal: 25
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
#TODO: Increase the price of this to 4-5/remove it when we get encrpytion keys
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkHeadset
|
||||
category: Utility
|
||||
itemId: ClothingHeadsetAltSyndicate
|
||||
listingName: Syndicate Overear-Headset
|
||||
name: Syndicate Overear-Headset
|
||||
description: A headset that allows you to listen in on departmental channels, or contact other traitors.
|
||||
icon: Clothing/Ears/Headsets/syndicate.rsi/icon_alt.png
|
||||
price: 2
|
||||
productEntity: ClothingHeadsetAltSyndicate
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkHypopen
|
||||
category: Utility
|
||||
itemId: Hypopen
|
||||
listingName: Hypopen
|
||||
name: Hypopen
|
||||
description: A chemical hypospray disguised as a pen, capable of instantly injecting up to 15u of reagents. Starts empty.
|
||||
price: 4
|
||||
productEntity: Hypopen
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkUtility
|
||||
|
||||
# Bundles
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkC20RBundle
|
||||
category: Bundles
|
||||
itemId: ClothingBackpackDuffelSyndicateFilledSMG
|
||||
price: 25
|
||||
icon: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi/icon.png
|
||||
productEntity: ClothingBackpackDuffelSyndicateFilledSMG
|
||||
cost:
|
||||
Telecrystal: 25
|
||||
categories:
|
||||
- UplinkBundles
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkBulldogBundle
|
||||
category: Bundles
|
||||
itemId: ClothingBackpackDuffelSyndicateFilledShotgun
|
||||
price: 25
|
||||
icon: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi/icon.png
|
||||
productEntity: ClothingBackpackDuffelSyndicateFilledShotgun
|
||||
cost:
|
||||
Telecrystal: 25
|
||||
categories:
|
||||
- UplinkBundles
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkGrenadeLauncherBundle
|
||||
category: Bundles
|
||||
itemId: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
|
||||
price: 30
|
||||
icon: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi/icon.png
|
||||
productEntity: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
|
||||
cost:
|
||||
Telecrystal: 30
|
||||
categories:
|
||||
- UplinkBundles
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkL6SawBundle
|
||||
category: Bundles
|
||||
itemId: ClothingBackpackDuffelSyndicateFilledLMG
|
||||
price: 40
|
||||
icon: /Textures/Objects/Weapons/Guns/LMGs/l6.rsi/icon.png
|
||||
productEntity: ClothingBackpackDuffelSyndicateFilledLMG
|
||||
cost:
|
||||
Telecrystal: 40
|
||||
categories:
|
||||
- UplinkBundles
|
||||
|
||||
# Add this back in once war ops/separate nukie inventories are added.
|
||||
#- type: uplinkListing
|
||||
# id: UplinkZombieBundle
|
||||
# category: Bundles
|
||||
# itemId: ClothingBackpackDuffelZombieBundle
|
||||
# price: 50
|
||||
# icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
|
||||
- type: listing
|
||||
id: UplinkZombieBundle
|
||||
icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
|
||||
productEntity: ClothingBackpackDuffelZombieBundle
|
||||
cost:
|
||||
Telecrystal: 40
|
||||
categories:
|
||||
- UplinkBundles
|
||||
conditions:
|
||||
- !type:StoreWhitelistCondition
|
||||
whitelist:
|
||||
tags:
|
||||
- NukeOpsUplink
|
||||
- !type:BuyerWhitelistCondition
|
||||
blacklist:
|
||||
components:
|
||||
- SurplusBundle
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSurplusBundle
|
||||
category: Bundles
|
||||
itemId: CrateSyndicateSurplusBundle
|
||||
description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good.
|
||||
price: 20
|
||||
surplus: false
|
||||
productEntity: CrateSyndicateSurplusBundle
|
||||
cost:
|
||||
Telecrystal: 20
|
||||
categories:
|
||||
- UplinkBundles
|
||||
conditions:
|
||||
- !type:StoreWhitelistCondition
|
||||
blacklist:
|
||||
tags:
|
||||
- NukeOpsUplink
|
||||
- !type:BuyerWhitelistCondition
|
||||
blacklist:
|
||||
components:
|
||||
- SurplusBundle
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSuperSurplusBundle
|
||||
category: Bundles
|
||||
itemId: CrateSyndicateSuperSurplusBundle
|
||||
description: Contains 125 telecrystals worth of completely random Syndicate items.
|
||||
price: 40
|
||||
surplus: false
|
||||
|
||||
#- type: uplinkListing
|
||||
# id: UplinkCarbineBundle
|
||||
# category: Bundles
|
||||
# itemId: ClothingBackpackDuffelSyndicateFilledCarbine
|
||||
# price: 35
|
||||
# icon: /Textures/Objects/Weapons/Guns/Rifles/carbine.rsi/icon.png
|
||||
productEntity: CrateSyndicateSuperSurplusBundle
|
||||
cost:
|
||||
Telecrystal: 40
|
||||
categories:
|
||||
- UplinkBundles
|
||||
conditions:
|
||||
- !type:StoreWhitelistCondition
|
||||
blacklist:
|
||||
tags:
|
||||
- NukeOpsUplink
|
||||
- !type:BuyerWhitelistCondition
|
||||
blacklist:
|
||||
components:
|
||||
- SurplusBundle
|
||||
|
||||
# Tools
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkToolbox
|
||||
category: Tools
|
||||
itemId: ToolboxSyndicateFilled
|
||||
price: 2
|
||||
productEntity: ToolboxSyndicateFilled
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkTools
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSyndicateJawsOfLife
|
||||
category: Tools
|
||||
itemId: SyndicateJawsOfLife
|
||||
price: 2
|
||||
productEntity: SyndicateJawsOfLife
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkTools
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkDuffelSurgery
|
||||
category: Tools
|
||||
itemId: ClothingBackpackDuffelSyndicateFilledMedical
|
||||
price: 5
|
||||
productEntity: ClothingBackpackDuffelSyndicateFilledMedical
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkTools
|
||||
|
||||
- type : uplinkListing
|
||||
- type: listing
|
||||
id: UplinkPowerSink
|
||||
category: Tools
|
||||
itemId: PowerSink
|
||||
price: 5
|
||||
productEntity: PowerSink
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkTools
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkCarpDehydrated
|
||||
category: Tools
|
||||
itemId: DehydratedSpaceCarp
|
||||
price: 3
|
||||
productEntity: DehydratedSpaceCarp
|
||||
cost:
|
||||
Telecrystal: 3
|
||||
categories:
|
||||
- UplinkTools
|
||||
|
||||
# Job Specific
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: uplinkGatfruitSeeds
|
||||
category: Job
|
||||
itemId: GatfruitSeeds
|
||||
description: And who says guns don't grow on trees?
|
||||
price: 4
|
||||
jobWhitelist:
|
||||
- Botanist
|
||||
productEntity: GatfruitSeeds
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkJob
|
||||
conditions:
|
||||
- !type:BuyerJobCondition
|
||||
whitelist:
|
||||
- Botanist
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: uplinkNecronomicon
|
||||
category: Job
|
||||
itemId: BibleNecronomicon
|
||||
description: An unholy book capable of summoning a demonic familiar.
|
||||
price: 6
|
||||
surplus: false
|
||||
jobWhitelist:
|
||||
- Chaplain
|
||||
productEntity: BibleNecronomicon
|
||||
cost:
|
||||
Telecrystal: 6
|
||||
categories:
|
||||
- UplinkJob
|
||||
conditions:
|
||||
- !type:BuyerJobCondition
|
||||
whitelist:
|
||||
- Chaplain
|
||||
- !type:BuyerWhitelistCondition
|
||||
blacklist:
|
||||
components:
|
||||
- SurplusBundle
|
||||
|
||||
# Armor
|
||||
|
||||
# Should be cameleon shoes, change when implemented.
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkClothingNoSlipsShoes
|
||||
category: Armor
|
||||
itemId: ClothingShoesChameleonNoSlips
|
||||
listingName: no-slip shoes
|
||||
name: no-slip shoes
|
||||
description: These protect you from slips while looking like normal sneakers.
|
||||
price: 2
|
||||
productEntity: ClothingShoesChameleonNoSlips
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkgClothingThievingGloves
|
||||
category: Armor
|
||||
itemId: ThievingGloves
|
||||
listingName: Thieving Gloves
|
||||
name: Thieving Gloves
|
||||
description: Discretely steal from pockets and increase your thieving technique with these fancy new gloves, all while looking like normal gloves!
|
||||
price: 4
|
||||
productEntity: ThievingGloves
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkClothingOuterVestWeb
|
||||
category: Armor
|
||||
itemId: ClothingOuterVestWeb
|
||||
price: 5
|
||||
productEntity: ClothingOuterVestWeb
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkHardsuitSyndie
|
||||
category: Armor
|
||||
itemId: ClothingBackpackDuffelSyndicateHardsuitBundle
|
||||
description: The Syndicate's well known armored blood red hardsuit, capable of space walks and bullet resistant.
|
||||
price: 8
|
||||
productEntity: ClothingBackpackDuffelSyndicateHardsuitBundle
|
||||
cost:
|
||||
Telecrystal: 8
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkClothingShoesBootsMagSyndie
|
||||
category: Armor
|
||||
itemId: ClothingShoesBootsMagSyndie
|
||||
description: A pair of magnetic boots that will keep you on the ground if the gravity fails or is sabotaged, giving you a mobility advantage. If activated with gravity they will protect from slips, but they will slow you down.
|
||||
price: 2
|
||||
productEntity: ClothingShoesBootsMagSyndie
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkEVASyndie
|
||||
category: Armor
|
||||
itemId: ClothingBackpackDuffelSyndicateEVABundle
|
||||
description: A simple EVA suit that offers no protection other than what's needed to survive in space.
|
||||
price: 4
|
||||
productEntity: ClothingBackpackDuffelSyndicateEVABundle
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkClothingOuterHardsuitJuggernaut
|
||||
category: Armor
|
||||
itemId: ClothingOuterHardsuitJuggernaut
|
||||
description: Hyper resilient armor made of materials tested in the Tau chromosphere facility. The only thing that's going to be slowing you down is this suit... and tasers.
|
||||
price: 12
|
||||
|
||||
productEntity: ClothingOuterHardsuitJuggernaut
|
||||
cost:
|
||||
Telecrystal: 12
|
||||
categories:
|
||||
- UplinkArmor
|
||||
|
||||
# Misc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkCyberpen
|
||||
category: Misc
|
||||
itemId: CyberPen
|
||||
description: Cybersun's legal department pen. Smells vaguely of hard-light and war profiteering.
|
||||
price: 4
|
||||
productEntity: CyberPen
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkDecoyDisk
|
||||
category: Misc
|
||||
itemId: NukeDiskFake
|
||||
listingName: Decoy nuclear disk
|
||||
name: decoy nuclear disk
|
||||
description: A piece of plastic with a lenticular printing, made to look like a nuclear auth disk.
|
||||
price: 1
|
||||
productEntity: NukeDiskFake
|
||||
cost:
|
||||
Telecrystal: 1
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkRevolverCapGun
|
||||
category: Misc
|
||||
itemId: RevolverCapGun
|
||||
price: 4
|
||||
productEntity: RevolverCapGun
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkCigarettes
|
||||
category: Misc
|
||||
itemId: CigPackSyndicate
|
||||
price: 2
|
||||
productEntity: CigPackSyndicate
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSoapSyndie
|
||||
category: Misc
|
||||
itemId: SoapSyndie
|
||||
price: 1
|
||||
productEntity: SoapSyndie
|
||||
cost:
|
||||
Telecrystal: 1
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkUltrabrightLantern
|
||||
category: Misc
|
||||
itemId: lanternextrabright
|
||||
price: 2
|
||||
productEntity: lanternextrabright #why is this item id not capitalized???
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
#- type: uplinkListing
|
||||
#- type: listing
|
||||
# id: UplinkCostumeCentcom
|
||||
# category: Misc
|
||||
# itemId: ClothingBackpackDuffelSyndicateCostumeCentcom
|
||||
# price: 4
|
||||
# productEntity: ClothingBackpackDuffelSyndicateCostumeCentcom
|
||||
# cost:
|
||||
# Telecrystal: 4
|
||||
# categories:
|
||||
# - UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkGigacancerScanner
|
||||
category: Misc
|
||||
itemId: HandheldHealthAnalyzerGigacancer
|
||||
listingName: Ultragigacancer Health Analyzer
|
||||
name: Ultragigacancer Health Analyzer
|
||||
description: Works like a normal health analyzer, other than giving everyone it scans ultragigacancer.
|
||||
price: 5
|
||||
productEntity: HandheldHealthAnalyzerGigacancer
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkNocturineChemistryBottle
|
||||
category: Misc
|
||||
itemId: NocturineChemistryBottle
|
||||
description: A chemical that makes it very hard for your target to stand up.
|
||||
price: 5
|
||||
productEntity: NocturineChemistryBottle
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkMisc
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSyndicateSegwayCrate
|
||||
category: Misc
|
||||
itemId: CrateFunSyndicateSegway
|
||||
listingName: syndicate segway
|
||||
name: syndicate segway
|
||||
description: Be an enemy of the corporation, in style!
|
||||
price: 5
|
||||
surplus: false
|
||||
productEntity: CrateFunSyndicateSegway
|
||||
cost:
|
||||
Telecrystal: 5
|
||||
categories:
|
||||
- UplinkMisc
|
||||
conditions:
|
||||
- !type:BuyerWhitelistCondition
|
||||
blacklist:
|
||||
components:
|
||||
- SurplusBundle
|
||||
|
||||
# Pointless
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkSyndicateStamp
|
||||
category: Pointless
|
||||
itemId: RubberStampSyndicate
|
||||
price: 2
|
||||
productEntity: RubberStampSyndicate
|
||||
cost:
|
||||
Telecrystal: 2
|
||||
categories:
|
||||
- UplinkPointless
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkCatEars
|
||||
category: Pointless
|
||||
itemId: ClothingHeadHatCatEars
|
||||
listingName: Cat Ears
|
||||
description: UwU.
|
||||
price: 21
|
||||
name: Cat Ears
|
||||
description: UwU
|
||||
productEntity: ClothingHeadHatCatEars
|
||||
cost:
|
||||
Telecrystal: 21
|
||||
categories:
|
||||
- UplinkPointless
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkOutlawHat
|
||||
category: Pointless
|
||||
itemId: ClothingHeadHatOutlawHat
|
||||
price: 1
|
||||
productEntity: ClothingHeadHatOutlawHat
|
||||
cost:
|
||||
Telecrystal: 1
|
||||
categories:
|
||||
- UplinkPointless
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkCostumePyjama
|
||||
category: Pointless
|
||||
itemId: ClothingBackpackDuffelSyndicatePyjamaBundle
|
||||
price: 4
|
||||
productEntity: ClothingBackpackDuffelSyndicatePyjamaBundle
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkPointless
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkCostumeClown
|
||||
category: Pointless
|
||||
itemId: ClothingBackpackDuffelSyndicateCostumeClown
|
||||
price: 4
|
||||
productEntity: ClothingBackpackDuffelSyndicateCostumeClown
|
||||
cost:
|
||||
Telecrystal: 4
|
||||
categories:
|
||||
- UplinkPointless
|
||||
|
||||
- type: uplinkListing
|
||||
- type: listing
|
||||
id: UplinkBalloon
|
||||
category: Pointless
|
||||
itemId: BalloonSyn
|
||||
price: 20
|
||||
productEntity: BalloonSyn
|
||||
cost:
|
||||
Telecrystal: 20
|
||||
categories:
|
||||
- UplinkPointless
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
interfaces:
|
||||
- key: enum.PDAUiKey.Key
|
||||
type: PDABoundUserInterface
|
||||
- key: enum.UplinkUiKey.Key
|
||||
type: UplinkBoundUserInterface
|
||||
- key: enum.StoreUiKey.Key
|
||||
type: StoreBoundUserInterface
|
||||
- key: enum.RingerUiKey.Key
|
||||
type: RingerBoundUserInterface
|
||||
- key: enum.InstrumentUiKey.Key
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
count: 20
|
||||
max: 999999 # todo: add support for unlimited stacks
|
||||
stackType: Telecrystal
|
||||
- type: Telecrystal
|
||||
- type: StackPrice
|
||||
price: 200
|
||||
- type: Currency
|
||||
price:
|
||||
Telecrystal: 1
|
||||
|
||||
- type: entity
|
||||
parent: Telecrystal
|
||||
@@ -61,45 +63,55 @@
|
||||
heldPrefix: old-radio
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
- key: enum.UplinkUiKey.Key
|
||||
type: UplinkBoundUserInterface
|
||||
- type: Uplink
|
||||
activatesInHands: true
|
||||
|
||||
- key: enum.StoreUiKey.Key
|
||||
type: StoreBoundUserInterface
|
||||
- type: ActivatableUI
|
||||
key: enum.StoreUiKey.Key
|
||||
- type: Store
|
||||
preset: StorePresetUplink
|
||||
balance:
|
||||
Telecrystal: 0
|
||||
|
||||
- type: entity
|
||||
parent: BaseUplinkRadio
|
||||
id: BaseUplinkRadio20TC
|
||||
suffix: 20 TC
|
||||
components:
|
||||
- type: Uplink
|
||||
presetInfo:
|
||||
balance: 20
|
||||
- type: Store
|
||||
preset: StorePresetUplink
|
||||
balance:
|
||||
Telecrystal: 20
|
||||
|
||||
|
||||
#Default Nuclear Operative amount, not considering crew count
|
||||
- type: entity
|
||||
parent: BaseUplinkRadio
|
||||
id: BaseUplinkRadio25TC
|
||||
suffix: 25 TC
|
||||
components:
|
||||
- type: Uplink
|
||||
presetInfo:
|
||||
balance: 25
|
||||
- type: Store
|
||||
preset: StorePresetUplink
|
||||
balance:
|
||||
Telecrystal: 25
|
||||
|
||||
#this uplink MUST be used for nukeops, as it has the tag for filtering the listing.
|
||||
- type: entity
|
||||
parent: BaseUplinkRadio
|
||||
id: BaseUplinkRadio40TC
|
||||
suffix: 40 TC
|
||||
suffix: 40 TC, NukeOps
|
||||
components:
|
||||
- type: Uplink
|
||||
presetInfo:
|
||||
balance: 40
|
||||
- type: Store
|
||||
preset: StorePresetUplink
|
||||
balance:
|
||||
Telecrystal: 40
|
||||
- type: Tag
|
||||
tags:
|
||||
- NukeOpsUplink
|
||||
|
||||
- type: entity
|
||||
parent: BaseUplinkRadio
|
||||
id: BaseUplinkRadioDebug
|
||||
suffix: Debug
|
||||
components:
|
||||
- type: Uplink
|
||||
presetInfo:
|
||||
balance: 9999
|
||||
- type: Store
|
||||
preset: StorePresetUplink
|
||||
balance:
|
||||
Telecrystal: 99999
|
||||
|
||||
60
Resources/Prototypes/Store/categories.yml
Normal file
60
Resources/Prototypes/Store/categories.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
#debug
|
||||
- type: storeCategory
|
||||
id: Debug
|
||||
name: debug category
|
||||
|
||||
- type: storeCategory
|
||||
id: Debug2
|
||||
name: debug category 2
|
||||
|
||||
#uplink categoires
|
||||
- type: storeCategory
|
||||
id: UplinkWeapons
|
||||
name: Weapons
|
||||
priority: 0
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkAmmo
|
||||
name: Ammo
|
||||
priority: 1
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkExplosives
|
||||
name: Explosives
|
||||
priority: 2
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkMisc
|
||||
name: Misc
|
||||
priority: 3
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkBundles
|
||||
name: Bundles
|
||||
priority: 4
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkTools
|
||||
name: Tools
|
||||
priority: 5
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkUtility
|
||||
name: Utility
|
||||
priority: 6
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkJob
|
||||
name: Job
|
||||
priority: 7
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkArmor
|
||||
name: Armor
|
||||
priority: 8
|
||||
|
||||
- type: storeCategory
|
||||
id: UplinkPointless
|
||||
name: Pointless
|
||||
priority: 9
|
||||
|
||||
12
Resources/Prototypes/Store/currency.yml
Normal file
12
Resources/Prototypes/Store/currency.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
- type: currency
|
||||
id: Telecrystal
|
||||
balanceDisplay: store-currency-balance-display-telecrystal
|
||||
priceDisplay: store-currency-price-display-telecrystal
|
||||
entityId: Telecrystal1
|
||||
canWithdraw: true
|
||||
|
||||
#debug
|
||||
- type: currency
|
||||
id: DebugDollar
|
||||
balanceDisplay: store-currency-balance-display-debugdollar
|
||||
priceDisplay: store-currency-price-display-debugdollar
|
||||
16
Resources/Prototypes/Store/presets.yml
Normal file
16
Resources/Prototypes/Store/presets.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
- type: storePreset
|
||||
id: StorePresetUplink
|
||||
storeName: Uplink
|
||||
categories:
|
||||
- UplinkWeapons
|
||||
- UplinkAmmo
|
||||
- UplinkExplosives
|
||||
- UplinkMisc
|
||||
- UplinkBundles
|
||||
- UplinkTools
|
||||
- UplinkUtility
|
||||
- UplinkJob
|
||||
- UplinkArmor
|
||||
- UplinkPointless
|
||||
currencyWhitelist:
|
||||
- Telecrystal
|
||||
@@ -332,6 +332,9 @@
|
||||
- type: Tag
|
||||
id: NoSpinOnThrow
|
||||
|
||||
- type: Tag
|
||||
id: NukeOpsUplink
|
||||
|
||||
- type: Tag
|
||||
id: Ointment
|
||||
|
||||
|
||||
Reference in New Issue
Block a user