Generalized Store System (#10201)

This commit is contained in:
Nemanja
2022-08-17 00:34:25 -04:00
committed by GitHub
parent 1b50928d50
commit 2152914acc
68 changed files with 2493 additions and 1568 deletions

View File

@@ -1,6 +1,4 @@
using Content.Client.Traitor.Uplink;
using Content.Shared.Revenant; using Content.Shared.Revenant;
using Content.Shared.Traitor.Uplink;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;

View 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();
}
}

View 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>

View 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;
}
}

View 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>

View 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;
}
}

View 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>

View 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -20,6 +20,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Server.Traitor; using Content.Server.Traitor;
using System.Data;
using Content.Server.Traitor.Uplink;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -35,6 +37,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!; [Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
private Dictionary<Mind.Mind, bool> _aliveNukeops = new(); private Dictionary<Mind.Mind, bool> _aliveNukeops = new();
private bool _opsWon; private bool _opsWon;

View File

@@ -8,7 +8,6 @@ using Content.Server.Station.Components;
using Content.Server.Suspicion; using Content.Server.Suspicion;
using Content.Server.Suspicion.Roles; using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Content.Shared.EntityList; using Content.Shared.EntityList;
@@ -17,7 +16,6 @@ using Content.Shared.Maps;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Suspicion; using Content.Shared.Suspicion;
using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Audio; using Robust.Shared.Audio;
@@ -48,6 +46,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!; [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly SharedDoorSystem _doorSystem = default!; [Dependency] private readonly SharedDoorSystem _doorSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "Suspicion"; public override string Prototype => "Suspicion";
@@ -173,16 +172,8 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
mind!.AddRole(traitorRole); mind!.AddRole(traitorRole);
traitors.Add(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 // try to place uplink
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>() if (!_uplink.AddUplink(mind.OwnedEntity!.Value, traitorStartingBalance))
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
continue; continue;
} }

View File

@@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Configurations; using Content.Server.GameTicking.Rules.Configurations;
@@ -6,19 +6,18 @@ using Content.Server.Hands.Components;
using Content.Server.PDA; using Content.Server.PDA;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.Spawners.Components; using Content.Server.Spawners.Components;
using Content.Server.Store.Components;
using Content.Server.Traitor; using Content.Server.Traitor;
using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account;
using Content.Server.Traitor.Uplink.Components;
using Content.Server.TraitorDeathMatch.Components; using Content.Server.TraitorDeathMatch.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
@@ -39,6 +38,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
[Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "TraitorDeathMatch"; public override string Prototype => "TraitorDeathMatch";
@@ -48,7 +48,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
private bool _safeToEndRound = false; private bool _safeToEndRound = false;
private readonly Dictionary<UplinkAccount, string> _allOriginalNames = new(); private readonly Dictionary<EntityUid, string> _allOriginalNames = new();
private const string TraitorPrototypeID = "Traitor"; private const string TraitorPrototypeID = "Traitor";
@@ -108,15 +108,10 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
newTmp = Spawn(BackpackPrototypeName, ownedCoords); newTmp = Spawn(BackpackPrototypeName, ownedCoords);
_inventory.TryEquip(owned, newTmp, "back", true); _inventory.TryEquip(owned, newTmp, "back", true);
// Like normal traitors, they need access to a traitor account. if (!_uplink.AddUplink(owned, startingBalance))
var uplinkAccount = new UplinkAccount(startingBalance, owned); return;
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
accounts.AddNewAccount(uplinkAccount);
EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>() _allOriginalNames[owned] = Name(owned);
.AddUplink(owned, uplinkAccount, newPDA);
_allOriginalNames[uplinkAccount] = Name(owned);
// The PDA needs to be marked with the correct owner. // The PDA needs to be marked with the correct owner.
var pda = Comp<PDAComponent>(newPDA); var pda = Comp<PDAComponent>(newPDA);
@@ -186,14 +181,17 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
var lines = new List<string>(); var lines = new List<string>();
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line")); 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; var owner = uplink.AccountOwner;
if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc)) if (owner != null && _allOriginalNames.ContainsKey(owner.Value))
{ {
var tcbalance = _uplink.GetTCBalance(uplink);
lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry", lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
("originalName", _allOriginalNames[uplinkAcc]), ("originalName", _allOriginalNames[owner.Value]),
("tcBalance", uplinkAcc.Balance))); ("tcBalance", tcbalance)));
} }
} }

View File

@@ -3,13 +3,14 @@ using Content.Server.Chat.Managers;
using Content.Server.Objectives.Interfaces; using Content.Server.Objectives.Interfaces;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.Store.Systems;
using Content.Server.Traitor; using Content.Server.Traitor;
using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Traitor.Uplink;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
@@ -28,6 +29,10 @@ public sealed class TraitorRuleSystem : GameRuleSystem
[Dependency] private readonly IObjectivesManager _objectivesManager = default!; [Dependency] private readonly IObjectivesManager _objectivesManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly GameTicker _gameTicker = 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"; public override string Prototype => "Traitor";
@@ -35,6 +40,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
public List<TraitorRole> Traitors = new(); public List<TraitorRole> Traitors = new();
private const string TraitorPrototypeID = "Traitor"; private const string TraitorPrototypeID = "Traitor";
private const string TraitorUplinkPresetId = "StorePresetUplink";
public int TotalTraitors => Traitors.Count; public int TotalTraitors => Traitors.Count;
public string[] Codewords = new string[3]; public string[] Codewords = new string[3];
@@ -173,16 +179,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem
} }
// creadth: we need to create uplink for the antag. // creadth: we need to create uplink for the antag.
// PDA should be in place already, so we just need to // PDA should be in place already
// initiate uplink account.
DebugTools.AssertNotNull(mind.OwnedEntity); DebugTools.AssertNotNull(mind.OwnedEntity);
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance); 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; return false;
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID); var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);

