Merge branch '3.8.2-DEV' into 3.9.0-DEV
This commit is contained in:
commit
735624eb9c
21
.gitea/workflows/clear-itemdb-cache.yaml
Normal file
21
.gitea/workflows/clear-itemdb-cache.yaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: Clear Item DB Website Cache
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
clear-cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- id: filter
|
||||||
|
uses: dorny/paths-filter@v3.0.2
|
||||||
|
with:
|
||||||
|
list-files: shell
|
||||||
|
filters: |
|
||||||
|
database:
|
||||||
|
- 'project/assets/database/**'
|
||||||
|
- name: Send Refresh Request
|
||||||
|
if: steps.filter.outputs.database == 'true'
|
||||||
|
run: curl --max-time 30 https://db.sp-tarkov.com/api/refresh
|
@ -17,5 +17,7 @@
|
|||||||
"patron_in_weapon"
|
"patron_in_weapon"
|
||||||
],
|
],
|
||||||
"returnTimeOverrideSeconds": 0,
|
"returnTimeOverrideSeconds": 0,
|
||||||
"runIntervalSeconds": 600
|
"runIntervalSeconds": 600,
|
||||||
|
"minAttachmentRoublePriceToBeTaken": 2000,
|
||||||
|
"chanceNoAttachmentsTakenPercent": 10
|
||||||
}
|
}
|
@ -33,7 +33,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"atomically": "~1.7",
|
"atomically": "~1.7",
|
||||||
"buffer-crc32": "^1.0.0",
|
"buffer-crc32": "^1.0.0",
|
||||||
"closest-match": "~1.3",
|
|
||||||
"date-fns": "~2.30",
|
"date-fns": "~2.30",
|
||||||
"date-fns-tz": "~2.0",
|
"date-fns-tz": "~2.0",
|
||||||
"i18n": "~0.15",
|
"i18n": "~0.15",
|
||||||
@ -44,6 +43,7 @@
|
|||||||
"reflect-metadata": "~0.2",
|
"reflect-metadata": "~0.2",
|
||||||
"semver": "~7.6",
|
"semver": "~7.6",
|
||||||
"source-map-support": "~0.5",
|
"source-map-support": "~0.5",
|
||||||
|
"string-similarity-js": "~2.1",
|
||||||
"tsyringe": "~4.8",
|
"tsyringe": "~4.8",
|
||||||
"typescript": "~5.4",
|
"typescript": "~5.4",
|
||||||
"winston": "~3.12",
|
"winston": "~3.12",
|
||||||
|
@ -4,6 +4,7 @@ import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper";
|
|||||||
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
||||||
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
|
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
|
||||||
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
|
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
|
||||||
|
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
|
||||||
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
|
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
|
||||||
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
|
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
|
||||||
import { IGetInsuranceCostRequestData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostRequestData";
|
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 { PaymentService } from "@spt-aki/services/PaymentService";
|
||||||
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
|
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
|
||||||
import { HashUtil } from "@spt-aki/utils/HashUtil";
|
import { HashUtil } from "@spt-aki/utils/HashUtil";
|
||||||
|
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
|
||||||
import { MathUtil } from "@spt-aki/utils/MathUtil";
|
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";
|
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -39,6 +41,7 @@ export class InsuranceController
|
|||||||
@inject("WinstonLogger") protected logger: ILogger,
|
@inject("WinstonLogger") protected logger: ILogger,
|
||||||
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
||||||
@inject("MathUtil") protected mathUtil: MathUtil,
|
@inject("MathUtil") protected mathUtil: MathUtil,
|
||||||
|
@inject("JsonUtil") protected jsonUtil: JsonUtil,
|
||||||
@inject("HashUtil") protected hashUtil: HashUtil,
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
||||||
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
|
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
|
||||||
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
||||||
@ -47,6 +50,7 @@ export class InsuranceController
|
|||||||
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
||||||
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
||||||
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
|
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
|
||||||
|
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
|
||||||
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
||||||
@inject("PaymentService") protected paymentService: PaymentService,
|
@inject("PaymentService") protected paymentService: PaymentService,
|
||||||
@inject("InsuranceService") protected insuranceService: InsuranceService,
|
@inject("InsuranceService") protected insuranceService: InsuranceService,
|
||||||
@ -448,83 +452,100 @@ export class InsuranceController
|
|||||||
*/
|
*/
|
||||||
protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set<string>): void
|
protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set<string>): void
|
||||||
{
|
{
|
||||||
const sortedAttachments = this.sortAttachmentsByPrice(attachments);
|
// Create dict of item ids + their flea/handbook price (highest is chosen)
|
||||||
this.logAttachmentsDetails(sortedAttachments);
|
const weightedAttachmentByPrice = this.weightAttachmentsByPrice(attachments);
|
||||||
|
|
||||||
const successfulRolls = this.countSuccessfulRolls(sortedAttachments, traderId);
|
// Get how many attachments we want to pull off parent
|
||||||
this.logger.debug(`Number of attachments to be deleted: ${successfulRolls}`);
|
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.logAttachmentsBeingRemoved(attachmentIdsToRemove, attachments, weightedAttachmentByPrice);
|
||||||
|
|
||||||
|
this.logger.debug(`Number of attachments to be deleted: ${attachmentIdsToRemove.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected logAttachmentsBeingRemoved(
|
||||||
* Sorts the attachment items by their dynamic price in descending order.
|
attachmentIdsToRemove: string[],
|
||||||
*
|
attachments: Item[],
|
||||||
* @param attachments The array of attachments items.
|
attachmentPrices: Record<string, number>,
|
||||||
* @returns An array of items enriched with their max price and common locale-name.
|
): void
|
||||||
*/
|
|
||||||
protected sortAttachmentsByPrice(attachments: Item[]): EnrichedItem[]
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs the details of each attachment item.
|
|
||||||
*
|
|
||||||
* @param attachments The array of attachment items.
|
|
||||||
*/
|
|
||||||
protected logAttachmentsDetails(attachments: EnrichedItem[]): void
|
|
||||||
{
|
{
|
||||||
let index = 1;
|
let index = 1;
|
||||||
for (const attachment of attachments)
|
for (const attachmentId of attachmentIdsToRemove)
|
||||||
{
|
{
|
||||||
this.logger.debug(`Attachment ${index}: "${attachment.name}" - Price: ${attachment.dynamicPrice}`);
|
this.logger.debug(
|
||||||
|
`Attachment ${index} Id: ${attachmentId} Tpl: ${
|
||||||
|
attachments.find((x) => x._id === attachmentId)?._tpl
|
||||||
|
} - Price: ${attachmentPrices[attachmentId]}`,
|
||||||
|
);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected weightAttachmentsByPrice(attachments: Item[]): Record<string, number>
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
protected countSuccessfulRolls(attachments: Item[], traderId: string): number
|
|
||||||
{
|
{
|
||||||
const rolls = Array.from({ length: attachments.length }, () => this.rollForDelete(traderId));
|
const result: Record<string, number> = {};
|
||||||
return rolls.filter(Boolean).length;
|
|
||||||
|
// Get a dictionary of item tpls + their rouble price
|
||||||
|
for (const attachment of attachments)
|
||||||
|
{
|
||||||
|
const price = this.ragfairPriceService.getDynamicItemPrice(attachment._tpl, this.roubleTpl);
|
||||||
|
if (price)
|
||||||
|
{
|
||||||
|
result[attachment._id] = Math.round(price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.weightedRandomHelper.reduceWeightValues(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the most valuable attachments for deletion based on the number of successful rolls made.
|
* 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 attachments The array of attachment items.
|
* @param traderId Trader attachment insured against
|
||||||
* @param successfulRolls The number of successful rolls.
|
* @returns Attachment count to remove
|
||||||
* @param toDelete The array that accumulates the IDs of the items to be deleted.
|
|
||||||
*/
|
*/
|
||||||
protected attachmentDeletionByValue(
|
protected getAttachmentCountToRemove(weightedAttachmentByPrice: Record<string, number>, traderId: string): number
|
||||||
attachments: EnrichedItem[],
|
|
||||||
successfulRolls: number,
|
|
||||||
toDelete: Set<string>,
|
|
||||||
): void
|
|
||||||
{
|
{
|
||||||
const valuableToDelete = attachments.slice(0, successfulRolls).map(({ _id }) => _id);
|
let removeCount = 0;
|
||||||
|
|
||||||
for (const attachmentsId of valuableToDelete)
|
if (this.randomUtil.getChance100(this.insuranceConfig.chanceNoAttachmentsTakenPercent))
|
||||||
{
|
{
|
||||||
const valuableChild = attachments.find(({ _id }) => _id === attachmentsId);
|
return removeCount;
|
||||||
if (valuableChild)
|
}
|
||||||
|
|
||||||
|
for (const attachmentId of Object.keys(weightedAttachmentByPrice))
|
||||||
|
{
|
||||||
|
// Below min price to be taken, skip
|
||||||
|
if (weightedAttachmentByPrice[attachmentId] < this.insuranceConfig.minAttachmentRoublePriceToBeTaken)
|
||||||
{
|
{
|
||||||
const { name, dynamicPrice } = valuableChild;
|
continue;
|
||||||
this.logger.debug(`Marked attachment "${name}" for removal - Dyanmic Price: ${dynamicPrice}`);
|
}
|
||||||
toDelete.add(attachmentsId);
|
|
||||||
|
if (this.rollForDelete(traderId))
|
||||||
|
{
|
||||||
|
removeCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return removeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -588,7 +609,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.
|
* trader-specific return chance.
|
||||||
*
|
*
|
||||||
* @param traderId The ID of the trader who insured the item.
|
* @param traderId The ID of the trader who insured the item.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { inject, injectable } from "tsyringe";
|
import { inject, injectable } from "tsyringe";
|
||||||
|
|
||||||
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
||||||
|
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
|
||||||
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
|
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
|
||||||
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
|
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
|
||||||
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
|
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
|
||||||
@ -22,6 +23,8 @@ export class PMCLootGenerator
|
|||||||
protected backpackLootPool: Record<string, number> = {};
|
protected backpackLootPool: Record<string, number> = {};
|
||||||
protected pmcConfig: IPmcConfig;
|
protected pmcConfig: IPmcConfig;
|
||||||
|
|
||||||
|
protected roubleTpl = "5449016a4bdc2d6f028b456f";
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
||||||
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
||||||
@ -29,6 +32,7 @@ export class PMCLootGenerator
|
|||||||
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
|
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
|
||||||
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
|
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
|
||||||
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
|
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
|
||||||
|
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
|
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
|
||||||
@ -74,7 +78,7 @@ export class PMCLootGenerator
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Set price of item as its weight
|
// Set price of item as its weight
|
||||||
const price = this.ragfairPriceService.getFleaPriceForItem(itemToAdd._id);
|
const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, this.roubleTpl);
|
||||||
this.pocketLootPool[itemToAdd._id] = price;
|
this.pocketLootPool[itemToAdd._id] = price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +91,7 @@ export class PMCLootGenerator
|
|||||||
this.pocketLootPool[key] = Math.round((1 / this.pocketLootPool[key]) * highestPrice);
|
this.pocketLootPool[key] = Math.round((1 / this.pocketLootPool[key]) * highestPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reduceWeightValues(this.pocketLootPool);
|
this.weightedRandomHelper.reduceWeightValues(this.pocketLootPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.pocketLootPool;
|
return this.pocketLootPool;
|
||||||
@ -132,7 +136,7 @@ export class PMCLootGenerator
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Set price of item as its weight
|
// Set price of item as its weight
|
||||||
const price = this.ragfairPriceService.getFleaPriceForItem(itemToAdd._id);
|
const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, this.roubleTpl);
|
||||||
this.vestLootPool[itemToAdd._id] = price;
|
this.vestLootPool[itemToAdd._id] = price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +149,7 @@ export class PMCLootGenerator
|
|||||||
this.vestLootPool[key] = Math.round((1 / this.vestLootPool[key]) * highestPrice);
|
this.vestLootPool[key] = Math.round((1 / this.vestLootPool[key]) * highestPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reduceWeightValues(this.vestLootPool);
|
this.weightedRandomHelper.reduceWeightValues(this.vestLootPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.vestLootPool;
|
return this.vestLootPool;
|
||||||
@ -200,7 +204,7 @@ export class PMCLootGenerator
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Set price of item as its weight
|
// Set price of item as its weight
|
||||||
const price = this.ragfairPriceService.getFleaPriceForItem(itemToAdd._id);
|
const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, this.roubleTpl);
|
||||||
this.backpackLootPool[itemToAdd._id] = price;
|
this.backpackLootPool[itemToAdd._id] = price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,71 +217,9 @@ export class PMCLootGenerator
|
|||||||
this.backpackLootPool[key] = Math.round((1 / this.backpackLootPool[key]) * highestPrice);
|
this.backpackLootPool[key] = Math.round((1 / this.backpackLootPool[key]) * highestPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reduceWeightValues(this.backpackLootPool);
|
this.weightedRandomHelper.reduceWeightValues(this.backpackLootPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.backpackLootPool;
|
return this.backpackLootPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the greated common divisor of all weights and use it on the passed in dictionary
|
|
||||||
* @param weightedDict
|
|
||||||
*/
|
|
||||||
protected reduceWeightValues(weightedDict: Record<string, number>): void
|
|
||||||
{
|
|
||||||
// No values, nothing to reduce
|
|
||||||
if (Object.keys(weightedDict).length === 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only one value, set to 1 and exit
|
|
||||||
if (Object.keys(weightedDict).length === 1)
|
|
||||||
{
|
|
||||||
const key = Object.keys(weightedDict)[0];
|
|
||||||
weightedDict[key] = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const weights = Object.values(weightedDict).slice();
|
|
||||||
const commonDivisor = this.commonDivisor(weights);
|
|
||||||
|
|
||||||
// No point in dividing by 1
|
|
||||||
if (commonDivisor === 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in weightedDict)
|
|
||||||
{
|
|
||||||
if (Object.hasOwn(weightedDict, key))
|
|
||||||
{
|
|
||||||
weightedDict[key] /= commonDivisor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected commonDivisor(numbers: number[]): number
|
|
||||||
{
|
|
||||||
let result = numbers[0];
|
|
||||||
for (let i = 1; i < numbers.length; i++)
|
|
||||||
{
|
|
||||||
result = this.gcd(result, numbers[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected gcd(a: number, b: number): number
|
|
||||||
{
|
|
||||||
let x = a;
|
|
||||||
let y = b;
|
|
||||||
while (y !== 0)
|
|
||||||
{
|
|
||||||
const temp = y;
|
|
||||||
y = x % y;
|
|
||||||
x = temp;
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISpt
|
|||||||
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
||||||
import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
|
import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
|
||||||
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
|
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
|
||||||
|
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
|
||||||
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
|
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
|
||||||
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
|
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
|
||||||
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
|
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
|
||||||
@ -13,7 +14,7 @@ import { LocaleService } from "@spt-aki/services/LocaleService";
|
|||||||
import { MailSendService } from "@spt-aki/services/MailSendService";
|
import { MailSendService } from "@spt-aki/services/MailSendService";
|
||||||
import { HashUtil } from "@spt-aki/utils/HashUtil";
|
import { HashUtil } from "@spt-aki/utils/HashUtil";
|
||||||
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
|
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
|
||||||
import { closestMatch, distance } from "closest-match";
|
import { stringSimilarity } from "string-similarity-js";
|
||||||
import { inject, injectable } from "tsyringe";
|
import { inject, injectable } from "tsyringe";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -28,7 +29,7 @@ export class GiveSptCommand implements ISptCommand
|
|||||||
* spt give 5 <== this is the reply when the algo isn't sure about an item
|
* spt give 5 <== this is the reply when the algo isn't sure about an item
|
||||||
*/
|
*/
|
||||||
private static commandRegex = /^spt give (((([a-z]{2,5}) )?"(.+)"|\w+) )?([0-9]+)$/;
|
private static commandRegex = /^spt give (((([a-z]{2,5}) )?"(.+)"|\w+) )?([0-9]+)$/;
|
||||||
private static maxAllowedDistance = 1.5;
|
private static acceptableConfidence = 0.9;
|
||||||
|
|
||||||
protected savedCommand: Map<string, SavedCommand> = new Map<string, SavedCommand>();
|
protected savedCommand: Map<string, SavedCommand> = new Map<string, SavedCommand>();
|
||||||
|
|
||||||
@ -125,44 +126,52 @@ export class GiveSptCommand implements ISptCommand
|
|||||||
|
|
||||||
if (isItemName)
|
if (isItemName)
|
||||||
{
|
{
|
||||||
locale = result[4] ? result[4] : this.localeService.getDesiredGameLocale();
|
try
|
||||||
if (!this.localeService.getServerSupportedLocales().includes(locale))
|
{
|
||||||
|
locale = result[4] ? result[4] : (this.localeService.getDesiredGameLocale() ?? "en");
|
||||||
|
if (!this.localeService.getServerSupportedLocales().includes(locale))
|
||||||
|
{
|
||||||
|
this.mailSendService.sendUserMessageToPlayer(
|
||||||
|
sessionId,
|
||||||
|
commandHandler,
|
||||||
|
`Unknown locale "${locale}". Use \"help\" for more information.`,
|
||||||
|
);
|
||||||
|
return request.dialogId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
{
|
{
|
||||||
this.mailSendService.sendUserMessageToPlayer(
|
this.mailSendService.sendUserMessageToPlayer(
|
||||||
sessionId,
|
sessionId,
|
||||||
commandHandler,
|
commandHandler,
|
||||||
`Unknown locale "${locale}". Use \"help\" for more information.`,
|
`An error occurred while trying to use localized text. Locale will be defaulted to 'en'.`,
|
||||||
);
|
);
|
||||||
return request.dialogId;
|
this.logger.warning(e);
|
||||||
|
locale = "en";
|
||||||
}
|
}
|
||||||
|
|
||||||
const localizedGlobal = this.databaseServer.getTables().locales.global[locale];
|
const localizedGlobal = this.databaseServer.getTables().locales.global[locale] ??
|
||||||
|
this.databaseServer.getTables().locales.global.en;
|
||||||
|
|
||||||
const closestItemsMatchedByName = closestMatch(
|
const closestItemsMatchedByName = this.itemHelper.getItems()
|
||||||
item.toLowerCase(),
|
.filter((i) => this.isItemAllowed(i))
|
||||||
this.itemHelper.getItems().filter((i) => i._type !== "Node").filter((i) =>
|
.map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase() ?? i._props.Name)
|
||||||
!this.itemFilterService.isItemBlacklisted(i._id)
|
.filter((i) => i !== undefined && i !== "")
|
||||||
).map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase()).filter((i) => i !== undefined),
|
.map(i => ({match: stringSimilarity(item.toLocaleLowerCase(), i.toLocaleLowerCase()), itemName: i}))
|
||||||
true,
|
.sort((a1, a2) => a2.match - a1.match);
|
||||||
) as string[];
|
|
||||||
|
|
||||||
if (closestItemsMatchedByName === undefined || closestItemsMatchedByName.length === 0)
|
if (closestItemsMatchedByName[0].match >= GiveSptCommand.acceptableConfidence)
|
||||||
{
|
{
|
||||||
this.mailSendService.sendUserMessageToPlayer(
|
item = closestItemsMatchedByName[0].itemName;
|
||||||
sessionId,
|
|
||||||
commandHandler,
|
|
||||||
"That item could not be found. Please refine your request and try again.",
|
|
||||||
);
|
|
||||||
return request.dialogId;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (closestItemsMatchedByName.length > 1)
|
{
|
||||||
{
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
const slicedItems = closestItemsMatchedByName.slice(0, 10);
|
const slicedItems = closestItemsMatchedByName.slice(0, 10);
|
||||||
// max 10 item names and map them
|
// max 10 item names and map them
|
||||||
const itemList = slicedItems.map((itemName) => `${i++}. ${itemName}`).join("\n");
|
const itemList = slicedItems.map((match) => `${i++}. ${match.itemName} (conf: ${(match.match * 100).toFixed(2)})`)
|
||||||
this.savedCommand.set(sessionId, new SavedCommand(quantity, slicedItems, locale));
|
.join("\n");
|
||||||
|
this.savedCommand.set(sessionId, new SavedCommand(quantity, slicedItems.map(i => i.itemName), locale));
|
||||||
this.mailSendService.sendUserMessageToPlayer(
|
this.mailSendService.sendUserMessageToPlayer(
|
||||||
sessionId,
|
sessionId,
|
||||||
commandHandler,
|
commandHandler,
|
||||||
@ -170,30 +179,15 @@ export class GiveSptCommand implements ISptCommand
|
|||||||
);
|
);
|
||||||
return request.dialogId;
|
return request.dialogId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dist = distance(item, closestItemsMatchedByName[0]);
|
|
||||||
if (dist > GiveSptCommand.maxAllowedDistance)
|
|
||||||
{
|
|
||||||
this.mailSendService.sendUserMessageToPlayer(
|
|
||||||
sessionId,
|
|
||||||
commandHandler,
|
|
||||||
`Found a possible match for "${item}" but uncertain. Match: "${
|
|
||||||
closestItemsMatchedByName[0]
|
|
||||||
}". Please refine your request and try again.`,
|
|
||||||
);
|
|
||||||
return request.dialogId;
|
|
||||||
}
|
|
||||||
// Only one available so we get that entry and use it
|
|
||||||
item = closestItemsMatchedByName[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If item is an item name, we need to search using that item name and the locale which one we want otherwise
|
// If item is an item name, we need to search using that item name and the locale which one we want otherwise
|
||||||
// item is just the tplId.
|
// item is just the tplId.
|
||||||
const tplId = isItemName
|
const tplId = isItemName
|
||||||
? this.itemHelper.getItems().filter((i) => !this.itemFilterService.isItemBlacklisted(i._id)).find((i) =>
|
? this.itemHelper.getItems()
|
||||||
this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() === item
|
.filter((i) => this.isItemAllowed(i))
|
||||||
)._id
|
.find((i) => (this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() ?? i._props.Name) === item)._id
|
||||||
: item;
|
: item;
|
||||||
|
|
||||||
const checkedItem = this.itemHelper.getItem(tplId);
|
const checkedItem = this.itemHelper.getItem(tplId);
|
||||||
@ -285,4 +279,21 @@ export class GiveSptCommand implements ISptCommand
|
|||||||
this.mailSendService.sendSystemMessageToPlayer(sessionId, "SPT GIVE", itemsToSend);
|
this.mailSendService.sendSystemMessageToPlayer(sessionId, "SPT GIVE", itemsToSend);
|
||||||
return request.dialogId;
|
return request.dialogId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "simple" function that checks if an item is supposed to be given to a player or not
|
||||||
|
* @param templateItem the template item to check
|
||||||
|
* @returns true if its obtainable, false if its not
|
||||||
|
*/
|
||||||
|
protected isItemAllowed(templateItem: ITemplateItem): boolean
|
||||||
|
{
|
||||||
|
return templateItem._type !== "Node" &&
|
||||||
|
!this.itemHelper.isQuestItem(templateItem._id) &&
|
||||||
|
!this.itemFilterService.isItemBlacklisted(templateItem._id) &&
|
||||||
|
(templateItem._props?.Prefab?.path ?? "") !== "" &&
|
||||||
|
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.HIDEOUT_AREA_CONTAINER) &&
|
||||||
|
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.LOOT_CONTAINER) &&
|
||||||
|
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.RANDOM_LOOT_CONTAINER) &&
|
||||||
|
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.MOB_CONTAINER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,4 +92,66 @@ export class WeightedRandomHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the greated common divisor of all weights and use it on the passed in dictionary
|
||||||
|
* @param weightedDict values to reduce
|
||||||
|
*/
|
||||||
|
public reduceWeightValues(weightedDict: Record<string, number>): void
|
||||||
|
{
|
||||||
|
// No values, nothing to reduce
|
||||||
|
if (Object.keys(weightedDict).length === 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only one value, set to 1 and exit
|
||||||
|
if (Object.keys(weightedDict).length === 1)
|
||||||
|
{
|
||||||
|
const key = Object.keys(weightedDict)[0];
|
||||||
|
weightedDict[key] = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const weights = Object.values(weightedDict).slice();
|
||||||
|
const commonDivisor = this.commonDivisor(weights);
|
||||||
|
|
||||||
|
// No point in dividing by 1
|
||||||
|
if (commonDivisor === 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in weightedDict)
|
||||||
|
{
|
||||||
|
if (Object.hasOwn(weightedDict, key))
|
||||||
|
{
|
||||||
|
weightedDict[key] /= commonDivisor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected commonDivisor(numbers: number[]): number
|
||||||
|
{
|
||||||
|
let result = numbers[0];
|
||||||
|
for (let i = 1; i < numbers.length; i++)
|
||||||
|
{
|
||||||
|
result = this.gcd(result, numbers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected gcd(a: number, b: number): number
|
||||||
|
{
|
||||||
|
let x = a;
|
||||||
|
let y = b;
|
||||||
|
while (y !== 0)
|
||||||
|
{
|
||||||
|
const temp = y;
|
||||||
|
y = x % y;
|
||||||
|
x = temp;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,4 +111,5 @@ export enum BaseClasses
|
|||||||
BARREL = "555ef6e44bdc2de9068b457e",
|
BARREL = "555ef6e44bdc2de9068b457e",
|
||||||
CHARGING_HANDLE = "55818a6f4bdc2db9688b456b",
|
CHARGING_HANDLE = "55818a6f4bdc2db9688b456b",
|
||||||
COMB_MUZZLE_DEVICE = "550aa4dd4bdc2dc9348b4569 ",
|
COMB_MUZZLE_DEVICE = "550aa4dd4bdc2dc9348b4569 ",
|
||||||
|
HIDEOUT_AREA_CONTAINER = "63da6da4784a55176c018dba"
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,8 @@ export interface IInsuranceConfig extends IBaseConfig
|
|||||||
returnTimeOverrideSeconds: number;
|
returnTimeOverrideSeconds: number;
|
||||||
/** How often server should process insurance in seconds */
|
/** How often server should process insurance in seconds */
|
||||||
runIntervalSeconds: number;
|
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;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ export class BundleSerializer extends Serializer
|
|||||||
{
|
{
|
||||||
this.logger.info(`[BUNDLE]: ${req.url}`);
|
this.logger.info(`[BUNDLE]: ${req.url}`);
|
||||||
|
|
||||||
const key = req.url.split("/bundle/")[1];
|
const key = decodeURI(req.url.split("/bundle/")[1]);
|
||||||
const bundle = this.bundleLoader.getBundle(key);
|
const bundle = this.bundleLoader.getBundle(key);
|
||||||
|
|
||||||
this.httpFileUtil.sendFile(resp, `${bundle.modpath}/bundles/${bundle.filename}`);
|
this.httpFileUtil.sendFile(resp, `${bundle.modpath}/bundles/${bundle.filename}`);
|
||||||
|
@ -168,6 +168,13 @@ export class RagfairOfferService
|
|||||||
{
|
{
|
||||||
const trader = this.databaseServer.getTables().traders[traderID];
|
const trader = this.databaseServer.getTables().traders[traderID];
|
||||||
|
|
||||||
|
if (!trader || !trader.base)
|
||||||
|
{
|
||||||
|
this.logger.error(`Trader ${traderID} lacks a base file, cannot check for refresh status`);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// No value, occurs when first run, trader offers need to be added to flea
|
// No value, occurs when first run, trader offers need to be added to flea
|
||||||
if (typeof trader.base.refreshTraderRagfairOffers !== "boolean")
|
if (typeof trader.base.refreshTraderRagfairOffers !== "boolean")
|
||||||
{
|
{
|
||||||
|
@ -253,9 +253,9 @@ export class RagfairPriceService implements OnLoad
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param itemTemplateId
|
* @param itemTemplateId items tpl value
|
||||||
* @param desiredCurrency
|
* @param desiredCurrency Currency to return result in
|
||||||
* @param item
|
* @param item Item object (used for weapon presets)
|
||||||
* @param offerItems
|
* @param offerItems
|
||||||
* @param isPackOffer
|
* @param isPackOffer
|
||||||
* @returns
|
* @returns
|
||||||
|
@ -57,6 +57,7 @@ export class CompareUtil
|
|||||||
{
|
{
|
||||||
return v1 === v2;
|
return v1 === v2;
|
||||||
}
|
}
|
||||||
throw new Error(`could not detect type match for ${typeOfv1} and ${typeOfv2}`);
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -928,7 +928,7 @@ describe("InsuranceController", () =>
|
|||||||
|
|
||||||
describe("processAttachmentByParent", () =>
|
describe("processAttachmentByParent", () =>
|
||||||
{
|
{
|
||||||
it("should handle sorting, rolling, and deleting attachments by calling helper methods", () =>
|
it("should handle weighing and counting of attachments by calling helper methods", () =>
|
||||||
{
|
{
|
||||||
const insured = insuranceFixture[0];
|
const insured = insuranceFixture[0];
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
||||||
@ -941,49 +941,24 @@ describe("InsuranceController", () =>
|
|||||||
const toDelete = new Set<string>();
|
const toDelete = new Set<string>();
|
||||||
|
|
||||||
// Mock helper methods.
|
// Mock helper methods.
|
||||||
const mockSortAttachmentsByPrice = vi.spyOn(insuranceController, "sortAttachmentsByPrice");
|
const weightAttachmentsByPrice = vi.spyOn(insuranceController, "weightAttachmentsByPrice");
|
||||||
const mockCountSuccessfulRolls = vi.spyOn(insuranceController, "countSuccessfulRolls").mockReturnValue(4);
|
const getAttachmentCountToRemove = vi.spyOn(insuranceController, "getAttachmentCountToRemove")
|
||||||
const mockAttachmentDeletionByValue = vi.spyOn(insuranceController, "attachmentDeletionByValue");
|
.mockReturnValue(4);
|
||||||
|
const logAttachmentsBeingRemoved = vi.spyOn(insuranceController, "logAttachmentsBeingRemoved");
|
||||||
|
|
||||||
// Execute the method.
|
// Execute the method.
|
||||||
insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete);
|
insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete);
|
||||||
|
|
||||||
// Verify that helper methods are called.
|
// Verify that helper methods are called.
|
||||||
expect(mockSortAttachmentsByPrice).toHaveBeenCalledWith(attachments);
|
expect(weightAttachmentsByPrice).toHaveBeenCalledWith(attachments);
|
||||||
expect(mockCountSuccessfulRolls).toHaveBeenCalled();
|
expect(getAttachmentCountToRemove).toHaveBeenCalled();
|
||||||
expect(mockAttachmentDeletionByValue).toHaveBeenCalled();
|
expect(logAttachmentsBeingRemoved).toHaveBeenCalled();
|
||||||
});
|
|
||||||
|
|
||||||
it("should log attachment details and number of attachments to be deleted", () =>
|
|
||||||
{
|
|
||||||
const insured = insuranceFixture[0];
|
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
|
||||||
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
|
|
||||||
insuranceController.hashUtil.generate(),
|
|
||||||
insured,
|
|
||||||
itemsMap,
|
|
||||||
);
|
|
||||||
const attachments = parentAttachmentsMap.entries().next().value;
|
|
||||||
const toDelete = new Set<string>();
|
|
||||||
const successfulRolls = 4;
|
|
||||||
|
|
||||||
// Mock helper methods.
|
|
||||||
const mockLogAttachmentsDetails = vi.spyOn(insuranceController, "logAttachmentsDetails");
|
|
||||||
vi.spyOn(insuranceController, "countSuccessfulRolls").mockReturnValue(successfulRolls);
|
|
||||||
const mockLoggerDebug = vi.spyOn(insuranceController.logger, "debug").mockImplementation(vi.fn());
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete);
|
|
||||||
|
|
||||||
// Verify that the logs were called/written.
|
|
||||||
expect(mockLogAttachmentsDetails).toBeCalled();
|
|
||||||
expect(mockLoggerDebug).toHaveBeenCalledWith(`Number of attachments to be deleted: ${successfulRolls}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sortAttachmentsByPrice", () =>
|
describe("getAttachmentCountToRemove", () =>
|
||||||
{
|
{
|
||||||
it("should sort the attachments array by dynamicPrice in descending order", () =>
|
it("should handle returning a count of attachments that should be removed that is below the total attachment count", () =>
|
||||||
{
|
{
|
||||||
const insured = insuranceFixture[0];
|
const insured = insuranceFixture[0];
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
||||||
@ -995,20 +970,12 @@ describe("InsuranceController", () =>
|
|||||||
const attachments = parentAttachmentsMap.entries().next().value;
|
const attachments = parentAttachmentsMap.entries().next().value;
|
||||||
const attachmentCount = attachments.length;
|
const attachmentCount = attachments.length;
|
||||||
|
|
||||||
// Execute the method.
|
const result = insuranceController.getAttachmentCountToRemove(attachments, insured.traderId);
|
||||||
const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments);
|
|
||||||
|
|
||||||
// Verify the length of the sorted attachments array is unchanged
|
expect(result).lessThanOrEqual(attachmentCount);
|
||||||
expect(sortedAttachments.length).toBe(attachmentCount);
|
|
||||||
|
|
||||||
// Verify that the attachments are sorted by dynamicPrice in descending order
|
|
||||||
for (let i = 1; i < sortedAttachments.length; i++)
|
|
||||||
{
|
|
||||||
expect(sortedAttachments[i - 1].dynamicPrice).toBeGreaterThanOrEqual(sortedAttachments[i].dynamicPrice);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should place attachments with null dynamicPrice at the bottom of the sorted list", () =>
|
it("should handle returning 0 when chanceNoAttachmentsTakenPercent is 100%", () =>
|
||||||
{
|
{
|
||||||
const insured = insuranceFixture[0];
|
const insured = insuranceFixture[0];
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
||||||
@ -1018,133 +985,36 @@ describe("InsuranceController", () =>
|
|||||||
itemsMap,
|
itemsMap,
|
||||||
);
|
);
|
||||||
const attachments = parentAttachmentsMap.entries().next().value;
|
const attachments = parentAttachmentsMap.entries().next().value;
|
||||||
|
insuranceController.insuranceConfig.chanceNoAttachmentsTakenPercent = 100;
|
||||||
|
|
||||||
// Set the dynamicPrice of the first attachment to null.
|
const result = insuranceController.getAttachmentCountToRemove(attachments, insured.traderId);
|
||||||
vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(666)
|
|
||||||
.mockReturnValueOnce(null);
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments);
|
|
||||||
|
|
||||||
// Verify that the attachments with null dynamicPrice are at the bottom of the list
|
|
||||||
const nullPriceAttachments = sortedAttachments.slice(-1);
|
|
||||||
for (const attachment of nullPriceAttachments)
|
|
||||||
{
|
|
||||||
expect(attachment.dynamicPrice).toBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the rest of the attachments are sorted by dynamicPrice in descending order
|
|
||||||
for (let i = 1; i < sortedAttachments.length - 2; i++)
|
|
||||||
{
|
|
||||||
expect(sortedAttachments[i - 1].dynamicPrice).toBeGreaterThanOrEqual(sortedAttachments[i].dynamicPrice);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("logAttachmentsDetails", () =>
|
|
||||||
{
|
|
||||||
it("should log details for each attachment", () =>
|
|
||||||
{
|
|
||||||
const attachments = [{ _id: "item1", name: "Item 1", dynamicPrice: 100 }, {
|
|
||||||
_id: "item2",
|
|
||||||
name: "Item 2",
|
|
||||||
dynamicPrice: 200,
|
|
||||||
}];
|
|
||||||
|
|
||||||
// Mock the logger.debug function.
|
|
||||||
const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug");
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
insuranceController.logAttachmentsDetails(attachments);
|
|
||||||
|
|
||||||
// Verify that logger.debug was called correctly.
|
|
||||||
expect(loggerDebugSpy).toHaveBeenCalledTimes(2);
|
|
||||||
expect(loggerDebugSpy).toHaveBeenNthCalledWith(1, "Attachment 1: \"Item 1\" - Price: 100");
|
|
||||||
expect(loggerDebugSpy).toHaveBeenNthCalledWith(2, "Attachment 2: \"Item 2\" - Price: 200");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not log anything when there are no attachments", () =>
|
|
||||||
{
|
|
||||||
const attachments = [];
|
|
||||||
|
|
||||||
// Mock the logger.debug function.
|
|
||||||
const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug");
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
insuranceController.logAttachmentsDetails(attachments);
|
|
||||||
|
|
||||||
// Verify that logger.debug was called correctly.
|
|
||||||
expect(loggerDebugSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("countSuccessfulRolls", () =>
|
|
||||||
{
|
|
||||||
it("should count the number of successful rolls made in the rollForDelete method", () =>
|
|
||||||
{
|
|
||||||
const insured = insuranceFixture[0];
|
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
|
||||||
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
|
|
||||||
insuranceController.hashUtil.generate(),
|
|
||||||
insured,
|
|
||||||
itemsMap,
|
|
||||||
);
|
|
||||||
const attachments = parentAttachmentsMap.values().next().value;
|
|
||||||
|
|
||||||
// Mock rollForDelete to return true for the first two attachments.
|
|
||||||
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false)
|
|
||||||
.mockReturnValueOnce(true).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
const result = insuranceController.countSuccessfulRolls(attachments, insured.traderId);
|
|
||||||
|
|
||||||
// Verify that two successful rolls were counted.
|
|
||||||
expect(mockRollForDelete).toHaveBeenCalledTimes(attachments.length);
|
|
||||||
expect(result).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return zero if no successful rolls were made in the rollForDelete method", () =>
|
|
||||||
{
|
|
||||||
const insured = insuranceFixture[0];
|
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
|
||||||
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
|
|
||||||
insuranceController.hashUtil.generate(),
|
|
||||||
insured,
|
|
||||||
itemsMap,
|
|
||||||
);
|
|
||||||
const attachments = parentAttachmentsMap.values().next().value;
|
|
||||||
|
|
||||||
// Mock rollForDelete to return false.
|
|
||||||
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false);
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
const result = insuranceController.countSuccessfulRolls(attachments, insured.traderId);
|
|
||||||
|
|
||||||
// Verify that zero successful rolls were counted.
|
|
||||||
expect(mockRollForDelete).toHaveBeenCalledTimes(attachments.length);
|
|
||||||
expect(result).toBe(0);
|
expect(result).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return zero if there are no attachments", () =>
|
it("should handle returning 0 when all attachments are below configured threshold price", () =>
|
||||||
{
|
{
|
||||||
const insured = insuranceFixture[0];
|
const insured = insuranceFixture[0];
|
||||||
const attachments = [];
|
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
||||||
|
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
|
||||||
|
insuranceController.hashUtil.generate(),
|
||||||
|
insured,
|
||||||
|
itemsMap,
|
||||||
|
);
|
||||||
|
const attachments = parentAttachmentsMap.values().next().value;
|
||||||
|
insuranceController.insuranceConfig.minAttachmentRoublePriceToBeTaken = 2;
|
||||||
|
vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(1);
|
||||||
|
|
||||||
// Spy on rollForDelete to ensure it is not called.
|
const weightedAttachments = insuranceController.weightAttachmentsByPrice(attachments);
|
||||||
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete");
|
const result = insuranceController.getAttachmentCountToRemove(weightedAttachments, insured.traderId);
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
const result = insuranceController.countSuccessfulRolls(attachments, insured.traderId);
|
|
||||||
|
|
||||||
// Verify that zero successful rolls were returned.
|
|
||||||
expect(mockRollForDelete).not.toHaveBeenCalled();
|
|
||||||
expect(result).toBe(0);
|
expect(result).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("attachmentDeletionByValue", () =>
|
describe("weightAttachmentsByPrice", () =>
|
||||||
{
|
{
|
||||||
it("should add the correct number of attachments to the toDelete set", () =>
|
it("Should create a dictionary of 2 items with weights of 1 for each", () =>
|
||||||
{
|
{
|
||||||
const insured = insuranceFixture[0];
|
const insured = insuranceFixture[0];
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
||||||
@ -1155,57 +1025,11 @@ describe("InsuranceController", () =>
|
|||||||
);
|
);
|
||||||
const attachments = parentAttachmentsMap.values().next().value;
|
const attachments = parentAttachmentsMap.values().next().value;
|
||||||
|
|
||||||
const successfulRolls = 2;
|
vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(1);
|
||||||
const toDelete = new Set<string>();
|
|
||||||
|
|
||||||
// Execute the method.
|
const result = insuranceController.weightAttachmentsByPrice(attachments);
|
||||||
insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete);
|
expect(Object.keys(result).length).toBe(2);
|
||||||
|
expect(Object.values(result)).toStrictEqual([1, 1]);
|
||||||
// Should add the first two valuable attachments to the toDelete set.
|
|
||||||
expect(toDelete.size).toEqual(successfulRolls);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not add any attachments to toDelete if successfulRolls is zero", () =>
|
|
||||||
{
|
|
||||||
const insured = insuranceFixture[0];
|
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
|
||||||
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
|
|
||||||
insuranceController.hashUtil.generate(),
|
|
||||||
insured,
|
|
||||||
itemsMap,
|
|
||||||
);
|
|
||||||
const attachments = parentAttachmentsMap.values().next().value;
|
|
||||||
|
|
||||||
const successfulRolls = 0;
|
|
||||||
const toDelete = new Set<string>();
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete);
|
|
||||||
|
|
||||||
// Should be empty.
|
|
||||||
expect(toDelete.size).toEqual(successfulRolls);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add all attachments to toDelete if successfulRolls is greater than the number of attachments", () =>
|
|
||||||
{
|
|
||||||
const insured = insuranceFixture[0];
|
|
||||||
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
|
|
||||||
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
|
|
||||||
insuranceController.hashUtil.generate(),
|
|
||||||
insured,
|
|
||||||
itemsMap,
|
|
||||||
);
|
|
||||||
const attachments = parentAttachmentsMap.values().next().value;
|
|
||||||
|
|
||||||
const successfulRolls = 999;
|
|
||||||
const toDelete = new Set<string>();
|
|
||||||
|
|
||||||
// Execute the method.
|
|
||||||
insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete);
|
|
||||||
|
|
||||||
// Should be empty.
|
|
||||||
expect(toDelete.size).toBeLessThan(successfulRolls);
|
|
||||||
expect(toDelete.size).toEqual(attachments.length);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user