mirror of
https://github.com/sp-tarkov/server.git
synced 2025-02-13 09:10:43 -05:00
Refactor quest and achievement reward handling to be more centralized
- Create a new RewardHelper class that contains the majority of shared reward handling - Move a lot of methods from QuestRewardHelper into RewardHelper - Fix a bug in `applyMoneyBoost` that could result in compounding money boost being applied across characters - Route achievement reward handling through RewardHelper
This commit is contained in:
parent
ad782e5906
commit
b541c04bac
@ -594,9 +594,7 @@
|
|||||||
"quest-handover_wrong_item": "Unable to hand item in for quest: {{questId}}, expected tpl: {{requiredTpl}} but handed in: {{handedInTpl}}",
|
"quest-handover_wrong_item": "Unable to hand item in for quest: {{questId}}, expected tpl: {{requiredTpl}} but handed in: {{handedInTpl}}",
|
||||||
"quest-item_not_found_in_inventory": "changeItemStack() Item with _id: %s not found in inventory",
|
"quest-item_not_found_in_inventory": "changeItemStack() Item with _id: %s not found in inventory",
|
||||||
"quest-no_skill_found": "Skill %s not found",
|
"quest-no_skill_found": "Skill %s not found",
|
||||||
"quest-reward_type_not_handled": "Quest reward type: {{rewardType}} not handled for quest: {{questId}} name: {{questName}}",
|
|
||||||
"quest-unable_to_find_compare_condition": "Unrecognised Comparison Method: %s",
|
"quest-unable_to_find_compare_condition": "Unrecognised Comparison Method: %s",
|
||||||
"quest-unable_to_find_matching_hideout_production": "Unable to find matching hideout craft unlock for quest: {{questName}}, matches found: {{matchCount}}",
|
|
||||||
"quest-unable_to_find_quest_in_db": "Quest id: {{questId}} with type: {{questType}} not found in database",
|
"quest-unable_to_find_quest_in_db": "Quest id: {{questId}} with type: {{questType}} not found in database",
|
||||||
"quest-unable_to_find_quest_in_db_no_quest_rewards": "Unable to find quest: %s in db, unable to give quest rewards to player",
|
"quest-unable_to_find_quest_in_db_no_quest_rewards": "Unable to find quest: %s in db, unable to give quest rewards to player",
|
||||||
"quest-unable_to_find_repeatable_to_replace": "Unable to find repeatable quest in profile to replace, skipping",
|
"quest-unable_to_find_repeatable_to_replace": "Unable to find repeatable quest in profile to replace, skipping",
|
||||||
@ -651,6 +649,8 @@
|
|||||||
"repeatable-quest_handover_failed_condition_invalid": "Quest handover error: condition not found or incorrect value. qid: {{body.qid}}, condition: {{body.conditionId}}",
|
"repeatable-quest_handover_failed_condition_invalid": "Quest handover error: condition not found or incorrect value. qid: {{body.qid}}, condition: {{body.conditionId}}",
|
||||||
"repeatable-unable_to_accept_quest_see_log": "Unable to accept quest, see server log for details",
|
"repeatable-unable_to_accept_quest_see_log": "Unable to accept quest, see server log for details",
|
||||||
"repeatable-unable_to_accept_quest_starting_message_not_found": "Unable to accept quest: {{questId}} cant find quest started message text with id: {{messageId}}",
|
"repeatable-unable_to_accept_quest_starting_message_not_found": "Unable to accept quest: {{questId}} cant find quest started message text with id: {{messageId}}",
|
||||||
|
"reward-type_not_handled": "Reward type: {{rewardType}} not handled for quest/achievement: {{questId}}",
|
||||||
|
"reward-unable_to_find_matching_hideout_production": "Unable to find matching hideout craft unlock for quest/achievement: {{questId}}, matches found: {{matchCount}}",
|
||||||
"route_onupdate_no_response": "onUpdate: %s route doesn't report success or fail",
|
"route_onupdate_no_response": "onUpdate: %s route doesn't report success or fail",
|
||||||
"scav-missing_karma_level_getting_default": "getScavKarmaLevel() failed, unable to find fence in profile.traderInfo. Defaulting to karma level 0",
|
"scav-missing_karma_level_getting_default": "getScavKarmaLevel() failed, unable to find fence in profile.traderInfo. Defaulting to karma level 0",
|
||||||
"scav-missing_karma_settings": "Unable to get karma settings for level %s",
|
"scav-missing_karma_settings": "Unable to get karma settings for level %s",
|
||||||
|
@ -120,6 +120,7 @@ import { RagfairSellHelper } from "@spt/helpers/RagfairSellHelper";
|
|||||||
import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper";
|
import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper";
|
||||||
import { RagfairSortHelper } from "@spt/helpers/RagfairSortHelper";
|
import { RagfairSortHelper } from "@spt/helpers/RagfairSortHelper";
|
||||||
import { RepairHelper } from "@spt/helpers/RepairHelper";
|
import { RepairHelper } from "@spt/helpers/RepairHelper";
|
||||||
|
import { RewardHelper } from "@spt/helpers/RewardHelper";
|
||||||
import { RepeatableQuestHelper } from "@spt/helpers/RepeatableQuestHelper";
|
import { RepeatableQuestHelper } from "@spt/helpers/RepeatableQuestHelper";
|
||||||
import { SecureContainerHelper } from "@spt/helpers/SecureContainerHelper";
|
import { SecureContainerHelper } from "@spt/helpers/SecureContainerHelper";
|
||||||
import { TradeHelper } from "@spt/helpers/TradeHelper";
|
import { TradeHelper } from "@spt/helpers/TradeHelper";
|
||||||
@ -616,6 +617,7 @@ export class Container {
|
|||||||
depContainer.register<RagfairOfferHelper>("RagfairOfferHelper", { useClass: RagfairOfferHelper });
|
depContainer.register<RagfairOfferHelper>("RagfairOfferHelper", { useClass: RagfairOfferHelper });
|
||||||
depContainer.register<RagfairServerHelper>("RagfairServerHelper", { useClass: RagfairServerHelper });
|
depContainer.register<RagfairServerHelper>("RagfairServerHelper", { useClass: RagfairServerHelper });
|
||||||
depContainer.register<RepairHelper>("RepairHelper", { useClass: RepairHelper });
|
depContainer.register<RepairHelper>("RepairHelper", { useClass: RepairHelper });
|
||||||
|
depContainer.register<RewardHelper>("RewardHelper", { useClass: RewardHelper });
|
||||||
depContainer.register<TraderHelper>("TraderHelper", TraderHelper);
|
depContainer.register<TraderHelper>("TraderHelper", TraderHelper);
|
||||||
depContainer.register<TraderAssortHelper>("TraderAssortHelper", TraderAssortHelper, {
|
depContainer.register<TraderAssortHelper>("TraderAssortHelper", TraderAssortHelper, {
|
||||||
lifecycle: Lifecycle.Singleton,
|
lifecycle: Lifecycle.Singleton,
|
||||||
|
@ -547,50 +547,6 @@ export class ProfileHelper {
|
|||||||
return pmcProfile.Info.Bans.some((ban) => ban.banType === BanType.RAGFAIR && currentTimestamp < ban.dateTime);
|
return pmcProfile.Info.Bans.some((ban) => ban.banType === BanType.RAGFAIR && currentTimestamp < ban.dateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an achievement to player profile + check for and add any hideout customisation unlocks to profile
|
|
||||||
* @param fullProfile Profile to add achievement to
|
|
||||||
* @param achievementId Id of achievement to add
|
|
||||||
*/
|
|
||||||
public addAchievementToProfile(fullProfile: ISptProfile, achievementId: string): void {
|
|
||||||
// Add achievement id to profile with timestamp it was unlocked
|
|
||||||
fullProfile.characters.pmc.Achievements[achievementId] = this.timeUtil.getTimestamp();
|
|
||||||
|
|
||||||
// Check for any customisation unlocks
|
|
||||||
const achievementDataDb = this.databaseService
|
|
||||||
.getTemplates()
|
|
||||||
.achievements.find((achievement) => achievement.id === achievementId);
|
|
||||||
if (!achievementDataDb) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get customisation reward object from achievement db
|
|
||||||
const customizationDirectReward = achievementDataDb.rewards.find(
|
|
||||||
(reward) => reward.type === "CustomizationDirect",
|
|
||||||
);
|
|
||||||
if (!customizationDirectReward) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customisationDataDb = this.databaseService
|
|
||||||
.getHideout()
|
|
||||||
.customisation.globals.find((customisation) => customisation.itemId === customizationDirectReward.target);
|
|
||||||
if (!customisationDataDb) {
|
|
||||||
this.logger.error(
|
|
||||||
`Unable to find customisation data for ${customizationDirectReward.target} in profile ${fullProfile.info.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reward found, add to profile
|
|
||||||
fullProfile.customisationUnlocks.push({
|
|
||||||
id: customizationDirectReward.target,
|
|
||||||
source: CustomisationSource.ACHIEVEMENT,
|
|
||||||
type: customisationDataDb.type as CustomisationType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasAccessToRepeatableFreeRefreshSystem(pmcProfile: IPmcData): boolean {
|
public hasAccessToRepeatableFreeRefreshSystem(pmcProfile: IPmcData): boolean {
|
||||||
return [GameEditions.EDGE_OF_DARKNESS, GameEditions.UNHEARD].includes(<any>pmcProfile.Info?.GameVersion);
|
return [GameEditions.EDGE_OF_DARKNESS, GameEditions.UNHEARD].includes(<any>pmcProfile.Info?.GameVersion);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { ItemHelper } from "@spt/helpers/ItemHelper";
|
|||||||
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
||||||
import { QuestConditionHelper } from "@spt/helpers/QuestConditionHelper";
|
import { QuestConditionHelper } from "@spt/helpers/QuestConditionHelper";
|
||||||
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
|
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
|
||||||
|
import { RewardHelper } from "@spt/helpers/RewardHelper";
|
||||||
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
||||||
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
||||||
import { Common, IQuestStatus } from "@spt/models/eft/common/tables/IBotBase";
|
import { Common, IQuestStatus } from "@spt/models/eft/common/tables/IBotBase";
|
||||||
@ -46,6 +47,7 @@ export class QuestHelper {
|
|||||||
@inject("LocaleService") protected localeService: LocaleService,
|
@inject("LocaleService") protected localeService: LocaleService,
|
||||||
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
||||||
@inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper,
|
@inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper,
|
||||||
|
@inject("RewardHelper") protected rewardHelper: RewardHelper,
|
||||||
@inject("LocalisationService") protected localisationService: LocalisationService,
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
||||||
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
|
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
|
||||||
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
||||||
@ -1060,7 +1062,7 @@ export class QuestHelper {
|
|||||||
// Remove any reward that doesn't pass the game edition check
|
// Remove any reward that doesn't pass the game edition check
|
||||||
for (const rewardType of Object.keys(quest.rewards)) {
|
for (const rewardType of Object.keys(quest.rewards)) {
|
||||||
quest.rewards[rewardType] = quest.rewards[rewardType].filter((reward: IReward) =>
|
quest.rewards[rewardType] = quest.rewards[rewardType].filter((reward: IReward) =>
|
||||||
this.questRewardHelper.questRewardIsForGameEdition(reward, gameVersion),
|
this.rewardHelper.rewardIsForGameEdition(reward, gameVersion),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,11 @@ import { PresetHelper } from "@spt/helpers/PresetHelper";
|
|||||||
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
||||||
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
||||||
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
||||||
import { CustomisationSource } from "@spt/models/eft/common/tables/ICustomisationStorage";
|
|
||||||
import { IItem } from "@spt/models/eft/common/tables/IItem";
|
import { IItem } from "@spt/models/eft/common/tables/IItem";
|
||||||
import { IQuest } from "@spt/models/eft/common/tables/IQuest";
|
import { IQuest } from "@spt/models/eft/common/tables/IQuest";
|
||||||
import { IReward } from "@spt/models/eft/common/tables/IReward";
|
import { IReward } from "@spt/models/eft/common/tables/IReward";
|
||||||
import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
|
|
||||||
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
|
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
|
||||||
import { QuestStatus } from "@spt/models/enums/QuestStatus";
|
import { QuestStatus } from "@spt/models/enums/QuestStatus";
|
||||||
import { RewardType } from "@spt/models/enums/RewardType";
|
|
||||||
import { SkillTypes } from "@spt/models/enums/SkillTypes";
|
import { SkillTypes } from "@spt/models/enums/SkillTypes";
|
||||||
import type { ILogger } from "@spt/models/spt/utils/ILogger";
|
import type { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||||
import { DatabaseService } from "@spt/services/DatabaseService";
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
||||||
@ -19,6 +16,8 @@ import { LocalisationService } from "@spt/services/LocalisationService";
|
|||||||
import { HashUtil } from "@spt/utils/HashUtil";
|
import { HashUtil } from "@spt/utils/HashUtil";
|
||||||
import type { ICloner } from "@spt/utils/cloners/ICloner";
|
import type { ICloner } from "@spt/utils/cloners/ICloner";
|
||||||
import { inject, injectable } from "tsyringe";
|
import { inject, injectable } from "tsyringe";
|
||||||
|
import { RewardHelper } from "@spt/helpers/RewardHelper";
|
||||||
|
import { CustomisationSource } from "@spt/models/eft/common/tables/ICustomisationStorage";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class QuestRewardHelper {
|
export class QuestRewardHelper {
|
||||||
@ -33,6 +32,7 @@ export class QuestRewardHelper {
|
|||||||
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
||||||
@inject("PresetHelper") protected presetHelper: PresetHelper,
|
@inject("PresetHelper") protected presetHelper: PresetHelper,
|
||||||
@inject("PrimaryCloner") protected cloner: ICloner,
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
||||||
|
@inject("RewardHelper") protected rewardHelper: RewardHelper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,99 +78,15 @@ export class QuestRewardHelper {
|
|||||||
|
|
||||||
// e.g. 'Success' or 'AvailableForFinish'
|
// e.g. 'Success' or 'AvailableForFinish'
|
||||||
const questStateAsString = QuestStatus[state];
|
const questStateAsString = QuestStatus[state];
|
||||||
const gameVersion = pmcProfile.Info.GameVersion;
|
const rewards = <IReward[]>questDetails.rewards[questStateAsString];
|
||||||
for (const reward of <IReward[]>questDetails.rewards[questStateAsString]) {
|
return this.rewardHelper.applyRewards(
|
||||||
// Handle quest reward availability for different game versions, notAvailableInGameEditions currently not used
|
rewards,
|
||||||
if (!this.questRewardIsForGameEdition(reward, gameVersion)) {
|
CustomisationSource.UNLOCKED_IN_GAME,
|
||||||
continue;
|
fullProfile,
|
||||||
}
|
|
||||||
|
|
||||||
switch (reward.type) {
|
|
||||||
case RewardType.SKILL:
|
|
||||||
this.profileHelper.addSkillPointsToPlayer(
|
|
||||||
profileData,
|
profileData,
|
||||||
reward.target as SkillTypes,
|
questId,
|
||||||
Number(reward.value),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case RewardType.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 RewardType.TRADER_STANDING:
|
|
||||||
this.traderHelper.addStandingToTrader(
|
|
||||||
sessionId,
|
|
||||||
reward.target,
|
|
||||||
Number.parseFloat(<string>reward.value),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case RewardType.TRADER_UNLOCK:
|
|
||||||
this.traderHelper.setTraderUnlockedState(reward.target, true, sessionId);
|
|
||||||
break;
|
|
||||||
case RewardType.ITEM:
|
|
||||||
// Handled by getQuestRewardItems() below
|
|
||||||
break;
|
|
||||||
case RewardType.ASSORTMENT_UNLOCK:
|
|
||||||
// Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player
|
|
||||||
break;
|
|
||||||
case RewardType.ACHIEVEMENT:
|
|
||||||
this.profileHelper.addAchievementToProfile(fullProfile, reward.target);
|
|
||||||
break;
|
|
||||||
case RewardType.STASH_ROWS:
|
|
||||||
this.profileHelper.addStashRowsBonusToProfile(sessionId, Number.parseInt(<string>reward.value)); // Add specified stash rows from quest reward - requires client restart
|
|
||||||
break;
|
|
||||||
case RewardType.PRODUCTIONS_SCHEME:
|
|
||||||
this.findAndAddHideoutProductionIdToProfile(
|
|
||||||
pmcProfile,
|
|
||||||
reward,
|
|
||||||
questDetails,
|
|
||||||
sessionId,
|
|
||||||
questResponse,
|
questResponse,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case RewardType.POCKETS:
|
|
||||||
this.profileHelper.replaceProfilePocketTpl(pmcProfile, reward.target);
|
|
||||||
break;
|
|
||||||
case RewardType.CUSTOMIZATION_DIRECT:
|
|
||||||
this.profileHelper.addHideoutCustomisationUnlock(
|
|
||||||
fullProfile,
|
|
||||||
reward,
|
|
||||||
CustomisationSource.UNLOCKED_IN_GAME,
|
|
||||||
);
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
public questRewardIsForGameEdition(reward: IReward, gameVersion: string): boolean {
|
|
||||||
if (reward.availableInGameEditions?.length > 0 && !reward.availableInGameEditions?.includes(gameVersion)) {
|
|
||||||
// Reward has edition whitelist and game version isnt in it
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reward.notAvailableInGameEditions?.length > 0 && reward.notAvailableInGameEditions?.includes(gameVersion)) {
|
|
||||||
// Reward has edition blacklist and game version is in it
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No whitelist/blacklist or reward isnt blacklisted/whitelisted
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -228,7 +144,8 @@ export class QuestRewardHelper {
|
|||||||
* @returns Updated quest
|
* @returns Updated quest
|
||||||
*/
|
*/
|
||||||
public applyMoneyBoost(quest: IQuest, bonusPercent: number, questStatus: QuestStatus): IQuest {
|
public applyMoneyBoost(quest: IQuest, bonusPercent: number, questStatus: QuestStatus): IQuest {
|
||||||
const rewards: IReward[] = quest.rewards?.[QuestStatus[questStatus]] ?? [];
|
const clonedQuest = this.cloner.clone(quest);
|
||||||
|
const rewards: IReward[] = clonedQuest.rewards?.[QuestStatus[questStatus]] ?? [];
|
||||||
const currencyRewards = rewards.filter(
|
const currencyRewards = rewards.filter(
|
||||||
(reward) => reward.type === "Item" && this.paymentHelper.isMoneyTpl(reward.items[0]._tpl),
|
(reward) => reward.type === "Item" && this.paymentHelper.isMoneyTpl(reward.items[0]._tpl),
|
||||||
);
|
);
|
||||||
@ -240,205 +157,6 @@ export class QuestRewardHelper {
|
|||||||
reward.value = newCurrencyAmount;
|
reward.value = newCurrencyAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return quest;
|
return clonedQuest;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: IReward,
|
|
||||||
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: IReward, 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: IReward) =>
|
|
||||||
reward.type === "Item" && this.questRewardIsForGameEdition(reward, gameVersion)
|
|
||||||
? this.processReward(reward)
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
return questRewards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take reward item from quest and set FiR status + fix stack sizes + fix mod Ids
|
|
||||||
* @param questReward Reward item to fix
|
|
||||||
* @returns Fixed rewards
|
|
||||||
*/
|
|
||||||
protected processReward(questReward: IReward): IItem[] {
|
|
||||||
/** item with mods to return */
|
|
||||||
let rewardItems: IItem[] = [];
|
|
||||||
let targets: IItem[] = [];
|
|
||||||
const mods: IItem[] = [];
|
|
||||||
|
|
||||||
// Is armor item that may need inserts / plates
|
|
||||||
if (questReward.items.length === 1 && this.itemHelper.armorItemCanHoldMods(questReward.items[0]._tpl)) {
|
|
||||||
// Only process items with slots
|
|
||||||
if (this.itemHelper.itemHasSlots(questReward.items[0]._tpl)) {
|
|
||||||
// Attempt to pull default preset from globals and add child items to reward (clones questReward.items)
|
|
||||||
this.generateArmorRewardChildSlots(questReward.items[0], questReward);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rewardItem of questReward.items) {
|
|
||||||
this.itemHelper.addUpdObjectToItem(rewardItem);
|
|
||||||
|
|
||||||
// Reward items are granted Found in Raid status
|
|
||||||
rewardItem.upd.SpawnedInSession = true;
|
|
||||||
|
|
||||||
// Is root item, fix stacks
|
|
||||||
if (rewardItem._id === questReward.target) {
|
|
||||||
// Is base reward item
|
|
||||||
if (
|
|
||||||
rewardItem.parentId !== undefined &&
|
|
||||||
rewardItem.parentId === "hideout" && // Has parentId of hideout
|
|
||||||
rewardItem.upd !== undefined &&
|
|
||||||
rewardItem.upd.StackObjectsCount !== undefined && // Has upd with stackobject count
|
|
||||||
rewardItem.upd.StackObjectsCount > 1 // More than 1 item in stack
|
|
||||||
) {
|
|
||||||
rewardItem.upd.StackObjectsCount = 1;
|
|
||||||
}
|
|
||||||
targets = this.itemHelper.splitStack(rewardItem);
|
|
||||||
// splitStack created new ids for the new stacks. This would destroy the relation to possible children.
|
|
||||||
// Instead, we reset the id to preserve relations and generate a new id in the downstream loop, where we are also reparenting if required
|
|
||||||
for (const target of targets) {
|
|
||||||
target._id = rewardItem._id;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Is child mod
|
|
||||||
if (questReward.items[0].upd.SpawnedInSession) {
|
|
||||||
// Propigate FiR status into child items
|
|
||||||
rewardItem.upd.SpawnedInSession = questReward.items[0].upd.SpawnedInSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
mods.push(rewardItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add mods to the base items, fix ids
|
|
||||||
for (const target of targets) {
|
|
||||||
// This has all the original id relations since we reset the id to the original after the splitStack
|
|
||||||
const itemsClone = [this.cloner.clone(target)];
|
|
||||||
// Here we generate a new id for the root item
|
|
||||||
target._id = this.hashUtil.generate();
|
|
||||||
|
|
||||||
for (const mod of mods) {
|
|
||||||
itemsClone.push(this.cloner.clone(mod));
|
|
||||||
}
|
|
||||||
|
|
||||||
rewardItems = rewardItems.concat(this.itemHelper.reparentItemAndChildren(target, itemsClone));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewardItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add missing mod items to a quest armor reward
|
|
||||||
* @param originalRewardRootItem Original armor reward item from IReward.items object
|
|
||||||
* @param questReward Armor reward from quest
|
|
||||||
*/
|
|
||||||
protected generateArmorRewardChildSlots(originalRewardRootItem: IItem, questReward: IReward): void {
|
|
||||||
// Look for a default preset from globals for armor
|
|
||||||
const defaultPreset = this.presetHelper.getDefaultPreset(originalRewardRootItem._tpl);
|
|
||||||
if (defaultPreset) {
|
|
||||||
// Found preset, use mods to hydrate reward item
|
|
||||||
const presetAndMods: IItem[] = this.itemHelper.replaceIDs(defaultPreset._items);
|
|
||||||
const newRootId = this.itemHelper.remapRootItemId(presetAndMods);
|
|
||||||
|
|
||||||
questReward.items = presetAndMods;
|
|
||||||
|
|
||||||
// Find root item and set its stack count
|
|
||||||
const rootItem = questReward.items.find((item) => item._id === newRootId);
|
|
||||||
|
|
||||||
// Remap target id to the new presets root id
|
|
||||||
questReward.target = rootItem._id;
|
|
||||||
|
|
||||||
// Copy over stack count otherwise reward shows as missing in client
|
|
||||||
this.itemHelper.addUpdObjectToItem(rootItem);
|
|
||||||
|
|
||||||
rootItem.upd.StackObjectsCount = originalRewardRootItem.upd.StackObjectsCount;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warning(
|
|
||||||
`Unable to find default preset for armor ${originalRewardRootItem._tpl}, adding mods manually`,
|
|
||||||
);
|
|
||||||
const itemDbData = this.itemHelper.getItem(originalRewardRootItem._tpl)[1];
|
|
||||||
|
|
||||||
// Hydrate reward with only 'required' mods - necessary for things like helmets otherwise you end up with nvgs/visors etc
|
|
||||||
questReward.items = this.itemHelper.addChildSlotItems(questReward.items, itemDbData, undefined, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
375
project/src/helpers/RewardHelper.ts
Normal file
375
project/src/helpers/RewardHelper.ts
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
||||||
|
import { PresetHelper } from "@spt/helpers/PresetHelper";
|
||||||
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
||||||
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
||||||
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
||||||
|
import { CustomisationSource } from "@spt/models/eft/common/tables/ICustomisationStorage";
|
||||||
|
import { IItem } from "@spt/models/eft/common/tables/IItem";
|
||||||
|
import { IReward } from "@spt/models/eft/common/tables/IReward";
|
||||||
|
import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
|
||||||
|
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
|
||||||
|
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
|
||||||
|
import { RewardType } from "@spt/models/enums/RewardType";
|
||||||
|
import { SkillTypes } from "@spt/models/enums/SkillTypes";
|
||||||
|
import type { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||||
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
||||||
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
||||||
|
import { HashUtil } from "@spt/utils/HashUtil";
|
||||||
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
||||||
|
import type { ICloner } from "@spt/utils/cloners/ICloner";
|
||||||
|
import { inject, injectable } from "tsyringe";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class RewardHelper {
|
||||||
|
constructor(
|
||||||
|
@inject("PrimaryLogger") protected logger: ILogger,
|
||||||
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
||||||
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
||||||
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
||||||
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
||||||
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
||||||
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
||||||
|
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
||||||
|
@inject("PresetHelper") protected presetHelper: PresetHelper,
|
||||||
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the given rewards to the passed in profile
|
||||||
|
* @param rewards List of rewards to apply
|
||||||
|
* @param source The source of the rewards (Achievement, quest)
|
||||||
|
* @param fullProfile The full profile to apply the rewards to
|
||||||
|
* @param questId The quest or achievement ID, used for finding production unlocks
|
||||||
|
* @param questResponse Response to quest completion when a production is unlocked
|
||||||
|
* @returns List of items that were rewarded
|
||||||
|
*/
|
||||||
|
public applyRewards(
|
||||||
|
rewards: IReward[],
|
||||||
|
source: CustomisationSource,
|
||||||
|
fullProfile: ISptProfile,
|
||||||
|
profileData: IPmcData,
|
||||||
|
questId: string,
|
||||||
|
questResponse?: IItemEventRouterResponse,
|
||||||
|
): IItem[] {
|
||||||
|
const sessionId = fullProfile?.info?.id;
|
||||||
|
const pmcProfile = fullProfile?.characters.pmc;
|
||||||
|
if (!pmcProfile) {
|
||||||
|
this.logger.error(`Unable to get pmc profile for: ${sessionId}, no rewards given`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameVersion = pmcProfile.Info.GameVersion;
|
||||||
|
|
||||||
|
for (const reward of rewards) {
|
||||||
|
// Handle reward availability for different game versions, notAvailableInGameEditions currently not used
|
||||||
|
if (!this.rewardIsForGameEdition(reward, gameVersion)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (reward.type) {
|
||||||
|
case RewardType.SKILL:
|
||||||
|
// This needs to use the passed in profileData, as it could be the scav profile
|
||||||
|
this.profileHelper.addSkillPointsToPlayer(
|
||||||
|
profileData,
|
||||||
|
reward.target as SkillTypes,
|
||||||
|
Number(reward.value),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RewardType.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 RewardType.TRADER_STANDING:
|
||||||
|
this.traderHelper.addStandingToTrader(
|
||||||
|
sessionId,
|
||||||
|
reward.target,
|
||||||
|
Number.parseFloat(<string>reward.value),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RewardType.TRADER_UNLOCK:
|
||||||
|
this.traderHelper.setTraderUnlockedState(reward.target, true, sessionId);
|
||||||
|
break;
|
||||||
|
case RewardType.ITEM:
|
||||||
|
// Item rewards are retrieved by getRewardItems() below, and returned to be handled by caller
|
||||||
|
break;
|
||||||
|
case RewardType.ASSORTMENT_UNLOCK:
|
||||||
|
// Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player
|
||||||
|
break;
|
||||||
|
case RewardType.ACHIEVEMENT:
|
||||||
|
this.addAchievementToProfile(fullProfile, reward.target);
|
||||||
|
break;
|
||||||
|
case RewardType.STASH_ROWS:
|
||||||
|
this.profileHelper.addStashRowsBonusToProfile(sessionId, Number.parseInt(<string>reward.value)); // Add specified stash rows from reward - requires client restart
|
||||||
|
break;
|
||||||
|
case RewardType.PRODUCTIONS_SCHEME:
|
||||||
|
this.findAndAddHideoutProductionIdToProfile(pmcProfile, reward, questId, sessionId, questResponse);
|
||||||
|
break;
|
||||||
|
case RewardType.POCKETS:
|
||||||
|
this.profileHelper.replaceProfilePocketTpl(pmcProfile, reward.target);
|
||||||
|
break;
|
||||||
|
case RewardType.CUSTOMIZATION_DIRECT:
|
||||||
|
this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, source);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.logger.error(
|
||||||
|
this.localisationService.getText("reward-type_not_handled", {
|
||||||
|
rewardType: reward.type,
|
||||||
|
questId: questId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getRewardItems(rewards, gameVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the provided 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
|
||||||
|
*/
|
||||||
|
public rewardIsForGameEdition(reward: IReward, gameVersion: string): boolean {
|
||||||
|
if (reward.availableInGameEditions?.length > 0 && !reward.availableInGameEditions?.includes(gameVersion)) {
|
||||||
|
// Reward has edition whitelist and game version isnt in it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reward.notAvailableInGameEditions?.length > 0 && reward.notAvailableInGameEditions?.includes(gameVersion)) {
|
||||||
|
// Reward has edition blacklist and game version is in it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No whitelist/blacklist or reward isnt blacklisted/whitelisted
|
||||||
|
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 with craft unlock details
|
||||||
|
* @param questId Quest or achievement ID with craft unlock reward
|
||||||
|
* @param sessionID Session id
|
||||||
|
* @param response Response to send back to client
|
||||||
|
*/
|
||||||
|
protected findAndAddHideoutProductionIdToProfile(
|
||||||
|
pmcData: IPmcData,
|
||||||
|
craftUnlockReward: IReward,
|
||||||
|
questId: string,
|
||||||
|
sessionID: string,
|
||||||
|
response?: IItemEventRouterResponse,
|
||||||
|
): void {
|
||||||
|
const matchingProductions = this.getRewardProductionMatch(craftUnlockReward, questId);
|
||||||
|
if (matchingProductions.length !== 1) {
|
||||||
|
this.logger.error(
|
||||||
|
this.localisationService.getText("reward-unable_to_find_matching_hideout_production", {
|
||||||
|
questId: questId,
|
||||||
|
matchCount: matchingProductions.length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add above match to pmc profile + client response
|
||||||
|
const matchingCraftId = matchingProductions[0]._id;
|
||||||
|
pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId);
|
||||||
|
if (response) {
|
||||||
|
response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find hideout craft for the specified reward
|
||||||
|
* @param craftUnlockReward Reward with craft unlock details
|
||||||
|
* @param questId Quest or achievement ID with craft unlock reward
|
||||||
|
* @returns Hideout craft
|
||||||
|
*/
|
||||||
|
public getRewardProductionMatch(craftUnlockReward: IReward, questId: string): 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 === questId) && // 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 === questId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingProductions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a flat list of reward items from the given rewards for the specified game version
|
||||||
|
* @param rewards Array of rewards to get the items from
|
||||||
|
* @param gameVersion The game version of the profile
|
||||||
|
* @returns array of items with the correct maxStack
|
||||||
|
*/
|
||||||
|
protected getRewardItems(rewards: IReward[], gameVersion: string): IItem[] {
|
||||||
|
// Iterate over all rewards with the desired status, flatten out items that have a type of Item
|
||||||
|
const rewardItems = rewards.flatMap((reward: IReward) =>
|
||||||
|
reward.type === "Item" && this.rewardIsForGameEdition(reward, gameVersion)
|
||||||
|
? this.processReward(reward)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return rewardItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take reward item and set FiR status + fix stack sizes + fix mod Ids
|
||||||
|
* @param reward Reward item to fix
|
||||||
|
* @returns Fixed rewards
|
||||||
|
*/
|
||||||
|
protected processReward(reward: IReward): IItem[] {
|
||||||
|
/** item with mods to return */
|
||||||
|
let rewardItems: IItem[] = [];
|
||||||
|
let targets: IItem[] = [];
|
||||||
|
const mods: IItem[] = [];
|
||||||
|
|
||||||
|
// Is armor item that may need inserts / plates
|
||||||
|
if (reward.items.length === 1 && this.itemHelper.armorItemCanHoldMods(reward.items[0]._tpl)) {
|
||||||
|
// Only process items with slots
|
||||||
|
if (this.itemHelper.itemHasSlots(reward.items[0]._tpl)) {
|
||||||
|
// Attempt to pull default preset from globals and add child items to reward (clones reward.items)
|
||||||
|
this.generateArmorRewardChildSlots(reward.items[0], reward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rewardItem of reward.items) {
|
||||||
|
this.itemHelper.addUpdObjectToItem(rewardItem);
|
||||||
|
|
||||||
|
// Reward items are granted Found in Raid status
|
||||||
|
rewardItem.upd.SpawnedInSession = true;
|
||||||
|
|
||||||
|
// Is root item, fix stacks
|
||||||
|
if (rewardItem._id === reward.target) {
|
||||||
|
// Is base reward item
|
||||||
|
if (
|
||||||
|
rewardItem.parentId !== undefined &&
|
||||||
|
rewardItem.parentId === "hideout" && // Has parentId of hideout
|
||||||
|
rewardItem.upd !== undefined &&
|
||||||
|
rewardItem.upd.StackObjectsCount !== undefined && // Has upd with stackobject count
|
||||||
|
rewardItem.upd.StackObjectsCount > 1 // More than 1 item in stack
|
||||||
|
) {
|
||||||
|
rewardItem.upd.StackObjectsCount = 1;
|
||||||
|
}
|
||||||
|
targets = this.itemHelper.splitStack(rewardItem);
|
||||||
|
// splitStack created new ids for the new stacks. This would destroy the relation to possible children.
|
||||||
|
// Instead, we reset the id to preserve relations and generate a new id in the downstream loop, where we are also reparenting if required
|
||||||
|
for (const target of targets) {
|
||||||
|
target._id = rewardItem._id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Is child mod
|
||||||
|
if (reward.items[0].upd.SpawnedInSession) {
|
||||||
|
// Propigate FiR status into child items
|
||||||
|
rewardItem.upd.SpawnedInSession = reward.items[0].upd.SpawnedInSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
mods.push(rewardItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add mods to the base items, fix ids
|
||||||
|
for (const target of targets) {
|
||||||
|
// This has all the original id relations since we reset the id to the original after the splitStack
|
||||||
|
const itemsClone = [this.cloner.clone(target)];
|
||||||
|
// Here we generate a new id for the root item
|
||||||
|
target._id = this.hashUtil.generate();
|
||||||
|
|
||||||
|
for (const mod of mods) {
|
||||||
|
itemsClone.push(this.cloner.clone(mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardItems = rewardItems.concat(this.itemHelper.reparentItemAndChildren(target, itemsClone));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewardItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add missing mod items to an armor reward
|
||||||
|
* @param originalRewardRootItem Original armor reward item from IReward.items object
|
||||||
|
* @param reward Armor reward
|
||||||
|
*/
|
||||||
|
protected generateArmorRewardChildSlots(originalRewardRootItem: IItem, reward: IReward): void {
|
||||||
|
// Look for a default preset from globals for armor
|
||||||
|
const defaultPreset = this.presetHelper.getDefaultPreset(originalRewardRootItem._tpl);
|
||||||
|
if (defaultPreset) {
|
||||||
|
// Found preset, use mods to hydrate reward item
|
||||||
|
const presetAndMods: IItem[] = this.itemHelper.replaceIDs(defaultPreset._items);
|
||||||
|
const newRootId = this.itemHelper.remapRootItemId(presetAndMods);
|
||||||
|
|
||||||
|
reward.items = presetAndMods;
|
||||||
|
|
||||||
|
// Find root item and set its stack count
|
||||||
|
const rootItem = reward.items.find((item) => item._id === newRootId);
|
||||||
|
|
||||||
|
// Remap target id to the new presets root id
|
||||||
|
reward.target = rootItem._id;
|
||||||
|
|
||||||
|
// Copy over stack count otherwise reward shows as missing in client
|
||||||
|
this.itemHelper.addUpdObjectToItem(rootItem);
|
||||||
|
|
||||||
|
rootItem.upd.StackObjectsCount = originalRewardRootItem.upd.StackObjectsCount;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warning(
|
||||||
|
`Unable to find default preset for armor ${originalRewardRootItem._tpl}, adding mods manually`,
|
||||||
|
);
|
||||||
|
const itemDbData = this.itemHelper.getItem(originalRewardRootItem._tpl)[1];
|
||||||
|
|
||||||
|
// Hydrate reward with only 'required' mods - necessary for things like helmets otherwise you end up with nvgs/visors etc
|
||||||
|
reward.items = this.itemHelper.addChildSlotItems(reward.items, itemDbData, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an achievement to player profile and handle any rewards for the achievement
|
||||||
|
* Triggered from a quest, or another achievement
|
||||||
|
* @param fullProfile Profile to add achievement to
|
||||||
|
* @param achievementId Id of achievement to add
|
||||||
|
*/
|
||||||
|
public addAchievementToProfile(fullProfile: ISptProfile, achievementId: string): void {
|
||||||
|
// Add achievement id to profile with timestamp it was unlocked
|
||||||
|
fullProfile.characters.pmc.Achievements[achievementId] = this.timeUtil.getTimestamp();
|
||||||
|
|
||||||
|
// Check for any customisation unlocks
|
||||||
|
const achievementDataDb = this.databaseService
|
||||||
|
.getTemplates()
|
||||||
|
.achievements.find((achievement) => achievement.id === achievementId);
|
||||||
|
if (!achievementDataDb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: At the moment, we don't know the exact quest and achievement data layout for an achievement
|
||||||
|
// that is triggered by a quest, that gives an item, because BSG has only done this once. However
|
||||||
|
// based on deduction, I am going to assume that the *quest* will handle the initial item reward,
|
||||||
|
// and the achievement reward should only be handled post-wipe.
|
||||||
|
// All of that is to say, we are going to ignore the list of returned reward items here
|
||||||
|
const pmcProfile = fullProfile.characters.pmc;
|
||||||
|
this.applyRewards(
|
||||||
|
achievementDataDb.rewards,
|
||||||
|
CustomisationSource.ACHIEVEMENT,
|
||||||
|
fullProfile,
|
||||||
|
pmcProfile,
|
||||||
|
achievementDataDb.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -51,7 +51,8 @@ import { RandomUtil } from "@spt/utils/RandomUtil";
|
|||||||
import { TimeUtil } from "@spt/utils/TimeUtil";
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
||||||
import type { ICloner } from "@spt/utils/cloners/ICloner";
|
import type { ICloner } from "@spt/utils/cloners/ICloner";
|
||||||
import { inject, injectable } from "tsyringe";
|
import { inject, injectable } from "tsyringe";
|
||||||
import { TransitionType } from "../models/enums/TransitionType";
|
import { TransitionType } from "@spt/models/enums/TransitionType";
|
||||||
|
import { RewardHelper } from "@spt/helpers/RewardHelper";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LocationLifecycleService {
|
export class LocationLifecycleService {
|
||||||
@ -73,6 +74,7 @@ export class LocationLifecycleService {
|
|||||||
@inject("InRaidHelper") protected inRaidHelper: InRaidHelper,
|
@inject("InRaidHelper") protected inRaidHelper: InRaidHelper,
|
||||||
@inject("HealthHelper") protected healthHelper: HealthHelper,
|
@inject("HealthHelper") protected healthHelper: HealthHelper,
|
||||||
@inject("QuestHelper") protected questHelper: QuestHelper,
|
@inject("QuestHelper") protected questHelper: QuestHelper,
|
||||||
|
@inject("RewardHelper") protected rewardHelper: RewardHelper,
|
||||||
@inject("MatchBotDetailsCacheService") protected matchBotDetailsCacheService: MatchBotDetailsCacheService,
|
@inject("MatchBotDetailsCacheService") protected matchBotDetailsCacheService: MatchBotDetailsCacheService,
|
||||||
@inject("PmcChatResponseService") protected pmcChatResponseService: PmcChatResponseService,
|
@inject("PmcChatResponseService") protected pmcChatResponseService: PmcChatResponseService,
|
||||||
@inject("PlayerScavGenerator") protected playerScavGenerator: PlayerScavGenerator,
|
@inject("PlayerScavGenerator") protected playerScavGenerator: PlayerScavGenerator,
|
||||||
@ -666,7 +668,7 @@ export class LocationLifecycleService {
|
|||||||
pmcProfile.SurvivorClass = postRaidProfile.SurvivorClass;
|
pmcProfile.SurvivorClass = postRaidProfile.SurvivorClass;
|
||||||
|
|
||||||
// MUST occur prior to profile achievements being overwritten by post-raid achievements
|
// MUST occur prior to profile achievements being overwritten by post-raid achievements
|
||||||
this.processAchievementCustomisationRewards(fullProfile, postRaidProfile.Achievements);
|
this.processAchievementRewards(fullProfile, postRaidProfile.Achievements);
|
||||||
|
|
||||||
pmcProfile.Achievements = postRaidProfile.Achievements;
|
pmcProfile.Achievements = postRaidProfile.Achievements;
|
||||||
pmcProfile.Quests = this.processPostRaidQuests(postRaidProfile.Quests);
|
pmcProfile.Quests = this.processPostRaidQuests(postRaidProfile.Quests);
|
||||||
@ -730,14 +732,13 @@ export class LocationLifecycleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for and add any customisations found via the gained achievements this raid
|
* Check for and add any rewards found via the gained achievements this raid
|
||||||
* @param fullProfile Profile to add customisations to
|
* @param fullProfile Profile to add customisations to
|
||||||
* @param postRaidAchievements Achievements gained this raid
|
* @param postRaidAchievements All profile achievements at the end of the raid
|
||||||
*/
|
*/
|
||||||
protected processAchievementCustomisationRewards(
|
protected processAchievementRewards(fullProfile: ISptProfile, postRaidAchievements: Record<string, number>): void {
|
||||||
fullProfile: ISptProfile,
|
const sessionId = fullProfile.info.id;
|
||||||
postRaidAchievements: Record<string, number>,
|
const pmcProfile = fullProfile.characters.pmc;
|
||||||
): void {
|
|
||||||
const preRaidAchievementIds = Object.keys(fullProfile.characters.pmc.Achievements);
|
const preRaidAchievementIds = Object.keys(fullProfile.characters.pmc.Achievements);
|
||||||
const postRaidAchievementIds = Object.keys(postRaidAchievements);
|
const postRaidAchievementIds = Object.keys(postRaidAchievements);
|
||||||
const achievementIdsAcquiredThisRaid = postRaidAchievementIds.filter(
|
const achievementIdsAcquiredThisRaid = postRaidAchievementIds.filter(
|
||||||
@ -756,14 +757,23 @@ export class LocationLifecycleService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get only customisation rewards from above achievements
|
for (const achievement of achievements) {
|
||||||
const customisationRewards = achievements
|
const rewardItems = this.rewardHelper.applyRewards(
|
||||||
.filter((achievement) => achievement?.rewards.some((reward) => reward.type === "CustomizationDirect"))
|
achievement.rewards,
|
||||||
.flatMap((achievement) => achievement?.rewards);
|
CustomisationSource.ACHIEVEMENT,
|
||||||
|
fullProfile,
|
||||||
// Insert customisations into profile
|
pmcProfile,
|
||||||
for (const reward of customisationRewards) {
|
achievement.id,
|
||||||
this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, CustomisationSource.ACHIEVEMENT);
|
);
|
||||||
|
if (rewardItems?.length > 0) {
|
||||||
|
this.mailSendService.sendLocalisedSystemMessageToPlayer(
|
||||||
|
sessionId,
|
||||||
|
"670547bb5fa0b1a7c30d5836 0",
|
||||||
|
rewardItems,
|
||||||
|
[],
|
||||||
|
this.timeUtil.getHoursAsSeconds(24 * 7),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { HideoutHelper } from "@spt/helpers/HideoutHelper";
|
|||||||
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
|
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
|
||||||
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
||||||
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
||||||
import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper";
|
import { RewardHelper } from "@spt/helpers/RewardHelper";
|
||||||
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
||||||
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
||||||
import { IBonus, IHideoutSlot } from "@spt/models/eft/common/tables/IBotBase";
|
import { IBonus, IHideoutSlot } from "@spt/models/eft/common/tables/IBotBase";
|
||||||
@ -51,7 +51,7 @@ export class ProfileFixerService {
|
|||||||
@inject("HashUtil") protected hashUtil: HashUtil,
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
||||||
@inject("ConfigServer") protected configServer: ConfigServer,
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
||||||
@inject("PrimaryCloner") protected cloner: ICloner,
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
||||||
@inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper,
|
@inject("RewardHelper") protected rewardHelper: RewardHelper,
|
||||||
) {
|
) {
|
||||||
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
|
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
|
||||||
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
|
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
|
||||||
@ -365,9 +365,9 @@ export class ProfileFixerService {
|
|||||||
productionUnlockReward: IReward,
|
productionUnlockReward: IReward,
|
||||||
questDetails: IQuest,
|
questDetails: IQuest,
|
||||||
): void {
|
): void {
|
||||||
const matchingProductions = this.questRewardHelper.getRewardProductionMatch(
|
const matchingProductions = this.rewardHelper.getRewardProductionMatch(
|
||||||
productionUnlockReward,
|
productionUnlockReward,
|
||||||
questDetails,
|
questDetails._id,
|
||||||
);
|
);
|
||||||
if (matchingProductions.length !== 1) {
|
if (matchingProductions.length !== 1) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user