0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 02:30:43 -05:00

Created new class QuestRewardHelper, migrated reward code from QuestHelper

This commit is contained in:
Chomp 2025-01-07 13:32:09 +00:00
parent 3e760e88e4
commit 20ec7f9858
6 changed files with 301 additions and 266 deletions

View File

@ -3,6 +3,7 @@ import { DialogueHelper } from "@spt/helpers/DialogueHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
import { TraderHelper } from "@spt/helpers/TraderHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { ITemplateSide } from "@spt/models/eft/common/tables/IProfileTemplate";
@ -55,6 +56,7 @@ export class ProfileController {
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
) {}
@ -405,7 +407,7 @@ export class ProfileController {
questFromDb.startedMessageText,
questFromDb.description,
);
const itemRewards = this.questHelper.applyQuestReward(
const itemRewards = this.questRewardHelper.applyQuestReward(
profileDetails.characters.pmc,
quest.qid,
QuestStatus.Started,

View File

@ -3,6 +3,7 @@ import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestConditionHelper } from "@spt/helpers/QuestConditionHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
import { TraderHelper } from "@spt/helpers/TraderHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItem } from "@spt/models/eft/common/tables/IItem";
@ -46,6 +47,7 @@ export class QuestController {
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper,
@inject("QuestConditionHelper") protected questConditionHelper: QuestConditionHelper,
@inject("PlayerService") protected playerService: PlayerService,
@inject("LocaleService") protected localeService: LocaleService,
@ -112,7 +114,7 @@ export class QuestController {
);
// Apply non-item rewards to profile + return item rewards
const startedQuestRewardItems = this.questHelper.applyQuestReward(
const startedQuestRewardItems = this.questRewardHelper.applyQuestReward(
pmcData,
acceptedQuest.qid,
QuestStatus.Started,

View File

@ -113,6 +113,7 @@ import { ProbabilityHelper } from "@spt/helpers/ProbabilityHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestConditionHelper } from "@spt/helpers/QuestConditionHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
import { RagfairHelper } from "@spt/helpers/RagfairHelper";
import { RagfairOfferHelper } from "@spt/helpers/RagfairOfferHelper";
import { RagfairSellHelper } from "@spt/helpers/RagfairSellHelper";
@ -607,6 +608,7 @@ export class Container {
depContainer.register<PresetHelper>("PresetHelper", PresetHelper, { lifecycle: Lifecycle.Singleton });
depContainer.register<ProfileHelper>("ProfileHelper", { useClass: ProfileHelper });
depContainer.register<QuestHelper>("QuestHelper", { useClass: QuestHelper });
depContainer.register<QuestRewardHelper>("QuestRewardHelper", { useClass: QuestRewardHelper });
depContainer.register<QuestConditionHelper>("QuestConditionHelper", QuestConditionHelper);
depContainer.register<RagfairHelper>("RagfairHelper", { useClass: RagfairHelper });
depContainer.register<RagfairSortHelper>("RagfairSortHelper", { useClass: RagfairSortHelper });

View File

@ -1,26 +1,20 @@
import { DialogueHelper } from "@spt/helpers/DialogueHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PaymentHelper } from "@spt/helpers/PaymentHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestConditionHelper } from "@spt/helpers/QuestConditionHelper";
import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper";
import { TraderHelper } from "@spt/helpers/TraderHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { Common, IQuestStatus } from "@spt/models/eft/common/tables/IBotBase";
import { IItem } from "@spt/models/eft/common/tables/IItem";
import { IQuest, IQuestCondition, IQuestReward } from "@spt/models/eft/common/tables/IQuest";
import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { IAcceptQuestRequestData } from "@spt/models/eft/quests/IAcceptQuestRequestData";
import { ICompleteQuestRequestData } from "@spt/models/eft/quests/ICompleteQuestRequestData";
import { IFailQuestRequestData } from "@spt/models/eft/quests/IFailQuestRequestData";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { MessageType } from "@spt/models/enums/MessageType";
import { QuestRewardType } from "@spt/models/enums/QuestRewardType";
import { QuestStatus } from "@spt/models/enums/QuestStatus";
import { SeasonalEventType } from "@spt/models/enums/SeasonalEventType";
import { SkillTypes } from "@spt/models/enums/SkillTypes";
import { IQuestConfig } from "@spt/models/spt/config/IQuestConfig";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
@ -49,10 +43,7 @@ export class QuestHelper {
@inject("QuestConditionHelper") protected questConditionHelper: QuestConditionHelper,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("LocaleService") protected localeService: LocaleService,
@inject("RagfairServerHelper") protected ragfairServerHelper: RagfairServerHelper,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@ -351,28 +342,6 @@ export class QuestHelper {
questReward.items = this.itemHelper.addChildSlotItems(questReward.items, itemDbData, undefined, true);
}
/**
* Gets a flat list of reward items for the given quest at a specific state for the specified game version (e.g. Fail/Success)
* @param quest quest to get rewards for
* @param status Quest status that holds the items (Started, Success, Fail)
* @returns array of items with the correct maxStack
*/
public getQuestRewardItems(quest: IQuest, status: QuestStatus, gameVersion: string): IItem[] {
if (!quest.rewards[QuestStatus[status]]) {
this.logger.warning(`Unable to find: ${status} reward for quest: ${quest.QuestName}`);
return [];
}
// Iterate over all rewards with the desired status, flatten out items that have a type of Item
const questRewards = quest.rewards[QuestStatus[status]].flatMap((reward: IQuestReward) =>
reward.type === "Item" && this.questRewardIsForGameEdition(reward, gameVersion)
? this.processReward(reward)
: [],
);
return questRewards;
}
/**
* Look up quest in db by accepted quest id and construct a profile-ready object ready to store in profile
* @param pmcData Player profile
@ -629,29 +598,6 @@ export class QuestHelper {
return this.getQuestsWithOnlyLevelRequirementStartCondition(quests);
}
/**
* Adjust quest money rewards by passed in multiplier
* @param quest Quest to multiple money rewards
* @param bonusPercent Pecent to adjust money rewards by
* @param questStatus Status of quest to apply money boost to rewards of
* @returns Updated quest
*/
public applyMoneyBoost(quest: IQuest, bonusPercent: number, questStatus: QuestStatus): IQuest {
const rewards: IQuestReward[] = quest.rewards?.[QuestStatus[questStatus]] ?? [];
const currencyRewards = rewards.filter(
(reward) => reward.type === "Item" && this.paymentHelper.isMoneyTpl(reward.items[0]._tpl),
);
for (const reward of currencyRewards) {
// Add % bonus to existing StackObjectsCount
const rewardItem = reward.items[0];
const newCurrencyAmount = Math.floor(rewardItem.upd.StackObjectsCount * (1 + bonusPercent / 100));
rewardItem.upd.StackObjectsCount = newCurrencyAmount;
reward.value = newCurrencyAmount;
}
return quest;
}
/**
* Sets the item stack to new value, or delete the item if value <= 0
* // TODO maybe merge this function and the one from customization
@ -901,126 +847,13 @@ export class QuestHelper {
}
}
/**
* Give player quest rewards - Skills/exp/trader standing/items/assort unlocks - Returns reward items player earned
* @param profileData Player profile (scav or pmc)
* @param questId questId of quest to get rewards for
* @param state State of the quest to get rewards for
* @param sessionId Session id
* @param questResponse Response to send back to client
* @returns Array of reward objects
*/
public applyQuestReward(
profileData: IPmcData,
questId: string,
state: QuestStatus,
sessionId: string,
questResponse: IItemEventRouterResponse,
): IItem[] {
// Repeatable quest base data is always in PMCProfile, `profileData` may be scav profile
// TODO: consider moving repeatable quest data to profile-agnostic location
const fullProfile = this.profileHelper.getFullProfile(sessionId);
const pmcProfile = fullProfile?.characters.pmc;
if (!pmcProfile) {
this.logger.error(`Unable to get pmc profile for: ${sessionId}, no rewards given`);
return [];
}
let questDetails = this.getQuestFromDb(questId, pmcProfile);
if (!questDetails) {
this.logger.warning(
this.localisationService.getText("quest-unable_to_find_quest_in_db_no_quest_rewards", questId),
);
return [];
}
// Check for and apply intel center money bonus if it exists
const questMoneyRewardBonusMultiplier = this.getQuestMoneyRewardBonusMultiplier(pmcProfile);
if (questMoneyRewardBonusMultiplier > 0) {
// Apply additional bonus from hideout skill
questDetails = this.applyMoneyBoost(questDetails, questMoneyRewardBonusMultiplier, state); // money = money + (money * intelCenterBonus / 100)
}
// e.g. 'Success' or 'AvailableForFinish'
const questStateAsString = QuestStatus[state];
const gameVersion = pmcProfile.Info.GameVersion;
for (const reward of <IQuestReward[]>questDetails.rewards[questStateAsString]) {
// Handle quest reward availability for different game versions, notAvailableInGameEditions currently not used
if (!this.questRewardIsForGameEdition(reward, gameVersion)) {
continue;
}
switch (reward.type) {
case QuestRewardType.SKILL:
this.profileHelper.addSkillPointsToPlayer(
profileData,
reward.target as SkillTypes,
Number(reward.value),
);
break;
case QuestRewardType.EXPERIENCE:
this.profileHelper.addExperienceToPmc(sessionId, Number.parseInt(<string>reward.value)); // this must occur first as the output object needs to take the modified profile exp value
break;
case QuestRewardType.TRADER_STANDING:
this.traderHelper.addStandingToTrader(
sessionId,
reward.target,
Number.parseFloat(<string>reward.value),
);
break;
case QuestRewardType.TRADER_UNLOCK:
this.traderHelper.setTraderUnlockedState(reward.target, true, sessionId);
break;
case QuestRewardType.ITEM:
// Handled by getQuestRewardItems() below
break;
case QuestRewardType.ASSORTMENT_UNLOCK:
// Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player
break;
case QuestRewardType.ACHIEVEMENT:
this.profileHelper.addAchievementToProfile(fullProfile, reward.target);
break;
case QuestRewardType.STASH_ROWS:
this.profileHelper.addStashRowsBonusToProfile(sessionId, Number.parseInt(<string>reward.value)); // Add specified stash rows from quest reward - requires client restart
break;
case QuestRewardType.PRODUCTIONS_SCHEME:
this.findAndAddHideoutProductionIdToProfile(
pmcProfile,
reward,
questDetails,
sessionId,
questResponse,
);
break;
case QuestRewardType.POCKETS:
this.profileHelper.replaceProfilePocketTpl(pmcProfile, reward.target);
break;
case QuestRewardType.CUSTOMIZATION_DIRECT:
this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, "unlockedInGame");
break;
default:
this.logger.error(
this.localisationService.getText("quest-reward_type_not_handled", {
rewardType: reward.type,
questId: questId,
questName: questDetails.QuestName,
}),
);
break;
}
}
return this.getQuestRewardItems(questDetails, state, gameVersion);
}
/**
* Does the provided quest reward have a game version requirement to be given and does it match
* @param reward Reward to check
* @param gameVersion Version of game to check reward against
* @returns True if it has requirement, false if it doesnt pass check
*/
protected questRewardIsForGameEdition(reward: IQuestReward, gameVersion: string) {
public questRewardIsForGameEdition(reward: IQuestReward, gameVersion: string) {
if (reward.availableInGameEditions?.length > 0 && !reward.availableInGameEditions?.includes(gameVersion)) {
// Reward has edition whitelist and game version isnt in it
return false;
@ -1035,98 +868,6 @@ export class QuestHelper {
return true;
}
/**
* WIP - Find hideout craft id and add to unlockedProductionRecipe array in player profile
* also update client response recipeUnlocked array with craft id
* @param pmcData Player profile
* @param craftUnlockReward Reward item from quest with craft unlock details
* @param questDetails Quest with craft unlock reward
* @param sessionID Session id
* @param response Response to send back to client
*/
protected findAndAddHideoutProductionIdToProfile(
pmcData: IPmcData,
craftUnlockReward: IQuestReward,
questDetails: IQuest,
sessionID: string,
response: IItemEventRouterResponse,
): void {
const matchingProductions = this.getRewardProductionMatch(craftUnlockReward, questDetails);
if (matchingProductions.length !== 1) {
this.logger.error(
this.localisationService.getText("quest-unable_to_find_matching_hideout_production", {
questName: questDetails.QuestName,
matchCount: matchingProductions.length,
}),
);
return;
}
// Add above match to pmc profile + client response
const matchingCraftId = matchingProductions[0]._id;
pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId);
response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true;
}
/**
* Find hideout craft for the specified quest reward
* @param craftUnlockReward Reward item from quest with craft unlock details
* @param questDetails Quest with craft unlock reward
* @returns Hideout craft
*/
public getRewardProductionMatch(craftUnlockReward: IQuestReward, questDetails: IQuest): IHideoutProduction[] {
// Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match
const craftingRecipes = this.databaseService.getHideout().production.recipes;
// Area that will be used to craft unlocked item
const desiredHideoutAreaType = Number.parseInt(craftUnlockReward.traderId);
let matchingProductions = craftingRecipes.filter(
(prod) =>
prod.areaType === desiredHideoutAreaType &&
//prod.requirements.some((requirement) => requirement.questId === questDetails._id) && // BSG dont store the quest id in requirement any more!
prod.requirements.some((requirement) => requirement.type === "QuestComplete") &&
prod.requirements.some((requirement) => requirement.requiredLevel === craftUnlockReward.loyaltyLevel) &&
prod.endProduct === craftUnlockReward.items[0]._tpl,
);
// More/less than single match, above filtering wasn't strict enough
if (matchingProductions.length !== 1) {
// Multiple matches were found, last ditch attempt to match by questid (value we add manually to production.json via `gen:productionquests` command)
matchingProductions = matchingProductions.filter((prod) =>
prod.requirements.some((requirement) => requirement.questId === questDetails._id),
);
}
return matchingProductions;
}
/**
* Get players money reward bonus from profile
* @param pmcData player profile
* @returns bonus as a percent
*/
protected getQuestMoneyRewardBonusMultiplier(pmcData: IPmcData): number {
// Check player has intel center
const moneyRewardBonuses = pmcData.Bonuses.filter((profileBonus) => profileBonus.type === "QuestMoneyReward");
// Get a total of the quest money reward percent bonuses
const moneyRewardBonusPercent = moneyRewardBonuses.reduce((acc, cur) => acc + cur.value, 0);
// Calculate hideout management bonus as a percentage (up to 51% bonus)
const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT);
// 5100 becomes 0.51, add 1 to it, 1.51
// We multiply the money reward bonuses by the hideout management skill multipler, giving the new result
const hideoutManagementBonusMultipler = hideoutManagementSkill
? 1 + hideoutManagementSkill.Progress / 10000
: 1;
// e.g 15% * 1.4
return moneyRewardBonusPercent * hideoutManagementBonusMultipler;
}
/**
* Find quest with 'findItem' condition that needs the item tpl be handed in
* @param itemTpl item tpl to look for

View File

@ -0,0 +1,285 @@
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PaymentHelper } from "@spt/helpers/PaymentHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { TraderHelper } from "@spt/helpers/TraderHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItem } from "@spt/models/eft/common/tables/IItem";
import { IQuest, IQuestReward } from "@spt/models/eft/common/tables/IQuest";
import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { QuestRewardType } from "@spt/models/enums/QuestRewardType";
import { QuestStatus } from "@spt/models/enums/QuestStatus";
import { SkillTypes } from "@spt/models/enums/SkillTypes";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import type { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class QuestRewardHelper {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DatabaseService") protected databaseService: DatabaseService,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("PrimaryCloner") protected cloner: ICloner,
) {}
/**
* Give player quest rewards - Skills/exp/trader standing/items/assort unlocks - Returns reward items player earned
* @param profileData Player profile (scav or pmc)
* @param questId questId of quest to get rewards for
* @param state State of the quest to get rewards for
* @param sessionId Session id
* @param questResponse Response to send back to client
* @returns Array of reward objects
*/
public applyQuestReward(
profileData: IPmcData,
questId: string,
state: QuestStatus,
sessionId: string,
questResponse: IItemEventRouterResponse,
): IItem[] {
// Repeatable quest base data is always in PMCProfile, `profileData` may be scav profile
// TODO: consider moving repeatable quest data to profile-agnostic location
const fullProfile = this.profileHelper.getFullProfile(sessionId);
const pmcProfile = fullProfile?.characters.pmc;
if (!pmcProfile) {
this.logger.error(`Unable to get pmc profile for: ${sessionId}, no rewards given`);
return [];
}
let questDetails = this.questHelper.getQuestFromDb(questId, pmcProfile);
if (!questDetails) {
this.logger.warning(
this.localisationService.getText("quest-unable_to_find_quest_in_db_no_quest_rewards", questId),
);
return [];
}
// Check for and apply intel center money bonus if it exists
const questMoneyRewardBonusMultiplier = this.getQuestMoneyRewardBonusMultiplier(pmcProfile);
if (questMoneyRewardBonusMultiplier > 0) {
// Apply additional bonus from hideout skill
questDetails = this.applyMoneyBoost(questDetails, questMoneyRewardBonusMultiplier, state); // money = money + (money * intelCenterBonus / 100)
}
// e.g. 'Success' or 'AvailableForFinish'
const questStateAsString = QuestStatus[state];
const gameVersion = pmcProfile.Info.GameVersion;
for (const reward of <IQuestReward[]>questDetails.rewards[questStateAsString]) {
// Handle quest reward availability for different game versions, notAvailableInGameEditions currently not used
if (!this.questHelper.questRewardIsForGameEdition(reward, gameVersion)) {
continue;
}
switch (reward.type) {
case QuestRewardType.SKILL:
this.profileHelper.addSkillPointsToPlayer(
profileData,
reward.target as SkillTypes,
Number(reward.value),
);
break;
case QuestRewardType.EXPERIENCE:
this.profileHelper.addExperienceToPmc(sessionId, Number.parseInt(<string>reward.value)); // this must occur first as the output object needs to take the modified profile exp value
break;
case QuestRewardType.TRADER_STANDING:
this.traderHelper.addStandingToTrader(
sessionId,
reward.target,
Number.parseFloat(<string>reward.value),
);
break;
case QuestRewardType.TRADER_UNLOCK:
this.traderHelper.setTraderUnlockedState(reward.target, true, sessionId);
break;
case QuestRewardType.ITEM:
// Handled by getQuestRewardItems() below
break;
case QuestRewardType.ASSORTMENT_UNLOCK:
// Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player
break;
case QuestRewardType.ACHIEVEMENT:
this.profileHelper.addAchievementToProfile(fullProfile, reward.target);
break;
case QuestRewardType.STASH_ROWS:
this.profileHelper.addStashRowsBonusToProfile(sessionId, Number.parseInt(<string>reward.value)); // Add specified stash rows from quest reward - requires client restart
break;
case QuestRewardType.PRODUCTIONS_SCHEME:
this.findAndAddHideoutProductionIdToProfile(
pmcProfile,
reward,
questDetails,
sessionId,
questResponse,
);
break;
case QuestRewardType.POCKETS:
this.profileHelper.replaceProfilePocketTpl(pmcProfile, reward.target);
break;
case QuestRewardType.CUSTOMIZATION_DIRECT:
this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, "unlockedInGame");
break;
default:
this.logger.error(
this.localisationService.getText("quest-reward_type_not_handled", {
rewardType: reward.type,
questId: questId,
questName: questDetails.QuestName,
}),
);
break;
}
}
return this.getQuestRewardItems(questDetails, state, gameVersion);
}
/**
* Get players money reward bonus from profile
* @param pmcData player profile
* @returns bonus as a percent
*/
protected getQuestMoneyRewardBonusMultiplier(pmcData: IPmcData): number {
// Check player has intel center
const moneyRewardBonuses = pmcData.Bonuses.filter((profileBonus) => profileBonus.type === "QuestMoneyReward");
// Get a total of the quest money reward percent bonuses
const moneyRewardBonusPercent = moneyRewardBonuses.reduce((acc, cur) => acc + cur.value, 0);
// Calculate hideout management bonus as a percentage (up to 51% bonus)
const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT);
// 5100 becomes 0.51, add 1 to it, 1.51
// We multiply the money reward bonuses by the hideout management skill multipler, giving the new result
const hideoutManagementBonusMultipler = hideoutManagementSkill
? 1 + hideoutManagementSkill.Progress / 10000
: 1;
// e.g 15% * 1.4
return moneyRewardBonusPercent * hideoutManagementBonusMultipler;
}
/**
* Adjust quest money rewards by passed in multiplier
* @param quest Quest to multiple money rewards
* @param bonusPercent Pecent to adjust money rewards by
* @param questStatus Status of quest to apply money boost to rewards of
* @returns Updated quest
*/
public applyMoneyBoost(quest: IQuest, bonusPercent: number, questStatus: QuestStatus): IQuest {
const rewards: IQuestReward[] = quest.rewards?.[QuestStatus[questStatus]] ?? [];
const currencyRewards = rewards.filter(
(reward) => reward.type === "Item" && this.paymentHelper.isMoneyTpl(reward.items[0]._tpl),
);
for (const reward of currencyRewards) {
// Add % bonus to existing StackObjectsCount
const rewardItem = reward.items[0];
const newCurrencyAmount = Math.floor(rewardItem.upd.StackObjectsCount * (1 + bonusPercent / 100));
rewardItem.upd.StackObjectsCount = newCurrencyAmount;
reward.value = newCurrencyAmount;
}
return quest;
}
/**
* WIP - Find hideout craft id and add to unlockedProductionRecipe array in player profile
* also update client response recipeUnlocked array with craft id
* @param pmcData Player profile
* @param craftUnlockReward Reward item from quest with craft unlock details
* @param questDetails Quest with craft unlock reward
* @param sessionID Session id
* @param response Response to send back to client
*/
protected findAndAddHideoutProductionIdToProfile(
pmcData: IPmcData,
craftUnlockReward: IQuestReward,
questDetails: IQuest,
sessionID: string,
response: IItemEventRouterResponse,
): void {
const matchingProductions = this.getRewardProductionMatch(craftUnlockReward, questDetails);
if (matchingProductions.length !== 1) {
this.logger.error(
this.localisationService.getText("quest-unable_to_find_matching_hideout_production", {
questName: questDetails.QuestName,
matchCount: matchingProductions.length,
}),
);
return;
}
// Add above match to pmc profile + client response
const matchingCraftId = matchingProductions[0]._id;
pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId);
response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true;
}
/**
* Find hideout craft for the specified quest reward
* @param craftUnlockReward Reward item from quest with craft unlock details
* @param questDetails Quest with craft unlock reward
* @returns Hideout craft
*/
public getRewardProductionMatch(craftUnlockReward: IQuestReward, questDetails: IQuest): IHideoutProduction[] {
// Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match
const craftingRecipes = this.databaseService.getHideout().production.recipes;
// Area that will be used to craft unlocked item
const desiredHideoutAreaType = Number.parseInt(craftUnlockReward.traderId);
let matchingProductions = craftingRecipes.filter(
(prod) =>
prod.areaType === desiredHideoutAreaType &&
//prod.requirements.some((requirement) => requirement.questId === questDetails._id) && // BSG dont store the quest id in requirement any more!
prod.requirements.some((requirement) => requirement.type === "QuestComplete") &&
prod.requirements.some((requirement) => requirement.requiredLevel === craftUnlockReward.loyaltyLevel) &&
prod.endProduct === craftUnlockReward.items[0]._tpl,
);
// More/less than single match, above filtering wasn't strict enough
if (matchingProductions.length !== 1) {
// Multiple matches were found, last ditch attempt to match by questid (value we add manually to production.json via `gen:productionquests` command)
matchingProductions = matchingProductions.filter((prod) =>
prod.requirements.some((requirement) => requirement.questId === questDetails._id),
);
}
return matchingProductions;
}
/**
* Gets a flat list of reward items for the given quest at a specific state for the specified game version (e.g. Fail/Success)
* @param quest quest to get rewards for
* @param status Quest status that holds the items (Started, Success, Fail)
* @returns array of items with the correct maxStack
*/
protected getQuestRewardItems(quest: IQuest, status: QuestStatus, gameVersion: string): IItem[] {
if (!quest.rewards[QuestStatus[status]]) {
this.logger.warning(`Unable to find: ${status} reward for quest: ${quest.QuestName}`);
return [];
}
// Iterate over all rewards with the desired status, flatten out items that have a type of Item
const questRewards = quest.rewards[QuestStatus[status]].flatMap((reward: IQuestReward) =>
reward.type === "Item" && this.questRewardIsForGameEdition(reward, gameVersion)
? this.processReward(reward)
: [],
);
return questRewards;
}
}

View File

@ -2,7 +2,7 @@ import { HideoutHelper } from "@spt/helpers/HideoutHelper";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
import { TraderHelper } from "@spt/helpers/TraderHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IBonus, IHideoutSlot } from "@spt/models/eft/common/tables/IBotBase";
@ -49,7 +49,7 @@ export class ProfileFixerService {
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper,
) {
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@ -78,7 +78,7 @@ export class ProfileFixerService {
/**
* Resolve any dialogue attachments that were accidentally created using the player's equipment ID as
* the stash root object ID
* @param fullProfile
* @param fullProfile
*/
public checkForAndFixDialogueAttachments(fullProfile: ISptProfile): void {
for (const traderDialogues of Object.values(fullProfile.dialogues)) {
@ -352,7 +352,10 @@ export class ProfileFixerService {
productionUnlockReward: IQuestReward,
questDetails: IQuest,
): void {
const matchingProductions = this.questHelper.getRewardProductionMatch(productionUnlockReward, questDetails);
const matchingProductions = this.questRewardHelper.getRewardProductionMatch(
productionUnlockReward,
questDetails,
);
if (matchingProductions.length !== 1) {
this.logger.error(
this.localisationService.getText("quest-unable_to_find_matching_hideout_production", {