927273d71d
+ Fixed a typo in `createRandomloot()` function name + Added more generically named`getWeightedValue()` function as future replacement for `getWeightedInventoryItem()` + Changed `LootRequest` into interface Added: Weapon/armor Barter Medical/Food Mixed
227 lines
8.0 KiB
TypeScript
227 lines
8.0 KiB
TypeScript
import { inject, injectable } from "tsyringe";
|
|
|
|
import { ItemHelper } from "../helpers/ItemHelper";
|
|
import { Preset } from "../models/eft/common/IGlobals";
|
|
import { ITemplateItem } from "../models/eft/common/tables/ITemplateItem";
|
|
import { BaseClasses } from "../models/enums/BaseClasses";
|
|
import { LootItem } from "../models/spt/services/LootItem";
|
|
import { LootRequest } from "../models/spt/services/LootRequest";
|
|
import { ILogger } from "../models/spt/utils/ILogger";
|
|
import { DatabaseServer } from "../servers/DatabaseServer";
|
|
import { ItemFilterService } from "../services/ItemFilterService";
|
|
import { LocalisationService } from "../services/LocalisationService";
|
|
import { HashUtil } from "../utils/HashUtil";
|
|
import { RandomUtil } from "../utils/RandomUtil";
|
|
|
|
@injectable()
|
|
export class LootGenerator
|
|
{
|
|
constructor(
|
|
@inject("WinstonLogger") protected logger: ILogger,
|
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
|
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
|
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
@inject("ItemFilterService") protected itemFilterService: ItemFilterService
|
|
)
|
|
{}
|
|
|
|
/**
|
|
* Generate a list of items based on configuration options parameter
|
|
* @param options parameters to adjust how loot is generated
|
|
* @returns An array of loot items
|
|
*/
|
|
public createRandomLoot(options: LootRequest): LootItem[]
|
|
{
|
|
const result: LootItem[] = [];
|
|
|
|
const itemTypeCounts = this.initItemLimitCounter(options.itemLimits);
|
|
|
|
const tables = this.databaseServer.getTables();
|
|
const itemBlacklist = this.itemFilterService.getBlacklistedItems();
|
|
itemBlacklist.push(...options.itemBlacklist);
|
|
|
|
// Get items from items.json that have a type of item + not in global blacklist + basetype is in whitelist
|
|
const items = Object.entries(tables.templates.items).filter(x => !itemBlacklist.includes(x[1]._id)
|
|
&& x[1]._type.toLowerCase() === "item"
|
|
&& !x[1]._props.QuestItem
|
|
&& options.itemTypeWhitelist.includes(x[1]._parent));
|
|
|
|
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
|
|
for (let index = 0; index < randomisedItemCount; index++)
|
|
{
|
|
if (!this.findAndAddRandomItemToLoot(items, itemTypeCounts, options, result))
|
|
{
|
|
index--;
|
|
}
|
|
}
|
|
|
|
const globalDefaultPresets = Object.entries(tables.globals.ItemPresets).filter(x => x[1]._encyclopedia !== undefined);
|
|
const randomisedPresetCount = this.randomUtil.getInt(options.presetCount.min, options.presetCount.max);
|
|
for (let index = 0; index < randomisedPresetCount; index++)
|
|
{
|
|
if (!this.findAndAddRandomPresetToLoot(globalDefaultPresets, itemTypeCounts, itemBlacklist, result))
|
|
{
|
|
index--;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Construct item limit record to hold max and current item count
|
|
* @param limits limits as defined in config
|
|
* @returns record, key: item tplId, value: current/max item count allowed
|
|
*/
|
|
protected initItemLimitCounter(limits: Record<string, number>): Record<string, {current: number, max: number}>
|
|
{
|
|
const itemTypeCounts: Record<string, {current: number, max: number}> = {};
|
|
|
|
for (const x in limits)
|
|
{
|
|
itemTypeCounts[x] = {
|
|
current: 0,
|
|
max: limits[x]
|
|
};
|
|
}
|
|
|
|
return itemTypeCounts;
|
|
}
|
|
|
|
/**
|
|
* Find a random item in items.json and add to result array
|
|
* @param items items to choose from
|
|
* @param itemTypeCounts item limit counts
|
|
* @param options item filters
|
|
* @param result array to add found item to
|
|
* @returns true if item was valid and added to pool
|
|
*/
|
|
protected findAndAddRandomItemToLoot(
|
|
items: [string, ITemplateItem][],
|
|
itemTypeCounts: Record<string, { current: number; max: number; }>,
|
|
options: LootRequest,
|
|
result: LootItem[]): boolean
|
|
{
|
|
const randomItem = this.randomUtil.getArrayValue(items)[1];
|
|
|
|
const itemLimitCount = itemTypeCounts[randomItem._parent];
|
|
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const newLootItem: LootItem = {
|
|
id: this.hashUtil.generate(),
|
|
tpl: randomItem._id,
|
|
isPreset: false,
|
|
stackCount: 1
|
|
};
|
|
|
|
// Check if armor has level in allowed whitelist
|
|
if (randomItem._parent === BaseClasses.ARMOR
|
|
|| randomItem._parent === BaseClasses.VEST)
|
|
{
|
|
if (!options.armorLevelWhitelist.includes(Number(randomItem._props.armorClass)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Special case - handle items that need a stackcount > 1
|
|
if (randomItem._props.StackMaxSize > 1)
|
|
{
|
|
newLootItem.stackCount = this.getRandomisedStackCount(randomItem, options);
|
|
}
|
|
|
|
newLootItem.tpl = randomItem._id;
|
|
result.push(newLootItem);
|
|
|
|
if (itemLimitCount)
|
|
{
|
|
// Increment item count as it's in limit array
|
|
itemLimitCount.current++;
|
|
}
|
|
|
|
// Item added okay
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get a randomised stack count for an item between its StackMinRandom and StackMaxSize values
|
|
* @param item item to get stack count of
|
|
* @param options loot options
|
|
* @returns stack count
|
|
*/
|
|
protected getRandomisedStackCount(item: ITemplateItem, options: LootRequest): number
|
|
{
|
|
let min = item._props.StackMinRandom;
|
|
let max = item._props.StackMaxSize;
|
|
|
|
if (options.itemStackLimits[item._id])
|
|
{
|
|
min = options.itemStackLimits[item._id].min;
|
|
max = options.itemStackLimits[item._id].max;
|
|
}
|
|
|
|
return this.randomUtil.getInt(min, max);
|
|
}
|
|
|
|
/**
|
|
* Find a random item in items.json and add to result array
|
|
* @param globalDefaultPresets presets to choose from
|
|
* @param itemTypeCounts item limit counts
|
|
* @param itemBlacklist items to skip
|
|
* @param result array to add found preset to
|
|
* @returns true if preset was valid and added to pool
|
|
*/
|
|
protected findAndAddRandomPresetToLoot(
|
|
globalDefaultPresets: [string, Preset][],
|
|
itemTypeCounts: Record<string, { current: number; max: number; }>,
|
|
itemBlacklist: string[],
|
|
result: LootItem[]): boolean
|
|
{
|
|
// Choose random preset and get details from item.json using encyclopedia value (encyclopedia === tplId)
|
|
const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets)[1];
|
|
const itemDetails = this.databaseServer.getTables().templates.items[randomPreset._encyclopedia];
|
|
|
|
// Skip blacklisted items
|
|
if (itemBlacklist.includes(randomPreset._items[0]._tpl))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Some custom mod items are lacking a parent property
|
|
if (!itemDetails._parent)
|
|
{
|
|
this.logger.error(this.localisationService.getText("loot-item_missing_parentid", itemDetails._name));
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check picked preset hasn't exceeded spawn limit
|
|
const itemLimitCount = itemTypeCounts[itemDetails._parent];
|
|
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const newLootItem: LootItem = {
|
|
tpl: randomPreset._items[0]._tpl,
|
|
isPreset: true,
|
|
stackCount: 1
|
|
};
|
|
|
|
result.push(newLootItem);
|
|
|
|
if (itemLimitCount)
|
|
{
|
|
// increment item count as its in limit array
|
|
itemLimitCount.current++;
|
|
}
|
|
|
|
// item added okay
|
|
return true;
|
|
}
|
|
} |