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; } }