using Newtonsoft.Json.Linq; using Campus_live_rewrite.api.server.Classes; namespace Campus_live_rewrite.api.server { /// /// One Rec Center Token price per item across every store (2026-09-27 request: Thriller Suit /// (Orange) was 4000 in the Watch and 4500 in Pop Up Shop 1). /// /// The price an item sells for is the one the admin most recently SET for it in the Stores tab, /// in whichever store (rows carry "PriceSetAt" from then on; older edits count as set before any /// stamped one, newest admin row first among them). An item never priced there keeps the Watch /// store's price. Every served store row priced in tokens is rewritten to it (subscriber price /// = the usual 10% off), and a purchase is charged it. Items priced in room currencies or other /// currencies are left alone. /// /// An item is its GiftDropId plus the pack size (ItemCount), so a 1-pack and a 5-pack of the /// same consumable keep their own prices. /// public static class StorePrices { public const string PriceSetAtKey = "PriceSetAt"; private const int Tokens = (int)CurrencyType.RecCenterTokens; public static string? ItemKey(JToken row) { var gd = row["GiftDrop"]; int drop = (int?)gd?["GiftDropId"] ?? 0; if (drop == 0) return null; int count = Math.Max(1, (int?)gd?["ItemCount"] ?? 1); return $"{drop}x{count}"; } /// The row's token price, or null when it is not sold for tokens. public static int? TokenPrice(JToken row) { foreach (var p in row["Prices"] as JArray ?? new JArray()) if (((int?)p["CurrencyType"] ?? 0) == Tokens) return (int?)p["Price"]; return null; } // ---- the canonical price table ------------------------------------------------------------ private static readonly object _lock = new(); private static string _key = ""; private static Dictionary _prices = new(); private static Dictionary _watchBase = new(); private static string _watchKey = ""; /// Called while the Watch store is built, BEFORE unification: its own prices are the fallback. public static void SetWatchBase(JObject watchRoot, string watchKey) { var map = new Dictionary(); foreach (var r in watchRoot["StoreItems"] as JArray ?? new JArray()) if (ItemKey(r) is { } k && TokenPrice(r) is { } p && !map.ContainsKey(k)) map[k] = p; lock (_lock) { _watchBase = map; _watchKey = watchKey; } } public static string WatchKey { get { lock (_lock) return _watchKey; } } private static Dictionary Table() { string key = $"{StoreEdits.Version}|{WatchKey}"; lock (_lock) { if (key == _key) return _prices; // Every admin price, newest first: stamped rows by stamp, unstamped (older) rows by // admin id (a higher id was added later). var edits = new List<(string item, int price, DateTime at, int pid)>(); foreach (int store in Enum.GetValues(typeof(StorefrontType)).Cast().Distinct()) { if (StoreServe.IsPerPlayer(store)) continue; foreach (var r in StoreEdits.Rows(store)) { if (ItemKey(r) is not { } item || TokenPrice(r) is not { } price) continue; DateTime at = DateTime.TryParse((string?)r[PriceSetAtKey], null, System.Globalization.DateTimeStyles.RoundtripKind, out var t) ? t : DateTime.MinValue; edits.Add((item, price, at, (int?)r["PurchasableItemId"] ?? 0)); } } var table = new Dictionary(_watchBase); foreach (var e in edits.OrderBy(e => e.at).ThenBy(e => e.pid)) table[e.item] = e.price; // later wins _prices = table; _key = key; return table; } } /// The one token price this item sells for, or null when no store has it for tokens. public static int? PriceFor(JToken row) => ItemKey(row) is { } k && Table().TryGetValue(k, out var p) ? p : null; /// Rewrite every token-priced row of a served store to the item's one price. Returns how many changed. public static int Apply(JObject root) { int changed = 0; foreach (var r in (root["StoreItems"] as JArray ?? new JArray()).OfType()) { r.Remove(PriceSetAtKey); // bookkeeping only - never sent to the client if (TokenPrice(r) is not { } now || PriceFor(r) is not { } want || want == now) continue; foreach (var p in r["Prices"] as JArray ?? new JArray()) if (((int?)p["CurrencyType"] ?? 0) == Tokens) p["Price"] = want; foreach (var p in r["SubscriberPrices"] as JArray ?? new JArray()) if (((int?)p["CurrencyType"] ?? 0) == Tokens) p["Price"] = StoreGenerator.SubscriberPriceFor(want); changed++; } return changed; } /// Charge the served (unified) price for a purchase priced in tokens. public static void ApplyTo(StoreCatalog.GiftDropInfo item, JObject? servedRow) { if (servedRow == null || item.Currency != Tokens || TokenPrice(servedRow) is not { } price) return; item.Cost = price; if (item.SubscriberCurrency < 0 || item.SubscriberCurrency == Tokens) { item.SubscriberCost = StoreGenerator.SubscriberPriceFor(price); item.SubscriberCurrency = Tokens; } } } }