diff --git a/project/assets/configs/insurance.json b/project/assets/configs/insurance.json index 9a57739f..145aaa34 100644 --- a/project/assets/configs/insurance.json +++ b/project/assets/configs/insurance.json @@ -17,5 +17,7 @@ "patron_in_weapon" ], "returnTimeOverrideSeconds": 0, - "runIntervalSeconds": 600 + "runIntervalSeconds": 600, + "minAttachmentRoublePriceToBeTaken": 2000, + "chanceNoAttachmentsTakenPercent": 10 } \ No newline at end of file diff --git a/project/src/controllers/InsuranceController.ts b/project/src/controllers/InsuranceController.ts index 7bb44a3e..5fa4ceb9 100644 --- a/project/src/controllers/InsuranceController.ts +++ b/project/src/controllers/InsuranceController.ts @@ -4,6 +4,7 @@ import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; +import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { IGetInsuranceCostRequestData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostRequestData"; @@ -25,8 +26,9 @@ import { MailSendService } from "@spt-aki/services/MailSendService"; import { PaymentService } from "@spt-aki/services/PaymentService"; import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService"; import { HashUtil } from "@spt-aki/utils/HashUtil"; +import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { MathUtil } from "@spt-aki/utils/MathUtil"; -import { RandomUtil } from "@spt-aki/utils/RandomUtil"; +import { ProbabilityObject, ProbabilityObjectArray, RandomUtil } from "@spt-aki/utils/RandomUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil"; @injectable() @@ -39,6 +41,7 @@ export class InsuranceController @inject("WinstonLogger") protected logger: ILogger, @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("MathUtil") protected mathUtil: MathUtil, + @inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("HashUtil") protected hashUtil: HashUtil, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("TimeUtil") protected timeUtil: TimeUtil, @@ -47,6 +50,7 @@ export class InsuranceController @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, + @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("PaymentService") protected paymentService: PaymentService, @inject("InsuranceService") protected insuranceService: InsuranceService, @@ -448,83 +452,77 @@ export class InsuranceController */ protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set): void { - const sortedAttachments = this.sortAttachmentsByPrice(attachments); - this.logAttachmentsDetails(sortedAttachments); + // Create dict of item ids + their flea/handbook price (highest is chosen) + const weightedAttachmentByPrice = this.weightAttachmentsByPrice(attachments); - const successfulRolls = this.countSuccessfulRolls(sortedAttachments, traderId); - this.logger.debug(`Number of attachments to be deleted: ${successfulRolls}`); + // Get how many attachments we want to pull off parent + const countOfAttachmentsToRemove = this.getAttachmentCountToRemove(weightedAttachmentByPrice, traderId); - this.attachmentDeletionByValue(sortedAttachments, successfulRolls, toDelete); + // Create prob array and add all attachments with rouble price as the weight + const attachmentsProbabilityArray = new ProbabilityObjectArray(this.mathUtil, this.jsonUtil); + for (const attachmentTpl of Object.keys(weightedAttachmentByPrice)) + { + attachmentsProbabilityArray.push( + new ProbabilityObject(attachmentTpl, weightedAttachmentByPrice[attachmentTpl]), + ); + } + + // Draw x attachments from weighted array to remove from parent, remove from pool after being picked + const attachmentIdsToRemove = attachmentsProbabilityArray.draw(countOfAttachmentsToRemove, false); + for (const attachmentId of attachmentIdsToRemove) + { + toDelete.add(attachmentId); + } + + this.logger.debug(`Number of attachments to be deleted: ${attachmentIdsToRemove.length}`); } - /** - * Sorts the attachment items by their dynamic price in descending order. - * - * @param attachments The array of attachments items. - * @returns An array of items enriched with their max price and common locale-name. - */ - protected sortAttachmentsByPrice(attachments: Item[]): EnrichedItem[] + protected weightAttachmentsByPrice(attachments: Item[]): Record { - return attachments.map((item) => ({ - ...item, - name: this.itemHelper.getItemName(item._tpl), - dynamicPrice: this.ragfairPriceService.getDynamicItemPrice(item._tpl, this.roubleTpl, item, null, false), - })).sort((a, b) => b.dynamicPrice - a.dynamicPrice); - } + const result: Record = {}; - /** - * Logs the details of each attachment item. - * - * @param attachments The array of attachment items. - */ - protected logAttachmentsDetails(attachments: EnrichedItem[]): void - { - let index = 1; + // Get a dictionary of item tpls + their rouble price for (const attachment of attachments) { - this.logger.debug(`Attachment ${index}: "${attachment.name}" - Price: ${attachment.dynamicPrice}`); - index++; + const price = this.ragfairPriceService.getDynamicItemPrice(attachment._tpl, this.roubleTpl); + result[attachment._id] = Math.round(price); } + + this.weightedRandomHelper.reduceWeightValues(result); + + return result; } /** - * Counts the number of successful rolls for the attachment items. - * - * @param attachments The array of attachment items. - * @param traderId The ID of the trader that has insured these attachments. - * @returns The number of successful rolls. + * Get count of items to remove from weapon (take into account trader + price of attachment) + * @param weightedAttachmentByPrice Dict of item Tpls and thier rouble price + * @param traderId Trader attachment insured against + * @returns Attachment count to remove */ - protected countSuccessfulRolls(attachments: Item[], traderId: string): number + protected getAttachmentCountToRemove(weightedAttachmentByPrice: Record, traderId: string): number { - const rolls = Array.from({ length: attachments.length }, () => this.rollForDelete(traderId)); - return rolls.filter(Boolean).length; - } + let removeCount = 0; - /** - * Marks the most valuable attachments for deletion based on the number of successful rolls made. - * - * @param attachments The array of attachment items. - * @param successfulRolls The number of successful rolls. - * @param toDelete The array that accumulates the IDs of the items to be deleted. - */ - protected attachmentDeletionByValue( - attachments: EnrichedItem[], - successfulRolls: number, - toDelete: Set, - ): void - { - const valuableToDelete = attachments.slice(0, successfulRolls).map(({ _id }) => _id); - - for (const attachmentsId of valuableToDelete) + if (this.randomUtil.getChance100(this.insuranceConfig.chanceNoAttachmentsTakenPercent)) { - const valuableChild = attachments.find(({ _id }) => _id === attachmentsId); - if (valuableChild) + return removeCount; + } + + for (const attachmentId of Object.keys(weightedAttachmentByPrice)) + { + // Below min price to be taken, skip + if (weightedAttachmentByPrice[attachmentId] < this.insuranceConfig.minAttachmentRoublePriceToBeTaken) { - const { name, dynamicPrice } = valuableChild; - this.logger.debug(`Marked attachment "${name}" for removal - Dyanmic Price: ${dynamicPrice}`); - toDelete.add(attachmentsId); + continue; + } + + if (this.rollForDelete(traderId)) + { + removeCount++; } } + + return removeCount; } /** @@ -588,7 +586,7 @@ export class InsuranceController } /** - * Determines whether a insured item should be removed from the player's inventory based on a random roll and + * Determines whether an insured item should be removed from the player's inventory based on a random roll and * trader-specific return chance. * * @param traderId The ID of the trader who insured the item. diff --git a/project/src/models/spt/config/IInsuranceConfig.ts b/project/src/models/spt/config/IInsuranceConfig.ts index 925280aa..40855ae1 100644 --- a/project/src/models/spt/config/IInsuranceConfig.ts +++ b/project/src/models/spt/config/IInsuranceConfig.ts @@ -15,4 +15,8 @@ export interface IInsuranceConfig extends IBaseConfig returnTimeOverrideSeconds: number; /** How often server should process insurance in seconds */ runIntervalSeconds: number; + // Lowest rouble price for an attachment to be allowed to be taken + minAttachmentRoublePriceToBeTaken: number; + // Chance out of 100% no attachments from a parent are taken + chanceNoAttachmentsTakenPercent: number; }