Repeatable quest generation (Part 1) (#417)
* Refactor and breakout CompletionQuestGenerator.cs * make `GenerateAvailableForFinish` protected
This commit is contained in:
+421
@@ -0,0 +1,421 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Models.Spt.Config;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Servers;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using SPTarkov.Server.Core.Utils.Json;
|
||||
|
||||
namespace SPTarkov.Server.Core.Generators.RepeatableQuestGeneration;
|
||||
|
||||
[Injectable]
|
||||
public class CompletionQuestGenerator(
|
||||
ISptLogger<CompletionQuestGenerator> logger,
|
||||
RepeatableQuestHelper repeatableQuestHelper,
|
||||
RepeatableQuestRewardGenerator repeatableQuestRewardGenerator,
|
||||
DatabaseService databaseService,
|
||||
SeasonalEventService seasonalEventService,
|
||||
LocalisationService localisationService,
|
||||
RandomUtil randomUtil,
|
||||
MathUtil mathUtil,
|
||||
HashUtil hashUtil,
|
||||
ItemHelper itemHelper
|
||||
)
|
||||
{
|
||||
protected const int MaxRandomNumberAttempts = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a valid Completion quest
|
||||
/// </summary>
|
||||
/// <param name="sessionId">session Id to generate the quest for</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="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>
|
||||
public RepeatableQuest? Generate(
|
||||
string sessionId,
|
||||
int pmcLevel,
|
||||
string traderId,
|
||||
RepeatableQuestConfig repeatableConfig
|
||||
)
|
||||
{
|
||||
var completionConfig = repeatableConfig.QuestConfig.Completion;
|
||||
var levelsConfig = repeatableConfig.RewardScaling.Levels;
|
||||
var roublesConfig = repeatableConfig.RewardScaling.Roubles;
|
||||
|
||||
var quest = repeatableQuestHelper.GenerateRepeatableTemplate(
|
||||
RepeatableQuestType.Completion,
|
||||
traderId,
|
||||
repeatableConfig.Side,
|
||||
sessionId
|
||||
);
|
||||
|
||||
if (quest is null)
|
||||
{
|
||||
logger.Error("Quest template null when attempting to create completion operational task.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existent"
|
||||
var itemsToRetrievePool = GetItemsToRetrievePool(
|
||||
completionConfig,
|
||||
repeatableConfig.RewardBlacklist
|
||||
);
|
||||
|
||||
// Filter items within our budget
|
||||
var (hashSet, budget) = GetItemsWithinBudget(pmcLevel, levelsConfig, roublesConfig, itemsToRetrievePool);
|
||||
itemsToRetrievePool = hashSet;
|
||||
|
||||
// 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)
|
||||
{
|
||||
itemsToRetrievePool = GetWhitelistedItemSelection(itemsToRetrievePool, pmcLevel);
|
||||
}
|
||||
|
||||
if (repeatableConfig.QuestConfig.Completion.UseBlacklist)
|
||||
{
|
||||
itemsToRetrievePool = GetBlacklistedItemSelection(itemsToRetrievePool, pmcLevel);
|
||||
}
|
||||
|
||||
// Filtering too harsh
|
||||
if (itemsToRetrievePool.Count == 0)
|
||||
{
|
||||
logger.Error(
|
||||
localisationService.GetText(
|
||||
"repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive"
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var selectedItems = GenerateAvailableForFinish(
|
||||
quest, completionConfig, repeatableConfig, itemsToRetrievePool.ToList(), budget
|
||||
);
|
||||
|
||||
quest.Rewards = repeatableQuestRewardGenerator.GenerateReward(
|
||||
pmcLevel,
|
||||
1,
|
||||
traderId,
|
||||
repeatableConfig,
|
||||
completionConfig,
|
||||
selectedItems
|
||||
);
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a pool of item tpls the player should reasonably be able to retrieve
|
||||
/// </summary>
|
||||
/// <param name="completionConfig">Completion quest type config</param>
|
||||
/// <param name="itemTplBlacklist">Item tpls to not add to pool</param>
|
||||
/// <returns>Set of item tpls</returns>
|
||||
protected HashSet<string> GetItemsToRetrievePool(
|
||||
Completion completionConfig,
|
||||
HashSet<string> itemTplBlacklist
|
||||
)
|
||||
{
|
||||
// Get seasonal items that should not be added to pool as seasonal event is not active
|
||||
var seasonalItems = seasonalEventService.GetInactiveSeasonalEventItems();
|
||||
|
||||
// Check for specific base classes 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
|
||||
return databaseService
|
||||
.GetItems()
|
||||
.Values.Where(itemTemplate =>
|
||||
{
|
||||
// Base "Item" item has no parent, ignore it
|
||||
if (itemTemplate.Parent == string.Empty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seasonalItems.Contains(itemTemplate.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valid reward items share same logic as items to retrieve
|
||||
return repeatableQuestRewardGenerator.IsValidRewardItem(
|
||||
itemTemplate.Id,
|
||||
itemTplBlacklist,
|
||||
completionConfig.RequiredItemTypeBlacklist
|
||||
);
|
||||
})
|
||||
.Select(item => item.Id)
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter item pool down to items we can afford on our budget
|
||||
/// </summary>
|
||||
/// <param name="pmcLevel">Level of pmc</param>
|
||||
/// <param name="levelsConfig">Levels config</param>
|
||||
/// <param name="roublesConfig">Roubles config</param>
|
||||
/// <param name="itemsToRetrievePool">Item pool</param>
|
||||
/// <returns>Filtered items and roubles budget</returns>
|
||||
protected (HashSet<string>, double) GetItemsWithinBudget(
|
||||
int pmcLevel,
|
||||
List<double> levelsConfig,
|
||||
List<double> roublesConfig,
|
||||
HashSet<string> itemsToRetrievePool)
|
||||
{
|
||||
// Be fair, don't value the items be more expensive than the reward
|
||||
var multiplier = randomUtil.GetDouble(0.5, 1);
|
||||
var roublesBudget = Math.Floor(
|
||||
mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multiplier
|
||||
);
|
||||
|
||||
// Make sure there is always a 5000 rouble budget available for selection
|
||||
roublesBudget = Math.Max(roublesBudget, 5000d);
|
||||
|
||||
return (
|
||||
itemsToRetrievePool.Where(itemTpl => itemHelper.GetItemPrice(itemTpl) < roublesBudget).ToHashSet(),
|
||||
roublesBudget
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter item selection to items in the whitelist
|
||||
/// </summary>
|
||||
/// <param name="itemSelection">Item selection to filter</param>
|
||||
/// <param name="pmcLevel">Level of pmc</param>
|
||||
/// <returns>Filtered selection, or original if null or empty</returns>
|
||||
protected HashSet<string> GetWhitelistedItemSelection(HashSet<string> itemSelection, int pmcLevel)
|
||||
{
|
||||
var itemWhitelist = databaseService
|
||||
.GetTemplates()
|
||||
.RepeatableQuests?.Data?.Completion?.ItemsWhitelist;
|
||||
|
||||
// Whitelist doesn't exist or is empty, return original
|
||||
if (itemWhitelist is null || itemWhitelist.Count == 0)
|
||||
{
|
||||
return itemSelection;
|
||||
}
|
||||
|
||||
// Filter and concatenate items according to current player level
|
||||
var itemIdsWhitelisted = itemWhitelist
|
||||
.Where(p => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds)
|
||||
.ToHashSet(); //.Aggregate((a, p) => a.Concat(p.ItemIds), []);
|
||||
|
||||
var filteredSelection = itemSelection
|
||||
.Where(x =>
|
||||
{
|
||||
// Whitelist can contain item tpls and item base type ids
|
||||
return itemIdsWhitelisted.Any(v => itemHelper.IsOfBaseclass(x, v))
|
||||
|| itemIdsWhitelisted.Contains(x);
|
||||
})
|
||||
.ToHashSet();
|
||||
|
||||
// check if items are missing
|
||||
// var flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []);
|
||||
// var missing = itemIdsWhitelisted.filter(l => !flatList.includes(l));
|
||||
|
||||
return filteredSelection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter item selection based on the blacklist
|
||||
/// </summary>
|
||||
/// <param name="itemSelection">Item selection to filter</param>
|
||||
/// <param name="pmcLevel">Level of pmc</param>
|
||||
/// <returns>Filtered selection, or original if null or empty</returns>
|
||||
protected HashSet<string> GetBlacklistedItemSelection(HashSet<string> itemSelection, int pmcLevel)
|
||||
{
|
||||
var itemBlacklist = databaseService
|
||||
.GetTemplates()
|
||||
.RepeatableQuests?.Data?.Completion?.ItemsBlacklist;
|
||||
|
||||
// Blacklist doesn't exist or is empty, return original
|
||||
if (itemBlacklist is null || itemBlacklist.Count == 0)
|
||||
{
|
||||
return itemSelection;
|
||||
}
|
||||
|
||||
// Filter and concatenate the arrays according to current player level
|
||||
var itemIdsBlacklisted = itemBlacklist
|
||||
.Where(p => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds)
|
||||
.ToHashSet(); //.Aggregate(List<ItemsBlacklist> , (a, p) => a.Concat(p.ItemIds) );
|
||||
|
||||
var filteredSelection = itemSelection
|
||||
.Where(x =>
|
||||
{
|
||||
return itemIdsBlacklisted.All(v => !itemHelper.IsOfBaseclass(x, v))
|
||||
|| !itemIdsBlacklisted.Contains(x);
|
||||
})
|
||||
.ToHashSet();
|
||||
|
||||
return filteredSelection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the available for finish conditions for this quest
|
||||
/// </summary>
|
||||
/// <param name="quest">Quest to add the conditions to</param>
|
||||
/// <param name="completionConfig">Completion config</param>
|
||||
/// <param name="repeatableConfig">Repeatable config</param>
|
||||
/// <param name="itemSelection">Filtered item selection</param>
|
||||
/// <param name="roublesBudget">Budget in roubles</param>
|
||||
/// <returns>Chosen item template Ids</returns>
|
||||
protected List<string> GenerateAvailableForFinish(
|
||||
RepeatableQuest quest,
|
||||
Completion completionConfig,
|
||||
RepeatableQuestConfig repeatableConfig,
|
||||
List<string> itemSelection,
|
||||
double roublesBudget
|
||||
)
|
||||
{
|
||||
// Store the indexes of items we are asking player to supply
|
||||
var distinctItemsToRetrieveCount = randomUtil.GetInt(1, completionConfig.UniqueItemCount);
|
||||
var chosenRequirementItemsTpls = new List<string>();
|
||||
var usedItemIndexes = new HashSet<int>();
|
||||
|
||||
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))
|
||||
{
|
||||
chosenItemIndex = randomUtil.RandInt(itemSelection.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
logger.Error(
|
||||
localisationService.GetText(
|
||||
"repeatable-no_reward_item_found_in_price_range",
|
||||
new { minPrice = 0, roublesBudget }
|
||||
)
|
||||
);
|
||||
|
||||
return chosenRequirementItemsTpls;
|
||||
}
|
||||
|
||||
// Store index of item we've already chosen for later checking
|
||||
usedItemIndexes.Add(chosenItemIndex);
|
||||
|
||||
var tplChosen = itemSelection[chosenItemIndex];
|
||||
var itemPrice = itemHelper.GetItemPrice(tplChosen).Value;
|
||||
var minValue = completionConfig.MinimumRequestedAmount;
|
||||
var maxValue = completionConfig.MaximumRequestedAmount;
|
||||
|
||||
var value = minValue;
|
||||
|
||||
// Get the value range within budget
|
||||
var x = (int)Math.Floor(roublesBudget / itemPrice);
|
||||
maxValue = Math.Min(maxValue, x);
|
||||
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(minValue, maxValue + 1);
|
||||
}
|
||||
|
||||
roublesBudget -= value * itemPrice;
|
||||
|
||||
// Push a CompletionCondition with the item and the amount of the item into quest
|
||||
chosenRequirementItemsTpls.Add(tplChosen);
|
||||
quest.Conditions.AvailableForFinish.Add(
|
||||
GenerateCondition(
|
||||
tplChosen,
|
||||
value,
|
||||
repeatableConfig.QuestConfig.Completion
|
||||
)
|
||||
);
|
||||
|
||||
// Is there budget left for more items
|
||||
if (roublesBudget > 0)
|
||||
{
|
||||
// Reduce item pool to fit budget
|
||||
itemSelection = itemSelection
|
||||
.Where(tpl => itemHelper.GetItemPrice(tpl) < roublesBudget)
|
||||
.ToList();
|
||||
|
||||
if (itemSelection.Count == 0)
|
||||
{
|
||||
// Nothing fits new budget, exit
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return chosenRequirementItemsTpls;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
/// </summary>
|
||||
/// <param name="itemTpl">Id of the item to request</param>
|
||||
/// <param name="value">Amount of items of this specific type to request</param>
|
||||
/// <param name="completionConfig">Completion config from quest.json</param>
|
||||
/// <returns>object of "Completion"-condition</returns>
|
||||
protected QuestCondition GenerateCondition(
|
||||
string itemTpl,
|
||||
double value,
|
||||
Completion completionConfig
|
||||
)
|
||||
{
|
||||
var onlyFoundInRaid = completionConfig.RequiredItemsAreFiR;
|
||||
var minDurability = itemHelper.IsOfBaseclasses(
|
||||
itemTpl,
|
||||
[BaseClasses.WEAPON, BaseClasses.ARMOR]
|
||||
)
|
||||
? randomUtil.GetArrayValue(
|
||||
[
|
||||
completionConfig.RequiredItemMinDurabilityMinMax.Min,
|
||||
completionConfig.RequiredItemMinDurabilityMinMax.Max,
|
||||
]
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Dog tags MUST NOT be FiR for them to work
|
||||
if (itemHelper.IsDogtag(itemTpl))
|
||||
{
|
||||
onlyFoundInRaid = false;
|
||||
}
|
||||
|
||||
return new QuestCondition
|
||||
{
|
||||
Id = hashUtil.Generate(),
|
||||
Index = 0,
|
||||
ParentId = "",
|
||||
DynamicLocale = true,
|
||||
VisibilityConditions = [],
|
||||
Target = new ListOrT<string>([itemTpl], null),
|
||||
Value = value,
|
||||
MinDurability = minDurability,
|
||||
MaxDurability = 100,
|
||||
DogtagLevel = 0,
|
||||
OnlyFoundInRaid = onlyFoundInRaid,
|
||||
IsEncoded = false,
|
||||
ConditionType = "HandoverItem",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Generators.RepeatableQuestGeneration;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
@@ -13,9 +14,11 @@ using SPTarkov.Server.Core.Utils.Cloners;
|
||||
using SPTarkov.Server.Core.Utils.Collections;
|
||||
using SPTarkov.Server.Core.Utils.Json;
|
||||
using BodyParts = SPTarkov.Server.Core.Constants.BodyParts;
|
||||
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
|
||||
|
||||
namespace SPTarkov.Server.Core.Generators;
|
||||
|
||||
[Obsolete("In the process of being removed, do NOT add any new logic!!")]
|
||||
[Injectable]
|
||||
public class RepeatableQuestGenerator(
|
||||
ISptLogger<RepeatableQuestGenerator> _logger,
|
||||
@@ -28,8 +31,9 @@ public class RepeatableQuestGenerator(
|
||||
DatabaseService _databaseService,
|
||||
LocalisationService _localisationService,
|
||||
ConfigServer _configServer,
|
||||
SeasonalEventService _seasonalEventService,
|
||||
ICloner _cloner
|
||||
ICloner _cloner,
|
||||
// This is temporary while this is being refactored, eventually these will all live in the RepeatableQuestController.
|
||||
CompletionQuestGenerator _completionQuestGenerator
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -72,11 +76,24 @@ public class RepeatableQuestGenerator(
|
||||
var traders = repeatableConfig
|
||||
.TraderWhitelist.Where(x => x.QuestTypes.Contains(questType))
|
||||
.Select(x => x.TraderId)
|
||||
// filter out locked traders
|
||||
.Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false))
|
||||
.ToList();
|
||||
// filter out locked traders
|
||||
traders = traders.Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)).ToList();
|
||||
|
||||
var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault();
|
||||
|
||||
if (traderId is null)
|
||||
{
|
||||
// TODO: Localize me!
|
||||
_logger.Error("Could not draw traderId from whitelist during repeatable quest generation");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_logger.IsLogEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.Debug($"Generating operation task type: {questType} for {traderId}");
|
||||
}
|
||||
|
||||
return questType switch
|
||||
{
|
||||
"Elimination" => GenerateEliminationQuest(
|
||||
@@ -86,7 +103,7 @@ public class RepeatableQuestGenerator(
|
||||
questTypePool,
|
||||
repeatableConfig
|
||||
),
|
||||
"Completion" => GenerateCompletionQuest(
|
||||
"Completion" => _completionQuestGenerator.Generate(
|
||||
sessionId,
|
||||
pmcLevel,
|
||||
traderId,
|
||||
@@ -417,8 +434,8 @@ public class RepeatableQuestGenerator(
|
||||
// crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1
|
||||
var difficulty = _mathUtil.MapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2);
|
||||
|
||||
var quest = GenerateRepeatableTemplate(
|
||||
"Elimination",
|
||||
var quest = _repeatableQuestHelper.GenerateRepeatableTemplate(
|
||||
RepeatableQuestType.Elimination,
|
||||
traderId,
|
||||
repeatableConfig.Side,
|
||||
sessionId
|
||||
@@ -595,301 +612,6 @@ public class RepeatableQuestGenerator(
|
||||
return killConditionProps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <returns>quest type format for "Completion" (see assets/database/templates/repeatableQuests.json)</returns>
|
||||
protected RepeatableQuest? GenerateCompletionQuest(
|
||||
string sessionId,
|
||||
int pmcLevel,
|
||||
string traderId,
|
||||
RepeatableQuestConfig repeatableConfig
|
||||
)
|
||||
{
|
||||
var completionConfig = repeatableConfig.QuestConfig.Completion;
|
||||
var levelsConfig = repeatableConfig.RewardScaling.Levels;
|
||||
var roublesConfig = repeatableConfig.RewardScaling.Roubles;
|
||||
|
||||
var quest = GenerateRepeatableTemplate(
|
||||
"Completion",
|
||||
traderId,
|
||||
repeatableConfig.Side,
|
||||
sessionId
|
||||
);
|
||||
|
||||
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existent"
|
||||
var itemsToRetrievePool = GetItemsToRetrievePool(
|
||||
completionConfig,
|
||||
repeatableConfig.RewardBlacklist
|
||||
);
|
||||
|
||||
// Be fair, don't value the items be more expensive than the reward
|
||||
var multiplier = _randomUtil.GetDouble(0.5, 1);
|
||||
var roublesBudget = Math.Floor(
|
||||
_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multiplier
|
||||
);
|
||||
roublesBudget = Math.Max(roublesBudget, 5000d);
|
||||
var itemSelection = itemsToRetrievePool
|
||||
.Where(itemTpl => _itemHelper.GetItemPrice(itemTpl) < 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)
|
||||
{
|
||||
var itemWhitelist = _databaseService
|
||||
.GetTemplates()
|
||||
.RepeatableQuests.Data.Completion.ItemsWhitelist;
|
||||
|
||||
// Filter and concatenate items according to current player level
|
||||
var itemIdsWhitelisted = itemWhitelist
|
||||
.Where(p => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds)
|
||||
.ToHashSet(); //.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, v))
|
||||
|| itemIdsWhitelisted.Contains(x);
|
||||
})
|
||||
.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)
|
||||
{
|
||||
var itemBlacklist = _databaseService
|
||||
.GetTemplates()
|
||||
.RepeatableQuests.Data.Completion.ItemsBlacklist;
|
||||
|
||||
// Filter and concatenate the arrays according to current player level
|
||||
var itemIdsBlacklisted = itemBlacklist
|
||||
.Where(p => p.MinPlayerLevel <= pmcLevel)
|
||||
.SelectMany(x => x.ItemIds)
|
||||
.ToHashSet(); //.Aggregate(List<ItemsBlacklist> , (a, p) => a.Concat(p.ItemIds) );
|
||||
|
||||
itemSelection = itemSelection
|
||||
.Where(x =>
|
||||
{
|
||||
return itemIdsBlacklisted.All(v => !_itemHelper.IsOfBaseclass(x, v))
|
||||
|| !itemIdsBlacklisted.Contains(x);
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Filtering too harsh
|
||||
if (!itemSelection.Any())
|
||||
{
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive"
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store the indexes of items we are asking player to supply
|
||||
var distinctItemsToRetrieveCount = _randomUtil.GetInt(1, completionConfig.UniqueItemCount);
|
||||
var chosenRequirementItemsTpls = new List<string>();
|
||||
var usedItemIndexes = new HashSet<int>();
|
||||
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))
|
||||
{
|
||||
chosenItemIndex = _randomUtil.RandInt(itemSelection.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"repeatable-no_reward_item_found_in_price_range",
|
||||
new { minPrice = 0, roublesBudget }
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store index of item we've already chosen for later checking
|
||||
usedItemIndexes.Add(chosenItemIndex);
|
||||
|
||||
var tplChosen = itemSelection[chosenItemIndex];
|
||||
var itemPrice = _itemHelper.GetItemPrice(tplChosen).Value;
|
||||
var minValue = completionConfig.MinimumRequestedAmount;
|
||||
var maxValue = completionConfig.MaximumRequestedAmount;
|
||||
|
||||
var value = minValue;
|
||||
|
||||
// Get the value range within budget
|
||||
var x = (int)Math.Floor(roublesBudget / itemPrice);
|
||||
maxValue = Math.Min(maxValue, x);
|
||||
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(minValue, maxValue + 1);
|
||||
}
|
||||
|
||||
roublesBudget -= value * itemPrice;
|
||||
|
||||
// Push a CompletionCondition with the item and the amount of the item into quest
|
||||
chosenRequirementItemsTpls.Add(tplChosen);
|
||||
quest.Conditions.AvailableForFinish.Add(
|
||||
GenerateCompletionAvailableForFinish(
|
||||
tplChosen,
|
||||
value,
|
||||
repeatableConfig.QuestConfig.Completion
|
||||
)
|
||||
);
|
||||
|
||||
// Is there budget left for more items
|
||||
if (roublesBudget > 0)
|
||||
{
|
||||
// Reduce item pool to fit budget
|
||||
itemSelection = itemSelection
|
||||
.Where(tpl => _itemHelper.GetItemPrice(tpl) < roublesBudget)
|
||||
.ToList();
|
||||
if (!itemSelection.Any())
|
||||
{
|
||||
// Nothing fits new budget, exit
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward(
|
||||
pmcLevel,
|
||||
1,
|
||||
traderId,
|
||||
repeatableConfig,
|
||||
completionConfig,
|
||||
chosenRequirementItemsTpls
|
||||
);
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a pool of item tpls the player should reasonably be able to retrieve
|
||||
/// </summary>
|
||||
/// <param name="completionConfig">Completion quest type config</param>
|
||||
/// <param name="itemTplBlacklist">Item tpls to not add to pool</param>
|
||||
/// <returns>Set of item tpls</returns>
|
||||
protected HashSet<string> GetItemsToRetrievePool(
|
||||
Completion completionConfig,
|
||||
HashSet<string> itemTplBlacklist
|
||||
)
|
||||
{
|
||||
// Get seasonal items that should not be added to pool as seasonal event is not active
|
||||
var seasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems();
|
||||
|
||||
// Check for specific base classes 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
|
||||
return _databaseService
|
||||
.GetItems()
|
||||
.Values.Where(itemTemplate =>
|
||||
{
|
||||
// Base "Item" item has no parent, ignore it
|
||||
if (itemTemplate.Parent == string.Empty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seasonalItems.Contains(itemTemplate.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valid reward items share same logic as items to retrieve
|
||||
return _repeatableQuestRewardGenerator.IsValidRewardItem(
|
||||
itemTemplate.Id,
|
||||
itemTplBlacklist,
|
||||
completionConfig.RequiredItemTypeBlacklist
|
||||
);
|
||||
})
|
||||
.Select(item => item.Id)
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
/// </summary>
|
||||
/// <param name="itemTpl">Id of the item to request</param>
|
||||
/// <param name="value">Amount of items of this specific type to request</param>
|
||||
/// <param name="completionConfig">Completion config from quest.json</param>
|
||||
/// <returns>object of "Completion"-condition</returns>
|
||||
protected QuestCondition GenerateCompletionAvailableForFinish(
|
||||
string itemTpl,
|
||||
double value,
|
||||
Completion completionConfig
|
||||
)
|
||||
{
|
||||
var onlyFoundInRaid = completionConfig.RequiredItemsAreFiR;
|
||||
var minDurability = _itemHelper.IsOfBaseclasses(
|
||||
itemTpl,
|
||||
[BaseClasses.WEAPON, BaseClasses.ARMOR]
|
||||
)
|
||||
? _randomUtil.GetArrayValue(
|
||||
[
|
||||
completionConfig.RequiredItemMinDurabilityMinMax.Min,
|
||||
completionConfig.RequiredItemMinDurabilityMinMax.Max,
|
||||
]
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Dog tags MUST NOT be FiR for them to work
|
||||
if (_itemHelper.IsDogtag(itemTpl))
|
||||
{
|
||||
onlyFoundInRaid = false;
|
||||
}
|
||||
|
||||
return new QuestCondition
|
||||
{
|
||||
Id = _hashUtil.Generate(),
|
||||
Index = 0,
|
||||
ParentId = "",
|
||||
DynamicLocale = true,
|
||||
VisibilityConditions = [],
|
||||
Target = new ListOrT<string>([itemTpl], null),
|
||||
Value = value,
|
||||
MinDurability = minDurability,
|
||||
MaxDurability = 100,
|
||||
DogtagLevel = 0,
|
||||
OnlyFoundInRaid = onlyFoundInRaid,
|
||||
IsEncoded = false,
|
||||
ConditionType = "HandoverItem",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a valid Exploration quest
|
||||
/// </summary>
|
||||
@@ -938,8 +660,8 @@ public class RepeatableQuestGenerator(
|
||||
: explorationConfig.MaximumExtracts + 1;
|
||||
var numExtracts = _randomUtil.RandInt(1, exitTimesMax);
|
||||
|
||||
var quest = GenerateRepeatableTemplate(
|
||||
"Exploration",
|
||||
var quest = _repeatableQuestHelper.GenerateRepeatableTemplate(
|
||||
RepeatableQuestType.Exploration,
|
||||
traderId,
|
||||
repeatableConfig.Side,
|
||||
sessionId
|
||||
@@ -1048,8 +770,8 @@ public class RepeatableQuestGenerator(
|
||||
{
|
||||
var pickupConfig = repeatableConfig.QuestConfig.Pickup;
|
||||
|
||||
var quest = GenerateRepeatableTemplate(
|
||||
"Pickup",
|
||||
var quest = _repeatableQuestHelper.GenerateRepeatableTemplate(
|
||||
RepeatableQuestType.Pickup,
|
||||
traderId,
|
||||
repeatableConfig.Side,
|
||||
sessionId
|
||||
@@ -1124,104 +846,4 @@ public class RepeatableQuestGenerator(
|
||||
ConditionType = "ExitName",
|
||||
};
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// </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="playerGroup">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>
|
||||
protected RepeatableQuest GenerateRepeatableTemplate(
|
||||
string type,
|
||||
string traderId,
|
||||
PlayerGroup playerGroup,
|
||||
string sessionId
|
||||
)
|
||||
{
|
||||
RepeatableQuest questData = null;
|
||||
switch (type)
|
||||
{
|
||||
case "Elimination":
|
||||
questData = _databaseService.GetTemplates().RepeatableQuests.Templates.Elimination;
|
||||
break;
|
||||
case "Completion":
|
||||
questData = _databaseService.GetTemplates().RepeatableQuests.Templates.Completion;
|
||||
break;
|
||||
case "Exploration":
|
||||
questData = _databaseService.GetTemplates().RepeatableQuests.Templates.Exploration;
|
||||
break;
|
||||
case "Pickup":
|
||||
questData = _databaseService.GetTemplates().RepeatableQuests.Templates.Pickup;
|
||||
break;
|
||||
}
|
||||
|
||||
var questClone = _cloner.Clone(questData);
|
||||
questClone.Id = _hashUtil.Generate();
|
||||
questClone.TraderId = traderId;
|
||||
|
||||
/* in locale, these id correspond to the text of quests
|
||||
template ids -pmc : Elimination = 616052ea3054fc0e2c24ce6e / Completion = 61604635c725987e815b1a46 / Exploration = 616041eb031af660100c9967
|
||||
template ids -scav : Elimination = 62825ef60e88d037dc1eb428 / Completion = 628f588ebb558574b2260fe5 / Exploration = 62825ef60e88d037dc1eb42c
|
||||
*/
|
||||
|
||||
// Get template id from config based on side and type of quest
|
||||
var typeIds = _repeatableQuestHelper.GetRepeatableQuestTemplatesByGroup(playerGroup);
|
||||
|
||||
questClone.TemplateId = typeIds[type];
|
||||
|
||||
// Force REF templates to use prapors ID - solves missing text issue
|
||||
var desiredTraderId = traderId == Traders.REF ? Traders.PRAPOR : traderId;
|
||||
|
||||
questClone.Name = questClone
|
||||
.Name.Replace("{traderId}", traderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.Note = questClone
|
||||
.Note.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.Description = questClone
|
||||
.Description.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.SuccessMessageText = questClone
|
||||
.SuccessMessageText.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.FailMessageText = questClone
|
||||
.FailMessageText.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.StartedMessageText = questClone
|
||||
.StartedMessageText.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.ChangeQuestMessageText = questClone
|
||||
.ChangeQuestMessageText.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.AcceptPlayerMessage = questClone
|
||||
.AcceptPlayerMessage.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.DeclinePlayerMessage = questClone
|
||||
.DeclinePlayerMessage.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.CompletePlayerMessage = questClone
|
||||
.CompletePlayerMessage.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questClone.TemplateId);
|
||||
|
||||
questClone.QuestStatus.Id = _hashUtil.Generate();
|
||||
questClone.QuestStatus.Uid = sessionId; // Needs to match user id
|
||||
questClone.QuestStatus.QId = questClone.Id; // Needs to match quest id
|
||||
|
||||
return questClone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Models.Spt.Config;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Servers;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using SPTarkov.Server.Core.Utils.Cloners;
|
||||
|
||||
namespace SPTarkov.Server.Core.Helpers;
|
||||
|
||||
[Injectable]
|
||||
public class RepeatableQuestHelper(
|
||||
ISptLogger<RepeatableQuestHelper> _logger,
|
||||
ConfigServer _configServer
|
||||
ISptLogger<RepeatableQuestHelper> logger,
|
||||
DatabaseService databaseService,
|
||||
HashUtil hashUtil,
|
||||
ICloner cloner,
|
||||
ConfigServer configServer
|
||||
)
|
||||
{
|
||||
protected QuestConfig _questConfig = _configServer.GetConfig<QuestConfig>();
|
||||
protected QuestConfig _questConfig = configServer.GetConfig<QuestConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the relevant elimination config based on the current players PMC level
|
||||
@@ -36,7 +43,7 @@ public class RepeatableQuestHelper(
|
||||
/// <param name="playerGroup">Side to get the templates for</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public Dictionary<string, string>? GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup)
|
||||
public Dictionary<string, string> GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup)
|
||||
{
|
||||
var templates = _questConfig.RepeatableQuestTemplates;
|
||||
|
||||
@@ -47,4 +54,145 @@ public class RepeatableQuestHelper(
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(playerGroup), playerGroup, null),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cloned repeatable quest template for the provided type with a unique id
|
||||
/// </summary>
|
||||
/// <param name="type">Type of template to retrieve</param>
|
||||
/// <param name="traderId">TraderId that should provide this quest</param>
|
||||
/// <returns>Cloned quest template</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public RepeatableQuest? GetClonedQuestTemplateForType(
|
||||
RepeatableQuestType type,
|
||||
string traderId
|
||||
)
|
||||
{
|
||||
var quest = type switch
|
||||
{
|
||||
RepeatableQuestType.Elimination => cloner.Clone(
|
||||
databaseService.GetTemplates().RepeatableQuests?.Templates?.Elimination),
|
||||
RepeatableQuestType.Completion => cloner.Clone(
|
||||
databaseService.GetTemplates().RepeatableQuests?.Templates?.Completion),
|
||||
RepeatableQuestType.Exploration => cloner.Clone(
|
||||
databaseService.GetTemplates().RepeatableQuests?.Templates?.Exploration),
|
||||
RepeatableQuestType.Pickup => cloner.Clone(
|
||||
databaseService.GetTemplates().RepeatableQuests?.Templates?.Pickup),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (quest is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
quest.Id = hashUtil.Generate();
|
||||
quest.TraderId = traderId;
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// </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="playerGroup">Scav daily or pmc daily/weekly quest</param>
|
||||
/// <param name="sessionId">sessionId to generate template for</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>
|
||||
public RepeatableQuest? GenerateRepeatableTemplate(
|
||||
RepeatableQuestType type,
|
||||
string traderId,
|
||||
PlayerGroup playerGroup,
|
||||
string sessionId
|
||||
)
|
||||
{
|
||||
var questData = GetClonedQuestTemplateForType(type, traderId);
|
||||
|
||||
if (questData is null)
|
||||
{
|
||||
// TODO: Localize me!
|
||||
logger.Error($"No repeatable quest template found for type {type}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get template id from config based on side and type of quest
|
||||
var typeIds = GetRepeatableQuestTemplatesByGroup(playerGroup);
|
||||
|
||||
var templateName = Enum.GetName(type);
|
||||
|
||||
if (templateName is null)
|
||||
{
|
||||
// TODO: Localize me!
|
||||
logger.Error($"Could not resolve template name for {type}");
|
||||
return null;
|
||||
}
|
||||
|
||||
questData.TemplateId = typeIds[templateName];
|
||||
|
||||
// Force REF templates to use prapors ID - solves missing text issue
|
||||
var desiredTraderId = traderId == Traders.REF ? Traders.PRAPOR : traderId;
|
||||
|
||||
/* in locale, these id correspond to the text of quests
|
||||
template ids -pmc : Elimination = 616052ea3054fc0e2c24ce6e / Completion = 61604635c725987e815b1a46 / Exploration = 616041eb031af660100c9967
|
||||
template ids -scav : Elimination = 62825ef60e88d037dc1eb428 / Completion = 628f588ebb558574b2260fe5 / Exploration = 62825ef60e88d037dc1eb42c
|
||||
*/
|
||||
|
||||
questData.Name = questData
|
||||
.Name.Replace("{traderId}", traderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.Note = questData
|
||||
.Note?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.Description = questData
|
||||
.Description.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.SuccessMessageText = questData
|
||||
.SuccessMessageText?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.FailMessageText = questData
|
||||
.FailMessageText?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.StartedMessageText = questData
|
||||
.StartedMessageText?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.ChangeQuestMessageText = questData
|
||||
.ChangeQuestMessageText?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.AcceptPlayerMessage = questData
|
||||
.AcceptPlayerMessage?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.DeclinePlayerMessage = questData
|
||||
.DeclinePlayerMessage?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
questData.CompletePlayerMessage = questData
|
||||
.CompletePlayerMessage?.Replace("{traderId}", desiredTraderId)
|
||||
.Replace("{templateId}", questData.TemplateId);
|
||||
|
||||
if (questData.QuestStatus is null)
|
||||
{
|
||||
// TODO: Localize me!
|
||||
logger.Error($"No quest status found for type {type}");
|
||||
return null;
|
||||
}
|
||||
|
||||
questData.QuestStatus.Id = hashUtil.Generate();
|
||||
questData.QuestStatus.Uid = sessionId; // Needs to match user id
|
||||
questData.QuestStatus.QId = questData.Id; // Needs to match quest id
|
||||
|
||||
return questData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using SPTarkov.Server.Core.Utils.Json.Converters;
|
||||
|
||||
namespace SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
[EftEnumConverter]
|
||||
public enum RepeatableQuestType
|
||||
{
|
||||
Elimination,
|
||||
Completion,
|
||||
Exploration,
|
||||
Pickup
|
||||
}
|
||||
Reference in New Issue
Block a user