2024-05-21 17:59:04 +00:00
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" ;
2024-07-23 11:12:53 -04:00
import { IPmcDataRepeatableQuest , IRepeatableQuest } from "@spt/models/eft/common/tables/IRepeatableQuests" ;
2024-05-21 17:59:04 +00:00
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse" ;
2024-06-16 10:58:35 +01:00
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile" ;
2024-05-21 17:59:04 +00:00
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" ;
2024-07-03 21:41:31 +01:00
import { IGetRepeatableByIdResult } from "@spt/models/spt/quests/IGetRepeatableByIdResult" ;
2024-05-21 17:59:04 +00:00
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" ;
2024-05-28 13:59:19 +01:00
import { DatabaseService } from "@spt/services/DatabaseService" ;
2024-05-24 16:42:42 +01:00
import { LocalisationService } from "@spt/services/LocalisationService" ;
2024-05-21 17:59:04 +00:00
import { PaymentService } from "@spt/services/PaymentService" ;
import { ProfileFixerService } from "@spt/services/ProfileFixerService" ;
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil" ;
import { ObjectId } from "@spt/utils/ObjectId" ;
import { RandomUtil } from "@spt/utils/RandomUtil" ;
import { TimeUtil } from "@spt/utils/TimeUtil" ;
2024-07-23 11:12:53 -04:00
import { ICloner } from "@spt/utils/cloners/ICloner" ;
import { inject , injectable } from "tsyringe" ;
2023-03-03 15:23:46 +00:00
@injectable ( )
2024-07-23 11:12:53 -04:00
export class RepeatableQuestController {
2023-03-03 15:23:46 +00:00
protected questConfig : IQuestConfig ;
constructor (
2024-05-28 14:04:20 +00:00
@inject ( "PrimaryLogger" ) protected logger : ILogger ,
2024-05-28 13:59:19 +01:00
@inject ( "DatabaseService" ) protected databaseService : DatabaseService ,
2023-11-07 10:20:59 +00:00
@inject ( "TimeUtil" ) protected timeUtil : TimeUtil ,
2023-03-03 15:23:46 +00:00
@inject ( "RandomUtil" ) protected randomUtil : RandomUtil ,
@inject ( "HttpResponseUtil" ) protected httpResponse : HttpResponseUtil ,
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
@inject ( "ProfileFixerService" ) protected profileFixerService : ProfileFixerService ,
2024-05-24 16:42:42 +01:00
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
2023-03-03 15:23:46 +00:00
@inject ( "EventOutputHolder" ) protected eventOutputHolder : EventOutputHolder ,
@inject ( "PaymentService" ) protected paymentService : PaymentService ,
@inject ( "ObjectId" ) protected objectId : ObjectId ,
2023-10-11 17:43:57 +01:00
@inject ( "RepeatableQuestGenerator" ) protected repeatableQuestGenerator : RepeatableQuestGenerator ,
@inject ( "RepeatableQuestHelper" ) protected repeatableQuestHelper : RepeatableQuestHelper ,
2023-10-21 20:22:11 +01:00
@inject ( "QuestHelper" ) protected questHelper : QuestHelper ,
2023-11-16 21:42:06 +00:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
2024-05-28 14:04:20 +00:00
@inject ( "PrimaryCloner" ) protected cloner : ICloner ,
2024-07-23 11:12:53 -04:00
) {
2023-03-03 15:23:46 +00:00
this . questConfig = this . configServer . getConfig ( ConfigTypes . QUEST ) ;
}
/ * *
2023-07-15 10:45:33 +01:00
* Handle client / repeatalbeQuests / activityPeriods
2023-03-03 15:23:46 +00:00
* 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
*
2024-01-31 14:38:18 +00:00
* @param { string } sessionID Player ' s session id
2024-02-02 13:54:07 -05:00
*
2024-05-07 23:57:08 -04:00
* @returns { array } Array of "repeatableQuestObjects" as described above
2023-03-03 15:23:46 +00:00
* /
2024-07-23 11:12:53 -04:00
public getClientRepeatableQuests ( sessionID : string ) : IPmcDataRepeatableQuest [ ] {
2023-03-03 15:23:46 +00:00
const returnData : Array < IPmcDataRepeatableQuest > = [ ] ;
2024-06-03 17:33:46 +01:00
const fullProfile = this . profileHelper . getFullProfile ( sessionID ) ! ;
const pmcData = fullProfile . characters . pmc ;
2024-06-03 16:51:26 +01:00
const currentTime = this . timeUtil . getTimestamp ( ) ;
2023-11-16 21:42:06 +00:00
2023-03-03 15:23:46 +00:00
// Daily / weekly / Daily_Savage
2024-07-23 11:12:53 -04:00
for ( const repeatableConfig of this . questConfig . repeatableQuests ) {
2024-06-03 17:33:46 +01:00
// Get daily/weekly data from profile, add empty object if missing
2024-06-15 13:28:25 +01:00
const generatedRepeatables = this . getRepeatableQuestSubTypeFromProfile ( repeatableConfig , pmcData ) ;
const repeatableTypeLower = repeatableConfig . name . toLowerCase ( ) ;
2023-11-16 21:42:06 +00:00
2024-06-07 21:33:09 +01:00
const canAccessRepeatables = this . canProfileAccessRepeatableQuests ( repeatableConfig , pmcData ) ;
2024-07-23 11:12:53 -04:00
if ( ! canAccessRepeatables ) {
2024-06-15 13:29:22 +01:00
// Dont send any repeatables, even existing ones
2024-06-03 17:33:46 +01:00
continue ;
}
2024-06-15 13:28:25 +01:00
// Existing repeatables are still valid, add to return data and move to next sub-type
2024-07-23 11:12:53 -04:00
if ( currentTime < generatedRepeatables . endTime - 1 ) {
2024-06-15 13:28:25 +01:00
returnData . push ( generatedRepeatables ) ;
this . logger . debug ( ` [Quest Check] ${ repeatableTypeLower } quests are still valid. ` ) ;
2024-06-03 17:33:46 +01:00
continue ;
}
// Current time is past expiry time
2024-06-15 13:29:22 +01:00
// Set endtime to be now + new duration
2024-06-15 13:28:25 +01:00
generatedRepeatables . endTime = currentTime + repeatableConfig . resetTime ;
generatedRepeatables . inactiveQuests = [ ] ;
this . logger . debug ( ` Generating new ${ repeatableTypeLower } ` ) ;
2024-06-03 17:33:46 +01:00
// Put old quests to inactive (this is required since only then the client makes them fail due to non-completion)
2024-06-15 13:29:22 +01:00
// Also need to push them to the "inactiveQuests" list since we need to remove them from offraidData.profile.Quests
2024-06-03 17:33:46 +01:00
// 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
2024-06-15 13:29:22 +01:00
this . processExpiredQuests ( generatedRepeatables , pmcData ) ;
2023-03-03 15:23:46 +00:00
2024-06-15 13:29:22 +01:00
// Create dynamic quest pool to avoid generating duplicates
2024-06-03 17:33:46 +01:00
const questTypePool = this . generateQuestPool ( repeatableConfig , pmcData . Info . Level ) ;
2023-03-03 15:23:46 +00:00
2024-06-15 13:29:22 +01:00
// Add repeatable quests of this loops sub-type (daily/weekly)
2024-07-23 11:12:53 -04:00
for ( let i = 0 ; i < this . getQuestCount ( repeatableConfig , pmcData ) ; i ++ ) {
2024-06-03 17:33:46 +01:00
let quest : IRepeatableQuest | undefined = undefined ;
let lifeline = 0 ;
2024-07-23 11:12:53 -04:00
while ( ! quest && questTypePool . types . length > 0 ) {
2024-06-03 17:33:46 +01:00
quest = this . repeatableQuestGenerator . generateRepeatableQuest (
pmcData . Info . Level ,
pmcData . TradersInfo ,
questTypePool ,
repeatableConfig ,
) ;
lifeline ++ ;
2024-07-23 11:12:53 -04:00
if ( lifeline > 10 ) {
2024-06-03 17:33:46 +01:00
this . logger . debug (
"We were stuck in repeatable quest generation. This should never happen. Please report" ,
) ;
break ;
2023-03-03 15:23:46 +00:00
}
}
2024-06-03 17:33:46 +01:00
// check if there are no more quest types available
2024-07-23 11:12:53 -04:00
if ( questTypePool . types . length === 0 ) {
2024-06-03 17:33:46 +01:00
break ;
2023-03-03 15:23:46 +00:00
}
2024-06-03 17:33:46 +01:00
quest . side = repeatableConfig . side ;
2024-06-15 13:28:25 +01:00
generatedRepeatables . activeQuests . push ( quest ) ;
2024-06-03 17:33:46 +01:00
}
2024-06-15 13:28:25 +01:00
// 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 ;
2023-03-03 15:23:46 +00:00
2024-01-05 19:52:21 +00:00
// Create stupid redundant change requirements from quest data
2024-07-23 11:12:53 -04:00
for ( const quest of generatedRepeatables . activeQuests ) {
2024-06-15 13:28:25 +01:00
generatedRepeatables . changeRequirement [ quest . _id ] = {
2023-03-03 15:23:46 +00:00
changeCost : quest.changeCost ,
2024-06-03 16:51:26 +01:00
changeStandingCost : this.randomUtil.getArrayValue ( [ 0 , 0.01 ] ) , // Randomise standing cost to replace
2023-03-03 15:23:46 +00:00
} ;
}
2024-06-20 16:12:34 +01:00
// Reset free repeatable values in player profile to defaults
generatedRepeatables . freeChanges = repeatableConfig . freeChanges ;
generatedRepeatables . freeChangesAvailable = repeatableConfig . freeChanges ;
2023-03-03 15:23:46 +00:00
returnData . push ( {
2023-10-19 21:36:17 +01:00
id : repeatableConfig.id ,
2024-06-15 13:28:25 +01:00
name : generatedRepeatables.name ,
endTime : generatedRepeatables.endTime ,
activeQuests : generatedRepeatables.activeQuests ,
inactiveQuests : generatedRepeatables.inactiveQuests ,
changeRequirement : generatedRepeatables.changeRequirement ,
freeChanges : generatedRepeatables.freeChanges ,
2024-06-20 16:17:07 +01:00
freeChangesAvailable : generatedRepeatables.freeChanges ,
2023-03-03 15:23:46 +00:00
} ) ;
}
return returnData ;
}
2024-06-15 13:29:22 +01:00
/ * *
* 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
* /
2024-07-23 11:12:53 -04:00
protected processExpiredQuests ( generatedRepeatables : IPmcDataRepeatableQuest , pmcData : IPmcData ) : void {
2024-06-15 13:29:22 +01:00
const questsToKeep = [ ] ;
2024-07-23 11:12:53 -04:00
for ( const activeQuest of generatedRepeatables . activeQuests ) {
2024-06-15 13:29:22 +01:00
const questStatusInProfile = pmcData . Quests . find ( ( quest ) = > quest . qid === activeQuest . _id ) ;
2024-07-23 11:12:53 -04:00
if ( ! questStatusInProfile ) {
2024-06-15 13:29:22 +01:00
continue ;
}
// Keep finished quests in list so player can hand in
2024-07-23 11:12:53 -04:00
if ( questStatusInProfile . status === QuestStatus . AvailableForFinish ) {
2024-06-15 13:29:22 +01:00
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 ;
}
2024-06-07 21:33:09 +01:00
/ * *
* 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
* /
2024-07-23 11:12:53 -04:00
protected canProfileAccessRepeatableQuests ( repeatableConfig : IRepeatableQuestConfig , pmcData : IPmcData ) : boolean {
2024-06-07 21:33:09 +01:00
// PMC and daily quests not unlocked yet
2024-07-23 11:12:53 -04:00
if ( repeatableConfig . side === "Pmc" && ! this . playerHasDailyPmcQuestsUnlocked ( pmcData , repeatableConfig ) ) {
2024-06-07 21:33:09 +01:00
return false ;
}
// Scav and daily quests not unlocked yet
2024-07-23 11:12:53 -04:00
if ( repeatableConfig . side === "Scav" && ! this . playerHasDailyScavQuestsUnlocked ( pmcData ) ) {
2024-06-15 13:28:25 +01:00
this . logger . debug ( "Daily scav quests still locked, Intel center not built" ) ;
2024-06-07 21:33:09 +01:00
return false ;
}
return true ;
}
2024-06-03 17:33:46 +01:00
/ * *
* Does player have daily scav quests unlocked
* @param pmcData Player profile to check
* @returns True if unlocked
* /
2024-07-23 11:12:53 -04:00
protected playerHasDailyScavQuestsUnlocked ( pmcData : IPmcData ) : boolean {
return (
pmcData ? . Hideout ? . Areas ? . find ( ( hideoutArea ) = > hideoutArea . type === HideoutAreas . INTEL_CENTER ) ? . level >= 1
) ;
2024-06-03 16:51:26 +01:00
}
2024-06-03 17:33:46 +01:00
/ * *
* 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
* /
2024-07-23 11:12:53 -04:00
protected playerHasDailyPmcQuestsUnlocked ( pmcData : IPmcData , repeatableConfig : IRepeatableQuestConfig ) : boolean {
2024-06-03 16:51:26 +01:00
return pmcData . Info . Level >= repeatableConfig . minPlayerLevel ;
}
2023-11-07 09:58:58 +00:00
/ * *
* Get the number of quests to generate - takes into account charisma state of player
* @param repeatableConfig Config
* @param pmcData Player profile
* @returns Quest count
* /
2024-07-23 11:12:53 -04:00
protected getQuestCount ( repeatableConfig : IRepeatableQuestConfig , pmcData : IPmcData ) : number {
2023-11-16 21:42:06 +00:00
if (
2024-07-23 11:12:53 -04:00
repeatableConfig . name . toLowerCase ( ) === "daily" &&
this . profileHelper . hasEliteSkillLevel ( SkillTypes . CHARISMA , pmcData )
) {
2023-11-07 10:20:59 +00:00
// Elite charisma skill gives extra daily quest(s)
2024-05-17 15:32:41 -04:00
return (
2024-07-23 11:12:53 -04:00
repeatableConfig . numQuests +
this . databaseService . getGlobals ( ) . config . SkillsSettings . Charisma . BonusSettings . EliteBonusSettings
2024-05-17 15:32:41 -04:00
. RepeatableQuestExtraCount
) ;
2023-11-07 09:58:58 +00:00
}
return repeatableConfig . numQuests ;
}
2023-03-03 15:23:46 +00:00
/ * *
* 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
* /
2023-11-16 21:42:06 +00:00
protected getRepeatableQuestSubTypeFromProfile (
repeatableConfig : IRepeatableQuestConfig ,
pmcData : IPmcData ,
2024-07-23 11:12:53 -04:00
) : IPmcDataRepeatableQuest {
2023-03-03 15:23:46 +00:00
// Get from profile, add if missing
2024-07-23 11:12:53 -04:00
let repeatableQuestDetails = pmcData . RepeatableQuests . find (
( repeatable ) = > repeatable . name === repeatableConfig . name ,
) ;
if ( ! repeatableQuestDetails ) {
// Not in profile, generate
2024-06-16 10:58:35 +01:00
const hasAccess = this . profileHelper . hasAccessToRepeatableFreeRefreshSystem ( pmcData ) ;
2023-03-03 15:23:46 +00:00
repeatableQuestDetails = {
2023-10-19 21:36:17 +01:00
id : repeatableConfig.id ,
2023-03-03 15:23:46 +00:00
name : repeatableConfig.name ,
activeQuests : [ ] ,
inactiveQuests : [ ] ,
endTime : 0 ,
2023-11-16 21:42:06 +00:00
changeRequirement : { } ,
2024-06-16 10:58:35 +01:00
freeChanges : hasAccess ? repeatableConfig.freeChanges : 0 ,
freeChangesAvailable : hasAccess ? repeatableConfig.freeChangesAvailable : 0 ,
2023-03-03 15:23:46 +00:00
} ;
// 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
* /
2024-07-23 11:12:53 -04:00
public generateDebugDailies ( dailiesPool : any , factory : any , number : number ) : any {
2023-03-03 15:23:46 +00:00
let randomQuests = [ ] ;
2024-02-05 18:51:32 -05:00
let numberOfQuests = number ;
2024-07-23 11:12:53 -04:00
if ( factory ) {
2023-03-03 15:23:46 +00:00
// First is factory extract always add for debugging
randomQuests . push ( dailiesPool [ 0 ] ) ;
2024-02-05 18:51:32 -05:00
numberOfQuests -= 1 ;
2023-03-03 15:23:46 +00:00
}
2024-02-05 18:51:32 -05:00
randomQuests = randomQuests . concat ( this . randomUtil . drawRandomFromList ( dailiesPool , numberOfQuests , false ) ) ;
2023-03-03 15:23:46 +00:00
2024-07-23 11:12:53 -04:00
for ( const element of randomQuests ) {
2023-10-11 17:43:57 +01:00
element . _id = this . objectId . generate ( ) ;
const conditions = element . conditions . AvailableForFinish ;
2024-07-23 11:12:53 -04:00
for ( const condition of conditions ) {
if ( "counter" in condition . _props ) {
2024-02-03 01:21:03 -05:00
condition . _props . counter . id = this . objectId . generate ( ) ;
2023-03-03 15:23:46 +00:00
}
}
}
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
* /
2024-07-23 11:12:53 -04:00
protected generateQuestPool ( repeatableConfig : IRepeatableQuestConfig , pmcLevel : number ) : IQuestTypePool {
2023-10-11 17:43:57 +01:00
const questPool = this . createBaseQuestPool ( repeatableConfig ) ;
2024-06-07 21:33:09 +01:00
// Get the allowed locations based on the PMC's level
const locations = this . getAllowedLocationsForPmcLevel ( repeatableConfig . locations , pmcLevel ) ;
// Populate Exploration and Pickup quest locations
2024-07-23 11:12:53 -04:00
for ( const location in locations ) {
if ( location !== ELocationName . ANY ) {
2024-02-17 10:48:52 +00:00
questPool . pool . Exploration . locations [ location ] = locations [ location ] ;
questPool . pool . Pickup . locations [ location ] = locations [ location ] ;
2023-03-03 15:23:46 +00:00
}
}
2023-10-17 16:28:48 +01:00
// Add "any" to pickup quest pool
2023-11-16 21:42:06 +00:00
questPool . pool . Pickup . locations . any = [ "any" ] ;
2023-10-17 16:28:48 +01:00
2023-10-11 17:43:57 +01:00
const eliminationConfig = this . repeatableQuestHelper . getEliminationConfigByPmcLevel ( pmcLevel , repeatableConfig ) ;
const targetsConfig = this . repeatableQuestHelper . probabilityObjectArray ( eliminationConfig . targets ) ;
2024-06-07 21:33:09 +01:00
// Populate Elimination quest targets and their locations
2024-07-23 11:12:53 -04:00
for ( const { data : target , key : targetKey } of targetsConfig ) {
2023-03-03 15:23:46 +00:00
// Target is boss
2024-07-23 11:12:53 -04:00
if ( target . isBoss ) {
2024-06-07 21:33:09 +01:00
questPool . pool . Elimination . targets [ targetKey ] = { locations : [ "any" ] } ;
2024-07-23 11:12:53 -04:00
} else {
2024-06-07 21:33:09 +01:00
// Non-boss targets
2024-02-17 10:48:52 +00:00
const possibleLocations = Object . keys ( locations ) ;
2023-03-03 15:23:46 +00:00
2024-07-23 11:12:53 -04:00
const allowedLocations =
targetKey === "Savage"
? possibleLocations . filter ( ( location ) = > location !== "laboratory" ) // Exclude labs for Savage targets.
: possibleLocations ;
2024-06-07 21:33:09 +01:00
questPool . pool . Elimination . targets [ targetKey ] = { locations : allowedLocations } ;
2023-03-03 15:23:46 +00:00
}
}
return questPool ;
}
2024-07-23 11:12:53 -04:00
protected createBaseQuestPool ( repeatableConfig : IRepeatableQuestConfig ) : IQuestTypePool {
2023-10-11 17:43:57 +01:00
return {
types : repeatableConfig.types.slice ( ) ,
2023-11-16 21:42:06 +00:00
pool : { Exploration : { locations : { } } , Elimination : { targets : { } } , Pickup : { locations : { } } } ,
2023-03-03 15:23:46 +00:00
} ;
}
2024-02-17 10:48:52 +00:00
/ * *
* Return the locations this PMC is allowed to get daily quests for based on their level
* @param locations The original list of locations
2024-06-07 21:33:09 +01:00
* @param pmcLevel The players level
2024-02-17 10:48:52 +00:00
* @returns A filtered list of locations that allow the player PMC level to access it
* /
2024-06-07 21:33:09 +01:00
protected getAllowedLocationsForPmcLevel (
2024-03-10 23:18:42 +00:00
locations : Record < ELocationName , string [ ] > ,
pmcLevel : number ,
2024-07-23 11:12:53 -04:00
) : Partial < Record < ELocationName , string [ ] > > {
2024-02-17 10:48:52 +00:00
const allowedLocation : Partial < Record < ELocationName , string [ ] > > = { } ;
2024-07-23 11:12:53 -04:00
for ( const location in locations ) {
2024-02-17 10:48:52 +00:00
const locationNames = [ ] ;
2024-07-23 11:12:53 -04:00
for ( const locationName of locations [ location ] ) {
if ( this . isPmcLevelAllowedOnLocation ( locationName , pmcLevel ) ) {
2024-02-17 10:48:52 +00:00
locationNames . push ( locationName ) ;
}
}
2024-07-23 11:12:53 -04:00
if ( locationNames . length > 0 ) {
2024-02-17 10:48:52 +00:00
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
* /
2024-07-23 11:12:53 -04:00
protected isPmcLevelAllowedOnLocation ( location : string , pmcLevel : number ) : boolean {
2024-06-07 21:33:09 +01:00
// All PMC levels are allowed for 'any' location requirement
2024-07-23 11:12:53 -04:00
if ( location === ELocationName . ANY ) {
2024-02-17 10:48:52 +00:00
return true ;
}
2024-06-07 21:33:09 +01:00
const locationBase = this . databaseService . getLocation ( location . toLowerCase ( ) ) ? . base ;
2024-07-23 11:12:53 -04:00
if ( ! locationBase ) {
2024-02-17 10:48:52 +00:00
return true ;
}
2024-05-07 23:57:08 -04:00
return pmcLevel <= locationBase . RequiredPlayerLevelMax && pmcLevel >= locationBase . RequiredPlayerLevelMin ;
2024-02-17 10:48:52 +00:00
}
2024-07-23 11:12:53 -04:00
public debugLogRepeatableQuestIds ( pmcData : IPmcData ) : void {
for ( const repeatable of pmcData . RepeatableQuests ) {
2023-03-03 15:23:46 +00:00
const activeQuestsIds = [ ] ;
const inactiveQuestsIds = [ ] ;
2024-07-23 11:12:53 -04:00
for ( const active of repeatable . activeQuests ) {
2023-03-03 15:23:46 +00:00
activeQuestsIds . push ( active . _id ) ;
}
2024-07-23 11:12:53 -04:00
for ( const inactive of repeatable . inactiveQuests ) {
2023-03-03 15:23:46 +00:00
inactiveQuestsIds . push ( inactive . _id ) ;
}
this . logger . debug ( ` ${ repeatable . name } activeIds ${ activeQuestsIds } ` ) ;
this . logger . debug ( ` ${ repeatable . name } inactiveIds ${ inactiveQuestsIds } ` ) ;
}
}
2023-07-15 10:45:33 +01:00
/ * *
* Handle RepeatableQuestChange event
2024-06-07 21:33:09 +01:00
*
* Replace a players repeatable quest
* @param pmcData Player profile
* @param changeRequest Request object
* @param sessionID Session id
* @returns IItemEventRouterResponse
2023-07-15 10:45:33 +01:00
* /
2023-11-16 21:42:06 +00:00
public changeRepeatableQuest (
pmcData : IPmcData ,
changeRequest : IRepeatableQuestChangeRequest ,
sessionID : string ,
2024-07-23 11:12:53 -04:00
) : IItemEventRouterResponse {
2024-06-07 21:33:09 +01:00
const output = this . eventOutputHolder . getOutput ( sessionID ) ;
const fullProfile = this . profileHelper . getFullProfile ( sessionID ) ;
2024-07-03 21:41:31 +01:00
// Check for existing quest in (daily/weekly/scav arrays)
2024-07-23 11:12:53 -04:00
const { quest : questToReplace , repeatableType : repeatablesInProfile } = this . getRepeatableById (
changeRequest . qid ,
pmcData ,
) ;
2023-03-03 15:23:46 +00:00
2024-07-03 21:41:31 +01:00
// Subtype name of quest - daily/weekly/scav
const repeatableTypeLower = repeatablesInProfile . name . toLowerCase ( ) ;
2023-10-19 20:04:47 +01:00
2024-07-03 21:41:31 +01:00
// Save for later standing loss calculation
const replacedQuestTraderId = questToReplace . traderId ;
2023-11-16 21:42:06 +00:00
2024-07-03 21:41:31 +01:00
// Update active quests to exclude the quest we're replacing
2024-07-23 11:12:53 -04:00
repeatablesInProfile . activeQuests = repeatablesInProfile . activeQuests . filter (
( quest ) = > quest . _id !== changeRequest . qid ,
) ;
2023-10-10 11:03:20 +00:00
2024-07-03 21:41:31 +01:00
// Save for later cost calculation
const previousChangeRequirement = this . cloner . clone ( repeatablesInProfile . changeRequirement [ changeRequest . qid ] ) ;
2023-10-10 11:03:20 +00:00
2024-07-03 21:41:31 +01:00
// Delete the replaced quest change requrement as we're going to replace it
delete repeatablesInProfile . changeRequirement [ changeRequest . qid ] ;
2024-06-15 13:28:25 +01:00
2024-07-03 21:41:31 +01:00
// Get config for this repeatable sub-type (daily/weekly/scav)
2024-07-23 11:12:53 -04:00
const repeatableConfig = this . questConfig . repeatableQuests . find (
( config ) = > config . name === repeatablesInProfile . name ,
) ;
2023-11-16 21:42:06 +00:00
2024-07-03 21:41:31 +01:00
// 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 ) ;
2024-07-23 11:12:53 -04:00
if ( ! newRepeatableQuest ) {
2024-07-03 21:41:31 +01:00
// 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 ) ;
2024-06-15 13:28:25 +01:00
2024-07-03 21:41:31 +01:00
return this . httpResponse . appendErrorToOutput ( output , message ) ;
}
2023-10-21 17:39:44 +01:00
2024-07-03 21:41:31 +01:00
// Add newly generated quest to daily/weekly/scav type array
newRepeatableQuest . side = repeatableConfig . side ;
repeatablesInProfile . activeQuests . push ( newRepeatableQuest ) ;
2023-11-16 21:42:06 +00:00
2024-07-03 21:41:31 +01:00
// Find quest we're replacing in pmc profile quests array and remove it
this . questHelper . findAndRemoveQuestFromArrayIfExists ( questToReplace . _id , pmcData . Quests ) ;
2024-06-15 13:28:25 +01:00
2024-07-03 21:41:31 +01:00
// 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 ) ;
2024-07-23 11:12:53 -04:00
if ( ! isFreeToReplace ) {
2024-07-21 08:02:44 +00:00
// Reduce standing with trader for not doing their quest
const traderOfReplacedQuest = pmcData . TradersInfo [ replacedQuestTraderId ] ;
traderOfReplacedQuest . standing -= previousChangeRequirement . changeStandingCost ;
2024-07-21 16:24:05 +01:00
const charismaBonus = this . profileHelper . getSkillFromProfile ( pmcData , SkillTypes . CHARISMA ) ? . Progress ? ? 0 ;
2024-07-23 11:12:53 -04:00
for ( const cost of previousChangeRequirement . changeCost ) {
2024-07-21 16:24:05 +01:00
// Not free, Charge player + appy charisma bonus to cost of replacement
2024-07-23 11:12:53 -04:00
cost . count = Math . trunc ( cost . count * ( 1 - Math . trunc ( charismaBonus / 100 ) * 0.001 ) ? ? 1 ) ;
2024-07-03 21:41:31 +01:00
this . paymentService . addPaymentToOutput ( pmcData , cost . templateId , cost . count , sessionID , output ) ;
2024-07-23 11:12:53 -04:00
if ( output . warnings . length > 0 ) {
2024-07-03 21:41:31 +01:00
return output ;
2024-07-01 10:42:16 +01:00
}
2023-03-03 15:23:46 +00:00
}
2024-07-03 21:41:31 +01:00
}
2023-10-19 20:04:47 +01:00
2024-07-03 21:41:31 +01:00
// Clone data before we send it to client
const repeatableToChangeClone = this . cloner . clone ( repeatablesInProfile ) ;
2024-06-03 17:33:46 +01:00
2024-07-03 21:41:31 +01:00
// Purge inactive repeatables
repeatableToChangeClone . inactiveQuests = [ ] ;
2023-03-03 15:23:46 +00:00
2024-07-23 11:12:53 -04:00
if ( ! repeatableToChangeClone ) {
2024-06-03 17:33:46 +01:00
// Unable to find quest being replaced
2024-05-24 16:42:42 +01:00
const message = this . localisationService . getText ( "quest-unable_to_find_repeatable_to_replace" ) ;
2023-10-19 19:30:23 +01:00
this . logger . error ( message ) ;
2023-10-21 17:39:44 +01:00
2023-10-19 19:30:23 +01:00
return this . httpResponse . appendErrorToOutput ( output , message ) ;
2023-03-03 15:23:46 +00:00
}
2024-06-15 13:28:25 +01:00
// Nullguard
output . profileChanges [ sessionID ] . repeatableQuests || = [ ] ;
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
// Update client output with new repeatable
2024-07-03 21:41:31 +01:00
output . profileChanges [ sessionID ] . repeatableQuests . push ( repeatableToChangeClone ) ;
2023-03-03 15:23:46 +00:00
return output ;
}
2023-10-19 20:04:47 +01:00
2024-07-03 21:41:31 +01:00
/ * *
* 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
* /
2024-07-23 11:12:53 -04:00
protected getRepeatableById ( questId : string , pmcData : IPmcData ) : IGetRepeatableByIdResult {
for ( const repeatablesInProfile of pmcData . RepeatableQuests ) {
2024-07-03 21:41:31 +01:00
// Check for existing quest in (daily/weekly/scav arrays)
2024-07-23 11:12:53 -04:00
const questToReplace = repeatablesInProfile . activeQuests . find ( ( repeatable ) = > repeatable . _id === questId ) ;
if ( ! questToReplace ) {
2024-07-03 21:41:31 +01:00
// Not found, skip to next repeatable sub-type
continue ;
}
return { quest : questToReplace , repeatableType : repeatablesInProfile } ;
}
return undefined ;
}
2023-11-16 21:42:06 +00:00
protected attemptToGenerateRepeatableQuest (
pmcData : IPmcData ,
questTypePool : IQuestTypePool ,
repeatableConfig : IRepeatableQuestConfig ,
2024-07-23 11:12:53 -04:00
) : IRepeatableQuest {
2024-06-07 21:33:09 +01:00
const maxAttempts = 10 ;
2024-05-27 20:06:07 +00:00
let newRepeatableQuest : IRepeatableQuest = undefined ;
2024-06-07 21:33:09 +01:00
let attempts = 0 ;
2024-07-23 11:12:53 -04:00
while ( attempts < maxAttempts && questTypePool . types . length > 0 ) {
2023-10-19 20:04:47 +01:00
newRepeatableQuest = this . repeatableQuestGenerator . generateRepeatableQuest (
pmcData . Info . Level ,
pmcData . TradersInfo ,
questTypePool ,
2023-11-16 21:42:06 +00:00
repeatableConfig ,
2023-10-19 20:04:47 +01:00
) ;
2024-06-07 21:33:09 +01:00
2024-07-23 11:12:53 -04:00
if ( newRepeatableQuest ) {
2024-06-07 21:33:09 +01:00
// Successfully generated a quest, exit loop
2023-10-19 20:04:47 +01:00
break ;
}
2024-06-07 21:33:09 +01:00
attempts ++ ;
}
2024-07-23 11:12:53 -04:00
if ( attempts > maxAttempts ) {
this . logger . debug ( "We were stuck in repeatable quest generation. This should never happen. Please report" ) ;
2023-10-19 20:04:47 +01:00
}
return newRepeatableQuest ;
}
2024-06-16 10:58:35 +01:00
/ * *
2024-07-01 10:42:16 +01:00
* Some accounts have access to free repeatable quest refreshes
2024-06-16 10:58:35 +01:00
* Track the usage of them inside players profile
2024-07-01 10:42:16 +01:00
* @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
2024-06-16 10:58:35 +01:00
* /
2024-07-01 10:42:16 +01:00
protected useFreeRefreshIfAvailable (
2024-06-16 10:58:35 +01:00
fullProfile : ISptProfile ,
repeatableSubType : IPmcDataRepeatableQuest ,
2024-07-23 11:12:53 -04:00
repeatableTypeName : string ,
) : boolean {
2024-07-01 10:42:16 +01:00
// No free refreshes, exit early
2024-07-23 11:12:53 -04:00
if ( repeatableSubType . freeChangesAvailable <= 0 ) {
2024-07-01 10:42:16 +01:00
// Reset counter to 0
2024-06-16 10:58:35 +01:00
repeatableSubType . freeChangesAvailable = 0 ;
2024-07-01 10:42:16 +01:00
return false ;
2024-06-16 10:58:35 +01:00
}
2024-07-01 10:42:16 +01:00
// Only certain game versions have access to free refreshes
2024-07-23 11:12:53 -04:00
const hasAccessToFreeRefreshSystem = this . profileHelper . hasAccessToRepeatableFreeRefreshSystem (
fullProfile . characters . pmc ,
) ;
2024-07-01 10:42:16 +01:00
// If the player has access and available refreshes:
2024-07-23 11:12:53 -04:00
if ( hasAccessToFreeRefreshSystem ) {
2024-07-01 10:42:16 +01:00
// 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
2024-06-16 10:58:35 +01:00
2024-07-01 10:42:16 +01:00
// Increment the used count and decrement the available count.
repeatableRefreshCounts [ repeatableTypeName ] ++ ;
2024-06-16 10:58:35 +01:00
repeatableSubType . freeChangesAvailable -- ;
2024-07-01 10:42:16 +01:00
return true ;
2024-06-16 10:58:35 +01:00
}
2024-07-01 10:42:16 +01:00
return false ;
2024-06-16 10:58:35 +01:00
}
2023-03-03 15:23:46 +00:00
}