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:
parent
3e760e88e4
commit
20ec7f9858
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 });
|
||||
|
@ -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
|
||||
|
285
project/src/helpers/QuestRewardHelper.ts
Normal file
285
project/src/helpers/QuestRewardHelper.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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", {
|
||||
|
Loading…
x
Reference in New Issue
Block a user