Server/project/src/helpers/HideoutHelper.ts

1306 lines
51 KiB
TypeScript
Raw Normal View History

2023-03-03 16:23:46 +01:00
import { inject, injectable } from "tsyringe";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { HideoutArea, IHideoutImprovement, Production, Productive } from "@spt/models/eft/common/tables/IBotBase";
import { Item, Upd } from "@spt/models/eft/common/tables/IItem";
import { StageBonus } from "@spt/models/eft/hideout/IHideoutArea";
import { IHideoutContinuousProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutContinuousProductionStartRequestData";
import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
import { IHideoutSingleProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutSingleProductionStartRequestData";
import { IHideoutTakeProductionRequestData } from "@spt/models/eft/hideout/IHideoutTakeProductionRequestData";
import { IAddItemsDirectRequest } from "@spt/models/eft/inventory/IAddItemsDirectRequest";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { BonusType } from "@spt/models/enums/BonusType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { HideoutAreas } from "@spt/models/enums/HideoutAreas";
import { SkillTypes } from "@spt/models/enums/SkillTypes";
import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { PlayerService } from "@spt/services/PlayerService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
2023-03-03 16:23:46 +01:00
@injectable()
export class HideoutHelper
{
public static bitcoinFarm = "5d5c205bd582a50d042a3c0e";
public static bitcoinProductionId = "5d5c205bd582a50d042a3c0e";
2023-03-03 16:23:46 +01:00
public static waterCollector = "5d5589c1f934db045e6c5492";
public static bitcoinTpl = "59faff1d86f7746c51718c9c";
2023-03-03 16:23:46 +01:00
public static expeditionaryFuelTank = "5d1b371186f774253763a656";
public static maxSkillPoint = 5000;
protected hideoutConfig: IHideoutConfig;
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
2023-03-03 16:23:46 +01:00
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("DatabaseService") protected databaseService: DatabaseService,
2023-03-03 16:23:46 +01:00
@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("ItemHelper") protected itemHelper: ItemHelper,
2023-11-16 02:35:05 +01:00
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
2023-03-03 16:23:46 +01:00
)
{
this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT);
}
/**
* Add production to profiles' Hideout.Production array
* @param pmcData Profile to add production to
* @param body Production request
* @param sessionID Session id
* @returns client response
*/
2023-11-16 02:35:05 +01:00
public registerProduction(
pmcData: IPmcData,
body: IHideoutSingleProductionStartRequestData | IHideoutContinuousProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
2023-03-03 16:23:46 +01:00
{
const recipe = this.databaseService.getHideout().production
.find((production) => production._id === body.recipeId);
2023-03-03 16:23:46 +01:00
if (!recipe)
{
this.logger.error(this.localisationService.getText("hideout-missing_recipe_in_db", body.recipeId));
return this.httpResponse.appendErrorToOutput(this.eventOutputHolder.getOutput(sessionID));
}
// @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"
if (!pmcData.Hideout.Production)
{
pmcData.Hideout.Production = {};
}
const modifiedProductionTime = this.getAdjustedCraftTimeWithSkills(pmcData, body.recipeId);
const production = this.initProduction(
2023-11-16 02:35:05 +01:00
body.recipeId,
modifiedProductionTime,
recipe.needFuelForAllProductionTime,
);
// Store the tools used for this production, so we can return them later
const bodyAsSingle = body as IHideoutSingleProductionStartRequestData;
if (bodyAsSingle && bodyAsSingle.tools?.length > 0)
{
production.sptRequiredTools = [];
for (const tool of bodyAsSingle.tools)
{
const toolItem = this.cloner.clone(pmcData.Inventory.items.find((x) => x._id === tool.id));
// Make sure we only return as many as we took
this.itemHelper.addUpdObjectToItem(toolItem);
toolItem.upd.StackObjectsCount = tool.count;
production.sptRequiredTools.push({
_id: this.hashUtil.generate(),
_tpl: toolItem._tpl,
upd: toolItem.upd,
});
}
}
pmcData.Hideout.Production[body.recipeId] = production;
2023-03-03 16:23:46 +01:00
}
/**
* This convenience function initializes new Production Object
* with all the constants.
*/
public initProduction(recipeId: string, productionTime: number, needFuelForAllProductionTime: boolean): Production
2023-03-03 16:23:46 +01:00
{
return {
Progress: 0,
inProgress: true,
RecipeId: recipeId,
StartTimestamp: this.timeUtil.getTimestamp(),
2023-03-03 16:23:46 +01:00
ProductionTime: productionTime,
Products: [],
GivenItemsInStart: [],
Interrupted: false,
NeedFuelForAllProductionTime: needFuelForAllProductionTime, // Used when sending to client
needFuelForAllProductionTime: needFuelForAllProductionTime, // used when stored in production.json
2023-11-16 02:35:05 +01:00
SkipTime: 0,
2023-03-03 16:23:46 +01:00
};
}
2023-07-24 16:52:55 +02:00
/**
* Is the provided object a Production type
2023-11-16 02:35:05 +01:00
* @param productive
* @returns
2023-07-24 16:52:55 +02:00
*/
2023-03-03 16:23:46 +01:00
public isProductionType(productive: Productive): productive is Production
{
return (productive as Production).Progress !== undefined || (productive as Production).RecipeId !== undefined;
}
/**
* Apply bonus to player profile given after completing hideout upgrades
* @param pmcData Profile to add bonus to
* @param bonus Bonus to add to profile
*/
2023-03-03 16:23:46 +01:00
public applyPlayerUpgradesBonuses(pmcData: IPmcData, bonus: StageBonus): void
{
// Handle additional changes some bonuses need before being added
2023-03-03 16:23:46 +01:00
switch (bonus.type)
{
case BonusType.STASH_SIZE:
2023-11-16 02:35:05 +01:00
{
// Find stash item and adjust tpl to new tpl from bonus
const stashItem = pmcData.Inventory.items.find((x) => x._id === pmcData.Inventory.stash);
if (!stashItem)
2023-03-03 16:23:46 +01:00
{
2024-05-24 17:42:42 +02:00
this.logger.warning(this.localisationService.getText("hideout-unable_to_apply_stashsize_bonus_no_stash_found", pmcData.Inventory.stash));
2023-03-03 16:23:46 +01:00
}
stashItem._tpl = bonus.templateId;
2024-05-24 17:42:42 +02:00
2023-03-03 16:23:46 +01:00
break;
}
case BonusType.MAXIMUM_ENERGY_RESERVE:
// Amend max energy in profile
pmcData.Health.Energy.Maximum += bonus.value;
2023-03-03 16:23:46 +01:00
break;
case BonusType.TEXT_BONUS:
2023-11-16 02:35:05 +01:00
// Delete values before they're added to profile
delete bonus.passive;
delete bonus.production;
delete bonus.visible;
2023-03-03 16:23:46 +01:00
break;
}
// Add bonus to player bonuses array in profile
// EnergyRegeneration, HealthRegeneration, RagfairCommission, ScavCooldownTimer, SkillGroupLevelingBoost, ExperienceRate, QuestMoneyReward etc
this.logger.debug(`Adding bonus: ${bonus.type} to profile, value: ${bonus.value ?? ""}`);
2023-03-03 16:23:46 +01:00
pmcData.Bonuses.push(bonus);
}
/**
* 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 = this.getHideoutProperties(pmcData);
2023-03-03 16:23:46 +01:00
this.updateAreasWithResources(sessionID, pmcData, hideoutProperties);
this.updateProductionTimers(pmcData, hideoutProperties);
pmcData.Hideout.sptUpdateLastRunTimestamp = this.timeUtil.getTimestamp();
}
/**
* Get various properties that will be passed to hideout update-related functions
* @param pmcData Player profile
* @returns Properties
*/
protected getHideoutProperties(pmcData: IPmcData): {
btcFarmCGs: number
isGeneratorOn: boolean
waterCollectorHasFilter: boolean
}
{
const bitcoinFarm = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.BITCOIN_FARM);
const bitcoinCount = bitcoinFarm?.slots.filter((slot) => slot.item).length ?? 0; // Get slots with an item property
const hideoutProperties = {
btcFarmCGs: bitcoinCount,
isGeneratorOn: pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.GENERATOR)?.active ?? false,
2023-11-16 02:35:05 +01:00
waterCollectorHasFilter: this.doesWaterCollectorHaveFilter(
pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.WATER_COLLECTOR),
2023-11-16 02:35:05 +01:00
),
};
return hideoutProperties;
}
protected doesWaterCollectorHaveFilter(waterCollector: HideoutArea): boolean
{
// Can put filters in from L3
2023-11-16 02:35:05 +01:00
if (waterCollector.level === 3)
{
// Has filter in at least one slot
return waterCollector.slots.some((slot) => slot.item);
}
2023-11-16 02:35:05 +01:00
// No Filter
return false;
}
2023-11-16 02:35:05 +01:00
2023-03-03 16:23:46 +01:00
/**
* Iterate over productions and update their progress timers
* @param pmcData Profile to check for productions and update
* @param hideoutProperties Hideout properties
*/
2023-11-16 02:35:05 +01:00
protected updateProductionTimers(
pmcData: IPmcData,
hideoutProperties: { btcFarmCGs: number, isGeneratorOn: boolean, waterCollectorHasFilter: boolean },
2023-11-16 02:35:05 +01:00
): void
2023-03-03 16:23:46 +01:00
{
const recipes = this.databaseService.getHideout().production;
2023-03-03 16:23:46 +01:00
// Check each production
for (const prodId in pmcData.Hideout.Production)
{
const craft = pmcData.Hideout.Production[prodId];
if (!craft)
{
// Craft value is undefined, get rid of it (could be from cancelling craft that needs cleaning up)
delete pmcData.Hideout.Production[prodId];
2023-11-11 21:15:46 +01:00
continue;
}
2023-03-03 16:23:46 +01:00
if (craft.Progress === undefined)
{
2024-05-24 17:42:42 +02:00
this.logger.warning(this.localisationService.getText("hideout-craft_has_undefined_progress_value_defaulting", prodId));
craft.Progress = 0;
}
2023-03-03 16:23:46 +01:00
// 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)
{
2023-11-16 02:35:05 +01:00
pmcData.Hideout.Production[prodId] = this.updateBitcoinFarm(
pmcData,
hideoutProperties.btcFarmCGs,
hideoutProperties.isGeneratorOn,
);
2023-03-03 16:23:46 +01:00
continue;
}
// Other recipes not covered by above
const recipe = recipes.find((r) => r._id === prodId);
2023-03-03 16:23:46 +01:00
if (!recipe)
{
this.logger.error(this.localisationService.getText("hideout-missing_recipe_for_area", prodId));
continue;
}
this.updateProductionProgress(pmcData, prodId, recipe, hideoutProperties);
}
}
/**
* 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
{
const timeElapsed = this.getTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.isGeneratorOn);
if (hideoutProperties.waterCollectorHasFilter)
{
pmcData.Hideout.Production[productionId].Progress += timeElapsed;
}
}
2023-03-03 16:23:46 +01:00
/**
* 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
2023-11-16 02:35:05 +01:00
* @param hideoutProperties
2023-03-03 16:23:46 +01:00
*/
2023-11-16 02:35:05 +01:00
protected updateProductionProgress(
pmcData: IPmcData,
prodId: string,
recipe: IHideoutProduction,
hideoutProperties: { btcFarmCGs?: number, isGeneratorOn: boolean, waterCollectorHasFilter?: boolean },
2023-11-16 02:35:05 +01:00
): void
2023-03-03 16:23:46 +01:00
{
// Production is complete, no need to do any calculations
if (this.doesProgressMatchProductionTime(pmcData, prodId))
{
return;
}
// Get seconds since last hideout update + now
const timeElapsed = this.getTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.isGeneratorOn, recipe);
2023-03-03 16:23:46 +01:00
// Increment progress by time passed
2023-04-23 20:47:39 +02:00
const production = pmcData.Hideout.Production[prodId];
production.Progress
+= production.needFuelForAllProductionTime && !hideoutProperties.isGeneratorOn ? 0 : timeElapsed; // Some items NEED power to craft (e.g. DSP)
2023-03-03 16:23:46 +01:00
// Limit progress to total production time if progress is over (dont run for continious crafts))
if (!recipe.continuous)
{
2023-04-23 20:47:39 +02:00
// If progress is larger than prod time, return ProductionTime, hard cap the vaue
production.Progress = Math.min(production.Progress, production.ProductionTime);
2023-03-03 16:23:46 +01:00
}
}
/**
* 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;
2023-03-03 16:23:46 +01:00
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
*/
2023-11-16 02:35:05 +01:00
protected updateAreasWithResources(
sessionID: string,
pmcData: IPmcData,
hideoutProperties: { btcFarmCGs: number, isGeneratorOn: boolean, waterCollectorHasFilter: boolean },
2023-11-16 02:35:05 +01:00
): void
2023-03-03 16:23:46 +01:00
{
for (const area of pmcData.Hideout.Areas)
{
switch (area.type)
{
case HideoutAreas.GENERATOR:
if (hideoutProperties.isGeneratorOn)
{
this.updateFuel(area, pmcData, hideoutProperties.isGeneratorOn);
2023-03-03 16:23:46 +01:00
}
break;
case HideoutAreas.WATER_COLLECTOR:
this.updateWaterCollector(sessionID, pmcData, area, hideoutProperties);
2023-03-03 16:23:46 +01:00
break;
case HideoutAreas.AIR_FILTERING:
if (hideoutProperties.isGeneratorOn)
{
this.updateAirFilters(area, pmcData, hideoutProperties.isGeneratorOn);
2023-03-03 16:23:46 +01:00
}
break;
}
}
}
/**
* Decrease fuel from generator slots based on amount of time since last time this occured
* @param generatorArea Hideout area
* @param pmcData Player profile
* @param isGeneratorOn Is the generator turned on since last update
*/
protected updateFuel(generatorArea: HideoutArea, pmcData: IPmcData, isGeneratorOn: boolean): void
2023-03-03 16:23:46 +01:00
{
// 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 fuelUsedSinceLastTick
= this.databaseService.getHideout().settings.generatorFuelFlowRate
* this.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
const profileFuelConsumptionBonus = pmcData.Bonuses.find((bonus) => bonus.type === BonusType.FUEL_CONSUMPTION);
// 0 to 1
const fuelConsumptionBonusMultipler
= (profileFuelConsumptionBonus ? Math.abs(profileFuelConsumptionBonus.value) : 0) / 100;
// 0 to 1
const hideoutManagementConsumptionBonusMultipler = this.getHideoutManagementConsumptionBonus(pmcData);
const combinedBonus = 1.0 - (fuelConsumptionBonusMultipler + hideoutManagementConsumptionBonusMultipler);
fuelUsedSinceLastTick *= combinedBonus;
2024-02-17 22:55:43 +01:00
2023-03-03 16:23:46 +01:00
let hasFuelRemaining = false;
let pointsConsumed = 0;
for (let i = 0; i < generatorArea.slots.length; i++)
{
2024-02-17 22:55:43 +01:00
const generatorSlot = generatorArea.slots[i];
if (!generatorSlot?.item)
{
// No item in slot, skip
continue;
}
2024-02-17 22:55:43 +01:00
const fuelItemInSlot = generatorSlot?.item[0];
if (!fuelItemInSlot)
2023-03-03 16:23:46 +01:00
{
// No item in slot, skip
continue;
}
2023-03-03 16:23:46 +01:00
let fuelRemaining = fuelItemInSlot.upd?.Resource?.Value;
if (fuelRemaining === 0)
{
// No fuel left, skip
continue;
}
// Undefined fuel, fresh fuel item and needs its max fuel amount looked up
if (!fuelRemaining)
{
const fuelItemTemplate = this.itemHelper.getItem(fuelItemInSlot._tpl)[1];
2024-02-17 22:55:43 +01:00
pointsConsumed = fuelUsedSinceLastTick;
fuelRemaining = fuelItemTemplate._props.MaxResource - fuelUsedSinceLastTick;
}
else
{
// Fuel exists already, deduct fuel from item remaining value
2024-02-17 22:55:43 +01:00
pointsConsumed = (fuelItemInSlot.upd.Resource.UnitsConsumed || 0) + fuelUsedSinceLastTick;
fuelRemaining -= fuelUsedSinceLastTick;
}
2023-03-03 16:23:46 +01:00
// Round values to keep accuracy
fuelRemaining = Math.round(fuelRemaining * 10000) / 10000;
pointsConsumed = Math.round(pointsConsumed * 10000) / 10000;
2023-03-03 16:23:46 +01:00
// Fuel consumed / 10 is over 1, add hideout management skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1);
pointsConsumed -= 10;
}
2023-03-03 16:23:46 +01:00
const isFuelItemFoundInRaid = fuelItemInSlot.upd?.SpawnedInSession ?? false;
if (fuelRemaining > 0)
{
2024-02-17 22:55:43 +01:00
// Deducted all used fuel from this container, clean up and exit loop
fuelItemInSlot.upd = this.getAreaUpdObject(1, fuelRemaining, pointsConsumed, isFuelItemFoundInRaid);
2023-04-26 23:29:08 +02:00
this.logger.debug(
`Profile: ${pmcData._id} Generator has: ${fuelRemaining} fuel left in slot ${i + 1}`,
true,
);
hasFuelRemaining = true;
2023-03-03 16:23:46 +01:00
2024-02-17 22:55:43 +01:00
break; // Break to avoid updating all the fuel tanks
2023-03-03 16:23:46 +01:00
}
fuelItemInSlot.upd = this.getAreaUpdObject(1, 0, 0, isFuelItemFoundInRaid);
2024-02-17 22:55:43 +01:00
// Ran out of fuel items to deduct fuel from
fuelUsedSinceLastTick = Math.abs(fuelRemaining);
this.logger.debug(`Profile: ${pmcData._id} Generator ran out of fuel`, true);
2023-03-03 16:23:46 +01:00
}
// Out of fuel, flag generator as offline
2023-03-03 16:23:46 +01:00
if (!hasFuelRemaining)
{
generatorArea.active = false;
}
}
2023-11-16 02:35:05 +01:00
protected updateWaterCollector(
sessionId: string,
pmcData: IPmcData,
area: HideoutArea,
hideoutProperties: { btcFarmCGs: number, isGeneratorOn: boolean, waterCollectorHasFilter: boolean },
2023-11-16 02:35:05 +01:00
): void
{
// Skip water collector when not level 3 (cant collect until 3)
if (area.level !== 3)
{
return;
}
if (!hideoutProperties.waterCollectorHasFilter)
{
return;
}
// Canister with purified water craft exists
const purifiedWaterCraft = pmcData.Hideout.Production[HideoutHelper.waterCollector];
if (purifiedWaterCraft && this.isProduction(purifiedWaterCraft))
{
// Update craft time to account for increases in players craft time skill
purifiedWaterCraft.ProductionTime = this.getAdjustedCraftTimeWithSkills(
pmcData,
purifiedWaterCraft.RecipeId,
true,
);
this.updateWaterFilters(area, purifiedWaterCraft, hideoutProperties.isGeneratorOn, pmcData);
}
else
{
// continuousProductionStart()
// seem to not trigger consistently
const recipe: IHideoutSingleProductionStartRequestData = {
recipeId: HideoutHelper.waterCollector,
Action: "HideoutSingleProductionStart",
items: [],
tools: [],
2023-11-16 02:35:05 +01:00
timestamp: this.timeUtil.getTimestamp(),
};
this.registerProduction(pmcData, recipe, sessionId);
}
}
/**
* Get craft time and make adjustments to account for dev profile + crafting skill level
* @param pmcData Player profile making craft
* @param recipeId Recipe being crafted
* @param applyHideoutManagementBonus should the hideout mgmt bonus be appled to the calculation
* @returns Items craft time with bonuses subtracted
*/
public getAdjustedCraftTimeWithSkills(
pmcData: IPmcData,
recipeId: string,
applyHideoutManagementBonus = false,
): number
{
const globalSkillsDb = this.databaseService.getGlobals().config.SkillsSettings;
const recipe = this.databaseService.getHideout().production
.find((production) => production._id === recipeId);
if (!recipe)
{
this.logger.error(this.localisationService.getText("hideout-missing_recipe_in_db", recipeId));
return undefined;
}
let timeReductionSeconds = 0;
// Bitcoin farm is excluded from crafting skill cooldown reduction
if (recipeId !== HideoutHelper.bitcoinFarm)
{
// Seconds to deduct from crafts total time
timeReductionSeconds += this.getSkillProductionTimeReduction(
pmcData,
recipe.productionTime,
SkillTypes.CRAFTING,
globalSkillsDb.Crafting.ProductionTimeReductionPerLevel,
);
}
// Some crafts take into account hideout management, e.g. fuel, water/air filters
if (applyHideoutManagementBonus)
{
timeReductionSeconds += this.getSkillProductionTimeReduction(
pmcData,
recipe.productionTime,
SkillTypes.HIDEOUT_MANAGEMENT,
globalSkillsDb.HideoutManagement.ConsumptionReductionPerLevel,
);
}
let modifiedProductionTime = recipe.productionTime - timeReductionSeconds;
if (modifiedProductionTime > 0 && this.profileHelper.isDeveloperAccount(pmcData._id))
{
modifiedProductionTime = 40;
}
return modifiedProductionTime;
}
2023-03-03 16:23:46 +01:00
/**
* 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
*/
2023-11-16 02:35:05 +01:00
protected updateWaterFilters(
waterFilterArea: HideoutArea,
production: Production,
isGeneratorOn: boolean,
pmcData: IPmcData,
): void
2023-03-03 16:23:46 +01:00
{
2023-04-23 22:23:44 +02:00
let filterDrainRate = this.getWaterFilterDrainRate(pmcData);
const productionTime = this.getTotalProductionTimeSeconds(HideoutHelper.waterCollector);
const secondsSinceServerTick = this.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
2023-11-16 02:35:05 +01:00
filterDrainRate = this.getTimeAdjustedWaterFilterDrainRate(
2023-11-16 02:35:05 +01:00
secondsSinceServerTick,
productionTime,
production.Progress,
filterDrainRate,
);
2023-03-03 16:23:46 +01:00
// Production hasn't completed
2023-04-23 22:23:44 +02:00
let pointsConsumed = 0;
2023-03-03 16:23:46 +01:00
if (production.Progress < productionTime)
{
// Check all slots that take water filters until we find one with filter in it
2023-03-03 16:23:46 +01:00
for (let i = 0; i < waterFilterArea.slots.length; i++)
{
// No water filter, skip
if (!waterFilterArea.slots[i].item)
2023-03-03 16:23:46 +01:00
{
continue;
}
const waterFilterItemInSlot = waterFilterArea.slots[i].item[0];
// How many units of filter are left
let resourceValue = waterFilterItemInSlot.upd?.Resource
? waterFilterItemInSlot.upd.Resource.Value
: undefined;
if (!resourceValue)
{
// Missing, is new filter, add default and subtract usage
resourceValue = 100 - filterDrainRate;
pointsConsumed = filterDrainRate;
}
else
{
pointsConsumed = (waterFilterItemInSlot.upd.Resource.UnitsConsumed || 0) + filterDrainRate;
resourceValue -= filterDrainRate;
}
2023-03-03 16:23:46 +01:00
// Round to get values to 3dp
resourceValue = Math.round(resourceValue * 1000) / 1000;
pointsConsumed = Math.round(pointsConsumed * 1000) / 1000;
2023-03-03 16:23:46 +01:00
// Check units consumed for possible increment of hideout mgmt skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1);
pointsConsumed -= 10;
}
// Filter has some fuel left in it after our adjustment
if (resourceValue > 0)
{
const isWaterFilterFoundInRaid = waterFilterItemInSlot.upd.SpawnedInSession ?? false;
// Set filters consumed amount
waterFilterItemInSlot.upd = this.getAreaUpdObject(
1,
resourceValue,
pointsConsumed,
isWaterFilterFoundInRaid,
);
this.logger.debug(`Water filter has: ${resourceValue} units left in slot ${i + 1}`);
2023-04-23 22:23:44 +02:00
break; // Break here to avoid iterating other filters now w're done
2023-03-03 16:23:46 +01:00
}
// Filter ran out / used up
delete waterFilterArea.slots[i].item;
// Update remaining resources to be subtracted
filterDrainRate = Math.abs(resourceValue);
2023-03-03 16:23:46 +01:00
}
}
}
/**
2023-11-16 02:35:05 +01:00
* Get an adjusted water filter drain rate based on time elapsed since last run,
* handle edge case when craft time has gone on longer than total production time
* @param secondsSinceServerTick Time passed
* @param totalProductionTime Total time collecting water
2023-11-16 02:35:05 +01:00
* @param productionProgress how far water collector has progressed
* @param baseFilterDrainRate Base drain rate
* @returns drain rate (adjusted)
*/
protected getTimeAdjustedWaterFilterDrainRate(
2023-11-16 02:35:05 +01:00
secondsSinceServerTick: number,
totalProductionTime: number,
productionProgress: number,
baseFilterDrainRate: number,
): number
{
const drainTimeSeconds
= secondsSinceServerTick > totalProductionTime
? totalProductionTime - productionProgress // More time passed than prod time, get total minus the current progress
: secondsSinceServerTick;
// Multiply base drain rate by time passed
return baseFilterDrainRate * drainTimeSeconds;
}
2023-04-23 22:23:44 +02:00
/**
* Get the water filter drain rate based on hideout bonues player has
* @param pmcData Player profile
* @returns Drain rate
*/
protected getWaterFilterDrainRate(pmcData: IPmcData): number
{
const globalSkillsDb = this.databaseService.getGlobals().config.SkillsSettings;
2023-04-23 22:23:44 +02:00
// 100 resources last 8 hrs 20 min, 100/8.33/60/60 = 0.00333
const filterDrainRate = 0.00333;
const hideoutManagementConsumptionBonus = this.getSkillBonusMultipliedBySkillLevel(
pmcData,
SkillTypes.HIDEOUT_MANAGEMENT,
globalSkillsDb.HideoutManagement.ConsumptionReductionPerLevel,
);
const craftSkillTimeReductionMultipler = this.getSkillBonusMultipliedBySkillLevel(
pmcData,
SkillTypes.CRAFTING,
globalSkillsDb.Crafting.CraftTimeReductionPerLevel,
);
// Never let bonus become 0
const reductionBonus
= hideoutManagementConsumptionBonus + craftSkillTimeReductionMultipler === 0
? 1
: 1 - (hideoutManagementConsumptionBonus + craftSkillTimeReductionMultipler);
return filterDrainRate * reductionBonus;
2023-04-23 22:23:44 +02:00
}
2023-04-23 15:02:46 +02:00
2023-04-23 22:23:44 +02:00
/**
* Get the production time in seconds for the desired production
* @param prodId Id, e.g. Water collector id
* @returns seconds to produce item
*/
protected getTotalProductionTimeSeconds(prodId: string): number
2023-04-23 22:23:44 +02:00
{
const recipe = this.databaseService.getHideout().production.find((prod) => prod._id === prodId);
2023-04-26 23:29:08 +02:00
return recipe.productionTime || 0;
2023-04-23 22:23:44 +02:00
}
2023-04-23 15:02:46 +02:00
/**
* Create a upd object using passed in parameters
2023-11-16 02:35:05 +01:00
* @param stackCount
* @param resourceValue
* @param resourceUnitsConsumed
* @returns Upd
*/
protected getAreaUpdObject(
stackCount: number,
resourceValue: number,
resourceUnitsConsumed: number,
isFoundInRaid: boolean,
): Upd
2023-03-03 16:23:46 +01:00
{
return {
StackObjectsCount: stackCount,
2023-11-16 02:35:05 +01:00
Resource: { Value: resourceValue, UnitsConsumed: resourceUnitsConsumed },
SpawnedInSession: isFoundInRaid,
2023-03-03 16:23:46 +01:00
};
}
protected updateAirFilters(airFilterArea: HideoutArea, pmcData: IPmcData, isGeneratorOn: boolean): void
2023-03-03 16:23:46 +01:00
{
// 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.databaseService.getHideout().settings.airFilterUnitFlowRate
* this.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
2023-03-03 16:23:46 +01:00
// 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
2023-03-03 16:23:46 +01:00
? airFilterArea.slots[i].item[0].upd.Resource.Value
: undefined;
2023-03-03 16:23:46 +01:00
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;
2023-11-16 02:35:05 +01:00
// check unit consumed for increment skill point
2023-03-03 16:23:46 +01:00
if (pmcData && Math.floor(pointsConsumed / 10) >= 1)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1);
2023-03-03 16:23:46 +01:00
pointsConsumed -= 10;
}
if (resourceValue > 0)
{
airFilterArea.slots[i].item[0].upd = {
StackObjectsCount: 1,
2023-11-16 02:35:05 +01:00
Resource: { Value: resourceValue, UnitsConsumed: pointsConsumed },
2023-03-03 16:23:46 +01:00
};
this.logger.debug(`Air filter: ${resourceValue} filter left on slot ${i + 1}`);
break; // Break here to avoid updating all filters
}
delete airFilterArea.slots[i].item;
// Update remaining resources to be subtracted
filterDrainRate = Math.abs(resourceValue);
2023-03-03 16:23:46 +01:00
}
}
}
2023-11-16 02:35:05 +01:00
protected updateBitcoinFarm(pmcData: IPmcData, btcFarmCGs: number, isGeneratorOn: boolean): Production | undefined
2023-03-03 16:23:46 +01:00
{
const btcProd = pmcData.Hideout.Production[HideoutHelper.bitcoinFarm];
const bitcoinProdData = this.databaseService.getHideout().production
.find((production) => production._id === HideoutHelper.bitcoinProductionId);
2023-03-03 16:23:46 +01:00
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;
2023-04-26 23:29:08 +02:00
2023-03-03 16:23:46 +01:00
return btcProd;
}
if (this.isProduction(btcProd))
{
// 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
2023-04-23 15:02:46 +02:00
{
get
{
return this.int_1;
}
protected set
{
if (this.int_1 === value)
2023-03-03 16:23:46 +01:00
{
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 adjustedCraftTime
= (this.profileHelper.isDeveloperAccount(pmcData.sessionId) ? 40 : bitcoinProdData.productionTime)
/ (1 + (btcFarmCGs - 1) * this.databaseService.getHideout().settings.gpuBoostRate);
Fix bitcoin desync between client/server while offline (!225) The server expects that the total "Progress" when a bitcoin is complete to be the "ProductionTime" value (145000 by default). However the server was using a modified target value, while only adding the actual time change to the Progress. This results in a drift over time between the client/server while the server is stopped, as the client gets an incorrect value on startup. If we instead scale the addition to Progress based on the adjusted craft time, and target the base productionTime for completion, we can get a much more accurate progress while offline that matches the client I used the profile from this ticket for testing: https://dev.sp-tarkov.com/SPT-AKI/Issues/issues/496 The user has both an upgrade and bitcoin going at the same time, so they should progress at the same rate. This is what the code was previously resulting in: ![image](/attachments/fe428a3b-d271-40e1-a3f6-08ef936224b6) While the server was shut down for 50 minutes (As noted by the upgrade), only 36 minutes was deducted from the bitcoin craft. This is the result of that same profile after these changes were made: ![image](/attachments/d2ce44e6-1a0e-4991-aa51-3eb340c22ca5) You can see that ~2 hours 25 minutes was deducted from both the upgrade, as well as the bitcoin craft timer. There is still a slight discrepancy, but in a total bitcoin run it should be minimal Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/225 Co-authored-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com> Co-committed-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
2024-02-15 09:57:26 +01:00
// The progress should be adjusted based on the GPU boost rate, but the target is still the base productionTime
const timeMultiplier = bitcoinProdData.productionTime / adjustedCraftTime;
const timeElapsedSeconds = this.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
btcProd.Progress += Math.floor(timeElapsedSeconds * timeMultiplier);
while (btcProd.Progress >= bitcoinProdData.productionTime)
2023-03-03 16:23:46 +01:00
{
if (btcProd.Products.length < coinSlotCount)
{
2023-04-26 23:29:08 +02:00
// Has space to add a coin to production
Fix bitcoin desync between client/server while offline (!225) The server expects that the total "Progress" when a bitcoin is complete to be the "ProductionTime" value (145000 by default). However the server was using a modified target value, while only adding the actual time change to the Progress. This results in a drift over time between the client/server while the server is stopped, as the client gets an incorrect value on startup. If we instead scale the addition to Progress based on the adjusted craft time, and target the base productionTime for completion, we can get a much more accurate progress while offline that matches the client I used the profile from this ticket for testing: https://dev.sp-tarkov.com/SPT-AKI/Issues/issues/496 The user has both an upgrade and bitcoin going at the same time, so they should progress at the same rate. This is what the code was previously resulting in: ![image](/attachments/fe428a3b-d271-40e1-a3f6-08ef936224b6) While the server was shut down for 50 minutes (As noted by the upgrade), only 36 minutes was deducted from the bitcoin craft. This is the result of that same profile after these changes were made: ![image](/attachments/d2ce44e6-1a0e-4991-aa51-3eb340c22ca5) You can see that ~2 hours 25 minutes was deducted from both the upgrade, as well as the bitcoin craft timer. There is still a slight discrepancy, but in a total bitcoin run it should be minimal Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/225 Co-authored-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com> Co-committed-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
2024-02-15 09:57:26 +01:00
this.addBtcToProduction(btcProd, bitcoinProdData.productionTime);
2023-03-03 16:23:46 +01:00
}
else
{
// Filled up bitcoin storage
2023-03-03 16:23:46 +01:00
btcProd.Progress = 0;
}
}
btcProd.StartTimestamp = this.timeUtil.getTimestamp();
return btcProd;
}
return undefined;
2023-03-03 16:23:46 +01:00
}
2023-04-26 23:29:08 +02:00
/**
* Add bitcoin object to btc production products array and set progress time
* @param btcProd Bitcoin production object
* @param coinCraftTimeSeconds Time to craft a bitcoin
*/
protected addBtcToProduction(btcProd: Production, coinCraftTimeSeconds: number): void
{
btcProd.Products.push({
_id: this.hashUtil.generate(),
_tpl: HideoutHelper.bitcoinTpl,
2023-11-16 02:35:05 +01:00
upd: { StackObjectsCount: 1 },
2023-04-26 23:29:08 +02:00
});
// Deduct time spent crafting from progress
2023-04-26 23:29:08 +02:00
btcProd.Progress -= coinCraftTimeSeconds;
}
/**
* Get number of ticks that have passed since hideout areas were last processed, reduced when generator is off
* @param pmcData Player profile
* @param isGeneratorOn Is the generator on for the duration of elapsed time
* @param recipe Hideout production recipe being crafted we need the ticks for
* @returns Amount of time elapsed in seconds
*/
2023-11-16 02:35:05 +01:00
protected getTimeElapsedSinceLastServerTick(
pmcData: IPmcData,
isGeneratorOn: boolean,
recipe?: IHideoutProduction,
2023-11-16 02:35:05 +01:00
): number
{
// Reduce time elapsed (and progress) when generator is off
let timeElapsed = this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp;
2023-11-16 02:35:05 +01:00
if (recipe?.areaType === HideoutAreas.LAVATORY)
{
// Lavatory works at 100% when power is on / off
return timeElapsed;
}
if (!isGeneratorOn)
{
timeElapsed *= this.databaseService.getHideout().settings.generatorSpeedWithoutFuel;
}
return timeElapsed;
}
2023-03-03 16:23:46 +01:00
/**
* Get a count of how many possible BTC can be gathered by the profile
2023-03-03 16:23:46 +01:00
* @param pmcData Profile to look up
* @returns Coin slot count
2023-03-03 16:23:46 +01:00
*/
protected getBTCSlots(pmcData: IPmcData): number
{
const bitcoinProductions = this.databaseService.getHideout().production
.find((production) => production._id === HideoutHelper.bitcoinFarm);
const productionSlots = bitcoinProductions?.productionLimitCount || 3; // Default to 3 if none found
const hasManagementSkillSlots = this.profileHelper.hasEliteSkillLevel(SkillTypes.HIDEOUT_MANAGEMENT, pmcData);
const managementSlotsCount = this.getEliteSkillAdditionalBitcoinSlotCount() || 2;
2023-03-03 16:23:46 +01:00
return productionSlots + (hasManagementSkillSlots ? managementSlotsCount : 0);
}
/**
* Get a count of how many additional bitcoins player hideout can hold with elite skill
*/
protected getEliteSkillAdditionalBitcoinSlotCount(): number
{
return this.databaseService.getGlobals().config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm
2023-11-16 02:35:05 +01:00
.Container;
}
/**
* HideoutManagement skill gives a consumption bonus the higher the level
* 0.5% per level per 1-51, (25.5% at max)
* @param pmcData Profile to get hideout consumption level level from
* @returns consumption bonus
*/
2023-03-03 16:23:46 +01:00
protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number
{
const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT);
if (!hideoutManagementSkill || hideoutManagementSkill.Progress === 0)
2023-03-03 16:23:46 +01:00
{
return 0;
}
2023-03-03 16:23:46 +01:00
// 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
let roundedLevel = Math.floor(hideoutManagementSkill.Progress / 100);
roundedLevel = roundedLevel === 51 ? roundedLevel - 1 : roundedLevel;
2023-03-03 16:23:46 +01:00
return (
(roundedLevel
* this.databaseService.getGlobals().config.SkillsSettings.HideoutManagement
.ConsumptionReductionPerLevel)
/ 100
);
2023-03-03 16:23:46 +01:00
}
/**
* Get a multipler based on players skill level and value per level
2023-03-03 16:23:46 +01:00
* @param pmcData Player profile
* @param skill Player skill from profile
* @param valuePerLevel Value from globals.config.SkillsSettings - `PerLevel`
* @returns Multipler from 0 to 1
2023-03-03 16:23:46 +01:00
*/
protected getSkillBonusMultipliedBySkillLevel(pmcData: IPmcData, skill: SkillTypes, valuePerLevel: number): number
2023-03-03 16:23:46 +01:00
{
const profileSkill = this.profileHelper.getSkillFromProfile(pmcData, skill);
if (!profileSkill || profileSkill.Progress === 0)
2023-03-03 16:23:46 +01:00
{
return 0;
2023-03-03 16:23:46 +01:00
}
// 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
let roundedLevel = Math.floor(profileSkill.Progress / 100);
roundedLevel = roundedLevel === 51 ? roundedLevel - 1 : roundedLevel;
return (roundedLevel * valuePerLevel) / 100;
}
/**
* @param pmcData Player profile
* @param productionTime Time to complete hideout craft in seconds
* @param skill Skill bonus to get reduction from
* @param amountPerLevel Skill bonus amount to apply
* @returns Seconds to reduce craft time by
*/
public getSkillProductionTimeReduction(
pmcData: IPmcData,
productionTime: number,
skill: SkillTypes,
amountPerLevel: number,
): number
{
const skillTimeReductionMultipler = this.getSkillBonusMultipliedBySkillLevel(pmcData, skill, amountPerLevel);
return productionTime * skillTimeReductionMultipler;
2023-03-03 16:23:46 +01:00
}
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
* @param output Output object to update
2023-03-03 16:23:46 +01:00
*/
2023-11-16 02:35:05 +01:00
public getBTC(
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
sessionId: string,
output: IItemEventRouterResponse,
): void
2023-03-03 16:23:46 +01:00
{
// 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);
2023-11-16 02:35:05 +01:00
this.httpResponse.appendErrorToOutput(output, errorMsg);
return;
2023-03-03 16:23:46 +01:00
}
const itemsToAdd: Item[][] = [];
for (let index = 0; index < craftedCoinCount; index++)
{
itemsToAdd.push([
{
_id: this.hashUtil.generate(),
_tpl: HideoutHelper.bitcoinTpl,
upd: { StackObjectsCount: 1 },
},
]);
}
// Create request for what we want to add to stash
const addItemsRequest: IAddItemsDirectRequest = {
itemsWithModsToAdd: itemsToAdd,
foundInRaid: true,
useSortingTable: false,
callback: undefined,
};
// Add FiR coins to player inventory
this.inventoryHelper.addItemsToStash(sessionId, addItemsRequest, pmcData, output);
if (output.warnings.length > 0)
{
return;
}
2023-03-03 16:23:46 +01:00
// Is at max capacity + we collected all coins - reset production start time
2023-03-03 16:23:46 +01:00
const coinSlotCount = this.getBTCSlots(pmcData);
if (pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length >= coinSlotCount)
{
// Set start to now
pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].StartTimestamp = this.timeUtil.getTimestamp();
}
2023-03-03 16:23:46 +01:00
// Remove crafted coins from production in profile now they've been collected
// Can only collect all coins, not individially
pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products = [];
2023-04-26 23:29:08 +02:00
}
2023-03-03 16:23:46 +01:00
/**
* Upgrade hideout wall from starting level to interactable level if necessary stations have been upgraded
2023-03-03 16:23:46 +01:00
* @param pmcProfile Profile to upgrade wall in
*/
public unlockHideoutWallInProfile(pmcProfile: IPmcData): void
{
const waterCollector = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WATER_COLLECTOR);
const medStation = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.MEDSTATION);
const wall = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.EMERGENCY_WALL);
// No collector or med station, skip
if (!(waterCollector && medStation))
{
return;
}
// If medstation > level 1 AND water collector > level 1 AND wall is level 0
if (waterCollector?.level >= 1 && medStation?.level >= 1 && wall?.level <= 0)
{
wall.level = 3;
2023-03-03 16:23:46 +01:00
}
}
/**
* Hideout improvement is flagged as complete
* @param improvement hideout improvement object
* @returns true if complete
*/
protected hideoutImprovementIsComplete(improvement: IHideoutImprovement): boolean
{
2023-11-16 02:35:05 +01:00
return improvement?.completed ? true : false;
2023-03-03 16:23:46 +01:00
}
/**
* 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.Improvement)
2023-03-03 16:23:46 +01:00
{
const improvementDetails = pmcProfile.Hideout.Improvement[improvementId];
2023-11-16 02:35:05 +01:00
if (
improvementDetails.completed === false
&& improvementDetails.improveCompleteTimestamp < this.timeUtil.getTimestamp()
)
2023-03-03 16:23:46 +01:00
{
improvementDetails.completed = true;
}
}
}
/**
* Add/remove bonus combat skill based on number of dogtags in place of fame hideout area
* @param pmcData Player profile
*/
public applyPlaceOfFameDogtagBonus(pmcData: IPmcData): void
{
const fameAreaProfile = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.PLACE_OF_FAME);
// Get hideout area 16 bonus array
const fameAreaDb = this.databaseService.getHideout().areas
.find((area) => area.type === HideoutAreas.PLACE_OF_FAME);
// Get SkillGroupLevelingBoost object
const combatBoostBonusDb = fameAreaDb.stages[fameAreaProfile.level].bonuses.find(
(bonus) => bonus.type === "SkillGroupLevelingBoost",
);
// Get SkillGroupLevelingBoost object in profile
const combatBonusProfile = pmcData.Bonuses.find((bonus) => bonus.id === combatBoostBonusDb.id);
// Get all slotted dogtag items
const activeDogtags = pmcData.Inventory.items.filter((item) => item?.slotId?.startsWith("dogtag"));
// Calculate bonus percent (apply hideoutManagement bonus)
const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT);
const hideoutManagementSkillBonusPercent = 1 + hideoutManagementSkill.Progress / 10000; // 5100 becomes 0.51, add 1 to it, 1.51
const bonus
= this.getDogtagCombatSkillBonusPercent(pmcData, activeDogtags) * hideoutManagementSkillBonusPercent;
// Update bonus value to above calcualted value
combatBonusProfile.value = Number.parseFloat(bonus.toFixed(2));
}
/**
* Calculate the raw dogtag combat skill bonus for place of fame based on number of dogtags
* Reverse engineered from client code
* @param pmcData Player profile
* @param activeDogtags Active dogtags in place of fame dogtag slots
* @returns combat bonus
*/
protected getDogtagCombatSkillBonusPercent(pmcData: IPmcData, activeDogtags: Item[]): number
{
// Not own dogtag
// Side = opposite of player
let result = 0;
for (const dogtag of activeDogtags)
{
if (!dogtag.upd.Dogtag)
{
continue;
}
if (Number.parseInt(dogtag.upd.Dogtag?.AccountId) === pmcData.aid)
{
continue;
}
result += 0.01 * dogtag.upd.Dogtag.Level;
}
return result;
}
2023-11-16 02:35:05 +01:00
}