Reworked how insurance picks attachments to delete before return

Now has a chance to not pick any to remove (default 10%)
Now only removes attachments that are above a rouble price (default 2000)

Stores attachments in a dictionary weighted by rouble price
Picks random amount of attachments to remove and then picks from pool by price, removing items from pool as they're picked
This commit is contained in:
Dev 2024-05-06 15:54:29 +01:00
parent 9fb1d9728e
commit 77a5b0a4b4
3 changed files with 65 additions and 61 deletions

View File

@ -17,5 +17,7 @@
"patron_in_weapon"
],
"returnTimeOverrideSeconds": 0,
"runIntervalSeconds": 600
"runIntervalSeconds": 600,
"minAttachmentRoublePriceToBeTaken": 2000,
"chanceNoAttachmentsTakenPercent": 10
}

View File

@ -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<string>): 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<string, number>(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<string, number>
{
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<string, number> = {};
/**
* 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<string, number>, 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<string>,
): 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.

View File

@ -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;
}