Feature: Add code to handle sealed weapon containers when opened in menu

Add handbook price for sealed containers (default of 100rub)
This commit is contained in:
Dev 2023-06-20 16:07:05 +01:00
parent 332dc140a6
commit 0c31719013
10 changed files with 433 additions and 26 deletions

View File

@ -19,5 +19,141 @@
}
}
},
"sealedAirdropContainer": {
"weaponRewardWeight": {
"5447a9cd4bdc2dbd208b4567": 1,
"5bb2475ed4351e00853264e3": 1,
"5bd70322209c4d00d7167b8f": 1,
"5ac66d2e5acfc43b321d4b53": 1,
"5ac66d725acfc43b321d4b60": 1,
"5ac66d9b5acfc4001633997a": 1,
"62e7c4fba689e8c9c50dfc38": 1,
"63171672192e68c5460cebc5": 1,
"5c488a752e221602b412af63": 1,
"5dcbd56fdbd3d91b3e5468d5": 1,
"623063e994fc3f7b302a9696": 1,
"5fbcc1d9016cce60e8341ab3": 1,
"606587252535c57a13424cfd": 1,
"5b0bbe4e5acfc40dc528a72d": 1,
"6184055050224f204c1da540": 1,
"6183afd850224f204c1da514": 1,
"5beed0f50db834001c062b12": 1,
"5cc82d76e24e8d00134b4b83": 1,
"5fc3e272f8b6a877a729eac5": 1,
"5fb6548dd1409e5ca04b54f9": 1,
"5aafa857e5b5b00018480968": 1,
"5bfea6e90db834001b7347f3": 1,
"5cadfbf7ae92152ac412eeef": 1,
"628a60ae6b1d481ff772e9c8": 1,
"628b5638ad252a16da6dd245": 1,
"5d43021ca4b9362eab4b5e25": 1,
"58948c8e86f77409493f7266": 1,
"62e14904c2699c0ec93adc47": 1,
"5c46fbd72e2216398b5a8c9c": 1,
"5df8ce05b11454561e39243b": 1,
"5df24cf80dee1b22f862e9bc": 1
},
"defaultPresetsOnly": true,
"weaponModRewardLimits": {
"5448bc234bdc2d3c308b4569": {
"type": "magazine",
"min": 2,
"max": 4
},
"55818b164bdc2ddc698b456c": {
"type": "laserLight",
"min": 1,
"max": 2
},
"55818ad54bdc2ddc698b4569": {
"type": "collimator",
"min": 0,
"max": 2
},
"55818acf4bdc2dde698b456b": {
"type": "compactCollimator",
"min": 0,
"max": 2
},
"55818b224bdc2dde698b456f": {
"type": "mount",
"min": 0,
"max": 2
},
"555ef6e44bdc2de9068b457e": {
"type": "barrel",
"min": 1,
"max": 1
},
"55818add4bdc2d5b648b456f": {
"type": "assaultScope",
"min": 0,
"max": 1
},
"55818ae44bdc2dde698b456c": {
"type": "opticScope",
"min": 0,
"max": 1
},
"55818af64bdc2d5b648b4570": {
"type": "foregrip",
"min": 0,
"max": 1
},
"550aa4cd4bdc2dd8348b456c": {
"type": "silencer",
"min": 0,
"max": 1
},
"55818b084bdc2d5b648b4571": {
"type": "flashlight",
"min": 0,
"max": 1
},
"55818a104bdc2db9688b4569": {
"type": "handguard",
"min": 0,
"max": 2
}
},
"rewardTypeLimits": {
"5448e8d04bdc2ddf718b4569": {
"type": "food",
"min": 2,
"max": 8
},
"5448f3a64bdc2d60728b456a": {
"type": "stim",
"min": 2,
"max": 5
},
"543be5cb4bdc2deb348b4568": {
"type": "ammobox",
"min": 2,
"max": 5
},
"5448f3ac4bdc2dce718b4569": {
"type": "medical",
"min": 2,
"max": 7
}
},
"ammoBoxWhitelist": [
"648983d6b5a2df1c815a04ec",
"6489848173c462723909a14b",
"648984b8d5b4df6140000a1a",
"648984e3f09d032aa9399d53",
"6489851fc827d4637f01791b",
"6489854673c462723909a14e",
"648985c074a806211e4fb682",
"6489875745f9ca4ba51c4808",
"648987d673c462723909a151",
"648986bbc827d4637f01791e",
"64898583d5b4df6140000a1d",
"64898602f09d032aa9399d56",
"6489870774a806211e4fb685",
"6489879db5a2df1c815a04ef"
]
}
"customMoneyTpls": []
}

