Handle changes to airdrop endpoint

Reworked airdrop loot generation to incorporate 'radar' drops

Added `forced loot` system to LootGenerator service, can now explicitly define what items it contains

Fixed airdrop system sending incorrect icon to client
This commit is contained in:
Dev 2024-09-27 14:30:35 +01:00
parent 3bbe167450
commit 80762d6a00
8 changed files with 117 additions and 13 deletions

View File

@ -3,10 +3,12 @@
"mixed": 5,
"weaponArmor": 2,
"foodMedical": 1,
"barter": 1
"barter": 1,
"radar": 0
},
"loot": {
"mixed": {
"icon": "Common",
"weaponPresetCount": {
"min": 3,
"max": 5
@ -108,6 +110,7 @@
"allowBossItems": false
},
"weaponArmor": {
"icon": "Weapon",
"weaponPresetCount": {
"min": 6,
"max": 8
@ -180,6 +183,7 @@
"allowBossItems": false
},
"foodMedical": {
"icon": "Medical",
"weaponPresetCount": {
"min": 0,
"max": 0
@ -260,6 +264,7 @@
"allowBossItems": false
},
"barter": {
"icon": "Supply",
"weaponPresetCount": {
"min": 0,
"max": 0
@ -339,6 +344,39 @@
},
"armorLevelWhitelist": [0],
"allowBossItems": false
}
}
},
"radar": {
"icon": "Supply",
"weaponPresetCount": {
"min": 0,
"max": 0
},
"armorPresetCount": {
"min": 0,
"max": 0
},
"itemCount": {
"min": 0,
"max": 0
},
"weaponCrateCount": {
"min": 0,
"max": 0
},
"itemBlacklist": [],
"itemTypeWhitelist": [],
"itemLimits": {},
"itemStackLimits": {},
"armorLevelWhitelist": [],
"allowBossItems": false,
"useForcedLoot": true,
"forcedLoot": {
"66d9f7256916142b3b02276e": {"min": 2, "max": 4 }
}
}
},
"customAirdropMapping": {
"66da1b49099cf6adcc07a36b": "radar",
"66da1b546916142b3b022777": "radar"
}
}

View File

@ -2,6 +2,7 @@ import { LocationController } from "@spt/controllers/LocationController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { IGetAirdropLootRequest } from "@spt/models/eft/location/IGetAirdropLootRequest";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@ -25,9 +26,9 @@ export class LocationCallbacks {
/** Handle client/airdrop/loot */
public getAirdropLoot(
url: string,
info: IEmptyRequestData,
info: IGetAirdropLootRequest,
sessionID: string,
): IGetBodyResponseData<IGetAirdropLootResponse> {
return this.httpResponse.getBody(this.locationController.getAirdropLoot());
return this.httpResponse.getBody(this.locationController.getAirdropLoot(info));
}
}

View File

@ -1,4 +1,5 @@
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetAirdropLootRequest } from "@spt/models/eft/location/IGetAirdropLootRequest";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig";
@ -50,7 +51,11 @@ export class LocationController {
}
/** Handle client/airdrop/loot */
public getAirdropLoot(): IGetAirdropLootResponse {
public getAirdropLoot(request: IGetAirdropLootRequest): IGetAirdropLootResponse {
if (request.containerId) {
return this.airdropService.generateCustomAirdropLoot(request);
}
return this.airdropService.generateAirdropLoot();
}
}

View File

