2024-03-10 23:15:03 +00:00
|
|
|
import { inject, injectable } from "tsyringe";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { HandbookHelper } from "@spt/helpers/HandbookHelper";
|
|
|
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
|
|
|
import { PresetHelper } from "@spt/helpers/PresetHelper";
|
|
|
|
import { Item } from "@spt/models/eft/common/tables/IItem";
|
|
|
|
import { IQuestReward, IQuestRewards } from "@spt/models/eft/common/tables/IQuest";
|
|
|
|
import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
|
|
|
|
import { BaseClasses } from "@spt/models/enums/BaseClasses";
|
|
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
|
|
|
import { Money } from "@spt/models/enums/Money";
|
|
|
|
import { QuestRewardType } from "@spt/models/enums/QuestRewardType";
|
|
|
|
import { Traders } from "@spt/models/enums/Traders";
|
2024-06-07 20:19:58 +01:00
|
|
|
import { IBaseQuestConfig, IQuestConfig, IRepeatableQuestConfig, IRewardScaling } from "@spt/models/spt/config/IQuestConfig";
|
|
|
|
import { IQuestRewardValues } from "@spt/models/spt/repeatable/IQuestRewardValues";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { ExhaustableArray } from "@spt/models/spt/server/ExhaustableArray";
|
|
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
2024-05-28 22:24:52 +01:00
|
|
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { ItemFilterService } from "@spt/services/ItemFilterService";
|
|
|
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
|
|
|
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
|
|
|
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
|
|
|
import { MathUtil } from "@spt/utils/MathUtil";
|
|
|
|
import { ObjectId } from "@spt/utils/ObjectId";
|
|
|
|
import { RandomUtil } from "@spt/utils/RandomUtil";
|
2024-03-10 23:15:03 +00:00
|
|
|
|
|
|
|
@injectable()
|
|
|
|
export class RepeatableQuestRewardGenerator
|
|
|
|
{
|
|
|
|
protected questConfig: IQuestConfig;
|
|
|
|
|
|
|
|
constructor(
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
2024-03-10 23:15:03 +00:00
|
|
|
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
|
|
|
@inject("MathUtil") protected mathUtil: MathUtil,
|
2024-05-28 22:24:52 +01:00
|
|
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
2024-03-10 23:15:03 +00:00
|
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
|
|
|
@inject("PresetHelper") protected presetHelper: PresetHelper,
|
|
|
|
@inject("HandbookHelper") protected handbookHelper: HandbookHelper,
|
|
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
|
|
@inject("ObjectId") protected objectId: ObjectId,
|
|
|
|
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
|
|
|
|
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
|
|
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
2024-03-10 23:15:03 +00:00
|
|
|
)
|
|
|
|
{
|
|
|
|
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate the reward for a mission. A reward can consist of
|
|
|
|
* - Experience
|
|
|
|
* - Money
|
|
|
|
* - Items
|
|
|
|
* - Trader Reputation
|
|
|
|
*
|
|
|
|
* 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 {integer} pmcLevel player's level
|
|
|
|
* @param {number} difficulty a reward scaling factor from 0.2 to 1
|
|
|
|
* @param {string} traderId the trader for reputation gain (and possible in the future filtering of reward item type based on trader)
|
|
|
|
* @param {object} repeatableConfig The configuration for the repeatable kind (daily, weekly) as configured in QuestConfig for the requested quest
|
|
|
|
* @returns {object} object of "Reward"-type that can be given for a repeatable mission
|
|
|
|
*/
|
|
|
|
public generateReward(
|
|
|
|
pmcLevel: number,
|
|
|
|
difficulty: number,
|
|
|
|
traderId: string,
|
|
|
|
repeatableConfig: IRepeatableQuestConfig,
|
|
|
|
questConfig: IBaseQuestConfig,
|
|
|
|
): IQuestRewards
|
|
|
|
{
|
2024-06-07 20:19:58 +01:00
|
|
|
// Get vars to configure rewards with
|
|
|
|
const rewardParams = this.getQuestRewardValues(repeatableConfig.rewardScaling, difficulty, pmcLevel);
|
2024-03-10 23:15:03 +00:00
|
|
|
|
2024-06-07 20:19:58 +01:00
|
|
|
// Get budget to spend on item rewards (copy of raw roubles given)
|
|
|
|
let itemRewardBudget = rewardParams.rewardRoubles;
|
2024-03-10 23:15:03 +00:00
|
|
|
|
|
|
|
// Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink)
|
|
|
|
const rewards: IQuestRewards = { Started: [], Success: [], Fail: [] };
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Start reward index to keep track
|
2024-03-10 23:15:03 +00:00
|
|
|
let rewardIndex = 0;
|
2024-06-07 18:25:27 +00:00
|
|
|
|
2024-03-10 23:15:03 +00:00
|
|
|
// Add xp reward
|
2024-06-07 20:19:58 +01:00
|
|
|
if (rewardParams.rewardXP > 0)
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-06-07 20:19:58 +01:00
|
|
|
rewards.Success.push({ value: rewardParams.rewardXP, type: QuestRewardType.EXPERIENCE, index: rewardIndex });
|
2024-03-10 23:15:03 +00:00
|
|
|
rewardIndex++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add money reward
|
2024-06-07 20:19:58 +01:00
|
|
|
rewards.Success.push(this.getMoneyReward(traderId, rewardParams.rewardRoubles, rewardIndex));
|
2024-03-10 23:15:03 +00:00
|
|
|
rewardIndex++;
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Add GP coin reward
|
|
|
|
rewards.Success.push(this.generateRewardItem(
|
|
|
|
Money.GP,
|
2024-06-07 20:19:58 +01:00
|
|
|
rewardParams.gpCoinRewardCount,
|
2024-06-07 18:25:27 +00:00
|
|
|
rewardIndex,
|
|
|
|
));
|
|
|
|
rewardIndex++;
|
|
|
|
|
|
|
|
// Add preset weapon to reward if checks pass
|
|
|
|
const traderWhitelistDetails = repeatableConfig.traderWhitelist
|
|
|
|
.find((traderWhitelist) => traderWhitelist.traderId === traderId);
|
2024-03-10 23:15:03 +00:00
|
|
|
if (
|
2024-06-07 18:25:27 +00:00
|
|
|
traderWhitelistDetails?.rewardCanBeWeapon
|
2024-03-10 23:15:03 +00:00
|
|
|
&& this.randomUtil.getChance100(traderWhitelistDetails.weaponRewardChancePercent)
|
|
|
|
)
|
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
const chosenWeapon = this.getRandomWeaponPresetWithinBudget(itemRewardBudget, rewardIndex);
|
|
|
|
if (chosenWeapon)
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
rewards.Success.push(chosenWeapon.weapon);
|
2024-03-10 23:15:03 +00:00
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Subtract price of preset from item budget so we dont give player too much stuff
|
|
|
|
itemRewardBudget -= chosenWeapon.price;
|
2024-03-10 23:15:03 +00:00
|
|
|
rewardIndex++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-07 20:19:58 +01:00
|
|
|
const inBudgetRewardItemPool = this.chooseRewardItemsWithinBudget(repeatableConfig, itemRewardBudget, traderId);
|
2024-06-07 18:25:27 +00:00
|
|
|
this.logger.debug(
|
2024-06-07 20:19:58 +01:00
|
|
|
`Generating daily quest for: ${traderId} with budget: ${itemRewardBudget} totalling: ${rewardParams.rewardNumItems} items`,
|
2024-06-07 18:25:27 +00:00
|
|
|
);
|
|
|
|
if (inBudgetRewardItemPool.length > 0)
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-06-07 20:19:58 +01:00
|
|
|
const itemsToReward = this.getRewardableItemsFromPoolWithinBudget(
|
|
|
|
inBudgetRewardItemPool,
|
|
|
|
rewardParams.rewardNumItems,
|
|
|
|
itemRewardBudget,
|
|
|
|
repeatableConfig,
|
|
|
|
);
|
2024-03-10 23:15:03 +00:00
|
|
|
|
2024-06-07 20:19:58 +01:00
|
|
|
// Add item rewards
|
|
|
|
for (const itemReward of itemsToReward)
|
|
|
|
{
|
|
|
|
rewards.Success.push(this.generateRewardItem(itemReward.item._id, itemReward.stackSize, rewardIndex));
|
2024-03-10 23:15:03 +00:00
|
|
|
rewardIndex++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add rep reward to rewards array
|
2024-06-07 20:19:58 +01:00
|
|
|
if (rewardParams.rewardReputation > 0)
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
|
|
|
const reward: IQuestReward = {
|
|
|
|
target: traderId,
|
2024-06-07 20:19:58 +01:00
|
|
|
value: rewardParams.rewardReputation,
|
2024-03-10 23:15:03 +00:00
|
|
|
type: QuestRewardType.TRADER_STANDING,
|
|
|
|
index: rewardIndex,
|
|
|
|
};
|
|
|
|
rewards.Success.push(reward);
|
|
|
|
rewardIndex++;
|
2024-03-11 00:03:41 +00:00
|
|
|
|
2024-06-07 20:19:58 +01:00
|
|
|
this.logger.debug(` Adding ${rewardParams.rewardReputation} trader reputation reward`);
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Chance of adding skill reward
|
2024-06-07 20:19:58 +01:00
|
|
|
if (this.randomUtil.getChance100(rewardParams.skillRewardChance * 100))
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-03-11 00:03:41 +00:00
|
|
|
const targetSkill = this.randomUtil.getArrayValue(questConfig.possibleSkillRewards);
|
2024-03-10 23:15:03 +00:00
|
|
|
const reward: IQuestReward = {
|
2024-03-11 00:03:41 +00:00
|
|
|
target: targetSkill,
|
2024-06-07 20:19:58 +01:00
|
|
|
value: rewardParams.skillPointReward,
|
2024-03-10 23:15:03 +00:00
|
|
|
type: QuestRewardType.SKILL,
|
|
|
|
index: rewardIndex,
|
|
|
|
};
|
|
|
|
rewards.Success.push(reward);
|
2024-03-11 00:03:41 +00:00
|
|
|
|
2024-06-07 20:19:58 +01:00
|
|
|
this.logger.debug(` Adding ${rewardParams.skillPointReward} skill points to ${targetSkill}`);
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return rewards;
|
|
|
|
}
|
|
|
|
|
2024-06-07 20:19:58 +01:00
|
|
|
protected getQuestRewardValues(
|
|
|
|
rewardScaling: IRewardScaling,
|
|
|
|
difficulty: number,
|
|
|
|
pmcLevel: number): IQuestRewardValues
|
|
|
|
{
|
|
|
|
// difficulty could go from 0.2 ... -> for lowest difficulty receive 0.2*nominal reward
|
|
|
|
const levelsConfig = rewardScaling.levels;
|
|
|
|
const roublesConfig = rewardScaling.roubles;
|
|
|
|
const gpCoinConfig = rewardScaling.gpCoins;
|
|
|
|
const xpConfig = rewardScaling.experience;
|
|
|
|
const itemsConfig = rewardScaling.items;
|
|
|
|
const rewardSpreadConfig = rewardScaling.rewardSpread;
|
|
|
|
const skillRewardChanceConfig = rewardScaling.skillRewardChance;
|
|
|
|
const skillPointRewardConfig = rewardScaling.skillPointReward;
|
|
|
|
const reputationConfig = rewardScaling.reputation;
|
|
|
|
|
|
|
|
const effectiveDifficulty = Number.isNaN(difficulty) ? 1 : difficulty;
|
|
|
|
if (Number.isNaN(difficulty))
|
|
|
|
{
|
|
|
|
this.logger.warning(this.localisationService.getText("repeatable-difficulty_was_nan"));
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
skillPointReward: this.mathUtil.interp1(pmcLevel, levelsConfig, skillPointRewardConfig),
|
|
|
|
skillRewardChance: this.mathUtil.interp1(pmcLevel, levelsConfig, skillRewardChanceConfig),
|
|
|
|
rewardReputation: Math.round(
|
|
|
|
100
|
|
|
|
* effectiveDifficulty
|
|
|
|
* this.mathUtil.interp1(pmcLevel, levelsConfig, reputationConfig)
|
|
|
|
* this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
|
|
|
|
) / 100,
|
|
|
|
rewardNumItems: this.randomUtil.randInt(
|
|
|
|
1,
|
|
|
|
Math.round(this.mathUtil.interp1(pmcLevel, levelsConfig, itemsConfig)) + 1,
|
|
|
|
),
|
|
|
|
rewardRoubles: Math.floor(
|
|
|
|
effectiveDifficulty
|
|
|
|
* this.mathUtil.interp1(pmcLevel, levelsConfig, roublesConfig)
|
|
|
|
* this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
|
|
|
|
),
|
2024-06-17 17:44:34 +01:00
|
|
|
gpCoinRewardCount: Math.ceil( // Ceil value to ensure it never drops below 1
|
2024-06-07 20:19:58 +01:00
|
|
|
effectiveDifficulty
|
|
|
|
* this.mathUtil.interp1(pmcLevel, levelsConfig, gpCoinConfig)
|
|
|
|
* this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
|
|
|
|
),
|
|
|
|
rewardXP: Math.floor(
|
|
|
|
effectiveDifficulty
|
|
|
|
* this.mathUtil.interp1(pmcLevel, levelsConfig, xpConfig)
|
|
|
|
* this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an array of items + stack size to give to player as reward that fit inside of a rouble budget
|
|
|
|
* @param itemPool All possible items to choose rewards from
|
|
|
|
* @param maxItemCount Total number of items to reward
|
|
|
|
* @param itemRewardBudget Rouble buget all item rewards must fit in
|
|
|
|
* @param repeatableConfig config for quest type
|
|
|
|
* @returns Items and stack size
|
|
|
|
*/
|
|
|
|
protected getRewardableItemsFromPoolWithinBudget(
|
|
|
|
itemPool: ITemplateItem[],
|
|
|
|
maxItemCount: number,
|
|
|
|
itemRewardBudget: number,
|
|
|
|
repeatableConfig: IRepeatableQuestConfig): { item: ITemplateItem, stackSize: number }[]
|
|
|
|
{
|
|
|
|
const itemsToReturn: { item: ITemplateItem, stackSize: number }[] = [];
|
|
|
|
let exhausableItemPool = new ExhaustableArray(
|
|
|
|
itemPool,
|
|
|
|
this.randomUtil,
|
|
|
|
this.cloner,
|
|
|
|
);
|
|
|
|
|
|
|
|
for (let i = 0; i < maxItemCount; i++)
|
|
|
|
{
|
|
|
|
// Default stack size to 1
|
|
|
|
let rewardItemStackCount = 1;
|
|
|
|
|
|
|
|
// Get a random item
|
|
|
|
const chosenItemFromPool = exhausableItemPool.getRandomValue();
|
|
|
|
if (!exhausableItemPool.hasValues())
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle edge case - ammo
|
|
|
|
if (this.itemHelper.isOfBaseclass(chosenItemFromPool._id, BaseClasses.AMMO))
|
|
|
|
{
|
|
|
|
// Don't reward ammo that stacks to less than what's allowed in config
|
|
|
|
if (chosenItemFromPool._props.StackMaxSize < repeatableConfig.rewardAmmoStackMinSize)
|
|
|
|
{
|
|
|
|
i--;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Choose smallest value between budget, fitting size and stack max
|
|
|
|
rewardItemStackCount = this.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 (this.canIncreaseRewardItemStackSize(chosenItemFromPool, 70000, 25))
|
|
|
|
{
|
|
|
|
rewardItemStackCount = this.getRandomisedRewardItemStackSizeByPrice(chosenItemFromPool);
|
|
|
|
}
|
|
|
|
|
|
|
|
itemsToReturn.push({ item: chosenItemFromPool, stackSize: rewardItemStackCount });
|
|
|
|
|
|
|
|
const itemCost = this.presetHelper.getDefaultPresetOrItemPrice(chosenItemFromPool._id);
|
|
|
|
itemRewardBudget -= rewardItemStackCount * itemCost;
|
|
|
|
this.logger.debug(`Added item: ${chosenItemFromPool._id} with price: ${rewardItemStackCount * itemCost}`);
|
|
|
|
|
|
|
|
// If we still have budget narrow down possible items
|
|
|
|
if (itemRewardBudget > 0)
|
|
|
|
{
|
|
|
|
// Filter possible reward items to only items with a price below the remaining budget
|
|
|
|
exhausableItemPool = new ExhaustableArray(
|
|
|
|
this.filterRewardPoolWithinBudget(itemPool, itemRewardBudget, 0),
|
|
|
|
this.randomUtil,
|
|
|
|
this.cloner,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!exhausableItemPool.hasValues())
|
|
|
|
{
|
|
|
|
this.logger.debug(`Reward pool empty with: ${itemRewardBudget} roubles of budget remaining`);
|
|
|
|
break; // No reward items left, exit
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No budget for more items, end loop
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return itemsToReturn;
|
|
|
|
}
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
/**
|
|
|
|
* Choose a random Weapon preset that fits inside of a rouble amount limit
|
|
|
|
* @param roublesBudget
|
|
|
|
* @param rewardIndex
|
|
|
|
* @returns IQuestReward
|
|
|
|
*/
|
|
|
|
protected getRandomWeaponPresetWithinBudget(
|
|
|
|
roublesBudget: number,
|
|
|
|
rewardIndex: number,
|
|
|
|
): { weapon: IQuestReward, price: number } | undefined
|
|
|
|
{
|
|
|
|
// Add a random default preset weapon as reward
|
|
|
|
const defaultPresetPool = new ExhaustableArray(
|
|
|
|
Object.values(this.presetHelper.getDefaultWeaponPresets()),
|
|
|
|
this.randomUtil,
|
|
|
|
this.cloner,
|
|
|
|
);
|
|
|
|
|
|
|
|
while (defaultPresetPool.hasValues())
|
|
|
|
{
|
|
|
|
const randomPreset = defaultPresetPool.getRandomValue();
|
|
|
|
if (!randomPreset)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gather all tpls so we can get prices of them
|
|
|
|
const tpls = randomPreset._items.map((item) => item._tpl);
|
|
|
|
|
|
|
|
// Does preset items fit our budget
|
|
|
|
const presetPrice = this.itemHelper.getItemAndChildrenPrice(tpls);
|
|
|
|
if (presetPrice <= roublesBudget)
|
|
|
|
{
|
|
|
|
this.logger.debug(`Added weapon: ${tpls[0]} with price: ${presetPrice}`);
|
|
|
|
const chosenPreset = this.cloner.clone(randomPreset);
|
|
|
|
|
|
|
|
return {
|
|
|
|
weapon: this.generateRewardItem(chosenPreset._encyclopedia, 1, rewardIndex, chosenPreset._items),
|
|
|
|
price: presetPrice };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2024-03-11 00:03:41 +00:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
protected filterRewardPoolWithinBudget(
|
|
|
|
rewardItems: ITemplateItem[],
|
|
|
|
roublesBudget: number,
|
|
|
|
minPrice: number,
|
2024-04-01 08:38:23 +00:00
|
|
|
): ITemplateItem[]
|
2024-03-11 00:03:41 +00:00
|
|
|
{
|
2024-04-01 08:38:23 +00:00
|
|
|
return rewardItems.filter((item) =>
|
2024-03-11 00:03:41 +00:00
|
|
|
{
|
|
|
|
const itemPrice = this.presetHelper.getDefaultPresetOrItemPrice(item._id);
|
|
|
|
return itemPrice < roublesBudget && itemPrice > minPrice;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-03-10 23:15:03 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2024-06-07 18:25:27 +00:00
|
|
|
* @returns matching stack size for the passed in items price
|
2024-03-10 23:15:03 +00:00
|
|
|
*/
|
|
|
|
protected getRandomisedRewardItemStackSizeByPrice(item: ITemplateItem): number
|
|
|
|
{
|
2024-03-11 00:03:41 +00:00
|
|
|
const rewardItemPrice = this.presetHelper.getDefaultPresetOrItemPrice(item._id);
|
2024-03-10 23:15:03 +00:00
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Define price tiers and corresponding stack size options
|
|
|
|
const priceTiers: {
|
|
|
|
priceThreshold: number
|
|
|
|
stackSizes: number[] }[] = [
|
|
|
|
{ priceThreshold: 3000, stackSizes: [2, 3, 4] },
|
|
|
|
{ priceThreshold: 10000, stackSizes: [2, 3] },
|
|
|
|
{ priceThreshold: Infinity, stackSizes: [2] }, // Default for prices 10001+ RUB
|
|
|
|
];
|
|
|
|
|
|
|
|
// Find the appropriate price tier and return a random stack size from its options
|
|
|
|
const tier = priceTiers.find((tier) => rewardItemPrice < tier.priceThreshold);
|
|
|
|
return this.randomUtil.getArrayValue(tier?.stackSizes || [2]); // Default to 2 if no tier matches
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should reward item have stack size increased (25% chance)
|
2024-06-07 18:25:27 +00:00
|
|
|
* @param item Item to increase reward stack size of
|
2024-03-10 23:15:03 +00:00
|
|
|
* @param maxRoublePriceToStack Maximum rouble price an item can be to still be chosen for stacking
|
2024-06-07 18:25:27 +00:00
|
|
|
* @param randomChanceToPass Additional randomised chance of passing
|
|
|
|
* @returns True if items stack size can be increased
|
2024-03-10 23:15:03 +00:00
|
|
|
*/
|
2024-06-07 18:25:27 +00:00
|
|
|
protected canIncreaseRewardItemStackSize(
|
|
|
|
item: ITemplateItem,
|
|
|
|
maxRoublePriceToStack: number,
|
|
|
|
randomChanceToPass?: number): boolean
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
const isEligibleForStackSizeIncrease
|
|
|
|
= this.presetHelper.getDefaultPresetOrItemPrice(item._id) < maxRoublePriceToStack
|
2024-05-17 15:32:41 -04:00
|
|
|
&& !this.itemHelper.isOfBaseclasses(item._id, [
|
|
|
|
BaseClasses.WEAPON,
|
|
|
|
BaseClasses.ARMORED_EQUIPMENT,
|
|
|
|
BaseClasses.AMMO,
|
|
|
|
])
|
2024-06-07 18:25:27 +00:00
|
|
|
&& !this.itemHelper.itemRequiresSoftInserts(item._id);
|
|
|
|
|
|
|
|
return isEligibleForStackSizeIncrease && this.randomUtil.getChance100(randomChanceToPass ?? 100);
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
*/
|
2024-03-10 23:15:03 +00:00
|
|
|
protected calculateAmmoStackSizeThatFitsBudget(
|
|
|
|
itemSelected: ITemplateItem,
|
|
|
|
roublesBudget: number,
|
|
|
|
rewardNumItems: number,
|
|
|
|
): number
|
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
// Calculate budget per reward item
|
2024-03-10 23:15:03 +00:00
|
|
|
const stackRoubleBudget = roublesBudget / rewardNumItems;
|
|
|
|
|
|
|
|
const singleCartridgePrice = this.handbookHelper.getTemplatePrice(itemSelected._id);
|
|
|
|
|
|
|
|
// Get a stack size of ammo that fits rouble budget
|
|
|
|
const stackSizeThatFitsBudget = Math.round(stackRoubleBudget / singleCartridgePrice);
|
|
|
|
|
|
|
|
// Get itemDbs max stack size for ammo - don't go above 100 (some mods mess around with stack sizes)
|
|
|
|
const stackMaxCount = Math.min(itemSelected._props.StackMaxSize, 100);
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Ensure stack size is at least 1 + is no larger than the max possible stack size
|
2024-03-23 11:32:10 +00:00
|
|
|
return Math.max(1, Math.min(stackSizeThatFitsBudget, stackMaxCount));
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Select a number of items that have a colelctive value of the passed in parameter
|
|
|
|
* @param repeatableConfig Config
|
|
|
|
* @param roublesBudget Total value of items to return
|
2024-06-07 18:25:27 +00:00
|
|
|
* @param traderId Id of the trader who will give player reward
|
2024-03-10 23:15:03 +00:00
|
|
|
* @returns Array of reward items that fit budget
|
|
|
|
*/
|
|
|
|
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 rewardableItemPool = this.getRewardableItems(repeatableConfig, traderId);
|
|
|
|
const minPrice = Math.min(25000, 0.5 * roublesBudget);
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
let rewardableItemPoolWithinBudget = this.filterRewardPoolWithinBudget(
|
|
|
|
rewardableItemPool.map((item) => item[1]),
|
2024-04-01 08:38:23 +00:00
|
|
|
roublesBudget,
|
|
|
|
minPrice,
|
|
|
|
);
|
2024-06-07 18:25:27 +00:00
|
|
|
|
2024-04-01 08:38:23 +00:00
|
|
|
if (rewardableItemPoolWithinBudget.length === 0)
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
|
|
|
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
|
2024-05-17 15:32:41 -04:00
|
|
|
rewardableItemPoolWithinBudget = rewardableItemPool
|
|
|
|
.filter((x) => this.itemHelper.getItemPrice(x[0]) < roublesBudget)
|
|
|
|
.map((x) => x[1]);
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return rewardableItemPoolWithinBudget;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper to create a reward item structured as required by the client
|
|
|
|
*
|
|
|
|
* @param {string} tpl ItemId of the rewarded item
|
|
|
|
* @param {integer} value Amount of items to give
|
|
|
|
* @param {integer} index All rewards will be appended to a list, for unknown reasons the client wants the index
|
2024-06-07 18:25:27 +00:00
|
|
|
* @param preset Optional array of preset items
|
2024-03-10 23:15:03 +00:00
|
|
|
* @returns {object} Object of "Reward"-item-type
|
|
|
|
*/
|
2024-05-27 20:06:07 +00:00
|
|
|
protected generateRewardItem(tpl: string, value: number, index: number, preset?: Item[]): IQuestReward
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
|
|
|
const id = this.objectId.generate();
|
2024-06-07 18:25:27 +00:00
|
|
|
const questRewardItem: IQuestReward = {
|
|
|
|
target: id,
|
|
|
|
value: value,
|
|
|
|
type: QuestRewardType.ITEM,
|
|
|
|
index: index,
|
|
|
|
items: [] };
|
2024-03-10 23:15:03 +00:00
|
|
|
|
|
|
|
if (preset)
|
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
// Get presets root item
|
|
|
|
const rootItem = preset.find((item) => item._tpl === tpl);
|
|
|
|
if (!rootItem)
|
|
|
|
{
|
|
|
|
this.logger.warning(`Root item of preset: ${tpl} not found`);
|
|
|
|
}
|
|
|
|
|
|
|
|
questRewardItem.items = this.itemHelper.reparentItemAndChildren(rootItem, preset);
|
|
|
|
questRewardItem.target = rootItem._id; // Target property and root items id must match
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
const rootItem = { _id: id, _tpl: tpl, upd: { StackObjectsCount: value, SpawnedInSession: true } };
|
2024-06-07 18:25:27 +00:00
|
|
|
questRewardItem.items = [rootItem];
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
2024-06-07 18:25:27 +00:00
|
|
|
|
|
|
|
return questRewardItem;
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-07 18:25:27 +00:00
|
|
|
* Picks rewardable items from items.json
|
|
|
|
* This means they must:
|
|
|
|
* - Fit into the inventory
|
|
|
|
* - Shouldn't be keys
|
|
|
|
* - Have a price greater than 0
|
2024-03-10 23:15:03 +00:00
|
|
|
* @param repeatableQuestConfig Config file
|
2024-06-07 18:25:27 +00:00
|
|
|
* @param traderId Id of trader who will give reward to player
|
2024-03-10 23:15:03 +00:00
|
|
|
* @returns List of rewardable items [[_tpl, itemTemplate],...]
|
|
|
|
*/
|
|
|
|
public getRewardableItems(
|
|
|
|
repeatableQuestConfig: IRepeatableQuestConfig,
|
|
|
|
traderId: string,
|
|
|
|
): [string, ITemplateItem][]
|
|
|
|
{
|
|
|
|
// Get an array of seasonal items that should not be shown right now as seasonal event is not active
|
|
|
|
const seasonalItems = this.seasonalEventService.getInactiveSeasonalEventItems();
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Check for specific baseclasses which don't make sense as reward item
|
2024-03-10 23:15:03 +00:00
|
|
|
// 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)
|
2024-05-28 22:24:52 +01:00
|
|
|
return Object.entries(this.databaseService.getItems()).filter(
|
2024-03-10 23:15:03 +00:00
|
|
|
([tpl, itemTemplate]) =>
|
|
|
|
{
|
|
|
|
// Base "Item" item has no parent, ignore it
|
|
|
|
if (itemTemplate._parent === "")
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (seasonalItems.includes(tpl))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-05-17 15:32:41 -04:00
|
|
|
const traderWhitelist = repeatableQuestConfig.traderWhitelist.find(
|
|
|
|
(trader) => trader.traderId === traderId,
|
2024-03-10 23:15:03 +00:00
|
|
|
);
|
|
|
|
return this.isValidRewardItem(tpl, repeatableQuestConfig, traderWhitelist?.rewardBaseWhitelist);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
protected isValidRewardItem(
|
|
|
|
tpl: string,
|
|
|
|
repeatableQuestConfig: IRepeatableQuestConfig,
|
|
|
|
itemBaseWhitelist: string[],
|
|
|
|
): boolean
|
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
// Return early if not valid item to give as reward
|
2024-03-10 23:15:03 +00:00
|
|
|
if (!this.itemHelper.isValidItem(tpl))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Check item is not blacklisted
|
|
|
|
if (this.itemFilterService.isItemBlacklisted(tpl)
|
|
|
|
|| this.itemFilterService.isItemRewardBlacklisted(tpl)
|
|
|
|
|| repeatableQuestConfig.rewardBlacklist.includes(tpl)
|
|
|
|
|| this.itemFilterService.isItemBlacklisted(tpl))
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
// Item has blacklisted base types
|
2024-03-10 23:15:03 +00:00
|
|
|
if (this.itemHelper.isOfBaseclasses(tpl, [...repeatableQuestConfig.rewardBaseTypeBlacklist]))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip boss items
|
|
|
|
if (this.itemFilterService.isBossItem(tpl))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Trader has specific item base types they can give as rewards to player
|
2024-06-07 18:25:27 +00:00
|
|
|
if (itemBaseWhitelist !== undefined
|
|
|
|
&& !this.itemHelper.isOfBaseclasses(tpl, [...itemBaseWhitelist]))
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
return false;
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-06-07 18:25:27 +00:00
|
|
|
protected getMoneyReward(
|
2024-05-17 15:32:41 -04:00
|
|
|
traderId: string,
|
|
|
|
rewardRoubles: number,
|
|
|
|
rewardIndex: number,
|
2024-06-07 18:25:27 +00:00
|
|
|
): IQuestReward
|
2024-03-10 23:15:03 +00:00
|
|
|
{
|
2024-06-07 18:25:27 +00:00
|
|
|
// Determine currency based on trader
|
|
|
|
// PK and Fence use Euros, everyone else is Roubles
|
|
|
|
const currency
|
|
|
|
= traderId === Traders.PEACEKEEPER || traderId === Traders.FENCE
|
|
|
|
? Money.EUROS
|
|
|
|
: Money.ROUBLES;
|
|
|
|
|
|
|
|
// Convert reward amount to Euros if necessary
|
|
|
|
const rewardAmountToGivePlayer
|
|
|
|
= currency === Money.EUROS
|
|
|
|
? this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS)
|
|
|
|
: rewardRoubles;
|
|
|
|
|
|
|
|
// Get chosen currency + amount and return
|
|
|
|
return this.generateRewardItem(currency, rewardAmountToGivePlayer, rewardIndex);
|
2024-03-10 23:15:03 +00:00
|
|
|
}
|
|
|
|
}
|