Repeatable quest improvements

This commit is contained in:
Chomp
2025-01-23 10:58:47 +00:00
parent 792cf3a52c
commit f09c6f6088
5 changed files with 175 additions and 29 deletions
@@ -497,7 +497,7 @@ public class RepeatableQuestGenerator(
(double)(_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi));
roublesBudget = Math.Max(roublesBudget, 5000d);
var itemSelection = possibleItemsToRetrievePool.Where(
(x) => _itemHelper.GetItemPrice(x.Key) < 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",...]}]
@@ -511,8 +511,8 @@ public class RepeatableQuestGenerator(
itemSelection = itemSelection.Where((x) => {
// Whitelist can contain item tpls and item base type ids
return (
itemIdsWhitelisted.Any((v) => _itemHelper.IsOfBaseclass(x.Key, v)) ||
itemIdsWhitelisted.Contains(x.Key)
itemIdsWhitelisted.Any((v) => _itemHelper.IsOfBaseclass(x.Id, v)) ||
itemIdsWhitelisted.Contains(x.Id)
);
}).ToList();
// check if items are missing
@@ -530,8 +530,8 @@ public class RepeatableQuestGenerator(
itemSelection = itemSelection.Where((x) => {
return (
itemIdsBlacklisted.All((v) => !_itemHelper.IsOfBaseclass(x.Key, v)) ||
!itemIdsBlacklisted.Contains(x.Key)
itemIdsBlacklisted.All((v) => !_itemHelper.IsOfBaseclass(x.Id, v)) ||
!itemIdsBlacklisted.Contains(x.Id)
);
}).ToList();
}
@@ -572,10 +572,10 @@ public class RepeatableQuestGenerator(
usedItemIndexes.Add(chosenItemIndex);
var itemSelected = itemSelection[chosenItemIndex];
var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Key).Value;
var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Id).Value;
var minValue = (double)completionConfig.MinimumRequestedAmount.Value;
var maxValue = (double)completionConfig.MaximumRequestedAmount.Value;
if (_itemHelper.IsOfBaseclass(itemSelected.Key, BaseClasses.AMMO)) {
if (_itemHelper.IsOfBaseclass(itemSelected.Id, BaseClasses.AMMO)) {
// Prevent multiple ammo requirements from being picked
if (isAmmo > 0 && isAmmo < _maxRandomNumberAttempts) {
isAmmo++;
@@ -600,12 +600,12 @@ public class RepeatableQuestGenerator(
roublesBudget -= value * itemUnitPrice;
// Push a CompletionCondition with the item and the amount of the item
chosenRequirementItemsTpls.Add(itemSelected.Key);
quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Key, value));
chosenRequirementItemsTpls.Add(itemSelected.Id);
quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Id, value));
if (roublesBudget > 0) {
// Reduce the list possible items to fulfill the new budget constraint
itemSelection = itemSelection.Where((dbItem) => _itemHelper.GetItemPrice(dbItem.Key) < roublesBudget).ToList();
itemSelection = itemSelection.Where((dbItem) => _itemHelper.GetItemPrice(dbItem.Id) < roublesBudget).ToList();
if (!itemSelection.Any()) {
break;
}
@@ -805,7 +805,7 @@ public class RepeatableQuestGenerator(
string side,
string sessionId)
{
Quest questData = null;
RepeatableQuest questData = null;
switch (type)
{
case "Elimination":
@@ -889,6 +889,6 @@ public class RepeatableQuestGenerator(
questClone.QuestStatus.Uid = sessionId; // Needs to match user id
questClone.QuestStatus.QId = questClone.Id; // Needs to match quest id
return (RepeatableQuest)questClone;
return questClone;
}
}
@@ -1,6 +1,7 @@
using SptCommon.Annotations;
using Core.Helpers;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Player;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Spt.Repeatable;
@@ -9,6 +10,7 @@ using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using System.Linq;
namespace Core.Generators;
@@ -69,7 +71,7 @@ public class RepeatableQuestRewardGenerator(
// Get budget to spend on item rewards (copy of raw roubles given)
var itemRewardBudget = rewardParams.RewardRoubles;
// Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink)
// Possible improvement -> draw trader-specific items e.g. with _itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink)
QuestRewards rewards = new() { Started = [], Success = [], Fail = [] };
// Start reward index to keep track
@@ -94,11 +96,11 @@ public class RepeatableQuestRewardGenerator(
}
// Add money reward
rewards.Success.Add(GetMoneyReward(traderId, rewardParams.RewardRoubles, rewardIndex));
rewards.Success.Add(GetMoneyReward(traderId, rewardParams.RewardRoubles.Value, rewardIndex));
rewardIndex++;
// Add GP coin reward
rewards.Success.Add(GenerateItemReward(Money.GP, rewardParams.GpCoinRewardCount, rewardIndex));
rewards.Success.Add(GenerateItemReward(Money.GP, rewardParams.GpCoinRewardCount.Value, rewardIndex));
rewardIndex++;
// Add preset weapon to reward if checks pass
@@ -283,9 +285,38 @@ public class RepeatableQuestRewardGenerator(
throw new NotImplementedException();
}
private List<TemplateItem> ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig, double? itemRewardBudget, string traderId)
private List<TemplateItem> ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig, double? roublesBudget, string traderId)
{
throw new NotImplementedException();
// First filter for type and baseclass to avoid lookup in handbook for non-available items
var rewardableItemPool = GetRewardableItems(repeatableConfig, traderId);
var minPrice = Math.Min(25000, 0.5 * roublesBudget.Value);
var rewardableItemPoolWithinBudget = FilterRewardPoolWithinBudget(
rewardableItemPool,
roublesBudget.Value,
minPrice);
if (rewardableItemPoolWithinBudget.Count == 0)
{
_logger.Warning(_localisationService.GetText("repeatable-no_reward_item_found_in_price_range", new {
minPrice = minPrice,
roublesBudget = roublesBudget }));
// In case we don't find any items in the price range
rewardableItemPoolWithinBudget = rewardableItemPool
.Where((x) => _itemHelper.GetItemPrice(x.Id) < roublesBudget)
.ToList();
}
return rewardableItemPoolWithinBudget;
}
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();
}
private KeyValuePair<Reward, double>? GetRandomWeaponPresetWithinBudget(double? itemRewardBudget, double rewardIndex)
@@ -293,19 +324,109 @@ public class RepeatableQuestRewardGenerator(
throw new NotImplementedException();
}
private Reward GenerateItemReward(string d235b4d86f7742e017bc88a, object gpCoinRewardCount, double rewardIndex)
private Reward GenerateItemReward(string tpl, double count, int index, bool foundInRaid = true)
{
throw new NotImplementedException();
var id = _hashUtil.Generate();
var questRewardItem = new Reward{
Id = _hashUtil.Generate(),
Unknown = false,
GameMode = [],
AvailableInGameEditions = [],
Index = index,
Target = id,
Value = count,
IsEncoded = false,
FindInRaid = foundInRaid,
Type = RewardType.Item,
Items = [],
};
var rootItem = new Item { Id = id, Template = tpl, Upd = new Upd { StackObjectsCount = count, SpawnedInSession = foundInRaid }
};
questRewardItem.Items = [rootItem];
return questRewardItem;
}
private Reward GetMoneyReward(string traderId, object rewardRoubles, double rewardIndex)
private Reward GetMoneyReward(string traderId, double rewardRoubles, int rewardIndex)
{
throw new NotImplementedException();
// Determine currency based on trader
// PK and Fence use Euros, everyone else is Roubles
var currency = traderId is Traders.PEACEKEEPER or Traders.FENCE ? Money.EUROS : Money.ROUBLES;
// Convert reward amount to Euros if necessary
var rewardAmountToGivePlayer =
currency == Money.EUROS ? _handbookHelper.FromRUB(rewardRoubles, Money.EUROS) : rewardRoubles;
// Get chosen currency + amount and return
return GenerateItemReward(currency, rewardAmountToGivePlayer, rewardIndex, false);
}
public Dictionary<string, List<TemplateItem>> GetRewardableItems(RepeatableQuestConfig repeatableConfig, string traderId)
public List<TemplateItem> GetRewardableItems(RepeatableQuestConfig repeatableQuestConfig, string traderId)
{
throw new NotImplementedException();
// Get an array of seasonal items that should not be shown right now as seasonal event is not active
var seasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems();
// 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;
}
if (seasonalItems.Contains(itemTemplate.Id))
{
return false;
}
var traderWhitelist = repeatableQuestConfig.TraderWhitelist.FirstOrDefault(
(trader) => trader.TraderId == traderId);
return IsValidRewardItem(itemTemplate.Id, repeatableQuestConfig, traderWhitelist?.RewardBaseWhitelist);
})).ToList();
}
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))
{
return false;
}
// Check item is not blacklisted
if (
_itemFilterService.IsItemBlacklisted(tpl) ||
_itemFilterService.IsItemRewardBlacklisted(tpl) ||
repeatableQuestConfig.RewardBlacklist.Contains(tpl) ||
_itemFilterService.IsItemBlacklisted(tpl)
)
{
return false;
}
// Item has blacklisted base types
if (_itemHelper.IsOfBaseclasses(tpl, repeatableQuestConfig.RewardBaseTypeBlacklist ))
{
return false;
}
// Skip boss items
if (_itemFilterService.IsBossItem(tpl))
{
return false;
}
// Trader has specific item base types they can give as rewards to player
if (itemBaseWhitelist is not null && !_itemHelper.IsOfBaseclasses(tpl, itemBaseWhitelist))
{
return false;
}
return true;
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
using SptCommon.Annotations;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Enums;
using Core.Services;
@@ -171,7 +171,7 @@ public class PresetHelper(
* @param tpl The item template to get the price of
* @returns The price of the given item preset, or base item if no preset exists
*/
public decimal GetDefaultPresetOrItemPrice(string tpl)
public double GetDefaultPresetOrItemPrice(string tpl)
{
// Get default preset if it exists
var defaultPreset = GetDefaultPreset(tpl);
@@ -71,16 +71,16 @@ public record RepeatableQuestStatus
public record RepeatableTemplates
{
[JsonPropertyName("Elimination")]
public Quest? Elimination { get; set; }
public RepeatableQuest? Elimination { get; set; }
[JsonPropertyName("Completion")]
public Quest? Completion { get; set; }
public RepeatableQuest? Completion { get; set; }
[JsonPropertyName("Exploration")]
public Quest? Exploration { get; set; }
public RepeatableQuest? Exploration { get; set; }
[JsonPropertyName("Pickup")]
public Quest? Pickup { get; set; }
public RepeatableQuest? Pickup { get; set; }
}
public record PmcDataRepeatableQuest
@@ -104,6 +104,11 @@ public class ItemFilterService(
throw new NotImplementedException();
}
/**
* Check if the provided template id is blacklisted in config/item.json/lootableItemBlacklist
* @param tpl template id
* @returns true if blacklisted
*/
public bool IsLootableItemBlacklisted(string itemKey)
{
if (_lootableItemBlacklistCache is null)
@@ -140,4 +145,24 @@ public class ItemFilterService(
_itemBlacklistCache.Add(item);
}
}
/**
* Check if the provided template id is boss item in config/item.json
* @param tpl template id
* @returns true if boss item
*/
public bool IsBossItem(string tpl)
{
return _itemConfig.BossItems.Contains(tpl);
}
/**
* Check if item is blacklisted from being a reward for player
* @param tpl item tpl to check is on blacklist
* @returns True when blacklisted
*/
public bool IsItemRewardBlacklisted(string tpl)
{
return _itemConfig.RewardItemBlacklist.Contains(tpl);
}
}