b34edb0d3b
Bundle up profile save time and display in one line + do not log to file Don't log when each profile is saved Do not log to file how much fuel is left in generator
1140 lines
45 KiB
TypeScript
1140 lines
45 KiB
TypeScript
import { inject, injectable } from "tsyringe";
|
|
|
|
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper";
|
|
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
|
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
|
|
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
|
|
import { HideoutArea, IHideoutImprovement, Production, Productive } from "@spt-aki/models/eft/common/tables/IBotBase";
|
|
import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem";
|
|
import { StageBonus } from "@spt-aki/models/eft/hideout/IHideoutArea";
|
|
import { IHideoutContinuousProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutContinuousProductionStartRequestData";
|
|
import { IHideoutProduction } from "@spt-aki/models/eft/hideout/IHideoutProduction";
|
|
import { IHideoutSingleProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutSingleProductionStartRequestData";
|
|
import { IHideoutTakeProductionRequestData } from "@spt-aki/models/eft/hideout/IHideoutTakeProductionRequestData";
|
|
import { IAddItemsDirectRequest } from "@spt-aki/models/eft/inventory/IAddItemsDirectRequest";
|
|
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
|
|
import { BonusType } from "@spt-aki/models/enums/BonusType";
|
|
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
|
|
import { HideoutAreas } from "@spt-aki/models/enums/HideoutAreas";
|
|
import { SkillTypes } from "@spt-aki/models/enums/SkillTypes";
|
|
import { IHideoutConfig } from "@spt-aki/models/spt/config/IHideoutConfig";
|
|
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
|
|
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
|
|
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
|
|
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
|
|
import { LocalisationService } from "@spt-aki/services/LocalisationService";
|
|
import { PlayerService } from "@spt-aki/services/PlayerService";
|
|
import { HashUtil } from "@spt-aki/utils/HashUtil";
|
|
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
|
|
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
|
|
|
|
@injectable()
|
|
export class HideoutHelper
|
|
{
|
|
public static bitcoinFarm = "5d5c205bd582a50d042a3c0e";
|
|
public static bitcoinProductionId = "5d5c205bd582a50d042a3c0e";
|
|
public static waterCollector = "5d5589c1f934db045e6c5492";
|
|
public static bitcoinTpl = "59faff1d86f7746c51718c9c";
|
|
public static expeditionaryFuelTank = "5d1b371186f774253763a656";
|
|
public static maxSkillPoint = 5000;
|
|
|
|
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("ItemHelper") protected itemHelper: ItemHelper,
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
|
)
|
|
{
|
|
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
|
|
*/
|
|
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));
|
|
}
|
|
|
|
let 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"
|
|
if (!pmcData.Hideout.Production)
|
|
{
|
|
pmcData.Hideout.Production = {};
|
|
}
|
|
|
|
if (modifiedProductionTime > 0 && this.profileHelper.isDeveloperAccount(sessionID))
|
|
{
|
|
modifiedProductionTime = 40;
|
|
}
|
|
|
|
pmcData.Hideout.Production[body.recipeId] = this.initProduction(
|
|
body.recipeId,
|
|
modifiedProductionTime,
|
|
recipe.needFuelForAllProductionTime,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This convenience function initializes new Production Object
|
|
* with all the constants.
|
|
*/
|
|
public initProduction(recipeId: string, productionTime: number, needFuelForAllProductionTime: boolean): Production
|
|
{
|
|
return {
|
|
Progress: 0,
|
|
inProgress: true,
|
|
RecipeId: recipeId,
|
|
StartTimestamp: this.timeUtil.getTimestamp(),
|
|
ProductionTime: productionTime,
|
|
Products: [],
|
|
GivenItemsInStart: [],
|
|
Interrupted: false,
|
|
NeedFuelForAllProductionTime: needFuelForAllProductionTime, // Used when sending to client
|
|
needFuelForAllProductionTime: needFuelForAllProductionTime, // used when stored in production.json
|
|
SkipTime: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Is the provided object a Production type
|
|
* @param productive
|
|
* @returns
|
|
*/
|
|
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
|
|
*/
|
|
public applyPlayerUpgradesBonuses(pmcData: IPmcData, bonus: StageBonus): void
|
|
{
|
|
// Handle additional changes some bonuses need before being added
|
|
switch (bonus.type)
|
|
{
|
|
case BonusType.STASH_SIZE:
|
|
{
|
|
// 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)
|
|
{
|
|
this.logger.warning(
|
|
`Unable to apply StashSize bonus, stash with id: ${pmcData.Inventory.stash} not found`,
|
|
);
|
|
}
|
|
|
|
stashItem._tpl = bonus.templateId;
|
|
break;
|
|
}
|
|
case BonusType.MAXIMUM_ENERGY_RESERVE:
|
|
// Amend max energy in profile
|
|
pmcData.Health.Energy.Maximum += bonus.value;
|
|
break;
|
|
case BonusType.TEXT_BONUS:
|
|
// Delete values before they're added to profile
|
|
delete bonus.passive;
|
|
delete bonus.production;
|
|
delete bonus.visible;
|
|
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 ?? ""}`);
|
|
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);
|
|
|
|
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((x) => x.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((x) => x.type === HideoutAreas.GENERATOR)?.active ?? false,
|
|
waterCollectorHasFilter: this.doesWaterCollectorHaveFilter(
|
|
pmcData.Hideout.Areas.find((x) => x.type === HideoutAreas.WATER_COLLECTOR),
|
|
),
|
|
};
|
|
|
|
return hideoutProperties;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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];
|
|
if (!craft)
|
|
{
|
|
// Craft value is null, get rid of it (could be from cancelling craft that needs cleaning up)
|
|
delete pmcData.Hideout.Production[prodId];
|
|
|
|
continue;
|
|
}
|
|
|
|
if (craft.Progress === undefined || craft.Progress === null)
|
|
{
|
|
this.logger.warning(`Craft ${prodId} has an undefined progress value, defaulting to 0`);
|
|
craft.Progress = 0;
|
|
}
|
|
|
|
// 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 last hideout update + now
|
|
const timeElapsed = this.getTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.isGeneratorOn, recipe);
|
|
|
|
// Increment progress by time passed
|
|
const production = pmcData.Hideout.Production[prodId];
|
|
production.Progress += (production.needFuelForAllProductionTime && !hideoutProperties.isGeneratorOn)
|
|
? 0
|
|
: timeElapsed; // Some items NEED power to craft (e.g. DSP)
|
|
|
|
// Limit progress to total production time if progress is over (dont run for continious crafts))
|
|
if (!recipe.continuous)
|
|
{
|
|
// If progress is larger than prod time, return ProductionTime, hard cap the vaue
|
|
production.Progress = Math.min(production.Progress, production.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:
|
|
if (hideoutProperties.isGeneratorOn)
|
|
{
|
|
this.updateFuel(area, pmcData, hideoutProperties.isGeneratorOn);
|
|
}
|
|
break;
|
|
case HideoutAreas.WATER_COLLECTOR:
|
|
this.updateWaterCollector(sessionID, pmcData, area, hideoutProperties.isGeneratorOn);
|
|
break;
|
|
|
|
case HideoutAreas.AIR_FILTERING:
|
|
if (hideoutProperties.isGeneratorOn)
|
|
{
|
|
this.updateAirFilters(area, pmcData, hideoutProperties.isGeneratorOn);
|
|
}
|
|
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
|
|
{
|
|
// 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.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
|
|
const fuelBonus = pmcData.Bonuses.find((bonus) => bonus.type === BonusType.FUEL_CONSUMPTION);
|
|
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)
|
|
{
|
|
// No item in slot, skip
|
|
continue;
|
|
}
|
|
|
|
const fuelItemInSlot = generatorArea.slots[i]?.item[0];
|
|
if (!fuelItemInSlot)
|
|
{
|
|
// No item in slot, skip
|
|
continue;
|
|
}
|
|
|
|
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];
|
|
pointsConsumed = fuelDrainRate;
|
|
fuelRemaining = fuelItemTemplate._props.MaxResource - fuelDrainRate;
|
|
}
|
|
else
|
|
{
|
|
// Fuel exists already, deduct fuel from item remaining value
|
|
pointsConsumed = (fuelItemInSlot.upd.Resource.UnitsConsumed || 0) + fuelDrainRate;
|
|
fuelRemaining -= fuelDrainRate;
|
|
}
|
|
|
|
fuelRemaining = Math.round(fuelRemaining * 10000) / 10000;
|
|
pointsConsumed = Math.round(pointsConsumed * 10000) / 10000;
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (fuelRemaining > 0)
|
|
{
|
|
fuelItemInSlot.upd = this.getAreaUpdObject(1, fuelRemaining, pointsConsumed);
|
|
|
|
this.logger.debug(
|
|
`Profile: ${pmcData._id} Generator has: ${fuelRemaining} fuel left in slot ${i + 1}`,
|
|
true,
|
|
);
|
|
hasFuelRemaining = true;
|
|
|
|
break; // Break here to avoid updating all the fuel tanks
|
|
}
|
|
|
|
fuelItemInSlot.upd = this.getAreaUpdObject(1, 0, 0);
|
|
|
|
// Update remaining resources to be subtracted
|
|
fuelDrainRate = Math.abs(fuelRemaining);
|
|
}
|
|
|
|
// Out of fuel, flag generator as offline
|
|
if (!hasFuelRemaining)
|
|
{
|
|
generatorArea.active = false;
|
|
}
|
|
}
|
|
|
|
protected updateWaterCollector(
|
|
sessionId: string,
|
|
pmcData: IPmcData,
|
|
area: HideoutArea,
|
|
isGeneratorOn: boolean,
|
|
): void
|
|
{
|
|
// Skip water collector when not level 3 (cant collect until 3)
|
|
if (area.level !== 3)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Canister with purified water craft exists
|
|
const prod = pmcData.Hideout.Production[HideoutHelper.waterCollector];
|
|
if (prod && this.isProduction(prod))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
protected updateWaterFilters(
|
|
waterFilterArea: HideoutArea,
|
|
production: Production,
|
|
isGeneratorOn: boolean,
|
|
pmcData: IPmcData,
|
|
): void
|
|
{
|
|
let filterDrainRate = this.getWaterFilterDrainRate(pmcData);
|
|
const productionTime = this.getTotalProductionTimeSeconds(HideoutHelper.waterCollector);
|
|
const secondsSinceServerTick = this.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
|
|
filterDrainRate = this.adjustWaterFilterDrainRate(
|
|
secondsSinceServerTick,
|
|
productionTime,
|
|
production.Progress,
|
|
filterDrainRate,
|
|
);
|
|
|
|
// Production hasn't completed
|
|
let pointsConsumed = 0;
|
|
if (production.Progress < productionTime)
|
|
{
|
|
// Check all slots that take water filters
|
|
// Must loop to find first water filter and use that
|
|
for (let i = 0; i < waterFilterArea.slots.length; i++)
|
|
{
|
|
// Has a water filter installed into slot
|
|
if (waterFilterArea.slots[i].item)
|
|
{
|
|
// How many units of filter are left
|
|
let resourceValue = (waterFilterArea.slots[i].item[0].upd?.Resource)
|
|
? waterFilterArea.slots[i].item[0].upd.Resource.Value
|
|
: null;
|
|
if (!resourceValue)
|
|
{
|
|
// None left
|
|
resourceValue = 100 - filterDrainRate;
|
|
pointsConsumed = filterDrainRate;
|
|
}
|
|
else
|
|
{
|
|
pointsConsumed = (waterFilterArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0)
|
|
+ filterDrainRate;
|
|
resourceValue -= filterDrainRate;
|
|
}
|
|
|
|
// Round to get values to 3dp
|
|
resourceValue = Math.round(resourceValue * 10000) / 10000;
|
|
pointsConsumed = Math.round(pointsConsumed * 10000) / 10000;
|
|
|
|
// Check amount of 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 juice left in it after we adjusted it
|
|
if (resourceValue > 0)
|
|
{
|
|
// Set filter consumed amount
|
|
waterFilterArea.slots[i].item[0].upd = this.getAreaUpdObject(1, resourceValue, pointsConsumed);
|
|
this.logger.debug(`Water filter has: ${resourceValue} units left in slot ${i + 1}`);
|
|
|
|
break; // Break here to avoid updating all filters
|
|
}
|
|
|
|
// Filter ran out / used up
|
|
delete waterFilterArea.slots[i].item;
|
|
// Update remaining resources to be subtracted
|
|
filterDrainRate = Math.abs(resourceValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @param productionProgress how far water collector has progressed
|
|
* @param baseFilterDrainRate Base drain rate
|
|
* @returns
|
|
*/
|
|
protected adjustWaterFilterDrainRate(
|
|
secondsSinceServerTick: number,
|
|
totalProductionTime: number,
|
|
productionProgress: number,
|
|
baseFilterDrainRate: number,
|
|
): number
|
|
{
|
|
const drainRateMultiplier = secondsSinceServerTick > totalProductionTime
|
|
? (totalProductionTime - productionProgress) // more time passed than prod time, get total minus the current progress
|
|
: secondsSinceServerTick;
|
|
|
|
// Multiply drain rate by calculated multiplier
|
|
baseFilterDrainRate *= drainRateMultiplier;
|
|
|
|
return baseFilterDrainRate;
|
|
}
|
|
|
|
/**
|
|
* Get the water filter drain rate based on hideout bonues player has
|
|
* @param pmcData Player profile
|
|
* @returns Drain rate
|
|
*/
|
|
protected getWaterFilterDrainRate(pmcData: IPmcData): number
|
|
{
|
|
// 100 resources last 8 hrs 20 min, 100/8.33/60/60 = 0.00333
|
|
const filterDrainRate = 0.00333;
|
|
const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData);
|
|
|
|
return filterDrainRate * hideoutManagementConsumptionBonus;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
const recipe = this.databaseServer.getTables().hideout.production.find((prod) => prod._id === prodId);
|
|
|
|
return (recipe.productionTime || 0);
|
|
}
|
|
|
|
/**
|
|
* Create a upd object using passed in parameters
|
|
* @param stackCount
|
|
* @param resourceValue
|
|
* @param resourceUnitsConsumed
|
|
* @returns Upd
|
|
*/
|
|
protected getAreaUpdObject(stackCount: number, resourceValue: number, resourceUnitsConsumed: number): Upd
|
|
{
|
|
return {
|
|
StackObjectsCount: stackCount,
|
|
Resource: { Value: resourceValue, UnitsConsumed: resourceUnitsConsumed },
|
|
};
|
|
}
|
|
|
|
protected updateAirFilters(airFilterArea: HideoutArea, pmcData: IPmcData, isGeneratorOn: boolean): 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.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
|
|
// 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.profileHelper.addSkillPointsToPlayer(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
|
|
}
|
|
|
|
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((production) =>
|
|
production._id === HideoutHelper.bitcoinProductionId
|
|
);
|
|
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))
|
|
{
|
|
const timeElapsedSeconds = this.getTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
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 =
|
|
((this.profileHelper.isDeveloperAccount(pmcData.sessionId)) ? 40 : bitcoinProdData.productionTime)
|
|
/ (1 + (btcFarmCGs - 1) * this.databaseServer.getTables().hideout.settings.gpuBoostRate);
|
|
|
|
while (btcProd.Progress >= coinCraftTimeSeconds)
|
|
{
|
|
if (btcProd.Products.length < coinSlotCount)
|
|
{
|
|
// Has space to add a coin to production
|
|
this.addBtcToProduction(btcProd, coinCraftTimeSeconds);
|
|
}
|
|
else
|
|
{
|
|
// Filled up bitcoin storage
|
|
btcProd.Progress = 0;
|
|
}
|
|
}
|
|
|
|
btcProd.StartTimestamp = this.timeUtil.getTimestamp();
|
|
|
|
return btcProd;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
upd: { StackObjectsCount: 1 },
|
|
});
|
|
|
|
// Deduct time spent crafting from progress
|
|
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
|
|
*/
|
|
protected getTimeElapsedSinceLastServerTick(
|
|
pmcData: IPmcData,
|
|
isGeneratorOn: boolean,
|
|
recipe: IHideoutProduction = null,
|
|
): number
|
|
{
|
|
// Reduce time elapsed (and progress) when generator is off
|
|
let timeElapsed = this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp;
|
|
|
|
if (recipe?.areaType === HideoutAreas.LAVATORY)
|
|
{
|
|
// Lavatory works at 100% when power is on / off
|
|
return timeElapsed;
|
|
}
|
|
|
|
if (!isGeneratorOn)
|
|
{
|
|
timeElapsed *= this.databaseServer.getTables().hideout.settings.generatorSpeedWithoutFuel;
|
|
}
|
|
|
|
return timeElapsed;
|
|
}
|
|
|
|
/**
|
|
* Get a count of how many possible BTC can be gathered by the profile
|
|
* @param pmcData Profile to look up
|
|
* @returns Coin slot count
|
|
*/
|
|
protected getBTCSlots(pmcData: IPmcData): number
|
|
{
|
|
const bitcoinProductions = this.databaseServer.getTables().hideout.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;
|
|
|
|
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.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm
|
|
.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
|
|
*/
|
|
protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number
|
|
{
|
|
const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT);
|
|
if (!hideoutManagementSkill)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// 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;
|
|
|
|
return (roundedLevel
|
|
* this.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement
|
|
.ConsumptionReductionPerLevel) / 100;
|
|
}
|
|
|
|
/**
|
|
* 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 = pmcData.Skills.Common.find((x) => x.Id === SkillTypes.CRAFTING);
|
|
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
|
|
* @param output Output object to update
|
|
*/
|
|
public getBTC(
|
|
pmcData: IPmcData,
|
|
request: IHideoutTakeProductionRequestData,
|
|
sessionId: string,
|
|
output: IItemEventRouterResponse,
|
|
): void
|
|
{
|
|
// 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);
|
|
|
|
this.httpResponse.appendErrorToOutput(output, errorMsg);
|
|
|
|
return;
|
|
}
|
|
|
|
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: null,
|
|
};
|
|
|
|
// Add FiR coins to player inventory
|
|
this.inventoryHelper.addItemsToStash(sessionId, addItemsRequest, pmcData, output);
|
|
if (output.warnings.length > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Is at max capacity + we collected all coins - reset production start time
|
|
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();
|
|
}
|
|
|
|
// 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 = [];
|
|
}
|
|
|
|
/**
|
|
* Upgrade hideout wall from starting level to interactable level if necessary stations have been upgraded
|
|
* @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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hideout improvement is flagged as complete
|
|
* @param improvement hideout improvement object
|
|
* @returns true if complete
|
|
*/
|
|
protected hideoutImprovementIsComplete(improvement: IHideoutImprovement): boolean
|
|
{
|
|
return improvement?.completed ? true : 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.Improvement)
|
|
{
|
|
const improvementDetails = pmcProfile.Hideout.Improvement[improvementId];
|
|
if (
|
|
improvementDetails.completed === false
|
|
&& improvementDetails.improveCompleteTimestamp < this.timeUtil.getTimestamp()
|
|
)
|
|
{
|
|
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.databaseServer.getTables().hideout.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;
|
|
}
|
|
}
|