2023-10-11 18:43:57 +02:00
import { inject , injectable } from "tsyringe" ;
2023-10-19 19:21:17 +02:00
import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper" ;
import { ItemHelper } from "@spt-aki/helpers/ItemHelper" ;
import { PresetHelper } from "@spt-aki/helpers/PresetHelper" ;
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper" ;
import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper" ;
import { RepeatableQuestHelper } from "@spt-aki/helpers/RepeatableQuestHelper" ;
import { Exit , ILocationBase } from "@spt-aki/models/eft/common/ILocationBase" ;
import { TraderInfo } from "@spt-aki/models/eft/common/tables/IBotBase" ;
2023-11-15 00:05:34 +01:00
import { Item } from "@spt-aki/models/eft/common/tables/IItem" ;
2023-10-11 18:43:57 +02:00
import {
ICompletion ,
ICompletionAvailableFor ,
IElimination ,
IEliminationCondition ,
2023-10-17 17:28:48 +02:00
IEquipmentConditionProps ,
2023-10-11 18:43:57 +02:00
IExploration ,
2023-11-16 22:42:06 +01:00
IExplorationCondition ,
IKillConditionProps ,
2023-10-17 17:28:48 +02:00
IPickup ,
2023-11-16 22:42:06 +01:00
IRepeatableQuest ,
IReward ,
IRewards ,
2023-10-19 19:21:17 +02:00
} from "@spt-aki/models/eft/common/tables/IRepeatableQuests" ;
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem" ;
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses" ;
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes" ;
import { Money } from "@spt-aki/models/enums/Money" ;
import { Traders } from "@spt-aki/models/enums/Traders" ;
2023-11-16 22:42:06 +01:00
import {
IBaseQuestConfig ,
IBossInfo ,
IEliminationConfig ,
IQuestConfig ,
IRepeatableQuestConfig ,
} from "@spt-aki/models/spt/config/IQuestConfig" ;
2023-10-19 19:21:17 +02:00
import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool" ;
import { ILogger } from "@spt-aki/models/spt/utils/ILogger" ;
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder" ;
import { ConfigServer } from "@spt-aki/servers/ConfigServer" ;
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer" ;
import { ItemFilterService } from "@spt-aki/services/ItemFilterService" ;
import { LocalisationService } from "@spt-aki/services/LocalisationService" ;
import { PaymentService } from "@spt-aki/services/PaymentService" ;
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService" ;
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil" ;
import { JsonUtil } from "@spt-aki/utils/JsonUtil" ;
import { MathUtil } from "@spt-aki/utils/MathUtil" ;
import { ObjectId } from "@spt-aki/utils/ObjectId" ;
2023-10-24 16:01:31 +02:00
import { ProbabilityObjectArray , RandomUtil } from "@spt-aki/utils/RandomUtil" ;
2023-10-19 19:21:17 +02:00
import { TimeUtil } from "@spt-aki/utils/TimeUtil" ;
2023-10-11 18:43:57 +02:00
@injectable ( )
export class RepeatableQuestGenerator
{
protected questConfig : IQuestConfig ;
constructor (
@inject ( "TimeUtil" ) protected timeUtil : TimeUtil ,
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "RandomUtil" ) protected randomUtil : RandomUtil ,
@inject ( "HttpResponseUtil" ) protected httpResponse : HttpResponseUtil ,
@inject ( "MathUtil" ) protected mathUtil : MathUtil ,
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
@inject ( "DatabaseServer" ) protected databaseServer : DatabaseServer ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
@inject ( "PresetHelper" ) protected presetHelper : PresetHelper ,
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
@inject ( "ProfileFixerService" ) protected profileFixerService : ProfileFixerService ,
@inject ( "HandbookHelper" ) protected handbookHelper : HandbookHelper ,
@inject ( "RagfairServerHelper" ) protected ragfairServerHelper : RagfairServerHelper ,
@inject ( "EventOutputHolder" ) protected eventOutputHolder : EventOutputHolder ,
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
@inject ( "PaymentService" ) protected paymentService : PaymentService ,
@inject ( "ObjectId" ) protected objectId : ObjectId ,
@inject ( "ItemFilterService" ) protected itemFilterService : ItemFilterService ,
@inject ( "RepeatableQuestHelper" ) protected repeatableQuestHelper : RepeatableQuestHelper ,
2023-11-16 22:42:06 +01:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
2023-10-11 18:43:57 +02:00
)
{
this . questConfig = this . configServer . getConfig ( ConfigTypes . QUEST ) ;
}
/ * *
* This method is called by / GetClientRepeatableQuests / and creates one element of quest type format ( see assets / database / templates / repeatableQuests . json ) .
* It randomly draws a quest type ( currently Elimination , Completion or Exploration ) as well as a trader who is providing the quest
* @param pmcLevel Player ' s level for requested items and reward generation
* @param pmcTraderInfo Players traper standing / rep levels
* @param questTypePool Possible quest types pool
* @param repeatableConfig Repeatable quest config
* @returns IRepeatableQuest
* /
public generateRepeatableQuest (
pmcLevel : number ,
pmcTraderInfo : Record < string , TraderInfo > ,
questTypePool : IQuestTypePool ,
2023-11-16 22:42:06 +01:00
repeatableConfig : IRepeatableQuestConfig ,
2023-10-11 18:43:57 +02:00
) : IRepeatableQuest
{
const questType = this . randomUtil . drawRandomFromList < string > ( questTypePool . types ) [ 0 ] ;
// get traders from whitelist and filter by quest type availability
2023-11-16 22:42:06 +01:00
let traders = repeatableConfig . traderWhitelist . filter ( ( x ) = > x . questTypes . includes ( questType ) ) . map ( ( x ) = >
x . traderId
) ;
2023-10-11 18:43:57 +02:00
// filter out locked traders
2023-11-16 22:42:06 +01:00
traders = traders . filter ( ( x ) = > pmcTraderInfo [ x ] . unlocked ) ;
2023-10-11 18:43:57 +02:00
const traderId = this . randomUtil . drawRandomFromList ( traders ) [ 0 ] ;
switch ( questType )
{
case "Elimination" :
return this . generateEliminationQuest ( pmcLevel , traderId , questTypePool , repeatableConfig ) ;
case "Completion" :
return this . generateCompletionQuest ( pmcLevel , traderId , repeatableConfig ) ;
case "Exploration" :
return this . generateExplorationQuest ( pmcLevel , traderId , questTypePool , repeatableConfig ) ;
2023-10-17 17:28:48 +02:00
case "Pickup" :
return this . generatePickupQuest ( pmcLevel , traderId , questTypePool , repeatableConfig ) ;
2023-10-11 18:43:57 +02:00
default :
throw new Error ( ` Unknown mission type ${ questType } . Should never be here! ` ) ;
}
}
/ * *
* Generate a randomised Elimination quest
* @param pmcLevel Player ' s level for requested items and reward generation
* @param traderId Trader from which the quest will be provided
* @param questTypePool Pools for quests ( used to avoid redundant quests )
* @param repeatableConfig The configuration for the repeatably kind ( daily , weekly ) as configured in QuestConfig for the requestd quest
* @returns Object of quest type format for "Elimination" ( see assets / database / templates / repeatableQuests . json )
* /
protected generateEliminationQuest (
pmcLevel : number ,
traderId : string ,
questTypePool : IQuestTypePool ,
2023-11-16 22:42:06 +01:00
repeatableConfig : IRepeatableQuestConfig ,
2023-10-11 18:43:57 +02:00
) : IElimination
{
const eliminationConfig = this . repeatableQuestHelper . getEliminationConfigByPmcLevel ( pmcLevel , repeatableConfig ) ;
const locationsConfig = repeatableConfig . locations ;
let targetsConfig = this . repeatableQuestHelper . probabilityObjectArray ( eliminationConfig . targets ) ;
const bodypartsConfig = this . repeatableQuestHelper . probabilityObjectArray ( eliminationConfig . bodyParts ) ;
2023-11-16 22:42:06 +01:00
const weaponCategoryRequirementConfig = this . repeatableQuestHelper . probabilityObjectArray (
eliminationConfig . weaponCategoryRequirements ,
) ;
const weaponRequirementConfig = this . repeatableQuestHelper . probabilityObjectArray (
eliminationConfig . weaponRequirements ,
) ;
2023-10-11 18:43:57 +02:00
// the difficulty of the quest varies in difficulty depending on the condition
// possible conditions are
// - amount of npcs to kill
// - type of npc to kill (scav, boss, pmc)
// - with hit to what body part they should be killed
// - from what distance they should be killed
// a random combination of listed conditions can be required
// possible conditions elements and their relative probability can be defined in QuestConfig.js
// We use ProbabilityObjectArray to draw by relative probability. e.g. for targets:
// "targets": {
// "Savage": 7,
// "AnyPmc": 2,
// "bossBully": 0.5
2023-11-16 22:42:06 +01:00
// }
2023-10-11 18:43:57 +02:00
// higher is more likely. We define the difficulty to be the inverse of the relative probability.
// We want to generate a reward which is scaled by the difficulty of this mission. To get a upper bound with which we scale
// the actual difficulty we calculate the minimum and maximum difficulty (max being the sum of max of each condition type
// times the number of kills we have to perform):
// the minumum difficulty is the difficulty for the most probable (= easiest target) with no additional conditions
const minDifficulty = 1 / targetsConfig . maxProbability ( ) ; // min difficulty is lowest amount of scavs without any constraints
// Target on bodyPart max. difficulty is that of the least probable element
const maxTargetDifficulty = 1 / targetsConfig . minProbability ( ) ;
const maxBodyPartsDifficulty = eliminationConfig . minKills / bodypartsConfig . minProbability ( ) ;
// maxDistDifficulty is defined by 2, this could be a tuning parameter if we don't like the reward generation
const maxDistDifficulty = 2 ;
const maxKillDifficulty = eliminationConfig . maxKills ;
2023-11-16 22:42:06 +01:00
function difficultyWeighing (
target : number ,
bodyPart : number ,
dist : number ,
kill : number ,
weaponRequirement : number ,
) : number
2023-10-11 18:43:57 +02:00
{
return Math . sqrt ( Math . sqrt ( target ) + bodyPart + dist + weaponRequirement ) * kill ;
}
2023-11-16 22:42:06 +01:00
targetsConfig = targetsConfig . filter ( ( x ) = >
Object . keys ( questTypePool . pool . Elimination . targets ) . includes ( x . key )
) ;
if ( targetsConfig . length === 0 || targetsConfig . every ( ( x ) = > x . data . isBoss ) )
2023-10-11 18:43:57 +02:00
{
// There are no more targets left for elimination; delete it as a possible quest type
// also if only bosses are left we need to leave otherwise it's a guaranteed boss elimination
// -> then it would not be a quest with low probability anymore
2023-11-16 22:42:06 +01:00
questTypePool . types = questTypePool . types . filter ( ( t ) = > t !== "Elimination" ) ;
2023-10-11 18:43:57 +02:00
return null ;
}
const targetKey = targetsConfig . draw ( ) [ 0 ] ;
const targetDifficulty = 1 / targetsConfig . probability ( targetKey ) ;
let locations : string [ ] = questTypePool . pool . Elimination . targets [ targetKey ] . locations ;
// we use any as location if "any" is in the pool and we do not hit the specific location random
// we use any also if the random condition is not met in case only "any" was in the pool
let locationKey = "any" ;
2023-11-16 22:42:06 +01:00
if (
locations . includes ( "any" )
&& ( eliminationConfig . specificLocationProb < Math . random ( ) || locations . length <= 1 )
)
2023-10-11 18:43:57 +02:00
{
locationKey = "any" ;
delete questTypePool . pool . Elimination . targets [ targetKey ] ;
}
else
{
2023-11-16 22:42:06 +01:00
locations = locations . filter ( ( l ) = > l !== "any" ) ;
2023-10-11 18:43:57 +02:00
if ( locations . length > 0 )
{
locationKey = this . randomUtil . drawRandomFromList < string > ( locations ) [ 0 ] ;
2023-11-16 22:42:06 +01:00
questTypePool . pool . Elimination . targets [ targetKey ] . locations = locations . filter ( ( l ) = >
l !== locationKey
) ;
2023-10-11 18:43:57 +02:00
if ( questTypePool . pool . Elimination . targets [ targetKey ] . locations . length === 0 )
{
delete questTypePool . pool . Elimination . targets [ targetKey ] ;
}
}
else
{
// never should reach this if everything works out
this . logger . debug ( "Ecountered issue when creating Elimination quest. Please report." ) ;
}
}
// draw the target body part and calculate the difficulty factor
let bodyPartsToClient = null ;
let bodyPartDifficulty = 0 ;
if ( eliminationConfig . bodyPartProb > Math . random ( ) )
{
// if we add a bodyPart condition, we draw randomly one or two parts
// each bodyPart of the BODYPARTS ProbabilityObjectArray includes the string(s) which need to be presented to the client in ProbabilityObjectArray.data
// e.g. we draw "Arms" from the probability array but must present ["LeftArm", "RightArm"] to the client
bodyPartsToClient = [ ] ;
const bodyParts = bodypartsConfig . draw ( this . randomUtil . randInt ( 1 , 3 ) , false ) ;
let probability = 0 ;
for ( const bi of bodyParts )
{
// more than one part lead to an "OR" condition hence more parts reduce the difficulty
probability += bodypartsConfig . probability ( bi ) ;
for ( const biClient of bodypartsConfig . data ( bi ) )
{
bodyPartsToClient . push ( biClient ) ;
}
}
bodyPartDifficulty = 1 / probability ;
}
// draw a distance condition
let distance = null ;
let distanceDifficulty = 0 ;
let isDistanceRequirementAllowed = ! eliminationConfig . distLocationBlacklist . includes ( locationKey ) ;
if ( targetsConfig . data ( targetKey ) . isBoss )
{
// get all boss spawn information
2023-11-16 22:42:06 +01:00
const bossSpawns = Object . values ( this . databaseServer . getTables ( ) . locations ) . filter ( ( x ) = >
"base" in x && "Id" in x . base
) . map ( ( x ) = > ( { Id : x.base.Id , BossSpawn : x.base.BossLocationSpawn } ) ) ;
2023-10-11 18:43:57 +02:00
// filter for the current boss to spawn on map
2023-11-16 22:42:06 +01:00
const thisBossSpawns = bossSpawns . map ( ( x ) = > ( {
Id : x.Id ,
BossSpawn : x.BossSpawn.filter ( ( e ) = > e . BossName === targetKey ) ,
} ) ) . filter ( ( x ) = > x . BossSpawn . length > 0 ) ;
2023-10-11 18:43:57 +02:00
// remove blacklisted locations
2023-11-16 22:42:06 +01:00
const allowedSpawns = thisBossSpawns . filter ( ( x ) = > ! eliminationConfig . distLocationBlacklist . includes ( x . Id ) ) ;
2023-10-11 18:43:57 +02:00
// if the boss spawns on nom-blacklisted locations and the current location is allowed we can generate a distance kill requirement
isDistanceRequirementAllowed = isDistanceRequirementAllowed && ( allowedSpawns . length > 0 ) ;
}
if ( eliminationConfig . distProb > Math . random ( ) && isDistanceRequirementAllowed )
{
// random distance with lower values more likely; simple distribution for starters...
2023-11-16 22:42:06 +01:00
distance = Math . floor (
Math . abs ( Math . random ( ) - Math . random ( ) ) * ( 1 + eliminationConfig . maxDist - eliminationConfig . minDist )
+ eliminationConfig . minDist ,
) ;
2023-10-11 18:43:57 +02:00
distance = Math . ceil ( distance / 5 ) * 5 ;
distanceDifficulty = maxDistDifficulty * distance / eliminationConfig . maxDist ;
}
let allowedWeaponsCategory : string = undefined ;
if ( eliminationConfig . weaponCategoryRequirementProb > Math . random ( ) )
{
// Pick a weighted weapon categroy
const weaponRequirement = weaponCategoryRequirementConfig . draw ( 1 , false ) ;
// Get the hideout id value stored in the .data array
allowedWeaponsCategory = weaponCategoryRequirementConfig . data ( weaponRequirement [ 0 ] ) [ 0 ] ;
}
// Only allow a specific weapon requirement if a weapon category was not chosen
let allowedWeapon : string = undefined ;
if ( ! allowedWeaponsCategory && eliminationConfig . weaponRequirementProb > Math . random ( ) )
{
const weaponRequirement = weaponRequirementConfig . draw ( 1 , false ) ;
const allowedWeaponsCategory = weaponRequirementConfig . data ( weaponRequirement [ 0 ] ) [ 0 ] ;
const allowedWeapons = this . itemHelper . getItemTplsOfBaseType ( allowedWeaponsCategory ) ;
allowedWeapon = this . randomUtil . getArrayValue ( allowedWeapons ) ;
}
// Draw how many npm kills are required
2023-10-24 16:01:31 +02:00
const desiredKillCount = this . getEliminationKillCount ( targetKey , targetsConfig , eliminationConfig ) ;
const killDifficulty = desiredKillCount ;
2023-10-11 18:43:57 +02:00
// not perfectly happy here; we give difficulty = 1 to the quest reward generation when we have the most diffucult mission
// e.g. killing reshala 5 times from a distance of 200m with a headshot.
const maxDifficulty = difficultyWeighing ( 1 , 1 , 1 , 1 , 1 ) ;
const curDifficulty = difficultyWeighing (
targetDifficulty / maxTargetDifficulty ,
bodyPartDifficulty / maxBodyPartsDifficulty ,
distanceDifficulty / maxDistDifficulty ,
killDifficulty / maxKillDifficulty ,
2023-11-16 22:42:06 +01:00
( allowedWeaponsCategory || allowedWeapon ) ? 1 : 0 ,
2023-10-11 18:43:57 +02:00
) ;
2023-10-15 11:49:23 +02:00
// Aforementioned issue makes it a bit crazy since now all easier quests give significantly lower rewards than Completion / Exploration
2023-10-11 18:43:57 +02:00
// I therefore moved the mapping a bit up (from 0.2...1 to 0.5...2) so that normal difficulty still gives good reward and having the
// crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1
const difficulty = this . mathUtil . mapToRange ( curDifficulty , minDifficulty , maxDifficulty , 0.5 , 2 ) ;
2023-10-17 17:28:48 +02:00
const quest = this . generateRepeatableTemplate ( "Elimination" , traderId , repeatableConfig . side ) as IElimination ;
2023-11-16 22:42:06 +01:00
2023-10-16 19:33:09 +02:00
// ASSUMPTION: All fence quests are for scavs
if ( traderId === Traders . FENCE )
{
quest . side = "Scav" ;
}
2023-10-11 18:43:57 +02:00
2023-10-17 17:28:48 +02:00
const availableForFinishCondition = quest . conditions . AvailableForFinish [ 0 ] ;
availableForFinishCondition . _props . counter . id = this . objectId . generate ( ) ;
availableForFinishCondition . _props . counter . conditions = [ ] ;
2023-11-01 14:29:47 +01:00
// Only add specific location condition if specific map selected
2023-10-11 18:43:57 +02:00
if ( locationKey !== "any" )
{
2023-11-16 22:42:06 +01:00
availableForFinishCondition . _props . counter . conditions . push (
this . generateEliminationLocation ( locationsConfig [ locationKey ] ) ,
) ;
2023-10-11 18:43:57 +02:00
}
2023-11-16 22:42:06 +01:00
availableForFinishCondition . _props . counter . conditions . push (
this . generateEliminationCondition (
targetKey ,
bodyPartsToClient ,
distance ,
allowedWeapon ,
allowedWeaponsCategory ,
) ,
) ;
2023-10-24 16:01:31 +02:00
availableForFinishCondition . _props . value = desiredKillCount ;
2023-10-17 17:28:48 +02:00
availableForFinishCondition . _props . id = this . objectId . generate ( ) ;
2023-10-11 18:43:57 +02:00
quest . location = this . getQuestLocationByMapId ( locationKey ) ;
2023-11-16 22:42:06 +01:00
quest . rewards = this . generateReward (
pmcLevel ,
Math . min ( difficulty , 1 ) ,
traderId ,
repeatableConfig ,
eliminationConfig ,
) ;
2023-10-11 18:43:57 +02:00
return quest ;
}
2023-10-24 16:01:31 +02:00
/ * *
* Get a number of kills neded to complete elimination quest
* @param targetKey Target type desired e . g . anyPmc / bossBully / Savage
* @param targetsConfig Config
* @param eliminationConfig Config
* @returns Number of AI to kill
* /
2023-11-16 22:42:06 +01:00
protected getEliminationKillCount (
targetKey : string ,
targetsConfig : ProbabilityObjectArray < string , IBossInfo > ,
eliminationConfig : IEliminationConfig ,
) : number
2023-10-24 16:01:31 +02:00
{
if ( targetsConfig . data ( targetKey ) . isBoss )
{
return this . randomUtil . randInt ( eliminationConfig . minBossKills , eliminationConfig . maxBossKills + 1 ) ;
}
if ( targetsConfig . data ( targetKey ) . isPmc )
{
2023-10-24 18:06:02 +02:00
return this . randomUtil . randInt ( eliminationConfig . minPmcKills , eliminationConfig . maxPmcKills + 1 ) ;
2023-10-24 16:01:31 +02:00
}
return this . randomUtil . randInt ( eliminationConfig . minKills , eliminationConfig . maxKills + 1 ) ;
}
2023-10-11 18:43:57 +02:00
/ * *
* A repeatable quest , besides some more or less static components , exists of reward and condition ( see assets / database / templates / repeatableQuests . json )
* This is a helper method for GenerateEliminationQuest to create a location condition .
*
* @param { string } location the location on which to fulfill the elimination quest
2023-11-01 14:29:47 +01:00
* @returns { IEliminationCondition } object of "Elimination" - location - subcondition
2023-10-11 18:43:57 +02:00
* /
2023-11-01 14:29:47 +01:00
protected generateEliminationLocation ( location : string [ ] ) : IEliminationCondition
2023-10-11 18:43:57 +02:00
{
const propsObject : IEliminationCondition = {
2023-11-16 22:42:06 +01:00
_props : { target : location , id : this.objectId.generate ( ) , dynamicLocale : true } ,
_parent : "Location" ,
2023-10-11 18:43:57 +02:00
} ;
2023-11-16 22:42:06 +01:00
2023-10-11 18:43:57 +02:00
return propsObject ;
}
/ * *
2023-11-01 14:29:47 +01:00
* Create kill condition for an elimination quest
* @param target Bot type target of elimination quest e . g . "AnyPmc" , "Savage"
* @param targetedBodyParts Body parts player must hit
* @param distance Distance from which to kill ( currently only >= supported
* @param allowedWeapon What weapon must be used - undefined = any
* @param allowedWeaponCategory What category of weapon must be used - undefined = any
* @returns IEliminationCondition object
2023-10-11 18:43:57 +02:00
* /
2023-11-16 22:42:06 +01:00
protected generateEliminationCondition (
target : string ,
targetedBodyParts : string [ ] ,
distance : number ,
allowedWeapon : string ,
allowedWeaponCategory : string ,
) : IEliminationCondition
2023-10-11 18:43:57 +02:00
{
const killConditionProps : IKillConditionProps = {
target : target ,
value : 1 ,
id : this.objectId.generate ( ) ,
2023-11-16 22:42:06 +01:00
dynamicLocale : true ,
2023-10-11 18:43:57 +02:00
} ;
if ( target . startsWith ( "boss" ) )
{
killConditionProps . target = "Savage" ;
killConditionProps . savageRole = [ target ] ;
}
2023-11-01 14:29:47 +01:00
// Has specific body part hit condition
if ( targetedBodyParts )
2023-10-11 18:43:57 +02:00
{
2023-11-01 14:29:47 +01:00
killConditionProps . bodyPart = targetedBodyParts ;
2023-10-11 18:43:57 +02:00
}
2023-10-15 11:49:23 +02:00
// Dont allow distance + melee requirement
if ( distance && allowedWeaponCategory !== "5b5f7a0886f77409407a7f96" )
2023-10-11 18:43:57 +02:00
{
2023-11-16 22:42:06 +01:00
killConditionProps . distance = { compareMethod : ">=" , value : distance } ;
2023-10-11 18:43:57 +02:00
}
2023-11-01 14:29:47 +01:00
// Has specific weapon requirement
2023-10-11 18:43:57 +02:00
if ( allowedWeapon )
{
killConditionProps . weapon = [ allowedWeapon ] ;
}
2023-11-01 14:29:47 +01:00
// Has specific weapon category requirement
2023-10-11 18:43:57 +02:00
if ( allowedWeaponCategory ? . length > 0 )
{
killConditionProps . weaponCategories = [ allowedWeaponCategory ] ;
}
2023-11-16 22:42:06 +01:00
return { _props : killConditionProps , _parent : "Kills" } ;
2023-10-11 18:43:57 +02:00
}
/ * *
* Generates a valid Completion quest
*
* @param { integer } pmcLevel player ' s level for requested items and reward generation
* @param { string } traderId trader from which the quest will be provided
* @param { object } repeatableConfig The configuration for the repeatably kind ( daily , weekly ) as configured in QuestConfig for the requestd quest
* @returns { object } object of quest type format for "Completion" ( see assets / database / templates / repeatableQuests . json )
* /
protected generateCompletionQuest (
pmcLevel : number ,
traderId : string ,
2023-11-16 22:42:06 +01:00
repeatableConfig : IRepeatableQuestConfig ,
2023-10-11 18:43:57 +02:00
) : ICompletion
{
const completionConfig = repeatableConfig . questConfig . Completion ;
const levelsConfig = repeatableConfig . rewardScaling . levels ;
const roublesConfig = repeatableConfig . rewardScaling . roubles ;
2023-11-19 15:58:45 +01:00
const distinctItemsToRetrieveCount = this . randomUtil . getInt ( 1 , completionConfig . uniqueItemCount ) ;
2023-10-11 18:43:57 +02:00
2023-11-16 22:42:06 +01:00
const quest = this . generateRepeatableTemplate ( "Completion" , traderId , repeatableConfig . side ) as ICompletion ;
2023-10-11 18:43:57 +02:00
2023-11-15 00:05:34 +01:00
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant"
const possibleItemsToRetrievePool = this . getRewardableItems ( repeatableConfig , traderId ) ;
2023-10-11 18:43:57 +02:00
// Be fair, don't let the items be more expensive than the reward
2023-11-16 22:42:06 +01:00
let roublesBudget = Math . floor (
this . mathUtil . interp1 ( pmcLevel , levelsConfig , roublesConfig ) * this . randomUtil . getFloat ( 0.5 , 1 ) ,
) ;
2023-10-11 18:43:57 +02:00
roublesBudget = Math . max ( roublesBudget , 5000 ) ;
2023-11-16 22:42:06 +01:00
let itemSelection = possibleItemsToRetrievePool . filter ( ( x ) = >
this . itemHelper . getItemPrice ( x [ 0 ] ) < roublesBudget
) ;
2023-10-11 18:43:57 +02:00
// We also have the option to use whitelist and/or blacklist which is defined in repeatableQuests.json as
// [{"minPlayerLevel": 1, "itemIds": ["id1",...]}, {"minPlayerLevel": 15, "itemIds": ["id3",...]}]
if ( repeatableConfig . questConfig . Completion . useWhitelist )
{
2023-11-16 22:42:06 +01:00
const itemWhitelist =
this . databaseServer . getTables ( ) . templates . repeatableQuests . data . Completion . itemsWhitelist ;
2023-10-11 18:43:57 +02:00
// Filter and concatenate the arrays according to current player level
2023-11-16 22:42:06 +01:00
const itemIdsWhitelisted = itemWhitelist . filter ( ( p ) = > p . minPlayerLevel <= pmcLevel ) . reduce (
( a , p ) = > a . concat ( p . itemIds ) ,
[ ] ,
) ;
itemSelection = itemSelection . filter ( ( x ) = >
2023-10-11 18:43:57 +02:00
{
// Whitelist can contain item tpls and item base type ids
2023-11-16 22:42:06 +01:00
return ( itemIdsWhitelisted . some ( ( v ) = > this . itemHelper . isOfBaseclass ( x [ 0 ] , v ) )
|| itemIdsWhitelisted . includes ( x [ 0 ] ) ) ;
2023-10-11 18:43:57 +02:00
} ) ;
// check if items are missing
2023-11-16 22:42:06 +01:00
// const flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []);
// const missing = itemIdsWhitelisted.filter(l => !flatList.includes(l));
2023-10-11 18:43:57 +02:00
}
if ( repeatableConfig . questConfig . Completion . useBlacklist )
{
2023-11-16 22:42:06 +01:00
const itemBlacklist =
this . databaseServer . getTables ( ) . templates . repeatableQuests . data . Completion . itemsBlacklist ;
2023-10-11 18:43:57 +02:00
// we filter and concatenate the arrays according to current player level
2023-11-16 22:42:06 +01:00
const itemIdsBlacklisted = itemBlacklist . filter ( ( p ) = > p . minPlayerLevel <= pmcLevel ) . reduce (
( a , p ) = > a . concat ( p . itemIds ) ,
[ ] ,
) ;
itemSelection = itemSelection . filter ( ( x ) = >
2023-10-11 18:43:57 +02:00
{
2023-11-16 22:42:06 +01:00
return itemIdsBlacklisted . every ( ( v ) = > ! this . itemHelper . isOfBaseclass ( x [ 0 ] , v ) )
|| ! itemIdsBlacklisted . includes ( x [ 0 ] ) ;
2023-10-11 18:43:57 +02:00
} ) ;
}
if ( itemSelection . length === 0 )
{
2023-11-16 22:42:06 +01:00
this . logger . error (
this . localisationService . getText (
"repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive" ,
) ,
) ;
2023-10-11 18:43:57 +02:00
return null ;
}
// Draw items to ask player to retrieve
2023-11-19 21:29:41 +01:00
let isAmmo = 0
2023-11-19 15:58:45 +01:00
for ( let i = 0 ; i < distinctItemsToRetrieveCount ; i ++ )
2023-10-11 18:43:57 +02:00
{
const itemSelected = itemSelection [ this . randomUtil . randInt ( itemSelection . length ) ] ;
const itemUnitPrice = this . itemHelper . getItemPrice ( itemSelected [ 0 ] ) ;
let minValue = completionConfig . minRequestedAmount ;
let maxValue = completionConfig . maxRequestedAmount ;
if ( this . itemHelper . isOfBaseclass ( itemSelected [ 0 ] , BaseClasses . AMMO ) )
{
2023-11-19 21:29:41 +01:00
// Prevent multiple ammo requirements from being picked, stop after 6 attempts
if ( isAmmo > 0 && isAmmo < 6 )
{
isAmmo ++ ;
i -- ;
continue ;
}
isAmmo ++ ;
2023-10-11 18:43:57 +02:00
minValue = completionConfig . minRequestedBulletAmount ;
maxValue = completionConfig . maxRequestedBulletAmount ;
}
let value = minValue ;
// get the value range within budget
maxValue = Math . min ( maxValue , Math . floor ( roublesBudget / itemUnitPrice ) ) ;
if ( maxValue > minValue )
{
// if it doesn't blow the budget we have for the request, draw a random amount of the selected
// item type to be requested
value = this . randomUtil . randInt ( minValue , maxValue + 1 ) ;
}
roublesBudget -= value * itemUnitPrice ;
// push a CompletionCondition with the item and the amount of the item
quest . conditions . AvailableForFinish . push ( this . generateCompletionAvailableForFinish ( itemSelected [ 0 ] , value ) ) ;
if ( roublesBudget > 0 )
{
// reduce the list possible items to fulfill the new budget constraint
2023-11-16 22:42:06 +01:00
itemSelection = itemSelection . filter ( ( x ) = > this . itemHelper . getItemPrice ( x [ 0 ] ) < roublesBudget ) ;
2023-10-11 18:43:57 +02:00
if ( itemSelection . length === 0 )
{
break ;
}
}
else
{
break ;
}
}
2023-11-07 15:38:13 +01:00
quest . rewards = this . generateReward ( pmcLevel , 1 , traderId , repeatableConfig , completionConfig ) ;
2023-10-11 18:43:57 +02:00
return quest ;
}
/ * *
* A repeatable quest , besides some more or less static components , exists of reward and condition ( see assets / database / templates / repeatableQuests . json )
* This is a helper method for GenerateCompletionQuest to create a completion condition ( of which a completion quest theoretically can have many )
*
2023-11-18 13:33:00 +01:00
* @param { string } itemTpl id of the item to request
2023-10-11 18:43:57 +02:00
* @param { integer } value amount of items of this specific type to request
* @returns { object } object of "Completion" - condition
* /
2023-11-18 13:33:00 +01:00
protected generateCompletionAvailableForFinish ( itemTpl : string , value : number ) : ICompletionAvailableFor
2023-10-11 18:43:57 +02:00
{
let minDurability = 0 ;
let onlyFoundInRaid = true ;
2023-11-16 22:42:06 +01:00
if (
2023-11-18 13:33:00 +01:00
this . itemHelper . isOfBaseclass ( itemTpl , BaseClasses . WEAPON )
|| this . itemHelper . isOfBaseclass ( itemTpl , BaseClasses . ARMOR )
2023-11-16 22:42:06 +01:00
)
2023-10-11 18:43:57 +02:00
{
2023-11-18 13:33:00 +01:00
minDurability = this . randomUtil . getArrayValue ( [ 60 , 80 ] ) ;
2023-10-11 18:43:57 +02:00
}
2023-11-18 13:33:00 +01:00
// By default all collected items must be FiR, except dog tags
2023-11-16 22:42:06 +01:00
if (
2023-11-18 13:33:00 +01:00
this . itemHelper . isOfBaseclass ( itemTpl , BaseClasses . DOG_TAG_USEC )
|| this . itemHelper . isOfBaseclass ( itemTpl , BaseClasses . DOG_TAG_BEAR )
2023-11-16 22:42:06 +01:00
)
2023-10-11 18:43:57 +02:00
{
onlyFoundInRaid = false ;
}
return {
_props : {
id : this.objectId.generate ( ) ,
parentId : "" ,
dynamicLocale : true ,
index : 0 ,
visibilityConditions : [ ] ,
2023-11-18 13:33:00 +01:00
target : [ itemTpl ] ,
2023-10-11 18:43:57 +02:00
value : value ,
minDurability : minDurability ,
maxDurability : 100 ,
dogtagLevel : 0 ,
2023-11-16 22:42:06 +01:00
onlyFoundInRaid : onlyFoundInRaid ,
2023-10-11 18:43:57 +02:00
} ,
_parent : "HandoverItem" ,
2023-11-16 22:42:06 +01:00
dynamicLocale : true ,
2023-10-11 18:43:57 +02:00
} ;
}
/ * *
* Generates a valid Exploration quest
*
* @param { integer } pmcLevel player ' s level for reward generation
* @param { string } traderId trader from which the quest will be provided
* @param { object } questTypePool Pools for quests ( used to avoid redundant quests )
* @param { object } repeatableConfig The configuration for the repeatably kind ( daily , weekly ) as configured in QuestConfig for the requestd quest
* @returns { object } object of quest type format for "Exploration" ( see assets / database / templates / repeatableQuests . json )
* /
protected generateExplorationQuest (
pmcLevel : number ,
traderId : string ,
questTypePool : IQuestTypePool ,
2023-11-16 22:42:06 +01:00
repeatableConfig : IRepeatableQuestConfig ,
2023-10-11 18:43:57 +02:00
) : IExploration
{
const explorationConfig = repeatableConfig . questConfig . Exploration ;
2023-11-18 13:32:03 +01:00
const requiresSpecificExtract = Math . random ( ) < repeatableConfig . questConfig . Exploration . specificExits . probability ;
2023-10-11 18:43:57 +02:00
if ( Object . keys ( questTypePool . pool . Exploration . locations ) . length === 0 )
{
// there are no more locations left for exploration; delete it as a possible quest type
2023-11-16 22:42:06 +01:00
questTypePool . types = questTypePool . types . filter ( ( t ) = > t !== "Exploration" ) ;
2023-10-11 18:43:57 +02:00
return null ;
}
// if the location we draw is factory, it's possible to either get factory4_day and factory4_night or only one
// of the both
const locationKey : string = this . randomUtil . drawRandomFromDict ( questTypePool . pool . Exploration . locations ) [ 0 ] ;
const locationTarget = questTypePool . pool . Exploration . locations [ locationKey ] ;
// remove the location from the available pool
delete questTypePool . pool . Exploration . locations [ locationKey ] ;
2023-11-18 13:32:03 +01:00
// Different max extract count when specific extract needed
const numExtracts = this . randomUtil . randInt ( 1 , requiresSpecificExtract ? explorationConfig.maxExtractsWithSpecificExit : explorationConfig.maxExtracts + 1 ) ;
2023-10-11 18:43:57 +02:00
2023-11-16 22:42:06 +01:00
const quest = this . generateRepeatableTemplate ( "Exploration" , traderId , repeatableConfig . side ) as IExploration ;
2023-10-11 18:43:57 +02:00
const exitStatusCondition : IExplorationCondition = {
_parent : "ExitStatus" ,
2023-11-16 22:42:06 +01:00
_props : { id : this.objectId.generate ( ) , dynamicLocale : true , status : [ "Survived" ] } ,
2023-10-11 18:43:57 +02:00
} ;
const locationCondition : IExplorationCondition = {
_parent : "Location" ,
2023-11-16 22:42:06 +01:00
_props : { id : this.objectId.generate ( ) , dynamicLocale : true , target : locationTarget } ,
2023-10-11 18:43:57 +02:00
} ;
quest . conditions . AvailableForFinish [ 0 ] . _props . counter . id = this . objectId . generate ( ) ;
2023-11-16 22:42:06 +01:00
quest . conditions . AvailableForFinish [ 0 ] . _props . counter . conditions = [ exitStatusCondition , locationCondition ] ;
2023-10-11 18:43:57 +02:00
quest . conditions . AvailableForFinish [ 0 ] . _props . value = numExtracts ;
quest . conditions . AvailableForFinish [ 0 ] . _props . id = this . objectId . generate ( ) ;
quest . location = this . getQuestLocationByMapId ( locationKey ) ;
2023-11-18 13:32:03 +01:00
if ( requiresSpecificExtract )
2023-10-11 18:43:57 +02:00
{
// Filter by whitelist, it's also possible that the field "PassageRequirement" does not exist (e.g. Shoreline)
// Scav exits are not listed at all in locations.base currently. If that changes at some point, additional filtering will be required
2023-11-16 22:42:06 +01:00
const mapExits =
( this . databaseServer . getTables ( ) . locations [ locationKey . toLowerCase ( ) ] . base as ILocationBase ) . exits ;
const possibleExists = mapExits . filter ( ( x ) = >
( ! ( "PassageRequirement" in x )
|| repeatableConfig . questConfig . Exploration . specificExits . passageRequirementWhitelist . includes (
x . PassageRequirement ,
) ) && x . Chance > 0
2023-10-11 18:43:57 +02:00
) ;
const exit = this . randomUtil . drawRandomFromList ( possibleExists , 1 ) [ 0 ] ;
const exitCondition = this . generateExplorationExitCondition ( exit ) ;
quest . conditions . AvailableForFinish [ 0 ] . _props . counter . conditions . push ( exitCondition ) ;
}
// Difficulty for exploration goes from 1 extract to maxExtracts
// Difficulty for reward goes from 0.2...1 -> map
const difficulty = this . mathUtil . mapToRange ( numExtracts , 1 , explorationConfig . maxExtracts , 0.2 , 1 ) ;
2023-11-07 15:38:13 +01:00
quest . rewards = this . generateReward ( pmcLevel , difficulty , traderId , repeatableConfig , explorationConfig ) ;
2023-10-11 18:43:57 +02:00
return quest ;
}
2023-10-17 17:28:48 +02:00
protected generatePickupQuest (
pmcLevel : number ,
traderId : string ,
questTypePool : IQuestTypePool ,
2023-11-16 22:42:06 +01:00
repeatableConfig : IRepeatableQuestConfig ,
2023-10-17 17:28:48 +02:00
) : IPickup
{
const pickupConfig = repeatableConfig . questConfig . Pickup ;
const quest = this . generateRepeatableTemplate ( "Pickup" , traderId , repeatableConfig . side ) as IPickup ;
const itemTypeToFetchWithCount = this . randomUtil . getArrayValue ( pickupConfig . ItemTypeToFetchWithMaxCount ) ;
2023-11-16 22:42:06 +01:00
const itemCountToFetch = this . randomUtil . randInt (
itemTypeToFetchWithCount . minPickupCount ,
itemTypeToFetchWithCount . maxPickupCount + 1 ,
) ;
2023-10-17 17:28:48 +02:00
// Choose location - doesnt seem to work for anything other than 'any'
2023-11-16 22:42:06 +01:00
// const locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0];
// const locationTarget = questTypePool.pool.Pickup.locations[locationKey];
2023-10-17 17:28:48 +02:00
2023-11-16 22:42:06 +01:00
const findCondition = quest . conditions . AvailableForFinish . find ( ( x ) = > x . _parent === "FindItem" ) ;
2023-10-17 17:28:48 +02:00
findCondition . _props . target = [ itemTypeToFetchWithCount . itemType ] ;
findCondition . _props . value = itemCountToFetch ;
2023-11-16 22:42:06 +01:00
const counterCreatorCondition = quest . conditions . AvailableForFinish . find ( ( x ) = > x . _parent === "CounterCreator" ) ;
// const locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location");
// (locationCondition._props as ILocationConditionProps).target = [...locationTarget];
2023-10-17 17:28:48 +02:00
2023-11-16 22:42:06 +01:00
const equipmentCondition = counterCreatorCondition . _props . counter . conditions . find ( ( x ) = >
x . _parent === "Equipment"
) ;
( equipmentCondition . _props as IEquipmentConditionProps ) . equipmentInclusive = [ [
itemTypeToFetchWithCount . itemType ,
] ] ;
2023-10-17 17:28:48 +02:00
// Add rewards
2023-11-07 15:38:13 +01:00
quest . rewards = this . generateReward ( pmcLevel , 1 , traderId , repeatableConfig , pickupConfig ) ;
2023-10-17 17:28:48 +02:00
return quest ;
}
2023-10-11 18:43:57 +02:00
/ * *
* Convert a location into an quest code can read ( e . g . factory4_day into 55 f2d3fd4bdc2d5f408b4567 )
* @param locationKey e . g factory4_day
* @returns guid
* /
protected getQuestLocationByMapId ( locationKey : string ) : string
{
return this . questConfig . locationIdMap [ locationKey ] ;
}
/ * *
* Exploration repeatable quests can specify a required extraction point .
* This method creates the according object which will be appended to the conditions array
*
* @param { string } exit The exit name to generate the condition for
* @returns { object } Exit condition
* /
protected generateExplorationExitCondition ( exit : Exit ) : IExplorationCondition
{
return {
_parent : "ExitName" ,
2023-11-16 22:42:06 +01:00
_props : { exitName : exit.Name , id : this.objectId.generate ( ) , dynamicLocale : true } ,
2023-10-11 18:43:57 +02:00
} ;
}
/ * *
* Generate the reward for a mission . A reward can consist of
* - Experience
* - Money
* - Items
* - Trader Reputation
*
* The reward is dependent on the player level as given by the wiki . The exact mapping of pmcLevel to
* experience / money / items / trader reputation can be defined in QuestConfig . js
*
* There ' s also a random variation of the reward the spread of which can be also defined in the config .
*
* Additonaly , a scaling factor w . r . t . quest difficulty going from 0.2 . . . 1 can be used
*
* @param { integer } pmcLevel player ' s level
* @param { number } difficulty a reward scaling factor goint from 0.2 to 1
* @param { string } traderId the trader for reputation gain ( and possible in the future filtering of reward item type based on trader )
* @param { object } repeatableConfig The configuration for the repeatably kind ( daily , weekly ) as configured in QuestConfig for the requestd quest
* @returns { object } object of "Reward" - type that can be given for a repeatable mission
* /
protected generateReward (
pmcLevel : number ,
difficulty : number ,
traderId : string ,
2023-11-07 15:38:13 +01:00
repeatableConfig : IRepeatableQuestConfig ,
2023-11-16 22:42:06 +01:00
questConfig : IBaseQuestConfig ,
2023-10-11 18:43:57 +02:00
) : IRewards
{
// difficulty could go from 0.2 ... -> for lowest diffuculty receive 0.2*nominal reward
const levelsConfig = repeatableConfig . rewardScaling . levels ;
const roublesConfig = repeatableConfig . rewardScaling . roubles ;
const xpConfig = repeatableConfig . rewardScaling . experience ;
const itemsConfig = repeatableConfig . rewardScaling . items ;
const rewardSpreadConfig = repeatableConfig . rewardScaling . rewardSpread ;
2023-11-07 15:38:13 +01:00
const skillRewardChanceConfig = repeatableConfig . rewardScaling . skillRewardChance ;
const skillPointRewardConfig = repeatableConfig . rewardScaling . skillPointReward ;
2023-10-11 18:43:57 +02:00
const reputationConfig = repeatableConfig . rewardScaling . reputation ;
2023-11-01 12:36:13 +01:00
if ( Number . isNaN ( difficulty ) )
2023-10-11 18:43:57 +02:00
{
difficulty = 1 ;
this . logger . warning ( this . localisationService . getText ( "repeatable-difficulty_was_nan" ) ) ;
}
// rewards are generated based on pmcLevel, difficulty and a random spread
2023-11-16 22:42:06 +01:00
const rewardXP = Math . floor (
difficulty * this . mathUtil . interp1 ( pmcLevel , levelsConfig , xpConfig )
* this . randomUtil . getFloat ( 1 - rewardSpreadConfig , 1 + rewardSpreadConfig ) ,
) ;
const rewardRoubles = Math . floor (
difficulty * this . mathUtil . interp1 ( pmcLevel , levelsConfig , roublesConfig )
* this . randomUtil . getFloat ( 1 - rewardSpreadConfig , 1 + rewardSpreadConfig ) ,
) ;
const rewardNumItems = this . randomUtil . randInt (
1 ,
Math . round ( this . mathUtil . interp1 ( pmcLevel , levelsConfig , itemsConfig ) ) + 1 ,
) ;
const rewardReputation =
Math . round (
100 * difficulty * this . mathUtil . interp1 ( pmcLevel , levelsConfig , reputationConfig )
* this . randomUtil . getFloat ( 1 - rewardSpreadConfig , 1 + rewardSpreadConfig ) ,
) / 100 ;
2023-11-07 15:38:13 +01:00
const skillRewardChance = this . mathUtil . interp1 ( pmcLevel , levelsConfig , skillRewardChanceConfig ) ;
const skillPointReward = this . mathUtil . interp1 ( pmcLevel , levelsConfig , skillPointRewardConfig ) ;
2023-10-11 18:43:57 +02:00
2023-10-15 11:44:12 +02:00
// Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink)
2023-10-11 18:43:57 +02:00
let roublesBudget = rewardRoubles ;
2023-11-15 00:05:34 +01:00
let rewardItemPool = this . chooseRewardItemsWithinBudget ( repeatableConfig , roublesBudget , traderId ) ;
2023-10-11 18:43:57 +02:00
const rewards : IRewards = {
Started : [ ] ,
2023-11-18 12:13:36 +01:00
Success : [ ] ,
2023-11-16 22:42:06 +01:00
Fail : [ ] ,
2023-10-11 18:43:57 +02:00
} ;
2023-11-18 12:13:36 +01:00
let rewardIndex = 0 ;
// Add xp reward
if ( rewardXP > 0 )
{
rewards . Success . push ( { value : rewardXP , type : "Experience" , index : rewardIndex } ) ;
rewardIndex ++ ;
}
2023-11-15 00:05:34 +01:00
// Add money reward
2023-11-17 19:39:08 +01:00
if ( traderId === Traders . PEACEKEEPER || traderId === Traders . FENCE )
2023-10-11 18:43:57 +02:00
{
// convert to equivalent dollars
2023-11-16 22:42:06 +01:00
rewards . Success . push (
2023-11-18 12:13:36 +01:00
this . generateRewardItem ( Money . EUROS , this . handbookHelper . fromRUB ( rewardRoubles , Money . EUROS ) , rewardIndex ) ,
2023-11-16 22:42:06 +01:00
) ;
2023-10-11 18:43:57 +02:00
}
else
{
2023-11-18 12:13:36 +01:00
rewards . Success . push ( this . generateRewardItem ( Money . ROUBLES , rewardRoubles , rewardIndex ) ) ;
2023-10-11 18:43:57 +02:00
}
2023-11-18 12:13:36 +01:00
rewardIndex ++ ;
2023-10-11 18:43:57 +02:00
2023-11-19 14:21:34 +01:00
const traderWhitelistDetails = repeatableConfig . traderWhitelist . find ( ( x ) = > x . traderId === traderId ) ;
if ( traderWhitelistDetails . rewardCanBeWeapon && this . randomUtil . getChance100 ( traderWhitelistDetails . weaponRewardChancePercent ) )
{
// Add a random default preset weapon as reward
const defaultPresets = Object . values ( this . presetHelper . getDefaultPresets ( ) ) ;
const defaultPreset = this . randomUtil . getArrayValue ( defaultPresets ) ;
// use _encyclopedia as its always the base items _tpl, items[0] isnt guaranteed to be base item
rewards . Success . push ( this . generateRewardItem ( defaultPreset . _encyclopedia , 1 , rewardIndex , defaultPreset . _items ) ) ;
rewardIndex ++ ;
}
2023-11-15 00:05:34 +01:00
if ( rewardItemPool . length > 0 )
2023-10-11 18:43:57 +02:00
{
2023-11-19 14:21:34 +01:00
for ( let i = 0 ; i < rewardNumItems ; i ++ )
2023-10-11 18:43:57 +02:00
{
2023-11-19 14:21:34 +01:00
let rewardItemStackCount = 1 ;
2023-11-15 00:05:34 +01:00
const itemSelected = rewardItemPool [ this . randomUtil . randInt ( rewardItemPool . length ) ] ;
2023-10-15 12:46:33 +02:00
if ( this . itemHelper . isOfBaseclass ( itemSelected . _id , BaseClasses . AMMO ) )
2023-10-11 18:43:57 +02:00
{
// Dont reward ammo that stacks to less than what's defined in config
2023-10-15 12:46:33 +02:00
if ( itemSelected . _props . StackMaxSize < repeatableConfig . rewardAmmoStackMinSize )
2023-10-11 18:43:57 +02:00
{
continue ;
}
2023-11-14 22:43:37 +01:00
// Randomise the cartridge count returned
2023-11-19 14:21:34 +01:00
rewardItemStackCount = this . randomUtil . randInt (
2023-11-16 22:42:06 +01:00
repeatableConfig . rewardAmmoStackMinSize ,
itemSelected . _props . StackMaxSize ,
) ;
2023-10-11 18:43:57 +02:00
}
2023-11-15 20:43:35 +01:00
// 25% chance to double reward stack (item should be stackable and not weapon)
2023-11-19 14:21:34 +01:00
if ( this . canIncreaseRewardItemStackSize ( itemSelected , 70000 ) )
2023-11-15 20:43:35 +01:00
{
2023-11-19 14:21:34 +01:00
rewardItemStackCount = this . getRandomisedRewardItemStackSizeByPrice ( itemSelected ) ;
2023-11-15 20:43:35 +01:00
}
2023-11-19 14:21:34 +01:00
rewards . Success . push ( this . generateRewardItem ( itemSelected . _id , rewardItemStackCount , rewardIndex ) ) ;
2023-11-18 12:13:36 +01:00
rewardIndex ++ ;
2023-11-19 14:21:34 +01:00
const itemCost = this . itemHelper . getStaticItemPrice ( itemSelected . _id ) ;
roublesBudget -= rewardItemStackCount * itemCost ;
2023-10-11 18:43:57 +02:00
2023-11-18 12:13:36 +01:00
// If we still have budget narrow down possible items
2023-10-11 18:43:57 +02:00
if ( roublesBudget > 0 )
{
2023-10-15 12:46:33 +02:00
// Filter possible reward items to only items with a price below the remaining budget
2023-11-16 22:42:06 +01:00
rewardItemPool = rewardItemPool . filter ( ( x ) = >
this . itemHelper . getStaticItemPrice ( x . _id ) < roublesBudget
) ;
2023-11-15 00:05:34 +01:00
if ( rewardItemPool . length === 0 )
2023-10-11 18:43:57 +02:00
{
2023-10-15 12:46:33 +02:00
break ; // No reward items left, exit
2023-10-11 18:43:57 +02:00
}
}
else
{
break ;
}
}
}
2023-10-15 12:46:33 +02:00
// Add rep reward to rewards array
2023-10-11 18:43:57 +02:00
if ( rewardReputation > 0 )
{
2023-11-18 12:13:36 +01:00
const reward : IReward = { target : traderId , value : rewardReputation , type : "TraderStanding" , index : rewardIndex } ;
2023-10-11 18:43:57 +02:00
rewards . Success . push ( reward ) ;
2023-11-18 12:13:36 +01:00
rewardIndex ++ ;
2023-10-11 18:43:57 +02:00
}
2023-11-18 12:13:36 +01:00
// Chance of adding skill reward
2023-11-07 15:38:13 +01:00
if ( this . randomUtil . getChance100 ( skillRewardChance * 100 ) )
{
const reward : IReward = {
target : this.randomUtil.getArrayValue ( questConfig . possibleSkillRewards ) ,
value : skillPointReward ,
type : "Skill" ,
2023-11-18 12:13:36 +01:00
index : rewardIndex ,
2023-11-07 15:38:13 +01:00
} ;
rewards . Success . push ( reward ) ;
}
2023-10-11 18:43:57 +02:00
return rewards ;
}
2023-11-15 20:43:35 +01:00
/ * *
* Should reward item have stack size increased ( 25 % chance )
* @param item Item to possibly increase stack size of
2023-11-19 14:21:34 +01:00
* @param maxRoublePriceToStack Maximum rouble price an item can be to still be chosen for stacking
2023-11-15 20:43:35 +01:00
* @returns True if it should
* /
2023-11-19 14:21:34 +01:00
protected canIncreaseRewardItemStackSize ( item : ITemplateItem , maxRoublePriceToStack : number ) : boolean
2023-11-15 20:43:35 +01:00
{
2023-11-19 14:21:34 +01:00
return this . itemHelper . getStaticItemPrice ( item . _id ) < maxRoublePriceToStack
&& ! this . itemHelper . isOfBaseclasses ( item . _id , [ BaseClasses . WEAPON , BaseClasses . AMMO ] )
2023-11-15 20:43:35 +01:00
&& this . randomUtil . getChance100 ( 25 ) ;
}
2023-11-19 14:21:34 +01:00
/ * *
* Get a randomised number a reward items stack size should be based on its handbook price
* @param item Reward item to get stack size for
* @returns Stack size value
* /
protected getRandomisedRewardItemStackSizeByPrice ( item : ITemplateItem ) : number
{
const rewardItemPrice = this . itemHelper . getStaticItemPrice ( item . _id ) ;
if ( rewardItemPrice < 3000 ) {
return this . randomUtil . getArrayValue ( [ 2 , 3 , 4 ] ) ;
}
else if ( rewardItemPrice < 10000 ) {
return this . randomUtil . getArrayValue ( [ 2 , 3 ] ) ;
}
return 2 ;
}
2023-10-15 12:46:33 +02:00
/ * *
* Select a number of items that have a colelctive value of the passed in parameter
* @param repeatableConfig Config
* @param roublesBudget Total value of items to return
* @returns Array of reward items that fit budget
* /
2023-11-16 22:42:06 +01:00
protected chooseRewardItemsWithinBudget (
repeatableConfig : IRepeatableQuestConfig ,
roublesBudget : number ,
traderId : string ,
) : ITemplateItem [ ]
2023-10-15 12:46:33 +02:00
{
// First filter for type and baseclass to avoid lookup in handbook for non-available items
2023-11-15 00:05:34 +01:00
const rewardableItemPool = this . getRewardableItems ( repeatableConfig , traderId ) ;
2023-10-15 12:46:33 +02:00
const minPrice = Math . min ( 25000 , 0.5 * roublesBudget ) ;
2023-11-15 00:05:34 +01:00
2023-11-16 22:42:06 +01:00
let rewardableItemPoolWithinBudget = rewardableItemPool . filter ( ( x ) = >
this . itemHelper . getItemPrice ( x [ 0 ] ) < roublesBudget && this . itemHelper . getItemPrice ( x [ 0 ] ) > minPrice
) . map ( ( x ) = > x [ 1 ] ) ;
2023-11-15 00:05:34 +01:00
if ( rewardableItemPoolWithinBudget . length === 0 )
2023-10-15 12:46:33 +02:00
{
2023-11-16 22:42:06 +01:00
this . logger . warning (
this . localisationService . getText ( "repeatable-no_reward_item_found_in_price_range" , {
minPrice : minPrice ,
roublesBudget : roublesBudget ,
} ) ,
) ;
2023-10-15 12:46:33 +02:00
// In case we don't find any items in the price range
2023-11-16 22:42:06 +01:00
rewardableItemPoolWithinBudget = rewardableItemPool . filter ( ( x ) = >
this . itemHelper . getItemPrice ( x [ 0 ] ) < roublesBudget
) . map ( ( x ) = > x [ 1 ] ) ;
2023-10-15 12:46:33 +02:00
}
2023-11-15 00:05:34 +01:00
return rewardableItemPoolWithinBudget ;
2023-10-15 12:46:33 +02:00
}
2023-10-11 18:43:57 +02:00
/ * *
* Helper to create a reward item structured as required by the client
*
2023-10-15 11:44:12 +02:00
* @param { string } tpl ItemId of the rewarded item
* @param { integer } value Amount of items to give
* @param { integer } index All rewards will be appended to a list , for unknown reasons the client wants the index
* @returns { object } Object of "Reward" - item - type
2023-10-11 18:43:57 +02:00
* /
2023-11-19 14:21:34 +01:00
protected generateRewardItem ( tpl : string , value : number , index : number , preset : Item [ ] = null ) : IReward
2023-10-11 18:43:57 +02:00
{
const id = this . objectId . generate ( ) ;
2023-11-16 22:42:06 +01:00
const rewardItem : IReward = { target : id , value : value , type : "Item" , index : index } ;
2023-10-11 18:43:57 +02:00
if ( preset )
{
2023-11-19 14:21:34 +01:00
const rootItem = preset . find ( x = > x . _tpl === tpl ) ;
rewardItem . target = rootItem . _id ; // Target property and root items id must match
2023-10-11 18:43:57 +02:00
rewardItem . items = this . ragfairServerHelper . reparentPresets ( rootItem , preset ) ;
}
else
{
2023-11-19 14:21:34 +01:00
const rootItem = { _id : id , _tpl : tpl , upd : { StackObjectsCount : value , SpawnedInSession : true } } ;
2023-10-11 18:43:57 +02:00
rewardItem . items = [ rootItem ] ;
}
return rewardItem ;
}
/ * *
2023-11-16 22:42:06 +01:00
* Picks rewardable items from items . json . This means they need to fit into the inventory and they shouldn ' t be keys ( debatable )
2023-10-15 11:44:12 +02:00
* @param repeatableQuestConfig Config file
* @returns List of rewardable items [ [ _tpl , itemTemplate ] , . . . ]
2023-10-11 18:43:57 +02:00
* /
2023-11-16 22:42:06 +01:00
protected getRewardableItems (
repeatableQuestConfig : IRepeatableQuestConfig ,
traderId : string ,
) : [ string , ITemplateItem ] [ ]
2023-10-11 18:43:57 +02:00
{
// check for specific baseclasses which don't make sense as reward item
// also check if the price is greater than 0; there are some items whose price can not be found
// those are not in the game yet (e.g. AGS grenade launcher)
return Object . entries ( this . databaseServer . getTables ( ) . templates . items ) . filter (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
( [ tpl , itemTemplate ] ) = >
{
// Base "Item" item has no parent, ignore it
if ( itemTemplate . _parent === "" )
{
return false ;
}
2023-11-16 22:42:06 +01:00
const traderWhitelist = repeatableQuestConfig . traderWhitelist . find ( ( x ) = > x . traderId === traderId ) ;
2023-11-15 00:05:34 +01:00
return this . isValidRewardItem ( tpl , repeatableQuestConfig , traderWhitelist ? . rewardBaseWhitelist ) ;
2023-11-16 22:42:06 +01:00
} ,
2023-10-11 18:43:57 +02:00
) ;
}
/ * *
* Checks if an id is a valid item . Valid meaning that it ' s an item that may be a reward
* or content of bot loot . Items that are tested as valid may be in a player backpack or stash .
* @param { string } tpl template id of item to check
2023-10-15 11:44:12 +02:00
* @returns True if item is valid reward
2023-10-11 18:43:57 +02:00
* /
2023-11-16 22:42:06 +01:00
protected isValidRewardItem (
tpl : string ,
repeatableQuestConfig : IRepeatableQuestConfig ,
itemBaseWhitelist : string [ ] ,
) : boolean
2023-10-11 18:43:57 +02:00
{
2023-11-15 00:05:34 +01:00
if ( ! this . itemHelper . isValidItem ( tpl ) )
2023-10-11 18:43:57 +02:00
{
2023-11-15 00:05:34 +01:00
return false ;
}
// Check global blacklist
if ( this . itemFilterService . isItemBlacklisted ( tpl ) )
{
return false ;
2023-10-11 18:43:57 +02:00
}
// Item is on repeatable or global blacklist
2023-11-16 22:42:06 +01:00
if ( repeatableQuestConfig . rewardBlacklist . includes ( tpl ) || this . itemFilterService . isItemBlacklisted ( tpl ) )
2023-10-11 18:43:57 +02:00
{
return false ;
}
// Item has blacklisted base type
2023-10-15 11:43:27 +02:00
if ( this . itemHelper . isOfBaseclasses ( tpl , [ . . . repeatableQuestConfig . rewardBaseTypeBlacklist ] ) )
2023-10-11 18:43:57 +02:00
{
2023-10-15 11:43:27 +02:00
return false ;
2023-10-11 18:43:57 +02:00
}
2023-11-15 00:05:34 +01:00
// Skip boss items
if ( this . itemFilterService . isBossItem ( tpl ) )
2023-10-15 11:28:52 +02:00
{
return false ;
}
2023-11-15 00:05:34 +01:00
// Trader has specific item base types they can give as rewards to player
if ( itemBaseWhitelist !== undefined )
{
if ( ! this . itemHelper . isOfBaseclasses ( tpl , [ . . . itemBaseWhitelist ] ) )
{
return false ;
}
}
2023-10-11 18:43:57 +02:00
2023-11-15 00:05:34 +01:00
return true ;
2023-10-11 18:43:57 +02:00
}
/ * *
* Generates the base object of quest type format given as templates in assets / database / templates / repeatableQuests . json
* The templates include Elimination , Completion and Extraction quest types
*
2023-10-15 11:44:12 +02:00
* @param { string } type Quest type : "Elimination" , "Completion" or "Extraction"
* @param { string } traderId Trader from which the quest will be provided
2023-11-16 22:42:06 +01:00
* @param { string } side Scav daily or pmc daily / weekly quest
2023-10-15 11:44:12 +02:00
* @returns { object } Object which contains the base elements for repeatable quests of the requests type
2023-10-11 18:43:57 +02:00
* ( needs to be filled with reward and conditions by called to make a valid quest )
* /
// @Incomplete: define Type for "type".
protected generateRepeatableTemplate ( type : string , traderId : string , side : string ) : IRepeatableQuest
{
2023-11-16 22:42:06 +01:00
const quest = this . jsonUtil . clone < IRepeatableQuest > (
this . databaseServer . getTables ( ) . templates . repeatableQuests . templates [ type ] ,
) ;
2023-10-11 18:43:57 +02:00
quest . _id = this . objectId . generate ( ) ;
quest . traderId = traderId ;
/ * i n l o c a l e , t h e s e i d c o r r e s p o n d t o t h e t e x t o f q u e s t s
template ids - pmc : Elimination = 616052 ea3054fc0e2c24ce6e / Completion = 61604635 c725987e815b1a46 / Exploration = 616041 eb031af660100c9967
2023-11-16 22:42:06 +01:00
template ids - scav : Elimination = 62825 ef60e88d037dc1eb428 / Completion = 628 f588ebb558574b2260fe5 / Exploration = 62825 ef60e88d037dc1eb42c
2023-10-11 18:43:57 +02:00
* /
// Get template id from config based on side and type of quest
quest . templateId = this . questConfig . questTemplateIds [ side . toLowerCase ( ) ] [ type . toLowerCase ( ) ] ;
2023-11-16 22:42:06 +01:00
quest . name = quest . name . replace ( "{traderId}" , traderId ) . replace ( "{templateId}" , quest . templateId ) ;
quest . note = quest . note . replace ( "{traderId}" , traderId ) . replace ( "{templateId}" , quest . templateId ) ;
quest . description = quest . description . replace ( "{traderId}" , traderId ) . replace ( "{templateId}" , quest . templateId ) ;
quest . successMessageText = quest . successMessageText . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
quest . failMessageText = quest . failMessageText . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
quest . startedMessageText = quest . startedMessageText . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
quest . changeQuestMessageText = quest . changeQuestMessageText . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
quest . acceptPlayerMessage = quest . acceptPlayerMessage . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
quest . declinePlayerMessage = quest . declinePlayerMessage . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
quest . completePlayerMessage = quest . completePlayerMessage . replace ( "{traderId}" , traderId ) . replace (
"{templateId}" ,
quest . templateId ,
) ;
2023-10-11 18:43:57 +02:00
return quest ;
}
2023-11-16 22:42:06 +01:00
}