View File

@ -1,10 +1,11 @@
import { inject, injectable } from "tsyringe";
import { LootGenerator } from "../generators/LootGenerator";
import { InventoryHelper } from "../helpers/InventoryHelper";
import { ItemHelper } from "../helpers/ItemHelper";
import { PaymentHelper } from "../helpers/PaymentHelper";
import { PresetHelper } from "../helpers/PresetHelper";
import { ProfileHelper } from "../helpers/ProfileHelper";
import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper";
import { IPmcData } from "../models/eft/common/IPmcData";
import { Item } from "../models/eft/common/tables/IItem";
import { IAddItemRequestData } from "../models/eft/inventory/IAddItemRequestData";
@ -38,7 +39,9 @@ import {
IOpenRandomLootContainerRequestData
} from "../models/eft/inventory/IOpenRandomLootContainerRequestData";
import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse";
import { BackendErrorCodes } from "../models/enums/BackendErrorCodes";
import { Traders } from "../models/enums/Traders";
import { RewardDetails } from "../models/spt/config/IInventoryConfig";
import { ILogger } from "../models/spt/utils/ILogger";
import { EventOutputHolder } from "../routers/EventOutputHolder";
import { DatabaseServer } from "../servers/DatabaseServer";
@ -57,6 +60,7 @@ export class InventoryController
@inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("FenceService") protected fenceService: FenceService,
@ -64,9 +68,9 @@ export class InventoryController
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
@inject("RagfairOfferService") protected ragfairOfferService: RagfairOfferService,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("LootGenerator") protected lootGenerator: LootGenerator,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("HttpResponseUtil") protected httpResponseUtil: HttpResponseUtil
)
@ -97,7 +101,7 @@ export class InventoryController
// Dont move items from trader to profile, this can happen when editing a traders preset weapons
if (moveRequest.fromOwner?.type === "Trader" && !items.isMail)
{
return this.httpResponseUtil.appendErrorToOutput(output, this.localisationService.getText("inventory-edit_trader_item"), 228);
return this.httpResponseUtil.appendErrorToOutput(output, this.localisationService.getText("inventory-edit_trader_item"), <BackendErrorCodes>228);
}
this.inventoryHelper.moveItemInternal(pmcData, items.from, moveRequest);
@ -760,29 +764,28 @@ export class InventoryController
public openRandomLootContainer(pmcData: IPmcData, body: IOpenRandomLootContainerRequestData, sessionID: string): IItemEventRouterResponse
{
const openedItem = pmcData.Inventory.items.find(x => x._id === body.item);
const rewardContainerDetails = this.inventoryHelper.getRandomLootContainerRewardDetails(openedItem._tpl);
const containerDetails = this.itemHelper.getItem(openedItem._tpl);
const isSealedWeaponBox = containerDetails[1]._name.includes("event_container_airdrop");
const newItemRequest: IAddItemRequestData = {
tid: "RandomLootContainer",
items: []
};
// Get random items and add to newItemRequest
for (let index = 0; index < rewardContainerDetails.rewardCount; index++)
let rewardContainerDetails: RewardDetails = {
rewardCount: 0,
foundInRaid: true
};
if (isSealedWeaponBox)
{
// Pick random reward from pool, add to request object
const chosenRewardItemTpl = this.weightedRandomHelper.getWeightedInventoryItem(rewardContainerDetails.rewardTplPool);
const existingItemInRequest = newItemRequest.items.find(x => x.item_id === chosenRewardItemTpl);
if (existingItemInRequest)
{
// Exists in request already, increment count
existingItemInRequest.count++;
newItemRequest.items.push(...this.lootGenerator.getSealedWeaponCaseLoot());
}
else
{
// eslint-disable-next-line @typescript-eslint/naming-convention
newItemRequest.items.push({item_id: chosenRewardItemTpl, count: 1});
}
// Get summary of loot from config
rewardContainerDetails = this.inventoryHelper.getRandomLootContainerRewardDetails(openedItem._tpl);
newItemRequest.items.push(...this.lootGenerator.getRandomLootContainerLoot(rewardContainerDetails));
}
const output = this.eventOutputHolder.getOutput(sessionID);

View File

@ -1,15 +1,21 @@
import { inject, injectable } from "tsyringe";
import { InventoryHelper } from "../helpers/InventoryHelper";
import { ItemHelper } from "../helpers/ItemHelper";
import { PresetHelper } from "../helpers/PresetHelper";
import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper";
import { Preset } from "../models/eft/common/IGlobals";
import { ITemplateItem } from "../models/eft/common/tables/ITemplateItem";
import { AddItem } from "../models/eft/inventory/IAddItemRequestData";
import { BaseClasses } from "../models/enums/BaseClasses";
import { ISealedAirdropContainerSettings, RewardDetails } from "../models/spt/config/IInventoryConfig";
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 { RagfairLinkedItemService } from "../services/RagfairLinkedItemService";
import { HashUtil } from "../utils/HashUtil";
import { RandomUtil } from "../utils/RandomUtil";
@ -27,7 +33,11 @@ export class LootGenerator
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("RagfairLinkedItemService") protected ragfairLinkedItemService: RagfairLinkedItemService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService
)
{}
@ -228,4 +238,204 @@ export class LootGenerator
// item added okay
return true;
}
/**
* Sealed weapon containers have a weapon + associated mods inside them + assortment of other things (food/meds)
* @returns Array of items to add to player inventory
*/
public getSealedWeaponCaseLoot(): AddItem[]
{
const itemsToReturn: AddItem[] = [];
const containerSettings = this.inventoryHelper.getInventoryConfig().sealedAirdropContainer;
// choose a weapon to give to the player (weighted)
const chosenWeaponTpl = this.weightedRandomHelper.getWeightedInventoryItem(containerSettings.weaponRewardWeight);
const weaponDetailsDb = this.itemHelper.getItem(chosenWeaponTpl);
if (!weaponDetailsDb[0])
{
this.logger.warning(`Non-item was picked as reward ${chosenWeaponTpl}, unable to continue`);
return itemsToReturn;
}
// Get weapon preset - default or choose a random one from all possible
const chosenWeaponPreset = containerSettings.defaultPresetsOnly
? this.presetHelper.getDefaultPreset(chosenWeaponTpl)
: this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl));
// Add preset to return object
itemsToReturn.push({
count: 1,
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: chosenWeaponPreset._id,
isPreset: true
});
// Get items related to chosen weapon
const linkedItemsToWeapon = this.ragfairLinkedItemService.getLinkedDbItems(chosenWeaponTpl);
itemsToReturn.push(...this.getSealedContainerWeaponModRewards(containerSettings, linkedItemsToWeapon, chosenWeaponPreset));
// Handle non-weapon mod reward types
itemsToReturn.push(...this.getSealedContainerNonWeaponModRewards(containerSettings, weaponDetailsDb[1]));
return itemsToReturn;
}
/**
* Get non-weapon mod rewards for a sealed container
* @param containerSettings Sealed weapon container settings
* @param weaponDetailsDb Details for the weapon to reward player
* @returns AddItem array
*/
protected getSealedContainerNonWeaponModRewards(containerSettings: ISealedAirdropContainerSettings, weaponDetailsDb: ITemplateItem): AddItem[]
{
const rewards: AddItem[] = [];
for (const rewardTypeId in containerSettings.rewardTypeLimits)
{
const settings = containerSettings.rewardTypeLimits[rewardTypeId];
const rewardCount = this.randomUtil.getInt(settings.min, settings.max);
if (rewardCount === 0)
{
continue;
}
// Edge case - ammo boxes
if (rewardTypeId === BaseClasses.AMMO_BOX)
{
// Get ammoboxes from db
const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map(x =>
{
const itemDetails = this.itemHelper.getItem(x);
return itemDetails[1];
});
// Need to find boxes that matches weapons caliber
const weaponCaliber = weaponDetailsDb._props.ammoCaliber;
const ammoBoxesMatchingCaliber = ammoBoxesDetails.filter(x => x._props.ammoCaliber === weaponCaliber);
if (ammoBoxesMatchingCaliber.length === 0)
{
this.logger.debug(`No ammo box with caliber ${weaponCaliber} found, skipping`);
continue;
}
// No need to add ammo to box, inventoryHelper.addItem() will handle it
const chosenAmmoBox = this.randomUtil.getArrayValue(ammoBoxesMatchingCaliber);
rewards.push({
count: rewardCount,
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: chosenAmmoBox._id,
isPreset: false
});
continue;
}
// Get all items of the desired type + not quest items + not globally blacklisted
const possibleRewardItems = Object.values(this.databaseServer.getTables().templates.items)
.filter(x => x._parent === rewardTypeId
&& x._type.toLowerCase() === "item"
&& !this.itemFilterService.isItemBlacklisted(x._id)
&& !x._props.QuestItem);
if (possibleRewardItems.length === 0)
{
this.logger.debug(`No items with base type of ${rewardTypeId} found, skipping`);
continue;
}
for (let index = 0; index < rewardCount; index++)
{
// choose a random item from pool
const chosenRewardItem = this.randomUtil.getArrayValue(possibleRewardItems);
this.addOrIncrementItemToArray(chosenRewardItem._id, rewards);
}
}
return rewards;
}
/**
* Iterate over the container weaponModRewardLimits settings and create an array of weapon mods to reward player
* @param containerSettings Sealed weapon container settings
* @param linkedItemsToWeapon All items that can be attached/inserted into weapon
* @param chosenWeaponPreset The weapon preset given to player as reward
* @returns AddItem array
*/
protected getSealedContainerWeaponModRewards(containerSettings: ISealedAirdropContainerSettings, linkedItemsToWeapon: ITemplateItem[], chosenWeaponPreset: Preset): AddItem[]
{
const modRewards: AddItem[] = [];
for (const rewardTypeId in containerSettings.weaponModRewardLimits)
{
const settings = containerSettings.weaponModRewardLimits[rewardTypeId];
const rewardCount = this.randomUtil.getInt(settings.min, settings.max);
// Nothing to add, skip reward type
if (rewardCount === 0)
{
continue;
}
// Get items that fulfil reward type criteral from items that fit on gun
const relatedItems = linkedItemsToWeapon.filter(x => x._parent === rewardTypeId);
if (!relatedItems || relatedItems.length === 0)
{
this.logger.debug(`no items found to fulfil reward type ${rewardTypeId} for weapon: ${chosenWeaponPreset._name}, skipping`);
continue;
}
// Find a random item of the desired type and add as reward
for (let index = 0; index < rewardCount; index++)
{
const chosenItem = this.randomUtil.drawRandomFromList(relatedItems);
this.addOrIncrementItemToArray(chosenItem[0]._id, modRewards);
}
}
return modRewards;
}
/**
* Handle event-related loot containers - currently just the halloween jack-o-lanterns that give food rewards
* @param rewardContainerDetails
* @returns AddItem array
*/
public getRandomLootContainerLoot(rewardContainerDetails: RewardDetails): AddItem[]
{
const itemsToReturn: AddItem[] = [];
// Get random items and add to newItemRequest
for (let index = 0; index < rewardContainerDetails.rewardCount; index++)
{
// Pick random reward from pool, add to request object
const chosenRewardItemTpl = this.weightedRandomHelper.getWeightedInventoryItem(rewardContainerDetails.rewardTplPool);
this.addOrIncrementItemToArray(chosenRewardItemTpl, itemsToReturn);
}
return itemsToReturn;
}
/**
* A bug in inventoryHelper.addItem() means you cannot add the same item to the array twice with a count of 1, it causes duplication
* Default adds 1, or increments count
* @param itemTplToAdd items tpl we want to add to array
* @param resultsArray Array to add item tpl to
*/
protected addOrIncrementItemToArray(itemTplToAdd: string, resultsArray: AddItem[]): void
{
const existingItemIndex = resultsArray.findIndex(x => x.item_id === itemTplToAdd);
if (existingItemIndex > -1)
{
// Exists in array already, increment count
resultsArray[existingItemIndex].count++;
}
else
{
// eslint-disable-next-line @typescript-eslint/naming-convention
resultsArray.push({item_id: itemTplToAdd, count: 1, isPreset: false});
}
}
}

