Merge branch 'main' of https://github.com/sp-tarkov/server-csharp
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
using SptCommon.Annotations;
|
||||
using Core.Generators;
|
||||
using Core.Helpers;
|
||||
using Core.Models.Eft.Common;
|
||||
using Core.Models.Eft.Common.Tables;
|
||||
using Core.Models.Eft.ItemEvent;
|
||||
using Core.Models.Eft.Quests;
|
||||
using Core.Models.Enums;
|
||||
using Core.Models.Spt.Config;
|
||||
using Core.Models.Spt.Repeatable;
|
||||
using Core.Generators;
|
||||
using Core.Helpers;
|
||||
using Core.Models.Enums;
|
||||
using Core.Models.Utils;
|
||||
using Core.Routers;
|
||||
using Core.Servers;
|
||||
using Core.Services;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using SptCommon.Annotations;
|
||||
|
||||
namespace Core.Controllers;
|
||||
|
||||
@@ -39,7 +39,8 @@ public class RepeatableQuestController(
|
||||
{
|
||||
protected QuestConfig _questConfig = _configServer.GetConfig<QuestConfig>();
|
||||
|
||||
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest info, string sessionId)
|
||||
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest info,
|
||||
string sessionId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -108,7 +109,9 @@ public class RepeatableQuestController(
|
||||
lifeline++;
|
||||
if (lifeline > 10)
|
||||
{
|
||||
_logger.Debug("We were stuck in repeatable quest generation. This should never happen. Please report");
|
||||
_logger.Debug(
|
||||
"We were stuck in repeatable quest generation. This should never happen. Please report"
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -133,13 +136,11 @@ public class RepeatableQuestController(
|
||||
|
||||
// Create stupid redundant change requirements from quest data
|
||||
foreach (var quest in generatedRepeatables.ActiveQuests)
|
||||
{
|
||||
generatedRepeatables.ChangeRequirement[quest.Id] = new ChangeRequirement
|
||||
{
|
||||
ChangeCost = quest.ChangeCost,
|
||||
ChangeStandingCost = _randomUtil.GetArrayValue([0, 0.01]), // Randomise standing cost to replace
|
||||
ChangeStandingCost = _randomUtil.GetArrayValue([0, 0.01]) // Randomise standing cost to replace
|
||||
};
|
||||
}
|
||||
|
||||
// Reset free repeatable values in player profile to defaults
|
||||
generatedRepeatables.FreeChanges = repeatableConfig.FreeChanges;
|
||||
@@ -155,7 +156,7 @@ public class RepeatableQuestController(
|
||||
InactiveQuests = generatedRepeatables.InactiveQuests,
|
||||
ChangeRequirement = generatedRepeatables.ChangeRequirement,
|
||||
FreeChanges = generatedRepeatables.FreeChanges,
|
||||
FreeChangesAvailable = generatedRepeatables.FreeChanges,
|
||||
FreeChangesAvailable = generatedRepeatables.FreeChanges
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -163,26 +164,26 @@ public class RepeatableQuestController(
|
||||
return returnData;
|
||||
}
|
||||
|
||||
private PmcDataRepeatableQuest GetRepeatableQuestSubTypeFromProfile(RepeatableQuestConfig repeatableConfig, PmcData pmcData)
|
||||
private PmcDataRepeatableQuest GetRepeatableQuestSubTypeFromProfile(RepeatableQuestConfig repeatableConfig,
|
||||
PmcData pmcData)
|
||||
{
|
||||
// Get from profile, add if missing
|
||||
var repeatableQuestDetails = pmcData.RepeatableQuests.FirstOrDefault(
|
||||
(repeatable) => repeatable.Name == repeatableConfig.Name
|
||||
repeatable => repeatable.Name == repeatableConfig.Name
|
||||
);
|
||||
if (repeatableQuestDetails is null)
|
||||
{
|
||||
// Not in profile, generate
|
||||
var hasAccess = _profileHelper.HasAccessToRepeatableFreeRefreshSystem(pmcData);
|
||||
repeatableQuestDetails = new PmcDataRepeatableQuest()
|
||||
repeatableQuestDetails = new PmcDataRepeatableQuest
|
||||
{
|
||||
Id = repeatableConfig.Id,
|
||||
Name = repeatableConfig.Name,
|
||||
ActiveQuests = [],
|
||||
InactiveQuests = [],
|
||||
EndTime = 0,
|
||||
ChangeRequirement = { },
|
||||
FreeChanges = hasAccess ? repeatableConfig.FreeChanges : 0,
|
||||
FreeChangesAvailable = hasAccess ? repeatableConfig.FreeChangesAvailable : 0,
|
||||
FreeChangesAvailable = hasAccess ? repeatableConfig.FreeChangesAvailable : 0
|
||||
};
|
||||
|
||||
// Add base object that holds repeatable data to profile
|
||||
@@ -229,9 +230,9 @@ public class RepeatableQuestController(
|
||||
*/
|
||||
private bool PlayerHasDailyScavQuestsUnlocked(PmcData pmcData)
|
||||
{
|
||||
return (
|
||||
pmcData?.Hideout?.Areas?.FirstOrDefault((hideoutArea) => hideoutArea.Type == HideoutAreas.INTEL_CENTER)?.Level >= 1
|
||||
);
|
||||
return pmcData?.Hideout?.Areas?.FirstOrDefault(hideoutArea => hideoutArea.Type == HideoutAreas.INTEL_CENTER)
|
||||
?.Level >=
|
||||
1;
|
||||
}
|
||||
|
||||
private void ProcessExpiredQuests(PmcDataRepeatableQuest generatedRepeatables, PmcData pmcData)
|
||||
@@ -239,7 +240,7 @@ public class RepeatableQuestController(
|
||||
var questsToKeep = new List<RepeatableQuest>();
|
||||
foreach (var activeQuest in generatedRepeatables.ActiveQuests)
|
||||
{
|
||||
var questStatusInProfile = pmcData.Quests.FirstOrDefault((quest) => quest.QId == activeQuest.Id);
|
||||
var questStatusInProfile = pmcData.Quests.FirstOrDefault(quest => quest.QId == activeQuest.Id);
|
||||
if (questStatusInProfile is null)
|
||||
{
|
||||
continue;
|
||||
@@ -249,7 +250,9 @@ public class RepeatableQuestController(
|
||||
if (questStatusInProfile.Status == QuestStatusEnum.AvailableForFinish)
|
||||
{
|
||||
questsToKeep.Add(activeQuest);
|
||||
_logger.Debug($"Keeping repeatable quest: {activeQuest.Id} in activeQuests since it is available to hand in");
|
||||
_logger.Debug(
|
||||
$"Keeping repeatable quest: {activeQuest.Id} in activeQuests since it is available to hand in"
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -258,7 +261,7 @@ public class RepeatableQuestController(
|
||||
_profileFixerService.RemoveDanglingConditionCounters(pmcData);
|
||||
|
||||
// Remove expired quest from pmc.quest array
|
||||
pmcData.Quests = pmcData.Quests.Where((quest) => quest.QId != activeQuest.Id).ToList();
|
||||
pmcData.Quests = pmcData.Quests.Where(quest => quest.QId != activeQuest.Id).ToList();
|
||||
|
||||
// Store in inactive array
|
||||
generatedRepeatables.InactiveQuests.Add(activeQuest);
|
||||
@@ -276,27 +279,25 @@ public class RepeatableQuestController(
|
||||
|
||||
// Populate Exploration and Pickup quest locations
|
||||
foreach (var (location, value) in locations)
|
||||
{
|
||||
if (location != ELocationName.any)
|
||||
{
|
||||
questPool.Pool.Exploration.Locations[location] = value;
|
||||
questPool.Pool.Pickup.Locations[location] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Add "any" to pickup quest pool
|
||||
questPool.Pool.Pickup.Locations[ELocationName.any] = ["any"];
|
||||
|
||||
var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel.Value, repeatableConfig);
|
||||
var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
|
||||
var targetsConfig =
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
|
||||
|
||||
// Populate Elimination quest targets and their locations
|
||||
foreach (var targetKvP in targetsConfig)
|
||||
{
|
||||
// Target is boss
|
||||
if (targetKvP.Data.IsBoss.GetValueOrDefault(false))
|
||||
{
|
||||
questPool.Pool.Elimination.Targets[targetKvP.Key] = new TargetLocation{ Locations = ["any"] };
|
||||
questPool.Pool.Elimination.Targets[targetKvP.Key] = new TargetLocation { Locations = ["any"] };
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -305,12 +306,14 @@ public class RepeatableQuestController(
|
||||
|
||||
var allowedLocations =
|
||||
targetKvP.Key == "Savage"
|
||||
? possibleLocations.Where((location) => location != ELocationName.laboratory) // Exclude labs for Savage targets.
|
||||
: possibleLocations;
|
||||
? possibleLocations.Where(
|
||||
location => location != ELocationName.laboratory
|
||||
) // Exclude labs for Savage targets.
|
||||
: possibleLocations;
|
||||
|
||||
questPool.Pool.Elimination.Targets[targetKvP.Key] = new TargetLocation{ Locations = allowedLocations.Select(x => x.ToString()).ToList() };
|
||||
questPool.Pool.Elimination.Targets[targetKvP.Key] = new TargetLocation
|
||||
{ Locations = allowedLocations.Select(x => x.ToString()).ToList() };
|
||||
}
|
||||
}
|
||||
|
||||
return questPool;
|
||||
}
|
||||
@@ -334,11 +337,12 @@ public class RepeatableQuestController(
|
||||
{
|
||||
Locations = new Dictionary<ELocationName, List<string>>()
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<ELocationName, List<string>> GetAllowedLocationsForPmcLevel(Dictionary<ELocationName, List<string>> locations, int pmcLevel)
|
||||
private Dictionary<ELocationName, List<string>> GetAllowedLocationsForPmcLevel(
|
||||
Dictionary<ELocationName, List<string>> locations, int pmcLevel)
|
||||
{
|
||||
var allowedLocation = new Dictionary<ELocationName, List<string>>();
|
||||
|
||||
@@ -346,12 +350,10 @@ public class RepeatableQuestController(
|
||||
{
|
||||
var locationNames = new List<string>();
|
||||
foreach (var locationName in value)
|
||||
{
|
||||
if (IsPmcLevelAllowedOnLocation(locationName, pmcLevel))
|
||||
{
|
||||
locationNames.Add(locationName);
|
||||
}
|
||||
}
|
||||
|
||||
if (locationNames.Count > 0)
|
||||
{
|
||||
@@ -363,11 +365,11 @@ public class RepeatableQuestController(
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given pmcLevel is allowed on the given location
|
||||
* @param location The location name to check
|
||||
* @param pmcLevel The level of the pmc
|
||||
* @returns True if the given pmc level is allowed to access the given location
|
||||
*/
|
||||
* Return true if the given pmcLevel is allowed on the given location
|
||||
* @param location The location name to check
|
||||
* @param pmcLevel The level of the pmc
|
||||
* @returns True if the given pmc level is allowed to access the given location
|
||||
*/
|
||||
protected bool IsPmcLevelAllowedOnLocation(string location, int pmcLevel)
|
||||
{
|
||||
// All PMC levels are allowed for 'any' location requirement
|
||||
@@ -386,7 +388,7 @@ public class RepeatableQuestController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get count of repeatable quests profile should have access to
|
||||
/// Get count of repeatable quests profile should have access to
|
||||
/// </summary>
|
||||
/// <param name="repeatableConfig"></param>
|
||||
/// <param name="pmcData">Player profile</param>
|
||||
@@ -400,7 +402,8 @@ public class RepeatableQuestController(
|
||||
}
|
||||
|
||||
// Add elite bonus to daily quests
|
||||
if (repeatableConfig.Name.ToLower() == "daily" && _profileHelper.HasEliteSkillLevel(SkillTypes.Charisma, pmcData)
|
||||
if (repeatableConfig.Name.ToLower() == "daily" &&
|
||||
_profileHelper.HasEliteSkillLevel(SkillTypes.Charisma, pmcData)
|
||||
)
|
||||
{
|
||||
// Elite charisma skill gives extra daily quest(s)
|
||||
|
||||
@@ -6,11 +6,11 @@ using Core.Models.Spt.Bots;
|
||||
using Core.Models.Spt.Config;
|
||||
using Core.Models.Utils;
|
||||
using Core.Helpers;
|
||||
using Core.Models.Spt.Server;
|
||||
using Core.Servers;
|
||||
using Core.Services;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using Core.Utils.Collections;
|
||||
|
||||
namespace Core.Generators;
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
using SptCommon.Annotations;
|
||||
using Core.Helpers;
|
||||
using Core.Models.Eft.Common;
|
||||
using Core.Models.Eft.Common.Tables;
|
||||
using Core.Models.Enums;
|
||||
using Core.Models.Spt.Config;
|
||||
using Core.Models.Spt.Repeatable;
|
||||
using Core.Models.Utils;
|
||||
using Core.Utils;
|
||||
using Core.Helpers;
|
||||
using Core.Servers;
|
||||
using Core.Services;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using Core.Utils.Collections;
|
||||
using Core.Utils.Json;
|
||||
using SptCommon.Annotations;
|
||||
using SptCommon.Extensions;
|
||||
using BodyPart = Core.Models.Spt.Config.BodyPart;
|
||||
using Core.Utils.Cloners;
|
||||
|
||||
namespace Core.Generators;
|
||||
|
||||
@@ -35,8 +36,10 @@ public class RepeatableQuestGenerator(
|
||||
protected int _maxRandomNumberAttempts = 6;
|
||||
|
||||
/// <summary>
|
||||
/// This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see assets/database/templates/repeatableQuests.json).
|
||||
/// It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is providing the quest
|
||||
/// This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see
|
||||
/// assets/database/templates/repeatableQuests.json).
|
||||
/// It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is
|
||||
/// providing the quest
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session id</param>
|
||||
/// <param name="pmcLevel">Player's level for requested items and reward generation</param>
|
||||
@@ -74,13 +77,16 @@ public class RepeatableQuestGenerator(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a randomised Elimination quest
|
||||
/// Generate a randomised Elimination quest
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session id</param>
|
||||
/// <param name="pmcLevel">Player's level for requested items and reward generation</param>
|
||||
/// <param name="traderId">Trader from which the quest will be provided</param>
|
||||
/// <param name="questTypePool">Pools for quests (used to avoid redundant quests)</param>
|
||||
/// <param name="repeatableConfig">The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig for the requestd quest</param>
|
||||
/// <param name="repeatableConfig">
|
||||
/// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig
|
||||
/// for the requestd quest
|
||||
/// </param>
|
||||
/// <returns>Object of quest type format for "Elimination" (see assets/database/templates/repeatableQuests.json)</returns>
|
||||
protected RepeatableQuest GenerateEliminationQuest(
|
||||
string sessionId,
|
||||
@@ -94,12 +100,18 @@ public class RepeatableQuestGenerator(
|
||||
|
||||
var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel, repeatableConfig);
|
||||
var locationsConfig = repeatableConfig.Locations;
|
||||
var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
|
||||
var bodyPartsConfig = _repeatableQuestHelper.ProbabilityObjectArray<BodyPart, string, List<string>>(eliminationConfig.BodyParts);
|
||||
var targetsConfig =
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
|
||||
var bodyPartsConfig =
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<BodyPart, string, List<string>>(eliminationConfig.BodyParts);
|
||||
var weaponCategoryRequirementConfig =
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<WeaponRequirement, string, List<string>>(eliminationConfig.WeaponCategoryRequirements);
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<WeaponRequirement, string, List<string>>(
|
||||
eliminationConfig.WeaponCategoryRequirements
|
||||
);
|
||||
var weaponRequirementConfig =
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<WeaponRequirement, string, List<string>>(eliminationConfig.WeaponRequirements);
|
||||
_repeatableQuestHelper.ProbabilityObjectArray<WeaponRequirement, string, List<string>>(
|
||||
eliminationConfig.WeaponRequirements
|
||||
);
|
||||
|
||||
// the difficulty of the quest varies in difficulty depending on the condition
|
||||
// possible conditions are
|
||||
@@ -122,7 +134,8 @@ public class RepeatableQuestGenerator(
|
||||
// times the number of kills we have to perform):
|
||||
|
||||
// the minimum difficulty is the difficulty for the most probable (= easiest target) with no additional conditions
|
||||
var minDifficulty = 1 / targetsConfig.MaxProbability(); // min difficulty is lowest amount of scavs without any constraints
|
||||
var minDifficulty =
|
||||
1 / targetsConfig.MaxProbability(); // min difficulty is lowest amount of scavs without any constraints
|
||||
|
||||
// Target on bodyPart max. difficulty is that of the least probable element
|
||||
var maxTargetDifficulty = 1 / targetsConfig.MinProbability();
|
||||
@@ -134,14 +147,14 @@ public class RepeatableQuestGenerator(
|
||||
var maxKillDifficulty = eliminationConfig.MaxKills;
|
||||
|
||||
var targetPool = questTypePool.Pool.Elimination;
|
||||
targetsConfig = targetsConfig.Filter((x) => questTypePool.Pool.Elimination.Targets.ContainsKey(x.Key));
|
||||
targetsConfig = targetsConfig.Filter(x => questTypePool.Pool.Elimination.Targets.ContainsKey(x.Key));
|
||||
|
||||
if (targetsConfig.Count == 0 || targetsConfig.All((x) => x.Data.IsBoss.GetValueOrDefault(false)))
|
||||
if (targetsConfig.Count == 0 || targetsConfig.All(x => x.Data.IsBoss.GetValueOrDefault(false)))
|
||||
{
|
||||
// There are no more targets left for elimination; delete it as a possible quest type
|
||||
// also if only bosses are left we need to leave otherwise it's a guaranteed boss elimination
|
||||
// -> then it would not be a quest with low probability anymore
|
||||
questTypePool.Types = questTypePool.Types.Where((t) => t != "Elimination").ToList();
|
||||
questTypePool.Types = questTypePool.Types.Where(t => t != "Elimination").ToList();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -154,7 +167,8 @@ public class RepeatableQuestGenerator(
|
||||
// we use any as location if "any" is in the pool and we do not hit the specific location random
|
||||
// we use any also if the random condition is not met in case only "any" was in the pool
|
||||
var locationKey = "any";
|
||||
if (locations.Contains("any") && (eliminationConfig.SpecificLocationProbability < rand.Next() || locations.Count <= 1)
|
||||
if (locations.Contains("any") &&
|
||||
(eliminationConfig.SpecificLocationProbability < rand.Next() || locations.Count <= 1)
|
||||
)
|
||||
{
|
||||
locationKey = "any";
|
||||
@@ -166,11 +180,13 @@ public class RepeatableQuestGenerator(
|
||||
if (locations.Count > 0)
|
||||
{
|
||||
locationKey = _randomUtil.DrawRandomFromList(locations).FirstOrDefault();
|
||||
questTypePool.Pool.Elimination.Targets.GetByJsonProp<TargetLocation>(targetKey).Locations = locations.Where(
|
||||
(l) => l != locationKey
|
||||
questTypePool.Pool.Elimination.Targets.GetByJsonProp<TargetLocation>(targetKey).Locations = locations
|
||||
.Where(
|
||||
l => l != locationKey
|
||||
)
|
||||
.ToList();
|
||||
if (questTypePool.Pool.Elimination.Targets.GetByJsonProp<TargetLocation>(targetKey).Locations.Count == 0)
|
||||
if (questTypePool.Pool.Elimination.Targets.GetByJsonProp<TargetLocation>(targetKey).Locations.Count ==
|
||||
0)
|
||||
{
|
||||
questTypePool.Pool.Elimination.Targets.Remove(targetKey);
|
||||
}
|
||||
@@ -197,10 +213,7 @@ public class RepeatableQuestGenerator(
|
||||
{
|
||||
// more than one part lead to an "OR" condition hence more parts reduce the difficulty
|
||||
probability += bodyPartsConfig.Probability(bi).Value;
|
||||
foreach (var biClient in bodyPartsConfig.Data(bi))
|
||||
{
|
||||
bodyPartsToClient.Add(biClient);
|
||||
}
|
||||
foreach (var biClient in bodyPartsConfig.Data(bi)) bodyPartsToClient.Add(biClient);
|
||||
}
|
||||
|
||||
bodyPartDifficulty = 1 / probability;
|
||||
@@ -218,7 +231,7 @@ public class RepeatableQuestGenerator(
|
||||
.GetDictionary()
|
||||
.Select(x => x.Value)
|
||||
.Where(x => x.Base?.Id != null)
|
||||
.Select(x => (new { x.Base.Id, BossSpawn = x.Base.BossLocationSpawn }));
|
||||
.Select(x => new { x.Base.Id, BossSpawn = x.Base.BossLocationSpawn });
|
||||
// filter for the current boss to spawn on map
|
||||
var thisBossSpawns = bossSpawns
|
||||
.Select(
|
||||
@@ -229,9 +242,9 @@ public class RepeatableQuestGenerator(
|
||||
.Where(e => e.BossName == targetKey)
|
||||
}
|
||||
)
|
||||
.Where((x) => x.BossSpawn.Count() > 0);
|
||||
.Where(x => x.BossSpawn.Count() > 0);
|
||||
// remove blacklisted locations
|
||||
var allowedSpawns = thisBossSpawns.Where((x) => !eliminationConfig.DistLocationBlacklist.Contains(x.Id));
|
||||
var allowedSpawns = thisBossSpawns.Where(x => !eliminationConfig.DistLocationBlacklist.Contains(x.Id));
|
||||
// if the boss spawns on nom-blacklisted locations and the current location is allowed we can generate a distance kill requirement
|
||||
isDistanceRequirementAllowed = isDistanceRequirementAllowed && allowedSpawns.Count() > 0;
|
||||
}
|
||||
@@ -240,7 +253,8 @@ public class RepeatableQuestGenerator(
|
||||
{
|
||||
// Random distance with lower values more likely; simple distribution for starters...
|
||||
distance = (int)Math.Floor(
|
||||
(decimal)(Math.Abs(rand.Next(0, 1) - rand.Next(0, 1)) * (1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) +
|
||||
(decimal)(Math.Abs(rand.Next(0, 1) - rand.Next(0, 1)) *
|
||||
(1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) +
|
||||
eliminationConfig.MinDistance)
|
||||
);
|
||||
distance = (int)Math.Ceiling((decimal)(distance / 5)) * 5;
|
||||
@@ -254,21 +268,23 @@ public class RepeatableQuestGenerator(
|
||||
if (distance > 50)
|
||||
{
|
||||
List<string> weaponTypes = ["Shotgun", "Pistol"];
|
||||
weaponCategoryRequirementConfig = (ProbabilityObjectArray<WeaponRequirement, string, List<string>>)weaponCategoryRequirementConfig
|
||||
.Where(
|
||||
(category) => weaponTypes
|
||||
.Contains(category.Key)
|
||||
);
|
||||
weaponCategoryRequirementConfig =
|
||||
(ProbabilityObjectArray<WeaponRequirement, string, List<string>>)weaponCategoryRequirementConfig
|
||||
.Where(
|
||||
category => weaponTypes
|
||||
.Contains(category.Key)
|
||||
);
|
||||
}
|
||||
else if (distance < 20)
|
||||
{
|
||||
List<string> weaponTypes = ["MarksmanRifle", "DMR"];
|
||||
// Filter out far range weapons from close distance requirement
|
||||
weaponCategoryRequirementConfig = (ProbabilityObjectArray<WeaponRequirement, string, List<string>>)weaponCategoryRequirementConfig
|
||||
.Where(
|
||||
(category) => weaponTypes
|
||||
.Contains(category.Key)
|
||||
);
|
||||
weaponCategoryRequirementConfig =
|
||||
(ProbabilityObjectArray<WeaponRequirement, string, List<string>>)weaponCategoryRequirementConfig
|
||||
.Where(
|
||||
category => weaponTypes
|
||||
.Contains(category.Key)
|
||||
);
|
||||
}
|
||||
|
||||
// Pick a weighted weapon category
|
||||
@@ -324,7 +340,9 @@ public class RepeatableQuestGenerator(
|
||||
if (locationKey != "any")
|
||||
{
|
||||
Enum.TryParse(typeof(ELocationName), locationKey, true, out var locationId);
|
||||
availableForFinishCondition.Counter.Conditions.Add(GenerateEliminationLocation(locationsConfig[(ELocationName)locationId]));
|
||||
availableForFinishCondition.Counter.Conditions.Add(
|
||||
GenerateEliminationLocation(locationsConfig[(ELocationName)locationId])
|
||||
);
|
||||
}
|
||||
|
||||
availableForFinishCondition.Counter.Conditions.Add(
|
||||
@@ -387,8 +405,9 @@ public class RepeatableQuestGenerator(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A repeatable quest, besides some more or less static components, exists of reward and condition (see assets/database/templates/repeatableQuests.json)
|
||||
/// This is a helper method for GenerateEliminationQuest to create a location condition.
|
||||
/// A repeatable quest, besides some more or less static components, exists of reward and condition (see
|
||||
/// assets/database/templates/repeatableQuests.json)
|
||||
/// This is a helper method for GenerateEliminationQuest to create a location condition.
|
||||
/// </summary>
|
||||
/// <param name="location">the location on which to fulfill the elimination quest</param>
|
||||
/// <returns>Elimination-location-subcondition object</returns>
|
||||
@@ -404,7 +423,7 @@ public class RepeatableQuestGenerator(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create kill condition for an elimination quest
|
||||
/// Create kill condition for an elimination quest
|
||||
/// </summary>
|
||||
/// <param name="target">Bot type target of elimination quest e.g. "AnyPmc", "Savage"</param>
|
||||
/// <param name="targetedBodyParts">Body parts player must hit</param>
|
||||
@@ -428,7 +447,7 @@ public class RepeatableQuestGenerator(
|
||||
Value = 1,
|
||||
ResetOnSessionEnd = false,
|
||||
EnemyHealthEffects = [],
|
||||
Daytime = new DaytimeCounter() { From = 0, To = 0 },
|
||||
Daytime = new DaytimeCounter { From = 0, To = 0 },
|
||||
ConditionType = "Kills"
|
||||
};
|
||||
|
||||
@@ -467,11 +486,14 @@ public class RepeatableQuestGenerator(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a valid Completion quest
|
||||
/// Generates a valid Completion quest
|
||||
/// </summary>
|
||||
/// <param name="pmcLevel">player's level for requested items and reward generation</param>
|
||||
/// <param name="traderId">trader from which the quest will be provided</param>
|
||||
/// <param name="repeatableConfig">The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig for the requested quest</param>
|
||||
/// <param name="repeatableConfig">
|
||||
/// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig
|
||||
/// for the requested quest
|
||||
/// </param>
|
||||
/// <returns>quest type format for "Completion" (see assets/database/templates/repeatableQuests.json)</returns>
|
||||
protected RepeatableQuest? GenerateCompletionQuest(
|
||||
string sessionId,
|
||||
@@ -489,55 +511,72 @@ public class RepeatableQuestGenerator(
|
||||
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant"
|
||||
var possibleItemsToRetrievePool = _repeatableQuestRewardGenerator.GetRewardableItems(
|
||||
repeatableConfig,
|
||||
traderId);
|
||||
traderId
|
||||
);
|
||||
|
||||
// Be fair, don't var the items be more expensive than the reward
|
||||
var multi = _randomUtil.GetFloat((float)0.5, 1);
|
||||
var roublesBudget = Math.Floor(
|
||||
(double)(_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi));
|
||||
(double)(_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi)
|
||||
);
|
||||
roublesBudget = Math.Max(roublesBudget, 5000d);
|
||||
var itemSelection = possibleItemsToRetrievePool.Where(
|
||||
(x) => _itemHelper.GetItemPrice(x.Id) < roublesBudget).ToList();
|
||||
x => _itemHelper.GetItemPrice(x.Id) < roublesBudget
|
||||
)
|
||||
.ToList();
|
||||
|
||||
// We also have the option to use whitelist and/or blacklist which is defined in repeatableQuests.json as
|
||||
// [{"minPlayerLevel": 1, "itemIds": ["id1",...]}, {"minPlayerLevel": 15, "itemIds": ["id3",...]}]
|
||||
if (repeatableConfig.QuestConfig.Completion.UseWhitelist.GetValueOrDefault(false)) {
|
||||
if (repeatableConfig.QuestConfig.Completion.UseWhitelist.GetValueOrDefault(false))
|
||||
{
|
||||
var itemWhitelist = _databaseService.GetTemplates().RepeatableQuests.Data.Completion.ItemsWhitelist;
|
||||
|
||||
// Filter and concatenate the arrays according to current player level
|
||||
var itemIdsWhitelisted = itemWhitelist
|
||||
.Where((p) => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds).ToList(); //.Aggregate((a, p) => a.Concat(p.ItemIds), []);
|
||||
itemSelection = itemSelection.Where((x) => {
|
||||
// Whitelist can contain item tpls and item base type ids
|
||||
return (
|
||||
itemIdsWhitelisted.Any((v) => _itemHelper.IsOfBaseclass(x.Id, v)) ||
|
||||
itemIdsWhitelisted.Contains(x.Id)
|
||||
);
|
||||
}).ToList();
|
||||
.Where(p => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds)
|
||||
.ToList(); //.Aggregate((a, p) => a.Concat(p.ItemIds), []);
|
||||
itemSelection = itemSelection.Where(
|
||||
x =>
|
||||
{
|
||||
// Whitelist can contain item tpls and item base type ids
|
||||
return itemIdsWhitelisted.Any(v => _itemHelper.IsOfBaseclass(x.Id, v)) ||
|
||||
itemIdsWhitelisted.Contains(x.Id);
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
// check if items are missing
|
||||
// var flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []);
|
||||
// var missing = itemIdsWhitelisted.filter(l => !flatList.includes(l));
|
||||
}
|
||||
|
||||
if (repeatableConfig.QuestConfig.Completion.UseBlacklist.GetValueOrDefault(false)) {
|
||||
if (repeatableConfig.QuestConfig.Completion.UseBlacklist.GetValueOrDefault(false))
|
||||
{
|
||||
var itemBlacklist = _databaseService.GetTemplates().RepeatableQuests.Data.Completion.ItemsBlacklist;
|
||||
|
||||
// we filter and concatenate the arrays according to current player level
|
||||
var itemIdsBlacklisted = itemBlacklist
|
||||
.Where((p) => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds).ToList(); //.Aggregate(List<ItemsBlacklist> , (a, p) => a.Concat(p.ItemIds) );
|
||||
.Where(p => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds)
|
||||
.ToList(); //.Aggregate(List<ItemsBlacklist> , (a, p) => a.Concat(p.ItemIds) );
|
||||
|
||||
itemSelection = itemSelection.Where((x) => {
|
||||
return (
|
||||
itemIdsBlacklisted.All((v) => !_itemHelper.IsOfBaseclass(x.Id, v)) ||
|
||||
!itemIdsBlacklisted.Contains(x.Id)
|
||||
);
|
||||
}).ToList();
|
||||
itemSelection = itemSelection.Where(
|
||||
x =>
|
||||
{
|
||||
return itemIdsBlacklisted.All(v => !_itemHelper.IsOfBaseclass(x.Id, v)) ||
|
||||
!itemIdsBlacklisted.Contains(x.Id);
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (!itemSelection.Any()) {
|
||||
_logger.Error(_localisationService.GetText("repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive"));
|
||||
if (!itemSelection.Any())
|
||||
{
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive"
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -549,67 +588,89 @@ public class RepeatableQuestGenerator(
|
||||
var distinctItemsToRetrieveCount = _randomUtil.GetInt(1, completionConfig.UniqueItemCount.Value);
|
||||
var chosenRequirementItemsTpls = new List<string>();
|
||||
var usedItemIndexes = new HashSet<int>();
|
||||
for (var i = 0; i < distinctItemsToRetrieveCount; i++) {
|
||||
for (var i = 0; i < distinctItemsToRetrieveCount; i++)
|
||||
{
|
||||
var chosenItemIndex = _randomUtil.RandInt(itemSelection.Count());
|
||||
var found = false;
|
||||
|
||||
for (var j = 0; j < _maxRandomNumberAttempts; j++) {
|
||||
if (usedItemIndexes.Contains(chosenItemIndex)) {
|
||||
for (var j = 0; j < _maxRandomNumberAttempts; j++)
|
||||
if (usedItemIndexes.Contains(chosenItemIndex))
|
||||
{
|
||||
chosenItemIndex = _randomUtil.RandInt(itemSelection.Count());
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
_logger.Error(_localisationService.GetText("repeatable-no_reward_item_found_in_price_range", new {
|
||||
minPrice = 0,
|
||||
roublesBudget = roublesBudget }));
|
||||
if (!found)
|
||||
{
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"repeatable-no_reward_item_found_in_price_range",
|
||||
new
|
||||
{
|
||||
minPrice = 0, roublesBudget
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
usedItemIndexes.Add(chosenItemIndex);
|
||||
|
||||
var itemSelected = itemSelection[chosenItemIndex];
|
||||
var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Id).Value;
|
||||
var minValue = (double)completionConfig.MinimumRequestedAmount.Value;
|
||||
var maxValue = (double)completionConfig.MaximumRequestedAmount.Value;
|
||||
if (_itemHelper.IsOfBaseclass(itemSelected.Id, BaseClasses.AMMO)) {
|
||||
if (_itemHelper.IsOfBaseclass(itemSelected.Id, BaseClasses.AMMO))
|
||||
{
|
||||
// Prevent multiple ammo requirements from being picked
|
||||
if (isAmmo > 0 && isAmmo < _maxRandomNumberAttempts) {
|
||||
if (isAmmo > 0 && isAmmo < _maxRandomNumberAttempts)
|
||||
{
|
||||
isAmmo++;
|
||||
i--;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
isAmmo++;
|
||||
minValue = (double)completionConfig.MinimumRequestedBulletAmount.Value;
|
||||
maxValue = (double)completionConfig.MaximumRequestedBulletAmount.Value;
|
||||
minValue = completionConfig.MinimumRequestedBulletAmount.Value;
|
||||
maxValue = completionConfig.MaximumRequestedBulletAmount.Value;
|
||||
}
|
||||
|
||||
var value = minValue;
|
||||
|
||||
// Get the value range within budget
|
||||
var x = Math.Floor(roublesBudget / itemUnitPrice);
|
||||
maxValue = Math.Min(maxValue, x);
|
||||
if (maxValue > minValue) {
|
||||
if (maxValue > minValue)
|
||||
{
|
||||
// If it doesn't blow the budget we have for the request, draw a random amount of the selected
|
||||
// Item type to be requested
|
||||
value = _randomUtil.RandInt((int)minValue, (int)maxValue + 1);
|
||||
}
|
||||
|
||||
roublesBudget -= value * itemUnitPrice;
|
||||
|
||||
// Push a CompletionCondition with the item and the amount of the item
|
||||
chosenRequirementItemsTpls.Add(itemSelected.Id);
|
||||
quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Id, value));
|
||||
|
||||
if (roublesBudget > 0) {
|
||||
if (roublesBudget > 0)
|
||||
{
|
||||
// Reduce the list possible items to fulfill the new budget constraint
|
||||
itemSelection = itemSelection.Where((dbItem) => _itemHelper.GetItemPrice(dbItem.Id) < roublesBudget).ToList();
|
||||
if (!itemSelection.Any()) {
|
||||
itemSelection = itemSelection.Where(dbItem => _itemHelper.GetItemPrice(dbItem.Id) < roublesBudget)
|
||||
.ToList();
|
||||
if (!itemSelection.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -620,32 +681,69 @@ public class RepeatableQuestGenerator(
|
||||
traderId,
|
||||
repeatableConfig,
|
||||
completionConfig,
|
||||
chosenRequirementItemsTpls);
|
||||
chosenRequirementItemsTpls
|
||||
);
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A repeatable quest, besides some more or less static components, exists of reward and condition (see assets/database/templates/repeatableQuests.json)
|
||||
/// This is a helper method for GenerateCompletionQuest to create a completion condition (of which a completion quest theoretically can have many)
|
||||
/// A repeatable quest, besides some more or less static components, exists of reward and condition (see
|
||||
/// assets/database/templates/repeatableQuests.json)
|
||||
/// This is a helper method for GenerateCompletionQuest to create a completion condition (of which a completion quest
|
||||
/// theoretically can have many)
|
||||
/// </summary>
|
||||
/// <param name="itemTpl">id of the item to request</param>
|
||||
/// <param name="value">amount of items of this specific type to request</param>
|
||||
/// <returns>object of "Completion"-condition</returns>
|
||||
protected QuestCondition GenerateCompletionAvailableForFinish(string itemTpl, double value)
|
||||
{
|
||||
_logger.Error("NOT IMPLEMENTED - GenerateCompletionAvailableForFinish");
|
||||
throw new NotImplementedException();
|
||||
var minDurability = 0;
|
||||
var onlyFoundInRaid = true;
|
||||
if (
|
||||
_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.WEAPON) ||
|
||||
_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.ARMOR)
|
||||
)
|
||||
{
|
||||
minDurability = _randomUtil.GetArrayValue([60, 80]);
|
||||
}
|
||||
|
||||
// By default all collected items must be FiR, except dog tags
|
||||
if (_itemHelper.IsDogtag(itemTpl))
|
||||
{
|
||||
onlyFoundInRaid = false;
|
||||
}
|
||||
|
||||
return new QuestCondition
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
Index = 0,
|
||||
ParentId = "",
|
||||
DynamicLocale = true,
|
||||
VisibilityConditions = [],
|
||||
GlobalQuestCounterId = "",
|
||||
Target = new ListOrT<string>([], itemTpl),
|
||||
Value = value,
|
||||
MinDurability = minDurability,
|
||||
MaxDurability = 100,
|
||||
DogtagLevel = 0,
|
||||
OnlyFoundInRaid = onlyFoundInRaid,
|
||||
IsEncoded = false,
|
||||
ConditionType = "HandoverItem"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a valid Exploration quest
|
||||
/// Generates a valid Exploration quest
|
||||
/// </summary>
|
||||
/// <param name="sessionId">session id for the quest</param>
|
||||
/// <param name="pmcLevel">player's level for reward generation</param>
|
||||
/// <param name="traderId">trader from which the quest will be provided</param>
|
||||
/// <param name="questTypePool">Pools for quests (used to avoid redundant quests)</param>
|
||||
/// <param name="repeatableConfig">The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig for the requested quest</param>
|
||||
/// <param name="repeatableConfig">
|
||||
/// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig
|
||||
/// for the requested quest
|
||||
/// </param>
|
||||
/// <returns>object of quest type format for "Exploration" (see assets/database/templates/repeatableQuests.json)</returns>
|
||||
protected RepeatableQuest? GenerateExplorationQuest(
|
||||
string sessionId,
|
||||
@@ -661,7 +759,7 @@ public class RepeatableQuestGenerator(
|
||||
if (questTypePool.Pool.Exploration.Locations.Count == 0)
|
||||
{
|
||||
// there are no more locations left for exploration; delete it as a possible quest type
|
||||
questTypePool.Types = questTypePool.Types.Where((t) => t != "Exploration").ToList();
|
||||
questTypePool.Types = questTypePool.Types.Where(t => t != "Exploration").ToList();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -681,17 +779,19 @@ public class RepeatableQuestGenerator(
|
||||
|
||||
var quest = GenerateRepeatableTemplate("Exploration", traderId, repeatableConfig.Side, sessionId);
|
||||
|
||||
var exitStatusCondition = new QuestConditionCounterCondition{
|
||||
var exitStatusCondition = new QuestConditionCounterCondition
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
DynamicLocale = true,
|
||||
Status = ["Survived"],
|
||||
ConditionType = "ExitStatus",
|
||||
ConditionType = "ExitStatus"
|
||||
};
|
||||
var locationCondition = new QuestConditionCounterCondition{
|
||||
var locationCondition = new QuestConditionCounterCondition
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
DynamicLocale = true,
|
||||
Target = locationTarget,
|
||||
ConditionType = "Location",
|
||||
ConditionType = "Location"
|
||||
};
|
||||
|
||||
quest.Conditions.AvailableForFinish[0].Counter.Id = _hashUtil.Generate();
|
||||
@@ -706,13 +806,17 @@ public class RepeatableQuestGenerator(
|
||||
var mapExits = GetLocationExitsForSide(locationKey.ToString(), repeatableConfig.Side);
|
||||
|
||||
// Only get exits that have a greater than 0% chance to spawn
|
||||
var exitPool = mapExits.Where((exit) => exit.Chance > 0).ToList();
|
||||
var exitPool = mapExits.Where(exit => exit.Chance > 0).ToList();
|
||||
|
||||
// Exclude exits with a requirement to leave (e.g. car extracts)
|
||||
var possibleExits = exitPool.Where(
|
||||
(exit) =>
|
||||
exit.PassageRequirement is not null ||
|
||||
repeatableConfig.QuestConfig.Exploration.SpecificExits.PassageRequirementWhitelist.Contains("PassageRequirement")).ToList();
|
||||
exit =>
|
||||
exit.PassageRequirement is not null ||
|
||||
repeatableConfig.QuestConfig.Exploration.SpecificExits.PassageRequirementWhitelist.Contains(
|
||||
"PassageRequirement"
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
|
||||
if (possibleExits.Count == 0)
|
||||
{
|
||||
@@ -721,7 +825,7 @@ public class RepeatableQuestGenerator(
|
||||
else
|
||||
{
|
||||
// Choose one of the exits we filtered above
|
||||
var chosenExit = _randomUtil.DrawRandomFromList(possibleExits, 1)[0];
|
||||
var chosenExit = _randomUtil.DrawRandomFromList(possibleExits)[0];
|
||||
|
||||
// Create a quest condition to leave raid via chosen exit
|
||||
var exitCondition = GenerateExplorationExitCondition(chosenExit);
|
||||
@@ -737,13 +841,14 @@ public class RepeatableQuestGenerator(
|
||||
difficulty,
|
||||
traderId,
|
||||
repeatableConfig,
|
||||
explorationConfig);
|
||||
explorationConfig
|
||||
);
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter a maps exits to just those for the desired side
|
||||
/// Filter a maps exits to just those for the desired side
|
||||
/// </summary>
|
||||
/// <param name="locationKey">Map id (e.g. factory4_day)</param>
|
||||
/// <param name="playerSide">Scav/Pmc</param>
|
||||
@@ -752,7 +857,7 @@ public class RepeatableQuestGenerator(
|
||||
{
|
||||
var mapExtracts = _databaseService.GetLocation(locationKey.ToLower()).AllExtracts;
|
||||
|
||||
return mapExtracts.Where((exit) => exit.Side == playerSide).ToList();
|
||||
return mapExtracts.Where(exit => exit.Side == playerSide).ToList();
|
||||
}
|
||||
|
||||
protected RepeatableQuest GeneratePickupQuest(
|
||||
@@ -762,11 +867,48 @@ public class RepeatableQuestGenerator(
|
||||
QuestTypePool questTypePool,
|
||||
RepeatableQuestConfig repeatableConfig)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var pickupConfig = repeatableConfig.QuestConfig.Pickup;
|
||||
|
||||
var quest = GenerateRepeatableTemplate("Pickup", traderId, repeatableConfig.Side, sessionId);
|
||||
|
||||
var itemTypeToFetchWithCount = _randomUtil.GetArrayValue(pickupConfig.ItemTypeToFetchWithMaxCount);
|
||||
var itemCountToFetch = _randomUtil.RandInt(
|
||||
itemTypeToFetchWithCount.MinimumPickupCount.Value,
|
||||
itemTypeToFetchWithCount.MaximumPickupCount + 1
|
||||
);
|
||||
// Choose location - doesnt seem to work for anything other than 'any'
|
||||
// var locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0];
|
||||
// var locationTarget = questTypePool.pool.Pickup.locations[locationKey];
|
||||
|
||||
var findCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => x.ConditionType == "FindItem");
|
||||
findCondition.Target = new ListOrT<string>([], itemTypeToFetchWithCount.ItemType);
|
||||
findCondition.Value = itemCountToFetch;
|
||||
|
||||
var counterCreatorCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(
|
||||
x => x.ConditionType == "CounterCreator"
|
||||
);
|
||||
// var locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location");
|
||||
// (locationCondition._props as ILocationConditionProps).target = [...locationTarget];
|
||||
|
||||
var equipmentCondition = counterCreatorCondition.Counter.Conditions.FirstOrDefault(
|
||||
x => x.ConditionType == "Equipment"
|
||||
);
|
||||
equipmentCondition.EquipmentInclusive = [[itemTypeToFetchWithCount.ItemType]];
|
||||
|
||||
// Add rewards
|
||||
quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward(
|
||||
pmcLevel,
|
||||
1,
|
||||
traderId,
|
||||
repeatableConfig,
|
||||
pickupConfig
|
||||
);
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a location into an quest code can read (e.g. factory4_day into 55f2d3fd4bdc2d5f408b4567)
|
||||
/// Convert a location into an quest code can read (e.g. factory4_day into 55f2d3fd4bdc2d5f408b4567)
|
||||
/// </summary>
|
||||
/// <param name="locationKey">e.g factory4_day</param>
|
||||
/// <returns>guid</returns>
|
||||
@@ -776,14 +918,15 @@ public class RepeatableQuestGenerator(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exploration repeatable quests can specify a required extraction point.
|
||||
/// This method creates the according object which will be appended to the conditions list
|
||||
/// Exploration repeatable quests can specify a required extraction point.
|
||||
/// This method creates the according object which will be appended to the conditions list
|
||||
/// </summary>
|
||||
/// <param name="exit">The exit name to generate the condition for</param>
|
||||
/// <returns>Exit condition</returns>
|
||||
protected QuestConditionCounterCondition GenerateExplorationExitCondition(Exit exit)
|
||||
{
|
||||
return new QuestConditionCounterCondition {
|
||||
return new QuestConditionCounterCondition
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
DynamicLocale = true,
|
||||
ExitName = exit.Name,
|
||||
@@ -792,14 +935,17 @@ public class RepeatableQuestGenerator(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the base object of quest type format given as templates in assets/database/templates/repeatableQuests.json
|
||||
/// The templates include Elimination, Completion and Extraction quest types
|
||||
/// Generates the base object of quest type format given as templates in
|
||||
/// assets/database/templates/repeatableQuests.json
|
||||
/// The templates include Elimination, Completion and Extraction quest types
|
||||
/// </summary>
|
||||
/// <param name="type">Quest type: "Elimination", "Completion" or "Extraction"</param>
|
||||
/// <param name="traderId">Trader from which the quest will be provided</param>
|
||||
/// <param name="side">Scav daily or pmc daily/weekly quest</param>
|
||||
/// <returns>Object which contains the base elements for repeatable quests of the requests type
|
||||
/// (needs to be filled with reward and conditions by called to make a valid quest)</returns>
|
||||
/// <returns>
|
||||
/// Object which contains the base elements for repeatable quests of the requests type
|
||||
/// (needs to be filled with reward and conditions by called to make a valid quest)
|
||||
/// </returns>
|
||||
protected RepeatableQuest GenerateRepeatableTemplate(
|
||||
string type,
|
||||
string traderId,
|
||||
@@ -822,6 +968,7 @@ public class RepeatableQuestGenerator(
|
||||
questData = _databaseService.GetTemplates().RepeatableQuests.Templates.Pickup;
|
||||
break;
|
||||
}
|
||||
|
||||
var questClone = _cloner.Clone(questData);
|
||||
questClone.Id = _hashUtil.Generate();
|
||||
questClone.TraderId = traderId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using SptCommon.Annotations;
|
||||
using Core.Helpers;
|
||||
using Core.Models.Eft.Common;
|
||||
using Core.Models.Eft.Common.Tables;
|
||||
using Core.Models.Enums;
|
||||
using Core.Models.Spt.Config;
|
||||
@@ -9,8 +9,8 @@ using Core.Servers;
|
||||
using Core.Services;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using Core.Models.Eft.Common;
|
||||
using Core.Models.Spt.Server;
|
||||
using Core.Utils.Collections;
|
||||
using SptCommon.Annotations;
|
||||
|
||||
namespace Core.Generators;
|
||||
|
||||
@@ -42,12 +42,12 @@ public class RepeatableQuestRewardGenerator(
|
||||
* - Items
|
||||
* - Trader Reputation
|
||||
* - Skill level experience
|
||||
*
|
||||
*
|
||||
* The reward is dependent on the player level as given by the wiki. The exact mapping of pmcLevel to
|
||||
* experience / money / items / trader reputation can be defined in QuestConfig.js
|
||||
*
|
||||
*
|
||||
* There's also a random variation of the reward the spread of which can be also defined in the config
|
||||
*
|
||||
*
|
||||
* Additionally, a scaling factor w.r.t. quest difficulty going from 0.2...1 can be used
|
||||
* @param pmcLevel Level of player reward is being generated for
|
||||
* @param difficulty Reward scaling factor from 0.2 to 1
|
||||
@@ -81,7 +81,7 @@ public class RepeatableQuestRewardGenerator(
|
||||
if (rewardParams.RewardXP > 0)
|
||||
{
|
||||
rewards.Success.Add(
|
||||
new()
|
||||
new Reward
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
Unknown = false,
|
||||
@@ -105,9 +105,10 @@ public class RepeatableQuestRewardGenerator(
|
||||
|
||||
// Add preset weapon to reward if checks pass
|
||||
var traderWhitelistDetails = repeatableConfig.TraderWhitelist.FirstOrDefault(
|
||||
(traderWhitelist) => traderWhitelist.TraderId == traderId
|
||||
traderWhitelist => traderWhitelist.TraderId == traderId
|
||||
);
|
||||
if (traderWhitelistDetails?.RewardCanBeWeapon ?? false && _randomUtil.GetChance100(traderWhitelistDetails.WeaponRewardChancePercent ?? 0)
|
||||
if (traderWhitelistDetails?.RewardCanBeWeapon ??
|
||||
(false && _randomUtil.GetChance100(traderWhitelistDetails.WeaponRewardChancePercent ?? 0))
|
||||
)
|
||||
{
|
||||
var chosenWeapon = GetRandomWeaponPresetWithinBudget(itemRewardBudget.Value, rewardIndex);
|
||||
@@ -126,7 +127,7 @@ public class RepeatableQuestRewardGenerator(
|
||||
{
|
||||
// Filter reward pool of items from blacklist, only use if there's at least 1 item remaining
|
||||
var filteredRewardItemPool = inBudgetRewardItemPool.Where(
|
||||
(item) => !rewardTplBlacklist.Contains(item.Id)
|
||||
item => !rewardTplBlacklist.Contains(item.Id)
|
||||
);
|
||||
if (filteredRewardItemPool.Count() > 0)
|
||||
{
|
||||
@@ -142,8 +143,8 @@ public class RepeatableQuestRewardGenerator(
|
||||
{
|
||||
var itemsToReward = GetRewardableItemsFromPoolWithinBudget(
|
||||
inBudgetRewardItemPool,
|
||||
rewardParams.RewardNumItems,
|
||||
itemRewardBudget,
|
||||
rewardParams.RewardNumItems.Value,
|
||||
itemRewardBudget.Value,
|
||||
repeatableConfig
|
||||
);
|
||||
|
||||
@@ -217,40 +218,61 @@ public class RepeatableQuestRewardGenerator(
|
||||
_logger.Warning(_localisationService.GetText("repeatable-difficulty_was_nan"));
|
||||
}
|
||||
|
||||
return new()
|
||||
return new QuestRewardValues
|
||||
{
|
||||
SkillPointReward = _mathUtil.Interp1(pmcLevel, levelsConfig, skillPointRewardConfig),
|
||||
SkillRewardChance = _mathUtil.Interp1(pmcLevel, levelsConfig, skillRewardChanceConfig),
|
||||
RewardReputation = GetRewardRep(effectiveDifficulty, pmcLevel, levelsConfig, reputationConfig, rewardSpreadConfig),
|
||||
RewardReputation = GetRewardRep(
|
||||
effectiveDifficulty,
|
||||
pmcLevel,
|
||||
levelsConfig,
|
||||
reputationConfig,
|
||||
rewardSpreadConfig
|
||||
),
|
||||
RewardNumItems = GetRewardNumItems(pmcLevel, levelsConfig, itemsConfig),
|
||||
RewardRoubles = GetRewardRoubles(effectiveDifficulty, pmcLevel, levelsConfig, roublesConfig, rewardSpreadConfig),
|
||||
GpCoinRewardCount = GetGpCoinRewardCount(effectiveDifficulty, pmcLevel, levelsConfig, gpCoinConfig, rewardSpreadConfig),
|
||||
RewardXP = GetRewardXp(effectiveDifficulty, pmcLevel, levelsConfig, xpConfig, rewardSpreadConfig),
|
||||
RewardRoubles = GetRewardRoubles(
|
||||
effectiveDifficulty,
|
||||
pmcLevel,
|
||||
levelsConfig,
|
||||
roublesConfig,
|
||||
rewardSpreadConfig
|
||||
),
|
||||
GpCoinRewardCount = GetGpCoinRewardCount(
|
||||
effectiveDifficulty,
|
||||
pmcLevel,
|
||||
levelsConfig,
|
||||
gpCoinConfig,
|
||||
rewardSpreadConfig
|
||||
),
|
||||
RewardXP = GetRewardXp(effectiveDifficulty, pmcLevel, levelsConfig, xpConfig, rewardSpreadConfig)
|
||||
};
|
||||
}
|
||||
|
||||
private double GetRewardXp(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig, List<double>? xpConfig, double? rewardSpreadConfig)
|
||||
private double GetRewardXp(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig,
|
||||
List<double>? xpConfig, double? rewardSpreadConfig)
|
||||
{
|
||||
return Math.Floor(
|
||||
(effectiveDifficulty *
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, xpConfig) *
|
||||
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig))) ??
|
||||
effectiveDifficulty *
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, xpConfig) *
|
||||
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private double GetGpCoinRewardCount(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig, List<double>? gpCoinConfig,
|
||||
private double GetGpCoinRewardCount(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig,
|
||||
List<double>? gpCoinConfig,
|
||||
double? rewardSpreadConfig)
|
||||
{
|
||||
return Math.Ceiling(
|
||||
(effectiveDifficulty *
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, gpCoinConfig) *
|
||||
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig))) ??
|
||||
effectiveDifficulty *
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, gpCoinConfig) *
|
||||
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private double GetRewardRep(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig, List<double>? reputationConfig,
|
||||
private double GetRewardRep(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig,
|
||||
List<double>? reputationConfig,
|
||||
double? rewardSpreadConfig)
|
||||
{
|
||||
return Math.Round(
|
||||
@@ -263,29 +285,175 @@ public class RepeatableQuestRewardGenerator(
|
||||
100;
|
||||
}
|
||||
|
||||
private double GetRewardNumItems(int pmcLevel, List<double>? levelsConfig, List<double>? itemsConfig)
|
||||
private int GetRewardNumItems(int pmcLevel, List<double>? levelsConfig, List<double>? itemsConfig)
|
||||
{
|
||||
return _randomUtil.RandInt(1, (int)Math.Round(_mathUtil.Interp1(pmcLevel, levelsConfig, itemsConfig) ?? 0) + 1);
|
||||
}
|
||||
|
||||
private double GetRewardRoubles(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig, List<double>? roublesConfig,
|
||||
private double GetRewardRoubles(double? effectiveDifficulty, int pmcLevel, List<double>? levelsConfig,
|
||||
List<double>? roublesConfig,
|
||||
double? rewardSpreadConfig)
|
||||
{
|
||||
return Math.Floor(
|
||||
(effectiveDifficulty *
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) *
|
||||
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig))) ??
|
||||
effectiveDifficulty *
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) *
|
||||
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private List<KeyValuePair<TemplateItem, double>> GetRewardableItemsFromPoolWithinBudget(List<TemplateItem> inBudgetRewardItemPool,
|
||||
object rewardNumItems, double? itemRewardBudget, RepeatableQuestConfig repeatableConfig)
|
||||
private Dictionary<TemplateItem, int> GetRewardableItemsFromPoolWithinBudget(List<TemplateItem> itemPool,
|
||||
int maxItemCount, double itemRewardBudget, RepeatableQuestConfig repeatableConfig)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var itemsToReturn = new Dictionary<TemplateItem, int>();
|
||||
var exhausableItemPool = new ExhaustableArray<TemplateItem>(itemPool, _randomUtil, _cloner);
|
||||
|
||||
for (var i = 0; i < maxItemCount; i++)
|
||||
{
|
||||
// Default stack size to 1
|
||||
var rewardItemStackCount = 1;
|
||||
|
||||
// Get a random item
|
||||
var chosenItemFromPool = exhausableItemPool.GetRandomValue();
|
||||
if (!exhausableItemPool.HasValues())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle edge case - ammo
|
||||
if (_itemHelper.IsOfBaseclass(chosenItemFromPool.Id, BaseClasses.AMMO))
|
||||
{
|
||||
// Don't reward ammo that stacks to less than what's allowed in config
|
||||
if (chosenItemFromPool.Properties.StackMaxSize < repeatableConfig.RewardAmmoStackMinSize)
|
||||
{
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Choose smallest value between budget, fitting size and stack max
|
||||
rewardItemStackCount = CalculateAmmoStackSizeThatFitsBudget(
|
||||
chosenItemFromPool,
|
||||
itemRewardBudget,
|
||||
maxItemCount
|
||||
);
|
||||
}
|
||||
|
||||
// 25% chance to double, triple or quadruple reward stack
|
||||
// (Only occurs when item is stackable and not weapon, armor or ammo)
|
||||
if (CanIncreaseRewardItemStackSize(chosenItemFromPool, 70000, 25))
|
||||
{
|
||||
rewardItemStackCount = GetRandomisedRewardItemStackSizeByPrice(chosenItemFromPool);
|
||||
}
|
||||
|
||||
itemsToReturn.Add(chosenItemFromPool, rewardItemStackCount);
|
||||
|
||||
var itemCost = _presetHelper.GetDefaultPresetOrItemPrice(chosenItemFromPool.Id);
|
||||
var calculatedItemRewardBudget = itemRewardBudget - rewardItemStackCount * itemCost;
|
||||
_logger.Debug($"Added item: {chosenItemFromPool.Id} with price: {rewardItemStackCount * itemCost}");
|
||||
|
||||
// If we still have budget narrow down possible items
|
||||
if (calculatedItemRewardBudget > 0)
|
||||
{
|
||||
// Filter possible reward items to only items with a price below the remaining budget
|
||||
exhausableItemPool = new ExhaustableArray<TemplateItem>(
|
||||
FilterRewardPoolWithinBudget(itemPool, calculatedItemRewardBudget, 0),
|
||||
_randomUtil,
|
||||
_cloner
|
||||
);
|
||||
|
||||
if (!exhausableItemPool.HasValues())
|
||||
{
|
||||
_logger.Debug($"Reward pool empty with: {calculatedItemRewardBudget} roubles of budget remaining");
|
||||
}
|
||||
}
|
||||
|
||||
// No budget for more items, end loop
|
||||
break;
|
||||
}
|
||||
|
||||
return itemsToReturn;
|
||||
}
|
||||
|
||||
private List<TemplateItem> ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig, double? roublesBudget, string traderId)
|
||||
/**
|
||||
* Get a count of cartridges that fits the rouble budget amount provided
|
||||
* e.g. how many M80s for 50,000 roubles
|
||||
* @param itemSelected Cartridge
|
||||
* @param roublesBudget Rouble budget
|
||||
* @param rewardNumItems
|
||||
* @returns Count that fits budget (min 1)
|
||||
*/
|
||||
private int CalculateAmmoStackSizeThatFitsBudget(TemplateItem itemSelected, double roublesBudget,
|
||||
int rewardNumItems)
|
||||
{
|
||||
// Calculate budget per reward item
|
||||
var stackRoubleBudget = roublesBudget / rewardNumItems;
|
||||
|
||||
var singleCartridgePrice = _handbookHelper.GetTemplatePrice(itemSelected.Id);
|
||||
|
||||
// Get a stack size of ammo that fits rouble budget
|
||||
var stackSizeThatFitsBudget = Math.Round(stackRoubleBudget / singleCartridgePrice.Value);
|
||||
|
||||
// Get itemDbs max stack size for ammo - don't go above 100 (some mods mess around with stack sizes)
|
||||
var stackMaxCount = Math.Min(itemSelected.Properties.StackMaxSize.Value, 100);
|
||||
|
||||
// Ensure stack size is at least 1 + is no larger than the max possible stack size
|
||||
return (int)Math.Max(1, Math.Min(stackSizeThatFitsBudget, stackMaxCount));
|
||||
}
|
||||
|
||||
private bool CanIncreaseRewardItemStackSize(TemplateItem item, int maxRoublePriceToStack,
|
||||
int? randomChanceToPass = null)
|
||||
{
|
||||
var isEligibleForStackSizeIncrease =
|
||||
_presetHelper.GetDefaultPresetOrItemPrice(item.Id) < maxRoublePriceToStack &&
|
||||
!_itemHelper.IsOfBaseclasses(
|
||||
item.Id,
|
||||
[
|
||||
BaseClasses.WEAPON,
|
||||
BaseClasses.ARMORED_EQUIPMENT,
|
||||
BaseClasses.AMMO
|
||||
]
|
||||
) &&
|
||||
!_itemHelper.ItemRequiresSoftInserts(item.Id);
|
||||
|
||||
return isEligibleForStackSizeIncrease && _randomUtil.GetChance100(randomChanceToPass ?? 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a randomised number a reward items stack size should be based on its handbook price
|
||||
* @param item Reward item to get stack size for
|
||||
* @returns matching stack size for the passed in items price
|
||||
*/
|
||||
private int GetRandomisedRewardItemStackSizeByPrice(TemplateItem item)
|
||||
{
|
||||
var rewardItemPrice = _presetHelper.GetDefaultPresetOrItemPrice(item.Id);
|
||||
|
||||
// Define price tiers and corresponding stack size options
|
||||
var priceTiers = new List<Tuple<int, List<int>?>>
|
||||
{
|
||||
new(3000, [2, 3, 4]),
|
||||
new(10000, [2, 3]),
|
||||
new(int.MaxValue, [2, 3, 4]) // Default for prices 10001+ RUB
|
||||
};
|
||||
|
||||
// Find the appropriate price tier and return a random stack size from its options
|
||||
var tier = priceTiers.FirstOrDefault(tier => rewardItemPrice < tier.Item1);
|
||||
if (tier is null)
|
||||
{
|
||||
return 4; // Default to 2 if no tier matches
|
||||
}
|
||||
|
||||
return _randomUtil.GetArrayValue(tier.Item2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a number of items that have a collective value of the passed in parameter
|
||||
* @param repeatableConfig Config
|
||||
* @param roublesBudget Total value of items to return
|
||||
* @param traderId Id of the trader who will give player reward
|
||||
* @returns Array of reward items that fit budget
|
||||
*/
|
||||
private List<TemplateItem> ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig,
|
||||
double? roublesBudget, string traderId)
|
||||
{
|
||||
// First filter for type and baseclass to avoid lookup in handbook for non-available items
|
||||
var rewardableItemPool = GetRewardableItems(repeatableConfig, traderId);
|
||||
@@ -294,29 +462,47 @@ public class RepeatableQuestRewardGenerator(
|
||||
var rewardableItemPoolWithinBudget = FilterRewardPoolWithinBudget(
|
||||
rewardableItemPool,
|
||||
roublesBudget.Value,
|
||||
minPrice);
|
||||
minPrice
|
||||
);
|
||||
|
||||
if (rewardableItemPoolWithinBudget.Count == 0)
|
||||
{
|
||||
_logger.Warning(_localisationService.GetText("repeatable-no_reward_item_found_in_price_range", new {
|
||||
minPrice = minPrice,
|
||||
roublesBudget = roublesBudget }));
|
||||
_logger.Warning(
|
||||
_localisationService.GetText(
|
||||
"repeatable-no_reward_item_found_in_price_range",
|
||||
new
|
||||
{
|
||||
minPrice, roublesBudget
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// In case we don't find any items in the price range
|
||||
rewardableItemPoolWithinBudget = rewardableItemPool
|
||||
.Where((x) => _itemHelper.GetItemPrice(x.Id) < roublesBudget)
|
||||
.Where(x => _itemHelper.GetItemPrice(x.Id) < roublesBudget)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return rewardableItemPoolWithinBudget;
|
||||
}
|
||||
|
||||
private List<TemplateItem> FilterRewardPoolWithinBudget(List<TemplateItem> rewardItems, double roublesBudget, double minPrice)
|
||||
/**
|
||||
* @param rewardItems List of reward items to filter
|
||||
* @param roublesBudget The budget remaining for rewards
|
||||
* @param minPrice The minimum priced item to include
|
||||
* @returns True if any items remain in `rewardItems`, false otherwise
|
||||
*/
|
||||
private List<TemplateItem> FilterRewardPoolWithinBudget(List<TemplateItem> rewardItems, double roublesBudget,
|
||||
double minPrice)
|
||||
{
|
||||
return rewardItems.Where((item) => {
|
||||
var itemPrice = _presetHelper.GetDefaultPresetOrItemPrice(item.Id);
|
||||
return itemPrice < roublesBudget && itemPrice > minPrice;
|
||||
}).ToList();
|
||||
return rewardItems.Where(
|
||||
item =>
|
||||
{
|
||||
var itemPrice = _presetHelper.GetDefaultPresetOrItemPrice(item.Id);
|
||||
return itemPrice < roublesBudget && itemPrice > minPrice;
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private KeyValuePair<Reward, double>? GetRandomWeaponPresetWithinBudget(double roublesBudget, int rewardIndex)
|
||||
@@ -325,7 +511,8 @@ public class RepeatableQuestRewardGenerator(
|
||||
var defaultPresetPool = new ExhaustableArray<Preset>(
|
||||
_presetHelper.GetDefaultWeaponPresets().Values.ToList(),
|
||||
_randomUtil,
|
||||
_cloner);
|
||||
_cloner
|
||||
);
|
||||
|
||||
while (defaultPresetPool.HasValues())
|
||||
{
|
||||
@@ -336,16 +523,19 @@ public class RepeatableQuestRewardGenerator(
|
||||
}
|
||||
|
||||
// Gather all tpls so we can get prices of them
|
||||
var tpls = randomPreset.Items.Select((item) => item.Template).ToList();
|
||||
var tpls = randomPreset.Items.Select(item => item.Template).ToList();
|
||||
|
||||
// Does preset items fit our budget
|
||||
var presetPrice = _itemHelper.GetItemAndChildrenPrice(tpls);
|
||||
if (presetPrice <= roublesBudget)
|
||||
{
|
||||
_logger.Debug("Added weapon: ${ tpls[0]}with price: ${ presetPrice}");
|
||||
_logger.Debug($"Added weapon: {tpls[0]}with price: {presetPrice}");
|
||||
var chosenPreset = _cloner.Clone(randomPreset);
|
||||
|
||||
return new KeyValuePair<Reward, double>(GeneratePresetReward(chosenPreset.Encyclopedia, 1, rewardIndex, chosenPreset.Items), presetPrice);
|
||||
return new KeyValuePair<Reward, double>(
|
||||
GeneratePresetReward(chosenPreset.Encyclopedia, 1, rewardIndex, chosenPreset.Items),
|
||||
presetPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +544,7 @@ public class RepeatableQuestRewardGenerator(
|
||||
|
||||
/**
|
||||
* Helper to create a reward item structured as required by the client
|
||||
*
|
||||
*
|
||||
* @param {string} tpl ItemId of the rewarded item
|
||||
* @param {integer} count Amount of items to give
|
||||
* @param {integer} index All rewards will be appended to a list, for unknown reasons the client wants the index
|
||||
@@ -364,25 +554,26 @@ public class RepeatableQuestRewardGenerator(
|
||||
protected Reward GeneratePresetReward(string tpl, int count, int index, List<Item>? preset, bool foundInRaid = true)
|
||||
{
|
||||
var id = _hashUtil.Generate();
|
||||
var questRewardItem = new Reward{
|
||||
var questRewardItem = new Reward
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
Unknown = false,
|
||||
GameMode =[],
|
||||
AvailableInGameEditions =[],
|
||||
GameMode = [],
|
||||
AvailableInGameEditions = [],
|
||||
Index = index,
|
||||
Target =id,
|
||||
Target = id,
|
||||
Value = count,
|
||||
IsEncoded = false,
|
||||
FindInRaid =foundInRaid,
|
||||
Type =RewardType.Item,
|
||||
Items =[],
|
||||
FindInRaid = foundInRaid,
|
||||
Type = RewardType.Item,
|
||||
Items = []
|
||||
};
|
||||
|
||||
// Get presets root item
|
||||
var rootItem = preset.FirstOrDefault((item) => item.Template == tpl);
|
||||
var rootItem = preset.FirstOrDefault(item => item.Template == tpl);
|
||||
if (rootItem is null)
|
||||
{
|
||||
_logger.Warning($"Root item of preset: ${ tpl} not found");
|
||||
_logger.Warning($"Root item of preset: {tpl} not found");
|
||||
}
|
||||
|
||||
if (rootItem.Upd is not null)
|
||||
@@ -396,10 +587,20 @@ public class RepeatableQuestRewardGenerator(
|
||||
return questRewardItem;
|
||||
}
|
||||
|
||||
private Reward GenerateItemReward(string tpl, double count, int index, bool foundInRaid = true)
|
||||
/**
|
||||
* Helper to create a reward item structured as required by the client
|
||||
*
|
||||
* @param {string} tpl ItemId of the rewarded item
|
||||
* @param {integer} count Amount of items to give
|
||||
* @param {integer} index All rewards will be appended to a list, for unknown reasons the client wants the index
|
||||
* @param preset Optional array of preset items
|
||||
* @returns {object} Object of "Reward"-item-type
|
||||
*/
|
||||
protected Reward GenerateItemReward(string tpl, double count, int index, bool foundInRaid = true)
|
||||
{
|
||||
var id = _hashUtil.Generate();
|
||||
var questRewardItem = new Reward{
|
||||
var questRewardItem = new Reward
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
Unknown = false,
|
||||
GameMode = [],
|
||||
@@ -410,10 +611,12 @@ public class RepeatableQuestRewardGenerator(
|
||||
IsEncoded = false,
|
||||
FindInRaid = foundInRaid,
|
||||
Type = RewardType.Item,
|
||||
Items = [],
|
||||
Items = []
|
||||
};
|
||||
|
||||
var rootItem = new Item { Id = id, Template = tpl, Upd = new Upd { StackObjectsCount = count, SpawnedInSession = foundInRaid }
|
||||
var rootItem = new Item
|
||||
{
|
||||
Id = id, Template = tpl, Upd = new Upd { StackObjectsCount = count, SpawnedInSession = foundInRaid }
|
||||
};
|
||||
questRewardItem.Items = [rootItem];
|
||||
|
||||
@@ -428,13 +631,23 @@ public class RepeatableQuestRewardGenerator(
|
||||
|
||||
// Convert reward amount to Euros if necessary
|
||||
var rewardAmountToGivePlayer =
|
||||
currency == Money.EUROS ? _handbookHelper.FromRUB(rewardRoubles, Money.EUROS) : rewardRoubles;
|
||||
currency == Money.EUROS ? _handbookHelper.FromRUB(rewardRoubles, Money.EUROS) : rewardRoubles;
|
||||
|
||||
// Get chosen currency + amount and return
|
||||
return GenerateItemReward(currency, rewardAmountToGivePlayer, rewardIndex, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Picks rewardable items from items.json
|
||||
* This means they must:
|
||||
* - Fit into the inventory
|
||||
* - Shouldn't be keys
|
||||
* - Have a price greater than 0
|
||||
* @param repeatableQuestConfig Config file
|
||||
* @param traderId Id of trader who will give reward to player
|
||||
* @returns List of rewardable items [[_tpl, itemTemplate],...]
|
||||
*/
|
||||
public List<TemplateItem> GetRewardableItems(RepeatableQuestConfig repeatableQuestConfig, string traderId)
|
||||
{
|
||||
// Get an array of seasonal items that should not be shown right now as seasonal event is not active
|
||||
@@ -443,26 +656,43 @@ public class RepeatableQuestRewardGenerator(
|
||||
// Check for specific baseclasses which don't make sense as reward item
|
||||
// also check if the price is greater than 0; there are some items whose price can not be found
|
||||
// those are not in the game yet (e.g. AGS grenade launcher)
|
||||
return _databaseService.GetItems().Values.Where((itemTemplate => {
|
||||
// Base "Item" item has no parent, ignore it
|
||||
if (itemTemplate.Parent == "")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return _databaseService.GetItems()
|
||||
.Values.Where(
|
||||
itemTemplate =>
|
||||
{
|
||||
// Base "Item" item has no parent, ignore it
|
||||
if (itemTemplate.Parent == "")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seasonalItems.Contains(itemTemplate.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (seasonalItems.Contains(itemTemplate.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var traderWhitelist = repeatableQuestConfig.TraderWhitelist.FirstOrDefault(
|
||||
(trader) => trader.TraderId == traderId);
|
||||
var traderWhitelist = repeatableQuestConfig.TraderWhitelist.FirstOrDefault(
|
||||
trader => trader.TraderId == traderId
|
||||
);
|
||||
|
||||
return IsValidRewardItem(itemTemplate.Id, repeatableQuestConfig, traderWhitelist?.RewardBaseWhitelist);
|
||||
})).ToList();
|
||||
return IsValidRewardItem(
|
||||
itemTemplate.Id,
|
||||
repeatableQuestConfig,
|
||||
traderWhitelist?.RewardBaseWhitelist
|
||||
);
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool IsValidRewardItem(string tpl, RepeatableQuestConfig repeatableQuestConfig, List<string>? itemBaseWhitelist = null)
|
||||
/**
|
||||
* Checks if an id is a valid item. Valid meaning that it's an item that may be a reward
|
||||
* or content of bot loot. Items that are tested as valid may be in a player backpack or stash.
|
||||
* @param {string} tpl template id of item to check
|
||||
* @returns True if item is valid reward
|
||||
*/
|
||||
private bool IsValidRewardItem(string tpl, RepeatableQuestConfig repeatableQuestConfig,
|
||||
List<string>? itemBaseWhitelist = null)
|
||||
{
|
||||
// Return early if not valid item to give as reward
|
||||
if (!_itemHelper.isValidItem(tpl))
|
||||
@@ -482,7 +712,7 @@ public class RepeatableQuestRewardGenerator(
|
||||
}
|
||||
|
||||
// Item has blacklisted base types
|
||||
if (_itemHelper.IsOfBaseclasses(tpl, repeatableQuestConfig.RewardBaseTypeBlacklist ))
|
||||
if (_itemHelper.IsOfBaseclasses(tpl, repeatableQuestConfig.RewardBaseTypeBlacklist))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class PresetHelper(
|
||||
{
|
||||
protected Dictionary<string, List<string>> _lookup = new();
|
||||
protected Dictionary<string, Preset> _defaultEquipmentPresets;
|
||||
protected Dictionary<string, Preset> _defaultWeaponPresets;
|
||||
protected Dictionary<string, Preset>? _defaultWeaponPresets;
|
||||
|
||||
public void HydratePresetStore(Dictionary<string, List<string>> input)
|
||||
{
|
||||
@@ -40,10 +40,10 @@ public class PresetHelper(
|
||||
*/
|
||||
public Dictionary<string, Preset> GetDefaultWeaponPresets()
|
||||
{
|
||||
if (_defaultWeaponPresets == null)
|
||||
if (_defaultWeaponPresets is null)
|
||||
{
|
||||
var tempPresets = _databaseService.GetGlobals().ItemPresets;
|
||||
tempPresets = tempPresets.Where(
|
||||
_defaultWeaponPresets = tempPresets.Where(
|
||||
p =>
|
||||
p.Value.Encyclopedia != null &&
|
||||
_itemHelper.IsOfBaseclass(p.Value.Encyclopedia, BaseClasses.WEAPON)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using SptCommon.Annotations;
|
||||
using Core.Models.Spt.Config;
|
||||
using Core.Models.Utils;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using Core.Utils.Collections;
|
||||
using SptCommon.Annotations;
|
||||
|
||||
namespace Core.Helpers;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class RepeatableQuestHelper(
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the relevant elimination config based on the current players PMC level
|
||||
/// Get the relevant elimination config based on the current players PMC level
|
||||
/// </summary>
|
||||
/// <param name="pmcLevel">Level of PMC character</param>
|
||||
/// <param name="repeatableConfig">Main repeatable config</param>
|
||||
@@ -23,7 +23,7 @@ public class RepeatableQuestHelper(
|
||||
public EliminationConfig? GetEliminationConfigByPmcLevel(int pmcLevel, RepeatableQuestConfig repeatableConfig)
|
||||
{
|
||||
return repeatableConfig.QuestConfig.Elimination.FirstOrDefault(
|
||||
(x) => pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max
|
||||
x => pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Models.Spt.Repeatable;
|
||||
|
||||
@@ -14,7 +14,7 @@ public record QuestRewardValues
|
||||
public double? RewardReputation { get; set; }
|
||||
|
||||
[JsonPropertyName("rewardNumItems")]
|
||||
public double? RewardNumItems { get; set; }
|
||||
public int? RewardNumItems { get; set; }
|
||||
|
||||
[JsonPropertyName("rewardRoubles")]
|
||||
public double? RewardRoubles { get; set; }
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
|
||||
namespace Core.Models.Spt.Server;
|
||||
|
||||
public record ExhaustableArray<T>
|
||||
{
|
||||
private readonly RandomUtil _randomUtil;
|
||||
private readonly ICloner _cloner;
|
||||
private List<T> pool;
|
||||
|
||||
public ExhaustableArray(List<T> itemPool, RandomUtil randomUtil, ICloner cloner)
|
||||
{
|
||||
_randomUtil = randomUtil;
|
||||
_cloner = cloner;
|
||||
this.pool = _cloner.Clone(itemPool);
|
||||
}
|
||||
|
||||
public T GetRandomValue()
|
||||
{
|
||||
if (pool == null || pool.Count == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var index = _randomUtil.GetInt(0, pool.Count - 1);
|
||||
T toReturn = _cloner.Clone(pool[index]);
|
||||
pool.RemoveAt(index);
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public T GetFirstValue()
|
||||
{
|
||||
if (pool == null || pool.Count == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
T toReturn = _cloner.Clone(pool[0]);
|
||||
pool.RemoveAt(0);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public bool HasValues()
|
||||
{
|
||||
return pool?.Count > 0;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using Core.Utils.Cloners;
|
||||
|
||||
namespace Core.Utils.Collections;
|
||||
|
||||
public class ExhaustableArray<T> : IExhaustableArray<T>
|
||||
public record ExhaustableArray<T> : IExhaustableArray<T>
|
||||
{
|
||||
private LinkedList<T>? pool;
|
||||
private RandomUtil _randomUtil;
|
||||
|
||||
Reference in New Issue
Block a user