From 1e79360b6e0b2222c8a4baa26216fdfa386af43d Mon Sep 17 00:00:00 2001 From: neckfire Date: Mon, 13 Jul 2026 02:55:54 +0200 Subject: [PATCH] feat: rotation shops (daily/weekly) + items insurance + season pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RequestStoreRotationData: rotation daily (reset 00:00 UTC) + weekly (lundi) avec storeIDs rotatifs déterministes et timers d'expiration corrects. - EnterMatchmakingStation: traite purchaseInsuranceRequest au déploiement — marque les items assurés (Insurance + owner) et débite la prime (TitleData InventoryInsurance). - FYPurchaseInventoryInsuranceRequest: modèle complété (insuranceId + itemIds). - RequestClaimFortunaPassRewards: claim saison-correct (S2/S3 séparés, plus d'écrasement) + octroi des récompenses depuis catalogue TitleData optionnel (repli sûr sinon). Co-Authored-By: Claude Opus 4.8 --- .../Functions/EnterMatchmakingStation.cs | 99 ++++++++++++++++- .../RequestClaimFortunaPassRewards.cs | 104 +++++++++++++++--- .../Functions/RequestStoreRotationData.cs | 30 ++++- .../FYPurchaseInventoryInsuranceRequest.cs | 15 ++- 4 files changed, 220 insertions(+), 28 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingStation.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingStation.cs index 018f235..aad04d6 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingStation.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingStation.cs @@ -1,18 +1,107 @@ -using Prospect.Server.Api.Services.CloudScript.Models; +using System.Text.Json; +using Prospect.Server.Api.Models.Data; +using Prospect.Server.Api.Services.Auth.Extensions; +using Prospect.Server.Api.Services.CloudScript.Models; +using Prospect.Server.Api.Services.CloudScript.Models.Data; +using Prospect.Server.Api.Services.UserData; namespace Prospect.Server.Api.Services.CloudScript.Functions; +// Config d'une police d'assurance (TitleData "InventoryInsurance"). +public class InventoryInsurancePolicy +{ + public string InsuranceId { get; set; } + public double Cost { get; set; } + public double Payout { get; set; } +} + [CloudScriptFunction("EnterMatchmakingStation")] public class EnterMatchmakingStationFunction : ICloudScriptFunction { - public Task ExecuteAsync(FYEnterMatchAzureFunction request) + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserDataService _userDataService; + private readonly TitleDataService _titleDataService; + private readonly ILogger _logger; + + public EnterMatchmakingStationFunction( + IHttpContextAccessor httpContextAccessor, + UserDataService userDataService, + TitleDataService titleDataService, + ILogger logger) { - return Task.FromResult(new FYEnterMatchmakingResult + _httpContextAccessor = httpContextAccessor; + _userDataService = userDataService; + _titleDataService = titleDataService; + _logger = logger; + } + + public async Task ExecuteAsync(FYEnterMatchAzureFunction request) + { + // Au déploiement, le client peut demander d'assurer une partie de l'inventaire. + var insurance = request.PurchaseInventoryInsurances; + var context = _httpContextAccessor.HttpContext; + if (context != null && insurance?.ItemIds is { Count: > 0 }) + { + try + { + await ApplyInsuranceAsync(context.User.FindAuthUserId(), insurance); + } + catch (Exception e) + { + // Ne jamais bloquer le déploiement à cause de l'assurance. + _logger.LogWarning(e, "Failed to apply inventory insurance on deploy"); + } + } + + return new FYEnterMatchmakingResult { Success = true, ErrorMessage = "", SingleplayerStation = true, // NOTE: This will always travel to single player station IsMatchTravel = false, - }); + }; } -} \ No newline at end of file + + // Marque les items choisis comme assurés (Insurance + propriétaire) et débite la + // prime en InsuranceCurrency selon le coût de la police (TitleData InventoryInsurance). + private async Task ApplyInsuranceAsync(string userId, FYPurchaseInventoryInsuranceRequest req) + { + var titleData = _titleDataService.Find(new List { "InventoryInsurance" }); + var policies = JsonSerializer.Deserialize>(titleData["InventoryInsurance"]); + var insuranceId = string.IsNullOrEmpty(req.InsuranceId) ? "Default" : req.InsuranceId; + if (policies == null || !policies.TryGetValue(insuranceId, out var policy)) + { + _logger.LogWarning("Unknown insurance policy {InsuranceId}", insuranceId); + return; + } + + var userData = await _userDataService.FindAsync(userId, userId, new List { "Inventory", "Balance" }); + var inventory = JsonSerializer.Deserialize>(userData["Inventory"].Value); + var balance = JsonSerializer.Deserialize(userData["Balance"].Value); + + var toInsure = new HashSet(req.ItemIds); + var insuredCount = 0; + foreach (var item in inventory) + { + if (!toInsure.Contains(item.ItemId)) continue; + item.Insurance = insuranceId; + item.InsuranceOwnerPlayfabId = userId; + insuredCount++; + } + if (insuredCount == 0) return; + + // Prime = coût de la police × nombre d'items assurés (arrondi au supérieur), + // plafonnée au solde disponible pour ne jamais passer en négatif. + var premium = (int)Math.Ceiling(policy.Cost * insuredCount); + premium = Math.Min(premium, balance.InsuranceCurrency); + balance.InsuranceCurrency -= premium; + + await _userDataService.UpdateAsync(userId, userId, new Dictionary + { + ["Inventory"] = JsonSerializer.Serialize(inventory), + ["Balance"] = JsonSerializer.Serialize(balance), + }); + _logger.LogInformation("Insured {Count} item(s) under policy {Policy} for user {User} (premium {Premium})", + insuredCount, insuranceId, userId, premium); + } +} diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs index fa7604b..1f0a7a2 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Prospect.Server.Api.Models.Data; using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript; using Prospect.Server.Api.Services.UserData; @@ -9,6 +10,15 @@ public class FYFortunaPassClaimedRewards { public List RewardsIDs { get; set; } }; +// Récompense d'un palier de la passe (catalogue optionnel TitleData "FortunaPass2_Rewards"). +// itemId "SoftCurrency"/"Aurum"/"InsuranceCurrency" = monnaie, sinon item d'inventaire. +public class FortunaPassRewardDef { + [JsonPropertyName("itemId")] + public string ItemId { get; set; } + [JsonPropertyName("amount")] + public int Amount { get; set; } +} + public class RequestClaimFortunaPassRewardsRequest { [JsonPropertyName("rewardsIds")] @@ -34,11 +44,13 @@ public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction ExecuteAsync(RequestClaimFortunaPassRewardsRequest request) @@ -49,28 +61,92 @@ public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction{"FortunaPass2_ClaimedRewards"}); - var claimedRewards = JsonSerializer.Deserialize(userData["FortunaPass2_ClaimedRewards"].Value); - foreach (var rewardId in request.RewardsIDs) { + + // Clé de la saison courante (déterminée par la config de build) — évite d'écraser + // la progression de l'autre saison (l'ancien code écrasait S2 ET S3). +#if SEASON_3_RELEASE || SEASON_3_DEBUG + const string claimedKey = "FortunaPass3_ClaimedRewards"; + const string rewardsCatalogKey = "FortunaPass3_Rewards"; +#else + const string claimedKey = "FortunaPass2_ClaimedRewards"; + const string rewardsCatalogKey = "FortunaPass2_Rewards"; +#endif + + var userData = await _userDataService.FindAsync(userId, userId, new List{ claimedKey, "Inventory", "Balance" }); + var claimedRewards = JsonSerializer.Deserialize(userData[claimedKey].Value) + ?? new FYFortunaPassClaimedRewards { RewardsIDs = new List() }; + claimedRewards.RewardsIDs ??= new List(); + + // Catalogue des récompenses (optionnel) : rewardId -> {itemId, amount}. + Dictionary catalog = null; + try + { + var td = _titleDataService.Find(new List { rewardsCatalogKey }); + if (td.TryGetValue(rewardsCatalogKey, out var raw) && !string.IsNullOrWhiteSpace(raw)) + catalog = JsonSerializer.Deserialize>(raw); + } + catch { /* pas de catalogue → on enregistre seulement la réclamation */ } + + var inventory = JsonSerializer.Deserialize>(userData["Inventory"].Value) ?? new List(); + var balance = JsonSerializer.Deserialize(userData["Balance"].Value); + var grantedItems = new List(); + var changedCurrencies = new List(); + + foreach (var rewardId in request.RewardsIDs) + { + // Ne pas re-créditer un palier déjà réclamé. + if (claimedRewards.RewardsIDs.Contains(rewardId)) continue; claimedRewards.RewardsIDs.Add(rewardId); + + if (catalog == null || !catalog.TryGetValue(rewardId, out var def) || def == null) continue; + + switch (def.ItemId) + { + case "SoftCurrency": + balance.SoftCurrency += def.Amount; + changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }); + break; + case "Aurum": + balance.HardCurrency += def.Amount; + changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }); + break; + case "InsuranceCurrency": + balance.InsuranceCurrency += def.Amount; + changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency }); + break; + default: + var item = new FYCustomItemInfo { + ItemId = Guid.NewGuid().ToString(), + Amount = def.Amount <= 0 ? 1 : def.Amount, + BaseItemId = def.ItemId, + Durability = 0, + Insurance = "", + InsuranceOwnerPlayfabId = "", + ModData = new FYModItems { M = [] }, + Origin = new FYItemOriginBackend { G = "", P = "", T = "" }, + InsuredAttachmentId = "", + PrimaryVanityId = 0, + SecondaryVanityId = 0, + RolledPerks = [], + }; + inventory.Add(item); + grantedItems.Add(item); + break; + } } - var output = JsonSerializer.Serialize(claimedRewards); - await _userDataService.UpdateAsync( - userId, userId, - new Dictionary{ - ["FortunaPass2_ClaimedRewards"] = output, - ["FortunaPass3_ClaimedRewards"] = output, // TODO: Separate logic for S2 and S3 - } - ); + var changes = new Dictionary { [claimedKey] = JsonSerializer.Serialize(claimedRewards) }; + if (grantedItems.Count > 0) changes["Inventory"] = JsonSerializer.Serialize(inventory); + if (changedCurrencies.Count > 0) changes["Balance"] = JsonSerializer.Serialize(balance); + await _userDataService.UpdateAsync(userId, userId, changes); return new RequestClaimFortunaPassRewardsResponse { UserId = userId, ErrorType = 3, // EYFortunaPassToastReponseType::Ok Error = "", - GrantedItems = [], - ChangedCurrencies = [], + GrantedItems = grantedItems.ToArray(), + ChangedCurrencies = changedCurrencies.ToArray(), }; } } diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestStoreRotationData.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestStoreRotationData.cs index 64d4598..1af67ae 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestStoreRotationData.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestStoreRotationData.cs @@ -35,6 +35,11 @@ public class RequestStoreRotationDataFunction : ICloudScriptFunction ExecuteAsync(RequestStoreRotationDataRequest request) { var context = _httpContextAccessor.HttpContext; @@ -43,18 +48,33 @@ public class RequestStoreRotationDataFunction : ICloudScriptFunction ItemIds { get; set; } +}