diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml new file mode 100644 index 000000000000..21b30497d712 --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs new file mode 100644 index 000000000000..f8216c03ee35 --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs @@ -0,0 +1,75 @@ +using System.Linq; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._DV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class PriceHistoryTable : BoxContainer +{ + public PriceHistoryTable() + { + RobustXamlLoader.Load(this); + + // Create the stylebox here so we can use the colors from StockTradingUi + var styleBox = new StyleBoxFlat + { + BackgroundColor = StockTradingUiFragment.PriceBackgroundColor, + ContentMarginLeftOverride = 6, + ContentMarginRightOverride = 6, + ContentMarginTopOverride = 4, + ContentMarginBottomOverride = 4, + BorderColor = StockTradingUiFragment.BorderColor, + BorderThickness = new Thickness(1), + }; + + HistoryPanel.PanelOverride = styleBox; + } + + public void Update(List priceHistory) + { + PriceGrid.RemoveAllChildren(); + + // Take last 5 prices + var lastFivePrices = priceHistory.TakeLast(5).ToList(); + + for (var i = 0; i < lastFivePrices.Count; i++) + { + var price = lastFivePrices[i]; + var previousPrice = i > 0 ? lastFivePrices[i - 1] : price; + var priceChange = ((price - previousPrice) / previousPrice) * 100; + + var entryContainer = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + MinWidth = 80, + HorizontalAlignment = HAlignment.Center, + }; + + var priceLabel = new Label + { + Text = $"§{price:F2}", + HorizontalAlignment = HAlignment.Center, + }; + + var changeLabel = new Label + { + Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%", + HorizontalAlignment = HAlignment.Center, + StyleClasses = { "LabelSubText" }, + Modulate = priceChange switch + { + > 0 => StockTradingUiFragment.PositiveColor, + < 0 => StockTradingUiFragment.NegativeColor, + _ => StockTradingUiFragment.NeutralColor, + } + }; + + entryContainer.AddChild(priceLabel); + entryContainer.AddChild(changeLabel); + PriceGrid.AddChild(entryContainer); + } + } +} diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUi.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUi.cs new file mode 100644 index 000000000000..a182311a7032 --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUi.cs @@ -0,0 +1,45 @@ +using Robust.Client.UserInterface; +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; + +namespace Content.Client._DV.CartridgeLoader.Cartridges; + +public sealed partial class StockTradingUi : UIFragment +{ + private StockTradingUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new StockTradingUiFragment(); + + _fragment.OnBuyButtonPressed += (company, amount) => + { + SendStockTradingUiMessage(StockTradingUiAction.Buy, company, amount, userInterface); + }; + _fragment.OnSellButtonPressed += (company, amount) => + { + SendStockTradingUiMessage(StockTradingUiAction.Sell, company, amount, userInterface); + }; + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is StockTradingUiState cast) + { + _fragment?.UpdateState(cast); + } + } + + private static void SendStockTradingUiMessage(StockTradingUiAction action, int company, int amount, BoundUserInterface userInterface) + { + var newsMessage = new StockTradingUiMessageEvent(action, company, amount); + var message = new CartridgeUiMessage(newsMessage); + userInterface.SendMessage(message); + } +} diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml new file mode 100644 index 000000000000..66647897d527 --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs new file mode 100644 index 000000000000..2d88bc6b0ead --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs @@ -0,0 +1,270 @@ +using System.Linq; +using Content.Client.Administration.UI.CustomControls; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._DV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class StockTradingUiFragment : BoxContainer +{ + private readonly Dictionary _companyEntries = new(); + + // Event handlers for the parent UI + public event Action? OnBuyButtonPressed; + public event Action? OnSellButtonPressed; + + // Define colors + public static readonly Color PositiveColor = Color.FromHex("#00ff00"); // Green + public static readonly Color NegativeColor = Color.FromHex("#ff0000"); // Red + public static readonly Color NeutralColor = Color.FromHex("#ffffff"); // White + public static readonly Color BackgroundColor = Color.FromHex("#25252a"); // Dark grey + public static readonly Color PriceBackgroundColor = Color.FromHex("#1a1a1a"); // Darker grey + public static readonly Color BorderColor = Color.FromHex("#404040"); // Light grey + + public StockTradingUiFragment() + { + RobustXamlLoader.Load(this); + } + + public void UpdateState(StockTradingUiState state) + { + NoEntries.Visible = state.Entries.Count == 0; + Balance.Text = Loc.GetString("stock-trading-balance", ("balance", state.Balance)); + + // Clear all existing entries + foreach (var entry in _companyEntries.Values) + { + entry.Container.RemoveAllChildren(); + } + _companyEntries.Clear(); + Entries.RemoveAllChildren(); + + // Add new entries + for (var i = 0; i < state.Entries.Count; i++) + { + var company = state.Entries[i]; + var entry = new CompanyEntry(i, company.LocalizedDisplayName, OnBuyButtonPressed, OnSellButtonPressed); + _companyEntries[i] = entry; + Entries.AddChild(entry.Container); + + var ownedStocks = state.OwnedStocks.GetValueOrDefault(i, 0); + entry.Update(company, ownedStocks); + } + } + + private sealed class CompanyEntry + { + public readonly BoxContainer Container; + private readonly Label _nameLabel; + private readonly Label _priceLabel; + private readonly Label _changeLabel; + private readonly Button _sellButton; + private readonly Button _buyButton; + private readonly Label _sharesLabel; + private readonly LineEdit _amountEdit; + private readonly PriceHistoryTable _priceHistory; + + public CompanyEntry(int companyIndex, + string displayName, + Action? onBuyPressed, + Action? onSellPressed) + { + Container = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + Margin = new Thickness(0, 0, 0, 2), + }; + + // Company info panel + var companyPanel = new PanelContainer(); + + var mainContent = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + Margin = new Thickness(8), + }; + + // Top row with company name and price info + var topRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + _nameLabel = new Label + { + HorizontalExpand = true, + Text = displayName, + }; + + // Create a panel for price and change + var pricePanel = new PanelContainer + { + HorizontalAlignment = HAlignment.Right, + }; + + // Style the price panel + var priceStyleBox = new StyleBoxFlat + { + BackgroundColor = BackgroundColor, + ContentMarginLeftOverride = 8, + ContentMarginRightOverride = 8, + ContentMarginTopOverride = 4, + ContentMarginBottomOverride = 4, + BorderColor = BorderColor, + BorderThickness = new Thickness(1), + }; + + pricePanel.PanelOverride = priceStyleBox; + + // Container for price and change labels + var priceContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + }; + + _priceLabel = new Label(); + + _changeLabel = new Label + { + HorizontalAlignment = HAlignment.Right, + Modulate = NeutralColor, + Margin = new Thickness(15, 0, 0, 0), + }; + + priceContainer.AddChild(_priceLabel); + priceContainer.AddChild(_changeLabel); + pricePanel.AddChild(priceContainer); + + topRow.AddChild(_nameLabel); + topRow.AddChild(pricePanel); + + // Add the top row + mainContent.AddChild(topRow); + + // Add the price history table between top and bottom rows + _priceHistory = new PriceHistoryTable(); + mainContent.AddChild(_priceHistory); + + // Trading controls (bottom row) + var bottomRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + Margin = new Thickness(0, 5, 0, 0), + }; + + _sharesLabel = new Label + { + Text = Loc.GetString("stock-trading-owned-shares"), + MinWidth = 100, + }; + + _amountEdit = new LineEdit + { + Text = "1", + PlaceHolder = Loc.GetString("stock-trading-amount-placeholder"), + HorizontalExpand = true, + MinWidth = 80, + }; + + var buttonContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalAlignment = HAlignment.Right, + MinWidth = 140, + }; + + _buyButton = new Button + { + Text = Loc.GetString("stock-trading-buy-button"), + MinWidth = 65, + Margin = new Thickness(3, 0, 3, 0), + }; + + _sellButton = new Button + { + Text = Loc.GetString("stock-trading-sell-button"), + MinWidth = 65, + }; + + buttonContainer.AddChild(_buyButton); + buttonContainer.AddChild(_sellButton); + + bottomRow.AddChild(_sharesLabel); + bottomRow.AddChild(_amountEdit); + bottomRow.AddChild(buttonContainer); + + // Add the bottom row last + mainContent.AddChild(bottomRow); + + companyPanel.AddChild(mainContent); + Container.AddChild(companyPanel); + + // Add horizontal separator after the panel + var separator = new HSeparator + { + Margin = new Thickness(5, 3, 5, 5), + }; + Container.AddChild(separator); + + // Button click events + _buyButton.OnPressed += _ => + { + if (int.TryParse(_amountEdit.Text, out var amount) && amount > 0) + onBuyPressed?.Invoke(companyIndex, amount); + }; + + _sellButton.OnPressed += _ => + { + if (int.TryParse(_amountEdit.Text, out var amount) && amount > 0) + onSellPressed?.Invoke(companyIndex, amount); + }; + + // There has to be a better way of doing this + _amountEdit.OnTextChanged += args => + { + var newText = string.Concat(args.Text.Where(char.IsDigit)); + if (newText != args.Text) + _amountEdit.Text = newText; + }; + } + + public void Update(StockCompany company, int ownedStocks) + { + _nameLabel.Text = company.LocalizedDisplayName; + _priceLabel.Text = $"§{company.CurrentPrice:F2}"; + _sharesLabel.Text = Loc.GetString("stock-trading-owned-shares", ("shares", ownedStocks)); + + var priceChange = 0f; + if (company.PriceHistory is { Count: > 0 }) + { + var previousPrice = company.PriceHistory[^1]; + priceChange = (company.CurrentPrice - previousPrice) / previousPrice * 100; + } + + _changeLabel.Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%"; + + // Update color based on price change + _changeLabel.Modulate = priceChange switch + { + > 0 => PositiveColor, + < 0 => NegativeColor, + _ => NeutralColor, + }; + + // Update the price history table if not null + if (company.PriceHistory != null) + _priceHistory.Update(company.PriceHistory); + + // Disable sell button if no shares owned + _sellButton.Disabled = ownedStocks <= 0; + } + } +} diff --git a/Content.Server/_DV/Cargo/Components/StationStockMarketComponent.cs b/Content.Server/_DV/Cargo/Components/StationStockMarketComponent.cs new file mode 100644 index 000000000000..9eb34bb88e94 --- /dev/null +++ b/Content.Server/_DV/Cargo/Components/StationStockMarketComponent.cs @@ -0,0 +1,64 @@ +using System.Numerics; +using Content.Server._DV.Cargo.Systems; +using Content.Server._DV.CartridgeLoader.Cartridges; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Timing; + +namespace Content.Server._DV.Cargo.Components; + +[RegisterComponent, AutoGenerateComponentPause] +[Access(typeof(StockMarketSystem), typeof(StockTradingCartridgeSystem))] +public sealed partial class StationStockMarketComponent : Component +{ + /// + /// The list of companies you can invest in + /// + [DataField] + public List Companies = []; + + /// + /// The list of shares owned by the station + /// + [DataField] + public Dictionary StockOwnership = new(); + + /// + /// The interval at which the stock market updates + /// + [DataField] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(300); // 5 minutes + + /// + /// The timespan of next update. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextUpdate = TimeSpan.Zero; + + /// + /// The sound to play after selling or buying stocks + /// + [DataField] + public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg"); + + /// + /// The sound to play if the don't have access to buy or sell stocks + /// + [DataField] + public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg"); + + // These work well as presets but can be changed in the yaml + [DataField] + public List MarketChanges = + [ + new(0.86f, new Vector2(-0.05f, 0.05f)), // Minor + new(0.10f, new Vector2(-0.3f, 0.2f)), // Moderate + new(0.03f, new Vector2(-0.5f, 1.5f)), // Major + new(0.01f, new Vector2(-0.9f, 4.0f)), // Catastrophic + ]; +} + +[DataRecord] +public record struct MarketChange(float Chance, Vector2 Range); diff --git a/Content.Server/_DV/Cargo/StocksCommands.cs b/Content.Server/_DV/Cargo/StocksCommands.cs new file mode 100644 index 000000000000..59693dd03161 --- /dev/null +++ b/Content.Server/_DV/Cargo/StocksCommands.cs @@ -0,0 +1,135 @@ +using Content.Server.Administration; +using Content.Server._DV.Cargo.Components; +using Content.Server._DV.Cargo.Systems; +using Content.Shared.Administration; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Shared.Console; + +namespace Content.Server._DV.Cargo; + +[AdminCommand(AdminFlags.Fun)] +public sealed class ChangeStocksPriceCommand : IConsoleCommand +{ + public string Command => "changestocksprice"; + public string Description => Loc.GetString("cmd-changestocksprice-desc"); + public string Help => Loc.GetString("cmd-changestocksprice-help", ("command", Command)); + + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!int.TryParse(args[0], out var companyIndex)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + if (!float.TryParse(args[1], out var newPrice)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + EntityUid? targetStation = null; + if (args.Length > 2) + { + if (!EntityUid.TryParse(args[2], out var station)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + targetStation = station; + } + + var stockMarket = _entitySystemManager.GetEntitySystem(); + var query = _entityManager.EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + // Skip if we're looking for a specific station and this isn't it + if (targetStation != null && uid != targetStation) + continue; + + if (stockMarket.TryChangeStocksPrice(uid, comp, newPrice, companyIndex)) + { + shell.WriteLine(Loc.GetString("shell-command-success")); + return; + } + + shell.WriteLine(Loc.GetString("cmd-changestocksprice-invalid-company")); + return; + } + + shell.WriteLine(targetStation != null + ? Loc.GetString("cmd-changestocksprice-invalid-station") + : Loc.GetString("cmd-changestocksprice-no-stations")); + } +} + +[AdminCommand(AdminFlags.Fun)] +public sealed class AddStocksCompanyCommand : IConsoleCommand +{ + public string Command => "addstockscompany"; + public string Description => Loc.GetString("cmd-addstockscompany-desc"); + public string Help => Loc.GetString("cmd-addstockscompany-help", ("command", Command)); + + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!float.TryParse(args[1], out var basePrice)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + EntityUid? targetStation = null; + if (args.Length > 2) + { + if (!EntityUid.TryParse(args[2], out var station)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + targetStation = station; + } + + var displayName = args[0]; + var stockMarket = _entitySystemManager.GetEntitySystem(); + var query = _entityManager.EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + // Skip if we're looking for a specific station and this isn't it + if (targetStation != null && uid != targetStation) + continue; + + if (stockMarket.TryAddCompany(uid, comp, basePrice, displayName)) + { + shell.WriteLine(Loc.GetString("shell-command-success")); + return; + } + + shell.WriteLine(Loc.GetString("cmd-addstockscompany-failure")); + return; + } + + shell.WriteLine(targetStation != null + ? Loc.GetString("cmd-addstockscompany-invalid-station") + : Loc.GetString("cmd-addstockscompany-no-stations")); + } +} diff --git a/Content.Server/_DV/Cargo/Systems/StockMarketSystem.cs b/Content.Server/_DV/Cargo/Systems/StockMarketSystem.cs new file mode 100644 index 000000000000..ccd539e38802 --- /dev/null +++ b/Content.Server/_DV/Cargo/Systems/StockMarketSystem.cs @@ -0,0 +1,351 @@ +using Content.Server.Access.Systems; +using Content.Server.Administration.Logs; +using Content.Server.Cargo.Components; +using Content.Server.Cargo.Systems; +using Content.Server._DV.Cargo.Components; +using Content.Server._DV.CartridgeLoader.Cartridges; +using Content.Shared.Access.Components; +using Content.Shared.Access.Systems; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.Database; +using Content.Shared.Popups; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server._DV.Cargo.Systems; + +/// +/// This handles the stock market updates +/// +public sealed class StockMarketSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _access = default!; + [Dependency] private readonly CargoSystem _cargo = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ILogManager _log = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IdCardSystem _idCard = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + private ISawmill _sawmill = default!; + private const float MaxPrice = 262144; // 1/64 of max safe integer + + public override void Initialize() + { + base.Initialize(); + + _sawmill = _log.GetSawmill("admin.stock_market"); + + SubscribeLocalEvent(OnStockTradingMessage); + } + + public override void Update(float frameTime) + { + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var component)) + { + if (curTime < component.NextUpdate) + continue; + + component.NextUpdate = curTime + component.UpdateInterval; + UpdateStockPrices(uid, component); + } + } + + private void OnStockTradingMessage(Entity ent, ref CartridgeMessageEvent args) + { + if (args is not StockTradingUiMessageEvent message) + return; + + var user = args.Actor; + var companyIndex = message.CompanyIndex; + var amount = message.Amount; + var loader = GetEntity(args.LoaderUid); + + // Ensure station and stock market components are valid + if (ent.Comp.Station is not {} station || !TryComp(station, out var stockMarket)) + return; + + // Validate company index + if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return; + + if (!TryComp(ent, out var access)) + return; + + // Attempt to retrieve ID card from loader, + // play deny sound and exit if access is not allowed + if (!_idCard.TryGetIdCard(loader, out var idCard) || !_access.IsAllowed(idCard, ent.Owner, access)) + { + _audio.PlayEntity(stockMarket.DenySound, user, loader); + _popup.PopupEntity(Loc.GetString("stock-trading-access-denied"), user, user); + return; + } + + try + { + var company = stockMarket.Companies[companyIndex]; + + // Attempt to buy or sell stocks based on the action + bool success; + switch (message.Action) + { + case StockTradingUiAction.Buy: + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(user):user} attempting to buy {amount} stocks of {company.LocalizedDisplayName}"); + success = TryChangeStocks(station, stockMarket, companyIndex, amount, user); + break; + + case StockTradingUiAction.Sell: + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(user):user} attempting to sell {amount} stocks of {company.LocalizedDisplayName}"); + success = TryChangeStocks(station, stockMarket, companyIndex, -amount, user); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + // Play confirmation sound if the transaction was successful + _audio.PlayEntity(success ? stockMarket.ConfirmSound : stockMarket.DenySound, user, loader); + if (!success) + { + _popup.PopupEntity(Loc.GetString("stock-trading-transaction-failed"), user, user); + } + } + finally + { + // Raise the event to update the UI regardless of outcome + UpdateStockMarket(station); + } + } + + private void UpdateStockMarket(EntityUid station) + { + var ev = new StockMarketUpdatedEvent(station); + RaiseLocalEvent(ref ev); + } + + private bool TryChangeStocks( + EntityUid station, + StationStockMarketComponent stockMarket, + int companyIndex, + int amount, + EntityUid user) + { + if (amount == 0 || companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return false; + + // Check if the station has a bank account + if (!TryComp(station, out var bank)) + return false; + + var company = stockMarket.Companies[companyIndex]; + var totalValue = (int)Math.Round(company.CurrentPrice * amount); + + if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned)) + currentOwned = 0; + + if (amount > 0) + { + // Buying: see if we can afford it + if (bank.Balance < totalValue) + return false; + } + else + { + // Selling: see if we have enough stocks to sell + var selling = -amount; + if (currentOwned < selling) + return false; + } + + var newAmount = currentOwned + amount; + if (newAmount > 0) + stockMarket.StockOwnership[companyIndex] = newAmount; + else + stockMarket.StockOwnership.Remove(companyIndex); + + // Update the bank account (take away for buying and give for selling) + _cargo.UpdateBankAccount(station, bank, -totalValue); + + // Log the transaction + var verb = amount > 0 ? "bought" : "sold"; + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"[StockMarket] {ToPrettyString(user):user} {verb} {Math.Abs(amount)} stocks of {company.LocalizedDisplayName} at {company.CurrentPrice:F2} credits each (Total: {totalValue})"); + + return true; + } + + private void UpdateStockPrices(EntityUid station, StationStockMarketComponent stockMarket) + { + for (var i = 0; i < stockMarket.Companies.Count; i++) + { + var company = stockMarket.Companies[i]; + var changeType = DetermineMarketChange(stockMarket.MarketChanges); + var multiplier = CalculatePriceMultiplier(changeType); + + UpdatePriceHistory(ref company); + + // Update price with multiplier + var oldPrice = company.CurrentPrice; + company.CurrentPrice *= (1 + multiplier); + + // Ensure price doesn't go below minimum threshold + company.CurrentPrice = MathF.Max(company.CurrentPrice, company.BasePrice * 0.1f); + + // Ensure price doesn't go above maximum threshold + company.CurrentPrice = MathF.Min(company.CurrentPrice, MaxPrice); + + stockMarket.Companies[i] = company; + + // Calculate the percentage change + var percentChange = (company.CurrentPrice - oldPrice) / oldPrice * 100; + + // Raise the event + UpdateStockMarket(station); + + // Log it + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"[StockMarket] Company '{company.LocalizedDisplayName}' price updated by {percentChange:+0.00;-0.00}% from {oldPrice:0.00} to {company.CurrentPrice:0.00}"); + } + } + + /// + /// Attempts to change the price for a specific company + /// + /// True if the operation was successful, false otherwise + public bool TryChangeStocksPrice(EntityUid station, + StationStockMarketComponent stockMarket, + float newPrice, + int companyIndex) + { + // Check if it exceeds the max price + if (newPrice > MaxPrice) + { + _sawmill.Error($"New price cannot be greater than {MaxPrice}."); + return false; + } + + if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return false; + + var company = stockMarket.Companies[companyIndex]; + UpdatePriceHistory(ref company); + + company.CurrentPrice = MathF.Max(newPrice, company.BasePrice * 0.1f); + stockMarket.Companies[companyIndex] = company; + + UpdateStockMarket(station); + return true; + } + + /// + /// Attempts to add a new company to the station + /// + /// False if the company already exists, true otherwise + public bool TryAddCompany(EntityUid station, + StationStockMarketComponent stockMarket, + float basePrice, + string displayName) + { + // Create a new company struct with the specified parameters + var company = new StockCompany + { + LocalizedDisplayName = displayName, // Assume there's no Loc for it + BasePrice = basePrice, + CurrentPrice = basePrice, + PriceHistory = [], + }; + + UpdatePriceHistory(ref company); + stockMarket.Companies.Add(company); + + UpdateStockMarket(station); + + return true; + } + + /// + /// Attempts to add a new company to the station using the StockCompany + /// + /// False if the company already exists, true otherwise + public bool TryAddCompany(Entity station, + StockCompany company) + { + // Make sure it has a price history + UpdatePriceHistory(ref company); + + // Add the new company to the dictionary + station.Comp.Companies.Add(company); + + UpdateStockMarket(station); + + return true; + } + + private static void UpdatePriceHistory(ref StockCompany company) + { + // Create if null + company.PriceHistory ??= []; + + // Make sure it has at least 5 entries + while (company.PriceHistory.Count < 5) + { + company.PriceHistory.Add(company.BasePrice); + } + + // Store previous price in history + company.PriceHistory.Add(company.CurrentPrice); + + if (company.PriceHistory.Count > 5) // Keep last 5 prices + company.PriceHistory.RemoveAt(1); // Always keep the base price + } + + private MarketChange DetermineMarketChange(List marketChanges) + { + var roll = _random.NextFloat(); + var cumulative = 0f; + + foreach (var change in marketChanges) + { + cumulative += change.Chance; + if (roll <= cumulative) + return change; + } + + return marketChanges[0]; // Default to first (usually minor) change if we somehow exceed 100% + } + + private float CalculatePriceMultiplier(MarketChange change) + { + // Using Box-Muller transform for normal distribution + var u1 = _random.NextFloat(); + var u2 = _random.NextFloat(); + var randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); + + // Scale and shift the result to our desired range + var range = change.Range.Y - change.Range.X; + var mean = (change.Range.Y + change.Range.X) / 2; + var stdDev = range / 6.0f; // 99.7% of values within range + + var result = (float)(mean + (stdDev * randStdNormal)); + return Math.Clamp(result, change.Range.X, change.Range.Y); + } +} + +/// +/// Broadcast whenever a stock market is updated. +/// +[ByRefEvent] +public record struct StockMarketUpdatedEvent(EntityUid Station); diff --git a/Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs b/Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs new file mode 100644 index 000000000000..d9b84aeee197 --- /dev/null +++ b/Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server._DV.CartridgeLoader.Cartridges; + +[RegisterComponent, Access(typeof(StockTradingCartridgeSystem))] +public sealed partial class StockTradingCartridgeComponent : Component +{ + /// + /// Station entity to keep track of + /// + [DataField] + public EntityUid? Station; +} diff --git a/Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs b/Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs new file mode 100644 index 000000000000..e8677ea01b78 --- /dev/null +++ b/Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs @@ -0,0 +1,93 @@ +using System.Linq; +using Content.Server.Cargo.Components; +using Content.Server._DV.Cargo.Components; +using Content.Server._DV.Cargo.Systems; +using Content.Server.Station.Systems; +using Content.Server.CartridgeLoader; +using Content.Shared.Cargo.Components; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; + +namespace Content.Server._DV.CartridgeLoader.Cartridges; + +public sealed class StockTradingCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!; + [Dependency] private readonly StationSystem _station = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUiReady); + SubscribeLocalEvent(OnStockMarketUpdated); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnBalanceUpdated); + } + + private void OnBalanceUpdated(Entity ent, ref BankBalanceUpdatedEvent args) + { + UpdateAllCartridges(args.Station); + } + + private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args) + { + UpdateUI(ent, args.Loader); + } + + private void OnStockMarketUpdated(ref StockMarketUpdatedEvent args) + { + UpdateAllCartridges(args.Station); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + // Initialize price history for each company + for (var i = 0; i < ent.Comp.Companies.Count; i++) + { + var company = ent.Comp.Companies[i]; + + // Create initial price history using base price + company.PriceHistory = new List(); + for (var j = 0; j < 5; j++) + { + company.PriceHistory.Add(company.BasePrice); + } + + ent.Comp.Companies[i] = company; + } + + if (_station.GetOwningStation(ent.Owner) is { } station) + UpdateAllCartridges(station); + } + + private void UpdateAllCartridges(EntityUid station) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var cartridge)) + { + if (cartridge.LoaderUid is not { } loader || comp.Station != station) + continue; + UpdateUI((uid, comp), loader); + } + } + + private void UpdateUI(Entity ent, EntityUid loader) + { + if (_station.GetOwningStation(loader) is { } station) + ent.Comp.Station = station; + + if (!TryComp(ent.Comp.Station, out var stockMarket) || + !TryComp(ent.Comp.Station, out var bankAccount)) + return; + + // Send the UI state with balance and owned stocks + var state = new StockTradingUiState( + entries: stockMarket.Companies, + ownedStocks: stockMarket.StockOwnership, + balance: bankAccount.Balance + ); + + _cartridgeLoader.UpdateCartridgeUiState(loader, state); + } +} diff --git a/Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs b/Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs new file mode 100644 index 000000000000..5981f03b28ea --- /dev/null +++ b/Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class StockTradingUiMessageEvent(StockTradingUiAction action, int companyIndex, int amount) + : CartridgeMessageEvent +{ + public readonly StockTradingUiAction Action = action; + public readonly int CompanyIndex = companyIndex; + public readonly int Amount = amount; +} + +[Serializable, NetSerializable] +public enum StockTradingUiAction +{ + Buy, + Sell, +} diff --git a/Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiState.cs b/Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiState.cs new file mode 100644 index 000000000000..42adb6feaa8d --- /dev/null +++ b/Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiState.cs @@ -0,0 +1,66 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class StockTradingUiState( + List entries, + Dictionary ownedStocks, + float balance) + : BoundUserInterfaceState +{ + public readonly List Entries = entries; + public readonly Dictionary OwnedStocks = ownedStocks; + public readonly float Balance = balance; +} + +// No structure, zero fucks given +[DataDefinition, Serializable] +public partial struct StockCompany +{ + /// + /// The displayed name of the company shown in the UI. + /// + [DataField(required: true)] + public LocId? DisplayName; + + // Used for runtime-added companies that don't have a localization entry + private string? _displayName; + + /// + /// Gets or sets the display name, using either the localized or direct string value + /// + [Access(Other = AccessPermissions.ReadWriteExecute)] + public string LocalizedDisplayName + { + get => _displayName ?? Loc.GetString(DisplayName ?? string.Empty); + set => _displayName = value; + } + + /// + /// The current price of the company's stock + /// + [DataField(required: true)] + public float CurrentPrice; + + /// + /// The base price of the company's stock + /// + [DataField(required: true)] + public float BasePrice; + + /// + /// The price history of the company's stock + /// + [DataField] + public List? PriceHistory; + + public StockCompany(string displayName, float currentPrice, float basePrice, List? priceHistory) + { + DisplayName = displayName; + _displayName = null; + CurrentPrice = currentPrice; + BasePrice = basePrice; + PriceHistory = priceHistory ?? []; + } +} diff --git a/Resources/Locale/en-US/_DV/cargo/stocks-comapnies.ftl b/Resources/Locale/en-US/_DV/cargo/stocks-comapnies.ftl new file mode 100644 index 000000000000..8afe9120ffd3 --- /dev/null +++ b/Resources/Locale/en-US/_DV/cargo/stocks-comapnies.ftl @@ -0,0 +1,10 @@ +# Company names used for stocks trading +stock-trading-company-nanotrasen = Nanotrasen [NT] +stock-trading-company-gorlex = Gorlex Marauders [GRX] +stock-trading-company-interdyne = Interdyne Pharmaceutics [INTP] +stock-trading-company-donk = Donk Co. [DONK] +# Imp specials +stock-trading-company-cybersun = CyberSun Industries [CSI] +stock-trading-company-rtvs = Radio TV Solutions [RTVS] +stock-trading-company-scargo = S-Cargo [SCG] +stock-trading-company-dahir = Dahir Insaat [DIS] \ No newline at end of file diff --git a/Resources/Locale/en-US/_DV/cargo/stocks-commands.ftl b/Resources/Locale/en-US/_DV/cargo/stocks-commands.ftl new file mode 100644 index 000000000000..8e0fe014999e --- /dev/null +++ b/Resources/Locale/en-US/_DV/cargo/stocks-commands.ftl @@ -0,0 +1,13 @@ +# changestockprice command +cmd-changestocksprice-desc = Changes a company's stock price to the specified number. +cmd-changestocksprice-help = changestockprice [Station UID] +cmd-changestocksprice-invalid-company = Failed to execute command! Invalid company index or the new price exceeds the allowed limit. +cmd-changestocksprice-invalid-station = No stock market found for specified station +cmd-changestocksprice-no-stations = No stations with stock markets found + +# addstockscompany command +cmd-addstockscompany-desc = Adds a new company to the stocks market. +cmd-addstockscompany-help = addstockscompany [Station UID] +cmd-addstockscompany-failure = Failed to add company to the stock market. +cmd-addstockscompany-invalid-station = No stock market found for specified station +cmd-addstockscompany-no-stations = No stations with stock markets found diff --git a/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl index 9db691d763b8..7095aecb12be 100644 --- a/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl @@ -36,3 +36,18 @@ log-probe-card-number = Card: {$number} log-probe-recipients = {$count} Recipients log-probe-recipient-list = Known Recipients: log-probe-message-format = {$sender} → {$recipient}: {$content} + +## StockTrading + +# General +stock-trading-program-name = StockTrading +stock-trading-title = Intergalactic Stock Market +stock-trading-balance = Balance: {$balance} credits +stock-trading-no-entries = No entries +stock-trading-owned-shares = Owned: {$shares} +stock-trading-buy-button = Buy +stock-trading-sell-button = Sell +stock-trading-amount-placeholder = Amount +stock-trading-price-history = Price History +stock-trading-access-denied = Access denied +stock-trading-transaction-failed = Transaction failed \ No newline at end of file diff --git a/Resources/Locale/en-US/_Impstation/objectives/conditions/steal-target-groups.ftl b/Resources/Locale/en-US/_Impstation/objectives/conditions/steal-target-groups.ftl index d2a7bc985479..8a90d092a182 100644 --- a/Resources/Locale/en-US/_Impstation/objectives/conditions/steal-target-groups.ftl +++ b/Resources/Locale/en-US/_Impstation/objectives/conditions/steal-target-groups.ftl @@ -9,6 +9,7 @@ steal-target-groups-seedextractor = seed extractor steal-target-groups-medtekcartridge = MedTek cartridge steal-target-groups-logprobecartridge = LogProbe cartridge steal-target-groups-astronavcartridge = AstroNav cartridge +steal-target-groups-stocktradingcartridge = StockTrading cartridge steal-target-groups-servicetechfab = service techfab machine board steal-target-groups-shipyardcomputercircuitboard = shipyard computer board diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index f5076ba5f3ab..8ea1d8e73a8e 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -20,6 +20,7 @@ - id: RubberStampDenied - id: RubberStampQm - id: AstroNavCartridge + - id: StockTradingCartridge # Delta-V - type: entity id: LockerQuarterMasterFilled diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index b75c84b47bf6..1f15f89ace8a 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -440,6 +440,13 @@ accentVColor: "#a23e3e" - type: Icon state: pda-qm + - type: CartridgeLoader # DeltaV + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - StockTradingCartridge # DeltaV + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -458,6 +465,13 @@ borderColor: "#e39751" - type: Icon state: pda-cargo + - type: CartridgeLoader # DeltaV + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - StockTradingCartridge # DeltaV + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -906,6 +920,7 @@ - LogProbeCartridge - AstroNavCartridge - NanoChatCartridge # DV + - StockTradingCartridge # DV - type: entity parent: CentcomPDA @@ -931,6 +946,7 @@ - MedTekCartridge - AstroNavCartridge - NanoChatCartridge # DV + - StockTradingCartridge # DV - type: Tag # Ignore Chameleon tags tags: - DoorBumpOpener diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml index 613bb1b1f4f1..7f64715f3ce3 100644 --- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml +++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml @@ -25,6 +25,7 @@ - BaseStationSiliconLawCrewsimov - BaseStationAllEventsEligible - BaseStationNanotrasen + - BaseStationStockMarket # DeltaV categories: [ HideSpawnMenu ] components: - type: Transform diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 478858a1989d..8de7d5b777b3 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -99,6 +99,7 @@ MedTekCartridgeStealObjective: 1 LogProbeCartridgeStealObjective: 1 AstroNavCartridgeStealObjective: 1 + StockTradingCartridgeStealObjective: 1 ServiceTechFabStealObjective: 1 ShiningSpringStealObjective: 1 ShipyardComputerCircuitboardStealObjective: 1 diff --git a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml index eb9dc667db3a..62d31c8d862c 100644 --- a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml @@ -18,3 +18,26 @@ - type: ActiveRadio channels: - Common + +- type: entity + parent: [BaseItem, BaseCargoContraband] + id: StockTradingCartridge + name: StockTrading cartridge + description: A cartridge that tracks the intergalactic stock market. + components: + - type: Sprite + sprite: _DV/Objects/Devices/cartridge.rsi + state: cart-stonk + - type: UIFragment + ui: !type:StockTradingUi + - type: StockTradingCartridge + - type: Cartridge + programName: stock-trading-program-name + icon: + sprite: _DV/Misc/program_icons.rsi + state: stock_trading + - type: BankClient + - type: AccessReader # This is so that we can restrict who can buy stocks + access: [["Cargo"]] + - type: StealTarget + stealGroup: StockTradingCartridge \ No newline at end of file diff --git a/Resources/Prototypes/_DV/Entities/Stations/base.yml b/Resources/Prototypes/_DV/Entities/Stations/base.yml new file mode 100644 index 000000000000..74d29deb6845 --- /dev/null +++ b/Resources/Prototypes/_DV/Entities/Stations/base.yml @@ -0,0 +1,30 @@ +- type: entity + id: BaseStationStockMarket + abstract: true + components: + - type: StationStockMarket + companies: + - displayName: stock-trading-company-nanotrasen + basePrice: 100 + currentPrice: 100 + - displayName: stock-trading-company-gorlex + basePrice: 75 + currentPrice: 75 + - displayName: stock-trading-company-interdyne + basePrice: 300 + currentPrice: 300 + - displayName: stock-trading-company-donk + basePrice: 90 + currentPrice: 90 + - displayName: stock-trading-company-cybersun + basePrice: 175 + currentPrice: 175 + - displayName: stock-trading-company-rtvs + basePrice: 200 + currentPrice: 200 + - displayName: stock-trading-company-scargo + basePrice: 150 + currentPrice: 150 + - displayName: stock-trading-company-dahir + basePrice: 50 + currentPrice: 50 \ No newline at end of file diff --git a/Resources/Prototypes/_Impstation/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/_Impstation/Entities/Objects/Devices/pda.yml index 0790436f201f..62a77083f5ae 100644 --- a/Resources/Prototypes/_Impstation/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/_Impstation/Entities/Objects/Devices/pda.yml @@ -55,6 +55,14 @@ accentVColor: "#FF9500" - type: Icon state: pda-seniorcargo + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - StockTradingCartridge # DeltaV + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA diff --git a/Resources/Prototypes/_Impstation/Objectives/stealTargetGroups.yml b/Resources/Prototypes/_Impstation/Objectives/stealTargetGroups.yml index 593cfc8d6308..15417bdd4679 100644 --- a/Resources/Prototypes/_Impstation/Objectives/stealTargetGroups.yml +++ b/Resources/Prototypes/_Impstation/Objectives/stealTargetGroups.yml @@ -277,3 +277,10 @@ sprite: sprite: Mobs/Species/Skeleton/parts.rsi state: skull_icon + +- type: stealTargetGroup + id: StockTradingCartridge + name: steal-target-groups-stocktradingcartridge + sprite: + sprite: _DV/Objects/Devices/cartridge.rsi + state: cart-stonk \ No newline at end of file diff --git a/Resources/Prototypes/_Impstation/Objectives/thief.yml b/Resources/Prototypes/_Impstation/Objectives/thief.yml index d11216de15ae..57e396a6e01f 100644 --- a/Resources/Prototypes/_Impstation/Objectives/thief.yml +++ b/Resources/Prototypes/_Impstation/Objectives/thief.yml @@ -463,3 +463,12 @@ descriptionText: objective-condition-thief-evil-skull-description - type: Objective difficulty: 0.6 #a little harder than the bible, it glows red and there's only on, and it's an artifact + +- type: entity + parent: BaseThiefStealObjective + id: StockTradingCartridgeStealObjective + components: + - type: StealCondition + stealGroup: StockTradingCartridge + - type: Objective + difficulty: 1 \ No newline at end of file diff --git a/Resources/Textures/_DV/Misc/program_icons.rsi/meta.json b/Resources/Textures/_DV/Misc/program_icons.rsi/meta.json index 935cb557bb13..1f5f472528d2 100644 --- a/Resources/Textures/_DV/Misc/program_icons.rsi/meta.json +++ b/Resources/Textures/_DV/Misc/program_icons.rsi/meta.json @@ -1,14 +1,17 @@ { "version": 1, "license": "CC0-1.0", - "copyright": "nanochat made by kushbreth (discord)", + "copyright": "stock_trading made by Malice, nanochat made by kushbreth (discord)", "size": { "x": 32, "y": 32 }, "states": [ + { + "name": "stock_trading" + }, { "name": "nanochat" } ] -} +} \ No newline at end of file diff --git a/Resources/Textures/_DV/Misc/program_icons.rsi/stock_trading.png b/Resources/Textures/_DV/Misc/program_icons.rsi/stock_trading.png new file mode 100644 index 000000000000..251b46a3f83c Binary files /dev/null and b/Resources/Textures/_DV/Misc/program_icons.rsi/stock_trading.png differ diff --git a/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/cart-stonk.png b/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/cart-stonk.png new file mode 100644 index 000000000000..ddfed6e915ca Binary files /dev/null and b/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/cart-stonk.png differ diff --git a/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/meta.json index d43baa385185..c892736d9553 100644 --- a/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/meta.json +++ b/Resources/Textures/_DV/Objects/Devices/cartridge.rsi/meta.json @@ -8,6 +8,9 @@ }, "states": [ { + "name": "cart-stonk" + }, + { "name": "cart-chat" } ]