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

View File

@@ -8,7 +8,6 @@ using Content.Server.Station.Components;
using Content.Server.Suspicion;
using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar;
using Content.Shared.Doors.Systems;
using Content.Shared.EntityList;
@@ -17,7 +16,6 @@ using Content.Shared.Maps;
using Content.Shared.MobState.Components;
using Content.Shared.Roles;
using Content.Shared.Suspicion;
using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
@@ -48,6 +46,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "Suspicion";
@@ -173,16 +172,8 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
mind!.AddRole(traitorRole);
traitors.Add(traitorRole);
// creadth: we need to create uplink for the antag.
// PDA should be in place already, so we just need to
// initiate uplink account.
var uplinkAccount = new UplinkAccount(traitorStartingBalance, mind.OwnedEntity!);
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
accounts.AddNewAccount(uplinkAccount);
// try to place uplink
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
if (!_uplink.AddUplink(mind.OwnedEntity!.Value, traitorStartingBalance))
continue;
}

View File

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

View File

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

View File

@@ -2,29 +2,28 @@ using Content.Server.Instruments;
using Content.Server.Light.Components;
using Content.Server.Light.EntitySystems;
using Content.Server.Light.Events;
using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account;
using Content.Server.Traitor.Uplink.Components;
using Content.Server.PDA.Ringer;
using Content.Server.Station.Components;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Server.Station.Systems;
using Content.Server.UserInterface;
using Content.Shared.PDA;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Content.Server.Mind.Components;
using Content.Server.Traitor;
namespace Content.Server.PDA
{
public sealed class PDASystem : SharedPDASystem
{
[Dependency] private readonly UplinkSystem _uplinkSystem = default!;
[Dependency] private readonly UplinkAccountsSystem _uplinkAccounts = default!;
[Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
[Dependency] private readonly RingerSystem _ringerSystem = default!;
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StoreSystem _storeSystem = default!;
public override void Initialize()
{
@@ -32,8 +31,8 @@ namespace Content.Server.PDA
SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle);
SubscribeLocalEvent<PDAComponent, AfterActivatableUIOpenEvent>(AfterUIOpen);
SubscribeLocalEvent<PDAComponent, UplinkInitEvent>(OnUplinkInit);
SubscribeLocalEvent<PDAComponent, UplinkRemovedEvent>(OnUplinkRemoved);
SubscribeLocalEvent<PDAComponent, StoreAddedEvent>(OnUplinkInit);
SubscribeLocalEvent<PDAComponent, StoreRemovedEvent>(OnUplinkRemoved);
SubscribeLocalEvent<PDAComponent, GridModifiedEvent>(OnGridChanged);
}
@@ -74,12 +73,12 @@ namespace Content.Server.PDA
UpdatePDAUserInterface(pda);
}
private void OnUplinkInit(EntityUid uid, PDAComponent pda, UplinkInitEvent args)
private void OnUplinkInit(EntityUid uid, PDAComponent pda, StoreAddedEvent args)
{
UpdatePDAUserInterface(pda);
}
private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, UplinkRemovedEvent args)
private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, StoreRemovedEvent args)
{
UpdatePDAUserInterface(pda);
}
@@ -111,7 +110,7 @@ namespace Content.Server.PDA
// players. This should really use a sort of key-code entry system that selects an account which is not directly tied to
// a player entity.
if (!HasComp<UplinkComponent>(pda.Owner))
if (!TryComp<StoreComponent>(pda.Owner, out var storeComponent))
return;
var uplinkState = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, hasInstrument);
@@ -121,7 +120,8 @@ namespace Content.Server.PDA
if (session.AttachedEntity is not EntityUid { Valid: true } user)
continue;
if (_uplinkAccounts.HasAccount(user))
if (storeComponent.AccountOwner == user || (TryComp<MindComponent>(session.AttachedEntity, out var mindcomp) && mindcomp.Mind != null &&
mindcomp.Mind.HasRole<TraitorRole>()))
ui.SetState(uplinkState, session);
}
}
@@ -143,8 +143,9 @@ namespace Content.Server.PDA
case PDAShowUplinkMessage _:
{
if (EntityManager.TryGetComponent(pda.Owner, out UplinkComponent? uplink))
_uplinkSystem.ToggleUplinkUI(uplink, msg.Session);
if (msg.Session.AttachedEntity != null &&
TryComp<StoreComponent>(pda.Owner, out var store))
_storeSystem.ToggleUi(msg.Session.AttachedEntity.Value, store);
break;
}
case PDAShowRingtoneMessage _:
@@ -170,8 +171,13 @@ namespace Content.Server.PDA
private void AfterUIOpen(EntityUid uid, PDAComponent pda, AfterActivatableUIOpenEvent args)
{
//TODO: this is awful
// A new user opened the UI --> Check if they are a traitor and should get a user specific UI state override.
if (!HasComp<UplinkComponent>(pda.Owner) || !_uplinkAccounts.HasAccount(args.User))
if (!TryComp<StoreComponent>(pda.Owner, out var storeComp))
return;
if (storeComp.AccountOwner != args.User &&
!(TryComp<MindComponent>(args.User, out var mindcomp) && mindcomp.Mind != null && mindcomp.Mind.HasRole<TraitorRole>()))
return;
if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui))

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

