diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/PriceHistoryTable.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/PriceHistoryTable.xaml
new file mode 100644
index 00000000000..249b4b35ab6
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/PriceHistoryTable.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs
new file mode 100644
index 00000000000..90cd778337d
--- /dev/null
+++ b/Content.Client/_CorvaxNext/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._CorvaxNext.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/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUi.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUi.cs
new file mode 100644
index 00000000000..06eef4e4460
--- /dev/null
+++ b/Content.Client/_CorvaxNext/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._CorvaxNext.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, float amount, BoundUserInterface userInterface)
+ {
+ var newsMessage = new StockTradingUiMessageEvent(action, company, amount);
+ var message = new CartridgeUiMessage(newsMessage);
+ userInterface.SendMessage(message);
+ }
+}
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml
new file mode 100644
index 00000000000..dc6035e15c0
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs
new file mode 100644
index 00000000000..7ed1aed5e6f
--- /dev/null
+++ b/Content.Client/_CorvaxNext/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._CorvaxNext.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 (float.TryParse(_amountEdit.Text, out var amount) && amount > 0)
+ onBuyPressed?.Invoke(companyIndex, amount);
+ };
+
+ _sellButton.OnPressed += _ =>
+ {
+ if (float.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(StockCompanyStruct 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/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs
index e4bd9515136..a03ef712c7b 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs
@@ -85,9 +85,11 @@ private void UpdateConsole(float frameTime)
{
_timer -= Delay;
- foreach (var account in EntityQuery())
+ var stationQuery = EntityQueryEnumerator(); // Corvax-Next-StockTrading: Early merge #33123
+ while (stationQuery.MoveNext(out var uid, out var bank)) // Corvax-Next-StockTrading: Early merge #33123
{
- account.Balance += account.IncreasePerSecond * Delay;
+ var balanceToAdd = bank.IncreasePerSecond * Delay;
+ UpdateBankAccount(uid, bank, balanceToAdd);
}
var query = EntityQueryEnumerator();
@@ -213,7 +215,7 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
orderDatabase.Orders.Remove(order);
- DeductFunds(bank, cost);
+ UpdateBankAccount(station.Value, bank, -cost); // Corvax-Next-StockTrading: Early merge #33123
UpdateOrders(station.Value);
}
@@ -536,11 +538,6 @@ private bool FulfillOrder(CargoOrderData order, EntityCoordinates spawn, string?
}
- private void DeductFunds(StationBankAccountComponent component, int amount)
- {
- component.Balance = Math.Max(0, component.Balance - amount);
- }
-
#region Station
private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)
diff --git a/Content.Server/_CorvaxNext/Cargo/Components/StationStockMarketComponent.cs b/Content.Server/_CorvaxNext/Cargo/Components/StationStockMarketComponent.cs
new file mode 100644
index 00000000000..a4830e9bfba
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Cargo/Components/StationStockMarketComponent.cs
@@ -0,0 +1,71 @@
+using System.Numerics;
+using Content.Server._CorvaxNext.Cargo.Systems;
+using Content.Server._CorvaxNext.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._CorvaxNext.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() { Chance = 0.86f, Range = new Vector2(-0.05f, 0.05f) }, // Minor
+ new() { Chance = 0.10f, Range = new Vector2(-0.3f, 0.2f) }, // Moderate
+ new() { Chance = 0.03f, Range = new Vector2(-0.5f, 1.5f) }, // Major
+ new() { Chance = 0.01f, Range = new Vector2(-0.9f, 4.0f) }, // Catastrophic
+ ];
+}
+
+[DataDefinition]
+public sealed partial class MarketChange
+{
+ [DataField(required: true)]
+ public float Chance;
+
+ [DataField(required: true)]
+ public Vector2 Range;
+}
diff --git a/Content.Server/_CorvaxNext/Cargo/StocksCommands.cs b/Content.Server/_CorvaxNext/Cargo/StocksCommands.cs
new file mode 100644
index 00000000000..08262d96156
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Cargo/StocksCommands.cs
@@ -0,0 +1,135 @@
+using Content.Server.Administration;
+using Content.Server._CorvaxNext.Cargo.Components;
+using Content.Server._CorvaxNext.Cargo.Systems;
+using Content.Shared.Administration;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Shared.Console;
+
+namespace Content.Server._CorvaxNext.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/_CorvaxNext/Cargo/Systems/StockMarketSystem.cs b/Content.Server/_CorvaxNext/Cargo/Systems/StockMarketSystem.cs
new file mode 100644
index 00000000000..eb0cc0af85b
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Cargo/Systems/StockMarketSystem.cs
@@ -0,0 +1,385 @@
+using Content.Server.Access.Systems;
+using Content.Server.Administration.Logs;
+using Content.Server.Cargo.Components;
+using Content.Server.Cargo.Systems;
+using Content.Server._CorvaxNext.Cargo.Components;
+using Content.Server._CorvaxNext.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 Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server._CorvaxNext.Cargo.Systems;
+
+///
+/// This handles the stock market updates
+///
+public sealed class StockMarketSystem : EntitySystem
+{
+ [Dependency] private readonly AccessReaderSystem _accessSystem = 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 _idCardSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = 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 companyIndex = message.CompanyIndex;
+ var amount = (int)message.Amount;
+ var station = ent.Comp.Station;
+ var loader = GetEntity(args.LoaderUid);
+ var xform = Transform(loader);
+
+ // Ensure station and stock market components are valid
+ if (station == null || !TryComp(station, out var stockMarket))
+ return;
+
+ // Validate company index
+ if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count)
+ return;
+
+ if (!TryComp(ent.Owner, out var access))
+ return;
+
+ // Attempt to retrieve ID card from loader
+ IdCardComponent? idCard = null;
+ if (_idCardSystem.TryGetIdCard(loader, out var pdaId))
+ idCard = pdaId;
+
+ // Play deny sound and exit if access is not allowed
+ if (idCard == null || !_accessSystem.IsAllowed(pdaId.Owner, ent.Owner, access))
+ {
+ _audio.PlayEntity(
+ stockMarket.DenySound,
+ Filter.Empty().AddInRange(_transform.GetMapCoordinates(loader, xform), 0.05f),
+ loader,
+ true,
+ AudioParams.Default.WithMaxDistance(0.05f)
+ );
+ 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(loader)} attempting to buy {amount} stocks of {company.LocalizedDisplayName}");
+ success = TryBuyStocks(station.Value, stockMarket, companyIndex, amount);
+ break;
+
+ case StockTradingUiAction.Sell:
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Medium,
+ $"{ToPrettyString(loader)} attempting to sell {amount} stocks of {company.LocalizedDisplayName}");
+ success = TrySellStocks(station.Value, stockMarket, companyIndex, amount);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ // Play confirmation sound if the transaction was successful
+ if (success)
+ {
+ _audio.PlayEntity(
+ stockMarket.ConfirmSound,
+ Filter.Empty().AddInRange(_transform.GetMapCoordinates(loader, xform), 0.05f),
+ loader,
+ true,
+ AudioParams.Default.WithMaxDistance(0.05f)
+ );
+ }
+ }
+ finally
+ {
+ // Raise the event to update the UI regardless of outcome
+ var ev = new StockMarketUpdatedEvent(station.Value);
+ RaiseLocalEvent(ev);
+ }
+ }
+
+ private bool TryBuyStocks(
+ EntityUid station,
+ StationStockMarketComponent stockMarket,
+ int companyIndex,
+ int amount)
+ {
+ 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);
+
+ // See if we can afford it
+ if (bank.Balance < totalValue)
+ return false;
+
+ if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned))
+ currentOwned = 0;
+
+ // Update the bank account
+ _cargo.UpdateBankAccount(station, bank, -totalValue);
+ stockMarket.StockOwnership[companyIndex] = currentOwned + amount;
+
+ // Log the transaction
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Medium,
+ $"[StockMarket] Bought {amount} stocks of {company.LocalizedDisplayName} at {company.CurrentPrice:F2} credits each (Total: {totalValue})");
+
+ return true;
+ }
+
+ private bool TrySellStocks(
+ EntityUid station,
+ StationStockMarketComponent stockMarket,
+ int companyIndex,
+ int amount)
+ {
+ 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;
+
+ if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned) || currentOwned < amount)
+ return false;
+
+ var company = stockMarket.Companies[companyIndex];
+ var totalValue = (int)Math.Round(company.CurrentPrice * amount);
+
+ // Update stock ownership
+ var newAmount = currentOwned - amount;
+ if (newAmount > 0)
+ stockMarket.StockOwnership[companyIndex] = newAmount;
+ else
+ stockMarket.StockOwnership.Remove(companyIndex);
+
+ // Update the bank account
+ _cargo.UpdateBankAccount(station, bank, totalValue);
+
+ // Log the transaction
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Medium,
+ $"[StockMarket] Sold {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(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
+ var ev = new StockMarketUpdatedEvent(station);
+ RaiseLocalEvent(ev);
+
+ // 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(company);
+
+ company.CurrentPrice = MathF.Max(newPrice, company.BasePrice * 0.1f);
+ stockMarket.Companies[companyIndex] = company;
+
+ var ev = new StockMarketUpdatedEvent(station);
+ RaiseLocalEvent(ev);
+ 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 StockCompanyStruct
+ {
+ LocalizedDisplayName = displayName, // Assume there's no Loc for it
+ BasePrice = basePrice,
+ CurrentPrice = basePrice,
+ PriceHistory = [],
+ };
+
+ stockMarket.Companies.Add(company);
+ UpdatePriceHistory(company);
+
+ var ev = new StockMarketUpdatedEvent(station);
+ RaiseLocalEvent(ev);
+
+ return true;
+ }
+
+ ///
+ /// Attempts to add a new company to the station using the StockCompanyStruct
+ ///
+ /// False if the company already exists, true otherwise
+ public bool TryAddCompany(EntityUid station,
+ StationStockMarketComponent stockMarket,
+ StockCompanyStruct company)
+ {
+ // Add the new company to the dictionary
+ stockMarket.Companies.Add(company);
+
+ // Make sure it has a price history
+ UpdatePriceHistory(company);
+
+ var ev = new StockMarketUpdatedEvent(station);
+ RaiseLocalEvent(ev);
+
+ return true;
+ }
+
+ private static void UpdatePriceHistory(StockCompanyStruct 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);
+ }
+}
+public sealed class StockMarketUpdatedEvent(EntityUid station) : EntityEventArgs
+{
+ public EntityUid Station = station;
+}
diff --git a/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs
new file mode 100644
index 00000000000..da842f2a27a
--- /dev/null
+++ b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server._CorvaxNext.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/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs
new file mode 100644
index 00000000000..f1f981ac774
--- /dev/null
+++ b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs
@@ -0,0 +1,101 @@
+using System.Linq;
+using Content.Server.Cargo.Components;
+using Content.Server._CorvaxNext.Cargo.Components;
+using Content.Server._CorvaxNext.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._CorvaxNext.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(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;
+
+ // Convert company data to UI state format
+ var entries = stockMarket.Companies.Select(company => new StockCompanyStruct(
+ displayName: company.LocalizedDisplayName,
+ currentPrice: company.CurrentPrice,
+ basePrice: company.BasePrice,
+ priceHistory: company.PriceHistory))
+ .ToList();
+
+ // Send the UI state with balance and owned stocks
+ var state = new StockTradingUiState(
+ entries: entries,
+ ownedStocks: stockMarket.StockOwnership,
+ balance: bankAccount.Balance
+ );
+
+ _cartridgeLoader.UpdateCartridgeUiState(loader, state);
+ }
+}
diff --git a/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs
new file mode 100644
index 00000000000..a80f8c6b8a8
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/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, float amount)
+ : CartridgeMessageEvent
+{
+ public readonly StockTradingUiAction Action = action;
+ public readonly int CompanyIndex = companyIndex;
+ public readonly float Amount = amount;
+}
+
+[Serializable, NetSerializable]
+public enum StockTradingUiAction
+{
+ Buy,
+ Sell,
+}
diff --git a/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiState.cs b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/StockTradingUiState.cs
new file mode 100644
index 00000000000..aea4ba5aa1d
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/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 StockCompanyStruct
+{
+ ///
+ /// 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 StockCompanyStruct(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/_corvaxnext/cargo/stocks-commands.ftl b/Resources/Locale/en-US/_corvaxnext/cargo/stocks-commands.ftl
new file mode 100644
index 00000000000..767f3f7b8c7
--- /dev/null
+++ b/Resources/Locale/en-US/_corvaxnext/cargo/stocks-commands.ftl
@@ -0,0 +1,12 @@
+# 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/ru-RU/_CorvaxNext/cargo/stocks-comapnies.ftl b/Resources/Locale/ru-RU/_CorvaxNext/cargo/stocks-comapnies.ftl
new file mode 100644
index 00000000000..7ab832daee4
--- /dev/null
+++ b/Resources/Locale/ru-RU/_CorvaxNext/cargo/stocks-comapnies.ftl
@@ -0,0 +1,24 @@
+stock-trading-company-ccoin = CCoins [CCN]
+stock-trading-company-nanotrasen = NanoTrasen [NT]
+stock-trading-company-vitezstvi = Vitezstvi [VTZ]
+stock-trading-company-kosmologistika = Kosmologistika [SPAC]
+stock-trading-company-nakamura = Nakamura Engineering [NKE]
+stock-trading-company-gefest = Gefest [GFS]
+stock-trading-company-saibasan = Saibasan [SBS]
+stock-trading-company-donk = Donk Co. [DONK]
+stock-trading-company-gorlex = Gorlex Securities LLC [GRX]
+stock-trading-company-deforest = DeForest Medical corp. [DFM]
+stock-trading-company-interdyne = Interdyne Pharmaceutics [IPA]
+stock-trading-company-conarex = Conarex [CNR]
+stock-trading-company-cybersun = Cybersun Industries [CSI]
+stock-trading-company-mrchang = Mr.Chang's [CHANG]
+stock-trading-company-drunkmasters = Drunk Masters [DRUNK]
+stock-trading-company-robust = Robust Industries LLC [ROBUST]
+stock-trading-company-azara = Azara Cleaning Сompany [CLEAN]
+stock-trading-company-waffle = Waffle corp. [WAFFLE]
+stock-trading-company-shady = Shady’s Cigarettes [CIG]
+stock-trading-company-intergalactic = interGalactic [IGA]
+stock-trading-company-hound = The Hound [HUNT]
+stock-trading-company-harvest = Harvest and Production [HAP]
+stock-trading-company-gasdef = GasDef [GD]
+stock-trading-company-hrc = High Rock Company [HRC]
diff --git a/Resources/Locale/ru-RU/_CorvaxNext/cargo/stocks-commands.ftl b/Resources/Locale/ru-RU/_CorvaxNext/cargo/stocks-commands.ftl
new file mode 100644
index 00000000000..fd04a78c2c2
--- /dev/null
+++ b/Resources/Locale/ru-RU/_CorvaxNext/cargo/stocks-commands.ftl
@@ -0,0 +1,13 @@
+# команда changestockprice (изменить цену акций)
+cmd-changestocksprice-desc = Изменяет цену акций компании на указанное значение.
+cmd-changestocksprice-help = changestockprice <Индекс компании> <Новая цена> [ID станции]
+cmd-changestocksprice-invalid-company = Не удалось выполнить команду! Неверный индекс компании или новая цена превышает допустимый лимит.
+cmd-changestocksprice-invalid-station = Биржа не найдена на указанной станции
+cmd-changestocksprice-no-stations = Не найдено станций с биржами
+
+# команда addstockscompany (добавить компанию)
+cmd-addstockscompany-desc = Добавляет новую компанию на биржевой рынок.
+cmd-addstockscompany-help = addstockscompany <Отображаемое название> <Базовая цена> [ID станции]
+cmd-addstockscompany-failure = Не удалось добавить компанию на биржу.
+cmd-addstockscompany-invalid-station = Биржа не найдена на указанной станции
+cmd-addstockscompany-no-stations = Не найдено станций с биржами
diff --git a/Resources/Locale/ru-RU/_CorvaxNext/cartridge-loader/stocktrading.ftl b/Resources/Locale/ru-RU/_CorvaxNext/cartridge-loader/stocktrading.ftl
new file mode 100644
index 00000000000..6a18445fe0b
--- /dev/null
+++ b/Resources/Locale/ru-RU/_CorvaxNext/cartridge-loader/stocktrading.ftl
@@ -0,0 +1,10 @@
+# General
+stock-trading-program-name = StockTrading
+stock-trading-title = Межгалактическая Фондовая Биржа
+stock-trading-balance = Баланс: {$balance} кредитов
+stock-trading-no-entries = Нет записей
+stock-trading-owned-shares = Во владении: {$shares}
+stock-trading-buy-button = Купить
+stock-trading-sell-button = Продать
+stock-trading-amount-placeholder = Количество
+stock-trading-price-history = История цен
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl
index 2d98eb112d8..95f8be17ade 100644
--- a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl
@@ -1,6 +1,8 @@
-ent-SecWatchCartridge = картридж "око сб"
+ent-SecWatchCartridge = картридж ОКО СБ
.desc = Программа для отслеживания статуса разыскиваемых службой безопасности лиц.
-ent-CrimeAssistCartridge = картридж "корзак про"
+ent-CrimeAssistCartridge = картридж КорЗак Про
.desc = Программа для помощи начинающим сотрудникам службы безопасности.
ent-NanoChatCartridge = картридж NanoChat
.desc = Для просто чилловых парней, которые любят початиться.
+ent-StockTradingCartridge = картридж StockTrading
+ .desc = Приложение для отслеживания цен на Межгалактической Фондовой Бирже.
\ 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 fd1fc15b5da..7108b785a9f 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
@@ -19,6 +19,7 @@
- id: RubberStampQm
- id: AstroNavCartridge
- id: PrinterDocFlatpack # Corvax-Printer
+ - id: StockTradingCartridge # Corvax-Next-StockTrading
- type: entity
id: LockerQuarterMasterFilled
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index 1006d21abb7..4f1157ab6c0 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
@@ -378,6 +378,13 @@
accentVColor: "#a23e3e"
- type: Icon
state: pda-qm
+ - type: CartridgeLoader # Corvax-Next
+ preinstalled:
+ - CrewManifestCartridge
+ - NotekeeperCartridge
+ - NewsReaderCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
+ - StockTradingCartridge # Corvax-Next-StockTrading
- type: entity
parent: BasePDA
@@ -522,6 +529,14 @@
borderColor: "#7C5D00"
- type: Icon
state: pda-captain
+ - type: CartridgeLoader
+ preinstalled:
+ - CrewManifestCartridge
+ - NotekeeperCartridge
+ - NewsReaderCartridge
+ - WantedListCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
+ - StockTradingCartridge # Corvax-Next-StockTrading
- type: entity
parent: BasePDA
@@ -783,7 +798,7 @@
- LogProbeCartridge
- WantedListCartridge
- MedTekCartridge
- - AstroNavCartridge
+ - StockTradingCartridge # Corvax-Next-StockTrading
- NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
@@ -1002,6 +1017,13 @@
borderColor: "#3f3f74"
- type: Icon
state: pda-reporter
+ - type: CartridgeLoader
+ preinstalled:
+ - CrewManifestCartridge
+ - NotekeeperCartridge
+ - NewsReaderCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
+ - StockTradingCartridge # Corvax-Next-StockTrading
- type: entity
parent: BasePDA
diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
index 09f3bda7075..56b17a5bad8 100644
--- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml
+++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
@@ -26,6 +26,7 @@
- BaseStationSiliconLawNTDefault # Corvax-NTDefault
- BaseStationAllEventsEligible
- BaseStationNanotrasen
+ - BaseStationStockMarket # Corvax-Next-StockTrading
categories: [ HideSpawnMenu ]
components:
- type: Transform
diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml
index 25c4c8b97ca..50e2b5d1260 100644
--- a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml
+++ b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml
@@ -61,3 +61,26 @@
state: cri
- type: CrimeAssistCartridge
+- type: entity
+ parent: BaseItem
+ id: StockTradingCartridge
+ name: StockTrading cartridge
+ description: A cartridge that tracks the intergalactic stock market.
+ components:
+ - type: Sprite
+ sprite: _CorvaxNext/Objects/Devices/cartridge.rsi
+ state: cart-stonk
+ - type: Icon
+ sprite: _CorvaxNext/Objects/Devices/cartridge.rsi
+ state: cart-stonk
+ - type: UIFragment
+ ui: !type:StockTradingUi
+ - type: StockTradingCartridge
+ - type: Cartridge
+ programName: stock-trading-program-name
+ icon:
+ sprite: _CorvaxNext/Misc/program_icons.rsi
+ state: stock_trading
+ - type: BankClient
+ - type: AccessReader # This is so that we can restrict who can buy stocks
+ access: [["Command"]]
diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Stations/base.yml b/Resources/Prototypes/_CorvaxNext/Entities/Stations/base.yml
new file mode 100644
index 00000000000..f5c41aee1c7
--- /dev/null
+++ b/Resources/Prototypes/_CorvaxNext/Entities/Stations/base.yml
@@ -0,0 +1,78 @@
+- type: entity
+ id: BaseStationStockMarket
+ abstract: true
+ components:
+ - type: StationStockMarket
+ companies:
+ - displayName: stock-trading-company-ccoin
+ basePrice: 1000
+ currentPrice: 1112
+ - displayName: stock-trading-company-nanotrasen
+ basePrice: 987
+ currentPrice: 955
+ - displayName: stock-trading-company-vitezstvi
+ basePrice: 642
+ currentPrice: 611
+ - displayName: stock-trading-company-kosmologistika
+ basePrice: 856
+ currentPrice: 831
+ - displayName: stock-trading-company-nakamura
+ basePrice: 765
+ currentPrice: 822
+ - displayName: stock-trading-company-gefest
+ basePrice: 698
+ currentPrice: 711
+ - displayName: stock-trading-company-saibasan
+ basePrice: 412
+ currentPrice: 389
+ - displayName: stock-trading-company-donk
+ basePrice: 98
+ currentPrice: 76
+ - displayName: stock-trading-company-gorlex
+ basePrice: 375
+ currentPrice: 391
+ - displayName: stock-trading-company-deforest
+ basePrice: 178
+ currentPrice: 193
+ - displayName: stock-trading-company-interdyne
+ basePrice: 521
+ currentPrice: 489
+ - displayName: stock-trading-company-conarex
+ basePrice: 455
+ currentPrice: 501
+ - displayName: stock-trading-company-cybersun
+ basePrice: 589
+ currentPrice: 577
+ - displayName: stock-trading-company-mrchang
+ basePrice: 311
+ currentPrice: 315
+ - displayName: stock-trading-company-drunkmasters
+ basePrice: 324
+ currentPrice: 322
+ - displayName: stock-trading-company-robust
+ basePrice: 478
+ currentPrice: 452
+ - displayName: stock-trading-company-azara
+ basePrice: 155
+ currentPrice: 159
+ - displayName: stock-trading-company-waffle
+ basePrice: 76
+ currentPrice: 81
+ - displayName: stock-trading-company-shady
+ basePrice: 221
+ currentPrice: 233
+ - displayName: stock-trading-company-intergalactic
+ basePrice: 431
+ currentPrice: 408
+ - displayName: stock-trading-company-hound
+ basePrice: 187
+ currentPrice: 176
+ - displayName: stock-trading-company-harvest
+ basePrice: 62
+ currentPrice: 55
+ - displayName: stock-trading-company-gasdef
+ basePrice: 250
+ currentPrice: 243
+ - displayName: stock-trading-company-hrc
+ basePrice: 280
+ currentPrice: 211
\ No newline at end of file
diff --git a/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json
index 935cb557bb1..7216991e9d1 100644
--- a/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json
+++ b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC0-1.0",
- "copyright": "nanochat made by kushbreth (discord)",
+ "copyright": "nanochat made by kushbreth (discord), stock_trading made by Malice",
"size": {
"x": 32,
"y": 32
@@ -9,6 +9,9 @@
"states": [
{
"name": "nanochat"
+ },
+ {
+ "name": "stock_trading"
}
]
}
diff --git a/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/stock_trading.png b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/stock_trading.png
new file mode 100644
index 00000000000..251b46a3f83
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/stock_trading.png differ
diff --git a/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-stonk.png b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-stonk.png
new file mode 100644
index 00000000000..ddfed6e915c
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-stonk.png differ
diff --git a/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json
index ebb40e495ca..6f9e3132805 100644
--- a/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json
+++ b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json
@@ -12,6 +12,9 @@
},
{
"name": "cart-chat"
+ },
+ {
+ "name": "cart-stonk"
}
]
-}
\ No newline at end of file
+}