From 79ad7e18a2b859269dc9d9ad1dfecd3a14689368 Mon Sep 17 00:00:00 2001 From: Epic Toast <44736041+EpicToastTM@users.noreply.github.com> Date: Sat, 11 Jan 2025 14:52:24 -0800 Subject: [PATCH 1/7] Add the stock market cartridge --- .../Cartridges/PriceHistoryTable.xaml | 25 ++ .../Cartridges/PriceHistoryTable.xaml.cs | 75 ++++ .../Cartridges/StockTradingUi.cs | 45 +++ .../Cartridges/StockTradingUiFragment.xaml | 44 +++ .../Cartridges/StockTradingUiFragment.xaml.cs | 269 ++++++++++++++ .../Components/StationStockMarketComponent.cs | 64 ++++ Content.Server/_DV/Cargo/StocksCommands.cs | 135 +++++++ .../_DV/Cargo/Systems/StockMarketSystem.cs | 351 ++++++++++++++++++ .../StockTradingCartridgeComponent.cs | 11 + .../Cartridges/StockTradingCartridgeSystem.cs | 93 +++++ .../Cartridges/StockTradingUiMessageEvent.cs | 19 + .../Cartridges/StockTradingUiState.cs | 66 ++++ .../en-US/_DV/cargo/stocks-comapnies.ftl | 10 + .../en-US/_DV/cargo/stocks-commands.ftl | 13 + .../en-US/_DV/cartridge-loader/cartridges.ftl | 15 + .../Catalog/Fills/Lockers/heads.yml | 1 + .../Entities/Objects/Devices/pda.yml | 23 ++ .../Entities/Stations/nanotrasen.yml | 1 + .../Entities/Objects/Devices/cartridges.yml | 24 ++ .../Prototypes/_DV/Entities/Stations/base.yml | 30 ++ .../Entities/Objects/Devices/pda.yml | 8 + .../_DV/Misc/program_icons.rsi/meta.json | 7 +- .../Misc/program_icons.rsi/stock_trading.png | Bin 0 -> 1012 bytes .../Devices/cartridge.rsi/cart-stonk.png | Bin 0 -> 367 bytes .../Objects/Devices/cartridge.rsi/meta.json | 3 + 25 files changed, 1330 insertions(+), 2 deletions(-) create mode 100644 Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml create mode 100644 Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs create mode 100644 Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUi.cs create mode 100644 Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml create mode 100644 Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs create mode 100644 Content.Server/_DV/Cargo/Components/StationStockMarketComponent.cs create mode 100644 Content.Server/_DV/Cargo/StocksCommands.cs create mode 100644 Content.Server/_DV/Cargo/Systems/StockMarketSystem.cs create mode 100644 Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs create mode 100644 Content.Server/_DV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs create mode 100644 Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs create mode 100644 Content.Shared/_DV/CartridgeLoader/Cartridges/StockTradingUiState.cs create mode 100644 Resources/Locale/en-US/_DV/cargo/stocks-comapnies.ftl create mode 100644 Resources/Locale/en-US/_DV/cargo/stocks-commands.ftl create mode 100644 Resources/Prototypes/_DV/Entities/Stations/base.yml create mode 100644 Resources/Textures/_DV/Misc/program_icons.rsi/stock_trading.png create mode 100644 Resources/Textures/_DV/Objects/Devices/cartridge.rsi/cart-stonk.png diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml new file mode 100644 index 00000000000000..21b30497d71294 --- /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 00000000000000..f04d01c297bdd1 --- /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 00000000000000..a182311a70329f --- /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 00000000000000..66647897d52728 --- /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 00000000000000..34f71058c2bdcd --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs @@ -0,0 +1,269 @@ +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 + { + 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 00000000000000..9eb34bb88e949f --- /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 00000000000000..59693dd0316166 --- /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 00000000000000..ccd539e3880222 --- /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 00000000000000..d9b84aeee197ab --- /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 00000000000000..e8677ea01b788c --- /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 00000000000000..5981f03b28ea59 --- /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 00000000000000..42adb6feaa8d0f --- /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 00000000000000..8afe9120ffd3be --- /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 00000000000000..8e0fe014999e5f --- /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 9db691d763b805..7095aecb12be81 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/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index f5076ba5f3abeb..8ea1d8e73a8e24 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 b75c84b47bf612..bc92cf71743c5f 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 - type: entity parent: CentcomPDA @@ -931,6 +946,7 @@ - MedTekCartridge - AstroNavCartridge - NanoChatCartridge # DV + - StockTradingCartridge - type: Tag # Ignore Chameleon tags tags: - DoorBumpOpener @@ -1187,6 +1203,13 @@ borderColor: "#3f3f74" - type: Icon state: pda-reporter + - type: CartridgeLoader # DeltaV + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - StockTradingCartridge # DeltaV + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml index 613bb1b1f4f165..7f64715f3ce384 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/_DV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml index eb9dc667db3ac1..7050de11fa40ef 100644 --- a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml @@ -18,3 +18,27 @@ - type: ActiveRadio channels: - Common + +- type: entity + parent: BaseItem + 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: Icon + sprite: _DV/Objects/Devices/cartridge.rsi + state: cart-mail + - 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"]] \ 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 00000000000000..74d29deb6845af --- /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 0790436f201f04..62a77083f5aec3 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/Textures/_DV/Misc/program_icons.rsi/meta.json b/Resources/Textures/_DV/Misc/program_icons.rsi/meta.json index 935cb557bb13d8..1f5f472528d265 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 0000000000000000000000000000000000000000..251b46a3f83cbe27088978ca4e58413d8ed9fe19 GIT binary patch literal 1012 zcmVH=4?=* zlOrL1eF_1!1=1Y=z}*FYhD8b_UXuVOkGCsu?;U-egrrF@97tCXgBWXbLdpjlyk-W+ z02rP=nq|6j1&8{V1Aab^>G;Kv4e;#9Ga%sWR_90>t5^cp0RUhM2scW%1qQjJblceyjNd_L=3x&b zYoy>S5ATq_xm}6e%MmrVVI^9O004#tqF$75!Bxf?kRvyAzn=4(*I6FBW&A!n^ToKQ zAG(fi8-RPY#eVnv(HTHVv>`QYo*T|yhm{*~M&|nAQsZri+b-GrQhVMBgog*@Mn}6i z2xBmbPM5yZV8vhI>_Mdd!Yn~VmT6485^FFoUu`*nsjq5=9?(9JSx*g~`HVk!| z=Spt<7C-!iUy&n>Z0*R;0V|EPbp)1zTh&y@ z%DQZ*+>LiZs%_p=$8P_*k=A5eP%7<{X%EQFB=t8v&<-B@L7dxKGo6+LI$7Oo|BPAp zKnOrX<~iMJnRBx?rsMn^=miIW z1+~G<18#If2EgE3(cfaSgzq+b%Z}0TF0000q!<_+7))m{Fa|R+rOk9uV_*niFzK9WFU!E-$zV2T zrh~>z2gUW17#K_^+}OgPtgLKcU?3qe7#J@dcmhO>_ z%)r2R7=#&*=dVZs3RZc#IEGmGzrApit3g4;HSls@YUXyw|Np~{r?Bl(+PmG6C3ca9 z(^B~*b_^di^zSufJX!lJ>U^<4(AEo5$6ri&;=s?$lPvQhQlLMo-P2krmg!tl*%qCU z9Z3vlEW{gMT~gK+{C~n-Ct~}ZQ%v>m+#lXw@ayW*x(TZq1j-CLJQeGAJyQm{f-g0~ dGtJkRK?}&{0Adih6g(M3dAj Date: Sat, 11 Jan 2025 15:06:20 -0800 Subject: [PATCH 2/7] Remove StockTrading cartridge from reporter PDAs to avoid confusion with access --- Resources/Prototypes/Entities/Objects/Devices/pda.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index bc92cf71743c5f..34a70b449b81ee 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -1203,13 +1203,6 @@ borderColor: "#3f3f74" - type: Icon state: pda-reporter - - type: CartridgeLoader # DeltaV - preinstalled: - - CrewManifestCartridge - - NotekeeperCartridge - - NewsReaderCartridge - - StockTradingCartridge # DeltaV - - NanoChatCartridge # DeltaV - type: entity parent: BasePDA From bcfd711b4f6c970b9b4ff84151db1859bb48bbfc Mon Sep 17 00:00:00 2001 From: Epic Toast <44736041+EpicToastTM@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:21:20 -0800 Subject: [PATCH 3/7] Remove icon component to match the NanoChat cartridge format --- .../Prototypes/_DV/Entities/Objects/Devices/cartridges.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml index 7050de11fa40ef..ae7e3f7fdd972e 100644 --- a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml @@ -28,9 +28,6 @@ - type: Sprite sprite: _DV/Objects/Devices/cartridge.rsi state: cart-stonk - - type: Icon - sprite: _DV/Objects/Devices/cartridge.rsi - state: cart-mail - type: UIFragment ui: !type:StockTradingUi - type: StockTradingCartridge From 5189b0be40dfef98ada1bbda5a22150f35711e8c Mon Sep 17 00:00:00 2001 From: Epic Toast <44736041+EpicToastTM@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:22:58 -0800 Subject: [PATCH 4/7] Update DV attributions --- Resources/Prototypes/Entities/Objects/Devices/pda.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 34a70b449b81ee..1f15f89ace8a2e 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -920,7 +920,7 @@ - LogProbeCartridge - AstroNavCartridge - NanoChatCartridge # DV - - StockTradingCartridge + - StockTradingCartridge # DV - type: entity parent: CentcomPDA @@ -946,7 +946,7 @@ - MedTekCartridge - AstroNavCartridge - NanoChatCartridge # DV - - StockTradingCartridge + - StockTradingCartridge # DV - type: Tag # Ignore Chameleon tags tags: - DoorBumpOpener From 4209e6b75f5ec62feecfe659e309738c67e44590 Mon Sep 17 00:00:00 2001 From: Epic Toast <44736041+EpicToastTM@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:43:56 -0800 Subject: [PATCH 5/7] Make the StockTrading cartridge a thief objective --- .../objectives/conditions/steal-target-groups.ftl | 1 + Resources/Prototypes/Objectives/objectiveGroups.yml | 1 + .../_DV/Entities/Objects/Devices/cartridges.yml | 4 +++- .../_Impstation/Objectives/stealTargetGroups.yml | 7 +++++++ Resources/Prototypes/_Impstation/Objectives/thief.yml | 9 +++++++++ 5 files changed, 21 insertions(+), 1 deletion(-) 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 d2a7bc985479c7..8a90d092a1820b 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/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 478858a1989d80..8de7d5b777b32c 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 ae7e3f7fdd972e..eb2d1c9df2875a 100644 --- a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml @@ -38,4 +38,6 @@ state: stock_trading - type: BankClient - type: AccessReader # This is so that we can restrict who can buy stocks - access: [["Cargo"]] \ No newline at end of file + access: [["Cargo"]] + - type: StealTarget + stealGroup: StockTradingCartridge \ No newline at end of file diff --git a/Resources/Prototypes/_Impstation/Objectives/stealTargetGroups.yml b/Resources/Prototypes/_Impstation/Objectives/stealTargetGroups.yml index 593cfc8d630886..15417bdd4679b9 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 d11216de15ae44..57e396a6e01fa9 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 From 7cf44aa1972e42cdaffc19368f1c1e5e299a34a6 Mon Sep 17 00:00:00 2001 From: Epic Toast <44736041+EpicToastTM@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:47:27 -0800 Subject: [PATCH 6/7] =?UTF-8?q?Change=20the=20speso=20symbol=20to=20=C2=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs | 2 +- .../CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs index f04d01c297bdd1..f8216c03ee3585 100644 --- a/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs @@ -50,7 +50,7 @@ public void Update(List priceHistory) var priceLabel = new Label { - Text = $"${price:F2}", + Text = $"§{price:F2}", HorizontalAlignment = HAlignment.Center, }; diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs index 34f71058c2bdcd..c5e5ed53b05204 100644 --- a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs @@ -238,7 +238,7 @@ public CompanyEntry(int companyIndex, public void Update(StockCompany company, int ownedStocks) { _nameLabel.Text = company.LocalizedDisplayName; - _priceLabel.Text = $"${company.CurrentPrice:F2}"; + _priceLabel.Text = $"§{company.CurrentPrice:F2}"; _sharesLabel.Text = Loc.GetString("stock-trading-owned-shares", ("shares", ownedStocks)); var priceChange = 0f; From 3c1d5d8b9142e55f6b422f4f29ba4ecea3adbec3 Mon Sep 17 00:00:00 2001 From: Epic Toast <44736041+EpicToastTM@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:09:15 -0800 Subject: [PATCH 7/7] Update default value & contraband status --- .../CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs | 1 + .../Prototypes/_DV/Entities/Objects/Devices/cartridges.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs index c5e5ed53b05204..2d88bc6b0ead0d 100644 --- a/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs @@ -168,6 +168,7 @@ public CompanyEntry(int companyIndex, _amountEdit = new LineEdit { + Text = "1", PlaceHolder = Loc.GetString("stock-trading-amount-placeholder"), HorizontalExpand = true, MinWidth = 80, diff --git a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml index eb2d1c9df2875a..62d31c8d862c05 100644 --- a/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/_DV/Entities/Objects/Devices/cartridges.yml @@ -20,7 +20,7 @@ - Common - type: entity - parent: BaseItem + parent: [BaseItem, BaseCargoContraband] id: StockTradingCartridge name: StockTrading cartridge description: A cartridge that tracks the intergalactic stock market.