View File

@@ -1,3 +1,6 @@
using Content.Shared.Store;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Traitor.Uplink.SurplusBundle;
/// <summary>
@@ -12,4 +15,11 @@ public sealed class SurplusBundleComponent : Component
[ViewVariables(VVAccess.ReadOnly)]
[DataField("totalPrice")]
public int TotalPrice = 20;
/// <summary>
/// The preset that will be used to get all the listings.
/// Currently just defaults to the basic uplink.
/// </summary>
[DataField("storePreset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))]
public string StorePreset = "StorePresetUplink";
}

View File

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

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.Mind.Components;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink.Account;
using Content.Server.Traitor.Uplink.Components;
using Content.Server.UserInterface;
using Content.Server.Store.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Content.Server.Store.Components;
using Content.Shared.FixedPoint;
namespace Content.Server.Traitor.Uplink
{
public sealed class UplinkSystem : EntitySystem
{
[Dependency]
private readonly UplinkAccountsSystem _accounts = default!;
[Dependency]
private readonly UplinkListingSytem _listing = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly StoreSystem _store = default!;
public override void Initialize()
public const string TelecrystalCurrencyPrototype = "Telecrystal";
/// <summary>
/// Gets the amount of TC on an "uplink"
/// Mostly just here for legacy systems based on uplink.
/// </summary>
/// <param name="component"></param>
/// <returns>the amount of TC</returns>
public int GetTCBalance(StoreComponent component)
{
base.Initialize();
SubscribeLocalEvent<UplinkComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<UplinkComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<UplinkComponent, ActivateInWorldEvent>(OnActivate);
// UI events
SubscribeLocalEvent<UplinkComponent, UplinkBuyListingMessage>(OnBuy);
SubscribeLocalEvent<UplinkComponent, UplinkRequestUpdateInterfaceMessage>(OnRequestUpdateUI);
SubscribeLocalEvent<UplinkComponent, UplinkTryWithdrawTC>(OnWithdrawTC);
SubscribeLocalEvent<UplinkAccountBalanceChanged>(OnBalanceChangedBroadcast);
FixedPoint2? tcBalance = component.Balance.GetValueOrDefault(TelecrystalCurrencyPrototype);
return tcBalance != null ? tcBalance.Value.Int() : 0;
}
public void SetAccount(UplinkComponent component, UplinkAccount account)
{
if (component.UplinkAccount != null)
{
Logger.Error("Can't init one uplink with different account!");
return;
}
component.UplinkAccount = account;
}
private void OnInit(EntityUid uid, UplinkComponent component, ComponentInit args)
{
RaiseLocalEvent(uid, new UplinkInitEvent(component), true);
// if component has a preset info (probably spawn by admin)
// create a new account and register it for this uplink
if (component.PresetInfo != null)
{
var account = new UplinkAccount(component.PresetInfo.StartingBalance);
_accounts.AddNewAccount(account);
SetAccount(component, account);
}
}
private void OnRemove(EntityUid uid, UplinkComponent component, ComponentRemove args)
{
RaiseLocalEvent(uid, new UplinkRemovedEvent(), true);
}
private void OnActivate(EntityUid uid, UplinkComponent component, ActivateInWorldEvent args)
{
if (args.Handled)
return;
// check if uplinks activates directly or use some proxy, like a PDA
if (!component.ActivatesInHands)
return;
if (component.UplinkAccount == null)
return;
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return;
ToggleUplinkUI(component, actor.PlayerSession);
args.Handled = true;
}
private void OnBalanceChangedBroadcast(UplinkAccountBalanceChanged ev)
{
foreach (var uplink in EntityManager.EntityQuery<UplinkComponent>())
{
if (uplink.UplinkAccount == ev.Account)
{
UpdateUserInterface(uplink);
}
}
}
private void OnRequestUpdateUI(EntityUid uid, UplinkComponent uplink, UplinkRequestUpdateInterfaceMessage args)
{
UpdateUserInterface(uplink);
}
private void OnBuy(EntityUid uid, UplinkComponent uplink, UplinkBuyListingMessage message)
{
if (message.Session.AttachedEntity is not { Valid: true } player) return;
if (uplink.UplinkAccount == null) return;
if (!_accounts.TryPurchaseItem(uplink.UplinkAccount, message.ItemId,
EntityManager.GetComponent<TransformComponent>(player).Coordinates, out var entity))
{
SoundSystem.Play(uplink.InsufficientFundsSound.GetSound(),
Filter.SinglePlayer(message.Session), uplink.Owner, AudioParams.Default);
RaiseNetworkEvent(new UplinkInsufficientFundsMessage(), message.Session.ConnectedClient);
return;
}
_handsSystem.PickupOrDrop(player, entity.Value);
SoundSystem.Play(uplink.BuySuccessSound.GetSound(),
Filter.SinglePlayer(message.Session), uplink.Owner, AudioParams.Default.WithVolume(-8f));
RaiseNetworkEvent(new UplinkBuySuccessMessage(), message.Session.ConnectedClient);
}
private void OnWithdrawTC(EntityUid uid, UplinkComponent uplink, UplinkTryWithdrawTC args)
{
var acc = uplink.UplinkAccount;
if (acc == null)
return;
if (args.Session.AttachedEntity is not { Valid: true } player) return;
var cords = EntityManager.GetComponent<TransformComponent>(player).Coordinates;
// try to withdraw TCs from account
if (!_accounts.TryWithdrawTC(acc, args.TC, cords, out var tcUid))
return;
// try to put it into players hands
_handsSystem.PickupOrDrop(player, tcUid.Value);
// play buying sound
SoundSystem.Play(uplink.BuySuccessSound.GetSound(),
Filter.SinglePlayer(args.Session), uplink.Owner, AudioParams.Default.WithVolume(-8f));
UpdateUserInterface(uplink);
}
public void ToggleUplinkUI(UplinkComponent component, IPlayerSession session)
{
var ui = component.Owner.GetUIOrNull(UplinkUiKey.Key);
ui?.Toggle(session);
UpdateUserInterface(component);
}
private void UpdateUserInterface(UplinkComponent component)
{
var ui = component.Owner.GetUIOrNull(UplinkUiKey.Key);
if (ui == null)
return;
var listings = _listing.GetListings().Values.ToList();
var acc = component.UplinkAccount;
UplinkAccountData accData;
if (acc != null)
{
// if we don't have a jobwhitelist stored, get a new one
if (component.JobWhitelist == null &&
acc.AccountHolder != null &&
TryComp<MindComponent>(acc.AccountHolder, out var mind) &&
mind.Mind != null)
{
HashSet<string>? jobList = new();
foreach (var role in mind.Mind.AllRoles.ToList())
{
if (role.GetType() == typeof(Job))
{
var job = (Job) role;
jobList.Add(job.Prototype.ID);
}
}
component.JobWhitelist = jobList;
}
// filter out items not on the whitelist
for (var i = 0; i < listings.Count; i++)
{
var entry = listings[i];
if (entry.JobWhitelist != null)
{
var found = false;
if (component.JobWhitelist != null)
{
foreach (var job in component.JobWhitelist)
{
if (entry.JobWhitelist.Contains(job))
{
found = true;
break;
}
}
}
if (!found)
{
listings.Remove(entry);
i--;
}
}
}
accData = new UplinkAccountData(acc.AccountHolder, acc.Balance);
}
else
{
accData = new UplinkAccountData(null, 0);
}
ui.SetState(new UplinkUpdateState(accData, listings.ToArray()));
}
public bool AddUplink(EntityUid user, UplinkAccount account, EntityUid? uplinkEntity = null)
/// <summary>
/// Adds an uplink to the target
/// </summary>
/// <param name="user">The person who is getting the uplink</param>
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
/// <param name="uplinkPresetId">The id of the storepreset</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
/// <returns>Whether or not the uplink was added successfully</returns>
public bool AddUplink(EntityUid user, FixedPoint2? balance, string uplinkPresetId = "StorePresetUplink", EntityUid? uplinkEntity = null)
{
// Try to find target item
if (uplinkEntity == null)
@@ -234,11 +45,17 @@ namespace Content.Server.Traitor.Uplink
return false;
}
var uplink = uplinkEntity.Value.EnsureComponent<UplinkComponent>();
SetAccount(uplink, account);
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
_store.InitializeFromPreset(uplinkPresetId, store);
store.AccountOwner = user;
store.Balance.Clear();
if (!HasComp<PDAComponent>(uplinkEntity.Value))
uplink.ActivatesInHands = true;
if (balance != null)
{
store.Balance.Clear();
_store.TryAddCurrency(
new Dictionary<string, FixedPoint2>() { { TelecrystalCurrencyPrototype, balance.Value } }, store);
}
// TODO add BUI. Currently can't be done outside of yaml -_-
@@ -248,14 +65,13 @@ namespace Content.Server.Traitor.Uplink
private EntityUid? FindUplinkTarget(EntityUid user)
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
if (!pdaUid.ContainedEntity.HasValue) continue;
if (HasComp<PDAComponent>(pdaUid.ContainedEntity.Value))
if (HasComp<PDAComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
}
@@ -263,7 +79,7 @@ namespace Content.Server.Traitor.Uplink
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
if (HasComp<PDAComponent>(item))
if (HasComp<PDAComponent>(item) || HasComp<StoreComponent>(item))
return item;
}

View File

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

View File

@@ -75,4 +75,5 @@ public enum LogType
Gib = 70,
Identity = 71,
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
- type: uplinkListing
- type: listing
id: UplinkPistolViper
category: Weapons
itemId: WeaponPistolViper
listingName: Viper
name: Viper
description: A small, easily concealable, but somewhat underpowered gun. Use pistol magazines (.35 auto).
price: 6
productEntity: WeaponPistolViper
cost:
Telecrystal: 6
categories:
- UplinkWeapons
- type: uplinkListing
- type: listing
id: UplinkRevolverPython
category: Weapons
itemId: WeaponRevolverPython
listingName: Python
name: Python
description: A loud and deadly revolver. Uses .40 Magnum.
price: 8
productEntity: WeaponRevolverPython
cost:
Telecrystal: 8
categories:
- UplinkWeapons
# Inbuilt suppressor so it's sneaky + more expensive.
- type: uplinkListing
- type: listing
id: UplinkPistolCobra
category: Weapons
itemId: WeaponPistolCobra
listingName: Cobra
name: Cobra
description: A rugged, robust operator handgun with inbuilt silencer. Use pistol magazines (.25 caseless).
price: 8
productEntity: WeaponPistolCobra
cost:
Telecrystal: 8
categories:
- UplinkWeapons
# Poor accuracy, slow to fire, cheap option
- type: uplinkListing
- type: listing
id: UplinkRifleMosin
category: Weapons
itemId: WeaponSniperMosin
listingName: Surplus Rifle
name: Surplus Rifle
description: A bolt action service rifle that has seen many wars. Not modern by any standard, hand loaded, and terrible recoil, but it is cheap.
price: 4
productEntity: WeaponSniperMosin
cost:
Telecrystal: 4
categories:
- UplinkWeapons
#- type: uplinkListing
# id: UplinkCrossbowEnergyMini
# category: Weapons
# itemId: CrossbowEnergyMini
# price: 8
- type: uplinkListing
- type: listing
id: UplinkEsword
category: Weapons
itemId: EnergySword
listingName: Energy Sword
name: Energy Sword
description: A very dangerous energy sword. Can be stored in pockets when turned off. Makes a lot of noise when used or turned on.
price: 8
productEntity: EnergySword
cost:
Telecrystal: 8
categories:
- UplinkWeapons
# bug swept to make
#- type: uplinkListing
# id: UplinkDoubleBladedESword
# category: Weapons
# itemId: DoubleBladedESword
# price: 16
- type: uplinkListing
- type: listing
id: UplinkEnergyDagger
category: Weapons
itemId: EnergyDagger
listingName: Energy Dagger
name: Energy Dagger
description: A small energy blade conveniently disguised in the form of a pen.
price: 2
productEntity: EnergyDagger
cost:
Telecrystal: 2
categories:
- UplinkWeapons
- type: uplinkListing
- type: listing
id: UplinkFireAxeFlaming
category: Weapons
itemId: FireAxeFlaming
listingName: Fire Axe
name: Fire Axe
description: A classic-style weapon infused with advanced atmos technology to allow it to set targets on fire.
price: 10
productEntity: FireAxeFlaming
cost:
Telecrystal: 10
categories:
- UplinkWeapons
# Explosives
- type: uplinkListing
- type: listing
id: UplinkExplosiveGrenade
category: Explosives
itemId: ExGrenade
price: 4
productEntity: ExGrenade
cost:
Telecrystal: 4
categories:
- UplinkExplosives
- type: uplinkListing
- type: listing
id: UplinkExplosiveGrenadeFlash
category: Explosives
itemId: GrenadeFlashBang
price: 2
productEntity: GrenadeFlashBang
cost:
Telecrystal: 2
categories:
- UplinkExplosives
- type: uplinkListing
- type: listing
id: UplinkSyndieMiniBomb
category: Explosives
itemId: SyndieMiniBomb
price: 7
productEntity: SyndieMiniBomb
cost:
Telecrystal: 7
categories:
- UplinkExplosives
- type: uplinkListing
- type: listing
id: UplinkGrenadePenguin
category: Explosives
itemId: MobGrenadePenguin
price: 6
surplus: false # got wrecked by penguins from surplus crate
productEntity: MobGrenadePenguin
cost:
Telecrystal: 6
categories:
- UplinkExplosives
conditions:
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
- type: uplinkListing
- type: listing
id: UplinkC4
category: Explosives
itemId: C4
price: 2
description: >
C-4 is plastic explosive of the common variety Composition C. You can use it to breach walls, airlocks or sabotage equipment.
It can be attached to almost all objects and has a modifiable timer with a minimum setting of 10 seconds.
productEntity: C4
cost:
Telecrystal: 2
categories:
- UplinkExplosives
- type: uplinkListing
- type: listing
id: UplinkC4Bundle
category: Explosives
itemId: ClothingBackpackDuffelSyndicateC4tBundle
price: 12 # 25% off
description: Because sometimes quantity is quality. Contains 8 C-4 plastic explosives.
productEntity: ClothingBackpackDuffelSyndicateC4tBundle
cost:
Telecrystal: 12
categories:
- UplinkExplosives
# Ammo
- type: uplinkListing
- type: listing
id: UplinkPistol9mmMagazine
category: Ammo
itemId: MagazinePistol
price: 2
productEntity: MagazinePistol
cost:
Telecrystal: 2
categories:
- UplinkAmmo
# For the Mandella
- type: uplinkListing
- type: listing
id: UplinkMagazinePistolCaselessRifle
category: Ammo
itemId: MagazinePistolCaselessRifle
price: 2
productEntity: MagazinePistolCaselessRifle
cost:
Telecrystal: 2
categories:
- UplinkAmmo
# For the Inspector
- type: uplinkListing
- type: listing
id: UplinkSpeedLoaderMagnum
category: Ammo
itemId: SpeedLoaderMagnum
price: 2
icon: /Textures/Objects/Weapons/Guns/Ammunition/SpeedLoaders/Magnum/magnum_speed_loader.rsi/base.png
productEntity: SpeedLoaderMagnum
cost:
Telecrystal: 2
categories:
- UplinkAmmo
# For the mosin
- type: uplinkListing
- type: listing
id: UplinkMosinAmmo
category: Ammo
itemId: BoxMagazineLightRifle
description: A box of cartridges for the surplus rifle.
price: 2
productEntity: BoxMagazineLightRifle
cost:
Telecrystal: 2
categories:
- UplinkAmmo
#Utility
- type: uplinkListing
- type: listing
id: UplinkHoloparaKit
category: Utility
itemId: BoxHoloparasite
listingName: Holoparasite Kit
name: Holoparasite Kit
description: The pride and joy of Cybersun. Contains an injector that hosts a sentient metaphysical guardian made of hard light which resides in the user's body when not active. The guardian can punch rapidly and is immune to hazardous environments and bullets, but shares any damage it takes with the user.
icon: /Textures/Objects/Misc/guardian_info.rsi/icon.png
price: 14
productEntity: BoxHoloparasite
cost:
Telecrystal: 14
categories:
- UplinkUtility
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- NukeOpsUplink
- type: uplinkListing
- type: listing
id: UplinkHolster
category: Utility
itemId: ClothingBeltSyndieHolster
listingName: Syndicate Shoulder Holster
name: Syndicate Shoulder Holster
description: A deep shoulder holster capable of holding many types of ballistics.
icon: /Textures/Clothing/Belt/syndieholster.rsi/icon.png
price: 2
productEntity: ClothingBeltSyndieHolster
cost:
Telecrystal: 2
categories:
- UplinkUtility
- type: uplinkListing
- type: listing
id: UplinkEmag
category: Utility
itemId: Emag
name: Emag
description: The business card of the syndicate, this sequencer is able to break open airlocks and tamper with a variety of station devices. Recharges automatically.
icon: /Textures/Objects/Tools/emag.rsi/icon.png
price: 8
productEntity: Emag
cost:
Telecrystal: 8
categories:
- UplinkUtility
- type: uplinkListing
- type: listing
id: UplinkAgentIDCard
category: Utility
itemId: AgentIDCard
listingName: Agent ID Card
name: Agent ID Card
description: A modified ID card that can copy accesses from other cards and change its name and job title at-will.
icon: Objects/Misc/id_cards.rsi/default.png
price: 3
productEntity: AgentIDCard
cost:
Telecrystal: 3
categories:
- UplinkUtility
- type: uplinkListing
- type: listing
id: UplinkJetpack
category: Utility
itemId: JetpackBlack
listingName: Black Jetpack
name: Black Jetpack
description: A black jetpack. It allows you to fly around in space. Additional fuel not included.
icon: Objects/Tanks/Jetpacks/black.rsi/icon.png
price: 5
productEntity: JetpackBlack
cost:
Telecrystal: 5
categories:
- UplinkUtility
- type: uplinkListing
id: ReinforcementTeleporterSyndicate
category: Utility
itemId: ReinforcementTeleporterSyndicate
listingName: Reinforcement Teleporter
- type: listing
id: UplinkReinforcementTeleporterSyndicate
name: Reinforcement Teleporter
description: Teleport in an agent of extremely questionable quality. No off button, buy this if you're ready to party. They have a pistol with no reserve ammo, and a knife. That's it.
productEntity: ReinforcementTeleporterSyndicate
icon: Objects/Devices/communication.rsi/old-radio.png
price: 25
cost:
Telecrystal: 25
categories:
- UplinkUtility
#TODO: Increase the price of this to 4-5/remove it when we get encrpytion keys
- type: uplinkListing
- type: listing
id: UplinkHeadset
category: Utility
itemId: ClothingHeadsetAltSyndicate
listingName: Syndicate Overear-Headset
name: Syndicate Overear-Headset
description: A headset that allows you to listen in on departmental channels, or contact other traitors.
icon: Clothing/Ears/Headsets/syndicate.rsi/icon_alt.png
price: 2
productEntity: ClothingHeadsetAltSyndicate
cost:
Telecrystal: 2
categories:
- UplinkUtility
- type: uplinkListing
- type: listing
id: UplinkHypopen
category: Utility
itemId: Hypopen
listingName: Hypopen
name: Hypopen
description: A chemical hypospray disguised as a pen, capable of instantly injecting up to 15u of reagents. Starts empty.
price: 4
productEntity: Hypopen
cost:
Telecrystal: 4
categories:
- UplinkUtility
# Bundles
- type: uplinkListing
- type: listing
id: UplinkC20RBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledSMG
price: 25
icon: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledSMG
cost:
Telecrystal: 25
categories:
- UplinkBundles
- type: uplinkListing
- type: listing
id: UplinkBulldogBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledShotgun
price: 25
icon: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledShotgun
cost:
Telecrystal: 25
categories:
- UplinkBundles
- type: uplinkListing
- type: listing
id: UplinkGrenadeLauncherBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
price: 30
icon: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
cost:
Telecrystal: 30
categories:
- UplinkBundles
- type: uplinkListing
- type: listing
id: UplinkL6SawBundle
category: Bundles
itemId: ClothingBackpackDuffelSyndicateFilledLMG
price: 40
icon: /Textures/Objects/Weapons/Guns/LMGs/l6.rsi/icon.png
productEntity: ClothingBackpackDuffelSyndicateFilledLMG
cost:
Telecrystal: 40
categories:
- UplinkBundles
# Add this back in once war ops/separate nukie inventories are added.
#- type: uplinkListing
# id: UplinkZombieBundle
# category: Bundles
# itemId: ClothingBackpackDuffelZombieBundle
# price: 50
# icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
- type: listing
id: UplinkZombieBundle
icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
productEntity: ClothingBackpackDuffelZombieBundle
cost:
Telecrystal: 40
categories:
- UplinkBundles
conditions:
- !type:StoreWhitelistCondition
whitelist:
tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
- type: uplinkListing
- type: listing
id: UplinkSurplusBundle
category: Bundles
itemId: CrateSyndicateSurplusBundle
description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good.
price: 20
surplus: false
productEntity: CrateSyndicateSurplusBundle
cost:
Telecrystal: 20
categories:
- UplinkBundles
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
- type: uplinkListing
- type: listing
id: UplinkSuperSurplusBundle
category: Bundles
itemId: CrateSyndicateSuperSurplusBundle
description: Contains 125 telecrystals worth of completely random Syndicate items.
price: 40
surplus: false
#- type: uplinkListing
# id: UplinkCarbineBundle
# category: Bundles
# itemId: ClothingBackpackDuffelSyndicateFilledCarbine
# price: 35
# icon: /Textures/Objects/Weapons/Guns/Rifles/carbine.rsi/icon.png
productEntity: CrateSyndicateSuperSurplusBundle
cost:
Telecrystal: 40
categories:
- UplinkBundles
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
# Tools
- type: uplinkListing
- type: listing
id: UplinkToolbox
category: Tools
itemId: ToolboxSyndicateFilled
price: 2
productEntity: ToolboxSyndicateFilled
cost:
Telecrystal: 2
categories:
- UplinkTools
- type: uplinkListing
- type: listing
id: UplinkSyndicateJawsOfLife
category: Tools
itemId: SyndicateJawsOfLife
price: 2
productEntity: SyndicateJawsOfLife
cost:
Telecrystal: 2
categories:
- UplinkTools
- type: uplinkListing
- type: listing
id: UplinkDuffelSurgery
category: Tools
itemId: ClothingBackpackDuffelSyndicateFilledMedical
price: 5
productEntity: ClothingBackpackDuffelSyndicateFilledMedical
cost:
Telecrystal: 5
categories:
- UplinkTools
- type : uplinkListing
- type: listing
id: UplinkPowerSink
category: Tools
itemId: PowerSink
price: 5
productEntity: PowerSink
cost:
Telecrystal: 5
categories:
- UplinkTools
- type: uplinkListing
- type: listing
id: UplinkCarpDehydrated
category: Tools
itemId: DehydratedSpaceCarp
price: 3
productEntity: DehydratedSpaceCarp
cost:
Telecrystal: 3
categories:
- UplinkTools
# Job Specific
- type: uplinkListing
- type: listing
id: uplinkGatfruitSeeds
category: Job
itemId: GatfruitSeeds
description: And who says guns don't grow on trees?
price: 4
jobWhitelist:
productEntity: GatfruitSeeds
cost:
Telecrystal: 4
categories:
- UplinkJob
conditions:
- !type:BuyerJobCondition
whitelist:
- Botanist
- type: uplinkListing
- type: listing
id: uplinkNecronomicon
category: Job
itemId: BibleNecronomicon
description: An unholy book capable of summoning a demonic familiar.
price: 6
surplus: false
jobWhitelist:
productEntity: BibleNecronomicon
cost:
Telecrystal: 6
categories:
- UplinkJob
conditions:
- !type:BuyerJobCondition
whitelist:
- Chaplain
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
# Armor
# Should be cameleon shoes, change when implemented.
- type: uplinkListing
- type: listing
id: UplinkClothingNoSlipsShoes
category: Armor
itemId: ClothingShoesChameleonNoSlips
listingName: no-slip shoes
name: no-slip shoes
description: These protect you from slips while looking like normal sneakers.
price: 2
productEntity: ClothingShoesChameleonNoSlips
cost:
Telecrystal: 2
categories:
- UplinkArmor
- type: uplinkListing
- type: listing
id: UplinkgClothingThievingGloves
category: Armor
itemId: ThievingGloves
listingName: Thieving Gloves
name: Thieving Gloves
description: Discretely steal from pockets and increase your thieving technique with these fancy new gloves, all while looking like normal gloves!
price: 4
productEntity: ThievingGloves
cost:
Telecrystal: 4
categories:
- UplinkArmor
- type: uplinkListing
- type: listing
id: UplinkClothingOuterVestWeb
category: Armor
itemId: ClothingOuterVestWeb
price: 5
productEntity: ClothingOuterVestWeb
cost:
Telecrystal: 5
categories:
- UplinkArmor
- type: uplinkListing
- type: listing
id: UplinkHardsuitSyndie
category: Armor
itemId: ClothingBackpackDuffelSyndicateHardsuitBundle
description: The Syndicate's well known armored blood red hardsuit, capable of space walks and bullet resistant.
price: 8
productEntity: ClothingBackpackDuffelSyndicateHardsuitBundle
cost:
Telecrystal: 8
categories:
- UplinkArmor
- type: uplinkListing
- type: listing
id: UplinkClothingShoesBootsMagSyndie
category: Armor
itemId: ClothingShoesBootsMagSyndie
description: A pair of magnetic boots that will keep you on the ground if the gravity fails or is sabotaged, giving you a mobility advantage. If activated with gravity they will protect from slips, but they will slow you down.
price: 2
productEntity: ClothingShoesBootsMagSyndie
cost:
Telecrystal: 2
categories:
- UplinkArmor
- type: uplinkListing
- type: listing
id: UplinkEVASyndie
category: Armor
itemId: ClothingBackpackDuffelSyndicateEVABundle
description: A simple EVA suit that offers no protection other than what's needed to survive in space.
price: 4
productEntity: ClothingBackpackDuffelSyndicateEVABundle
cost:
Telecrystal: 4
categories:
- UplinkArmor
- type: uplinkListing
- type: listing
id: UplinkClothingOuterHardsuitJuggernaut
category: Armor
itemId: ClothingOuterHardsuitJuggernaut
description: Hyper resilient armor made of materials tested in the Tau chromosphere facility. The only thing that's going to be slowing you down is this suit... and tasers.
price: 12
productEntity: ClothingOuterHardsuitJuggernaut
cost:
Telecrystal: 12
categories:
- UplinkArmor
# Misc
- type: uplinkListing
- type: listing
id: UplinkCyberpen
category: Misc
itemId: CyberPen
description: Cybersun's legal department pen. Smells vaguely of hard-light and war profiteering.
price: 4
productEntity: CyberPen
cost:
Telecrystal: 4
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkDecoyDisk
category: Misc
itemId: NukeDiskFake
listingName: Decoy nuclear disk
name: decoy nuclear disk
description: A piece of plastic with a lenticular printing, made to look like a nuclear auth disk.
price: 1
productEntity: NukeDiskFake
cost:
Telecrystal: 1
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkRevolverCapGun
category: Misc
itemId: RevolverCapGun
price: 4
productEntity: RevolverCapGun
cost:
Telecrystal: 4
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkCigarettes
category: Misc
itemId: CigPackSyndicate
price: 2
productEntity: CigPackSyndicate
cost:
Telecrystal: 2
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkSoapSyndie
category: Misc
itemId: SoapSyndie
price: 1
productEntity: SoapSyndie
cost:
Telecrystal: 1
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkUltrabrightLantern
category: Misc
itemId: lanternextrabright
price: 2
productEntity: lanternextrabright #why is this item id not capitalized???
cost:
Telecrystal: 2
categories:
- UplinkMisc
#- type: uplinkListing
#- type: listing
# id: UplinkCostumeCentcom
# category: Misc
# itemId: ClothingBackpackDuffelSyndicateCostumeCentcom
# price: 4
# productEntity: ClothingBackpackDuffelSyndicateCostumeCentcom
# cost:
# Telecrystal: 4
# categories:
# - UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkGigacancerScanner
category: Misc
itemId: HandheldHealthAnalyzerGigacancer
listingName: Ultragigacancer Health Analyzer
name: Ultragigacancer Health Analyzer
description: Works like a normal health analyzer, other than giving everyone it scans ultragigacancer.
price: 5
productEntity: HandheldHealthAnalyzerGigacancer
cost:
Telecrystal: 5
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkNocturineChemistryBottle
category: Misc
itemId: NocturineChemistryBottle
description: A chemical that makes it very hard for your target to stand up.
price: 5
productEntity: NocturineChemistryBottle
cost:
Telecrystal: 5
categories:
- UplinkMisc
- type: uplinkListing
- type: listing
id: UplinkSyndicateSegwayCrate
category: Misc
itemId: CrateFunSyndicateSegway
listingName: syndicate segway
name: syndicate segway
description: Be an enemy of the corporation, in style!
price: 5
surplus: false
productEntity: CrateFunSyndicateSegway
cost:
Telecrystal: 5
categories:
- UplinkMisc
conditions:
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
# Pointless
- type: uplinkListing
- type: listing
id: UplinkSyndicateStamp
category: Pointless
itemId: RubberStampSyndicate
price: 2
productEntity: RubberStampSyndicate
cost:
Telecrystal: 2
categories:
- UplinkPointless
- type: uplinkListing
- type: listing
id: UplinkCatEars
category: Pointless
itemId: ClothingHeadHatCatEars
listingName: Cat Ears
description: UwU.
price: 21
name: Cat Ears
description: UwU
productEntity: ClothingHeadHatCatEars
cost:
Telecrystal: 21
categories:
- UplinkPointless
- type: uplinkListing
- type: listing
id: UplinkOutlawHat
category: Pointless
itemId: ClothingHeadHatOutlawHat
price: 1
productEntity: ClothingHeadHatOutlawHat
cost:
Telecrystal: 1
categories:
- UplinkPointless
- type: uplinkListing
- type: listing
id: UplinkCostumePyjama
category: Pointless
itemId: ClothingBackpackDuffelSyndicatePyjamaBundle
price: 4
productEntity: ClothingBackpackDuffelSyndicatePyjamaBundle
cost:
Telecrystal: 4
categories:
- UplinkPointless
- type: uplinkListing
- type: listing
id: UplinkCostumeClown
category: Pointless
itemId: ClothingBackpackDuffelSyndicateCostumeClown
price: 4
productEntity: ClothingBackpackDuffelSyndicateCostumeClown
cost:
Telecrystal: 4
categories:
- UplinkPointless
- type: uplinkListing
- type: listing
id: UplinkBalloon
category: Pointless
itemId: BalloonSyn
price: 20
productEntity: BalloonSyn
cost:
Telecrystal: 20
categories:
- UplinkPointless

View File

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

View File

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

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
id: NoSpinOnThrow
- type: Tag
id: NukeOpsUplink
- type: Tag
id: Ointment