From 345a822a2e16154c4fc17496bd1611fd9ecf2a37 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 02:20:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(preprod):=20contrats=20fonctionnels,=20r?= =?UTF-8?q?=C3=A9putation=20factions,=20gating=20tech-tree=20&=20shop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RequestPlayerContracts: renvoie les vrais contrats actifs + job boards du joueur (peuplement initial des boards vides) au lieu d'une liste hardcodée. - RequestFactionProgression: renvoie la réputation réelle des 3 factions (au lieu de 0). - StartTechTreeNodeUpgradeClient: dependency check PlayerQuarterLevelRequired. - PurchaseWeaponShopItem: critère de déblocage par contrat (UnlockData.ContractLockPurchase). Co-Authored-By: Claude Opus 4.8 --- .../Functions/PurchaseWeaponShopItem.cs | 19 +- .../Functions/RequestFactionProgression.cs | 55 +++--- .../Functions/RequestPlayerContracts.cs | 167 ++++++++++-------- .../StartTechTreeNodeUpgradeClient.cs | 12 +- 4 files changed, 149 insertions(+), 104 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/PurchaseWeaponShopItem.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/PurchaseWeaponShopItem.cs index f450267..8df29de 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/PurchaseWeaponShopItem.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/PurchaseWeaponShopItem.cs @@ -3,6 +3,7 @@ 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.CloudScript.Functions; using Prospect.Server.Api.Services.UserData; public class PurchaseWeaponShopItemRequest @@ -137,7 +138,7 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction{"Inventory", "Balance"}); + var userData = await _userDataService.FindAsync(userId, userId, new List{"Inventory", "Balance", "ContractsOneTimeCompleted"}); var inventory = JsonSerializer.Deserialize>(userData["Inventory"].Value); var balance = JsonSerializer.Deserialize(userData["Balance"].Value); var blueprintsData = _titleDataService.Find(new List{"Blueprints"}); @@ -153,7 +154,21 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction(userData["ContractsOneTimeCompleted"].Value); + if (completed?.ContractsIDs == null || !completed.ContractsIDs.Contains(blueprintData.UnlockData.ContractLockPurchase)) + { + return new PurchaseWeaponShopItemResponse + { + UserID = userId, + Error = "Item is locked: complete the required contract first", + }; + } + } // Validate price var shopCraftingData = blueprintData.ItemShopsCraftingData[request.ShopItemBelongsTo]; diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestFactionProgression.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestFactionProgression.cs index 4f4a667..16912b8 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestFactionProgression.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestFactionProgression.cs @@ -1,5 +1,7 @@ -using Prospect.Server.Api.Services.Auth.Extensions; +using System.Text.Json; +using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript.Models; +using Prospect.Server.Api.Services.UserData; namespace Prospect.Server.Api.Services.CloudScript.Functions; @@ -7,42 +9,43 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions; public class RequestFactionProgression : ICloudScriptFunction { private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserDataService _userDataService; - public RequestFactionProgression(IHttpContextAccessor httpContextAccessor) + public RequestFactionProgression(IHttpContextAccessor httpContextAccessor, UserDataService userDataService) { _httpContextAccessor = httpContextAccessor; + _userDataService = userDataService; } - - public Task ExecuteAsync(FYQueryFactionProgressionRequest request) + + public async Task ExecuteAsync(FYQueryFactionProgressionRequest request) { var context = _httpContextAccessor.HttpContext; if (context == null) { - return Task.FromResult(null); + return null; } - - return Task.FromResult(new + var userId = context.User.FindAuthUserId(); + + var userData = await _userDataService.FindAsync( + userId, userId, + new List { "FactionProgressionICA", "FactionProgressionKorolev", "FactionProgressionOsiris" } + ); + + int Progression(string faction) => + userData.TryGetValue("FactionProgression" + faction, out var v) && v.Value != null + ? JsonSerializer.Deserialize(v.Value) + : 0; + + return new { - factions = new [] + factions = new[] { - new - { - factionId = "ICA", - currentProgression = 0 - }, - new - { - factionId = "Korolev", - currentProgression = 0 - }, - new - { - factionId = "Osiris", - currentProgression = 0 - }, + new { factionId = "ICA", currentProgression = Progression("ICA") }, + new { factionId = "Korolev", currentProgression = Progression("Korolev") }, + new { factionId = "Osiris", currentProgression = Progression("Osiris") }, }, - userId = context.User.FindAuthUserId(), + userId, error = "" - }); + }; } -} \ No newline at end of file +} diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestPlayerContracts.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestPlayerContracts.cs index c3fd37f..c2963b9 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestPlayerContracts.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestPlayerContracts.cs @@ -1,100 +1,119 @@ -using Prospect.Server.Api.Services.Auth.Extensions; +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; [CloudScriptFunction("RequestPlayerContracts")] public class RequestPlayerContracts : ICloudScriptFunction { - private readonly IHttpContextAccessor _httpContextAccessor; + // Board slots are ordered Easy / Medium / Hard (same convention as ClaimActiveContract). + private static readonly string[] Difficulties = ["Easy", "Medium", "Hard"]; + private const int RefreshHours = 12; - public RequestPlayerContracts(IHttpContextAccessor httpContextAccessor) + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserDataService _userDataService; + private readonly TitleDataService _titleDataService; + private readonly Random _rnd = new(); + + public RequestPlayerContracts(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService) { _httpContextAccessor = httpContextAccessor; + _userDataService = userDataService; + _titleDataService = titleDataService; } - public Task ExecuteAsync(FYGetPlayerContractsRequest request) + public async Task ExecuteAsync(FYGetPlayerContractsRequest request) { var context = _httpContextAccessor.HttpContext; if (context == null) { throw new CloudScriptException("CloudScript was not called within a http request"); } + var userId = context.User.FindAuthUserId(); - return Task.FromResult(new FYGetPlayerContractsResult + var userData = await _userDataService.FindAsync( + userId, userId, + new List + { + "ContractsActive", "JobBoardsData", + "FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris", + } + ); + + var contractsActive = JsonSerializer.Deserialize(userData["ContractsActive"].Value)!; + var jobBoardsData = JsonSerializer.Deserialize(userData["JobBoardsData"].Value)!; + + var titleData = _titleDataService.Find(new List { "Contracts", "Jobs", "LevelData" }); + var contracts = JsonSerializer.Deserialize>(titleData["Contracts"])!; + var jobs = JsonSerializer.Deserialize>>(titleData["Jobs"])!; + var levelsData = JsonSerializer.Deserialize>(titleData["LevelData"])!; + + var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var lastRefresh = jobBoardsData.LastBoardRefreshTimeUtc?.Seconds ?? 0; + + // Initial fill only: populate boards that were seeded empty / never refreshed. + // The periodic rotation is owned by RequestActiveObjectivesAndBoardsData, so we + // don't refresh on a timer here to avoid two competing refresh policies. + var boardsEmpty = jobBoardsData.Boards == null + || jobBoardsData.Boards.Any(b => b.Contracts == null + || b.Contracts.Any(c => string.IsNullOrEmpty(c.ContractId))); + + if (jobBoardsData.Boards != null && (lastRefresh == 0 || boardsEmpty)) { - UserId = context.User.FindAuthUserId(), + foreach (var board in jobBoardsData.Boards) + { + if (board.Contracts == null || !jobs.TryGetValue(board.FactionId, out var factionJobs)) + { + continue; + } + + var factionKey = "FactionProgression" + board.FactionId; + var factionProgression = userData.TryGetValue(factionKey, out var fp) + ? JsonSerializer.Deserialize(fp.Value) + : 0; + + var level = levelsData.TryGetValue(board.FactionId, out var levels) + ? levels.Length - Array.FindIndex(levels, xp => factionProgression >= xp) + : 0; + + for (var idx = 0; idx < board.Contracts.Count && idx < Difficulties.Length; idx++) + { + if (!factionJobs.TryGetValue(Difficulties[idx], out var difficultyJobs)) + { + continue; + } + var available = Array.FindAll(difficultyJobs, + jobId => contracts.TryGetValue(jobId, out var c) && level >= c.UnlockData.Level); + if (available.Length > 0) + { + board.Contracts[idx].ContractId = available[_rnd.Next(available.Length)]; + } + } + } + + jobBoardsData.LastBoardRefreshTimeUtc = new FYTimestamp { Seconds = now }; + + await _userDataService.UpdateAsync( + userId, userId, + new Dictionary { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) } + ); + } + + return new FYGetPlayerContractsResult + { + UserId = userId, Error = null, - ActiveContracts = new List(), + ActiveContracts = contractsActive.Contracts, FactionsContracts = new FYFactionsContractsData { - Boards = new List - { - new FYFactionContractsData - { - FactionId = "ICA", - Contracts = new List - { - new FYFactionContractData - { - ContractId = "NEW-Easy-ICA-Gather-1", - }, - new FYFactionContractData - { - ContractId = "NEW-Medium-ICA-Uplink-1", - }, - new FYFactionContractData - { - ContractId = "NEW-Hard-ICA-Uplink-1", - } - } - }, - new FYFactionContractsData - { - FactionId = "Korolev", - Contracts = new List - { - new FYFactionContractData - { - ContractId = "NEW-Easy-KOR-Mine-4", - }, - new FYFactionContractData - { - ContractId = "NEW-Medium-KOR-Mine-1", - }, - new FYFactionContractData - { - ContractId = "NEW-Hard-KOR-PvP-6", - } - } - }, - new FYFactionContractsData - { - FactionId = "Osiris", - Contracts = new List - { - new FYFactionContractData - { - ContractId = "NEW-Easy-Osiris-Brightcaps-1", - }, - new FYFactionContractData - { - ContractId = "NEW-Medium-Osiris-Gather-1", - }, - new FYFactionContractData - { - ContractId = "NEW-Hard-Osiris-Gather-7", - } - } - } - }, - LastBoardRefreshTimeUtc = new FYTimestamp - { - Seconds = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() - } + Boards = jobBoardsData.Boards, + LastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc ?? new FYTimestamp { Seconds = now }, }, - RefreshHours24UtcFromBackend = 12 - }); + RefreshHours24UtcFromBackend = RefreshHours, + }; } -} \ No newline at end of file +} diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/StartTechTreeNodeUpgradeClient.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/StartTechTreeNodeUpgradeClient.cs index 61b8713..ac382ef 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/StartTechTreeNodeUpgradeClient.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/StartTechTreeNodeUpgradeClient.cs @@ -54,7 +54,7 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction{"TechTreeNodeData", "Balance", "Inventory"} + new List{"TechTreeNodeData", "Balance", "Inventory", "PlayerQuartersLevel"} ); var userTechTree = JsonSerializer.Deserialize(userData["TechTreeNodeData"].Value); @@ -79,7 +79,15 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction(userData["PlayerQuartersLevel"].Value); + if (playerQuarters != null && playerQuarters.Level < techTreeNodeData.PlayerQuarterLevelRequired) { + return new FYStartTechTreeNodeUpgradeClientResult + { + UserID = userId, + Error = $"Requires player quarters level {techTreeNodeData.PlayerQuarterLevelRequired}", + }; + } // Validate price // TODO: Refactor