diff --git a/project/src/controllers/HideoutController.ts b/project/src/controllers/HideoutController.ts index fcd2ad89..645fdf6d 100644 --- a/project/src/controllers/HideoutController.ts +++ b/project/src/controllers/HideoutController.ts @@ -10,13 +10,13 @@ import { HideoutArea, ITaskConditionCounter, Product, ScavCase } from "@spt/mode import { Item } from "@spt/models/eft/common/tables/IItem"; import { HideoutUpgradeCompleteRequestData } from "@spt/models/eft/hideout/HideoutUpgradeCompleteRequestData"; import { IHandleQTEEventRequestData } from "@spt/models/eft/hideout/IHandleQTEEventRequestData"; -import { IHideoutArea, IStageRequirement, Stage } from "@spt/models/eft/hideout/IHideoutArea"; +import { IHideoutArea, Stage } from "@spt/models/eft/hideout/IHideoutArea"; import { IHideoutCancelProductionRequestData } from "@spt/models/eft/hideout/IHideoutCancelProductionRequestData"; import { IHideoutCircleOfCultistProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutCircleOfCultistProductionStartRequestData"; import { IHideoutContinuousProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutContinuousProductionStartRequestData"; import { IHideoutDeleteProductionRequestData } from "@spt/models/eft/hideout/IHideoutDeleteProductionRequestData"; import { IHideoutImproveAreaRequestData } from "@spt/models/eft/hideout/IHideoutImproveAreaRequestData"; -import { IHideoutProduction, Requirement } from "@spt/models/eft/hideout/IHideoutProduction"; +import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction"; import { IHideoutPutItemInRequestData } from "@spt/models/eft/hideout/IHideoutPutItemInRequestData"; import { IHideoutScavCaseStartRequestData } from "@spt/models/eft/hideout/IHideoutScavCaseStartRequestData"; import { IHideoutSingleProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutSingleProductionStartRequestData"; @@ -30,7 +30,6 @@ import { IAddItemDirectRequest } from "@spt/models/eft/inventory/IAddItemDirectR import { IAddItemsDirectRequest } from "@spt/models/eft/inventory/IAddItemsDirectRequest"; import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse"; import { BackendErrorCodes } from "@spt/models/enums/BackendErrorCodes"; -import { BaseClasses } from "@spt/models/enums/BaseClasses"; import { BonusType } from "@spt/models/enums/BonusType"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { HideoutAreas } from "@spt/models/enums/HideoutAreas"; @@ -41,6 +40,7 @@ import { ILogger } from "@spt/models/spt/utils/ILogger"; import { EventOutputHolder } from "@spt/routers/EventOutputHolder"; import { ConfigServer } from "@spt/servers/ConfigServer"; import { SaveServer } from "@spt/servers/SaveServer"; +import { CircleOfCultistService } from "@spt/services/CircleOfCultistService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { FenceService } from "@spt/services/FenceService"; import { LocalisationService } from "@spt/services/LocalisationService"; @@ -57,7 +57,6 @@ import { inject, injectable } from "tsyringe"; export class HideoutController { /** Key used in TaskConditionCounters array */ protected static nameTaskConditionCountersCrafting = "CounterHoursCrafting"; - protected static circleOfCultistSlotId = "CircleOfCultistsGrid1"; protected hideoutConfig: IHideoutConfig; constructor( @@ -81,6 +80,7 @@ export class HideoutController { @inject("ProfileActivityService") protected profileActivityService: ProfileActivityService, @inject("ConfigServer") protected configServer: ConfigServer, @inject("FenceService") protected fenceService: FenceService, + @inject("CircleOfCultistService") protected circleOfCultistService: CircleOfCultistService, @inject("PrimaryCloner") protected cloner: ICloner, ) { this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT); @@ -1303,221 +1303,7 @@ export class HideoutController { pmcData: IPmcData, request: IHideoutCircleOfCultistProductionStartRequestData, ): IItemEventRouterResponse { - const cultistCircleStashId = pmcData.Inventory.hideoutAreaStashes[HideoutAreas.CIRCLE_OF_CULTISTS]; - - // Sparse, just has id - const cultistCraftData = this.databaseService.getHideout().production.cultistRecipes[0]; - const sacrificedItems: Item[] = this.getSacrificedItems(pmcData); - const sacrificedItemCostRoubles = sacrificedItems.reduce( - (sum, curr) => sum + (this.itemHelper.getItemPrice(curr._tpl) ?? 0), - 0, - ); - - // TODO - include hideout management skill to bonus - const rewardAmountmultiplier = this.randomUtil.getFloat(0.7, 1.4); - const rewardAmountRoubles = sacrificedItemCostRoubles * rewardAmountmultiplier; - - // Create production in pmc profile - this.hideoutHelper.registerCircleOfCultistProduction(sessionId, pmcData, cultistCraftData._id, sacrificedItems); - - const output = this.eventOutputHolder.getOutput(sessionId); - - // Remove sacrified items - for (const item of sacrificedItems) { - if (item.slotId === HideoutController.circleOfCultistSlotId) { - this.inventoryHelper.removeItem(pmcData, item._id, sessionId, output); - } - } - - const rewardItemPool = this.getCultistCircleRewardPool(sessionId, pmcData); - this.logger.warning(`Reward pool item count: ${rewardItemPool.length}`); - - const rewards = this.getRewardsWithinBudget(rewardItemPool, rewardAmountRoubles, cultistCircleStashId); - - // Get the container grid for cultist stash area - const cultistStashDbItem = this.itemHelper.getItem(ItemTpl.HIDEOUTAREACONTAINER_CIRCLEOFCULTISTS_STASH_1); - - // Ensure items fit into container - 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.logger.warning(`Placing reward: ${itemToAdd[0]._tpl} in circle grid`); - this.inventoryHelper.placeItemInContainer( - containerGrid, - itemToAdd, - cultistCircleStashId, - HideoutController.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; - } - - /** - * 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 - * @returns Array of items - */ - protected getRewardsWithinBudget( - rewardItemTplPool: string[], - rewardBudget: number, - cultistCircleStashId: string, - ): Item[][] { - // Prep rewards array (reward can be item with children, hence array of arrays) - const rewards: Item[][] = []; - - // Pick random rewards until we have exhausted the sacrificed items budget - let totalCost = 0; - let itemsRewardedCount = 0; - let failedAttempts = 0; - while (totalCost < rewardBudget && rewardItemTplPool.length > 0 && itemsRewardedCount < 5) { - if (failedAttempts > 5) { - 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 - const presetAndMods: Item[] = this.itemHelper.replaceIDs(defaultPreset._items); - - this.itemHelper.remapRootItemId(presetAndMods); - - rewards.push(presetAndMods); - } - - // Some items can have variable stack size, e.g. ammo - const stackSize = this.getRewardStackSize(randomItemTplFromPool); - - // Not a weapon/armor, standard single item - const rewardItem: Item = { - _id: this.hashUtil.generate(), - _tpl: randomItemTplFromPool, - parentId: cultistCircleStashId, - slotId: HideoutController.circleOfCultistSlotId, - upd: { - StackObjectsCount: stackSize, - SpawnedInSession: true, - }, - }; - - // Increment price of rewards to give to player and add to reward array - itemsRewardedCount++; - totalCost += this.itemHelper.getItemPrice(randomItemTplFromPool); - rewards.push([rewardItem]); - } - this.logger.warning(`Circle will reward ${itemsRewardedCount} items costing a total of ${totalCost} roubles`); - - return rewards; - } - - protected getRewardStackSize(randomItemTplFromPool: string) { - if (this.itemHelper.isOfBaseclass(randomItemTplFromPool, BaseClasses.AMMO)) { - const ammoTemplate = this.itemHelper.getItem(randomItemTplFromPool)[1]; - return this.itemHelper.getRandomisedAmmoStackSize(ammoTemplate); - } - - return 1; - } - - /** - * Get a pool of tpl IDs of items the player needs to complete hideout crafts/upgrade areas - * @param sessionId Session id - * @param pmcData Player profile - * @returns Array of tpls - */ - protected getCultistCircleRewardPool(sessionId: string, pmcData: IPmcData): string[] { - const rewardPool = new Set(); - - // What does player need to upgrade hideout areas - const dbAreas = this.databaseService.getHideout().areas; - for (const area of pmcData.Hideout.Areas) { - const currentStageLevel = area.level; - const areaType = area.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.getNonFunctionalItemRequirements(nextStageDbData.requirements); - for (const rewardToAdd of itemRequirements) { - rewardPool.add(rewardToAdd.templateId); - } - } - } - - // What does player need to craft items with - const playerUnlockedRecipes = pmcData.UnlockedInfo.unlockedProductionRecipe; - const allRecipes = this.databaseService.getHideout().production; - - // Get default unlocked recipes + locked recipes they've unlocked - const playerAccessibleRecipes = allRecipes.recipes.filter( - (recipe) => !recipe.locked || playerUnlockedRecipes.includes(recipe._id), - ); - for (const recipe of playerAccessibleRecipes) { - const itemRequirements = this.getNonFunctionalItemRequirements(recipe.requirements); - for (const requirement of itemRequirements) { - rewardPool.add(requirement.templateId); - } - } - - // Check for scav case unlock in profile - const hasScavCaseAreaUnlocked = pmcData.Hideout.Areas[HideoutAreas.SCAV_CASE]?.level > 0; - if (hasScavCaseAreaUnlocked) { - // Gather up items used to start scav case crafts - const scavCaseCrafts = this.databaseService.getHideout().scavcase; - for (const craft of scavCaseCrafts) { - // Find the item requirements from each craft - const itemRequirements = this.getNonFunctionalItemRequirements(craft.Requirements); - for (const requirement of itemRequirements) { - // Add tpl to reward pool - rewardPool.add(requirement.templateId); - } - } - } - - return Array.from(rewardPool); - } - - /** - * Iterate over passed in hideout requirements and return the Item + nonfunctional requirements - * @param requirements Requirements to iterate over - * @returns Array of item requirements - */ - protected getNonFunctionalItemRequirements(requirements: IStageRequirement[] | Requirement[]) { - return requirements.filter((requirement) => requirement.type === "Item" && !requirement.isFunctional); + return this.circleOfCultistService.startSacrifice(sessionId, pmcData, request); } public hideoutDeleteProductionCommand( @@ -1532,26 +1318,8 @@ export class HideoutController { return output; } - protected getSacrificedItems(pmcData: IPmcData): Item[] { - // Get root items that are in the cultist sacrifice window - const inventoryRootItemsInCultistGrid = pmcData.Inventory.items.filter( - (item) => item.slotId === "CircleOfCultistsGrid1", - ); - - // Get rootitem + its children - const sacrificedItems: Item[] = []; - for (const rootItem of inventoryRootItemsInCultistGrid) { - const rootItemWithChildren = this.itemHelper.findAndReturnChildrenAsItems( - pmcData.Inventory.items, - rootItem._id, - ); - sacrificedItems.push(...rootItemWithChildren); - } - return sacrificedItems; - } - /** - * Function called every x seconds as part of onUpdate event + * Function called every `hideoutConfig.runIntervalSeconds` seconds as part of onUpdate event */ public update(): void { for (const sessionID in this.saveServer.getProfiles()) { diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 55ac1d18..eb7e262c 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -202,6 +202,7 @@ import { BotEquipmentModPoolService } from "@spt/services/BotEquipmentModPoolSer import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService"; import { BotLootCacheService } from "@spt/services/BotLootCacheService"; import { BotWeaponModLimitService } from "@spt/services/BotWeaponModLimitService"; +import { CircleOfCultistService } from "@spt/services/CircleOfCultistService"; import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { FenceService } from "@spt/services/FenceService"; @@ -793,6 +794,9 @@ export class Container { depContainer.register("LocationLifecycleService", LocationLifecycleService, { lifecycle: Lifecycle.Singleton, }); + depContainer.register("CircleOfCultistService", CircleOfCultistService, { + lifecycle: Lifecycle.Singleton, + }); } private static registerServers(depContainer: DependencyContainer): void {