2024-05-28 11:25:23 +02:00
|
|
|
import { error } from "node:console";
|
2023-03-03 16:23:46 +01:00
|
|
|
import { inject, injectable } from "tsyringe";
|
2024-05-21 19:59:04 +02:00
|
|
|
import { HandbookHelper } from "@spt/helpers/HandbookHelper";
|
|
|
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
|
|
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
|
|
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
2024-05-25 16:09:52 +02:00
|
|
|
import { BanType } from "@spt/models/eft/common/tables/IBotBase";
|
2024-05-21 19:59:04 +02:00
|
|
|
import { Item } from "@spt/models/eft/common/tables/IItem";
|
|
|
|
import { ProfileTraderTemplate } from "@spt/models/eft/common/tables/IProfileTemplate";
|
|
|
|
import { ITraderAssort, ITraderBase, LoyaltyLevel } from "@spt/models/eft/common/tables/ITrader";
|
2024-05-27 18:05:16 +02:00
|
|
|
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
|
2024-05-21 19:59:04 +02:00
|
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
2024-06-16 22:22:28 +02:00
|
|
|
import { GameEditions } from "@spt/models/enums/GameEditions";
|
2024-05-21 19:59:04 +02:00
|
|
|
import { Money } from "@spt/models/enums/Money";
|
|
|
|
import { Traders } from "@spt/models/enums/Traders";
|
|
|
|
import { ITraderConfig } from "@spt/models/spt/config/ITraderConfig";
|
|
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
2024-05-29 16:15:45 +02:00
|
|
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
2024-05-21 19:59:04 +02:00
|
|
|
import { FenceService } from "@spt/services/FenceService";
|
|
|
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
|
|
|
import { PlayerService } from "@spt/services/PlayerService";
|
|
|
|
import { RandomUtil } from "@spt/utils/RandomUtil";
|
|
|
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
@injectable()
|
|
|
|
export class TraderHelper
|
|
|
|
{
|
|
|
|
protected traderConfig: ITraderConfig;
|
2023-03-15 20:06:03 +01:00
|
|
|
/** Dictionary of item tpl and the highest trader sell rouble price */
|
2024-05-27 18:05:16 +02:00
|
|
|
protected highestTraderPriceItems?: Record<string, number> = undefined;
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
constructor(
|
2024-05-28 16:04:20 +02:00
|
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
2024-05-29 16:15:45 +02:00
|
|
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
2023-03-03 16:23:46 +01:00
|
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
|
|
|
@inject("HandbookHelper") protected handbookHelper: HandbookHelper,
|
2023-03-15 20:06:03 +01:00
|
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
2023-03-03 16:23:46 +01:00
|
|
|
@inject("PlayerService") protected playerService: PlayerService,
|
|
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
|
|
@inject("FenceService") protected fenceService: FenceService,
|
|
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
2023-03-15 20:06:03 +01:00
|
|
|
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
2023-11-16 02:35:05 +01:00
|
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
2023-03-03 16:23:46 +01:00
|
|
|
)
|
|
|
|
{
|
|
|
|
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER);
|
|
|
|
}
|
|
|
|
|
2023-10-25 13:18:27 +02:00
|
|
|
/**
|
2023-11-16 02:35:05 +01:00
|
|
|
* Get a trader base object, update profile to reflect players current standing in profile
|
2023-10-25 13:18:27 +02:00
|
|
|
* when trader not found in profile
|
|
|
|
* @param traderID Traders Id to get
|
|
|
|
* @param sessionID Players id
|
|
|
|
* @returns Trader base
|
|
|
|
*/
|
2024-05-27 18:05:16 +02:00
|
|
|
public getTrader(traderID: string, sessionID: string): ITraderBase | undefined
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
|
|
|
const pmcData = this.profileHelper.getPmcProfile(sessionID);
|
2023-10-25 13:18:27 +02:00
|
|
|
if (!pmcData)
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2024-05-28 11:25:23 +02:00
|
|
|
throw new error(this.localisationService.getText("trader-unable_to_find_profile_with_id", sessionID));
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
2023-11-16 02:35:05 +01:00
|
|
|
|
2023-10-25 13:18:27 +02:00
|
|
|
// Profile has traderInfo dict (profile beyond creation stage) but no requested trader in profile
|
|
|
|
if (pmcData.TradersInfo && !(traderID in pmcData.TradersInfo))
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2023-10-25 13:18:27 +02:00
|
|
|
// Add trader values to profile
|
2023-03-03 16:23:46 +01:00
|
|
|
this.resetTrader(sessionID, traderID);
|
2023-10-10 13:03:20 +02:00
|
|
|
this.lvlUp(traderID, pmcData);
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
2023-11-16 02:35:05 +01:00
|
|
|
|
2024-05-29 16:15:45 +02:00
|
|
|
const traderBase = this.databaseService.getTrader(traderID).base;
|
|
|
|
if (!traderBase)
|
2023-10-25 13:18:27 +02:00
|
|
|
{
|
2024-05-21 13:40:16 +02:00
|
|
|
this.logger.error(this.localisationService.getText("trader-unable_to_find_trader_by_id", traderID));
|
2023-10-25 13:18:27 +02:00
|
|
|
}
|
2023-03-03 16:23:46 +01:00
|
|
|
|
2024-05-29 16:15:45 +02:00
|
|
|
return traderBase;
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
/**
|
|
|
|
* Get all assort data for a particular trader
|
|
|
|
* @param traderId Trader to get assorts for
|
|
|
|
* @returns ITraderAssort
|
|
|
|
*/
|
|
|
|
public getTraderAssortsByTraderId(traderId: string): ITraderAssort
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
|
|
|
return traderId === Traders.FENCE
|
|
|
|
? this.fenceService.getRawFenceAssorts()
|
2024-05-29 16:15:45 +02:00
|
|
|
: this.databaseService.getTrader(traderId).assort;
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
/**
|
|
|
|
* Retrieve the Item from a traders assort data by its id
|
|
|
|
* @param traderId Trader to get assorts for
|
|
|
|
* @param assortId Id of assort to find
|
|
|
|
* @returns Item object
|
|
|
|
*/
|
2024-05-27 18:05:16 +02:00
|
|
|
public getTraderAssortItemByAssortId(traderId: string, assortId: string): Item | undefined
|
2023-10-10 13:03:20 +02:00
|
|
|
{
|
|
|
|
const traderAssorts = this.getTraderAssortsByTraderId(traderId);
|
|
|
|
if (!traderAssorts)
|
|
|
|
{
|
|
|
|
this.logger.debug(`No assorts on trader: ${traderId} found`);
|
|
|
|
|
2024-05-27 18:05:16 +02:00
|
|
|
return undefined;
|
2023-10-10 13:03:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Find specific assort in traders data
|
2024-05-17 21:32:41 +02:00
|
|
|
const purchasedAssort = traderAssorts.items.find((item) => item._id === assortId);
|
2023-10-10 13:03:20 +02:00
|
|
|
if (!purchasedAssort)
|
|
|
|
{
|
|
|
|
this.logger.debug(`No assort ${assortId} on trader: ${traderId} found`);
|
|
|
|
|
2024-05-27 18:05:16 +02:00
|
|
|
return undefined;
|
2023-10-10 13:03:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return purchasedAssort;
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
/**
|
|
|
|
* Reset a profiles trader data back to its initial state as seen by a level 1 player
|
|
|
|
* Does NOT take into account different profile levels
|
2024-03-25 11:38:28 +01:00
|
|
|
* @param sessionID session id of player
|
2023-03-03 16:23:46 +01:00
|
|
|
* @param traderID trader id to reset
|
|
|
|
*/
|
|
|
|
public resetTrader(sessionID: string, traderID: string): void
|
|
|
|
{
|
2024-05-29 16:15:45 +02:00
|
|
|
const profiles = this.databaseService.getProfiles();
|
|
|
|
const trader = this.databaseService.getTrader(traderID);
|
|
|
|
|
2024-05-27 17:30:03 +02:00
|
|
|
const fullProfile = this.profileHelper.getFullProfile(sessionID);
|
2024-05-28 11:25:23 +02:00
|
|
|
if (!fullProfile)
|
|
|
|
{
|
|
|
|
throw new error(this.localisationService.getText("trader-unable_to_find_profile_by_id", sessionID));
|
|
|
|
}
|
|
|
|
|
2024-05-27 17:30:03 +02:00
|
|
|
const pmcData = fullProfile.characters.pmc;
|
2024-05-08 05:57:08 +02:00
|
|
|
const rawProfileTemplate: ProfileTraderTemplate
|
2024-05-29 16:15:45 +02:00
|
|
|
= profiles[fullProfile.info.edition][pmcData.Info.Side.toLowerCase()]
|
2023-11-16 02:35:05 +01:00
|
|
|
.trader;
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
pmcData.TradersInfo[traderID] = {
|
|
|
|
disabled: false,
|
2024-02-03 16:53:28 +01:00
|
|
|
loyaltyLevel: rawProfileTemplate.initialLoyaltyLevel[traderID] ?? 1,
|
2023-03-03 16:23:46 +01:00
|
|
|
salesSum: rawProfileTemplate.initialSalesSum,
|
2023-11-16 02:35:05 +01:00
|
|
|
standing: this.getStartingStanding(traderID, rawProfileTemplate),
|
2024-05-29 16:15:45 +02:00
|
|
|
nextResupply: trader.base.nextResupply,
|
|
|
|
unlocked: trader.base.unlockedByDefault,
|
2023-03-03 16:23:46 +01:00
|
|
|
};
|
|
|
|
|
2024-05-25 22:21:23 +02:00
|
|
|
// Check if trader should be locked by default
|
|
|
|
if (rawProfileTemplate.lockedByDefaultOverride?.includes(traderID))
|
|
|
|
{
|
2024-06-04 19:20:10 +02:00
|
|
|
pmcData.TradersInfo[traderID].unlocked = true;
|
2024-05-25 22:21:23 +02:00
|
|
|
}
|
|
|
|
|
2024-05-27 18:05:16 +02:00
|
|
|
if (rawProfileTemplate.purchaseAllClothingByDefaultForTrader?.includes(traderID))
|
|
|
|
{
|
|
|
|
// Get traders clothing
|
2024-05-29 16:15:45 +02:00
|
|
|
const clothing = this.databaseService.getTrader(traderID).suits;
|
|
|
|
if (clothing?.length > 0)
|
|
|
|
{
|
|
|
|
// Force suit ids into profile
|
|
|
|
this.addSuitsToProfile(fullProfile, clothing!.map((suit) => suit.suiteId));
|
|
|
|
}
|
2024-05-27 18:05:16 +02:00
|
|
|
}
|
2024-05-27 17:30:03 +02:00
|
|
|
|
2024-05-27 18:05:16 +02:00
|
|
|
if ((rawProfileTemplate.fleaBlockedDays ?? 0) > 0)
|
2024-05-25 16:09:52 +02:00
|
|
|
{
|
2024-05-27 18:05:16 +02:00
|
|
|
const newBanDateTime = this.timeUtil.getTimeStampFromNowDays(rawProfileTemplate.fleaBlockedDays!);
|
2024-05-25 16:46:01 +02:00
|
|
|
const existingBan = pmcData.Info.Bans.find((ban) => ban.banType === BanType.RAGFAIR);
|
|
|
|
if (existingBan)
|
|
|
|
{
|
|
|
|
existingBan.dateTime = newBanDateTime;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
pmcData.Info.Bans.push(
|
|
|
|
{
|
|
|
|
banType: BanType.RAGFAIR,
|
|
|
|
dateTime: newBanDateTime,
|
|
|
|
});
|
|
|
|
}
|
2024-05-25 16:09:52 +02:00
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
if (traderID === Traders.JAEGER)
|
|
|
|
{
|
|
|
|
pmcData.TradersInfo[traderID].unlocked = rawProfileTemplate.jaegerUnlocked;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-14 16:47:01 +01:00
|
|
|
/**
|
|
|
|
* Get the starting standing of a trader based on the current profiles type (e.g. EoD, Standard etc)
|
|
|
|
* @param traderId Trader id to get standing for
|
|
|
|
* @param rawProfileTemplate Raw profile from profiles.json to look up standing from
|
|
|
|
* @returns Standing value
|
|
|
|
*/
|
2023-08-02 17:15:33 +02:00
|
|
|
protected getStartingStanding(traderId: string, rawProfileTemplate: ProfileTraderTemplate): number
|
|
|
|
{
|
2024-06-14 16:40:27 +02:00
|
|
|
const initialStanding = rawProfileTemplate.initialStanding[traderId]
|
|
|
|
?? rawProfileTemplate.initialStanding.default;
|
2023-08-02 17:15:33 +02:00
|
|
|
// Edge case for Lightkeeper, 0 standing means seeing `Make Amends - Buyout` quest
|
2024-06-14 16:40:27 +02:00
|
|
|
if (traderId === Traders.LIGHTHOUSEKEEPER && initialStanding === 0)
|
2023-08-02 17:15:33 +02:00
|
|
|
{
|
|
|
|
return 0.01;
|
|
|
|
}
|
|
|
|
|
2024-06-14 16:40:27 +02:00
|
|
|
return initialStanding;
|
2023-08-02 17:15:33 +02:00
|
|
|
}
|
2024-05-27 17:30:03 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an array of suit ids to a profiles suit array, no duplicates
|
|
|
|
* @param fullProfile Profile to add to
|
|
|
|
* @param suitIds Suit Ids to add
|
|
|
|
*/
|
|
|
|
protected addSuitsToProfile(fullProfile: ISptProfile, suitIds: string[]): void
|
|
|
|
{
|
|
|
|
if (!fullProfile.suits)
|
|
|
|
{
|
|
|
|
fullProfile.suits = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const suitId of suitIds)
|
|
|
|
{
|
|
|
|
// Don't add dupes
|
|
|
|
if (!fullProfile.suits.includes(suitId))
|
|
|
|
{
|
|
|
|
fullProfile.suits.push(suitId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-02 17:15:33 +02:00
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
/**
|
|
|
|
* Alter a traders unlocked status
|
|
|
|
* @param traderId Trader to alter
|
|
|
|
* @param status New status to use
|
2024-03-25 11:38:28 +01:00
|
|
|
* @param sessionId Session id of player
|
2023-03-03 16:23:46 +01:00
|
|
|
*/
|
|
|
|
public setTraderUnlockedState(traderId: string, status: boolean, sessionId: string): void
|
|
|
|
{
|
|
|
|
const pmcData = this.profileHelper.getPmcProfile(sessionId);
|
|
|
|
pmcData.TradersInfo[traderId].unlocked = status;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add standing to a trader and level them up if exp goes over level threshold
|
2024-03-24 23:16:59 +01:00
|
|
|
* @param sessionId Session id of player
|
|
|
|
* @param traderId Traders id to add standing to
|
2023-03-03 16:23:46 +01:00
|
|
|
* @param standingToAdd Standing value to add to trader
|
|
|
|
*/
|
|
|
|
public addStandingToTrader(sessionId: string, traderId: string, standingToAdd: number): void
|
|
|
|
{
|
2024-03-24 23:16:59 +01:00
|
|
|
const fullProfile = this.profileHelper.getFullProfile(sessionId);
|
|
|
|
const pmcTraderInfo = fullProfile.characters.pmc.TradersInfo[traderId];
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// Add standing to trader
|
2024-03-24 23:16:59 +01:00
|
|
|
pmcTraderInfo.standing = this.addStandingValuesTogether(pmcTraderInfo.standing, standingToAdd);
|
|
|
|
|
|
|
|
if (traderId === Traders.FENCE)
|
|
|
|
{
|
|
|
|
// Must add rep to scav profile to ensure consistency
|
|
|
|
fullProfile.characters.scav.TradersInfo[traderId].standing = pmcTraderInfo.standing;
|
|
|
|
}
|
2023-03-03 16:23:46 +01:00
|
|
|
|
2024-03-24 23:16:59 +01:00
|
|
|
this.lvlUp(traderId, fullProfile.characters.pmc);
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
2023-04-23 00:41:04 +02:00
|
|
|
/**
|
|
|
|
* Add standing to current standing and clamp value if it goes too low
|
|
|
|
* @param currentStanding current trader standing
|
|
|
|
* @param standingToAdd stansding to add to trader standing
|
|
|
|
* @returns current standing + added standing (clamped if needed)
|
|
|
|
*/
|
|
|
|
protected addStandingValuesTogether(currentStanding: number, standingToAdd: number): number
|
|
|
|
{
|
|
|
|
const newStanding = currentStanding + standingToAdd;
|
|
|
|
|
2024-03-25 11:38:28 +01:00
|
|
|
// Never let standing fall below 0
|
2023-11-16 02:35:05 +01:00
|
|
|
return newStanding < 0 ? 0 : newStanding;
|
2023-04-23 00:41:04 +02:00
|
|
|
}
|
|
|
|
|
2024-05-25 16:45:27 +02:00
|
|
|
/**
|
|
|
|
* iterate over a profiles traders and ensure they have the correct loyaltyLevel for the player
|
|
|
|
* @param sessionId Profile to check
|
|
|
|
*/
|
|
|
|
public validateTraderStandingsAndPlayerLevelForProfile(sessionId: string): void
|
|
|
|
{
|
|
|
|
const profile = this.profileHelper.getPmcProfile(sessionId);
|
2024-05-29 16:15:45 +02:00
|
|
|
const traders = Object.keys(this.databaseService.getTraders());
|
2024-05-25 16:45:27 +02:00
|
|
|
for (const trader of traders)
|
|
|
|
{
|
|
|
|
this.lvlUp(trader, profile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
/**
|
|
|
|
* Calculate traders level based on exp amount and increments level if over threshold
|
2024-05-25 16:45:27 +02:00
|
|
|
* Also validates and updates player level if not correct based on XP value
|
|
|
|
* @param traderID Trader to check standing of
|
|
|
|
* @param pmcData Profile to update trader in
|
2023-03-03 16:23:46 +01:00
|
|
|
*/
|
2023-10-10 13:03:20 +02:00
|
|
|
public lvlUp(traderID: string, pmcData: IPmcData): void
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2024-05-29 16:15:45 +02:00
|
|
|
const loyaltyLevels = this.databaseService.getTrader(traderID).base.loyaltyLevels;
|
2023-03-03 16:23:46 +01:00
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
// Level up player
|
2023-03-03 16:23:46 +01:00
|
|
|
pmcData.Info.Level = this.playerService.calculateLevel(pmcData);
|
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
// Level up traders
|
2023-03-03 16:23:46 +01:00
|
|
|
let targetLevel = 0;
|
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
// Round standing to 2 decimal places to address floating point inaccuracies
|
2023-03-03 16:23:46 +01:00
|
|
|
pmcData.TradersInfo[traderID].standing = Math.round(pmcData.TradersInfo[traderID].standing * 100) / 100;
|
|
|
|
|
|
|
|
for (const level in loyaltyLevels)
|
|
|
|
{
|
|
|
|
const loyalty = loyaltyLevels[level];
|
|
|
|
|
2023-11-16 02:35:05 +01:00
|
|
|
if (
|
2024-05-08 05:57:08 +02:00
|
|
|
loyalty.minLevel <= pmcData.Info.Level
|
|
|
|
&& loyalty.minSalesSum <= pmcData.TradersInfo[traderID].salesSum
|
2024-05-17 21:32:41 +02:00
|
|
|
&& loyalty.minStanding <= pmcData.TradersInfo[traderID].standing
|
|
|
|
&& targetLevel < 4
|
2023-11-16 02:35:05 +01:00
|
|
|
)
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
|
|
|
// level reached
|
|
|
|
targetLevel++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// set level
|
|
|
|
pmcData.TradersInfo[traderID].loyaltyLevel = targetLevel;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the next update timestamp for a trader
|
|
|
|
* @param traderID Trader to look up update value for
|
|
|
|
* @returns future timestamp
|
|
|
|
*/
|
|
|
|
public getNextUpdateTimestamp(traderID: string): number
|
|
|
|
{
|
|
|
|
const time = this.timeUtil.getTimestamp();
|
2024-05-27 18:05:16 +02:00
|
|
|
const updateSeconds = this.getTraderUpdateSeconds(traderID) ?? 0;
|
2023-03-03 16:23:46 +01:00
|
|
|
return time + updateSeconds;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the reset time between trader assort refreshes in seconds
|
|
|
|
* @param traderId Trader to look up
|
|
|
|
* @returns Time in seconds
|
|
|
|
*/
|
2024-05-27 18:05:16 +02:00
|
|
|
public getTraderUpdateSeconds(traderId: string): number | undefined
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2024-05-17 21:32:41 +02:00
|
|
|
const traderDetails = this.traderConfig.updateTime.find((x) => x.traderId === traderId);
|
2024-04-25 19:46:27 +02:00
|
|
|
if (!traderDetails || traderDetails.seconds.min === undefined || traderDetails.seconds.max === undefined)
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2023-11-16 02:35:05 +01:00
|
|
|
this.logger.warning(
|
|
|
|
this.localisationService.getText("trader-missing_trader_details_using_default_refresh_time", {
|
2023-03-03 16:23:46 +01:00
|
|
|
traderId: traderId,
|
2023-11-16 02:35:05 +01:00
|
|
|
updateTime: this.traderConfig.updateTimeDefault,
|
|
|
|
}),
|
|
|
|
);
|
2024-03-12 16:45:48 +01:00
|
|
|
|
2024-05-17 21:32:41 +02:00
|
|
|
this.traderConfig.updateTime.push(
|
|
|
|
// create temporary entry to prevent logger spam
|
2024-03-16 23:12:03 +01:00
|
|
|
{
|
|
|
|
traderId: traderId,
|
|
|
|
seconds: { min: this.traderConfig.updateTimeDefault, max: this.traderConfig.updateTimeDefault },
|
|
|
|
},
|
2023-03-03 16:23:46 +01:00
|
|
|
);
|
2024-05-27 18:05:16 +02:00
|
|
|
return undefined;
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-03-16 23:12:03 +01:00
|
|
|
return this.randomUtil.getInt(traderDetails.seconds.min, traderDetails.seconds.max);
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getLoyaltyLevel(traderID: string, pmcData: IPmcData): LoyaltyLevel
|
|
|
|
{
|
2024-05-29 16:15:45 +02:00
|
|
|
const traderBase = this.databaseService.getTrader(traderID).base;
|
2023-03-03 16:23:46 +01:00
|
|
|
let loyaltyLevel = pmcData.TradersInfo[traderID].loyaltyLevel;
|
|
|
|
|
|
|
|
if (!loyaltyLevel || loyaltyLevel < 1)
|
|
|
|
{
|
|
|
|
loyaltyLevel = 1;
|
|
|
|
}
|
|
|
|
|
2024-05-29 16:15:45 +02:00
|
|
|
if (loyaltyLevel > traderBase.loyaltyLevels.length)
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2024-05-29 16:15:45 +02:00
|
|
|
loyaltyLevel = traderBase.loyaltyLevels.length;
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
2024-05-29 16:15:45 +02:00
|
|
|
return traderBase.loyaltyLevels[loyaltyLevel - 1];
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Store the purchase of an assort from a trader in the player profile
|
|
|
|
* @param sessionID Session id
|
|
|
|
* @param newPurchaseDetails New item assort id + count
|
|
|
|
*/
|
2023-11-16 02:35:05 +01:00
|
|
|
public addTraderPurchasesToPlayerProfile(
|
|
|
|
sessionID: string,
|
2024-05-08 05:57:08 +02:00
|
|
|
newPurchaseDetails: { items: { itemId: string, count: number }[], traderId: string },
|
2024-03-30 19:29:08 +01:00
|
|
|
itemPurchased: Item,
|
2023-11-16 02:35:05 +01:00
|
|
|
): void
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
|
|
|
const profile = this.profileHelper.getFullProfile(sessionID);
|
2024-01-15 15:25:17 +01:00
|
|
|
const traderId = newPurchaseDetails.traderId;
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// Iterate over assorts bought and add to profile
|
|
|
|
for (const purchasedItem of newPurchaseDetails.items)
|
|
|
|
{
|
2024-06-08 20:38:16 +02:00
|
|
|
const currentTime = this.timeUtil.getTimestamp();
|
2023-03-03 16:23:46 +01:00
|
|
|
|
2024-06-08 20:38:16 +02:00
|
|
|
// Nullguard traderPurchases
|
|
|
|
profile.traderPurchases ||= {};
|
|
|
|
// Nullguard traderPurchases for this trader
|
|
|
|
profile.traderPurchases[traderId] ||= {};
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// Null guard when dict doesnt exist
|
2024-06-08 20:38:16 +02:00
|
|
|
|
2024-01-15 15:25:17 +01:00
|
|
|
if (!profile.traderPurchases[traderId][purchasedItem.itemId])
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2024-01-15 15:25:17 +01:00
|
|
|
profile.traderPurchases[traderId][purchasedItem.itemId] = {
|
2023-03-03 16:23:46 +01:00
|
|
|
count: purchasedItem.count,
|
2023-11-16 02:35:05 +01:00
|
|
|
purchaseTimestamp: currentTime,
|
2023-03-03 16:23:46 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-03-30 19:29:08 +01:00
|
|
|
if (
|
|
|
|
profile.traderPurchases[traderId][purchasedItem.itemId].count + purchasedItem.count
|
2024-06-08 20:38:16 +02:00
|
|
|
> this.getAccountTypeAdjustedTraderPurchaseLimit(
|
|
|
|
itemPurchased.upd!.BuyRestrictionMax!,
|
|
|
|
profile.characters.pmc.Info.GameVersion)
|
2024-03-30 19:29:08 +01:00
|
|
|
)
|
2024-03-30 13:55:18 +01:00
|
|
|
{
|
2024-05-24 17:42:42 +02:00
|
|
|
throw new Error(
|
|
|
|
this.localisationService.getText("trader-unable_to_purchase_item_limit_reached",
|
|
|
|
{
|
|
|
|
traderId: traderId,
|
2024-05-27 18:05:16 +02:00
|
|
|
limit: itemPurchased.upd!.BuyRestrictionMax,
|
2024-05-24 17:42:42 +02:00
|
|
|
}),
|
|
|
|
);
|
2024-03-30 13:55:18 +01:00
|
|
|
}
|
2024-01-15 15:25:17 +01:00
|
|
|
profile.traderPurchases[traderId][purchasedItem.itemId].count += purchasedItem.count;
|
|
|
|
profile.traderPurchases[traderId][purchasedItem.itemId].purchaseTimestamp = currentTime;
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-08 20:38:16 +02:00
|
|
|
/**
|
|
|
|
* EoD and Unheard get a 20% bonus to personal trader limit purchases
|
|
|
|
* @param buyRestrictionMax Existing value from trader item
|
|
|
|
* @param gameVersion Profiles game version
|
|
|
|
* @returns buyRestrictionMax value
|
|
|
|
*/
|
|
|
|
public getAccountTypeAdjustedTraderPurchaseLimit(buyRestrictionMax: number, gameVersion: string): number
|
|
|
|
{
|
2024-06-19 12:11:28 +02:00
|
|
|
if (([GameEditions.EDGE_OF_DARKNESS, GameEditions.UNHEARD] as string[]).includes(gameVersion))
|
2024-06-08 20:38:16 +02:00
|
|
|
{
|
|
|
|
return Math.floor(buyRestrictionMax * 1.2);
|
|
|
|
}
|
|
|
|
|
|
|
|
return buyRestrictionMax;
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
/**
|
|
|
|
* Get the highest rouble price for an item from traders
|
2023-03-15 20:06:03 +01:00
|
|
|
* UNUSED
|
2023-03-03 16:23:46 +01:00
|
|
|
* @param tpl Item to look up highest pride for
|
|
|
|
* @returns highest rouble cost for item
|
|
|
|
*/
|
|
|
|
public getHighestTraderPriceRouble(tpl: string): number
|
|
|
|
{
|
|
|
|
if (this.highestTraderPriceItems)
|
|
|
|
{
|
|
|
|
return this.highestTraderPriceItems[tpl];
|
|
|
|
}
|
|
|
|
|
2023-03-15 20:06:03 +01:00
|
|
|
if (!this.highestTraderPriceItems)
|
|
|
|
{
|
|
|
|
this.highestTraderPriceItems = {};
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
// Init dict and fill
|
|
|
|
for (const traderName in Traders)
|
|
|
|
{
|
|
|
|
// Skip some traders
|
|
|
|
if (traderName === Traders.FENCE)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get assorts for trader, skip trader if no assorts found
|
2024-05-29 16:15:45 +02:00
|
|
|
const traderAssorts = this.databaseService.getTrader(Traders[traderName]).assort;
|
2023-03-03 16:23:46 +01:00
|
|
|
if (!traderAssorts)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all item assorts that have parentid of hideout (base item and not a mod of other item)
|
2024-05-17 21:32:41 +02:00
|
|
|
for (const item of traderAssorts.items.filter((x) => x.parentId === "hideout"))
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
|
|
|
// Get barter scheme (contains cost of item)
|
|
|
|
const barterScheme = traderAssorts.barter_scheme[item._id][0][0];
|
|
|
|
|
|
|
|
// Convert into roubles
|
2024-05-17 21:32:41 +02:00
|
|
|
const roubleAmount
|
|
|
|
= barterScheme._tpl === Money.ROUBLES
|
|
|
|
? barterScheme.count
|
|
|
|
: this.handbookHelper.inRUB(barterScheme.count, barterScheme._tpl);
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// Existing price smaller in dict than current iteration, overwrite
|
|
|
|
if (this.highestTraderPriceItems[item._tpl] ?? 0 < roubleAmount)
|
|
|
|
{
|
|
|
|
this.highestTraderPriceItems[item._tpl] = roubleAmount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.highestTraderPriceItems[tpl];
|
|
|
|
}
|
2023-03-15 20:06:03 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the highest price item can be sold to trader for (roubles)
|
|
|
|
* @param tpl Item to look up best trader sell-to price
|
|
|
|
* @returns Rouble price
|
|
|
|
*/
|
|
|
|
public getHighestSellToTraderPrice(tpl: string): number
|
|
|
|
{
|
|
|
|
// Find highest trader price for item
|
2024-06-08 00:14:18 +02:00
|
|
|
let highestPrice = 1; // Default price
|
2023-03-15 20:06:03 +01:00
|
|
|
for (const traderName in Traders)
|
|
|
|
{
|
|
|
|
// Get trader and check buy category allows tpl
|
2024-05-29 16:15:45 +02:00
|
|
|
const traderBase = this.databaseService.getTrader(Traders[traderName]).base;
|
2024-06-08 00:14:18 +02:00
|
|
|
|
|
|
|
// Skip traders that dont sell
|
|
|
|
if (!traderBase || !this.itemHelper.isOfBaseclasses(tpl, traderBase.items_buy.category))
|
2023-03-15 20:06:03 +01:00
|
|
|
{
|
2024-06-08 00:14:18 +02:00
|
|
|
continue;
|
|
|
|
}
|
2023-03-15 20:06:03 +01:00
|
|
|
|
2024-06-08 00:14:18 +02:00
|
|
|
// Get loyalty level details player has achieved with this trader
|
|
|
|
// Uses lowest loyalty level as this function is used before a player has logged into server
|
|
|
|
// We have no idea what player loyalty is with traders
|
|
|
|
const traderBuyBackPricePercent = traderBase.loyaltyLevels[0].buy_price_coef;
|
2023-03-15 20:06:03 +01:00
|
|
|
|
2024-06-08 00:14:18 +02:00
|
|
|
const itemHandbookPrice = this.handbookHelper.getTemplatePrice(tpl);
|
|
|
|
const priceTraderBuysItemAt = Math.round(
|
|
|
|
this.randomUtil.getPercentOfValue(traderBuyBackPricePercent, itemHandbookPrice),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Price from this trader is higher than highest found, update
|
|
|
|
if (priceTraderBuysItemAt > highestPrice)
|
|
|
|
{
|
|
|
|
highestPrice = priceTraderBuysItemAt;
|
2023-03-15 20:06:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-08 00:14:18 +02:00
|
|
|
return highestPrice;
|
2023-03-15 20:06:03 +01:00
|
|
|
}
|
2023-07-21 19:08:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a trader enum key by its value
|
|
|
|
* @param traderId Traders id
|
|
|
|
* @returns Traders key
|
|
|
|
*/
|
2024-05-27 18:05:16 +02:00
|
|
|
public getTraderById(traderId: string): Traders | undefined
|
2023-07-21 19:08:32 +02:00
|
|
|
{
|
2024-05-17 21:32:41 +02:00
|
|
|
const keys = Object.keys(Traders).filter((x) => Traders[x] === traderId);
|
2023-07-21 19:08:32 +02:00
|
|
|
|
2023-08-02 09:29:23 +02:00
|
|
|
if (keys.length === 0)
|
|
|
|
{
|
2024-05-21 13:40:16 +02:00
|
|
|
this.logger.error(this.localisationService.getText("trader-unable_to_find_trader_in_enum", traderId));
|
2023-08-02 09:29:23 +02:00
|
|
|
|
2024-05-27 18:05:16 +02:00
|
|
|
return undefined;
|
2023-08-02 09:29:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return keys[0] as Traders;
|
2023-07-21 19:08:32 +02:00
|
|
|
}
|
2023-08-04 12:19:27 +02:00
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
/**
|
2023-11-16 02:35:05 +01:00
|
|
|
* Validates that the provided traderEnumValue exists in the Traders enum. If the value is valid, it returns the
|
|
|
|
* same enum value, effectively serving as a trader ID; otherwise, it logs an error and returns an empty string.
|
2023-10-10 13:03:20 +02:00
|
|
|
* This method provides a runtime check to prevent undefined behavior when using the enum as a dictionary key.
|
2023-11-16 02:35:05 +01:00
|
|
|
*
|
2023-10-10 13:03:20 +02:00
|
|
|
* For example, instead of this:
|
|
|
|
* `const traderId = Traders[Traders.PRAPOR];`
|
2023-11-16 02:35:05 +01:00
|
|
|
*
|
2023-10-10 13:03:20 +02:00
|
|
|
* You can use safely use this:
|
|
|
|
* `const traderId = this.traderHelper.getValidTraderIdByEnumValue(Traders.PRAPOR);`
|
2023-11-16 02:35:05 +01:00
|
|
|
*
|
2023-10-10 13:03:20 +02:00
|
|
|
* @param traderEnumValue The trader enum value to validate
|
|
|
|
* @returns The validated trader enum value as a string, or an empty string if invalid
|
|
|
|
*/
|
|
|
|
public getValidTraderIdByEnumValue(traderEnumValue: Traders): string
|
|
|
|
{
|
|
|
|
if (!this.traderEnumHasKey(traderEnumValue))
|
|
|
|
{
|
2024-05-21 13:40:16 +02:00
|
|
|
this.logger.error(this.localisationService.getText("trader-unable_to_find_trader_in_enum", traderEnumValue));
|
2023-10-10 13:03:20 +02:00
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
2023-11-16 02:35:05 +01:00
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
return Traders[traderEnumValue];
|
|
|
|
}
|
|
|
|
|
2023-08-04 12:19:27 +02:00
|
|
|
/**
|
|
|
|
* Does the 'Traders' enum has a value that matches the passed in parameter
|
2023-10-10 13:03:20 +02:00
|
|
|
* @param key Value to check for
|
2023-08-04 12:19:27 +02:00
|
|
|
* @returns True, values exists in Traders enum as a value
|
|
|
|
*/
|
2023-10-10 13:03:20 +02:00
|
|
|
public traderEnumHasKey(key: string): boolean
|
|
|
|
{
|
2024-05-17 21:32:41 +02:00
|
|
|
return Object.keys(Traders).some((x) => x === key);
|
2023-10-10 13:03:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Accepts a trader id
|
|
|
|
* @param traderId Trader id
|
|
|
|
* @returns Ttrue if Traders enum has the param as a value
|
|
|
|
*/
|
|
|
|
public traderEnumHasValue(traderId: string): boolean
|
2023-08-04 12:19:27 +02:00
|
|
|
{
|
2024-05-17 21:32:41 +02:00
|
|
|
return Object.values(Traders).some((x) => x === traderId);
|
2023-08-04 12:19:27 +02:00
|
|
|
}
|
2023-11-16 02:35:05 +01:00
|
|
|
}
|