Server/project/src/controllers/HideoutController.ts
Dev 0166e30dd1 Reduce instances of IItemEventRouterResponse being passed into a function and then returned, its an object and passed by ref, no need to return it
Reduce instances of `IItemEventRouterResponse` being reassigned in a function

Rename `getMoney` to `giveProfileMoney`
2024-01-16 12:21:42 +00:00

1216 lines
46 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { ScavCaseRewardGenerator } from "@spt-aki/generators/ScavCaseRewardGenerator";
import { HideoutHelper } from "@spt-aki/helpers/HideoutHelper";
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper";
import { PaymentHelper } from "@spt-aki/helpers/PaymentHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { HideoutArea, Product, Production, ScavCase } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Upd } from "@spt-aki/models/eft/common/tables/IItem";
import { HideoutUpgradeCompleteRequestData } from "@spt-aki/models/eft/hideout/HideoutUpgradeCompleteRequestData";
import { IHandleQTEEventRequestData } from "@spt-aki/models/eft/hideout/IHandleQTEEventRequestData";
import { IHideoutArea, Stage } from "@spt-aki/models/eft/hideout/IHideoutArea";
import { IHideoutCancelProductionRequestData } from "@spt-aki/models/eft/hideout/IHideoutCancelProductionRequestData";
import { IHideoutContinuousProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutContinuousProductionStartRequestData";
import { IHideoutImproveAreaRequestData } from "@spt-aki/models/eft/hideout/IHideoutImproveAreaRequestData";
import { IHideoutProduction } from "@spt-aki/models/eft/hideout/IHideoutProduction";
import { IHideoutPutItemInRequestData } from "@spt-aki/models/eft/hideout/IHideoutPutItemInRequestData";
import { IHideoutScavCaseStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutScavCaseStartRequestData";
import { IHideoutSingleProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutSingleProductionStartRequestData";
import { IHideoutTakeItemOutRequestData } from "@spt-aki/models/eft/hideout/IHideoutTakeItemOutRequestData";
import { IHideoutTakeProductionRequestData } from "@spt-aki/models/eft/hideout/IHideoutTakeProductionRequestData";
import { IHideoutToggleAreaRequestData } from "@spt-aki/models/eft/hideout/IHideoutToggleAreaRequestData";
import { IHideoutUpgradeRequestData } from "@spt-aki/models/eft/hideout/IHideoutUpgradeRequestData";
import { IQteData } from "@spt-aki/models/eft/hideout/IQteData";
import { IRecordShootingRangePoints } from "@spt-aki/models/eft/hideout/IRecordShootingRangePoints";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
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 { SaveServer } from "@spt-aki/servers/SaveServer";
import { FenceService } from "@spt-aki/services/FenceService";
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 { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@injectable()
export class HideoutController
{
protected static nameTaskConditionCountersCrafting = "CounterHoursCrafting";
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("RandomUtil") protected randomUtil: RandomUtil,
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("PlayerService") protected playerService: PlayerService,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
@inject("ScavCaseRewardGenerator") protected scavCaseRewardGenerator: ScavCaseRewardGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("FenceService") protected fenceService: FenceService,
)
{
this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT);
}
/**
* Handle HideoutUpgrade event
* Start a hideout area upgrade
* @param pmcData Player profile
* @param request upgrade start request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public startUpgrade(
pmcData: IPmcData,
request: IHideoutUpgradeRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
const items = request.items.map((reqItem) =>
{
const item = pmcData.Inventory.items.find((invItem) => invItem._id === reqItem.id);
return { inventoryItem: item, requestedItem: reqItem };
});
// If it's not money, its construction / barter items
for (const item of items)
{
if (!item.inventoryItem)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_in_inventory", item.requestedItem.id),
);
return this.httpResponse.appendErrorToOutput(output);
}
if (
this.paymentHelper.isMoneyTpl(item.inventoryItem._tpl)
&& item.inventoryItem.upd
&& item.inventoryItem.upd.StackObjectsCount
&& item.inventoryItem.upd.StackObjectsCount > item.requestedItem.count
)
{
item.inventoryItem.upd.StackObjectsCount -= item.requestedItem.count;
}
else
{
this.inventoryHelper.removeItem(pmcData, item.inventoryItem._id, sessionID, output);
}
}
// Construction time management
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!hideoutArea)
{
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
const hideoutData = this.databaseServer.getTables().hideout.areas.find((area) =>
area.type === request.areaType
);
if (!hideoutData)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_area_in_database", request.areaType),
);
return this.httpResponse.appendErrorToOutput(output);
}
const ctime = hideoutData.stages[hideoutArea.level + 1].constructionTime;
if (ctime > 0)
{
const timestamp = this.timeUtil.getTimestamp();
hideoutArea.completeTime = Math.round(timestamp + ctime);
hideoutArea.constructing = true;
}
return output;
}
/**
* Handle HideoutUpgradeComplete event
* Complete a hideout area upgrade
* @param pmcData Player profile
* @param request Completed upgrade request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public upgradeComplete(
pmcData: IPmcData,
request: HideoutUpgradeCompleteRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
const db = this.databaseServer.getTables();
const profileHideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!profileHideoutArea)
{
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
// Upgrade profile values
profileHideoutArea.level++;
profileHideoutArea.completeTime = 0;
profileHideoutArea.constructing = false;
const hideoutData = db.hideout.areas.find((area) => area.type === profileHideoutArea.type);
if (!hideoutData)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_area_in_database", request.areaType),
);
return this.httpResponse.appendErrorToOutput(output);
}
// Apply bonuses
const hideoutStage = hideoutData.stages[profileHideoutArea.level];
const bonuses = hideoutStage.bonuses;
if (bonuses?.length > 0)
{
for (const bonus of bonuses)
{
this.hideoutHelper.applyPlayerUpgradesBonuses(pmcData, bonus);
}
}
// Upgrade includes a container improvement/addition
if (hideoutStage?.container)
{
this.addContainerImprovementToProfile(
output,
sessionID,
pmcData,
profileHideoutArea,
hideoutData,
hideoutStage,
);
}
// Upgrading water collector / med station
if (
profileHideoutArea.type === HideoutAreas.WATER_COLLECTOR
|| profileHideoutArea.type === HideoutAreas.MEDSTATION
)
{
this.checkAndUpgradeWall(pmcData);
}
// Add Skill Points Per Area Upgrade
this.profileHelper.addSkillPointsToPlayer(
pmcData,
SkillTypes.HIDEOUT_MANAGEMENT,
db.globals.config.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade,
);
return output;
}
/**
* Upgrade wall status to visible in profile if medstation/water collector are both level 1
* @param pmcData Player profile
*/
protected checkAndUpgradeWall(pmcData: IPmcData): void
{
const medStation = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.MEDSTATION);
const waterCollector = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.WATER_COLLECTOR);
if (medStation?.level >= 1 && waterCollector?.level >= 1)
{
const wall = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.EMERGENCY_WALL);
if (wall?.level === 0)
{
wall.level = 3;
}
}
}
/**
* @param pmcData Profile to edit
* @param output Object to send back to client
* @param sessionID Session/player id
* @param profileParentHideoutArea Current hideout area for profile
* @param dbHideoutArea Hideout area being upgraded
* @param hideoutStage Stage hideout area is being upgraded to
*/
protected addContainerImprovementToProfile(
output: IItemEventRouterResponse,
sessionID: string,
pmcData: IPmcData,
profileParentHideoutArea: HideoutArea,
dbHideoutArea: IHideoutArea,
hideoutStage: Stage,
): void
{
// Add key/value to `hideoutAreaStashes` dictionary - used to link hideout area to inventory stash by its id
if (!pmcData.Inventory.hideoutAreaStashes[dbHideoutArea.type])
{
pmcData.Inventory.hideoutAreaStashes[dbHideoutArea.type] = dbHideoutArea._id;
}
// Add/upgrade stash item in player inventory
this.addUpdateInventoryItemToProfile(pmcData, dbHideoutArea, hideoutStage);
// Inform client of changes
this.addContainerUpgradeToClientOutput(output, sessionID, dbHideoutArea.type, dbHideoutArea, hideoutStage);
// Some areas like gun stand have a child area linked to it, it needs to do the same as above
const childDbArea = this.databaseServer.getTables().hideout.areas.find((x) =>
x.parentArea === dbHideoutArea._id
);
if (childDbArea)
{
// Add key/value to `hideoutAreaStashes` dictionary - used to link hideout area to inventory stash by its id
if (!pmcData.Inventory.hideoutAreaStashes[childDbArea.type])
{
pmcData.Inventory.hideoutAreaStashes[childDbArea.type] = childDbArea._id;
}
// Set child area level to same as parent area
pmcData.Hideout.Areas.find((x) => x.type === childDbArea.type).level = pmcData.Hideout.Areas.find((x) =>
x.type === profileParentHideoutArea.type
).level;
// Add/upgrade stash item in player inventory
const childDbAreaStage = childDbArea.stages[profileParentHideoutArea.level];
this.addUpdateInventoryItemToProfile(pmcData, childDbArea, childDbAreaStage);
// Inform client of the changes
this.addContainerUpgradeToClientOutput(output, sessionID, childDbArea.type, childDbArea, childDbAreaStage);
}
}
/**
* Add an inventory item to profile from a hideout area stage data
* @param pmcData Profile to update
* @param dbHideoutData Hideout area from db being upgraded
* @param hideoutStage Stage area upgraded to
*/
protected addUpdateInventoryItemToProfile(pmcData: IPmcData, dbHideoutData: IHideoutArea, hideoutStage: Stage): void
{
const existingInventoryItem = pmcData.Inventory.items.find((x) => x._id === dbHideoutData._id);
if (existingInventoryItem)
{
// Update existing items container tpl to point to new id (tpl)
existingInventoryItem._tpl = hideoutStage.container;
return;
}
// Add new item as none exists
pmcData.Inventory.items.push({ _id: dbHideoutData._id, _tpl: hideoutStage.container });
}
/**
* @param output Object to send to client
* @param sessionID Session/player id
* @param areaType Hideout area that had stash added
* @param hideoutDbData Hideout area that caused addition of stash
* @param hideoutStage Hideout area upgraded to this
*/
protected addContainerUpgradeToClientOutput(
output: IItemEventRouterResponse,
sessionID: string,
areaType: HideoutAreas,
hideoutDbData: IHideoutArea,
hideoutStage: Stage,
): void
{
if (!output.profileChanges[sessionID].changedHideoutStashes)
{
output.profileChanges[sessionID].changedHideoutStashes = {};
}
output.profileChanges[sessionID].changedHideoutStashes[areaType] = {
Id: hideoutDbData._id,
Tpl: hideoutStage.container,
};
}
/**
* Handle HideoutPutItemsInAreaSlots
* Create item in hideout slot item array, remove item from player inventory
* @param pmcData Profile data
* @param addItemToHideoutRequest reqeust from client to place item in area slot
* @param sessionID Session id
* @returns IItemEventRouterResponse object
*/
public putItemsInAreaSlots(
pmcData: IPmcData,
addItemToHideoutRequest: IHideoutPutItemInRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
const itemsToAdd = Object.entries(addItemToHideoutRequest.items).map((kvp) =>
{
const item = pmcData.Inventory.items.find((invItem) => invItem._id === kvp[1].id);
return { inventoryItem: item, requestedItem: kvp[1], slot: kvp[0] };
});
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === addItemToHideoutRequest.areaType);
if (!hideoutArea)
{
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_area_in_database",
addItemToHideoutRequest.areaType,
),
);
return this.httpResponse.appendErrorToOutput(output);
}
for (const item of itemsToAdd)
{
if (!item.inventoryItem)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_in_inventory", {
itemId: item.requestedItem.id,
area: hideoutArea.type,
}),
);
return this.httpResponse.appendErrorToOutput(output);
}
// Add item to area.slots
const destinationLocationIndex = Number(item.slot);
const hideoutSlotIndex = hideoutArea.slots.findIndex((x) => x.locationIndex === destinationLocationIndex);
hideoutArea.slots[hideoutSlotIndex].item = [{
_id: item.inventoryItem._id,
_tpl: item.inventoryItem._tpl,
upd: item.inventoryItem.upd,
}];
this.inventoryHelper.removeItem(pmcData, item.inventoryItem._id, sessionID, output);
}
// Trigger a forced update
this.hideoutHelper.updatePlayerHideout(sessionID);
return output;
}
/**
* Handle HideoutTakeItemsFromAreaSlots event
* Remove item from hideout area and place into player inventory
* @param pmcData Player profile
* @param request Take item out of area request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public takeItemsFromAreaSlots(
pmcData: IPmcData,
request: IHideoutTakeItemOutRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!hideoutArea)
{
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
if (!hideoutArea.slots || hideoutArea.slots.length === 0)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_to_remove_from_area", hideoutArea.type),
);
return this.httpResponse.appendErrorToOutput(output);
}
// Handle areas that have resources that can be placed in/taken out of slots from the area
if (
[
HideoutAreas.AIR_FILTERING,
HideoutAreas.WATER_COLLECTOR,
HideoutAreas.GENERATOR,
HideoutAreas.BITCOIN_FARM,
].includes(hideoutArea.type)
)
{
const response = this.removeResourceFromArea(sessionID, pmcData, request, output, hideoutArea);
this.update();
return response;
}
throw new Error(
this.localisationService.getText("hideout-unhandled_remove_item_from_area_request", hideoutArea.type),
);
}
/**
* Find resource item in hideout area, add copy to player inventory, remove Item from hideout slot
* @param sessionID Session id
* @param pmcData Profile to update
* @param removeResourceRequest client request
* @param output response to send to client
* @param hideoutArea Area fuel is being removed from
* @returns IItemEventRouterResponse response
*/
protected removeResourceFromArea(
sessionID: string,
pmcData: IPmcData,
removeResourceRequest: IHideoutTakeItemOutRequestData,
output: IItemEventRouterResponse,
hideoutArea: HideoutArea,
): IItemEventRouterResponse
{
const slotIndexToRemove = removeResourceRequest.slots[0];
const itemToReturn = hideoutArea.slots.find((x) => x.locationIndex === slotIndexToRemove).item[0];
const newReq = {
items: [{
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: itemToReturn._tpl,
count: 1,
}],
tid: "ragfair",
};
output = this.inventoryHelper.addItem(
pmcData,
newReq,
output,
sessionID,
null,
!!itemToReturn.upd.SpawnedInSession,
itemToReturn.upd,
);
// If addItem returned with errors, drop out
if (output.warnings && output.warnings.length > 0)
{
return output;
}
// Remove items from slot, locationIndex remains
const hideoutSlotIndex = hideoutArea.slots.findIndex((x) => x.locationIndex === slotIndexToRemove);
hideoutArea.slots[hideoutSlotIndex].item = undefined;
return output;
}
/**
* Handle HideoutToggleArea event
* Toggle area on/off
* @param pmcData Player profile
* @param request Toggle area request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public toggleArea(
pmcData: IPmcData,
request: IHideoutToggleAreaRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
// Force a production update (occur before area is toggled as it could be generator and doing it after generator enabled would cause incorrect calculaton of production progress)
this.hideoutHelper.updatePlayerHideout(sessionID);
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!hideoutArea)
{
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
hideoutArea.active = request.enabled;
return output;
}
/**
* Handle HideoutSingleProductionStart event
* Start production for an item from hideout area
* @param pmcData Player profile
* @param body Start prodution of single item request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public singleProductionStart(
pmcData: IPmcData,
body: IHideoutSingleProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
// Start production
this.registerProduction(pmcData, body, sessionID);
// Find the recipe of the production
const recipe = this.databaseServer.getTables().hideout.production.find((p) => p._id === body.recipeId);
// Find the actual amount of items we need to remove because body can send weird data
const requirements = this.jsonUtil.clone(recipe.requirements.filter((i) => i.type === "Item"));
const output = this.eventOutputHolder.getOutput(sessionID);
for (const itemToDelete of body.items)
{
const itemToCheck = pmcData.Inventory.items.find((i) => i._id === itemToDelete.id);
const requirement = requirements.find((requirement) => requirement.templateId === itemToCheck._tpl);
if (requirement.count <= 0)
{
continue;
}
this.inventoryHelper.removeItemByCount(pmcData, itemToDelete.id, requirement.count, sessionID, output);
requirement.count -= itemToDelete.count;
}
return output;
}
/**
* Handle HideoutScavCaseProductionStart event
* Handles event after clicking 'start' on the scav case hideout page
* @param pmcData player profile
* @param body client request object
* @param sessionID session id
* @returns item event router response
*/
public scavCaseProductionStart(
pmcData: IPmcData,
body: IHideoutScavCaseStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
for (const requestedItem of body.items)
{
const inventoryItem = pmcData.Inventory.items.find((item) => item._id === requestedItem.id);
if (!inventoryItem)
{
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_scavcase_requested_item_in_profile_inventory",
requestedItem.id,
),
);
return this.httpResponse.appendErrorToOutput(output);
}
if (inventoryItem.upd?.StackObjectsCount && inventoryItem.upd.StackObjectsCount > requestedItem.count)
{
inventoryItem.upd.StackObjectsCount -= requestedItem.count;
}
else
{
this.inventoryHelper.removeItem(pmcData, requestedItem.id, sessionID, output);
}
}
const recipe = this.databaseServer.getTables().hideout.scavcase.find((r) => r._id === body.recipeId);
if (!recipe)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_scav_case_recipie_in_database", body.recipeId),
);
return this.httpResponse.appendErrorToOutput(output);
}
// @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"
const modifiedScavCaseTime = this.getScavCaseTime(pmcData, recipe.ProductionTime);
pmcData.Hideout.Production[body.recipeId] = this.hideoutHelper.initProduction(
body.recipeId,
modifiedScavCaseTime,
false,
);
pmcData.Hideout.Production[body.recipeId].sptIsScavCase = true;
return output;
}
/**
* Adjust scav case time based on fence standing
*
* @param pmcData Player profile
* @param productionTime Time to complete scav case in seconds
* @returns Adjusted scav case time in seconds
*/
protected getScavCaseTime(pmcData: IPmcData, productionTime: number): number
{
const fenceLevel = this.fenceService.getFenceInfo(pmcData);
if (!fenceLevel)
{
return productionTime;
}
return productionTime * fenceLevel.ScavCaseTimeModifier;
}
/**
* Add generated scav case rewards to player profile
* @param pmcData player profile to add rewards to
* @param rewards reward items to add to profile
* @param recipeId recipe id to save into Production dict
*/
protected addScavCaseRewardsToProfile(pmcData: IPmcData, rewards: Product[], recipeId: string): void
{
pmcData.Hideout.Production[`ScavCase${recipeId}`] = { Products: rewards };
}
/**
* Start production of continuously created item
* @param pmcData Player profile
* @param request Continious production request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public continuousProductionStart(
pmcData: IPmcData,
request: IHideoutContinuousProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
this.registerProduction(pmcData, request, sessionID);
return this.eventOutputHolder.getOutput(sessionID);
}
/**
* Handle HideoutTakeProduction event
* Take completed item out of hideout area and place into player inventory
* @param pmcData Player profile
* @param request Remove production from area request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public takeProduction(
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
sessionID: string,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionID);
const hideoutDb = this.databaseServer.getTables().hideout;
if (request.recipeId === HideoutHelper.bitcoinFarm)
{
return this.hideoutHelper.getBTC(pmcData, request, sessionID);
}
const recipe = hideoutDb.production.find((r) => r._id === request.recipeId);
if (recipe)
{
return this.handleRecipe(sessionID, recipe, pmcData, request, output);
}
const scavCase = hideoutDb.scavcase.find((r) => r._id === request.recipeId);
if (scavCase)
{
return this.handleScavCase(sessionID, pmcData, request, output);
}
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_production_in_profile_by_recipie_id",
request.recipeId,
),
);
return this.httpResponse.appendErrorToOutput(output);
}
/**
* Take recipe-type production out of hideout area and place into player inventory
* @param sessionID Session id
* @param recipe Completed recipe of item
* @param pmcData Player profile
* @param request Remove production from area request
* @param output Output object to update
* @returns IItemEventRouterResponse
*/
protected handleRecipe(
sessionID: string,
recipe: IHideoutProduction,
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
// Variables for managemnet of skill
let craftingExpAmount = 0;
// ? move the logic of TaskConditionCounters in new method?
let counterHoursCrafting = pmcData.TaskConditionCounters[HideoutController.nameTaskConditionCountersCrafting];
if (!counterHoursCrafting)
{
pmcData.TaskConditionCounters[HideoutController.nameTaskConditionCountersCrafting] = {
id: recipe._id,
type: HideoutController.nameTaskConditionCountersCrafting,
sourceId: "CounterCrafting",
value: 0,
};
counterHoursCrafting = pmcData.TaskConditionCounters[HideoutController.nameTaskConditionCountersCrafting];
}
let hoursCrafting = counterHoursCrafting.value;
// create item and throw it into profile
let id = recipe.endProduct;
// replace the base item with its main preset
if (this.presetHelper.hasPreset(id))
{
id = this.presetHelper.getDefaultPreset(id)._id;
}
const newReq = {
items: [{
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: id,
count: recipe.count,
}],
tid: "ragfair",
};
const entries = Object.entries(pmcData.Hideout.Production);
let prodId: string;
for (const x of entries)
{
// Skip null production objects
if (!x[1])
{
continue;
}
if (this.hideoutHelper.isProductionType(x[1]))
{ // Production or ScavCase
if ((x[1] as Production).RecipeId === request.recipeId)
{
prodId = x[0]; // set to objects key
break;
}
}
}
if (prodId === undefined)
{
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_production_in_profile_by_recipie_id",
request.recipeId,
),
);
return this.httpResponse.appendErrorToOutput(output);
}
// Check if the recipe is the same as the last one
const area = pmcData.Hideout.Areas.find((x) => x.type === recipe.areaType);
if (area && request.recipeId !== area.lastRecipe)
{
// 1 point per craft upon the end of production for alternating between 2 different crafting recipes in the same module
craftingExpAmount += this.hideoutConfig.expCraftAmount; // Default is 10
}
// 1 point per 8 hours of crafting
hoursCrafting += recipe.productionTime;
if ((hoursCrafting / this.hideoutConfig.hoursForSkillCrafting) >= 1)
{
const multiplierCrafting = Math.floor(hoursCrafting / this.hideoutConfig.hoursForSkillCrafting);
craftingExpAmount += 1 * multiplierCrafting;
hoursCrafting -= this.hideoutConfig.hoursForSkillCrafting * multiplierCrafting;
}
// Increment
// if addItem passes validation:
// - increment skill point for crafting
// - delete the production in profile Hideout.Production
const callback = () =>
{
// Manager Hideout skill
// ? use a configuration variable for the value?
const globals = this.databaseServer.getTables().globals;
this.profileHelper.addSkillPointsToPlayer(
pmcData,
SkillTypes.HIDEOUT_MANAGEMENT,
globals.config.SkillsSettings.HideoutManagement.SkillPointsPerCraft,
true,
);
// Manager Crafting skill
if (craftingExpAmount > 0)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CRAFTING, craftingExpAmount);
const intellectAmountToGive = 0.5 * (Math.round(craftingExpAmount / 15));
if (intellectAmountToGive > 0)
{
this.profileHelper.addSkillPointsToPlayer(
pmcData,
SkillTypes.INTELLECT,
intellectAmountToGive,
);
}
}
area.lastRecipe = request.recipeId;
counterHoursCrafting.value = hoursCrafting;
// Continuous crafts have special handling in EventOutputHolder.updateOutputProperties()
pmcData.Hideout.Production[prodId].sptIsComplete = true;
pmcData.Hideout.Production[prodId].sptIsContinuous = recipe.continuous;
// Flag normal crafts as complete
if (!recipe.continuous)
{
pmcData.Hideout.Production[prodId].inProgress = false;
}
};
// Handle the isEncoded flag from recipe
if (recipe.isEncoded)
{
const upd: Upd = { RecodableComponent: { IsEncoded: true } };
return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, true, upd);
}
return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, true);
}
/**
* Handles generating case rewards and sending to player inventory
* @param sessionID Session id
* @param pmcData Player profile
* @param request Get rewards from scavcase craft request
* @param output Output object to update
* @returns IItemEventRouterResponse
*/
protected handleScavCase(
sessionID: string,
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
const ongoingProductions = Object.entries(pmcData.Hideout.Production);
let prodId: string;
for (const production of ongoingProductions)
{
if (this.hideoutHelper.isProductionType(production[1]))
{ // Production or ScavCase
if ((production[1] as ScavCase).RecipeId === request.recipeId)
{
prodId = production[0]; // Set to objects key
break;
}
}
}
if (prodId === undefined)
{
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_production_in_profile_by_recipie_id",
request.recipeId,
),
);
return this.httpResponse.appendErrorToOutput(output);
}
// Create rewards for scav case
const scavCaseRewards = this.scavCaseRewardGenerator.generate(request.recipeId);
// Add scav case rewards to player profile
pmcData.Hideout.Production[prodId].Products = scavCaseRewards;
// Remove the old production from output object before its sent to client
delete output.profileChanges[sessionID].production[request.recipeId];
// Get array of item created + count of them after completing hideout craft
const itemsToAdd = pmcData.Hideout.Production[prodId].Products.map(
(x: { _tpl: string; upd?: { StackObjectsCount?: number; }; }) =>
{
const itemTpl = this.presetHelper.hasPreset(x._tpl)
? this.presetHelper.getDefaultPreset(x._tpl)._id
: x._tpl;
// Count of items crafted
const numOfItems = !x.upd?.StackObjectsCount ? 1 : x.upd.StackObjectsCount;
// eslint-disable-next-line @typescript-eslint/naming-convention
return { item_id: itemTpl, count: numOfItems };
},
);
const newReq = { items: itemsToAdd, tid: "ragfair" };
const callback = () =>
{
// Flag as complete - will be cleaned up later by hideoutController.update()
pmcData.Hideout.Production[prodId].sptIsComplete = true;
// Crafting complete, flag as such
pmcData.Hideout.Production[prodId].inProgress = false;
};
// Add crafted item to player inventory
return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, true);
}
/**
* Start area production for item by adding production to profiles' Hideout.Production array
* @param pmcData Player profile
* @param request Start production request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public registerProduction(
pmcData: IPmcData,
request: IHideoutSingleProductionStartRequestData | IHideoutContinuousProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
return this.hideoutHelper.registerProduction(pmcData, request, sessionID);
}
/**
* Get quick time event list for hideout
* // TODO - implement this
* @param sessionId Session id
* @returns IQteData array
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getQteList(sessionId: string): IQteData[]
{
return this.databaseServer.getTables().hideout.qte;
}
/**
* Handle HideoutQuickTimeEvent on client/game/profile/items/moving
* Called after completing workout at gym
* @param sessionId Session id
* @param pmcData Profile to adjust
* @param request QTE result object
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public handleQTEEventOutcome(
sessionId: string,
pmcData: IPmcData,
request: IHandleQTEEventRequestData,
): IItemEventRouterResponse
{
// {
// "Action": "HideoutQuickTimeEvent",
// "results": [true, false, true, true, true, true, true, true, true, false, false, false, false, false, false],
// "id": "63b16feb5d012c402c01f6ef",
// "timestamp": 1672585349
// }
// Skill changes are done in
// /client/hideout/workout (applyWorkoutChanges).
pmcData.Health.Energy.Current -= 50;
if (pmcData.Health.Energy.Current < 1)
{
pmcData.Health.Energy.Current = 1;
}
pmcData.Health.Hydration.Current -= 50;
if (pmcData.Health.Hydration.Current < 1)
{
pmcData.Health.Hydration.Current = 1;
}
return this.eventOutputHolder.getOutput(sessionId);
}
/**
* Record a high score from the shooting range into a player profiles overallcounters
* @param sessionId Session id
* @param pmcData Profile to update
* @param request shooting range score request
* @returns IItemEventRouterResponse
*/
public recordShootingRangePoints(
sessionId: string,
pmcData: IPmcData,
request: IRecordShootingRangePoints,
): IItemEventRouterResponse
{
// Check if counter exists, add placeholder if it doesnt
if (!pmcData.Stats.Eft.OverallCounters.Items.find((x) => x.Key.includes("ShootingRangePoints")))
{
pmcData.Stats.Eft.OverallCounters.Items.push({ Key: ["ShootingRangePoints"], Value: 0 });
}
// Find counter by key and update value
const shootingRangeHighScore = pmcData.Stats.Eft.OverallCounters.Items.find((x) =>
x.Key.includes("ShootingRangePoints")
);
shootingRangeHighScore.Value = request.points;
// Check against live, maybe a response isnt necessary
return this.eventOutputHolder.getOutput(sessionId);
}
/**
* Handle client/game/profile/items/moving - HideoutImproveArea
* @param sessionId Session id
* @param pmcData Profile to improve area in
* @param request Improve area request data
*/
public improveArea(
sessionId: string,
pmcData: IPmcData,
request: IHideoutImproveAreaRequestData,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionId);
// Create mapping of required item with corrisponding item from player inventory
const items = request.items.map((reqItem) =>
{
const item = pmcData.Inventory.items.find((invItem) => invItem._id === reqItem.id);
return { inventoryItem: item, requestedItem: reqItem };
});
// If it's not money, its construction / barter items
for (const item of items)
{
if (!item.inventoryItem)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_in_inventory", item.requestedItem.id),
);
return this.httpResponse.appendErrorToOutput(output);
}
if (
this.paymentHelper.isMoneyTpl(item.inventoryItem._tpl)
&& item.inventoryItem.upd
&& item.inventoryItem.upd.StackObjectsCount
&& item.inventoryItem.upd.StackObjectsCount > item.requestedItem.count
)
{
item.inventoryItem.upd.StackObjectsCount -= item.requestedItem.count;
}
else
{
this.inventoryHelper.removeItem(pmcData, item.inventoryItem._id, sessionId, output);
}
}
const profileHideoutArea = pmcData.Hideout.Areas.find((x) => x.type === request.areaType);
if (!profileHideoutArea)
{
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
const hideoutDbData = this.databaseServer.getTables().hideout.areas.find((x) => x.type === request.areaType);
if (!hideoutDbData)
{
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_area_in_database", request.areaType),
);
return this.httpResponse.appendErrorToOutput(output);
}
// Add all improvemets to output object
const improvements = hideoutDbData.stages[profileHideoutArea.level].improvements;
const timestamp = this.timeUtil.getTimestamp();
if (!output.profileChanges[sessionId].improvements)
{
output.profileChanges[sessionId].improvements = {};
}
for (const improvement of improvements)
{
const improvementDetails = {
completed: false,
improveCompleteTimestamp: timestamp + improvement.improvementTime,
};
output.profileChanges[sessionId].improvements[improvement.id] = improvementDetails;
pmcData.Hideout.Improvement[improvement.id] = improvementDetails;
}
return output;
}
/**
* Handle client/game/profile/items/moving HideoutCancelProductionCommand
* @param sessionId Session id
* @param pmcData Profile with craft to cancel
* @param request Cancel production request data
* @returns IItemEventRouterResponse
*/
public cancelProduction(
sessionId: string,
pmcData: IPmcData,
request: IHideoutCancelProductionRequestData,
): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionId);
const craftToCancel = pmcData.Hideout.Production[request.recipeId];
if (!craftToCancel)
{
const errorMessage = `Unable to find craft ${request.recipeId} to cancel`;
this.logger.error(errorMessage);
return this.httpResponse.appendErrorToOutput(output, errorMessage);
}
// Null out production data so client gets informed when response send back
pmcData.Hideout.Production[request.recipeId] = null;
// TODO - handle timestamp somehow?
return output;
}
/**
* Function called every x seconds as part of onUpdate event
*/
public update(): void
{
for (const sessionID in this.saveServer.getProfiles())
{
if ("Hideout" in this.saveServer.getProfile(sessionID).characters.pmc)
{
this.hideoutHelper.updatePlayerHideout(sessionID);
}
}
}
}