Make each trader reward different types of items

Prevent trader from rewarding more than 1 weapon
Fixed trader failing to find default weapon to send as reward
This commit is contained in:
Dev 2023-11-14 23:05:34 +00:00
parent cd36e3993d
commit c9dc0d2192
4 changed files with 230 additions and 53 deletions

View File

@ -166,25 +166,98 @@
},
"traderWhitelist": [{
"traderId": "54cb50c76803fa8b248b4571",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "prapor",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"543be6564bdc2df4348b4568",
"5485a8684bdc2da71d8b4567",
"590c745b86f7743cc433c5f2",
"5422acb9af1c889c16000029"
]
}, {
"traderId": "54cb57776803fa99248b456e",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "therapist",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"57864a66245977548f04a81f",
"5448f39d4bdc2d0a728b4568",
"5448f3ac4bdc2dce718b4569",
"5448f3a64bdc2d60728b456a",
"57864c322459775490116fbf",
"57864c8c245977548867e7f1",
"5448e8d04bdc2ddf718b4569",
"57864e4c24597754843f8723",
"57864ee62459775490116fc1"
]
}, {
"traderId": "58330581ace78e27b8b10cee",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "skier",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5a341c4086f77401f2541505",
"5448e8d64bdc2dce718b4568",
"5448e8d04bdc2ddf718b4569",
"5422acb9af1c889c16000029",
"55818ad54bdc2ddc698b4569",
"57864a3d24597754843f8721",
"5a341c4686f77469e155819e",
"55818b224bdc2dde698b456f",
"5c99f98d86f7745c314214b3",
"55818aeb4bdc2ddc698b456a",
"55818acf4bdc2dde698b456b",
"57864bb7245977548b3b66c2",
"590c745b86f7743cc433c5f2"
]
}, {
"traderId": "5935c25fb3acc3127c3d8cd9",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "peacekeeper",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5422acb9af1c889c16000029",
"543be6564bdc2df4348b4568",
"5448e5284bdc2dcb718b4567",
"5485a8684bdc2da71d8b4567",
"57864a3d24597754843f8721",
"55818af64bdc2d5b648b4570"
]
}, {
"traderId": "5a7c2eca46aef81a7ca2145d",
"questTypes": ["Completion", "Exploration"]
"name": "mechanic",
"questTypes": ["Completion", "Exploration"],
"rewardBaseWhitelist": [
"55818af64bdc2d5b648b4570",
"5448bc234bdc2d3c308b4569",
"55818b164bdc2ddc698b456c",
"55818a684bdc2ddd698b456d",
"550aa4cd4bdc2dd8348b456c",
"5422acb9af1c889c16000029",
"5485a8684bdc2da71d8b4567",
"55818b224bdc2dde698b456f"
]
}, {
"traderId": "5ac3b934156ae10c4430e83c",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "ragman",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5a341c4086f77401f2541505",
"5448e5724bdc2ddf718b4568",
"5448e54d4bdc2dcc718b4568",
"590c745b86f7743cc433c5f2",
"57bef4c42459772e8d35a53b",
"5448e53e4bdc2d60728b4567",
"5448e5284bdc2dcb718b4567",
"57864a66245977548f04a81f"
]
}, {
"traderId": "5c0647fdd443bc2504c2d371",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "jaeger",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5448f3ac4bdc2dce718b4569",
"5c99f98d86f7745c314214b3",
"590c745b86f7743cc433c5f2",
"57864bb7245977548b3b66c2"
]
}
],
"questConfig": {
@ -692,7 +765,7 @@
}
]
},
"rewardBaseTypeBlacklist": ["543be5e94bdc2df1348b4568", "5b3f15d486f77432d0509248"],
"rewardBaseTypeBlacklist": ["543be5e94bdc2df1348b4568", "5b3f15d486f77432d0509248", "59f32c3b86f77472a31742f0", "59f32bb586f774757e1e8442"],
"rewardBlacklist": ["627bce33f21bc425b06ab967"],
"rewardAmmoStackMinSize": 20
}, {
@ -731,25 +804,98 @@
},
"traderWhitelist": [{
"traderId": "54cb50c76803fa8b248b4571",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "prapor",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"543be6564bdc2df4348b4568",
"5485a8684bdc2da71d8b4567",
"590c745b86f7743cc433c5f2",
"5422acb9af1c889c16000029"
]
}, {
"traderId": "54cb57776803fa99248b456e",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "therapist",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"57864a66245977548f04a81f",
"5448f39d4bdc2d0a728b4568",
"5448f3ac4bdc2dce718b4569",
"5448f3a64bdc2d60728b456a",
"57864c322459775490116fbf",
"57864c8c245977548867e7f1",
"5448e8d04bdc2ddf718b4569",
"57864e4c24597754843f8723",
"57864ee62459775490116fc1"
]
}, {
"traderId": "58330581ace78e27b8b10cee",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "skier",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5a341c4086f77401f2541505",
"5448e8d64bdc2dce718b4568",
"5448e8d04bdc2ddf718b4569",
"5422acb9af1c889c16000029",
"55818ad54bdc2ddc698b4569",
"57864a3d24597754843f8721",
"5a341c4686f77469e155819e",
"55818b224bdc2dde698b456f",
"5c99f98d86f7745c314214b3",
"55818aeb4bdc2ddc698b456a",
"55818acf4bdc2dde698b456b",
"57864bb7245977548b3b66c2",
"590c745b86f7743cc433c5f2"
]
}, {
"traderId": "5935c25fb3acc3127c3d8cd9",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "peacekeeper",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5422acb9af1c889c16000029",
"543be6564bdc2df4348b4568",
"5448e5284bdc2dcb718b4567",
"5485a8684bdc2da71d8b4567",
"57864a3d24597754843f8721",
"55818af64bdc2d5b648b4570"
]
}, {
"traderId": "5a7c2eca46aef81a7ca2145d",
"questTypes": ["Completion", "Exploration"]
"name": "mechanic",
"questTypes": ["Completion", "Exploration"],
"rewardBaseWhitelist": [
"55818af64bdc2d5b648b4570",
"5448bc234bdc2d3c308b4569",
"55818b164bdc2ddc698b456c",
"55818a684bdc2ddd698b456d",
"550aa4cd4bdc2dd8348b456c",
"5422acb9af1c889c16000029",
"5485a8684bdc2da71d8b4567",
"55818b224bdc2dde698b456f"
]
}, {
"traderId": "5ac3b934156ae10c4430e83c",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "ragman",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5a341c4086f77401f2541505",
"5448e5724bdc2ddf718b4568",
"5448e54d4bdc2dcc718b4568",
"590c745b86f7743cc433c5f2",
"57bef4c42459772e8d35a53b",
"5448e53e4bdc2d60728b4567",
"5448e5284bdc2dcb718b4567",
"57864a66245977548f04a81f"
]
}, {
"traderId": "5c0647fdd443bc2504c2d371",
"questTypes": ["Completion", "Exploration", "Elimination"]
"name": "jaeger",
"questTypes": ["Completion", "Exploration", "Elimination"],
"rewardBaseWhitelist": [
"5448f3ac4bdc2dce718b4569",
"5c99f98d86f7745c314214b3",
"590c745b86f7743cc433c5f2",
"57864bb7245977548b3b66c2"
]
}
],
"questConfig": {
@ -1323,7 +1469,7 @@
}
]
},
"rewardBaseTypeBlacklist": ["543be5e94bdc2df1348b4568", "5b3f15d486f77432d0509248"],
"rewardBaseTypeBlacklist": ["543be5e94bdc2df1348b4568", "5b3f15d486f77432d0509248", "59f32c3b86f77472a31742f0", "59f32bb586f774757e1e8442"],
"rewardBlacklist": ["627bce33f21bc425b06ab967"],
"rewardAmmoStackMinSize": 15
}, {
@ -1362,7 +1508,15 @@
},
"traderWhitelist": [{
"traderId": "579dc571d53a0658a154fbec",
"questTypes": ["Completion", "Exploration", "Elimination", "Pickup"]
"questTypes": ["Completion", "Exploration", "Elimination", "Pickup"],
"rewardBaseWhitelist": [
"55818a684bdc2ddd698b456d",
"55818a594bdc2db9688b456a",
"57864c8c245977548867e7f1",
"5448ecbe4bdc2d60728b4568",
"5422acb9af1c889c16000029",
"57864bb7245977548b3b66c2"
]
}
],
"questConfig": {

View File

@ -533,7 +533,7 @@ export class InsuranceController
for (const key of body.items)
{
itemsToPay.push({
id: this.roubleTpl, // TODO: update to handle difference currencies
id: this.roubleTpl, // TODO: update to handle different currencies
count: Math.round(this.insuranceService.getPremium(pmcData, inventoryItemsHash[key], body.tid))
});
}

View File

@ -8,6 +8,7 @@ import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper";
import { RepeatableQuestHelper } from "@spt-aki/helpers/RepeatableQuestHelper";
import { Exit, ILocationBase } from "@spt-aki/models/eft/common/ILocationBase";
import { TraderInfo } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import {
ICompletion,
ICompletionAvailableFor,
@ -449,7 +450,7 @@ export class RepeatableQuestGenerator
const levelsConfig = repeatableConfig.rewardScaling.levels;
const roublesConfig = repeatableConfig.rewardScaling.roubles;
// in the available dumps only 2 distinct items were ever requested
// In the available dumps only 2 distinct items were ever requested
let numberDistinctItems = 1;
if (Math.random() > 0.75)
{
@ -458,13 +459,13 @@ export class RepeatableQuestGenerator
const quest = this.generateRepeatableTemplate("Completion", traderId,repeatableConfig.side) as ICompletion;
// Filter the items.json items to items the player must retrieve to complete queist: shouldn't be a quest item or "non-existant"
let itemSelection = this.getRewardableItems(repeatableConfig);
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant"
const possibleItemsToRetrievePool = this.getRewardableItems(repeatableConfig, traderId);
// Be fair, don't let the items be more expensive than the reward
let roublesBudget = Math.floor(this.mathUtil.interp1(pmcLevel, levelsConfig, roublesConfig) * this.randomUtil.getFloat(0.5, 1));
roublesBudget = Math.max(roublesBudget, 5000);
itemSelection = itemSelection.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget);
let itemSelection = possibleItemsToRetrievePool.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget);
// 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",...]}]
@ -796,8 +797,9 @@ export class RepeatableQuestGenerator
// Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink)
let roublesBudget = rewardRoubles;
let chosenRewardItems = this.chooseRewardItemsWithinBudget(repeatableConfig, roublesBudget);
let rewardItemPool = this.chooseRewardItemsWithinBudget(repeatableConfig, roublesBudget, traderId);
// Add xp reward
const rewards: IRewards = {
Started: [],
Success: [
@ -810,6 +812,7 @@ export class RepeatableQuestGenerator
Fail: []
};
// Add money reward
if (traderId === Traders.PEACEKEEPER)
{
// convert to equivalent dollars
@ -821,13 +824,14 @@ export class RepeatableQuestGenerator
}
let index = 2;
if (chosenRewardItems.length > 0)
if (rewardItemPool.length > 0)
{
let weaponRewardCount = 0;
for (let i = 0; i < rewardNumItems; i++)
{
let value = 1;
let children = null;
const itemSelected = chosenRewardItems[this.randomUtil.randInt(chosenRewardItems.length)];
let itemCount = 1;
let children: Item[] = null;
const itemSelected = rewardItemPool[this.randomUtil.randInt(rewardItemPool.length)];
if (this.itemHelper.isOfBaseclass(itemSelected._id, BaseClasses.AMMO))
{
// Dont reward ammo that stacks to less than what's defined in config
@ -837,10 +841,16 @@ export class RepeatableQuestGenerator
}
// Randomise the cartridge count returned
value = this.randomUtil.randInt(repeatableConfig.rewardAmmoStackMinSize, itemSelected._props.StackMaxSize);
itemCount = this.randomUtil.randInt(repeatableConfig.rewardAmmoStackMinSize, itemSelected._props.StackMaxSize);
}
else if (this.itemHelper.isOfBaseclass(itemSelected._id, BaseClasses.WEAPON))
{
if (weaponRewardCount >= 1)
{
// Limit weapon rewards to 1 per daily
rewardItemPool = rewardItemPool.filter(x => !this.itemHelper.isOfBaseclass(x._id, BaseClasses.WEAPON));
continue;
}
let defaultPreset = this.presetHelper.getDefaultPreset(itemSelected._id);
if (!defaultPreset)
{
@ -850,21 +860,21 @@ export class RepeatableQuestGenerator
}
children = this.ragfairServerHelper.reparentPresets(defaultPreset._items[0], defaultPreset._items);
}
rewards.Success.push(this.generateRewardItem(itemSelected._id, value, index, children));
weaponRewardCount ++;
// TODO: maybe also non-default use ragfair to calculate the price
// TODO: maybe also non-default use ragfair to calculate the price
// this.ragfairServer.getWeaponPresetPrice(item, items, existingPrice)
roublesBudget -= value * this.itemHelper.getStaticItemPrice(itemSelected._id);
}
rewards.Success.push(this.generateRewardItem(itemSelected._id, itemCount, index, children));
roublesBudget -= itemCount * this.itemHelper.getStaticItemPrice(itemSelected._id);
index += 1;
// if we still have budget narrow down the items
if (roublesBudget > 0)
{
// Filter possible reward items to only items with a price below the remaining budget
chosenRewardItems = chosenRewardItems.filter(x => this.itemHelper.getStaticItemPrice(x._id) < roublesBudget);
if (chosenRewardItems.length === 0)
rewardItemPool = rewardItemPool.filter(x => this.itemHelper.getStaticItemPrice(x._id) < roublesBudget);
if (rewardItemPool.length === 0)
{
break; // No reward items left, exit
}
@ -909,20 +919,21 @@ export class RepeatableQuestGenerator
* @param roublesBudget Total value of items to return
* @returns Array of reward items that fit budget
*/
protected chooseRewardItemsWithinBudget(repeatableConfig: IRepeatableQuestConfig, roublesBudget: number): ITemplateItem[]
protected chooseRewardItemsWithinBudget(repeatableConfig: IRepeatableQuestConfig, roublesBudget: number, traderId: string): ITemplateItem[]
{
// First filter for type and baseclass to avoid lookup in handbook for non-available items
const rewardableItems = this.getRewardableItems(repeatableConfig);
const rewardableItemPool = this.getRewardableItems(repeatableConfig, traderId);
const minPrice = Math.min(25000, 0.5 * roublesBudget);
let itemSelection = rewardableItems.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget && this.itemHelper.getItemPrice(x[0]) > minPrice).map(x => x[1]);
if (itemSelection.length === 0)
let rewardableItemPoolWithinBudget = rewardableItemPool.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget && this.itemHelper.getItemPrice(x[0]) > minPrice).map(x => x[1]);
if (rewardableItemPoolWithinBudget.length === 0)
{
this.logger.warning(this.localisationService.getText("repeatable-no_reward_item_found_in_price_range", {minPrice: minPrice, roublesBudget: roublesBudget}));
// In case we don't find any items in the price range
itemSelection = rewardableItems.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget).map(x => x[1]);
rewardableItemPoolWithinBudget = rewardableItemPool.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget).map(x => x[1]);
}
return itemSelection;
return rewardableItemPoolWithinBudget;
}
/**
@ -968,7 +979,7 @@ export class RepeatableQuestGenerator
* @param repeatableQuestConfig Config file
* @returns List of rewardable items [[_tpl, itemTemplate],...]
*/
protected getRewardableItems(repeatableQuestConfig: IRepeatableQuestConfig): [string, ITemplateItem][]
protected getRewardableItems(repeatableQuestConfig: IRepeatableQuestConfig, traderId: string): [string, ITemplateItem][]
{
// 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
@ -983,7 +994,8 @@ export class RepeatableQuestGenerator
return false;
}
return this.isValidRewardItem(tpl, repeatableQuestConfig);
const traderWhitelist = repeatableQuestConfig.traderWhitelist.find(x => x.traderId === traderId);
return this.isValidRewardItem(tpl, repeatableQuestConfig, traderWhitelist?.rewardBaseWhitelist);
}
);
}
@ -994,12 +1006,17 @@ export class RepeatableQuestGenerator
* @param {string} tpl template id of item to check
* @returns True if item is valid reward
*/
protected isValidRewardItem(tpl: string, repeatableQuestConfig: IRepeatableQuestConfig): boolean
protected isValidRewardItem(tpl: string, repeatableQuestConfig: IRepeatableQuestConfig, itemBaseWhitelist: string[]): boolean
{
let valid = this.itemHelper.isValidItem(tpl);
if (!valid)
if (!this.itemHelper.isValidItem(tpl))
{
return valid;
return false;
}
// Check global blacklist
if (this.itemFilterService.isItemBlacklisted(tpl))
{
return false;
}
// Item is on repeatable or global blacklist
@ -1015,17 +1032,22 @@ export class RepeatableQuestGenerator
return false;
}
if (this.itemHelper.isOfBaseclasses(tpl, [BaseClasses.DOG_TAG_USEC, BaseClasses.DOG_TAG_BEAR, BaseClasses.MOUNT, BaseClasses.KEY, BaseClasses.ARMBAND]))
// Skip boss items
if (this.itemFilterService.isBossItem(tpl))
{
return false;
}
// Skip globally blacklisted items + boss items
// rome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
valid = !this.itemFilterService.isItemBlacklisted(tpl)
&& !this.itemFilterService.isBossItem(tpl);
// Trader has specific item base types they can give as rewards to player
if (itemBaseWhitelist !== undefined)
{
if (!this.itemHelper.isOfBaseclasses(tpl, [...itemBaseWhitelist]))
{
return false;
}
}
return valid;
return true;
}
/**

View File

@ -76,6 +76,7 @@ export interface ITraderWhitelist
{
traderId: string
questTypes: string[]
rewardBaseWhitelist: string[]
}
export interface IRepeatableQuestTypesConfig