2024-08-26 13:41:59 +01:00
import { HideoutHelper } from "@spt/helpers/HideoutHelper" ;
import { InventoryHelper } from "@spt/helpers/InventoryHelper" ;
import { ItemHelper } from "@spt/helpers/ItemHelper" ;
import { PresetHelper } from "@spt/helpers/PresetHelper" ;
2024-08-31 14:14:23 +01:00
import { ProfileHelper } from "@spt/helpers/ProfileHelper" ;
2024-12-06 17:15:06 +00:00
import { QuestHelper } from "@spt/helpers/QuestHelper" ;
2024-08-26 13:41:59 +01:00
import { IPmcData } from "@spt/models/eft/common/IPmcData" ;
2024-09-24 11:26:45 +01:00
import { IBotHideoutArea } from "@spt/models/eft/common/tables/IBotBase" ;
2024-09-24 12:47:29 +01:00
import { IItem } from "@spt/models/eft/common/tables/IItem" ;
2024-08-26 13:41:59 +01:00
import { IStageRequirement } from "@spt/models/eft/hideout/IHideoutArea" ;
import { IHideoutCircleOfCultistProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutCircleOfCultistProductionStartRequestData" ;
2024-12-06 17:15:06 +00:00
import { IRequirement , IRequirementBase } from "@spt/models/eft/hideout/IHideoutProduction" ;
2024-08-26 13:41:59 +01:00
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse" ;
2024-12-06 17:15:06 +00:00
import { IAcceptedCultistReward } from "@spt/models/eft/profile/ISptProfile" ;
2024-08-26 13:41:59 +01:00
import { BaseClasses } from "@spt/models/enums/BaseClasses" ;
import { ConfigTypes } from "@spt/models/enums/ConfigTypes" ;
import { HideoutAreas } from "@spt/models/enums/HideoutAreas" ;
import { ItemTpl } from "@spt/models/enums/ItemTpl" ;
2024-12-06 17:15:06 +00:00
import { QuestStatus } from "@spt/models/enums/QuestStatus" ;
2024-08-31 14:14:23 +01:00
import { SkillTypes } from "@spt/models/enums/SkillTypes" ;
2024-12-06 17:15:06 +00:00
import {
ICraftTimeThreshhold ,
ICultistCircleSettings ,
IDirectRewardSettings ,
IHideoutConfig ,
} from "@spt/models/spt/config/IHideoutConfig" ;
2024-12-07 09:53:21 +00:00
import { IHideout } from "@spt/models/spt/hideout/IHideout" ;
2024-08-26 13:41:59 +01:00
import { ILogger } from "@spt/models/spt/utils/ILogger" ;
import { EventOutputHolder } from "@spt/routers/EventOutputHolder" ;
import { ConfigServer } from "@spt/servers/ConfigServer" ;
2024-09-03 09:58:13 +01:00
import { DatabaseService } from "@spt/services/DatabaseService" ;
import { ItemFilterService } from "@spt/services/ItemFilterService" ;
2024-09-03 10:19:24 +01:00
import { SeasonalEventService } from "@spt/services/SeasonalEventService" ;
2024-08-26 13:41:59 +01:00
import { HashUtil } from "@spt/utils/HashUtil" ;
import { RandomUtil } from "@spt/utils/RandomUtil" ;
2024-09-13 10:42:13 +01:00
import { TimeUtil } from "@spt/utils/TimeUtil" ;
2024-08-26 13:41:59 +01:00
import { ICloner } from "@spt/utils/cloners/ICloner" ;
import { inject , injectable } from "tsyringe" ;
@injectable ( )
export class CircleOfCultistService {
protected static circleOfCultistSlotId = "CircleOfCultistsGrid1" ;
protected hideoutConfig : IHideoutConfig ;
constructor (
@inject ( "PrimaryLogger" ) protected logger : ILogger ,
2024-09-13 10:42:13 +01:00
@inject ( "TimeUtil" ) protected timeUtil : TimeUtil ,
2024-08-26 13:41:59 +01:00
@inject ( "PrimaryCloner" ) protected cloner : ICloner ,
@inject ( "EventOutputHolder" ) protected eventOutputHolder : EventOutputHolder ,
@inject ( "RandomUtil" ) protected randomUtil : RandomUtil ,
@inject ( "HashUtil" ) protected hashUtil : HashUtil ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
@inject ( "PresetHelper" ) protected presetHelper : PresetHelper ,
2024-08-31 14:14:23 +01:00
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
2024-08-26 13:41:59 +01:00
@inject ( "InventoryHelper" ) protected inventoryHelper : InventoryHelper ,
@inject ( "HideoutHelper" ) protected hideoutHelper : HideoutHelper ,
2024-12-06 17:15:06 +00:00
@inject ( "QuestHelper" ) protected questHelper : QuestHelper ,
2024-08-26 13:41:59 +01:00
@inject ( "DatabaseService" ) protected databaseService : DatabaseService ,
2024-09-03 09:58:13 +01:00
@inject ( "ItemFilterService" ) protected itemFilterService : ItemFilterService ,
2024-09-03 10:19:24 +01:00
@inject ( "SeasonalEventService" ) protected seasonalEventService : SeasonalEventService ,
2024-08-26 13:41:59 +01:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
) {
this . hideoutConfig = this . configServer . getConfig ( ConfigTypes . HIDEOUT ) ;
}
/ * *
* Start a sacrifice event
* Generate rewards
* Delete sacrificed items
* @param sessionId Session id
* @param pmcData Player profile doing sacrifice
* @param request Client request
* @returns IItemEventRouterResponse
* /
public startSacrifice (
sessionId : string ,
pmcData : IPmcData ,
request : IHideoutCircleOfCultistProductionStartRequestData ,
) : IItemEventRouterResponse {
const cultistCircleStashId = pmcData . Inventory . hideoutAreaStashes [ HideoutAreas . CIRCLE_OF_CULTISTS ] ;
2024-12-06 17:15:06 +00:00
// `cultistRecipes` just has single recipeId
2024-08-26 13:41:59 +01:00
const cultistCraftData = this . databaseService . getHideout ( ) . production . cultistRecipes [ 0 ] ;
2024-09-24 12:47:29 +01:00
const sacrificedItems : IItem [ ] = this . getSacrificedItems ( pmcData ) ;
2024-08-26 13:41:59 +01:00
const sacrificedItemCostRoubles = sacrificedItems . reduce (
( sum , curr ) = > sum + ( this . itemHelper . getItemPrice ( curr . _tpl ) ? ? 0 ) ,
0 ,
) ;
2024-12-06 17:15:06 +00:00
const rewardAmountMultiplier = this . getRewardAmountMultipler ( pmcData , this . hideoutConfig . cultistCircle ) ;
2024-08-31 14:14:23 +01:00
// Get the rouble amount we generate rewards with from cost of sacrified items * above multipler
2024-12-06 17:15:06 +00:00
const rewardAmountRoubles = Math . round ( sacrificedItemCostRoubles * rewardAmountMultiplier ) ;
2024-08-26 13:41:59 +01:00
2024-12-06 17:15:06 +00:00
// Check if it matches any direct swap recipes
const directRewardsCache = this . generateSacrificedItemsCache ( this . hideoutConfig . cultistCircle . directRewards ) ;
const directRewardSettings = this . checkForDirectReward ( sessionId , sacrificedItems , directRewardsCache ) ;
const hasDirectReward = directRewardSettings ? . reward . length > 0 ;
// Get craft time and bonus status
const craftingInfo = this . getCircleCraftingInfo (
rewardAmountRoubles ,
this . hideoutConfig . cultistCircle ,
directRewardSettings ,
) ;
2024-09-13 11:27:42 +01:00
2024-08-26 13:41:59 +01:00
// Create production in pmc profile
2024-09-13 10:42:13 +01:00
this . registerCircleOfCultistProduction (
sessionId ,
pmcData ,
cultistCraftData . _id ,
sacrificedItems ,
2024-12-06 17:15:06 +00:00
craftingInfo . time ,
2024-09-13 10:42:13 +01:00
) ;
2024-08-26 13:41:59 +01:00
const output = this . eventOutputHolder . getOutput ( sessionId ) ;
2024-12-06 17:15:06 +00:00
// Remove sacrificed items from circle inventory
2024-08-26 13:41:59 +01:00
for ( const item of sacrificedItems ) {
if ( item . slotId === CircleOfCultistService . circleOfCultistSlotId ) {
this . inventoryHelper . removeItem ( pmcData , item . _id , sessionId , output ) ;
}
}
2024-12-06 17:15:06 +00:00
const rewards = hasDirectReward
? this . getDirectRewards ( sessionId , directRewardSettings , cultistCircleStashId )
: this . getRewardsWithinBudget (
this . getCultistCircleRewardPool ( sessionId , pmcData , craftingInfo , this . hideoutConfig . cultistCircle ) ,
rewardAmountRoubles ,
cultistCircleStashId ,
this . hideoutConfig . cultistCircle ,
) ;
2024-08-26 13:41:59 +01:00
// Get the container grid for cultist stash area
const cultistStashDbItem = this . itemHelper . getItem ( ItemTpl . HIDEOUTAREACONTAINER_CIRCLEOFCULTISTS_STASH_1 ) ;
2024-12-06 17:15:06 +00:00
// Ensure rewards fit into container
2024-08-26 13:41:59 +01:00
const containerGrid = this . inventoryHelper . getContainerSlotMap ( cultistStashDbItem [ 1 ] . _id ) ;
const canAddToContainer = this . inventoryHelper . canPlaceItemsInContainer (
this . cloner . clone ( containerGrid ) , // MUST clone grid before passing in as function modifies grid
rewards ,
) ;
if ( canAddToContainer ) {
for ( const itemToAdd of rewards ) {
this . inventoryHelper . placeItemInContainer (
containerGrid ,
itemToAdd ,
cultistCircleStashId ,
CircleOfCultistService . circleOfCultistSlotId ,
) ;
// Add item + mods to output and profile inventory
output . profileChanges [ sessionId ] . items . new . push ( . . . itemToAdd ) ;
pmcData . Inventory . items . push ( . . . itemToAdd ) ;
}
} else {
this . logger . error (
` Unable to fit all: ${ rewards . length } reward items into sacrifice grid, nothing will be returned ` ,
) ;
}
return output ;
}
2024-12-06 17:15:06 +00:00
/ * *
* Create a map of the possible direct rewards , keyed by the items needed to be sacrificed
* @param directRewards Direct rewards array from hideout config
* @returns Map
* /
protected generateSacrificedItemsCache ( directRewards : IDirectRewardSettings [ ] ) : Map < string , IDirectRewardSettings > {
const result = new Map < string , IDirectRewardSettings > ( ) ;
for ( const rewardSettings of directRewards ) {
const key = this . hashUtil . generateMd5ForData ( rewardSettings . requiredItems . sort ( ) . join ( "," ) ) ;
result . set ( key , rewardSettings ) ;
}
return result ;
}
/ * *
* Get the reward amount multiple value based on players hideout management skill + configs rewardPriceMultiplerMinMax values
* @param pmcData Player profile
* @param cultistCircleSettings Circle config settings
* @returns Reward Amount Multipler
* /
protected getRewardAmountMultipler ( pmcData : IPmcData , cultistCircleSettings : ICultistCircleSettings ) : number {
// Get a randomised value to multiply the sacrificed rouble cost by
let rewardAmountMultiplier = this . randomUtil . getFloat (
cultistCircleSettings . rewardPriceMultiplerMinMax . min ,
cultistCircleSettings . rewardPriceMultiplerMinMax . max ,
) ;
// Adjust value generated by the players hideout management skill
const hideoutManagementSkill = this . profileHelper . getSkillFromProfile ( pmcData , SkillTypes . HIDEOUT_MANAGEMENT ) ;
if ( hideoutManagementSkill ) {
rewardAmountMultiplier *= 1 + hideoutManagementSkill . Progress / 10000 ; // 5100 becomes 0.51, add 1 to it, 1.51, multiply the bonus by it (e.g. 1.2 x 1.51)
}
return rewardAmountMultiplier ;
}
2024-09-13 11:27:42 +01:00
/ * *
* Register production inside player profile
* @param sessionId Session id
* @param pmcData Player profile
* @param recipeId Recipe id
* @param sacrificedItems Items player sacrificed
2024-12-06 17:15:06 +00:00
* @param craftingTime How long the ritual should take
2024-09-13 11:27:42 +01:00
* /
2024-09-13 10:42:13 +01:00
protected registerCircleOfCultistProduction (
sessionId : string ,
pmcData : IPmcData ,
recipeId : string ,
2024-09-24 12:47:29 +01:00
sacrificedItems : IItem [ ] ,
2024-12-06 17:15:06 +00:00
craftingTime : number ,
2024-09-13 10:42:13 +01:00
) : void {
2024-09-13 11:27:42 +01:00
// Create circle production/craft object to add to player profile
2024-12-06 17:15:06 +00:00
const cultistProduction = this . hideoutHelper . initProduction ( recipeId , craftingTime , false ) ;
2024-09-13 11:27:42 +01:00
2024-10-15 12:48:25 +01:00
// Flag as cultist circle for code to pick up later
cultistProduction . sptIsCultistCircle = true ;
2024-09-13 11:27:42 +01:00
// Add items player sacrificed
2024-09-13 10:42:13 +01:00
cultistProduction . GivenItemsInStart = sacrificedItems ;
2024-09-13 11:27:42 +01:00
// Add circle production to profile keyed to recipe id
2024-09-13 10:42:13 +01:00
pmcData . Hideout . Production [ recipeId ] = cultistProduction ;
}
/ * *
* Get the circle craft time as seconds , value is based on reward item value
2024-12-06 17:15:06 +00:00
* And get the bonus status to determine what tier of reward is given
2024-09-13 10:42:13 +01:00
* @param rewardAmountRoubles Value of rewards in roubles
2024-12-06 17:15:06 +00:00
* @param circleConfig Circle config values
* @param directRewardSettings OPTIONAL - Values related to direct reward being given
* @returns craft time + type of reward + reward details
2024-09-13 10:42:13 +01:00
* /
2024-12-06 17:15:06 +00:00
protected getCircleCraftingInfo (
2024-09-13 11:27:42 +01:00
rewardAmountRoubles : number ,
2024-12-06 17:15:06 +00:00
circleConfig : ICultistCircleSettings ,
2024-10-19 12:43:38 +01:00
directRewardSettings? : IDirectRewardSettings ,
2024-12-06 17:15:06 +00:00
) : ICraftDetails {
const result = {
time : - 1 ,
rewardType : CircleRewardType.RANDOM ,
rewardAmountRoubles : rewardAmountRoubles ,
rewardDetails : null ,
} ;
// Direct reward edge case
if ( directRewardSettings ) {
result . time = directRewardSettings . craftTimeSeconds ;
return result ;
2024-09-13 10:42:13 +01:00
}
2024-12-06 17:15:06 +00:00
// Get a threshold where sacrificed amount is between thresholds min and max
const matchingThreshold = this . getMatchingThreshold ( circleConfig . craftTimeThreshholds , rewardAmountRoubles ) ;
if (
rewardAmountRoubles >= circleConfig . hideoutCraftSacrificeThresholdRub &&
Math . random ( ) <= circleConfig . bonusChanceMultiplier
) {
// Sacrifice amount is enough + passed 25% check to get hideout/task rewards
result . time =
circleConfig . craftTimeOverride !== - 1
? circleConfig . craftTimeOverride
: circleConfig . hideoutTaskRewardTimeSeconds ;
result . rewardType = CircleRewardType . HIDEOUT_TASK ;
return result ;
2024-09-13 11:27:42 +01:00
}
2024-12-06 17:15:06 +00:00
// Edge case, check if override exists, Otherwise use matching threshold craft time
result . time =
circleConfig . craftTimeOverride !== - 1 ? circleConfig.craftTimeOverride : matchingThreshold.craftTimeSeconds ;
result . rewardDetails = matchingThreshold ;
return result ;
}
protected getMatchingThreshold (
thresholds : ICraftTimeThreshhold [ ] ,
rewardAmountRoubles : number ,
) : ICraftTimeThreshhold {
2024-09-13 10:42:13 +01:00
const matchingThreshold = thresholds . find (
( craftThreshold ) = > craftThreshold . min <= rewardAmountRoubles && craftThreshold . max >= rewardAmountRoubles ,
) ;
2024-12-06 17:15:06 +00:00
// No matching threshold, make one
2024-09-13 10:42:13 +01:00
if ( ! matchingThreshold ) {
2024-12-06 17:15:06 +00:00
// None found, use a defalt
this . logger . warning ( "Unable to find a matching cultist circle threshold, using fallback of 12 hours" ) ;
// Use first threshold value (cheapest) from parameter array, otherwise use 12 hours
const firstThreshold = thresholds [ 0 ] ;
const craftTime = firstThreshold ? . craftTimeSeconds
? firstThreshold . craftTimeSeconds
: this . timeUtil . getHoursAsSeconds ( 12 ) ;
return { min : firstThreshold?.min ? ? 1 , max : firstThreshold?.max ? ? 34999 , craftTimeSeconds : craftTime } ;
2024-09-13 10:42:13 +01:00
}
2024-12-06 17:15:06 +00:00
return matchingThreshold ;
2024-09-13 10:42:13 +01:00
}
2024-08-26 13:41:59 +01:00
/ * *
* Get the items player sacrificed in circle
* @param pmcData Player profile
* @returns Array of its from player inventory
* /
2024-09-24 12:47:29 +01:00
protected getSacrificedItems ( pmcData : IPmcData ) : IItem [ ] {
2024-08-26 13:41:59 +01:00
// Get root items that are in the cultist sacrifice window
const inventoryRootItemsInCultistGrid = pmcData . Inventory . items . filter (
( item ) = > item . slotId === CircleOfCultistService . circleOfCultistSlotId ,
) ;
// Get rootitem + its children
2024-09-24 12:47:29 +01:00
const sacrificedItems : IItem [ ] = [ ] ;
2024-08-26 13:41:59 +01:00
for ( const rootItem of inventoryRootItemsInCultistGrid ) {
const rootItemWithChildren = this . itemHelper . findAndReturnChildrenAsItems (
pmcData . Inventory . items ,
rootItem . _id ,
) ;
sacrificedItems . push ( . . . rootItemWithChildren ) ;
}
return sacrificedItems ;
}
/ * *
* Given a pool of items + rouble budget , pick items until the budget is reached
* @param rewardItemTplPool Items that can be picekd
* @param rewardBudget Rouble budget to reach
* @param cultistCircleStashId Id of stash item
2024-09-01 09:56:00 +01:00
* @returns Array of item arrays
2024-08-26 13:41:59 +01:00
* /
protected getRewardsWithinBudget (
rewardItemTplPool : string [ ] ,
rewardBudget : number ,
cultistCircleStashId : string ,
2024-12-06 17:15:06 +00:00
circleConfig : ICultistCircleSettings ,
2024-09-24 12:47:29 +01:00
) : IItem [ ] [ ] {
2024-08-26 13:41:59 +01:00
// Prep rewards array (reward can be item with children, hence array of arrays)
2024-09-24 12:47:29 +01:00
const rewards : IItem [ ] [ ] = [ ] ;
2024-08-26 13:41:59 +01:00
// Pick random rewards until we have exhausted the sacrificed items budget
2024-09-13 09:43:39 +01:00
let totalRewardCost = 0 ;
let rewardItemCount = 0 ;
2024-08-26 13:41:59 +01:00
let failedAttempts = 0 ;
while (
2024-09-13 09:43:39 +01:00
totalRewardCost < rewardBudget &&
2024-08-26 13:41:59 +01:00
rewardItemTplPool . length > 0 &&
2024-12-06 17:15:06 +00:00
rewardItemCount < circleConfig . maxRewardItemCount
2024-08-26 13:41:59 +01:00
) {
2024-12-06 17:15:06 +00:00
if ( failedAttempts > circleConfig . maxAttemptsToPickRewardsWithinBudget ) {
2024-08-26 13:41:59 +01:00
this . logger . warning ( ` Exiting reward generation after ${ failedAttempts } failed attempts ` ) ;
break ;
}
// Choose a random tpl from pool
const randomItemTplFromPool = this . randomUtil . getArrayValue ( rewardItemTplPool ) ;
// Is weapon/armor, handle differently
if (
this . itemHelper . armorItemHasRemovableOrSoftInsertSlots ( randomItemTplFromPool ) ||
this . itemHelper . isOfBaseclass ( randomItemTplFromPool , BaseClasses . WEAPON )
) {
const defaultPreset = this . presetHelper . getDefaultPreset ( randomItemTplFromPool ) ;
if ( ! defaultPreset ) {
this . logger . warning ( ` Reward tpl: ${ randomItemTplFromPool } lacks a default preset, skipping reward ` ) ;
failedAttempts ++ ;
continue ;
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
2024-09-24 12:47:29 +01:00
const presetAndMods : IItem [ ] = this . itemHelper . replaceIDs ( defaultPreset . _items ) ;
2024-08-26 13:41:59 +01:00
this . itemHelper . remapRootItemId ( presetAndMods ) ;
2024-09-13 09:43:39 +01:00
rewardItemCount ++ ;
totalRewardCost += this . itemHelper . getItemPrice ( randomItemTplFromPool ) ;
2024-08-26 13:41:59 +01:00
rewards . push ( presetAndMods ) ;
2024-09-01 09:52:50 +01:00
continue ;
2024-08-26 13:41:59 +01:00
}
2024-09-13 09:43:39 +01:00
// Some items can have variable stack size, e.g. ammo / currency
const stackSize = this . getRewardStackSize (
randomItemTplFromPool ,
rewardBudget / ( rewardItemCount === 0 ? 1 : rewardItemCount ) , // Remaining rouble budget
) ;
2024-08-26 13:41:59 +01:00
// Not a weapon/armor, standard single item
2024-09-24 12:47:29 +01:00
const rewardItem : IItem = {
2024-08-26 13:41:59 +01:00
_id : this.hashUtil.generate ( ) ,
_tpl : randomItemTplFromPool ,
parentId : cultistCircleStashId ,
slotId : CircleOfCultistService.circleOfCultistSlotId ,
upd : {
StackObjectsCount : stackSize ,
SpawnedInSession : true ,
} ,
} ;
2024-09-13 09:40:29 +01:00
// Increment price of rewards to give to player + add to reward array
rewardItemCount ++ ;
const singleItemPrice = this . itemHelper . getItemPrice ( randomItemTplFromPool ) ;
const itemPrice = singleItemPrice * stackSize ;
totalRewardCost += itemPrice ;
2024-08-26 13:41:59 +01:00
rewards . push ( [ rewardItem ] ) ;
}
return rewards ;
}
2024-09-01 09:56:00 +01:00
/ * *
2024-12-06 17:15:06 +00:00
* Get direct rewards
* @param sessionId sessionId
* @param directReward Items sacrificed
2024-09-01 09:56:00 +01:00
* @param cultistCircleStashId Id of stash item
2024-12-06 17:15:06 +00:00
* @returns The reward object
2024-09-01 09:56:00 +01:00
* /
2024-12-06 17:15:06 +00:00
protected getDirectRewards (
sessionId : string ,
directReward : IDirectRewardSettings ,
2024-09-24 12:47:29 +01:00
cultistCircleStashId : string ,
) : IItem [ ] [ ] {
2024-09-01 09:56:00 +01:00
// Prep rewards array (reward can be item with children, hence array of arrays)
2024-09-24 12:47:29 +01:00
const rewards : IItem [ ] [ ] = [ ] ;
2024-09-01 09:56:00 +01:00
2024-12-06 17:15:06 +00:00
// Handle special case of tagilla helmets - only one reward is allowed
if ( directReward . reward . includes ( ItemTpl . FACECOVER_TAGILLAS_WELDING_MASK_GORILLA ) ) {
directReward . reward = [ this . randomUtil . getArrayValue ( directReward . reward ) ] ;
}
2024-09-01 09:56:00 +01:00
2024-12-06 17:15:06 +00:00
// Loop because these can include multiple rewards
2024-12-07 09:43:41 +00:00
for ( const rewardTpl of directReward . reward ) {
// Is weapon/armor, handle differently
if (
this . itemHelper . armorItemHasRemovableOrSoftInsertSlots ( rewardTpl ) ||
this . itemHelper . isOfBaseclass ( rewardTpl , BaseClasses . WEAPON )
) {
const defaultPreset = this . presetHelper . getDefaultPreset ( rewardTpl ) ;
if ( ! defaultPreset ) {
this . logger . warning ( ` Reward tpl: ${ rewardTpl } lacks a default preset, skipping reward ` ) ;
continue ;
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
const presetAndMods : IItem [ ] = this . itemHelper . replaceIDs ( defaultPreset . _items ) ;
this . itemHelper . remapRootItemId ( presetAndMods ) ;
rewards . push ( presetAndMods ) ;
continue ;
}
// 'Normal' item, non-preset
const stackSize = this . getDirectRewardBaseTypeStackSize ( rewardTpl ) ;
2024-09-24 12:47:29 +01:00
const rewardItem : IItem = {
2024-09-01 09:56:00 +01:00
_id : this.hashUtil.generate ( ) ,
2024-12-07 09:43:41 +00:00
_tpl : rewardTpl ,
2024-09-01 09:56:00 +01:00
parentId : cultistCircleStashId ,
slotId : CircleOfCultistService.circleOfCultistSlotId ,
upd : {
StackObjectsCount : stackSize ,
SpawnedInSession : true ,
} ,
} ;
rewards . push ( [ rewardItem ] ) ;
}
2024-12-06 17:15:06 +00:00
// Direct reward is not repeatable, flag collected in profile
if ( ! directReward . repeatable ) {
this . flagDirectRewardAsAcceptedInProfile ( sessionId , directReward ) ;
}
2024-09-01 09:56:00 +01:00
return rewards ;
}
2024-12-06 17:15:06 +00:00
/ * *
* Check for direct rewards from what player sacrificed
* @param sessionId sessionId
* @param sacrificedItems Items sacrificed
* @returns Direct reward items to send to player
* /
protected checkForDirectReward (
sessionId : string ,
sacrificedItems : IItem [ ] ,
directRewardsCache : Map < string , IDirectRewardSettings > ,
) : IDirectRewardSettings {
// Get sacrificed tpls
const sacrificedItemTpls = sacrificedItems . map ( ( item ) = > item . _tpl ) ;
// Create md5 key of the items player sacrificed so we can compare against the direct reward cache
const sacrificedItemsKey = this . hashUtil . generateMd5ForData ( sacrificedItemTpls . sort ( ) . join ( "," ) ) ;
const matchingDirectReward = directRewardsCache . get ( sacrificedItemsKey ) ;
if ( ! matchingDirectReward ) {
// No direct reward
return null ;
}
const fullProfile = this . profileHelper . getFullProfile ( sessionId ) ;
const directRewardHash = this . getDirectRewardHashKey ( matchingDirectReward ) ;
2024-12-07 09:44:12 +00:00
if ( fullProfile . spt . cultistRewards ? . [ directRewardHash ] ) {
2024-12-06 17:15:06 +00:00
// Player has already received this direct reward
return null ;
}
return matchingDirectReward ;
}
/ * *
* Create an md5 key of the sacrificed + reward items
* @param directReward Direct reward to create key for
* @returns Key
* /
protected getDirectRewardHashKey ( directReward : IDirectRewardSettings ) : string {
// Key is sacrificed items separated by commas, a dash, then the rewards separated by commas
const key = ` { ${ directReward . requiredItems . sort ( ) . join ( "," ) } - ${ directReward . reward . sort ( ) . join ( "," ) } ` ;
return this . hashUtil . generateMd5ForData ( key ) ;
}
2024-09-13 11:27:42 +01:00
/ * *
* Explicit rewards have thier own stack sizes as they dont use a reward rouble pool
* @param rewardTpl Item being rewarded to get stack size of
* @returns stack size of item
* /
2024-12-06 17:15:06 +00:00
protected getDirectRewardBaseTypeStackSize ( rewardTpl : string ) : number {
2024-09-13 11:27:42 +01:00
const itemDetails = this . itemHelper . getItem ( rewardTpl ) ;
if ( ! itemDetails [ 0 ] ) {
this . logger . warning ( ` ${ rewardTpl } is not an item, setting stack size to 1 ` ) ;
return 1 ;
}
// Look for parent in dict
const settings = this . hideoutConfig . cultistCircle . directRewardStackSize [ itemDetails [ 1 ] . _parent ] ;
2024-09-13 09:58:07 +01:00
if ( ! settings ) {
return 1 ;
}
return this . randomUtil . getInt ( settings . min , settings . max ) ;
}
2024-12-06 17:15:06 +00:00
/ * *
* Add a record to the players profile to signal they have accepted a non - repeatable direct reward
* @param sessionId Session id
* @param directReward Reward sent to player
* /
protected flagDirectRewardAsAcceptedInProfile ( sessionId : string , directReward : IDirectRewardSettings ) {
const fullProfile = this . profileHelper . getFullProfile ( sessionId ) ;
const dataToStoreInProfile : IAcceptedCultistReward = {
timestamp : this.timeUtil.getTimestamp ( ) ,
sacrificeItems : directReward.requiredItems ,
rewardItems : directReward.reward ,
} ;
2024-12-07 09:44:12 +00:00
fullProfile . spt . cultistRewards [ this . getDirectRewardHashKey ( directReward ) ] = dataToStoreInProfile ;
2024-12-06 17:15:06 +00:00
}
2024-08-26 13:41:59 +01:00
/ * *
* Get the size of a reward items stack
* 1 for everything except ammo , ammo can be between min stack and max stack
* @param itemTpl Item chosen
2024-09-13 09:43:39 +01:00
* @param rewardPoolRemaining Rouble amount of pool remaining to fill
2024-08-26 13:41:59 +01:00
* @returns Size of stack
* /
2024-09-13 09:43:39 +01:00
protected getRewardStackSize ( itemTpl : string , rewardPoolRemaining : number ) {
2024-08-26 13:41:59 +01:00
if ( this . itemHelper . isOfBaseclass ( itemTpl , BaseClasses . AMMO ) ) {
const ammoTemplate = this . itemHelper . getItem ( itemTpl ) [ 1 ] ;
return this . itemHelper . getRandomisedAmmoStackSize ( ammoTemplate ) ;
}
2024-09-13 09:43:39 +01:00
if ( this . itemHelper . isOfBaseclass ( itemTpl , BaseClasses . MONEY ) ) {
// Get currency-specific values from config
const settings = this . hideoutConfig . cultistCircle . currencyRewards [ itemTpl ] ;
// What % of the pool remaining should be rewarded as chosen currency
const percentOfPoolToUse = this . randomUtil . getInt ( settings . min , settings . max ) ;
// Rouble amount of pool we want to reward as currency
const roubleAmountToFill = this . randomUtil . getPercentOfValue ( percentOfPoolToUse , rewardPoolRemaining ) ;
// Convert currency to roubles
const currencyPriceAsRouble = this . itemHelper . getItemPrice ( itemTpl ) ;
// How many items can we fit into chosen pool
const itemCountToReward = Math . round ( roubleAmountToFill / currencyPriceAsRouble ) ;
return itemCountToReward ? ? 1 ;
}
2024-08-26 13:41:59 +01:00
return 1 ;
}
/ * *
* Get a pool of tpl IDs of items the player needs to complete hideout crafts / upgrade areas
* @param sessionId Session id
2024-09-03 09:58:13 +01:00
* @param pmcData Profile of player who will be getting the rewards
2024-12-06 17:15:06 +00:00
* @param rewardType Do we return bonus items ( hideout / task items )
* @param cultistCircleConfig Circle config
2024-08-26 13:41:59 +01:00
* @returns Array of tpls
* /
2024-12-06 17:15:06 +00:00
protected getCultistCircleRewardPool (
sessionId : string ,
pmcData : IPmcData ,
craftingInfo : ICraftDetails ,
cultistCircleConfig : ICultistCircleSettings ,
) : string [ ] {
2024-08-26 13:41:59 +01:00
const rewardPool = new Set < string > ( ) ;
2024-09-03 09:58:13 +01:00
const hideoutDbData = this . databaseService . getHideout ( ) ;
2024-12-06 17:15:06 +00:00
// Merge reward item blacklist and boss item blacklist with cultist circle blacklist from config
2024-09-03 09:58:13 +01:00
const itemRewardBlacklist = [
2024-11-13 11:19:28 +00:00
. . . this . seasonalEventService . getInactiveSeasonalEventItems ( ) ,
2024-09-03 09:58:13 +01:00
. . . this . itemFilterService . getItemRewardBlacklist ( ) ,
. . . cultistCircleConfig . rewardItemBlacklist ,
] ;
2024-08-26 13:41:59 +01:00
2024-12-06 17:15:06 +00:00
// Hideout and task rewards are ONLY if the bonus is active
switch ( craftingInfo . rewardType ) {
case CircleRewardType . RANDOM : {
// Does reward pass the high value threshold
const isHighValueReward = craftingInfo . rewardAmountRoubles >= cultistCircleConfig . highValueThresholdRub ;
2024-12-07 10:04:12 +00:00
this . generateRandomisedItemsAndAddToRewardPool ( rewardPool , itemRewardBlacklist , isHighValueReward ) ;
2024-12-07 09:49:49 +00:00
2024-12-06 17:15:06 +00:00
break ;
2024-08-26 13:41:59 +01:00
}
2024-12-06 17:15:06 +00:00
case CircleRewardType . HIDEOUT_TASK : {
// Hideout/Task loot
2024-12-07 09:53:21 +00:00
this . addHideoutUpgradeRequirementsToRewardPool ( hideoutDbData , pmcData , itemRewardBlacklist , rewardPool ) ;
this . addTaskItemRequirementsToRewardPool ( pmcData , itemRewardBlacklist , rewardPool ) ;
2024-09-03 09:58:13 +01:00
2024-12-06 17:15:06 +00:00
// If we have no tasks or hideout stuff left or need more loot to fill it out, default to high value
if ( rewardPool . size < cultistCircleConfig . maxRewardItemCount + 2 ) {
2024-12-07 10:04:12 +00:00
this . generateRandomisedItemsAndAddToRewardPool ( rewardPool , itemRewardBlacklist , true ) ;
2024-08-26 13:41:59 +01:00
}
2024-12-06 17:15:06 +00:00
break ;
2024-08-26 13:41:59 +01:00
}
}
2024-09-03 09:58:13 +01:00
// Add custom rewards from config
if ( cultistCircleConfig . additionalRewardItemPool . length > 0 ) {
for ( const additionalReward of cultistCircleConfig . additionalRewardItemPool ) {
2024-09-06 15:39:52 +01:00
if ( itemRewardBlacklist . includes ( additionalReward ) ) {
2024-09-03 09:58:13 +01:00
continue ;
}
// Add tpl to reward pool
rewardPool . add ( additionalReward ) ;
}
}
2024-08-26 13:41:59 +01:00
return Array . from ( rewardPool ) ;
}
2024-12-07 09:53:21 +00:00
protected addTaskItemRequirementsToRewardPool (
pmcData : IPmcData ,
itemRewardBlacklist : string [ ] ,
rewardPool : Set < string > ,
) {
const activeTasks = pmcData . Quests . filter ( ( quest ) = > quest . status === QuestStatus . Started ) ;
for ( const task of activeTasks ) {
const questData = this . questHelper . getQuestFromDb ( task . qid , pmcData ) ;
const handoverConditions = questData . conditions . AvailableForFinish . filter (
( condition ) = > condition . conditionType === "HandoverItem" ,
) ;
for ( const condition of handoverConditions ) {
for ( const neededItem of condition . target ) {
if ( itemRewardBlacklist . includes ( neededItem ) || ! this . itemHelper . isValidItem ( neededItem ) ) {
continue ;
}
this . logger . debug ( ` Added Task Loot: ${ this . itemHelper . getItemName ( neededItem ) } ` ) ;
rewardPool . add ( neededItem ) ;
}
}
}
}
protected addHideoutUpgradeRequirementsToRewardPool (
hideoutDbData : IHideout ,
pmcData : IPmcData ,
itemRewardBlacklist : string [ ] ,
rewardPool : Set < string > ,
) {
const dbAreas = hideoutDbData . areas ;
for ( const profileArea of this . getPlayerAccessibleHideoutAreas ( pmcData . Hideout . Areas ) ) {
const currentStageLevel = profileArea . level ;
const areaType = profileArea . type ;
// Get next stage of area
const dbArea = dbAreas . find ( ( area ) = > area . type === areaType ) ;
const nextStageDbData = dbArea ? . stages [ currentStageLevel + 1 ] ;
if ( nextStageDbData ) {
// Next stage exists, gather up requirements and add to pool
const itemRequirements = this . getItemRequirements ( nextStageDbData . requirements ) ;
for ( const rewardToAdd of itemRequirements ) {
if (
itemRewardBlacklist . includes ( rewardToAdd . templateId ) ||
! this . itemHelper . isValidItem ( rewardToAdd . templateId )
) {
// Dont reward items sacrificed
continue ;
}
this . logger . debug ( ` Added Hideout Loot: ${ this . itemHelper . getItemName ( rewardToAdd . templateId ) } ` ) ;
rewardPool . add ( rewardToAdd . templateId ) ;
}
}
}
}
2024-09-03 10:19:24 +01:00
/ * *
* Get all active hideout areas
* @param areas Hideout areas to iterate over
* @returns Active area array
* /
2024-09-24 11:26:45 +01:00
protected getPlayerAccessibleHideoutAreas ( areas : IBotHideoutArea [ ] ) : IBotHideoutArea [ ] {
2024-09-03 10:19:24 +01:00
return areas . filter ( ( area ) = > {
if ( area . type === HideoutAreas . CHRISTMAS_TREE && ! this . seasonalEventService . christmasEventEnabled ( ) ) {
// Christmas tree area and not Christmas, skip
return false ;
}
return true ;
} ) ;
}
2024-09-13 11:27:42 +01:00
/ * *
2024-12-06 17:15:06 +00:00
* Get array of random reward items
* @param rewardPool Reward pool to add to
* @param itemRewardBlacklist Reward Blacklist
2024-12-07 10:04:12 +00:00
* @param itemsShouldBeHighValue Should these items meet the valuable threshold
2024-12-06 17:15:06 +00:00
* @returns rewardPool
2024-09-13 11:27:42 +01:00
* /
2024-12-07 10:04:12 +00:00
protected generateRandomisedItemsAndAddToRewardPool (
rewardPool : Set < string > ,
itemRewardBlacklist : string [ ] ,
itemsShouldBeHighValue : boolean ,
) : Set < string > {
2024-12-06 17:15:06 +00:00
const allItems = this . itemHelper . getItems ( ) ;
let currentItemCount = 0 ;
let attempts = 0 ;
// currentItemCount var will look for the correct number of items, attempts var will keep this from never stopping if the highValueThreshold is too high
while (
currentItemCount < this . hideoutConfig . cultistCircle . maxRewardItemCount + 2 &&
attempts < allItems . length
) {
attempts ++ ;
const randomItem = this . randomUtil . getArrayValue ( allItems ) ;
if (
itemRewardBlacklist . includes ( randomItem . _id ) ||
2024-12-07 10:04:12 +00:00
itemRewardBlacklist . includes ( randomItem . _parent ) ||
2024-12-06 17:15:06 +00:00
! this . itemHelper . isValidItem ( randomItem . _id )
) {
continue ;
}
// Valuable check
2024-12-07 10:04:12 +00:00
if ( itemsShouldBeHighValue ) {
2024-12-06 17:15:06 +00:00
const itemValue = this . itemHelper . getItemMaxPrice ( randomItem . _id ) ;
if ( itemValue < this . hideoutConfig . cultistCircle . highValueThresholdRub ) {
this . logger . debug ( ` Ignored due to value: ${ this . itemHelper . getItemName ( randomItem . _id ) } ` ) ;
continue ;
}
}
this . logger . debug ( ` Added: ${ this . itemHelper . getItemName ( randomItem . _id ) } ` ) ;
rewardPool . add ( randomItem . _id ) ;
currentItemCount ++ ;
}
2024-12-07 10:04:12 +00:00
2024-12-06 17:15:06 +00:00
return rewardPool ;
2024-09-03 10:19:24 +01:00
}
2024-08-26 13:41:59 +01:00
/ * *
2024-08-26 13:55:42 +01:00
* Iterate over passed in hideout requirements and return the Item
2024-08-26 13:41:59 +01:00
* @param requirements Requirements to iterate over
* @returns Array of item requirements
* /
2024-10-19 12:43:38 +01:00
protected getItemRequirements ( requirements : IRequirementBase [ ] ) : ( IStageRequirement | IRequirement ) [ ] {
2024-08-26 13:55:42 +01:00
return requirements . filter ( ( requirement ) = > requirement . type === "Item" ) ;
2024-08-26 13:41:59 +01:00
}
}
2024-12-06 17:15:06 +00:00
export enum CircleRewardType {
RANDOM = 0 ,
HIDEOUT_TASK = 1 ,
}
export interface ICraftDetails {
time : number ;
rewardType : CircleRewardType ;
rewardAmountRoubles : number ;
rewardDetails? : ICraftTimeThreshhold ;
}