diff --git a/Libraries/Core/Helpers/TraderHelper.cs b/Libraries/Core/Helpers/TraderHelper.cs
index f0a9cde0..a275940e 100644
--- a/Libraries/Core/Helpers/TraderHelper.cs
+++ b/Libraries/Core/Helpers/TraderHelper.cs
@@ -76,7 +76,9 @@ public class TraderHelper(
/// TraderAssort
public TraderAssort GetTraderAssortsByTraderId(string traderId)
{
- throw new NotImplementedException();
+ return traderId == Traders.FENCE
+ ? _fenceService.GetRawFenceAssorts()
+ : _databaseService.GetTrader(traderId).Assort;
}
///
@@ -85,9 +87,26 @@ public class TraderHelper(
/// Trader to get assorts for
/// Id of assort to find
/// Item object
- public Item GetTraderAssortItemByAssortId(string traderId, string assortId)
+ public Item? GetTraderAssortItemByAssortId(string traderId, string assortId)
{
- throw new NotImplementedException();
+ var traderAssorts = GetTraderAssortsByTraderId(traderId);
+ if (traderAssorts is null)
+ {
+ _logger.Debug($"No assorts on trader: {traderId} found");
+
+ return null;
+ }
+
+ // Find specific assort in traders data
+ var purchasedAssort = traderAssorts.Items.FirstOrDefault(item => item.Id == assortId);
+ if (purchasedAssort is null)
+ {
+ _logger.Debug($"No assort {assortId} on trader: {traderId} found");
+
+ return null;
+ }
+
+ return purchasedAssort;
}
///
@@ -98,8 +117,73 @@ public class TraderHelper(
/// trader id to reset
public void ResetTrader(string sessionID, string traderID)
{
- // TODO: implement actually
- return;
+ var profiles = _databaseService.GetProfiles();
+ var trader = _databaseService.GetTrader(traderID);
+
+ var fullProfile = _profileHelper.GetFullProfile(sessionID);
+ if (fullProfile is null)
+ {
+ throw new Exception(_localisationService.GetText("trader-unable_to_find_profile_by_id", sessionID));
+ }
+
+ var pmcData = fullProfile.CharacterData.PmcData;
+ ProfileTraderTemplate rawProfileTemplate =
+ profiles[fullProfile.ProfileInfo.Edition][pmcData.Info.Side.ToLower()].Trader;
+
+ pmcData.TradersInfo[traderID] = new TraderInfo
+ {
+ Disabled = false,
+ LoyaltyLevel = rawProfileTemplate.InitialLoyaltyLevel[traderID] ?? 1,
+ SalesSum = rawProfileTemplate.InitialSalesSum,
+ Standing = GetStartingStanding(traderID, rawProfileTemplate),
+ NextResupply = trader.Base.NextResupply,
+ Unlocked = trader.Base.UnlockedByDefault
+ };
+
+ // Check if trader should be locked by default
+ if (rawProfileTemplate.LockedByDefaultOverride?.Contains(traderID) ?? false)
+ {
+ pmcData.TradersInfo[traderID].Unlocked = true;
+ }
+
+ if (rawProfileTemplate.PurchaseAllClothingByDefaultForTrader?.Contains(traderID) ?? false)
+ {
+ // Get traders clothing
+ var clothing = _databaseService.GetTrader(traderID).Suits;
+ if (clothing?.Count > 0)
+ {
+ // Force suit ids into profile
+ AddSuitsToProfile(
+ fullProfile,
+ clothing.Select(suit => suit.SuiteId).ToList()
+ );
+ }
+ }
+
+ if ((rawProfileTemplate.FleaBlockedDays ?? 0) > 0)
+ {
+ var newBanDateTime = _timeUtil.GetTimeStampFromNowDays(rawProfileTemplate.FleaBlockedDays ?? 0);
+ var existingBan = pmcData.Info.Bans.FirstOrDefault(ban => ban.BanType == BanType.RAGFAIR);
+ if (existingBan is not null)
+ {
+ existingBan.DateTime = newBanDateTime;
+ }
+ else
+ {
+ pmcData.Info.Bans.Add(
+ new Ban
+ {
+ BanType = BanType.RAGFAIR,
+ DateTime = newBanDateTime
+ }
+ );
+ }
+ }
+
+ if (traderID == Traders.JAEGER)
+ {
+ pmcData.TradersInfo[traderID].Unlocked = rawProfileTemplate.JaegerUnlocked;
+ }
}
///
@@ -110,7 +194,13 @@ public class TraderHelper(
/// Standing value
protected double GetStartingStanding(string traderId, ProfileTraderTemplate rawProfileTemplate)
{
- throw new NotImplementedException();
+ var initialStanding = rawProfileTemplate.InitialStanding[traderId] ?? 0D;
+ // Edge case for Lightkeeper, 0 standing means seeing `Make Amends - Buyout` quest
+ if (traderId == Traders.LIGHTHOUSEKEEPER && initialStanding == 0) {
+ return 0.01;
+ }
+
+ return initialStanding;
}
///
@@ -120,7 +210,16 @@ public class TraderHelper(
/// Suit Ids to add
protected void AddSuitsToProfile(SptProfile fullProfile, List suitIds)
{
- throw new NotImplementedException();
+ if (fullProfile.Suits is null) {
+ fullProfile.Suits = [];
+ }
+
+ foreach (var suitId in suitIds) {
+ // Don't add dupes
+ if (!fullProfile.Suits.Contains(suitId)) {
+ fullProfile.Suits.Add(suitId);
+ }
+ }
}
///
@@ -131,7 +230,15 @@ public class TraderHelper(
/// Session id of player
public void SetTraderUnlockedState(string traderId, bool status, string sessionId)
{
- throw new NotImplementedException();
+ var pmcData = _profileHelper.GetPmcProfile(sessionId);
+ var profileTraderData = pmcData.TradersInfo[traderId];
+ if (profileTraderData is null) {
+ _logger.Error($"Unable to set trader {traderId} unlocked state to: {status} as trader cannot be found in profile");
+
+ return;
+ }
+
+ profileTraderData.Unlocked = status;
}
///
@@ -142,7 +249,18 @@ public class TraderHelper(
/// Standing value to add to trader
public void AddStandingToTrader(string sessionId, string traderId, double standingToAdd)
{
- throw new NotImplementedException();
+ var fullProfile = _profileHelper.GetFullProfile(sessionId);
+ var pmcTraderInfo = fullProfile.CharacterData.PmcData.TradersInfo[traderId];
+
+ // Add standing to trader
+ pmcTraderInfo.Standing = AddStandingValuesTogether(pmcTraderInfo.Standing, standingToAdd);
+
+ if (traderId == Traders.FENCE) {
+ // Must add rep to scav profile to ensure consistency
+ fullProfile.CharacterData.ScavData.TradersInfo[traderId].Standing = pmcTraderInfo.Standing;
+ }
+
+ this.LevelUp(traderId, fullProfile.CharacterData.PmcData);
}
///
@@ -151,9 +269,12 @@ public class TraderHelper(
/// current trader standing
/// standing to add to trader standing
/// current standing + added standing (clamped if needed)
- protected double AddStandingValuesTogether(double currentStanding, double standingToAdd)
+ protected double? AddStandingValuesTogether(double? currentStanding, double standingToAdd)
{
- throw new NotImplementedException();
+ var newStanding = currentStanding + standingToAdd;
+
+ // Never let standing fall below 0
+ return newStanding < 0 ? 0 : newStanding;
}
///
@@ -162,7 +283,11 @@ public class TraderHelper(
/// Profile to check.
public void ValidateTraderStandingsAndPlayerLevelForProfile(string sessionId)
{
- throw new NotImplementedException();
+ var profile = _profileHelper.GetPmcProfile(sessionId);
+ var traders = _databaseService.GetTraders();
+ foreach (var trader in traders) {
+ this.LevelUp(trader.Key, profile);
+ }
}
///
@@ -173,8 +298,30 @@ public class TraderHelper(
/// Profile to update trader in.
public void LevelUp(string traderID, PmcData pmcData)
{
- // TODO: implement actually
- return;
+ var loyaltyLevels = _databaseService.GetTrader(traderID).Base.LoyaltyLevels;
+
+ // Level up player
+ pmcData.Info.Level = _playerService.CalculateLevel(pmcData);
+
+ // Level up traders
+ var targetLevel = 0;
+
+ // Round standing to 2 decimal places to address floating point inaccuracies
+ pmcData.TradersInfo[traderID].Standing = Math.Round((pmcData.TradersInfo[traderID].Standing * 100) ?? 0) / 100;
+
+ foreach (var loyaltyLevel in loyaltyLevels) {
+ if (loyaltyLevel.MinLevel <= pmcData.Info.Level &&
+ loyaltyLevel.MinSalesSum <= pmcData.TradersInfo[traderID].SalesSum &&
+ loyaltyLevel.MinStanding <= pmcData.TradersInfo[traderID].Standing &&
+ targetLevel < 4
+ ) {
+ // level reached
+ targetLevel++;
+ }
+ }
+
+ // set level
+ pmcData.TradersInfo[traderID].LoyaltyLevel = targetLevel;
}
///
@@ -429,7 +576,8 @@ public class TraderHelper(
{
var kvp = Traders.TradersDictionary.Where(x => x.Value == traderId);
- if (!kvp.Any()) {
+ if (!kvp.Any())
+ {
_logger.Error(_localisationService.GetText("trader-unable_to_find_trader_in_enum", traderId));
return null;
diff --git a/Libraries/Core/Models/Eft/Common/Tables/ProfileTemplate.cs b/Libraries/Core/Models/Eft/Common/Tables/ProfileTemplate.cs
index c464569b..113e0997 100644
--- a/Libraries/Core/Models/Eft/Common/Tables/ProfileTemplate.cs
+++ b/Libraries/Core/Models/Eft/Common/Tables/ProfileTemplate.cs
@@ -89,10 +89,10 @@ public record TemplateSide
public record ProfileTraderTemplate
{
[JsonPropertyName("initialLoyaltyLevel")]
- public Dictionary? InitialLoyaltyLevel { get; set; }
+ public Dictionary? InitialLoyaltyLevel { get; set; }
[JsonPropertyName("initialStanding")]
- public Dictionary? InitialStanding { get; set; }
+ public Dictionary? InitialStanding { get; set; }
[JsonPropertyName("setQuestsAvailableForStart")]
public bool? SetQuestsAvailableForStart { get; set; }
diff --git a/Libraries/Core/Services/PlayerService.cs b/Libraries/Core/Services/PlayerService.cs
index ee742687..0b521c71 100644
--- a/Libraries/Core/Services/PlayerService.cs
+++ b/Libraries/Core/Services/PlayerService.cs
@@ -1,13 +1,31 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common;
+using Core.Models.Utils;
+using Core.Utils;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
-public class PlayerService
+public class PlayerService(
+ DatabaseService _databaseService
+)
{
- public int CalculateLevel(PmcData pmcData)
+ public int? CalculateLevel(PmcData pmcData)
{
- throw new NotImplementedException();
+ var accExp = 0;
+
+ for (int i = 0; i < _databaseService.GetGlobals().Configuration.Exp.Level.ExperienceTable.Length; i++)
+ {
+ accExp += _databaseService.GetGlobals().Configuration.Exp.Level.ExperienceTable[i].Experience ?? 0;
+
+ if (pmcData.Info.Experience < accExp)
+ {
+ break;
+ }
+
+ pmcData.Info.Level = i + 1;
+ }
+
+ return pmcData.Info.Level;
}
}