diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs index b7a2c285fe..0e8754ab8e 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml.cs +++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs @@ -147,6 +147,10 @@ private void AddListingGui(ListingData listing) } var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture); + + if (listing.SaleAmount > 0) // WD EDIT + newListing.StoreItemBuyButton.AddStyleClass("ButtonColorRed"); + newListing.StoreItemBuyButton.OnButtonDown += args => OnListingButtonPressed?.Invoke(args, listing); diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 25f64ba4b6..54f4aec746 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -265,6 +265,14 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi listing.PurchaseAmount++; //track how many times something has been purchased _audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching! + //WD EDIT START + if (listing.SaleLimit != 0 && listing.SaleAmount > 0 && listing.PurchaseAmount >= listing.SaleLimit) + { + listing.SaleAmount = 0; + listing.Cost = listing.OldCost; + } + //WD EDIT END + UpdateUserInterface(buyer, uid, component); } diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 72aeb29d19..5d54c17e59 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -10,6 +10,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using System.Linq; +using Content.Server._White.StoreDiscount; using Robust.Shared.Utility; namespace Content.Server.Store.Systems; @@ -22,6 +23,7 @@ public sealed partial class StoreSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly StoreDiscountSystem _storeDiscount = default!; // WD EDIT public override void Initialize() { @@ -199,6 +201,8 @@ public void InitializeFromPreset(StorePresetPrototype preset, EntityUid uid, Sto if (component.Balance == new Dictionary() && preset.InitialBalance != null) //if we don't have a value stored, use the preset TryAddCurrency(preset.InitialBalance, uid, component); + _storeDiscount.ApplySales(component.Listings, preset); // WD EDIT + var ui = _ui.GetUiOrNull(uid, StoreUiKey.Key); if (ui != null) { @@ -225,7 +229,7 @@ public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid us /// -/// Nyano/DeltaV Code. For penguin bombs and what not. +/// Nyano/DeltaV Code. For penguin bombs and what not. /// Raised on an item when it is purchased. /// An item may need to set it upself up for its purchaser. /// For example, to make sure it isn't hostile to them or diff --git a/Content.Server/_White/StoreDiscount/StoreDiscountSystem.cs b/Content.Server/_White/StoreDiscount/StoreDiscountSystem.cs new file mode 100644 index 0000000000..f9f45fe96d --- /dev/null +++ b/Content.Server/_White/StoreDiscount/StoreDiscountSystem.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Content.Shared.FixedPoint; +using Content.Shared.Store; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._White.StoreDiscount; + +public sealed class StoreDiscountSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + public void ApplySales(IEnumerable listings, StorePresetPrototype store) + { + if (!store.Sales.Enabled) + return; + + var count = _random.Next(store.Sales.MinItems, store.Sales.MaxItems + 1); + + listings = listings + .Where(l => !l.SaleBlacklist && l.Cost.Any(x => x.Value > 1) + && store.Categories.Overlaps(ChangedFormatCategories(l.Categories))) + .OrderBy(_ => _random.Next()).Take(count).ToList(); + + foreach (var listing in listings) + { + var sale = GetSale(store.Sales.MinMultiplier, store.Sales.MaxMultiplier); + var newCost = listing.Cost.ToDictionary(x => x.Key, + x => FixedPoint2.New(Math.Max(1, (int) MathF.Round(x.Value.Float() * sale)))); + + if (listing.Cost.All(x => x.Value.Int() == newCost[x.Key].Int())) + continue; + + var key = listing.Cost.First(x => x.Value > 0).Key; + listing.OldCost = listing.Cost; + listing.SaleAmount = 100 - (newCost[key] / listing.Cost[key] * 100).Int(); + listing.Cost = newCost; + listing.Categories = new() {store.Sales.SalesCategory}; + } + } + + private IEnumerable ChangedFormatCategories(List> categories) + { + var modified = from p in categories select p.Id; + + return modified; + } + + private float GetSale(float minMultiplier, float maxMultiplier) + { + return _random.NextFloat() * (maxMultiplier - minMultiplier) + minMultiplier; + } +} diff --git a/Content.Shared/Store/ListingLocalisationHelpers.cs b/Content.Shared/Store/ListingLocalisationHelpers.cs index 882300109c..390b17cb41 100644 --- a/Content.Shared/Store/ListingLocalisationHelpers.cs +++ b/Content.Shared/Store/ListingLocalisationHelpers.cs @@ -18,6 +18,13 @@ public static string GetLocalisedNameOrEntityName(ListingData listingData, IProt else if (listingData.ProductEntity != null) name = prototypeManager.Index(listingData.ProductEntity.Value).Name; + // WD START + if (listingData.SaleAmount > 0) + name += " " + Loc.GetString("store-sales-amount", ("amount", listingData.SaleAmount)); + else if (listingData.OldCost.Count > 0) + name += " " + Loc.GetString("store-sales-over"); + // WD END + return name; } diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs index d3d2e13cdf..d7203abce3 100644 --- a/Content.Shared/Store/ListingPrototype.cs +++ b/Content.Shared/Store/ListingPrototype.cs @@ -109,6 +109,21 @@ public partial class ListingData : IEquatable, ICloneable [DataField] public TimeSpan RestockTime = TimeSpan.Zero; + // WD START + [DataField("saleLimit")] + public int SaleLimit = 3; + + [DataField("saleBlacklist")] + public bool SaleBlacklist; + + public int SaleAmount; + + public Dictionary, FixedPoint2> OldCost = new(); + + [DataField] + public List Components = new(); + // WD END + public bool Equals(ListingData? listing) { if (listing == null) @@ -166,6 +181,13 @@ public object Clone() ProductEvent = ProductEvent, PurchaseAmount = PurchaseAmount, RestockTime = RestockTime, + // WD START + SaleLimit = SaleLimit, + SaleBlacklist = SaleBlacklist, + SaleAmount = SaleAmount, + OldCost = OldCost, + Components = Components, + // WD END }; } } diff --git a/Content.Shared/Store/StorePresetPrototype.cs b/Content.Shared/Store/StorePresetPrototype.cs index ce7f0312b6..410ab591a5 100644 --- a/Content.Shared/Store/StorePresetPrototype.cs +++ b/Content.Shared/Store/StorePresetPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared._White.StoreDiscount; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; @@ -38,4 +39,9 @@ public sealed partial class StorePresetPrototype : IPrototype /// [DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] public HashSet CurrencyWhitelist { get; private set; } = new(); + + // WD EDIT START + [DataField("sales")] + public SalesSpecifier Sales { get; private set; } = new(); + // WD EDIT END } diff --git a/Content.Shared/_White/StoreDiscount/SalesSpecifier.cs b/Content.Shared/_White/StoreDiscount/SalesSpecifier.cs new file mode 100644 index 0000000000..450f65041f --- /dev/null +++ b/Content.Shared/_White/StoreDiscount/SalesSpecifier.cs @@ -0,0 +1,38 @@ +namespace Content.Shared._White.StoreDiscount; + +[DataDefinition] +public sealed partial class SalesSpecifier +{ + [DataField("enabled")] + public bool Enabled { get; private set; } + + [DataField("minMultiplier")] + public float MinMultiplier { get; private set; } + + [DataField("maxMultiplier")] + public float MaxMultiplier { get; private set; } + + [DataField("minItems")] + public int MinItems { get; private set; } + + [DataField("maxItems")] + public int MaxItems { get; private set; } + + [DataField("salesCategory")] + public string SalesCategory { get; private set; } = string.Empty; + + public SalesSpecifier() + { + } + + public SalesSpecifier(bool enabled, float minMultiplier, float maxMultiplier, int minItems, int maxItems, + string salesCategory) + { + Enabled = enabled; + MinMultiplier = minMultiplier; + MaxMultiplier = maxMultiplier; + MinItems = minItems; + MaxItems = maxItems; + SalesCategory = salesCategory; + } +} diff --git a/Resources/Locale/en-US/_white/store/sales.ftl b/Resources/Locale/en-US/_white/store/sales.ftl new file mode 100644 index 0000000000..be52988698 --- /dev/null +++ b/Resources/Locale/en-US/_white/store/sales.ftl @@ -0,0 +1,2 @@ +store-sales-amount = [SALE] { $amount }%! +store-sales-over = [The sale is over] \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_white/store/sales.ftl b/Resources/Locale/ru-RU/_white/store/sales.ftl new file mode 100644 index 0000000000..1f0e3b83d3 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/store/sales.ftl @@ -0,0 +1,2 @@ +store-sales-amount = [СКИДКА] { $amount }%! +store-sales-over = [Скидка закончилась] \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index a1a60e3fef..94a5cc94a6 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -10,6 +10,7 @@ Telecrystal: 3 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkRevolverPython @@ -20,6 +21,7 @@ Telecrystal: 8 # Originally was 13 TC but was not used due to high cost categories: - UplinkWeapons + saleLimit: 1 # WD EDIT # Inbuilt suppressor so it's sneaky + more expensive. - type: listing @@ -31,6 +33,7 @@ Telecrystal: 4 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT # Poor accuracy, slow to fire, cheap option - type: listing @@ -42,6 +45,7 @@ Telecrystal: 1 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkEsword @@ -53,6 +57,7 @@ Telecrystal: 8 categories: - UplinkWeapons + saleLimit: 2 # WD EDIT - type: listing id: UplinkEnergyDagger @@ -64,6 +69,7 @@ Telecrystal: 2 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkThrowingKnivesKit @@ -85,6 +91,7 @@ Telecrystal: 8 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkDisposableTurret @@ -100,6 +107,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 2 # WD EDIT - type: listing id: BaseBallBatHomeRun @@ -112,6 +120,7 @@ Telecrystal: 16 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT # Explosives @@ -214,6 +223,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkC4Bundle @@ -224,6 +234,7 @@ Telecrystal: 12 #you're buying bulk so its a 25% discount categories: - UplinkExplosives + saleLimit: 1 # WD EDIT - type: listing id: UplinkEmpGrenade @@ -261,6 +272,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkSyndicateBombNukie @@ -276,6 +288,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkClusterGrenade @@ -451,6 +464,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns an agent with the NukeOperative component. @@ -467,6 +481,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkReinforcementRadioSyndicateCyborgAssault @@ -483,6 +498,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkReinforcementRadioSyndicateMonkey @@ -515,6 +531,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkStealthBox @@ -545,7 +562,6 @@ productEntity: EncryptionKeyBinary cost: Telecrystal: 1 - categories: - UplinkUtility @@ -748,6 +764,7 @@ blacklist: tags: - NukeOpsUplink + saleBlacklist: true # WD EDIT - type: listing id: UplinkDeathRattle @@ -821,6 +838,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 # WD EDIT - type: listing id: UplinkChemistryKitBundle @@ -862,6 +880,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 # WD EDIT - type: listing id: UplinkSniperBundle @@ -873,6 +892,7 @@ Telecrystal: 12 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkC20RBundle @@ -884,6 +904,7 @@ Telecrystal: 17 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkBulldogBundle @@ -895,6 +916,7 @@ Telecrystal: 20 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkGrenadeLauncherBundle @@ -906,6 +928,7 @@ Telecrystal: 25 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkL6SawBundle @@ -917,6 +940,7 @@ Telecrystal: 30 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkZombieBundle @@ -937,6 +961,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 # WD EDIT - type: listing id: UplinkSurplusBundle @@ -956,6 +981,7 @@ blacklist: components: - SurplusBundle + saleBlacklist: true # WD EDIT - type: listing id: UplinkSuperSurplusBundle @@ -975,6 +1001,7 @@ blacklist: components: - SurplusBundle + saleBlacklist: true # WD EDIT # Tools @@ -1113,6 +1140,7 @@ - !type:BuyerJobCondition whitelist: - Chaplain + saleLimit: 1 # WD EDIT - type: listing id: uplinkRevolverCapGunFake @@ -1128,6 +1156,7 @@ whitelist: - Mime - Clown + saleLimit: 1 # WD EDIT - type: listing id: uplinkBananaPeelExplosive @@ -1172,6 +1201,7 @@ - !type:BuyerJobCondition whitelist: - Clown + saleLimit: 1 # WD EDIT - type: listing id: uplinkHotPotato @@ -1335,6 +1365,7 @@ Telecrystal: 8 categories: - UplinkArmor + saleLimit: 1 # WD EDIT - type: listing id: UplinkHardsuitSyndieElite @@ -1346,6 +1377,7 @@ Telecrystal: 10 categories: - UplinkArmor + saleLimit: 1 # WD EDIT - type: listing id: UplinkClothingOuterHardsuitJuggernaut @@ -1357,6 +1389,7 @@ Telecrystal: 12 categories: - UplinkArmor + saleLimit: 1 # WD EDIT # Misc @@ -1425,6 +1458,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkSoapSyndie @@ -1515,6 +1549,7 @@ Telecrystal: 12 categories: - UplinkMisc + saleLimit: 1 # WD EDIT - type: listing id: UplinkBribe @@ -1541,6 +1576,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkBackpackSyndicate diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index e623f4c8cd..14cce8feb2 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -13,5 +13,13 @@ - UplinkJob - UplinkArmor - UplinkPointless + - UplinkSales # WD EDIT currencyWhitelist: - Telecrystal + sales: # WD EDIT + enabled: true + minMultiplier: 0.01 + maxMultiplier: 0.99 + minItems: 3 + maxItems: 8 + salesCategory: UplinkSales diff --git a/Resources/Prototypes/_White/Store/categories.yml b/Resources/Prototypes/_White/Store/categories.yml new file mode 100644 index 0000000000..90755f4f2f --- /dev/null +++ b/Resources/Prototypes/_White/Store/categories.yml @@ -0,0 +1,4 @@ +- type: storeCategory + id: UplinkSales + name: Sales! + priority: 10