View File

@ -467,7 +467,7 @@ export class RagfairOfferGenerator
if ("Repairable" in item.upd)
{
// Randomise non-0 class armor
if (itemDetails._props.armorClass && itemDetails._props.armorClass >= 1)
if (itemDetails._props.armorClass && <number>itemDetails._props.armorClass >= 1)
{
this.randomiseDurabilityValues(item, multiplier);
}

View File

@ -333,6 +333,7 @@ export class InventoryHelper
...itemLocation,
upd: upd
});
this.logger.debug(`Added ${itemLib[tmpKey]._tpl} with id: ${idForItemToAdd} to inventory`);
}
toDo.push([itemLib[tmpKey]._id, idForItemToAdd]);
@ -958,6 +959,11 @@ export class InventoryHelper
{
return this.inventoryConfig.randomLootContainers[itemTpl];
}
public getInventoryConfig(): IInventoryConfig
{
return this.inventoryConfig;
}
}
namespace InventoryHelper

View File

@ -76,7 +76,7 @@ export class RagfairHelper
const data = this.ragfairLinkedItemService.getLinkedItems(info.linkedSearchId);
result = !data
? []
: Array.from(data);
: [...data];
}
// Case: category

View File

@ -1,3 +1,4 @@
import { MinMax } from "../../../models/common/MinMax";
import { IBaseConfig } from "./IBaseConfig";
export interface IInventoryConfig extends IBaseConfig
@ -5,6 +6,7 @@ export interface IInventoryConfig extends IBaseConfig
kind: "aki-inventory"
newItemsMarkedFound: boolean
randomLootContainers: Record<string, RewardDetails>
sealedAirdropContainer: ISealedAirdropContainerSettings
/** Contains item tpls that the server should consider money and treat the same as roubles/euros/dollars */
customMoneyTpls: string[]
}
@ -13,5 +15,15 @@ export interface RewardDetails
{
rewardCount: number
foundInRaid: boolean
rewardTplPool: Record<string, number>
rewardTplPool?: Record<string, number>
rewardTypePool?: Record<string, number>
}
export interface ISealedAirdropContainerSettings
{
weaponRewardWeight: Record<string, number>
defaultPresetsOnly: boolean
weaponModRewardLimits: Record<string, MinMax>
rewardTypeLimits: Record<string, MinMax>
ammoBoxWhitelist: string[]
}