View File

@@ -2,29 +2,28 @@ using Content.Server.Instruments;
using Content.Server.Light.Components; using Content.Server.Light.Components;
using Content.Server.Light.EntitySystems; using Content.Server.Light.EntitySystems;
using Content.Server.Light.Events; 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.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.Station.Systems;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Shared.PDA; using Content.Shared.PDA;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Map; using Robust.Shared.Map;
using Content.Server.Mind.Components;
using Content.Server.Traitor;
namespace Content.Server.PDA namespace Content.Server.PDA
{ {
public sealed class PDASystem : SharedPDASystem 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 UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
[Dependency] private readonly RingerSystem _ringerSystem = default!; [Dependency] private readonly RingerSystem _ringerSystem = default!;
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!; [Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StoreSystem _storeSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -32,8 +31,8 @@ namespace Content.Server.PDA
SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle); SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle);
SubscribeLocalEvent<PDAComponent, AfterActivatableUIOpenEvent>(AfterUIOpen); SubscribeLocalEvent<PDAComponent, AfterActivatableUIOpenEvent>(AfterUIOpen);
SubscribeLocalEvent<PDAComponent, UplinkInitEvent>(OnUplinkInit); SubscribeLocalEvent<PDAComponent, StoreAddedEvent>(OnUplinkInit);
SubscribeLocalEvent<PDAComponent, UplinkRemovedEvent>(OnUplinkRemoved); SubscribeLocalEvent<PDAComponent, StoreRemovedEvent>(OnUplinkRemoved);
SubscribeLocalEvent<PDAComponent, GridModifiedEvent>(OnGridChanged); SubscribeLocalEvent<PDAComponent, GridModifiedEvent>(OnGridChanged);
} }
@@ -74,12 +73,12 @@ namespace Content.Server.PDA
UpdatePDAUserInterface(pda); UpdatePDAUserInterface(pda);
} }
private void OnUplinkInit(EntityUid uid, PDAComponent pda, UplinkInitEvent args) private void OnUplinkInit(EntityUid uid, PDAComponent pda, StoreAddedEvent args)
{ {
UpdatePDAUserInterface(pda); UpdatePDAUserInterface(pda);
} }
private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, UplinkRemovedEvent args) private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, StoreRemovedEvent args)
{ {
UpdatePDAUserInterface(pda); 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 // 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. // a player entity.
if (!HasComp<UplinkComponent>(pda.Owner)) if (!TryComp<StoreComponent>(pda.Owner, out var storeComponent))
return; return;
var uplinkState = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, hasInstrument); 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) if (session.AttachedEntity is not EntityUid { Valid: true } user)
continue; 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); ui.SetState(uplinkState, session);
} }
} }
@@ -143,8 +143,9 @@ namespace Content.Server.PDA
case PDAShowUplinkMessage _: case PDAShowUplinkMessage _:
{ {
if (EntityManager.TryGetComponent(pda.Owner, out UplinkComponent? uplink)) if (msg.Session.AttachedEntity != null &&
_uplinkSystem.ToggleUplinkUI(uplink, msg.Session); TryComp<StoreComponent>(pda.Owner, out var store))
_storeSystem.ToggleUi(msg.Session.AttachedEntity.Value, store);
break; break;
} }
case PDAShowRingtoneMessage _: case PDAShowRingtoneMessage _:
@@ -170,8 +171,13 @@ namespace Content.Server.PDA
private void AfterUIOpen(EntityUid uid, PDAComponent pda, AfterActivatableUIOpenEvent args) 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. // 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; return;
if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui)) if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui))

View 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();
}

View 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 { };

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,8 +1,7 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Traitor.Uplink.Account;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Traitor.Uplink; using Content.Shared.FixedPoint;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Console; using Robust.Shared.Console;
@@ -82,15 +81,10 @@ namespace Content.Server.Traitor.Uplink.Commands
// Get TC count // Get TC count
var configManager = IoCManager.Resolve<IConfigurationManager>(); var configManager = IoCManager.Resolve<IConfigurationManager>();
var tcCount = configManager.GetCVar(CCVars.TraitorStartingBalance); var tcCount = configManager.GetCVar(CCVars.TraitorStartingBalance);
Logger.Debug(entityManager.ToPrettyString(user));
// Get account
var uplinkAccount = new UplinkAccount(tcCount, user);
var accounts = entityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
accounts.AddNewAccount(uplinkAccount);
// Finally add uplink // Finally add uplink
if (!entityManager.EntitySysManager.GetEntitySystem<UplinkSystem>() var uplinkSys = entityManager.EntitySysManager.GetEntitySystem<UplinkSystem>();
.AddUplink(user, uplinkAccount, uplinkEntity)) if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity))
{ {
shell.WriteLine(Loc.GetString("add-uplink-command-error-2")); shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
return; return;

View File

@@ -1,3 +1,6 @@
using Content.Shared.Store;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Traitor.Uplink.SurplusBundle; namespace Content.Server.Traitor.Uplink.SurplusBundle;
/// <summary> /// <summary>
@@ -12,4 +15,11 @@ public sealed class SurplusBundleComponent : Component
[ViewVariables(VVAccess.ReadOnly)] [ViewVariables(VVAccess.ReadOnly)]
[DataField("totalPrice")] [DataField("totalPrice")]
public int TotalPrice = 20; 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";
} }

