Seperates ragfair pricing into seperate method
This changes the `RagfairPriceService.getDynamicOfferPriceForOffer()` method to handle the logic surrounding collecting prices for items in offers, while offloading the individual price generation to a new method. The new method, `RagfairPriceService.getDynamicItemPrice()`, is responsible for generating a price for either an item template, or optionally an offerItems collection. This change also allows `getDynamicItemPrice()` to be used elsewhere in the code-base to gather more "realistic" pricing for specific item templates. Fixes an edge-case where unreasonable prices would only be adjusted on the first item within an offer. Includes some tests. Related to #618. This will allow the insurance system to use this method to get better pricing for items.
This commit is contained in:
parent
834a2e3ef5
commit
3d77ed8595
@ -85,6 +85,17 @@ export class PresetHelper
|
|||||||
return id in this.databaseServer.getTables().globals.ItemPresets;
|
return id in this.databaseServer.getTables().globals.ItemPresets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the preset is of the given base class.
|
||||||
|
* @param id The id of the preset
|
||||||
|
* @param baseClass The BaseClasses enum to check against
|
||||||
|
* @returns True if the preset is of the given base class, false otherwise
|
||||||
|
*/
|
||||||
|
public isPresetBaseClass(id: string, baseClass: BaseClasses): boolean
|
||||||
|
{
|
||||||
|
return this.isPreset(id) && this.itemHelper.isOfBaseclass(this.getPreset(id)._encyclopedia, baseClass);
|
||||||
|
}
|
||||||
|
|
||||||
public hasPreset(templateId: string): boolean
|
public hasPreset(templateId: string): boolean
|
||||||
{
|
{
|
||||||
return templateId in this.lookup;
|
return templateId in this.lookup;
|
||||||
|
@ -223,109 +223,129 @@ export class RagfairPriceService implements OnLoad
|
|||||||
*/
|
*/
|
||||||
public getDynamicOfferPriceForOffer(offerItems: Item[], desiredCurrency: string, isPackOffer: boolean): number
|
public getDynamicOfferPriceForOffer(offerItems: Item[], desiredCurrency: string, isPackOffer: boolean): number
|
||||||
{
|
{
|
||||||
const rootItem = offerItems[0];
|
// Price to return.
|
||||||
|
|
||||||
// Price to return
|
|
||||||
let price = 0;
|
let price = 0;
|
||||||
|
|
||||||
let endLoop = false;
|
// Iterate over each item in the offer.
|
||||||
let isPreset = false;
|
|
||||||
let manuallyAdjusted = false;
|
|
||||||
for (const item of offerItems)
|
for (const item of offerItems)
|
||||||
{
|
{
|
||||||
// Armor insert, skip - we dont factor these into an items price
|
// Skip over armour inserts as those are not factored into item prices.
|
||||||
if (this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.BUILT_IN_INSERTS))
|
if (this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.BUILT_IN_INSERTS))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get dynamic price, fallback to handbook price if value of 1 found
|
price += this.getDynamicItemPrice(item._tpl, desiredCurrency, item, offerItems, isPackOffer);
|
||||||
let itemPrice = this.getFleaPriceForItem(item._tpl);
|
|
||||||
|
|
||||||
if (this.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice)
|
// Check if the item is a weapon preset.
|
||||||
{
|
if (item?.upd?.sptPresetId && this.presetHelper.isPresetBaseClass(item.upd.sptPresetId, BaseClasses.WEAPON))
|
||||||
itemPrice = this.adjustPriceIfBelowHandbook(itemPrice, item._tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher)
|
|
||||||
{
|
|
||||||
// Get highest trader price for item, if greater than value found so far, use it
|
|
||||||
const traderPrice = this.traderHelper.getHighestSellToTraderPrice(item._tpl);
|
|
||||||
if (traderPrice > itemPrice)
|
|
||||||
{
|
|
||||||
itemPrice = traderPrice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if item type is weapon preset, handle differently
|
|
||||||
const itemDetails = this.itemHelper.getItem(item._tpl);
|
|
||||||
if (this.presetHelper.isPreset(item.upd?.sptPresetId) && itemDetails[1]._props.weapFireType)
|
|
||||||
{
|
|
||||||
itemPrice = this.getWeaponPresetPrice(item, offerItems, itemPrice);
|
|
||||||
endLoop = true;
|
|
||||||
isPreset = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existance of manual price adjustment multiplier
|
|
||||||
const manualPriceMultipler = this.ragfairConfig.dynamic.itemPriceMultiplier[item._tpl];
|
|
||||||
if (manualPriceMultipler)
|
|
||||||
{
|
|
||||||
manuallyAdjusted = true;
|
|
||||||
itemPrice *= manualPriceMultipler;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiply dynamic price by quality modifier
|
|
||||||
const itemQualityModifier = this.itemHelper.getItemQualityModifier(item);
|
|
||||||
price += itemPrice * itemQualityModifier;
|
|
||||||
|
|
||||||
// Stop loop if weapon preset price function has been run
|
|
||||||
if (endLoop)
|
|
||||||
{
|
{
|
||||||
|
// This is a weapon preset, which has it's own price calculation that takes into account the mods in the
|
||||||
|
// preset. Since we've already calculated the price for the preset entire preset in
|
||||||
|
// `getDynamicItemPrice`, we can skip the rest of the items in the offer.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for unreasonable price on singular items
|
return Math.round(price);
|
||||||
if (offerItems.length === 1 && !manuallyAdjusted)
|
}
|
||||||
{
|
|
||||||
const rootItemDb = this.itemHelper.getItem(rootItem._tpl)[1];
|
|
||||||
let unreasonableItemPriceChange: IUnreasonableModPrices;
|
|
||||||
for (const key of Object.keys(this.ragfairConfig.dynamic.unreasonableModPrices))
|
|
||||||
{
|
|
||||||
if (this.itemHelper.isOfBaseclass(rootItemDb._id, key))
|
|
||||||
{
|
|
||||||
unreasonableItemPriceChange = this.ragfairConfig.dynamic.unreasonableModPrices[key];
|
|
||||||
|
|
||||||
break;
|
/**
|
||||||
}
|
* @param itemTemplateId
|
||||||
}
|
* @param desiredCurrency
|
||||||
if (unreasonableItemPriceChange?.enabled)
|
* @param item
|
||||||
|
* @param offerItems
|
||||||
|
* @param isPackOffer
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public getDynamicItemPrice(
|
||||||
|
itemTemplateId: string,
|
||||||
|
desiredCurrency: string,
|
||||||
|
item?: Item,
|
||||||
|
offerItems?: Item[],
|
||||||
|
isPackOffer?: boolean,
|
||||||
|
): number
|
||||||
|
{
|
||||||
|
let isPreset = false;
|
||||||
|
let price = this.getFleaPriceForItem(itemTemplateId);
|
||||||
|
|
||||||
|
// Adjust price if below handbook price, based on config.
|
||||||
|
if (this.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice)
|
||||||
|
{
|
||||||
|
price = this.adjustPriceIfBelowHandbook(price, itemTemplateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use trader price if higher, based on config.
|
||||||
|
if (this.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher)
|
||||||
|
{
|
||||||
|
const traderPrice = this.traderHelper.getHighestSellToTraderPrice(itemTemplateId);
|
||||||
|
if (traderPrice > price)
|
||||||
{
|
{
|
||||||
price = this.adjustUnreasonablePrice(
|
price = traderPrice;
|
||||||
this.databaseServer.getTables().templates.handbook.Items,
|
|
||||||
unreasonableItemPriceChange,
|
|
||||||
rootItem._tpl,
|
|
||||||
price,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get price multiplier min/max to vary price
|
// Prices for weapon presets are handled differently.
|
||||||
const rangeValues = this.getOfferTypeRangeValues(isPreset, isPackOffer);
|
if (
|
||||||
price = this.randomiseOfferPrice(price, rangeValues);
|
item?.upd?.sptPresetId
|
||||||
|
&& offerItems
|
||||||
|
&& this.presetHelper.isPresetBaseClass(item.upd.sptPresetId, BaseClasses.WEAPON)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
price = this.getWeaponPresetPrice(item, offerItems, price);
|
||||||
|
isPreset = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to different currency if desiredCurrency param is not roubles
|
// Check for existence of manual price adjustment multiplier
|
||||||
if (desiredCurrency !== Money.ROUBLES)
|
const multiplier = this.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId];
|
||||||
|
if (multiplier)
|
||||||
|
{
|
||||||
|
price *= multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The quality of the item affects the price.
|
||||||
|
if (item)
|
||||||
|
{
|
||||||
|
const qualityModifier = this.itemHelper.getItemQualityModifier(item);
|
||||||
|
price *= qualityModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make adjustments for unreasonably priced items.
|
||||||
|
for (const baseClassTemplateId of Object.keys(this.ragfairConfig.dynamic.unreasonableModPrices))
|
||||||
|
{
|
||||||
|
if (this.itemHelper.isOfBaseclass(itemTemplateId, baseClassTemplateId))
|
||||||
|
{
|
||||||
|
// Found an unreasonable price type.
|
||||||
|
const unreasonableModifier: IUnreasonableModPrices =
|
||||||
|
this.ragfairConfig.dynamic.unreasonableModPrices[baseClassTemplateId];
|
||||||
|
|
||||||
|
if (unreasonableModifier.enabled)
|
||||||
|
{
|
||||||
|
price = this.adjustUnreasonablePrice(
|
||||||
|
this.databaseServer.getTables().templates.handbook.Items,
|
||||||
|
unreasonableModifier,
|
||||||
|
itemTemplateId,
|
||||||
|
price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vary the price based on the type of offer.
|
||||||
|
const range = this.getOfferTypeRangeValues(isPreset, isPackOffer);
|
||||||
|
price = this.randomiseOfferPrice(price, range);
|
||||||
|
|
||||||
|
// Convert to different currency if required.
|
||||||
|
const roublesId = Money.ROUBLES;
|
||||||
|
if (desiredCurrency !== roublesId)
|
||||||
{
|
{
|
||||||
price = this.handbookHelper.fromRUB(price, desiredCurrency);
|
price = this.handbookHelper.fromRUB(price, desiredCurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard against weird prices
|
|
||||||
if (price < 1)
|
if (price < 1)
|
||||||
{
|
{
|
||||||
price = 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return price;
|
return price;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +420,7 @@ export class RagfairPriceService implements OnLoad
|
|||||||
const itemHandbookPrice = this.getStaticPriceForItem(itemTpl);
|
const itemHandbookPrice = this.getStaticPriceForItem(itemTpl);
|
||||||
const priceDifferencePercent = this.getPriceDifference(itemHandbookPrice, itemPrice);
|
const priceDifferencePercent = this.getPriceDifference(itemHandbookPrice, itemPrice);
|
||||||
|
|
||||||
// Only adjust price if difference is > a percent AND item price passes threshhold set in config
|
// Only adjust price if difference is > a percent AND item price passes threshold set in config
|
||||||
if (
|
if (
|
||||||
priceDifferencePercent > this.ragfairConfig.dynamic.offerAdjustment.maxPriceDifferenceBelowHandbookPercent
|
priceDifferencePercent > this.ragfairConfig.dynamic.offerAdjustment.maxPriceDifferenceBelowHandbookPercent
|
||||||
&& itemPrice >= this.ragfairConfig.dynamic.offerAdjustment.priceThreshholdRub
|
&& itemPrice >= this.ragfairConfig.dynamic.offerAdjustment.priceThreshholdRub
|
||||||
|
501
project/tests/services/RagfairPriceService.test.ts
Normal file
501
project/tests/services/RagfairPriceService.test.ts
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { container } from "tsyringe";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
|
||||||
|
|
||||||
|
import { MinMax } from "@spt-aki/models/common/MinMax";
|
||||||
|
import { Money } from "@spt-aki/models/enums/Money";
|
||||||
|
|
||||||
|
describe("RagfairPriceService", () =>
|
||||||
|
{
|
||||||
|
let ragfairPriceService: any; // Using "any" to access private/protected methods without type errors.
|
||||||
|
|
||||||
|
beforeEach(() =>
|
||||||
|
{
|
||||||
|
ragfairPriceService = container.resolve<RagfairPriceService>("RagfairPriceService");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() =>
|
||||||
|
{
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDynamicOfferPriceForOffer", () =>
|
||||||
|
{
|
||||||
|
it("should return zero when empty offerItems array is passed", () =>
|
||||||
|
{
|
||||||
|
const offerItems = [];
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const isPackOffer = false;
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
|
||||||
|
|
||||||
|
expect(price).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return non-zero number when valid item is passed", () =>
|
||||||
|
{
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "d445ea263cdfc5f278334264",
|
||||||
|
_tpl: "57e3dba62459770f0c32322b",
|
||||||
|
parentId: "631abbff398cc0170cbd3089",
|
||||||
|
slotId: "mod_pistol_grip",
|
||||||
|
}];
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const isPackOffer = false;
|
||||||
|
const expectedPrice = 42069;
|
||||||
|
|
||||||
|
// Mock the getDynamicItemPrice method to return a static price.
|
||||||
|
vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(expectedPrice);
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
|
||||||
|
|
||||||
|
expect(price).toBe(expectedPrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should always return a whole number", () =>
|
||||||
|
{
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "d445ea263cdfc5f278334264",
|
||||||
|
_tpl: "57e3dba62459770f0c32322b",
|
||||||
|
parentId: "631abbff398cc0170cbd3089",
|
||||||
|
slotId: "mod_pistol_grip",
|
||||||
|
}];
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const isPackOffer = false;
|
||||||
|
const originalPrice = 42069.999999999;
|
||||||
|
|
||||||
|
// Mock the getDynamicItemPrice method to return a static price.
|
||||||
|
vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(originalPrice);
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
|
||||||
|
|
||||||
|
expect(price).toBeGreaterThan(originalPrice);
|
||||||
|
expect(price).toBe(Math.round(originalPrice));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip prices for soft armour inserts", () =>
|
||||||
|
{
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "d445ea263cdfc5f278334264",
|
||||||
|
_tpl: "657080a212755ae0d907ad04",
|
||||||
|
parentId: "631abbff398cc0170cbd3089",
|
||||||
|
slotId: "Soft_armor_front",
|
||||||
|
}];
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const isPackOffer = false;
|
||||||
|
|
||||||
|
// Mock the getDynamicItemPrice method.
|
||||||
|
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice");
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
|
||||||
|
|
||||||
|
expect(price).toBe(0);
|
||||||
|
expect(getDynamicItemPriceSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add value of mods to weapon preset", () =>
|
||||||
|
{
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "344d02bbf2102ce4e145bf35",
|
||||||
|
_tpl: "579204f224597773d619e051",
|
||||||
|
upd: {
|
||||||
|
StackObjectsCount: 1,
|
||||||
|
UnlimitedCount: true,
|
||||||
|
sptPresetId: "5841499024597759f825ff3e",
|
||||||
|
Repairable: { Durability: 90, MaxDurability: 90 },
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
_id: "59c6897a59ed48f1ca02f659",
|
||||||
|
_tpl: "5448c12b4bdc2d02308b456f",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_magazine",
|
||||||
|
}, {
|
||||||
|
_id: "7e8062d4bc57b56927c2d117",
|
||||||
|
_tpl: "6374a822e629013b9c0645c8",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_reciever",
|
||||||
|
}, {
|
||||||
|
_id: "3b09149e8b7833dc5fdd32a4",
|
||||||
|
_tpl: "63c6adcfb4ba094317063742",
|
||||||
|
parentId: "7e8062d4bc57b56927c2d117",
|
||||||
|
slotId: "mod_sight_rear",
|
||||||
|
}, {
|
||||||
|
_id: "e833a5c26af29870df9cdd2e",
|
||||||
|
_tpl: "6374a7e7417239a7bf00f042",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_pistolgrip",
|
||||||
|
}];
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const isPackOffer = false;
|
||||||
|
const expectedPrice = 10000;
|
||||||
|
|
||||||
|
// Mock the getDynamicItemPrice method to return a static price.
|
||||||
|
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(
|
||||||
|
expectedPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
|
||||||
|
|
||||||
|
expect(price).toBe(expectedPrice);
|
||||||
|
expect(getDynamicItemPriceSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sum value of all offer items", () =>
|
||||||
|
{
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "59c6897a59ed48f1ca02f659",
|
||||||
|
_tpl: "5448c12b4bdc2d02308b456f",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_magazine",
|
||||||
|
}, {
|
||||||
|
_id: "7e8062d4bc57b56927c2d117",
|
||||||
|
_tpl: "6374a822e629013b9c0645c8",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_reciever",
|
||||||
|
}, {
|
||||||
|
_id: "3b09149e8b7833dc5fdd32a4",
|
||||||
|
_tpl: "63c6adcfb4ba094317063742",
|
||||||
|
parentId: "7e8062d4bc57b56927c2d117",
|
||||||
|
slotId: "mod_sight_rear",
|
||||||
|
}, {
|
||||||
|
_id: "e833a5c26af29870df9cdd2e",
|
||||||
|
_tpl: "6374a7e7417239a7bf00f042",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_pistolgrip",
|
||||||
|
}];
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const isPackOffer = false;
|
||||||
|
const expectedPrice = 10000;
|
||||||
|
|
||||||
|
// Mock the getDynamicItemPrice method to return a static price.
|
||||||
|
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(
|
||||||
|
expectedPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
|
||||||
|
|
||||||
|
expect(price).toBe(expectedPrice * offerItems.length);
|
||||||
|
expect(getDynamicItemPriceSpy).toHaveBeenCalledTimes(offerItems.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDynamicItemPrice", () =>
|
||||||
|
{
|
||||||
|
it("should not return zero for a valid template ID", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5e54f6af86f7742199090bf3";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
expect(price).not.toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use trader price if it is higher than flea price and configuration allows it", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5e54f6af86f7742199090bf3";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockTraderPrice = 20000;
|
||||||
|
const mockFleaPrice = 15000;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
|
||||||
|
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = true;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock the getFleaPriceForItem method to return a static price.
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
|
||||||
|
|
||||||
|
// Mock the getHighestSellToTraderPrice method to return a higher static price.
|
||||||
|
vi.spyOn((ragfairPriceService as any).traderHelper, "getHighestSellToTraderPrice").mockReturnValue(
|
||||||
|
mockTraderPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax.
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
expect(price).toBe(mockTraderPrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should adjust flea price when below handbook price and configuration allows it", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5e54f6af86f7742199090bf3";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockFleaPrice = 1;
|
||||||
|
const handbookPrice = 10000;
|
||||||
|
const adjustedPrice = 9000;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
|
||||||
|
// Enable adjustment for prices below handbook price. Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = true;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock the getFleaPriceForItem method to return a static price below the handbook price.
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
|
||||||
|
|
||||||
|
// Mock the adjustPriceIfBelowHandbook method to simulate price adjustment.
|
||||||
|
vi.spyOn(ragfairPriceService, "adjustPriceIfBelowHandbook").mockImplementation(
|
||||||
|
(price: number, templateId) =>
|
||||||
|
{
|
||||||
|
return price < handbookPrice ? adjustedPrice : price;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax.
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
// Verify the price is adjusted correctly according to the mocked handbook price adjustment logic.
|
||||||
|
expect(price).toBe(adjustedPrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle weapon preset prices correctly", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "579204f224597773d619e051";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockPresetPrice = 25000;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "344d02bbf2102ce4e145bf35",
|
||||||
|
_tpl: "579204f224597773d619e051",
|
||||||
|
upd: {
|
||||||
|
StackObjectsCount: 1,
|
||||||
|
UnlimitedCount: true,
|
||||||
|
sptPresetId: "5841499024597759f825ff3e",
|
||||||
|
Repairable: { Durability: 90, MaxDurability: 90 },
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
_id: "7e8062d4bc57b56927c2d117",
|
||||||
|
_tpl: "6374a822e629013b9c0645c8",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_reciever",
|
||||||
|
}];
|
||||||
|
const item = offerItems[0];
|
||||||
|
|
||||||
|
// Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock getFleaPriceForItem to bypass initial flea price fetch
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(0);
|
||||||
|
|
||||||
|
// Mock the isPresetBaseClass method to return true for the item
|
||||||
|
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(true);
|
||||||
|
|
||||||
|
// Mock the getWeaponPresetPrice method to return a specific preset price
|
||||||
|
const getWeaponPresetPriceSpy = vi.spyOn(ragfairPriceService, "getWeaponPresetPrice").mockReturnValue(
|
||||||
|
mockPresetPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax.
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Mock the getItemQualityModifier method to return 1 (no change)
|
||||||
|
vi.spyOn((ragfairPriceService as any).itemHelper, "getItemQualityModifier").mockReturnValue(1);
|
||||||
|
|
||||||
|
// Call the method with the mock item and offer items
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency, item, offerItems);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
expect(price).toBe(mockPresetPrice);
|
||||||
|
|
||||||
|
// Additionally, you can verify that getWeaponPresetPrice was called with the correct parameters
|
||||||
|
expect(getWeaponPresetPriceSpy).toHaveBeenCalledWith(item, offerItems, expect.any(Number));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update price based on the ragfair config item price multiplier values", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5e54f6af86f7742199090bf3";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockFleaPrice = 20000;
|
||||||
|
const itemPriceMultiplier = 2;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
|
||||||
|
// Mock the ragfair config to have a price multiplier of 2. Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = itemPriceMultiplier;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
|
||||||
|
// Mock the getFleaPriceForItem method to return a static price.
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax.
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
expect(price).toBe(mockFleaPrice * itemPriceMultiplier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should adjust price when durability is not perfect", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "579204f224597773d619e051";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockPrice = 25000;
|
||||||
|
const mockDurabilityMulti = 0.5;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
const offerItems = [{
|
||||||
|
_id: "344d02bbf2102ce4e145bf35",
|
||||||
|
_tpl: "579204f224597773d619e051",
|
||||||
|
upd: {
|
||||||
|
StackObjectsCount: 1,
|
||||||
|
UnlimitedCount: true,
|
||||||
|
sptPresetId: "5841499024597759f825ff3e",
|
||||||
|
Repairable: { Durability: 40, MaxDurability: 90 },
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
_id: "7e8062d4bc57b56927c2d117",
|
||||||
|
_tpl: "6374a822e629013b9c0645c8",
|
||||||
|
parentId: "344d02bbf2102ce4e145bf35",
|
||||||
|
slotId: "mod_reciever",
|
||||||
|
}];
|
||||||
|
const item = offerItems[0];
|
||||||
|
|
||||||
|
// Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock getFleaPriceForItem to bypass initial flea price fetch
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(0);
|
||||||
|
|
||||||
|
// Mock the isPresetBaseClass method to return true for the item
|
||||||
|
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(true);
|
||||||
|
|
||||||
|
// Mock the getWeaponPresetPrice method to return a specific preset price
|
||||||
|
vi.spyOn(ragfairPriceService, "getWeaponPresetPrice").mockReturnValue(mockPrice);
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax.
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Mock the getItemQualityModifier method to return 1 (no change)
|
||||||
|
const getItemQualityModifierSpy = vi.spyOn(
|
||||||
|
(ragfairPriceService as any).itemHelper,
|
||||||
|
"getItemQualityModifier",
|
||||||
|
).mockReturnValue(mockDurabilityMulti);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency, item, offerItems);
|
||||||
|
|
||||||
|
expect(getItemQualityModifierSpy).toHaveBeenCalled();
|
||||||
|
expect(price).toBe(mockPrice * mockDurabilityMulti);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should adjust unreasonable prices based on ragfair config unreasonable price values", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5c052f6886f7746b1e3db148";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockFleaPrice = 9999999;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
const mockBaseClassTemplateId = "57864a66245977548f04a81f";
|
||||||
|
const mockUnreasonableModPrices = {
|
||||||
|
itemType: "Electronics",
|
||||||
|
enabled: true,
|
||||||
|
handbookPriceOverMultiplier: 11,
|
||||||
|
newPriceHandbookMultiplier: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the Disable unreasonableModPrices config. Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.unreasonableModPrices[mockBaseClassTemplateId] =
|
||||||
|
mockUnreasonableModPrices;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock getFleaPriceForItem to bypass initial flea price fetch
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
|
||||||
|
|
||||||
|
// Mock isOfBaseclass to ensure that the item is always of the base class
|
||||||
|
const isOfBaseclassSpy = vi.spyOn((ragfairPriceService as any).itemHelper, "isOfBaseclass").mockReturnValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the adjustUnreasonablePrice method to ensure it was called
|
||||||
|
const adjustUnreasonablePriceSpy = vi.spyOn(ragfairPriceService, "adjustUnreasonablePrice");
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
expect(isOfBaseclassSpy).toHaveBeenCalled();
|
||||||
|
expect(adjustUnreasonablePriceSpy).toHaveBeenCalled();
|
||||||
|
expect(price).toBeLessThan(mockFleaPrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should vary the price within a random range", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5e54f6af86f7742199090bf3";
|
||||||
|
const desiredCurrency = Money.ROUBLES;
|
||||||
|
const mockFleaPrice = 10000;
|
||||||
|
const mockRandomiseOfferPrice = 9500;
|
||||||
|
|
||||||
|
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock the getFleaPriceForItem method to return a static price
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
|
||||||
|
|
||||||
|
// Mock the isPresetBaseClass method to return false
|
||||||
|
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(false);
|
||||||
|
|
||||||
|
// Mock the randomiseOfferPrice method to have a simplified implementation
|
||||||
|
const randomiseOfferPriceSpy = vi.spyOn(ragfairPriceService, "randomiseOfferPrice").mockReturnValue(
|
||||||
|
mockRandomiseOfferPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
expect(randomiseOfferPriceSpy).toHaveBeenCalled();
|
||||||
|
expect(price).toBe(mockRandomiseOfferPrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert currency", () =>
|
||||||
|
{
|
||||||
|
const itemTemplateId = "5e54f6af86f7742199090bf3";
|
||||||
|
const desiredCurrency = Money.DOLLARS;
|
||||||
|
const mockRoublePrice = 10000;
|
||||||
|
const mockDollarPrice = 500;
|
||||||
|
const getOfferTypeRangeValues = { max: 1, min: 1 };
|
||||||
|
|
||||||
|
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
|
||||||
|
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
|
||||||
|
|
||||||
|
// Mock the getFleaPriceForItem method to return a static price.
|
||||||
|
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockRoublePrice);
|
||||||
|
|
||||||
|
// Mock the getOfferTypeRangeValues method to return a static minMax
|
||||||
|
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
|
||||||
|
|
||||||
|
// Mock the fromRUB method to convert the price to a different currency
|
||||||
|
const fromRUBSpy = vi.spyOn((ragfairPriceService as any).handbookHelper, "fromRUB").mockReturnValue(
|
||||||
|
mockDollarPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the method.
|
||||||
|
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
|
||||||
|
|
||||||
|
expect(fromRUBSpy).toHaveBeenCalledWith(mockRoublePrice, desiredCurrency);
|
||||||
|
expect(price).not.toBe(mockRoublePrice);
|
||||||
|
expect(price).toBe(mockDollarPrice);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user