View File

@ -8,7 +8,7 @@ import { DatabaseServer } from "../servers/DatabaseServer";
@injectable()
export class RagfairLinkedItemService
{
protected linkedItemsCache: Record<string, Iterable<string>> = {};
protected linkedItemsCache: Record<string, Set<string>> = {};
constructor(
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@ -16,7 +16,7 @@ export class RagfairLinkedItemService
)
{ }
public getLinkedItems(linkedSearchId: string): Iterable<string>
public getLinkedItems(linkedSearchId: string): Set<string>
{
if (Object.keys(this.linkedItemsCache).length === 0)
{
@ -26,6 +26,21 @@ export class RagfairLinkedItemService
return this.linkedItemsCache[linkedSearchId];
}
/**
* Use ragfair linked item service to get an array of items that can fit on or in designated itemtpl
* @param itemTpl Item to get sub-items for
* @returns ITemplateItem array
*/
public getLinkedDbItems(itemTpl: string): ITemplateItem[]
{
const linkedItemsToWeaponTpls = this.getLinkedItems(itemTpl);
return [...linkedItemsToWeaponTpls].map(x =>
{
const itemDetails = this.itemHelper.getItem(x);
return itemDetails[1];
});
}
/**
* Create Dictionary of every item and the items associated with it
*/
@ -91,7 +106,12 @@ export class RagfairLinkedItemService
}
}
/* Scans a given slot type for filters and returns them as a Set */
/**
* Scans a given slot type for filters and returns them as a Set
* @param item
* @param slot
* @returns array of ids
*/
protected getFilters(item: ITemplateItem, slot: string): string[]
{
if (!(slot in item._props && item._props[slot].length))

View File

@ -51,6 +51,8 @@ export class RagfairPriceService implements OnLoad
*/
public async onLoad(): Promise<void>
{
this.addMissingHandbookPrices();
if (!this.generatedStaticPrices)
{
this.generateStaticPrices();
@ -64,6 +66,24 @@ export class RagfairPriceService implements OnLoad
}
}
/**
* Add placeholder values for the new sealed weapon containers
*/
protected addMissingHandbookPrices(): void
{
const db = this.databaseServer.getTables();
const sealedWeaponContainers = Object.values(db.templates.items).filter(x => x._name.includes("event_container_airdrop"));
for (const container of sealedWeaponContainers)
{
// doesnt have a handbook value
if (db.templates.handbook.Items.findIndex(x => x.Id === container._id) === -1)
{
db.templates.handbook.Items.push({Id: container._id, ParentId: container._parent, Price: 100});
}
}
}
public getRoute(): string
{
return "RagfairPriceService";

View File

@ -300,7 +300,7 @@ export class RandomUtil
* Drawing can be with or without replacement
* @param {array} list The array we want to draw randomly from
* @param {integer} count The number of times we want to draw
* @param {boolean} replacement Draw with or without replacement from the input array
* @param {boolean} replacement Draw with or without replacement from the input array(defult true)
* @return {array} Array consisting of N random elements
*/
public drawRandomFromList<T>(list: Array<T>, count = 1, replacement = true): Array<T>