import { inject, injectable } from "tsyringe"; import { IPmcData } from "../models/eft/common/IPmcData"; import { Common, HideoutArea, IHideoutImprovement, Production, Productive } from "../models/eft/common/tables/IBotBase"; import { Upd } from "../models/eft/common/tables/IItem"; import { StageBonus } from "../models/eft/hideout/IHideoutArea"; import { IHideoutContinuousProductionStartRequestData } from "../models/eft/hideout/IHideoutContinuousProductionStartRequestData"; import { IHideoutProduction } from "../models/eft/hideout/IHideoutProduction"; import { IHideoutSingleProductionStartRequestData } from "../models/eft/hideout/IHideoutSingleProductionStartRequestData"; import { IHideoutTakeProductionRequestData } from "../models/eft/hideout/IHideoutTakeProductionRequestData"; import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse"; import { ConfigTypes } from "../models/enums/ConfigTypes"; import { HideoutAreas } from "../models/enums/HideoutAreas"; import { SkillTypes } from "../models/enums/SkillTypes"; import { IHideoutConfig } from "../models/spt/config/IHideoutConfig"; import { ILogger } from "../models/spt/utils/ILogger"; import { EventOutputHolder } from "../routers/EventOutputHolder"; import { ConfigServer } from "../servers/ConfigServer"; import { DatabaseServer } from "../servers/DatabaseServer"; import { LocalisationService } from "../services/LocalisationService"; import { PlayerService } from "../services/PlayerService"; import { HashUtil } from "../utils/HashUtil"; import { HttpResponseUtil } from "../utils/HttpResponseUtil"; import { TimeUtil } from "../utils/TimeUtil"; import { InventoryHelper } from "./InventoryHelper"; import { ProfileHelper } from "./ProfileHelper"; @injectable() export class HideoutHelper { public static bitcoinFarm = "5d5c205bd582a50d042a3c0e"; public static waterCollector = "5d5589c1f934db045e6c5492"; public static bitcoin = "59faff1d86f7746c51718c9c"; public static expeditionaryFuelTank = "5d1b371186f774253763a656"; public static maxSkillPoint = 5000; private static generatorOffMultipler = 0.2; protected hideoutConfig: IHideoutConfig; constructor( @inject("WinstonLogger") protected logger: ILogger, @inject("HashUtil") protected hashUtil: HashUtil, @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("InventoryHelper") protected inventoryHelper: InventoryHelper, @inject("PlayerService") protected playerService: PlayerService, @inject("LocalisationService") protected localisationService: LocalisationService, @inject("ConfigServer") protected configServer: ConfigServer ) { this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT); } public registerProduction(pmcData: IPmcData, body: IHideoutSingleProductionStartRequestData | IHideoutContinuousProductionStartRequestData, sessionID: string): IItemEventRouterResponse { const recipe = this.databaseServer.getTables().hideout.production.find(p => p._id === body.recipeId); if (!recipe) { this.logger.error(this.localisationService.getText("hideout-missing_recipe_in_db", body.recipeId)); return this.httpResponse.appendErrorToOutput(this.eventOutputHolder.getOutput(sessionID)); } const modifiedProductionTime = recipe.productionTime - this.getCraftingSkillProductionTimeReduction(pmcData, recipe.productionTime); // @Important: Here we need to be very exact: // - normal recipe: Production time value is stored in attribute "productionType" with small "p" // - scav case recipe: Production time value is stored in attribute "ProductionType" with capital "P" pmcData.Hideout.Production[body.recipeId] = this.initProduction(body.recipeId, modifiedProductionTime); } /** * This convenience function initializes new Production Object * with all the constants. */ public initProduction(recipeId: string, productionTime: number): Production { return { Progress: 0, inProgress: true, RecipeId: recipeId, Products: [], SkipTime: 0, ProductionTime: productionTime, StartTimestamp: this.timeUtil.getTimestamp() }; } public isProductionType(productive: Productive): productive is Production { return (productive as Production).Progress !== undefined || (productive as Production).RecipeId !== undefined; } // BALIST0N, I got bad news for you // we do need to implement these after all // ... // with that I mean manual implementation // RIP, GL whoever is going to do this public applyPlayerUpgradesBonuses(pmcData: IPmcData, bonus: StageBonus): void { switch (bonus.type) { case "StashSize": // TODO: bonus should only have type/templateId for (const item in pmcData.Inventory.items) { if (pmcData.Inventory.items[item]._id === pmcData.Inventory.stash) { pmcData.Inventory.items[item]._tpl = bonus.templateId; } } break; case "MaximumEnergyReserve": pmcData.Health.Energy.Maximum += 10; break; case "EnergyRegeneration": case "HydrationRegeneration": case "HealthRegeneration": case "DebuffEndDelay": case "QuestMoneyReward": case "ExperienceRate": case "SkillGroupLevelingBoost": this.applySkillXPBoost(pmcData, bonus); break; case "ScavCooldownTimer": case "InsuranceReturnTime": case "RagfairCommission": case "FuelConsumption": // These skill is being applied automatically on the RagfairController, InsuranceController, ProfileController, HideoutController // ScavCooldownTimer, InsuranceReturnTime, RagfairCommission, FuelConsumption break; case "AdditionalSlots": // Some of these are also implemented on the HideoutController break; case "UnlockWeaponModification": case "RepairArmorBonus": case "RepairWeaponBonus": case "UnlockArmorRepair": case "UnlockWeaponRepair": case "TextBonus": // TODO: remove properties, only needs id/icon/type break; } pmcData.Bonuses.push(bonus); } /** * TODO: * After looking at the skills there doesnt seem to be a configuration per skill to boost * the XP gain PER skill. I THINK you should be able to put the variable "SkillProgress" (just like health has it) * and be able to tune the skill gain PER skill, but I havent tested it and Im not sure! * @param pmcData * @param bonus */ protected applySkillXPBoost(pmcData: IPmcData, bonus: StageBonus): void { const skillGroupType = bonus.skillType; if (skillGroupType) { switch (skillGroupType) { case "Physical": case "Mental": case "Combat": case "Practical": case "Special": default: break; } } } /** * Process a players hideout, update areas that use resources + increment production timers * @param sessionID Session id */ public updatePlayerHideout(sessionID: string): void { const pmcData = this.profileHelper.getPmcProfile(sessionID); const hideoutProperties = { btcFarmCGs: 0, isGeneratorOn: false, waterCollectorHasFilter: false }; this.updateAreasWithResources(sessionID, pmcData, hideoutProperties); this.updateProductionTimers(pmcData, hideoutProperties); pmcData.Hideout.sptUpdateLastRunTimestamp = this.timeUtil.getTimestamp(); } /** * Update progress timer for water collector * @param pmcData profile to update * @param productionId id of water collection production to update * @param hideoutProperties Hideout properties */ protected updateWaterCollectorProductionTimer(pmcData: IPmcData, productionId: string, hideoutProperties: { btcFarmCGs?: number; isGeneratorOn: boolean; waterCollectorHasFilter: boolean; }): void { let timeElapsed = (this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp); if (!hideoutProperties.isGeneratorOn) { timeElapsed = Math.floor(timeElapsed * HideoutHelper.generatorOffMultipler); } if (hideoutProperties.waterCollectorHasFilter) { pmcData.Hideout.Production[productionId].Progress += timeElapsed; } } /** * Iterate over productions and update their progress timers * @param pmcData Profile to check for productions and update * @param hideoutProperties Hideout properties */ protected updateProductionTimers(pmcData: IPmcData, hideoutProperties: { btcFarmCGs: number; isGeneratorOn: boolean; waterCollectorHasFilter: boolean; }): void { const recipes = this.databaseServer.getTables().hideout.production; // Check each production for (const prodId in pmcData.Hideout.Production) { const craft = pmcData.Hideout.Production[prodId]; // Craft complete, skip processing (Don't skip continious crafts like bitcoin farm) if (craft.Progress >= craft.ProductionTime && prodId !== HideoutHelper.bitcoinFarm) { continue; } if (craft.sptIsScavCase) { this.updateScavCaseProductionTimer(pmcData, prodId); continue; } if (prodId === HideoutHelper.waterCollector) { this.updateWaterCollectorProductionTimer(pmcData, prodId, hideoutProperties); continue; } if (prodId === HideoutHelper.bitcoinFarm) { pmcData.Hideout.Production[prodId] = this.updateBitcoinFarm(pmcData, hideoutProperties.btcFarmCGs, hideoutProperties.isGeneratorOn); continue; } // Other recipes not covered by above const recipe = recipes.find(r => r._id === prodId); if (!recipe) { this.logger.error(this.localisationService.getText("hideout-missing_recipe_for_area", prodId)); continue; } this.updateProductionProgress(pmcData, prodId, recipe, hideoutProperties); } } /** * Update a productions progress value based on the amount of time that has passed * @param pmcData Player profile * @param prodId Production id being crafted * @param recipe Recipe data being crafted * @param hideoutProperties */ protected updateProductionProgress(pmcData: IPmcData, prodId: string, recipe: IHideoutProduction, hideoutProperties: { btcFarmCGs?: number; isGeneratorOn: boolean; waterCollectorHasFilter?: boolean; }): void { // Production is complete, no need to do any calculations if (this.doesProgressMatchProductionTime(pmcData, prodId)) { return; } // Get seconds since production started + now let timeElapsed = (this.timeUtil.getTimestamp() - pmcData.Hideout.Production[prodId].StartTimestamp) - pmcData.Hideout.Production[prodId].Progress; if (!hideoutProperties.isGeneratorOn) { // Adjust for running without fuel timeElapsed = Math.floor(timeElapsed * this.databaseServer.getTables().hideout.settings.generatorSpeedWithoutFuel); } // Increment progress by time passed pmcData.Hideout.Production[prodId].Progress += timeElapsed; // Limit progress to total production time if progress is over (dont run for continious crafts)) if (!recipe.continuous) { if (pmcData.Hideout.Production[prodId].Progress > pmcData.Hideout.Production[prodId].ProductionTime) { pmcData.Hideout.Production[prodId].Progress = pmcData.Hideout.Production[prodId].ProductionTime; } } } /** * Check if a productions progress value matches its corresponding recipes production time value * @param pmcData Player profile * @param prodId Production id * @param recipe Recipe being crafted * @returns progress matches productionTime from recipe */ protected doesProgressMatchProductionTime(pmcData: IPmcData, prodId: string): boolean { return pmcData.Hideout.Production[prodId].Progress === pmcData.Hideout.Production[prodId].ProductionTime; } /** * Update progress timer for scav case * @param pmcData Profile to update * @param productionId Id of scav case production to update */ protected updateScavCaseProductionTimer(pmcData: IPmcData, productionId: string): void { const timeElapsed = (this.timeUtil.getTimestamp() - pmcData.Hideout.Production[productionId].StartTimestamp) - pmcData.Hideout.Production[productionId].Progress; pmcData.Hideout.Production[productionId].Progress += timeElapsed; } /** * Iterate over hideout areas that use resources (fuel/filters etc) and update associated values * @param sessionID Session id * @param pmcData Profile to update areas of * @param hideoutProperties hideout properties */ protected updateAreasWithResources(sessionID: string, pmcData: IPmcData, hideoutProperties: { btcFarmCGs: number; isGeneratorOn: boolean; waterCollectorHasFilter: boolean; }): void { for (const area of pmcData.Hideout.Areas) { switch (area.type) { case HideoutAreas.GENERATOR: hideoutProperties.isGeneratorOn = area.active; if (hideoutProperties.isGeneratorOn) { this.updateFuel(area, pmcData); } break; case HideoutAreas.WATER_COLLECTOR: this.updateWaterCollector(sessionID, pmcData, area, hideoutProperties.isGeneratorOn); hideoutProperties.waterCollectorHasFilter = this.doesWaterCollectorHaveFilter(area); break; case HideoutAreas.AIR_FILTERING: if (hideoutProperties.isGeneratorOn) { this.updateAirFilters(area, pmcData); } break; case HideoutAreas.BITCOIN_FARM: for (const slot of area.slots) { if (slot.item) { hideoutProperties.btcFarmCGs++; } } break; } } } protected updateWaterCollector(sessionId: string, pmcData: IPmcData, area: HideoutArea, isGeneratorOn: boolean): void { if (area.level === 3) { const prod = pmcData.Hideout.Production[HideoutHelper.waterCollector]; if (prod && this.isProduction(prod)) { area = this.updateWaterFilters(area, prod, isGeneratorOn, pmcData); } else { // continuousProductionStart() // seem to not trigger consistently const recipe: IHideoutSingleProductionStartRequestData = { recipeId: HideoutHelper.waterCollector, Action: "HideoutSingleProductionStart", items: [], timestamp: this.timeUtil.getTimestamp() }; this.registerProduction(pmcData, recipe, sessionId); } } } protected doesWaterCollectorHaveFilter(waterCollector: HideoutArea): boolean { if (waterCollector.level === 3) // can put filters in from L3 { // Has filter in at least one slot return waterCollector.slots.some(x => x.item); } // No Filter return false; } protected updateFuel(generatorArea: HideoutArea, pmcData: IPmcData): void { // 1 resource last 14 min 27 sec, 1/14.45/60 = 0.00115 // 10-10-2021 From wiki, 1 resource last 12 minutes 38 seconds, 1/12.63333/60 = 0.00131 let fuelDrainRate = this.databaseServer.getTables().hideout.settings.generatorFuelFlowRate * this.hideoutConfig.runIntervalSeconds; // implemented moddable bonus for fuel consumption bonus instead of using solar power variable as before const fuelBonus = pmcData.Bonuses.find(b => b.type === "FuelConsumption"); const fuelBonusPercent = 1.0 - (fuelBonus ? Math.abs(fuelBonus.value) : 0) / 100; fuelDrainRate *= fuelBonusPercent; // Hideout management resource consumption bonus: const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData); fuelDrainRate *= hideoutManagementConsumptionBonus; let hasFuelRemaining = false; let pointsConsumed = 0; for (let i = 0; i < generatorArea.slots.length; i++) { if (generatorArea.slots[i].item) { let resourceValue = (generatorArea.slots[i].item[0].upd?.Resource) ? generatorArea.slots[i].item[0].upd.Resource.Value : null; if (resourceValue === 0) { continue; } else if (!resourceValue) { const fuelItem = HideoutHelper.expeditionaryFuelTank; resourceValue = generatorArea.slots[i].item[0]._tpl === fuelItem ? resourceValue = 60 - fuelDrainRate : resourceValue = 100 - fuelDrainRate; pointsConsumed = fuelDrainRate; } else { pointsConsumed = (generatorArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0) + fuelDrainRate; resourceValue -= fuelDrainRate; } resourceValue = Math.round(resourceValue * 10000) / 10000; pointsConsumed = Math.round(pointsConsumed * 10000) / 10000; //check unit consumed for increment skill point if (pmcData && Math.floor(pointsConsumed / 10) >= 1) { this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); pointsConsumed -= 10; } if (resourceValue > 0) { generatorArea.slots[i].item[0].upd = this.getAreaUpdObject(1, resourceValue, pointsConsumed); this.logger.debug(`Generator: ${resourceValue} fuel left in slot ${i + 1}`); hasFuelRemaining = true; break; // Break here to avoid updating all the fuel tanks } else { generatorArea.slots[i].item[0].upd = this.getAreaUpdObject(1, 0, 0); // Update remaining resources to be subtracted fuelDrainRate = Math.abs(resourceValue); } } } if (!hasFuelRemaining) { generatorArea.active = false; } } /** * Adjust water filter objects resourceValue or delete when they reach 0 resource * @param waterFilterArea water filter area to update * @param production production object * @param isGeneratorOn is generator enabled * @param pmcData Player profile * @returns Updated HideoutArea object */ protected updateWaterFilters(waterFilterArea: HideoutArea, production: Production, isGeneratorOn: boolean, pmcData: IPmcData): HideoutArea { let timeElapsed = this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp; // 100 resources last 8 hrs 20 min, 100/8.33/60/60 = 0.00333 let filterDrainRate = 0.00333; // Hideout management resource consumption bonus: const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData); filterDrainRate *= hideoutManagementConsumptionBonus; let productionTime = 0; let pointsConsumed = 0; const recipe = this.databaseServer.getTables().hideout.production.find(prod => prod._id === HideoutHelper.waterCollector); productionTime = (recipe.productionTime || 0); // Reduce time elapsed (and progress) when generator is off if (!isGeneratorOn) { timeElapsed = Math.floor(timeElapsed * HideoutHelper.generatorOffMultipler); } if (timeElapsed > productionTime) { // We've gone beyond completion filterDrainRate *= (productionTime - production.Progress); } else { filterDrainRate *= timeElapsed; } // Production hasn't completed if (production.Progress < productionTime) { // Check all slots that take water filters for (let i = 0; i < waterFilterArea.slots.length; i++) { // Has a water filter installed into slot if (waterFilterArea.slots[i].item) { let resourceValue = (waterFilterArea.slots[i].item[0].upd?.Resource) ? waterFilterArea.slots[i].item[0].upd.Resource.Value : null; if (!resourceValue) { resourceValue = 100 - filterDrainRate; pointsConsumed = filterDrainRate; } else { pointsConsumed = (waterFilterArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0) + filterDrainRate; resourceValue -= filterDrainRate; } resourceValue = Math.round(resourceValue * 10000) / 10000; pointsConsumed = Math.round(pointsConsumed * 10000) / 10000; //check unit consumed for increment skill point if (pmcData && Math.floor(pointsConsumed / 10) >= 1) { this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); pointsConsumed -= 10; } // Filter has some juice left in it if (resourceValue > 0) { waterFilterArea.slots[i].item[0].upd = this.getAreaUpdObject(1, resourceValue, pointsConsumed); this.logger.debug(`Water filter: ${resourceValue} filter left on slot ${i + 1}`); break; // Break here to avoid updating all filters } else { // Filter ran out / used up delete waterFilterArea.slots[i].item; // Update remaining resources to be subtracted filterDrainRate = Math.abs(resourceValue); } } } } return waterFilterArea; } protected getAreaUpdObject(stackCount: number, resourceValue: number, resourceUnitsConsumed: number): Upd { return { StackObjectsCount: stackCount, Resource: { Value: resourceValue, UnitsConsumed: resourceUnitsConsumed } }; } protected updateAirFilters(airFilterArea: HideoutArea, pmcData: IPmcData): void { // 300 resources last 20 hrs, 300/20/60/60 = 0.00416 /* 10-10-2021 from WIKI (https://escapefromtarkov.fandom.com/wiki/FP-100_filter_absorber) Lasts for 17 hours 38 minutes and 49 seconds (23 hours 31 minutes and 45 seconds with elite hideout management skill), 300/17.64694/60/60 = 0.004722 */ let filterDrainRate = this.databaseServer.getTables().hideout.settings.airFilterUnitFlowRate * this.hideoutConfig.runIntervalSeconds; // Hideout management resource consumption bonus: const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData); filterDrainRate *= hideoutManagementConsumptionBonus; let pointsConsumed = 0; for (let i = 0; i < airFilterArea.slots.length; i++) { if (airFilterArea.slots[i].item) { let resourceValue = (airFilterArea.slots[i].item[0].upd?.Resource) ? airFilterArea.slots[i].item[0].upd.Resource.Value : null; if (!resourceValue) { resourceValue = 300 - filterDrainRate; pointsConsumed = filterDrainRate; } else { pointsConsumed = (airFilterArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0) + filterDrainRate; resourceValue -= filterDrainRate; } resourceValue = Math.round(resourceValue * 10000) / 10000; pointsConsumed = Math.round(pointsConsumed * 10000) / 10000; //check unit consumed for increment skill point if (pmcData && Math.floor(pointsConsumed / 10) >= 1) { this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); pointsConsumed -= 10; } if (resourceValue > 0) { airFilterArea.slots[i].item[0].upd = { StackObjectsCount: 1, Resource: { Value: resourceValue, UnitsConsumed: pointsConsumed } }; this.logger.debug(`Air filter: ${resourceValue} filter left on slot ${i + 1}`); break; // Break here to avoid updating all filters } else { delete airFilterArea.slots[i].item; // Update remaining resources to be subtracted filterDrainRate = Math.abs(resourceValue); } } } } protected updateBitcoinFarm(pmcData: IPmcData, btcFarmCGs: number, isGeneratorOn: boolean): Production { const btcProd = pmcData.Hideout.Production[HideoutHelper.bitcoinFarm]; const bitcoinProdData = this.databaseServer.getTables().hideout.production.find(p => p._id === "5d5c205bd582a50d042a3c0e"); const coinSlotCount = this.getBTCSlots(pmcData); // Full on bitcoins, halt progress if (this.isProduction(btcProd) && btcProd.Products.length >= coinSlotCount) { // Set progress to 0 btcProd.Progress = 0; return btcProd; } if (this.isProduction(btcProd)) { let timeElapsedSeconds = this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp; if (!isGeneratorOn) { // Generator off, reduce time elapsed timeElapsedSeconds = Math.floor(timeElapsedSeconds * HideoutHelper.generatorOffMultipler); } btcProd.Progress += timeElapsedSeconds; // The wiki has a wrong formula! // Do not change unless you validate it with the Client code files! // This formula was found on the client files: // ******************************************************* /* public override int InstalledSuppliesCount { get { return this.int_1; } protected set { if (this.int_1 === value) { return; } this.int_1 = value; base.Single_0 = ((this.int_1 === 0) ? 0f : (1f + (float)(this.int_1 - 1) * this.float_4)); } } */ // ********************************************************** // At the time of writing this comment, this was GClass1667 // To find it in case of weird results, use DNSpy and look for usages on class AreaData // Look for a GClassXXXX that has a method called "InitDetails" and the only parameter is the AreaData // That should be the bitcoin farm production. To validate, try to find the snippet below: /* protected override void InitDetails(AreaData data) { base.InitDetails(data); this.gclass1678_1.Type = EDetailsType.Farming; } */ // BSG finally fixed their settings, they now get loaded from the settings and used in the client const coinCraftTimeSeconds = bitcoinProdData.productionTime / (1 + (btcFarmCGs - 1) * this.databaseServer.getTables().hideout.settings.gpuBoostRate); while (btcProd.Progress > coinCraftTimeSeconds) { if (btcProd.Products.length < coinSlotCount) { btcProd.Products.push({ "_id": this.hashUtil.generate(), "_tpl": "59faff1d86f7746c51718c9c", "upd": { "StackObjectsCount": 1 } }); btcProd.Progress -= coinCraftTimeSeconds; } else { btcProd.Progress = 0; } } btcProd.StartTimestamp = this.timeUtil.getTimestamp(); return btcProd; } else { return null; } } /** * Get a count of how many BTC can be gathered by the profile * @param pmcData Profile to look up * @returns coin slot count */ protected getBTCSlots(pmcData: IPmcData): number { const bitcoinProduction = this.databaseServer.getTables().hideout.production.find(p => p._id === HideoutHelper.bitcoinFarm); const productionSlots = bitcoinProduction?.productionLimitCount || 3; const hasManagementSkillSlots = this.hasEliteHideoutManagementSkill(pmcData); const managementSlotsCount = this.getManagementSkillsSlots() || 2; return productionSlots + (hasManagementSkillSlots ? managementSlotsCount : 0); } /** * Get a count of bitcoins player miner can hold */ protected getManagementSkillsSlots(): number { return this.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Container; } /** * Does profile have elite hideout management skill * @param pmcData Profile to look at * @returns True if profile has skill */ protected hasEliteHideoutManagementSkill(pmcData: IPmcData): boolean { return this.getHideoutManagementSkill(pmcData)?.Progress >= 5100; // level 51+ } /** * Get the hideout management skill from player profile * @param pmcData Profile to look at * @returns Hideout management skill object */ protected getHideoutManagementSkill(pmcData: IPmcData): Common { return pmcData.Skills.Common.find(x => x.Id === "HideoutManagement"); } protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number { const hideoutManagementSkill = this.getHideoutManagementSkill(pmcData); if (!hideoutManagementSkill) { return 0; } let roundedLevel = Math.floor(hideoutManagementSkill.Progress / 100); // If the level is 51 we need to round it at 50 so on elite you dont get 25.5% // at level 1 you already get 0.5%, so it goes up until level 50. For some reason the wiki // says that it caps at level 51 with 25% but as per dump data that is incorrect apparently roundedLevel = (roundedLevel === 51) ? roundedLevel - 1 : roundedLevel; return (roundedLevel * this.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement.ConsumptionReductionPerLevel) / 100; } /** * Get the crafting skill details from player profile * @param pmcData Player profile * @returns crafting skill, null if not found */ protected getCraftingSkill(pmcData: IPmcData): Common { for (const skill of pmcData.Skills.Common) { if (skill.Id === SkillTypes.CRAFTING) { return skill; } } return null; } /** * Adjust craft time based on crafting skill level found in player profile * @param pmcData Player profile * @param productionTime Time to complete hideout craft in seconds * @returns Adjusted craft time in seconds */ protected getCraftingSkillProductionTimeReduction(pmcData: IPmcData, productionTime: number): number { const craftingSkill = this.getCraftingSkill(pmcData); if (!craftingSkill) { return productionTime; } const roundedLevel = Math.floor(Math.min(HideoutHelper.maxSkillPoint, craftingSkill.Progress) / 100); const percentageToDrop = roundedLevel * 0.75; return (productionTime * percentageToDrop) / 100; } public isProduction(productive: Productive): productive is Production { return (productive as Production).Progress !== undefined || (productive as Production).RecipeId !== undefined; } /** * Gather crafted BTC from hideout area and add to inventory * Reset production start timestamp if hideout area at full coin capacity * @param pmcData Player profile * @param request Take production request * @param sessionId Session id * @returns IItemEventRouterResponse */ public getBTC(pmcData: IPmcData, request: IHideoutTakeProductionRequestData, sessionId: string): IItemEventRouterResponse { const output = this.eventOutputHolder.getOutput(sessionId); // Get how many coins were crafted and ready to pick up const craftedCoinCount = pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length; if (!craftedCoinCount) { const errorMsg = this.localisationService.getText("hideout-no_bitcoins_to_collect"); this.logger.error(errorMsg); return this.httpResponse.appendErrorToOutput(output, errorMsg); } const coinsToAddToInventory = { items: [{ // eslint-disable-next-line @typescript-eslint/naming-convention item_id: HideoutHelper.bitcoin, count: pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length }], tid: "ragfair" }; const coinSlotCount = this.getBTCSlots(pmcData); // Run callback after coins are added to player inventory const callback = () => { // Is at max capacity if (pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length >= coinSlotCount) { // Set start to now pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].StartTimestamp = this.timeUtil.getTimestamp(); } // Remove crafted coins from production in profile pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products = []; }; // Add FiR coins to player inventory return this.inventoryHelper.addItem(pmcData, coinsToAddToInventory, output, sessionId, callback, true); } /** * Upgrade hideout wall from starting level to interactable level if enough time has passed * @param pmcProfile Profile to upgrade wall in */ public unlockHideoutWallInProfile(pmcProfile: IPmcData): void { // Sufficient time has passed since account created, upgrade wall to next level to be interactable const wallUnlockTimestamp = this.hideoutConfig.hideoutWallAppearTimeSeconds + pmcProfile.Info.RegistrationDate; if (wallUnlockTimestamp < this.timeUtil.getTimestamp()) { // Get wall area const wall = pmcProfile.Hideout.Areas.find(x => x.type === HideoutAreas.EMERGENCY_WALL); if (!wall) { return; } if (wall.level === 0) { wall.level++; wall.constructing = true; return; } if (wall.level === 1) { if (this.hideoutImprovementIsComplete(pmcProfile.Hideout.Improvements["639199277a9178252d38c98f"])) { this.logger.debug("Improvement 639199277a9178252d38c98f found, upgrading hideout wall from level: 1 to 3"); wall.level = 3; wall.constructing = false; // 0 = no wall | Idle State // 1 - EATS EVERYTHING without areas.json change to include improvements // 2 - Should be Moppable wall / Interactable wall While Constructing = true Sledgehammer is Smashable // 2 - While false UI is broken. While true mimics level 3 hideout // 3 - Smashable wall / Sledgehammer // 4 - Installable door } return; } // Workaround for old profiles that have the wall at level 2 if (wall.level === 2) { this.logger.debug("Old wall level 2 found, fixing"); if (this.hideoutImprovementIsComplete(pmcProfile.Hideout.Improvements["639199277a9178252d38c98f"])) { this.logger.debug("Wall level adjusted to 3"); wall.level++; } else { this.logger.debug("Wall level adjusted to 1"); wall.level--; } } } } /** * Hideout improvement is flagged as complete * @param improvement hideout improvement object * @returns true if complete */ protected hideoutImprovementIsComplete(improvement: IHideoutImprovement): boolean { if (improvement?.completed) { return true; } return false; } /** * Iterate over hideout improvements not completed and check if they need to be adjusted * @param pmcProfile Profile to adjust */ public setHideoutImprovementsToCompleted(pmcProfile: IPmcData): void { for (const improvementId in pmcProfile.Hideout.Improvements) { const improvementDetails = pmcProfile.Hideout.Improvements[improvementId]; if (improvementDetails.completed === false && improvementDetails.improveCompleteTimestamp < this.timeUtil.getTimestamp()) { improvementDetails.completed = true; } } } }