feat(preprod): contrats fonctionnels, réputation factions, gating tech-tree & shop
Build & Deploy / build (push) Successful in 21s
Build & Deploy / build (push) Successful in 21s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
using Prospect.Server.Api.Models.Data;
|
using Prospect.Server.Api.Models.Data;
|
||||||
using Prospect.Server.Api.Services.Auth.Extensions;
|
using Prospect.Server.Api.Services.Auth.Extensions;
|
||||||
using Prospect.Server.Api.Services.CloudScript;
|
using Prospect.Server.Api.Services.CloudScript;
|
||||||
|
using Prospect.Server.Api.Services.CloudScript.Functions;
|
||||||
using Prospect.Server.Api.Services.UserData;
|
using Prospect.Server.Api.Services.UserData;
|
||||||
|
|
||||||
public class PurchaseWeaponShopItemRequest
|
public class PurchaseWeaponShopItemRequest
|
||||||
@@ -137,7 +138,7 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeapo
|
|||||||
Error = "Invalid purchase amount",
|
Error = "Invalid purchase amount",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance"});
|
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance", "ContractsOneTimeCompleted"});
|
||||||
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
|
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
|
||||||
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
|
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
|
||||||
var blueprintsData = _titleDataService.Find(new List<string>{"Blueprints"});
|
var blueprintsData = _titleDataService.Find(new List<string>{"Blueprints"});
|
||||||
@@ -153,7 +154,21 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeapo
|
|||||||
|
|
||||||
var blueprintData = blueprints[request.BaseItemID];
|
var blueprintData = blueprints[request.BaseItemID];
|
||||||
|
|
||||||
// TODO: Contract unlock criteria
|
// Contract unlock criteria: some items can only be purchased after completing a
|
||||||
|
// specific contract. Only gate when the item explicitly declares such a lock, so
|
||||||
|
// items with no lock keep working as before.
|
||||||
|
if (!string.IsNullOrEmpty(blueprintData.UnlockData?.ContractLockPurchase))
|
||||||
|
{
|
||||||
|
var completed = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(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
|
// Validate price
|
||||||
var shopCraftingData = blueprintData.ItemShopsCraftingData[request.ShopItemBelongsTo];
|
var shopCraftingData = blueprintData.ItemShopsCraftingData[request.ShopItemBelongsTo];
|
||||||
|
|||||||
+26
-23
@@ -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.CloudScript.Models;
|
||||||
|
using Prospect.Server.Api.Services.UserData;
|
||||||
|
|
||||||
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
||||||
|
|
||||||
@@ -7,42 +9,43 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
|||||||
public class RequestFactionProgression : ICloudScriptFunction<FYQueryFactionProgressionRequest, object?>
|
public class RequestFactionProgression : ICloudScriptFunction<FYQueryFactionProgressionRequest, object?>
|
||||||
{
|
{
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly UserDataService _userDataService;
|
||||||
|
|
||||||
public RequestFactionProgression(IHttpContextAccessor httpContextAccessor)
|
public RequestFactionProgression(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
|
||||||
{
|
{
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_userDataService = userDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<object?> ExecuteAsync(FYQueryFactionProgressionRequest request)
|
public async Task<object?> ExecuteAsync(FYQueryFactionProgressionRequest request)
|
||||||
{
|
{
|
||||||
var context = _httpContextAccessor.HttpContext;
|
var context = _httpContextAccessor.HttpContext;
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<object?>(null);
|
return null;
|
||||||
}
|
}
|
||||||
|
var userId = context.User.FindAuthUserId();
|
||||||
|
|
||||||
return Task.FromResult<object?>(new
|
var userData = await _userDataService.FindAsync(
|
||||||
|
userId, userId,
|
||||||
|
new List<string> { "FactionProgressionICA", "FactionProgressionKorolev", "FactionProgressionOsiris" }
|
||||||
|
);
|
||||||
|
|
||||||
|
int Progression(string faction) =>
|
||||||
|
userData.TryGetValue("FactionProgression" + faction, out var v) && v.Value != null
|
||||||
|
? JsonSerializer.Deserialize<int>(v.Value)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return new
|
||||||
{
|
{
|
||||||
factions = new []
|
factions = new[]
|
||||||
{
|
{
|
||||||
new
|
new { factionId = "ICA", currentProgression = Progression("ICA") },
|
||||||
{
|
new { factionId = "Korolev", currentProgression = Progression("Korolev") },
|
||||||
factionId = "ICA",
|
new { factionId = "Osiris", currentProgression = Progression("Osiris") },
|
||||||
currentProgression = 0
|
|
||||||
},
|
},
|
||||||
new
|
userId,
|
||||||
{
|
|
||||||
factionId = "Korolev",
|
|
||||||
currentProgression = 0
|
|
||||||
},
|
|
||||||
new
|
|
||||||
{
|
|
||||||
factionId = "Osiris",
|
|
||||||
currentProgression = 0
|
|
||||||
},
|
|
||||||
},
|
|
||||||
userId = context.User.FindAuthUserId(),
|
|
||||||
error = ""
|
error = ""
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
||||||
|
using Prospect.Server.Api.Services.UserData;
|
||||||
|
|
||||||
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
||||||
|
|
||||||
[CloudScriptFunction("RequestPlayerContracts")]
|
[CloudScriptFunction("RequestPlayerContracts")]
|
||||||
public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsRequest, FYGetPlayerContractsResult>
|
public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsRequest, FYGetPlayerContractsResult>
|
||||||
{
|
{
|
||||||
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;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_userDataService = userDataService;
|
||||||
|
_titleDataService = titleDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<FYGetPlayerContractsResult> ExecuteAsync(FYGetPlayerContractsRequest request)
|
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYGetPlayerContractsRequest request)
|
||||||
{
|
{
|
||||||
var context = _httpContextAccessor.HttpContext;
|
var context = _httpContextAccessor.HttpContext;
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new CloudScriptException("CloudScript was not called within a http request");
|
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<string>
|
||||||
{
|
{
|
||||||
UserId = context.User.FindAuthUserId(),
|
"ContractsActive", "JobBoardsData",
|
||||||
|
"FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value)!;
|
||||||
|
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value)!;
|
||||||
|
|
||||||
|
var titleData = _titleDataService.Find(new List<string> { "Contracts", "Jobs", "LevelData" });
|
||||||
|
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"])!;
|
||||||
|
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"])!;
|
||||||
|
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(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))
|
||||||
|
{
|
||||||
|
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<int>(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<string, string> { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FYGetPlayerContractsResult
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
Error = null,
|
Error = null,
|
||||||
ActiveContracts = new List<FYActiveContractPlayerData>(),
|
ActiveContracts = contractsActive.Contracts,
|
||||||
FactionsContracts = new FYFactionsContractsData
|
FactionsContracts = new FYFactionsContractsData
|
||||||
{
|
{
|
||||||
Boards = new List<FYFactionContractsData>
|
Boards = jobBoardsData.Boards,
|
||||||
{
|
LastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc ?? new FYTimestamp { Seconds = now },
|
||||||
new FYFactionContractsData
|
|
||||||
{
|
|
||||||
FactionId = "ICA",
|
|
||||||
Contracts = new List<FYFactionContractData>
|
|
||||||
{
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Easy-ICA-Gather-1",
|
|
||||||
},
|
},
|
||||||
new FYFactionContractData
|
RefreshHours24UtcFromBackend = RefreshHours,
|
||||||
{
|
};
|
||||||
ContractId = "NEW-Medium-ICA-Uplink-1",
|
|
||||||
},
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Hard-ICA-Uplink-1",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new FYFactionContractsData
|
|
||||||
{
|
|
||||||
FactionId = "Korolev",
|
|
||||||
Contracts = new List<FYFactionContractData>
|
|
||||||
{
|
|
||||||
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<FYFactionContractData>
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RefreshHours24UtcFromBackend = 12
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-2
@@ -54,7 +54,7 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTr
|
|||||||
|
|
||||||
var userData = await _userDataService.FindAsync(
|
var userData = await _userDataService.FindAsync(
|
||||||
userId, userId,
|
userId, userId,
|
||||||
new List<string>{"TechTreeNodeData", "Balance", "Inventory"}
|
new List<string>{"TechTreeNodeData", "Balance", "Inventory", "PlayerQuartersLevel"}
|
||||||
);
|
);
|
||||||
|
|
||||||
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
|
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
|
||||||
@@ -79,7 +79,15 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTr
|
|||||||
var techTreeNodeData = techTreeNodes[request.NodeID];
|
var techTreeNodeData = techTreeNodes[request.NodeID];
|
||||||
var nextTreeNodeLevel = techTreeNodeData.PerkLevels[currentLevel]; // Tech tree node levels start from 0
|
var nextTreeNodeLevel = techTreeNodeData.PerkLevels[currentLevel]; // Tech tree node levels start from 0
|
||||||
|
|
||||||
// TODO: Dependency check
|
// Dependency check: the player's quarters must be high enough to unlock this node.
|
||||||
|
var playerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
|
||||||
|
if (playerQuarters != null && playerQuarters.Level < techTreeNodeData.PlayerQuarterLevelRequired) {
|
||||||
|
return new FYStartTechTreeNodeUpgradeClientResult
|
||||||
|
{
|
||||||
|
UserID = userId,
|
||||||
|
Error = $"Requires player quarters level {techTreeNodeData.PlayerQuarterLevelRequired}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Validate price
|
// Validate price
|
||||||
// TODO: Refactor
|
// TODO: Refactor
|
||||||
|
|||||||
Reference in New Issue
Block a user