@ -153,6 +153,32 @@ export class LootGenerator {
return result;
}
public createForcedLoot(airdropConfig: LootRequest) {
const result: IItem[] = [];
const forcedItems = Object.keys(airdropConfig.forcedLoot);
for (const tpl of forcedItems) {
const details = airdropConfig.forcedLoot[tpl];
const randomisedItemCount = this.randomUtil.getInt(details.min, details.max);
// Add forced loot item to result
const newLootItem: IItem = {
_id: this.hashUtil.generate(),
_tpl: tpl,
upd: {
StackObjectsCount: randomisedItemCount,
SpawnedInSession: true,
},
};
const splitResults = this.itemHelper.splitStack(newLootItem);
result.push(...splitResults);
}
return result;
}
protected getItemRewardPool(
itemTplBlacklist: string[],
itemTypeWhitelist: string[],

View File

@ -3,4 +3,5 @@ export enum AirdropTypeEnum {
SUPPLY = "barter",
FOOD_MEDICAL = "foodMedical",
WEAPON_ARMOR = "weaponArmor",
RADAR = "radar",
}

View File

@ -7,6 +7,7 @@ export interface IAirdropConfig extends IBaseConfig {
airdropTypeWeightings: Record<AirdropTypeEnum, number>;
/** What rewards will the loot crate contain, keyed by drop type e.g. mixed/weaponArmor/foodMedical/barter */
loot: Record<string, IAirdropLoot>;
customAirdropMapping: Record<string, AirdropTypeEnum>;
}
/** Chance map will have an airdrop occur out of 100 - locations not included count as 0% */
@ -23,6 +24,7 @@ export interface IAirdropChancePercent {
/** Loot inside crate */
export interface IAirdropLoot {
icon: AirdropTypeEnum;
/** Min/max of weapons inside crate */
weaponPresetCount?: MinMax;
/** Min/max of armors (head/chest/rig) inside crate */
@ -43,4 +45,6 @@ export interface IAirdropLoot {
armorLevelWhitelist?: number[];
/** Should boss items be added to airdrop crate */
allowBossItems: boolean;
useForcedLoot?: boolean;
forcedLoot?: Record<string, MinMax>;
}

View File

@ -1,4 +1,5 @@
import { MinMax } from "@spt/models/common/MinMax";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
export interface LootRequest {
weaponPresetCount: MinMax;
@ -13,4 +14,10 @@ export interface LootRequest {
armorLevelWhitelist: number[];
allowBossItems: boolean;
useRewarditemBlacklist?: boolean;
useForcedLoot?: boolean;
forcedLoot?: Record<string, MinMax>;
}
export interface IAirdropLootRequest extends LootRequest {
icon?: AirdropTypeEnum;
}

View File

@ -2,12 +2,13 @@ import { LootGenerator } from "@spt/generators/LootGenerator";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper";
import { IItem } from "@spt/models/eft/common/tables/IItem";
import { IGetAirdropLootRequest } from "@spt/models/eft/location/IGetAirdropLootRequest";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ItemTpl } from "@spt/models/enums/ItemTpl";
import { IAirdropConfig, IAirdropLoot } from "@spt/models/spt/config/IAirdropConfig";
import { LootRequest } from "@spt/models/spt/services/LootRequest";
import { IAirdropLootRequest, LootRequest } from "@spt/models/spt/services/LootRequest";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseService } from "@spt/services/DatabaseService";
@ -36,21 +37,36 @@ export class AirdropService {
this.airdropConfig = this.configServer.getConfig(ConfigTypes.AIRDROP);
}
public generateCustomAirdropLoot(request: IGetAirdropLootRequest): IGetAirdropLootResponse {
const customAirdropInformation = this.airdropConfig.customAirdropMapping[request.containerId];
if (!customAirdropInformation) {
this.logger.warning(
`Unable to find data for custom airdrop ${request.containerId}, returning random airdrop instead`,
);
return this.generateAirdropLoot();
}
return this.generateAirdropLoot(customAirdropInformation);
}
/**
* Handle client/location/getAirdropLoot
* Get loot for an airdrop container
* Generates it randomly based on config/airdrop.json values
* @returns Array of LootItem objects
*/
public generateAirdropLoot(): IGetAirdropLootResponse {
const airdropType = this.chooseAirdropType();
public generateAirdropLoot(forcedAirdropType = null): IGetAirdropLootResponse {
const airdropType = forcedAirdropType ? forcedAirdropType : this.chooseAirdropType();
this.logger.debug(`Chose ${airdropType} for airdrop loot`);
// Common/weapon/etc
const airdropConfig = this.getAirdropLootConfigByType(airdropType);
// generate loot to put into airdrop crate
const crateLoot = this.lootGenerator.createRandomLoot(airdropConfig);
const crateLoot = airdropConfig.useForcedLoot
? this.lootGenerator.createForcedLoot(airdropConfig)
: this.lootGenerator.createRandomLoot(airdropConfig);
// Create airdrop crate and add to result in first spot
const airdropCrateItem = this.getAirdropCrateItem(airdropType);
@ -58,7 +74,7 @@ export class AirdropService {
// Add crate to front of array
crateLoot.unshift(airdropCrateItem);
// Reparent loot items to create we added above
// Reparent loot items to crate we added above
for (const item of crateLoot) {
if (item._id === airdropCrateItem._id) {
// Crate itself, don't alter
@ -72,7 +88,7 @@ export class AirdropService {
}
}
return { icon: airdropType, container: crateLoot };
return { icon: airdropConfig.icon, container: crateLoot };
}
/**
@ -103,6 +119,9 @@ export class AirdropService {
case AirdropTypeEnum.COMMON:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break;
case AirdropTypeEnum.RADAR:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_TECHNICAL_SUPPLY_CRATE_EVENT_1;
break;
default:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break;
@ -126,7 +145,7 @@ export class AirdropService {
* @param airdropType Type of airdrop to get settings for
* @returns LootRequest
*/
protected getAirdropLootConfigByType(airdropType: AirdropTypeEnum): LootRequest {
protected getAirdropLootConfigByType(airdropType: AirdropTypeEnum): IAirdropLootRequest {
let lootSettingsByType: IAirdropLoot = this.airdropConfig.loot[airdropType];
if (!lootSettingsByType) {
this.logger.error(
@ -136,6 +155,7 @@ export class AirdropService {
}
return {
icon: lootSettingsByType.icon,
weaponPresetCount: lootSettingsByType.weaponPresetCount,
armorPresetCount: lootSettingsByType.armorPresetCount,
itemCount: lootSettingsByType.itemCount,
@ -150,6 +170,8 @@ export class AirdropService {
itemStackLimits: lootSettingsByType.itemStackLimits,
armorLevelWhitelist: lootSettingsByType.armorLevelWhitelist,
allowBossItems: lootSettingsByType.allowBossItems,
useForcedLoot: lootSettingsByType.useForcedLoot,
forcedLoot: lootSettingsByType.forcedLoot,
};
}
}