0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00
server/project/src/controllers/RepeatableQuestController.ts
HiddenCirno 7a05acacdd fix free change still deduct trader standing (!379)
Fix that free change repeatable chance quest still deduct trader standing, now it will deduct when player have no access to free change or have no free change counts

Reviewed-on: SPT/Server#379
Co-authored-by: HiddenCirno <2301697863@qq.com>
Co-committed-by: HiddenCirno <2301697863@qq.com>
(cherry picked from commit dcb98f7d1b21ccde76d0a6c26514a718290b19b6)
2024-07-21 09:03:12 +01:00

743 lines
30 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { RepeatableQuestGenerator } from "@spt/generators/RepeatableQuestGenerator";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { RepeatableQuestHelper } from "@spt/helpers/RepeatableQuestHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import {
IPmcDataRepeatableQuest,
IRepeatableQuest,
} from "@spt/models/eft/common/tables/IRepeatableQuests";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
import { IRepeatableQuestChangeRequest } from "@spt/models/eft/quests/IRepeatableQuestChangeRequest";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ELocationName } from "@spt/models/enums/ELocationName";
import { HideoutAreas } from "@spt/models/enums/HideoutAreas";
import { QuestStatus } from "@spt/models/enums/QuestStatus";
import { SkillTypes } from "@spt/models/enums/SkillTypes";
import { IQuestConfig, IRepeatableQuestConfig } from "@spt/models/spt/config/IQuestConfig";
import { IGetRepeatableByIdResult } from "@spt/models/spt/quests/IGetRepeatableByIdResult";
import { IQuestTypePool } from "@spt/models/spt/repeatable/IQuestTypePool";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { PaymentService } from "@spt/services/PaymentService";
import { ProfileFixerService } from "@spt/services/ProfileFixerService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { ObjectId } from "@spt/utils/ObjectId";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
@injectable()
export class RepeatableQuestController
{
protected questConfig: IQuestConfig;
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("DatabaseService") protected databaseService: DatabaseService,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("ProfileFixerService") protected profileFixerService: ProfileFixerService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("PaymentService") protected paymentService: PaymentService,
@inject("ObjectId") protected objectId: ObjectId,
@inject("RepeatableQuestGenerator") protected repeatableQuestGenerator: RepeatableQuestGenerator,
@inject("RepeatableQuestHelper") protected repeatableQuestHelper: RepeatableQuestHelper,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
}
/**
* Handle client/repeatalbeQuests/activityPeriods
* Returns an array of objects in the format of repeatable quests to the client.
* repeatableQuestObject = {
* id: Unique Id,
* name: "Daily",
* endTime: the time when the quests expire
* activeQuests: currently available quests in an array. Each element of quest type format (see assets/database/templates/repeatableQuests.json).
* inactiveQuests: the quests which were previously active (required by client to fail them if they are not completed)
* }
*
* The method checks if the player level requirement for repeatable quests (e.g. daily lvl5, weekly lvl15) is met and if the previously active quests
* are still valid. This ischecked by endTime persisted in profile accordning to the resetTime configured for each repeatable kind (daily, weekly)
* in QuestCondig.js
*
* If the condition is met, new repeatableQuests are created, old quests (which are persisted in the profile.RepeatableQuests[i].activeQuests) are
* moved to profile.RepeatableQuests[i].inactiveQuests. This memory is required to get rid of old repeatable quest data in the profile, otherwise
* they'll litter the profile's Quests field.
* (if the are on "Succeed" but not "Completed" we keep them, to allow the player to complete them and get the rewards)
* The new quests generated are again persisted in profile.RepeatableQuests
*
* @param {string} sessionID Player's session id
*
* @returns {array} Array of "repeatableQuestObjects" as described above
*/
public getClientRepeatableQuests(sessionID: string): IPmcDataRepeatableQuest[]
{
const returnData: Array<IPmcDataRepeatableQuest> = [];
const fullProfile = this.profileHelper.getFullProfile(sessionID)!;
const pmcData = fullProfile.characters.pmc;
const currentTime = this.timeUtil.getTimestamp();
// Daily / weekly / Daily_Savage
for (const repeatableConfig of this.questConfig.repeatableQuests)
{
// Get daily/weekly data from profile, add empty object if missing
const generatedRepeatables = this.getRepeatableQuestSubTypeFromProfile(repeatableConfig, pmcData);
const repeatableTypeLower = repeatableConfig.name.toLowerCase();
const canAccessRepeatables = this.canProfileAccessRepeatableQuests(repeatableConfig, pmcData);
if (!canAccessRepeatables)
{
// Dont send any repeatables, even existing ones
continue;
}
// Existing repeatables are still valid, add to return data and move to next sub-type
if (currentTime < generatedRepeatables.endTime - 1)
{
returnData.push(generatedRepeatables);
this.logger.debug(`[Quest Check] ${repeatableTypeLower} quests are still valid.`);
continue;
}
// Current time is past expiry time
// Set endtime to be now + new duration
generatedRepeatables.endTime = currentTime + repeatableConfig.resetTime;
generatedRepeatables.inactiveQuests = [];
this.logger.debug(`Generating new ${repeatableTypeLower}`);
// Put old quests to inactive (this is required since only then the client makes them fail due to non-completion)
// Also need to push them to the "inactiveQuests" list since we need to remove them from offraidData.profile.Quests
// after a raid (the client seems to keep quests internally and we want to get rid of old repeatable quests)
// and remove them from the PMC's Quests and RepeatableQuests[i].activeQuests
this.processExpiredQuests(generatedRepeatables, pmcData);
// Create dynamic quest pool to avoid generating duplicates
const questTypePool = this.generateQuestPool(repeatableConfig, pmcData.Info.Level);
// Add repeatable quests of this loops sub-type (daily/weekly)
for (let i = 0; i < this.getQuestCount(repeatableConfig, pmcData); i++)
{
let quest: IRepeatableQuest | undefined = undefined;
let lifeline = 0;
while (!quest && questTypePool.types.length > 0)
{
quest = this.repeatableQuestGenerator.generateRepeatableQuest(
pmcData.Info.Level,
pmcData.TradersInfo,
questTypePool,
repeatableConfig,
);
lifeline++;
if (lifeline > 10)
{
this.logger.debug(
"We were stuck in repeatable quest generation. This should never happen. Please report",
);
break;
}
}
// check if there are no more quest types available
if (questTypePool.types.length === 0)
{
break;
}
quest.side = repeatableConfig.side;
generatedRepeatables.activeQuests.push(quest);
}
// Nullguard
fullProfile.spt.freeRepeatableRefreshUsedCount ||= {};
// Reset players free quest count for this repeatable sub-type as we're generating new repeatables for this group (daily/weekly)
fullProfile.spt.freeRepeatableRefreshUsedCount[repeatableTypeLower] = 0;
// Create stupid redundant change requirements from quest data
for (const quest of generatedRepeatables.activeQuests)
{
generatedRepeatables.changeRequirement[quest._id] = {
changeCost: quest.changeCost,
changeStandingCost: this.randomUtil.getArrayValue([0, 0.01]), // Randomise standing cost to replace
};
}
// Reset free repeatable values in player profile to defaults
generatedRepeatables.freeChanges = repeatableConfig.freeChanges;
generatedRepeatables.freeChangesAvailable = repeatableConfig.freeChanges;
returnData.push({
id: repeatableConfig.id,
name: generatedRepeatables.name,
endTime: generatedRepeatables.endTime,
activeQuests: generatedRepeatables.activeQuests,
inactiveQuests: generatedRepeatables.inactiveQuests,
changeRequirement: generatedRepeatables.changeRequirement,
freeChanges: generatedRepeatables.freeChanges,
freeChangesAvailable: generatedRepeatables.freeChanges,
});
}
return returnData;
}
/**
* Expire quests and replace expired quests with ready-to-hand-in quests inside generatedRepeatables.activeQuests
* @param generatedRepeatables Repeatables to process (daily/weekly)
* @param pmcData Player profile
*/
protected processExpiredQuests(generatedRepeatables: IPmcDataRepeatableQuest, pmcData: IPmcData): void
{
const questsToKeep = [];
for (const activeQuest of generatedRepeatables.activeQuests)
{
const questStatusInProfile = pmcData.Quests.find((quest) => quest.qid === activeQuest._id);
if (!questStatusInProfile)
{
continue;
}
// Keep finished quests in list so player can hand in
if (questStatusInProfile.status === QuestStatus.AvailableForFinish)
{
questsToKeep.push(activeQuest);
this.logger.debug(
`Keeping repeatable quest: ${activeQuest._id} in activeQuests since it is available to hand in`,
);
continue;
}
// Clean up quest-related counters being left in profile
this.profileFixerService.removeDanglingConditionCounters(pmcData);
// Remove expired quest from pmc.quest array
pmcData.Quests = pmcData.Quests.filter((quest) => quest.qid !== activeQuest._id);
// Store in inactive array
generatedRepeatables.inactiveQuests.push(activeQuest);
}
generatedRepeatables.activeQuests = questsToKeep;
}
/**
* Check if a repeatable quest type (daily/weekly) is active for the given profile
* @param repeatableConfig Repeatable quest config
* @param pmcData Player profile
* @returns True if profile is allowed to access dailies
*/
protected canProfileAccessRepeatableQuests(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): boolean
{
// PMC and daily quests not unlocked yet
if (repeatableConfig.side === "Pmc" && !this.playerHasDailyPmcQuestsUnlocked(pmcData, repeatableConfig))
{
return false;
}
// Scav and daily quests not unlocked yet
if (repeatableConfig.side === "Scav" && !this.playerHasDailyScavQuestsUnlocked(pmcData))
{
this.logger.debug("Daily scav quests still locked, Intel center not built");
return false;
}
return true;
}
/**
* Does player have daily scav quests unlocked
* @param pmcData Player profile to check
* @returns True if unlocked
*/
protected playerHasDailyScavQuestsUnlocked(pmcData: IPmcData): boolean
{
return pmcData?.Hideout?.Areas
?.find((hideoutArea) => hideoutArea.type === HideoutAreas.INTEL_CENTER)
?.level >= 1;
}
/**
* Does player have daily pmc quests unlocked
* @param pmcData Player profile to check
* @param repeatableConfig Config of daily type to check
* @returns True if unlocked
*/
protected playerHasDailyPmcQuestsUnlocked(pmcData: IPmcData, repeatableConfig: IRepeatableQuestConfig): boolean
{
return pmcData.Info.Level >= repeatableConfig.minPlayerLevel;
}
/**
* Get the number of quests to generate - takes into account charisma state of player
* @param repeatableConfig Config
* @param pmcData Player profile
* @returns Quest count
*/
protected getQuestCount(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): number
{
if (
repeatableConfig.name.toLowerCase() === "daily"
&& this.profileHelper.hasEliteSkillLevel(SkillTypes.CHARISMA, pmcData)
)
{
// Elite charisma skill gives extra daily quest(s)
return (
repeatableConfig.numQuests
+ this.databaseService.getGlobals().config.SkillsSettings.Charisma.BonusSettings.EliteBonusSettings
.RepeatableQuestExtraCount
);
}
return repeatableConfig.numQuests;
}
/**
* Get repeatable quest data from profile from name (daily/weekly), creates base repeatable quest object if none exists
* @param repeatableConfig daily/weekly config
* @param pmcData Profile to search
* @returns IPmcDataRepeatableQuest
*/
protected getRepeatableQuestSubTypeFromProfile(
repeatableConfig: IRepeatableQuestConfig,
pmcData: IPmcData,
): IPmcDataRepeatableQuest
{
// Get from profile, add if missing
let repeatableQuestDetails = pmcData.RepeatableQuests
.find((repeatable) => repeatable.name === repeatableConfig.name);
if (!repeatableQuestDetails) // Not in profile, generate
{
const hasAccess = this.profileHelper.hasAccessToRepeatableFreeRefreshSystem(pmcData);
repeatableQuestDetails = {
id: repeatableConfig.id,
name: repeatableConfig.name,
activeQuests: [],
inactiveQuests: [],
endTime: 0,
changeRequirement: {},
freeChanges: hasAccess ? repeatableConfig.freeChanges : 0,
freeChangesAvailable: hasAccess ? repeatableConfig.freeChangesAvailable : 0,
};
// Add base object that holds repeatable data to profile
pmcData.RepeatableQuests.push(repeatableQuestDetails);
}
return repeatableQuestDetails;
}
/**
* Just for debug reasons. Draws dailies a random assort of dailies extracted from dumps
*/
public generateDebugDailies(dailiesPool: any, factory: any, number: number): any
{
let randomQuests = [];
let numberOfQuests = number;
if (factory)
{
// First is factory extract always add for debugging
randomQuests.push(dailiesPool[0]);
numberOfQuests -= 1;
}
randomQuests = randomQuests.concat(this.randomUtil.drawRandomFromList(dailiesPool, numberOfQuests, false));
for (const element of randomQuests)
{
element._id = this.objectId.generate();
const conditions = element.conditions.AvailableForFinish;
for (const condition of conditions)
{
if ("counter" in condition._props)
{
condition._props.counter.id = this.objectId.generate();
}
}
}
return randomQuests;
}
/**
* Used to create a quest pool during each cycle of repeatable quest generation. The pool will be subsequently
* narrowed down during quest generation to avoid duplicate quests. Like duplicate extractions or elimination quests
* where you have to e.g. kill scavs in same locations.
* @param repeatableConfig main repeatable quest config
* @param pmcLevel level of pmc generating quest pool
* @returns IQuestTypePool
*/
protected generateQuestPool(repeatableConfig: IRepeatableQuestConfig, pmcLevel: number): IQuestTypePool
{
const questPool = this.createBaseQuestPool(repeatableConfig);
// Get the allowed locations based on the PMC's level
const locations = this.getAllowedLocationsForPmcLevel(repeatableConfig.locations, pmcLevel);
// Populate Exploration and Pickup quest locations
for (const location in locations)
{
if (location !== ELocationName.ANY)
{
questPool.pool.Exploration.locations[location] = locations[location];
questPool.pool.Pickup.locations[location] = locations[location];
}
}
// Add "any" to pickup quest pool
questPool.pool.Pickup.locations.any = ["any"];
const eliminationConfig = this.repeatableQuestHelper.getEliminationConfigByPmcLevel(pmcLevel, repeatableConfig);
const targetsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.targets);
// Populate Elimination quest targets and their locations
for (const { data: target, key: targetKey } of targetsConfig)
{
// Target is boss
if (target.isBoss)
{
questPool.pool.Elimination.targets[targetKey] = { locations: ["any"] };
}
else
{
// Non-boss targets
const possibleLocations = Object.keys(locations);
const allowedLocations = (targetKey === "Savage")
? possibleLocations.filter((location) => location !== "laboratory") // Exclude labs for Savage targets.
: possibleLocations;
questPool.pool.Elimination.targets[targetKey] = { locations: allowedLocations };
}
}
return questPool;
}
protected createBaseQuestPool(repeatableConfig: IRepeatableQuestConfig): IQuestTypePool
{
return {
types: repeatableConfig.types.slice(),
pool: { Exploration: { locations: {} }, Elimination: { targets: {} }, Pickup: { locations: {} } },
};
}
/**
* Return the locations this PMC is allowed to get daily quests for based on their level
* @param locations The original list of locations
* @param pmcLevel The players level
* @returns A filtered list of locations that allow the player PMC level to access it
*/
protected getAllowedLocationsForPmcLevel(
locations: Record<ELocationName, string[]>,
pmcLevel: number,
): Partial<Record<ELocationName, string[]>>
{
const allowedLocation: Partial<Record<ELocationName, string[]>> = {};
for (const location in locations)
{
const locationNames = [];
for (const locationName of locations[location])
{
if (this.isPmcLevelAllowedOnLocation(locationName, pmcLevel))
{
locationNames.push(locationName);
}
}
if (locationNames.length > 0)
{
allowedLocation[location] = locationNames;
}
}
return allowedLocation;
}
/**
* Return true if the given pmcLevel is allowed on the given location
* @param location The location name to check
* @param pmcLevel The level of the pmc
* @returns True if the given pmc level is allowed to access the given location
*/
protected isPmcLevelAllowedOnLocation(location: string, pmcLevel: number): boolean
{
// All PMC levels are allowed for 'any' location requirement
if (location === ELocationName.ANY)
{
return true;
}
const locationBase = this.databaseService.getLocation(location.toLowerCase())?.base;
if (!locationBase)
{
return true;
}
return pmcLevel <= locationBase.RequiredPlayerLevelMax && pmcLevel >= locationBase.RequiredPlayerLevelMin;
}
public debugLogRepeatableQuestIds(pmcData: IPmcData): void
{
for (const repeatable of pmcData.RepeatableQuests)
{
const activeQuestsIds = [];
const inactiveQuestsIds = [];
for (const active of repeatable.activeQuests)
{
activeQuestsIds.push(active._id);
}
for (const inactive of repeatable.inactiveQuests)
{
inactiveQuestsIds.push(inactive._id);
}
this.logger.debug(`${repeatable.name} activeIds ${activeQuestsIds}`);
this.logger.debug(`${repeatable.name} inactiveIds ${inactiveQuestsIds}`);
}
}
/**
* Handle RepeatableQuestChange event
*
* Replace a players repeatable quest
* @param pmcData Player profile
* @param changeRequest Request object
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public changeRepeatableQuest(
pmcData: IPmcData,
changeRequest: IRepeatableQuestChangeRequest,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
const fullProfile = this.profileHelper.getFullProfile(sessionID);
// Check for existing quest in (daily/weekly/scav arrays)
const { quest: questToReplace, repeatableType: repeatablesInProfile }
= this.getRepeatableById(changeRequest.qid, pmcData);
// Subtype name of quest - daily/weekly/scav
const repeatableTypeLower = repeatablesInProfile.name.toLowerCase();
// Save for later standing loss calculation
const replacedQuestTraderId = questToReplace.traderId;
// Update active quests to exclude the quest we're replacing
repeatablesInProfile.activeQuests = repeatablesInProfile.activeQuests
.filter((quest) => quest._id !== changeRequest.qid);
// Save for later cost calculation
const previousChangeRequirement = this.cloner.clone(repeatablesInProfile.changeRequirement[changeRequest.qid]);
// Delete the replaced quest change requrement as we're going to replace it
delete repeatablesInProfile.changeRequirement[changeRequest.qid];
// Get config for this repeatable sub-type (daily/weekly/scav)
const repeatableConfig = this.questConfig.repeatableQuests
.find((config) => config.name === repeatablesInProfile.name,
);
// Generate meta-data for what type/levelrange of quests can be generated for player
const allowedQuestTypes = this.generateQuestPool(repeatableConfig, pmcData.Info.Level);
const newRepeatableQuest = this.attemptToGenerateRepeatableQuest(pmcData, allowedQuestTypes, repeatableConfig);
if (!newRepeatableQuest)
{
// Unable to find quest being replaced
const message = `Unable to generate repeatable quest of type: ${repeatableTypeLower} to replace trader: ${replacedQuestTraderId} quest ${changeRequest.qid}`;
this.logger.error(message);
return this.httpResponse.appendErrorToOutput(output, message);
}
// Add newly generated quest to daily/weekly/scav type array
newRepeatableQuest.side = repeatableConfig.side;
repeatablesInProfile.activeQuests.push(newRepeatableQuest);
// Find quest we're replacing in pmc profile quests array and remove it
this.questHelper.findAndRemoveQuestFromArrayIfExists(questToReplace._id, pmcData.Quests);
// Find quest we're replacing in scav profile quests array and remove it
this.questHelper.findAndRemoveQuestFromArrayIfExists(
questToReplace._id,
fullProfile.characters.scav?.Quests ?? [],
);
// Add new quests replacement cost to profile
repeatablesInProfile.changeRequirement[newRepeatableQuest._id] = {
changeCost: newRepeatableQuest.changeCost,
changeStandingCost: this.randomUtil.getArrayValue([0, 0.01]),
};
// Check if we should charge player for replacing quest
const isFreeToReplace = this.useFreeRefreshIfAvailable(fullProfile, repeatablesInProfile, repeatableTypeLower);
if (!isFreeToReplace)
{
// Reduce standing with trader for not doing their quest
const traderOfReplacedQuest = pmcData.TradersInfo[replacedQuestTraderId];
traderOfReplacedQuest.standing -= previousChangeRequirement.changeStandingCost;
// not free, Charge player
for (const cost of previousChangeRequirement.changeCost)
{
this.paymentService.addPaymentToOutput(pmcData, cost.templateId, cost.count, sessionID, output);
if (output.warnings.length > 0)
{
return output;
}
}
}
// Clone data before we send it to client
const repeatableToChangeClone = this.cloner.clone(repeatablesInProfile);
// Purge inactive repeatables
repeatableToChangeClone.inactiveQuests = [];
if (!repeatableToChangeClone)
{
// Unable to find quest being replaced
const message = this.localisationService.getText("quest-unable_to_find_repeatable_to_replace");
this.logger.error(message);
return this.httpResponse.appendErrorToOutput(output, message);
}
// Nullguard
output.profileChanges[sessionID].repeatableQuests ||= [];
// Update client output with new repeatable
output.profileChanges[sessionID].repeatableQuests.push(repeatableToChangeClone);
return output;
}
/**
* Find a repeatable (daily/weekly/scav) from a players profile by its id
* @param questId Id of quest to find
* @param pmcData Profile that contains quests to look through
* @returns IGetRepeatableByIdResult
*/
protected getRepeatableById(questId: string, pmcData: IPmcData): IGetRepeatableByIdResult
{
for (const repeatablesInProfile of pmcData.RepeatableQuests)
{
// Check for existing quest in (daily/weekly/scav arrays)
const questToReplace = repeatablesInProfile.activeQuests
.find((repeatable) => repeatable._id === questId);
if (!questToReplace)
{
// Not found, skip to next repeatable sub-type
continue;
}
return { quest: questToReplace, repeatableType: repeatablesInProfile };
}
return undefined;
}
protected attemptToGenerateRepeatableQuest(
pmcData: IPmcData,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig,
): IRepeatableQuest
{
const maxAttempts = 10;
let newRepeatableQuest: IRepeatableQuest = undefined;
let attempts = 0;
while (attempts < maxAttempts
&& questTypePool.types.length > 0)
{
newRepeatableQuest = this.repeatableQuestGenerator.generateRepeatableQuest(
pmcData.Info.Level,
pmcData.TradersInfo,
questTypePool,
repeatableConfig,
);
if (newRepeatableQuest)
{
// Successfully generated a quest, exit loop
break;
}
attempts++;
}
if (attempts > maxAttempts)
{
this.logger.debug(
"We were stuck in repeatable quest generation. This should never happen. Please report",
);
}
return newRepeatableQuest;
}
/**
* Some accounts have access to free repeatable quest refreshes
* Track the usage of them inside players profile
* @param fullProfile Player profile
* @param repeatableSubType Can be daily / weekly / scav repeatable
* @param repeatableTypeName Subtype of repeatable quest: daily / weekly / scav
* @returns Is the repeatable being replaced for free
*/
protected useFreeRefreshIfAvailable(
fullProfile: ISptProfile,
repeatableSubType: IPmcDataRepeatableQuest,
repeatableTypeName: string): boolean
{
// No free refreshes, exit early
if (repeatableSubType.freeChangesAvailable <= 0)
{
// Reset counter to 0
repeatableSubType.freeChangesAvailable = 0;
return false;
}
// Only certain game versions have access to free refreshes
const hasAccessToFreeRefreshSystem
= this.profileHelper.hasAccessToRepeatableFreeRefreshSystem(fullProfile.characters.pmc);
// If the player has access and available refreshes:
if (hasAccessToFreeRefreshSystem)
{
// Initialize/retrieve free refresh count for the desired subtype: daily/weekly
fullProfile.spt.freeRepeatableRefreshUsedCount ||= {};
const repeatableRefreshCounts = fullProfile.spt.freeRepeatableRefreshUsedCount;
repeatableRefreshCounts[repeatableTypeName] ||= 0; // Set to 0 if undefined
// Increment the used count and decrement the available count.
repeatableRefreshCounts[repeatableTypeName]++;
repeatableSubType.freeChangesAvailable--;
return true;
}
return false;
}
}