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 +}