From c9dc0d2192154f7a7eff144913e6376909687042 Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 14 Nov 2023 23:05:34 +0000 Subject: [PATCH] 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 --- project/assets/configs/quest.json | 188 ++++++++++++++++-- .../src/controllers/InsuranceController.ts | 2 +- .../generators/RepeatableQuestGenerator.ts | 92 +++++---- project/src/models/spt/config/IQuestConfig.ts | 1 + 4 files changed, 230 insertions(+), 53 deletions(-) diff --git a/project/assets/configs/quest.json b/project/assets/configs/quest.json index c4083a01..d98fd72a 100644 --- a/project/assets/configs/quest.json +++ b/project/assets/configs/quest.json @@ -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": { diff --git a/project/src/controllers/InsuranceController.ts b/project/src/controllers/InsuranceController.ts index 9ba20416..d6ab3634 100644 --- a/project/src/controllers/InsuranceController.ts +++ b/project/src/controllers/InsuranceController.ts @@ -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)) }); } diff --git a/project/src/generators/RepeatableQuestGenerator.ts b/project/src/generators/RepeatableQuestGenerator.ts index 07439862..e26c6208 100644 --- a/project/src/generators/RepeatableQuestGenerator.ts +++ b/project/src/generators/RepeatableQuestGenerator.ts @@ -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: - 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; } /** diff --git a/project/src/models/spt/config/IQuestConfig.ts b/project/src/models/spt/config/IQuestConfig.ts index 34753847..1c157684 100644 --- a/project/src/models/spt/config/IQuestConfig.ts +++ b/project/src/models/spt/config/IQuestConfig.ts @@ -76,6 +76,7 @@ export interface ITraderWhitelist { traderId: string questTypes: string[] + rewardBaseWhitelist: string[] } export interface IRepeatableQuestTypesConfig