View File

@@ -1,7 +1,8 @@
using System.Linq; using System.Linq;
using Content.Server.Storage.Components; using Content.Server.Store.Systems;
using Content.Server.Storage.EntitySystems; using Content.Server.Storage.EntitySystems;
using Content.Shared.PDA; using Content.Shared.Store;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
@@ -12,23 +13,25 @@ public sealed class SurplusBundleSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityStorageSystem _entityStorage = 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SurplusBundleComponent, MapInitEvent>(OnMapInit); 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 var storePreset = _prototypeManager.Index<StorePresetPrototype>(component.StorePreset);
_uplinks = _prototypeManager.EnumeratePrototypes<UplinkStoreListingPrototype>()
.Where(item => item.CanSurplus).ToArray(); _listings = _store.GetAvailableListings(uid, null, storePreset.Categories).ToArray();
Array.Sort(_uplinks, (a, b) => b.Price - a.Price);
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) private void OnMapInit(EntityUid uid, SurplusBundleComponent component, MapInitEvent args)
@@ -46,19 +49,19 @@ public sealed class SurplusBundleSystem : EntitySystem
var content = GetRandomContent(component.TotalPrice); var content = GetRandomContent(component.TotalPrice);
foreach (var item in content) foreach (var item in content)
{ {
var ent = EntityManager.SpawnEntity(item.ItemId, cords); var ent = EntityManager.SpawnEntity(item.ProductEntity, cords);
_entityStorage.Insert(ent, component.Owner); _entityStorage.Insert(ent, component.Owner);
} }
} }
// wow, is this leetcode reference? // wow, is this leetcode reference?
private List<UplinkStoreListingPrototype> GetRandomContent(int targetCost) private List<ListingData> GetRandomContent(FixedPoint2 targetCost)
{ {
var ret = new List<UplinkStoreListingPrototype>(); var ret = new List<ListingData>();
if (_uplinks.Length == 0) if (_listings.Length == 0)
return ret; return ret;
var totalCost = 0; var totalCost = FixedPoint2.Zero;
var index = 0; var index = 0;
while (totalCost < targetCost) while (totalCost < targetCost)
{ {
@@ -66,10 +69,10 @@ public sealed class SurplusBundleSystem : EntitySystem
// Find new item with the lowest acceptable price // Find new item with the lowest acceptable price
// All expansive items will be before index, all acceptable after // All expansive items will be before index, all acceptable after
var remainingBudget = targetCost - totalCost; var remainingBudget = targetCost - totalCost;
while (_uplinks[index].Price > remainingBudget) while (_listings[index].Cost.Values.Sum() > remainingBudget)
{ {
index++; index++;
if (index >= _uplinks.Length) if (index >= _listings.Length)
{ {
// Looks like no cheap items left // Looks like no cheap items left
// It shouldn't be case for ss14 content // It shouldn't be case for ss14 content
@@ -79,10 +82,10 @@ public sealed class SurplusBundleSystem : EntitySystem
} }
// Select random listing and add into crate // Select random listing and add into crate
var randomIndex = _random.Next(index, _uplinks.Length); var randomIndex = _random.Next(index, _listings.Length);
var randomItem = _uplinks[randomIndex]; var randomItem = _listings[randomIndex];
ret.Add(randomItem); ret.Add(randomItem);
totalCost += randomItem.Price; totalCost += randomItem.Cost.Values.Sum();
} }
return ret; return ret;

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Traitor.Uplink.Telecrystal
{
[RegisterComponent]
public sealed class TelecrystalComponent : Component
{
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,230 +1,41 @@
using System.Linq; using Content.Server.Store.Systems;
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.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Traitor.Uplink; using Content.Server.Store.Components;
using Robust.Server.GameObjects; using Content.Shared.FixedPoint;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Player;
namespace Content.Server.Traitor.Uplink namespace Content.Server.Traitor.Uplink
{ {
public sealed class UplinkSystem : EntitySystem 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 InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = 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(); FixedPoint2? tcBalance = component.Balance.GetValueOrDefault(TelecrystalCurrencyPrototype);
return tcBalance != null ? tcBalance.Value.Int() : 0;
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);
} }
public void SetAccount(UplinkComponent component, UplinkAccount account) /// <summary>
{ /// Adds an uplink to the target
if (component.UplinkAccount != null) /// </summary>
{ /// <param name="user">The person who is getting the uplink</param>
Logger.Error("Can't init one uplink with different account!"); /// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
return; /// <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>
component.UplinkAccount = account; public bool AddUplink(EntityUid user, FixedPoint2? balance, string uplinkPresetId = "StorePresetUplink", EntityUid? uplinkEntity = null)
}
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)
{ {
// Try to find target item // Try to find target item
if (uplinkEntity == null) if (uplinkEntity == null)
@@ -234,11 +45,17 @@ namespace Content.Server.Traitor.Uplink
return false; return false;
} }
var uplink = uplinkEntity.Value.EnsureComponent<UplinkComponent>(); var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
SetAccount(uplink, account); _store.InitializeFromPreset(uplinkPresetId, store);
store.AccountOwner = user;
store.Balance.Clear();
if (!HasComp<PDAComponent>(uplinkEntity.Value)) if (balance != null)
uplink.ActivatesInHands = true; {
store.Balance.Clear();
_store.TryAddCurrency(
new Dictionary<string, FixedPoint2>() { { TelecrystalCurrencyPrototype, balance.Value } }, store);
}
// TODO add BUI. Currently can't be done outside of yaml -_- // 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) private EntityUid? FindUplinkTarget(EntityUid user)
{ {
// Try to find PDA in inventory // Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{ {
while (containerSlotEnumerator.MoveNext(out var pdaUid)) while (containerSlotEnumerator.MoveNext(out var pdaUid))
{ {
if (!pdaUid.ContainedEntity.HasValue) continue; 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; return pdaUid.ContainedEntity.Value;
} }
} }
@@ -263,7 +79,7 @@ namespace Content.Server.Traitor.Uplink
// Also check hands // Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user)) foreach (var item in _handsSystem.EnumerateHeld(user))
{ {
if (HasComp<PDAComponent>(item)) if (HasComp<PDAComponent>(item) || HasComp<StoreComponent>(item))
return item; return item;
} }

View File

@@ -1,7 +1,9 @@
using Content.Server.Mind.Components; using Content.Server.Mind.Components;
using Content.Server.Traitor.Uplink.Account;
using Content.Server.Traitor.Uplink.Components;
using Content.Server.TraitorDeathMatch.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.Interaction;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Popups; using Content.Shared.Popups;
@@ -12,8 +14,11 @@ namespace Content.Server.TraitorDeathMatch;
public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
{ {
[Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly UplinkAccountsSystem _uplink = default!;
[Dependency] private readonly SharedPopupSystem _popup = 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() public override void Initialize()
{ {
@@ -43,7 +48,7 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
return; return;
} }
if (!EntityManager.TryGetComponent<UplinkComponent>(args.Used, out var victimUplink)) if (!EntityManager.TryGetComponent<StoreComponent>(args.Used, out var victimUplink))
{ {
_popup.PopupEntity(Loc.GetString( _popup.PopupEntity(Loc.GetString(
"traitor-death-match-redemption-component-interact-using-main-message", "traitor-death-match-redemption-component-interact-using-main-message",
@@ -72,10 +77,10 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
return; return;
} }
UplinkComponent? userUplink = null; StoreComponent? userUplink = null;
if (_inventory.TryGetSlotEntity(args.User, "id", out var pdaUid) && 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; userUplink = userUplinkComponent;
if (userUplink == null) if (userUplink == null)
@@ -88,35 +93,13 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
return; return;
} }
// We have finally determined both PDA components. FINALLY. // We have finally determined both PDA components. FINALLY.
var userAccount = userUplink.UplinkAccount; // 4 is the per-PDA bonus amount
var victimAccount = victimUplink.UplinkAccount; var transferAmount = _uplink.GetTCBalance(victimUplink) + 4;
victimUplink.Balance.Clear();
if (userAccount == null) _store.TryAddCurrency(new Dictionary<string, FixedPoint2>() { {"Telecrystal", FixedPoint2.New(transferAmount)}}, userUplink);
{
_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);
EntityManager.DeleteEntity(victimUplink.Owner); EntityManager.DeleteEntity(victimUplink.Owner);

View File

@@ -75,4 +75,5 @@ public enum LogType
Gib = 70, Gib = 70,
Identity = 71, Identity = 71,
CableCut = 72, CableCut = 72,
StorePurchase = 73,
} }

View File

@@ -1,17 +0,0 @@
namespace Content.Shared.PDA
{
public enum UplinkCategory
{
Weapons,
Ammo,
Explosives,
Misc,
Bundles,
Tools,
Utility,
Job,
Armor,
Pointless,
}
}

View File

@@ -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;
}
}

View 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;
}

View 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);

View 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!;
}

View 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;
}

View 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();
}

View 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,10 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public enum UplinkUiKey : byte
{
Key
}
}

View 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

View File

@@ -0,0 +1,4 @@
store-ui-default-title = Store
store-ui-default-withdraw-text = Withdraw
store-withdraw-button-ui = Withdraw {$currency}

View 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

View File

@@ -1,500 +1,641 @@
# Guns # Guns
- type: uplinkListing - type: listing
id: UplinkPistolViper id: UplinkPistolViper
category: Weapons name: Viper
itemId: WeaponPistolViper
listingName: Viper
description: A small, easily concealable, but somewhat underpowered gun. Use pistol magazines (.35 auto). 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 id: UplinkRevolverPython
category: Weapons name: Python
itemId: WeaponRevolverPython
listingName: Python
description: A loud and deadly revolver. Uses .40 Magnum. 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. # Inbuilt suppressor so it's sneaky + more expensive.
- type: uplinkListing - type: listing
id: UplinkPistolCobra id: UplinkPistolCobra
category: Weapons name: Cobra
itemId: WeaponPistolCobra
listingName: Cobra
description: A rugged, robust operator handgun with inbuilt silencer. Use pistol magazines (.25 caseless). 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 # Poor accuracy, slow to fire, cheap option
- type: uplinkListing - type: listing
id: UplinkRifleMosin id: UplinkRifleMosin
category: Weapons name: Surplus Rifle
itemId: WeaponSniperMosin
listingName: 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. 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 - type: listing
# id: UplinkCrossbowEnergyMini
# category: Weapons
# itemId: CrossbowEnergyMini
# price: 8
- type: uplinkListing
id: UplinkEsword id: UplinkEsword
category: Weapons name: Energy Sword
itemId: EnergySword
listingName: 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. 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: listing
#- type: uplinkListing
# id: UplinkDoubleBladedESword
# category: Weapons
# itemId: DoubleBladedESword
# price: 16
- type: uplinkListing
id: UplinkEnergyDagger id: UplinkEnergyDagger
category: Weapons name: Energy Dagger
itemId: EnergyDagger
listingName: Energy Dagger
description: A small energy blade conveniently disguised in the form of a pen. 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 id: UplinkFireAxeFlaming
category: Weapons name: Fire Axe
itemId: FireAxeFlaming
listingName: Fire Axe
description: A classic-style weapon infused with advanced atmos technology to allow it to set targets on fire. 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 # Explosives
- type: uplinkListing - type: listing
id: UplinkExplosiveGrenade id: UplinkExplosiveGrenade
category: Explosives productEntity: ExGrenade
itemId: ExGrenade cost:
price: 4 Telecrystal: 4
categories:
- UplinkExplosives
- type: uplinkListing - type: listing
id: UplinkExplosiveGrenadeFlash id: UplinkExplosiveGrenadeFlash
category: Explosives productEntity: GrenadeFlashBang
itemId: GrenadeFlashBang cost:
price: 2 Telecrystal: 2
categories:
- UplinkExplosives
- type: uplinkListing - type: listing
id: UplinkSyndieMiniBomb id: UplinkSyndieMiniBomb
category: Explosives productEntity: SyndieMiniBomb
itemId: SyndieMiniBomb cost:
price: 7 Telecrystal: 7
categories:
- UplinkExplosives
- type: uplinkListing - type: listing
id: UplinkGrenadePenguin id: UplinkGrenadePenguin
category: Explosives productEntity: MobGrenadePenguin
itemId: MobGrenadePenguin cost:
price: 6 Telecrystal: 6
surplus: false # got wrecked by penguins from surplus crate categories:
- UplinkExplosives
conditions:
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
- type: uplinkListing - type: listing
id: UplinkC4 id: UplinkC4
category: Explosives
itemId: C4
price: 2
description: > description: >
C-4 is plastic explosive of the common variety Composition C. You can use it to breach walls, airlocks or sabotage equipment. 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. 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 id: UplinkC4Bundle
category: Explosives
itemId: ClothingBackpackDuffelSyndicateC4tBundle
price: 12 # 25% off
description: Because sometimes quantity is quality. Contains 8 C-4 plastic explosives. description: Because sometimes quantity is quality. Contains 8 C-4 plastic explosives.
productEntity: ClothingBackpackDuffelSyndicateC4tBundle
cost:
Telecrystal: 12
categories:
- UplinkExplosives
# Ammo # Ammo
- type: uplinkListing - type: listing
id: UplinkPistol9mmMagazine id: UplinkPistol9mmMagazine
category: Ammo productEntity: MagazinePistol
itemId: MagazinePistol cost:
price: 2 Telecrystal: 2
categories:
- UplinkAmmo
# For the Mandella # For the Mandella
- type: uplinkListing - type: listing
id: UplinkMagazinePistolCaselessRifle id: UplinkMagazinePistolCaselessRifle
category: Ammo productEntity: MagazinePistolCaselessRifle
itemId: MagazinePistolCaselessRifle cost:
price: 2 Telecrystal: 2
categories:
- UplinkAmmo
# For the Inspector # For the Inspector
- type: uplinkListing - type: listing
id: UplinkSpeedLoaderMagnum id: UplinkSpeedLoaderMagnum
category: Ammo
itemId: SpeedLoaderMagnum
price: 2
icon: /Textures/Objects/Weapons/Guns/Ammunition/SpeedLoaders/Magnum/magnum_speed_loader.rsi/base.png icon: /Textures/Objects/Weapons/Guns/Ammunition/SpeedLoaders/Magnum/magnum_speed_loader.rsi/base.png
productEntity: SpeedLoaderMagnum
cost:
Telecrystal: 2
categories:
- UplinkAmmo
# For the mosin # For the mosin
- type: uplinkListing - type: listing
id: UplinkMosinAmmo id: UplinkMosinAmmo
category: Ammo
itemId: BoxMagazineLightRifle
description: A box of cartridges for the surplus rifle. description: A box of cartridges for the surplus rifle.
price: 2 productEntity: BoxMagazineLightRifle
cost:
Telecrystal: 2
categories:
- UplinkAmmo
#Utility #Utility
- type: uplinkListing - type: listing
id: UplinkHoloparaKit id: UplinkHoloparaKit
category: Utility name: Holoparasite Kit
itemId: BoxHoloparasite
listingName: 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. 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 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 id: UplinkHolster
category: Utility name: Syndicate Shoulder Holster
itemId: ClothingBeltSyndieHolster
listingName: Syndicate Shoulder Holster
description: A deep shoulder holster capable of holding many types of ballistics. description: A deep shoulder holster capable of holding many types of ballistics.
icon: /Textures/Clothing/Belt/syndieholster.rsi/icon.png productEntity: ClothingBeltSyndieHolster
price: 2 cost:
Telecrystal: 2
categories:
- UplinkUtility
- type: uplinkListing - type: listing
id: UplinkEmag id: UplinkEmag
category: Utility name: Emag
itemId: 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. 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 productEntity: Emag
price: 8 cost:
Telecrystal: 8
categories:
- UplinkUtility
- type: uplinkListing - type: listing
id: UplinkAgentIDCard id: UplinkAgentIDCard
category: Utility name: Agent ID Card
itemId: AgentIDCard
listingName: Agent ID Card
description: A modified ID card that can copy accesses from other cards and change its name and job title at-will. 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 productEntity: AgentIDCard
price: 3 cost:
Telecrystal: 3
categories:
- UplinkUtility
- type: uplinkListing - type: listing
id: UplinkJetpack id: UplinkJetpack
category: Utility name: Black Jetpack
itemId: JetpackBlack
listingName: Black Jetpack
description: A black jetpack. It allows you to fly around in space. Additional fuel not included. description: A black jetpack. It allows you to fly around in space. Additional fuel not included.
icon: Objects/Tanks/Jetpacks/black.rsi/icon.png productEntity: JetpackBlack
price: 5 cost:
Telecrystal: 5
categories:
- UplinkUtility
- type: uplinkListing - type: listing
id: ReinforcementTeleporterSyndicate id: UplinkReinforcementTeleporterSyndicate
category: Utility name: Reinforcement Teleporter
itemId: ReinforcementTeleporterSyndicate
listingName: 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. 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 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 #TODO: Increase the price of this to 4-5/remove it when we get encrpytion keys
- type: uplinkListing - type: listing
id: UplinkHeadset id: UplinkHeadset
category: Utility name: Syndicate Overear-Headset
itemId: ClothingHeadsetAltSyndicate
listingName: Syndicate Overear-Headset
description: A headset that allows you to listen in on departmental channels, or contact other traitors. 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 productEntity: ClothingHeadsetAltSyndicate
price: 2 cost:
Telecrystal: 2
categories:
- UplinkUtility
- type: uplinkListing - type: listing
id: UplinkHypopen id: UplinkHypopen
category: Utility name: Hypopen
itemId: Hypopen
listingName: Hypopen
description: A chemical hypospray disguised as a pen, capable of instantly injecting up to 15u of reagents. Starts empty. 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 # Bundles
- type: uplinkListing - type: listing
id: UplinkC20RBundle id: UplinkC20RBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledSMG
price: 25
icon: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi/icon.png icon: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledSMG
cost:
Telecrystal: 25
categories:
- UplinkBundles
- type: uplinkListing - type: listing
id: UplinkBulldogBundle id: UplinkBulldogBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledShotgun
price: 25
icon: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi/icon.png icon: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledShotgun
cost:
Telecrystal: 25
categories:
- UplinkBundles
- type: uplinkListing - type: listing
id: UplinkGrenadeLauncherBundle id: UplinkGrenadeLauncherBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
price: 30
icon: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi/icon.png icon: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
cost:
Telecrystal: 30
categories:
- UplinkBundles
- type: uplinkListing - type: listing
id: UplinkL6SawBundle id: UplinkL6SawBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledLMG
price: 40
icon: /Textures/Objects/Weapons/Guns/LMGs/l6.rsi/icon.png 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: listing
#- type: uplinkListing id: UplinkZombieBundle
# id: UplinkZombieBundle icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
# category: Bundles productEntity: ClothingBackpackDuffelZombieBundle
# itemId: ClothingBackpackDuffelZombieBundle cost:
# price: 50 Telecrystal: 40
# icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png categories:
- UplinkBundles
conditions:
- !type:StoreWhitelistCondition
whitelist:
tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
- type: uplinkListing - type: listing
id: UplinkSurplusBundle id: UplinkSurplusBundle
category: Bundles
itemId: CrateSyndicateSurplusBundle
description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good. description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good.
price: 20 productEntity: CrateSyndicateSurplusBundle
surplus: false cost:
Telecrystal: 20
categories:
- UplinkBundles
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
- type: uplinkListing - type: listing
id: UplinkSuperSurplusBundle id: UplinkSuperSurplusBundle
category: Bundles
itemId: CrateSyndicateSuperSurplusBundle
description: Contains 125 telecrystals worth of completely random Syndicate items. description: Contains 125 telecrystals worth of completely random Syndicate items.
price: 40 productEntity: CrateSyndicateSuperSurplusBundle
surplus: false cost:
Telecrystal: 40
#- type: uplinkListing categories:
# id: UplinkCarbineBundle - UplinkBundles
# category: Bundles conditions:
# itemId: ClothingBackpackDuffelSyndicateFilledCarbine - !type:StoreWhitelistCondition
# price: 35 blacklist:
# icon: /Textures/Objects/Weapons/Guns/Rifles/carbine.rsi/icon.png tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
# Tools # Tools
- type: uplinkListing - type: listing
id: UplinkToolbox id: UplinkToolbox
category: Tools productEntity: ToolboxSyndicateFilled
itemId: ToolboxSyndicateFilled cost:
price: 2 Telecrystal: 2
categories:
- UplinkTools
- type: uplinkListing - type: listing
id: UplinkSyndicateJawsOfLife id: UplinkSyndicateJawsOfLife
category: Tools productEntity: SyndicateJawsOfLife
itemId: SyndicateJawsOfLife cost:
price: 2 Telecrystal: 2
categories:
- UplinkTools
- type: uplinkListing - type: listing
id: UplinkDuffelSurgery id: UplinkDuffelSurgery
category: Tools productEntity: ClothingBackpackDuffelSyndicateFilledMedical
itemId: ClothingBackpackDuffelSyndicateFilledMedical cost:
price: 5 Telecrystal: 5
categories:
- UplinkTools
- type : uplinkListing - type: listing
id: UplinkPowerSink id: UplinkPowerSink
category: Tools productEntity: PowerSink
itemId: PowerSink cost:
price: 5 Telecrystal: 5
categories:
- UplinkTools
- type: uplinkListing - type: listing
id: UplinkCarpDehydrated id: UplinkCarpDehydrated
category: Tools productEntity: DehydratedSpaceCarp
itemId: DehydratedSpaceCarp cost:
price: 3 Telecrystal: 3
categories:
- UplinkTools
# Job Specific # Job Specific
- type: uplinkListing - type: listing
id: uplinkGatfruitSeeds id: uplinkGatfruitSeeds
category: Job
itemId: GatfruitSeeds
description: And who says guns don't grow on trees? description: And who says guns don't grow on trees?
price: 4 productEntity: GatfruitSeeds
jobWhitelist: cost:
Telecrystal: 4
categories:
- UplinkJob
conditions:
- !type:BuyerJobCondition
whitelist:
- Botanist - Botanist
- type: uplinkListing - type: listing
id: uplinkNecronomicon id: uplinkNecronomicon
category: Job
itemId: BibleNecronomicon
description: An unholy book capable of summoning a demonic familiar. description: An unholy book capable of summoning a demonic familiar.
price: 6 productEntity: BibleNecronomicon
surplus: false cost:
jobWhitelist: Telecrystal: 6
categories:
- UplinkJob
conditions:
- !type:BuyerJobCondition
whitelist:
- Chaplain - Chaplain
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
# Armor # Armor
# Should be cameleon shoes, change when implemented. # Should be cameleon shoes, change when implemented.
- type: uplinkListing - type: listing
id: UplinkClothingNoSlipsShoes id: UplinkClothingNoSlipsShoes
category: Armor name: no-slip shoes
itemId: ClothingShoesChameleonNoSlips
listingName: no-slip shoes
description: These protect you from slips while looking like normal sneakers. 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 id: UplinkgClothingThievingGloves
category: Armor name: Thieving Gloves
itemId: ThievingGloves
listingName: Thieving Gloves
description: Discretely steal from pockets and increase your thieving technique with these fancy new gloves, all while looking like normal 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 id: UplinkClothingOuterVestWeb
category: Armor productEntity: ClothingOuterVestWeb
itemId: ClothingOuterVestWeb cost:
price: 5 Telecrystal: 5
categories:
- UplinkArmor
- type: uplinkListing - type: listing
id: UplinkHardsuitSyndie id: UplinkHardsuitSyndie
category: Armor
itemId: ClothingBackpackDuffelSyndicateHardsuitBundle
description: The Syndicate's well known armored blood red hardsuit, capable of space walks and bullet resistant. 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 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. 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 id: UplinkEVASyndie
category: Armor
itemId: ClothingBackpackDuffelSyndicateEVABundle
description: A simple EVA suit that offers no protection other than what's needed to survive in space. 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 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. 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 # Misc
- type: uplinkListing - type: listing
id: UplinkCyberpen id: UplinkCyberpen
category: Misc
itemId: CyberPen
description: Cybersun's legal department pen. Smells vaguely of hard-light and war profiteering. 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 id: UplinkDecoyDisk
category: Misc name: decoy nuclear disk
itemId: NukeDiskFake
listingName: Decoy nuclear disk
description: A piece of plastic with a lenticular printing, made to look like a nuclear auth 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 id: UplinkRevolverCapGun
category: Misc productEntity: RevolverCapGun
itemId: RevolverCapGun cost:
price: 4 Telecrystal: 4
categories:
- UplinkMisc
- type: uplinkListing - type: listing
id: UplinkCigarettes id: UplinkCigarettes
category: Misc productEntity: CigPackSyndicate
itemId: CigPackSyndicate cost:
price: 2 Telecrystal: 2
categories:
- UplinkMisc
- type: uplinkListing - type: listing
id: UplinkSoapSyndie id: UplinkSoapSyndie
category: Misc productEntity: SoapSyndie
itemId: SoapSyndie cost:
price: 1 Telecrystal: 1
categories:
- UplinkMisc
- type: uplinkListing - type: listing
id: UplinkUltrabrightLantern id: UplinkUltrabrightLantern
category: Misc productEntity: lanternextrabright #why is this item id not capitalized???
itemId: lanternextrabright cost:
price: 2 Telecrystal: 2
categories:
- UplinkMisc
#- type: uplinkListing #- type: listing
# id: UplinkCostumeCentcom # id: UplinkCostumeCentcom
# category: Misc # productEntity: ClothingBackpackDuffelSyndicateCostumeCentcom
# itemId: ClothingBackpackDuffelSyndicateCostumeCentcom # cost:
# price: 4 # Telecrystal: 4
# categories:
# - UplinkMisc
- type: uplinkListing - type: listing
id: UplinkGigacancerScanner id: UplinkGigacancerScanner
category: Misc name: Ultragigacancer Health Analyzer
itemId: HandheldHealthAnalyzerGigacancer
listingName: Ultragigacancer Health Analyzer
description: Works like a normal health analyzer, other than giving everyone it scans ultragigacancer. 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 id: UplinkNocturineChemistryBottle
category: Misc
itemId: NocturineChemistryBottle
description: A chemical that makes it very hard for your target to stand up. 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 id: UplinkSyndicateSegwayCrate
category: Misc name: syndicate segway
itemId: CrateFunSyndicateSegway
listingName: syndicate segway
description: Be an enemy of the corporation, in style! description: Be an enemy of the corporation, in style!
price: 5 productEntity: CrateFunSyndicateSegway
surplus: false cost:
Telecrystal: 5
categories:
- UplinkMisc
conditions:
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
# Pointless # Pointless
- type: uplinkListing - type: listing
id: UplinkSyndicateStamp id: UplinkSyndicateStamp
category: Pointless productEntity: RubberStampSyndicate
itemId: RubberStampSyndicate cost:
price: 2 Telecrystal: 2
categories:
- UplinkPointless
- type: uplinkListing - type: listing
id: UplinkCatEars id: UplinkCatEars
category: Pointless name: Cat Ears
itemId: ClothingHeadHatCatEars description: UwU
listingName: Cat Ears productEntity: ClothingHeadHatCatEars
description: UwU. cost:
price: 21 Telecrystal: 21
categories:
- UplinkPointless
- type: uplinkListing - type: listing
id: UplinkOutlawHat id: UplinkOutlawHat
category: Pointless productEntity: ClothingHeadHatOutlawHat
itemId: ClothingHeadHatOutlawHat cost:
price: 1 Telecrystal: 1
categories:
- UplinkPointless
- type: uplinkListing - type: listing
id: UplinkCostumePyjama id: UplinkCostumePyjama
category: Pointless productEntity: ClothingBackpackDuffelSyndicatePyjamaBundle
itemId: ClothingBackpackDuffelSyndicatePyjamaBundle cost:
price: 4 Telecrystal: 4
categories:
- UplinkPointless
- type: uplinkListing - type: listing
id: UplinkCostumeClown id: UplinkCostumeClown
category: Pointless productEntity: ClothingBackpackDuffelSyndicateCostumeClown
itemId: ClothingBackpackDuffelSyndicateCostumeClown cost:
price: 4 Telecrystal: 4
categories:
- UplinkPointless
- type: uplinkListing - type: listing
id: UplinkBalloon id: UplinkBalloon
category: Pointless productEntity: BalloonSyn
itemId: BalloonSyn cost:
price: 20 Telecrystal: 20
categories:
- UplinkPointless

View File

@@ -48,8 +48,8 @@
interfaces: interfaces:
- key: enum.PDAUiKey.Key - key: enum.PDAUiKey.Key
type: PDABoundUserInterface type: PDABoundUserInterface
- key: enum.UplinkUiKey.Key - key: enum.StoreUiKey.Key
type: UplinkBoundUserInterface type: StoreBoundUserInterface
- key: enum.RingerUiKey.Key - key: enum.RingerUiKey.Key
type: RingerBoundUserInterface type: RingerBoundUserInterface
- key: enum.InstrumentUiKey.Key - key: enum.InstrumentUiKey.Key

View File

@@ -15,9 +15,11 @@
count: 20 count: 20
max: 999999 # todo: add support for unlimited stacks max: 999999 # todo: add support for unlimited stacks
stackType: Telecrystal stackType: Telecrystal
- type: Telecrystal
- type: StackPrice - type: StackPrice
price: 200 price: 200
- type: Currency
price:
Telecrystal: 1
- type: entity - type: entity
parent: Telecrystal parent: Telecrystal
@@ -61,45 +63,55 @@
heldPrefix: old-radio heldPrefix: old-radio
- type: UserInterface - type: UserInterface
interfaces: interfaces:
- key: enum.UplinkUiKey.Key - key: enum.StoreUiKey.Key
type: UplinkBoundUserInterface type: StoreBoundUserInterface
- type: Uplink - type: ActivatableUI
activatesInHands: true key: enum.StoreUiKey.Key
- type: Store
preset: StorePresetUplink
balance:
Telecrystal: 0
- type: entity - type: entity
parent: BaseUplinkRadio parent: BaseUplinkRadio
id: BaseUplinkRadio20TC id: BaseUplinkRadio20TC
suffix: 20 TC suffix: 20 TC
components: components:
- type: Uplink - type: Store
presetInfo: preset: StorePresetUplink
balance: 20 balance:
Telecrystal: 20
#Default Nuclear Operative amount, not considering crew count
- type: entity - type: entity
parent: BaseUplinkRadio parent: BaseUplinkRadio
id: BaseUplinkRadio25TC id: BaseUplinkRadio25TC
suffix: 25 TC suffix: 25 TC
components: components:
- type: Uplink - type: Store
presetInfo: preset: StorePresetUplink
balance: 25 balance:
Telecrystal: 25
#this uplink MUST be used for nukeops, as it has the tag for filtering the listing.
- type: entity - type: entity
parent: BaseUplinkRadio parent: BaseUplinkRadio
id: BaseUplinkRadio40TC id: BaseUplinkRadio40TC
suffix: 40 TC suffix: 40 TC, NukeOps
components: components:
- type: Uplink - type: Store
presetInfo: preset: StorePresetUplink
balance: 40 balance:
Telecrystal: 40
- type: Tag
tags:
- NukeOpsUplink
- type: entity - type: entity
parent: BaseUplinkRadio parent: BaseUplinkRadio
id: BaseUplinkRadioDebug id: BaseUplinkRadioDebug
suffix: Debug suffix: Debug
components: components:
- type: Uplink - type: Store
presetInfo: preset: StorePresetUplink
balance: 9999 balance:
Telecrystal: 99999

View 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

View 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

View File

@@ -0,0 +1,16 @@
- type: storePreset
id: StorePresetUplink
storeName: Uplink
categories:
- UplinkWeapons
- UplinkAmmo
- UplinkExplosives
- UplinkMisc
- UplinkBundles
- UplinkTools
- UplinkUtility
- UplinkJob
- UplinkArmor
- UplinkPointless
currencyWhitelist:
- Telecrystal

View File

@@ -332,6 +332,9 @@
- type: Tag - type: Tag
id: NoSpinOnThrow id: NoSpinOnThrow
- type: Tag
id: NukeOpsUplink
- type: Tag - type: Tag
id: Ointment id: Ointment