Server/project/src/generators/PlayerScavGenerator.ts
Dev f3964639bd Rename mods to weaponMods and add new object equipmentMods
regenerate bot jsons to include this new data (includes correct inclusion of equipment slot "TacticalVest" which was previously missing)

Fix issue with PMM ammo causes generation issues
2024-01-09 15:31:56 +00:00

340 lines
13 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { BotGenerator } from "@spt-aki/generators/BotGenerator";
import { BotGeneratorHelper } from "@spt-aki/helpers/BotGeneratorHelper";
import { BotHelper } from "@spt-aki/helpers/BotHelper";
import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Settings, Skills, Stats } from "@spt-aki/models/eft/common/tables/IBotBase";
import { IBotType } from "@spt-aki/models/eft/common/tables/IBotType";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { AccountTypes } from "@spt-aki/models/enums/AccountTypes";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { ItemAddedResult } from "@spt-aki/models/enums/ItemAddedResult";
import { MemberCategory } from "@spt-aki/models/enums/MemberCategory";
import { Traders } from "@spt-aki/models/enums/Traders";
import { IPlayerScavConfig, KarmaLevel } from "@spt-aki/models/spt/config/IPlayerScavConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { SaveServer } from "@spt-aki/servers/SaveServer";
import { BotLootCacheService } from "@spt-aki/services/BotLootCacheService";
import { FenceService } from "@spt-aki/services/FenceService";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
@injectable()
export class PlayerScavGenerator
{
protected playerScavConfig: IPlayerScavConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
@inject("BotGeneratorHelper") protected botGeneratorHelper: BotGeneratorHelper,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("BotHelper") protected botHelper: BotHelper,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("FenceService") protected fenceService: FenceService,
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotGenerator") protected botGenerator: BotGenerator,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.playerScavConfig = this.configServer.getConfig(ConfigTypes.PLAYERSCAV);
}
/**
* Update a player profile to include a new player scav profile
* @param sessionID session id to specify what profile is updated
* @returns profile object
*/
public generate(sessionID: string): IPmcData
{
// get karma level from profile
const profile = this.saveServer.getProfile(sessionID);
const pmcData = this.jsonUtil.clone(profile.characters.pmc);
const existingScavData = this.jsonUtil.clone(profile.characters.scav);
// scav profile can be empty on first profile creation
const scavKarmaLevel = (Object.keys(existingScavData).length === 0) ? 0 : this.getScavKarmaLevel(pmcData);
// use karma level to get correct karmaSettings
const playerScavKarmaSettings = this.playerScavConfig.karmaLevel[scavKarmaLevel];
if (!playerScavKarmaSettings)
{
this.logger.error(this.localisationService.getText("scav-missing_karma_settings", scavKarmaLevel));
}
this.logger.debug(`generated player scav loadout with karma level ${scavKarmaLevel}`);
// Edit baseBotNode values
const baseBotNode: IBotType = this.constructBotBaseTemplate(playerScavKarmaSettings.botTypeForLoot);
this.adjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode);
let scavData = this.botGenerator.generatePlayerScav(
sessionID,
playerScavKarmaSettings.botTypeForLoot.toLowerCase(),
"easy",
baseBotNode,
);
// Remove cached bot data after scav was generated
this.botLootCacheService.clearCache();
// Add scav metadata
scavData.savage = null;
scavData.aid = pmcData.aid;
scavData.TradersInfo = pmcData.TradersInfo;
scavData.Info.Settings = {} as Settings;
scavData.Info.Bans = [];
scavData.Info.RegistrationDate = pmcData.Info.RegistrationDate;
scavData.Info.GameVersion = pmcData.Info.GameVersion;
scavData.Info.MemberCategory = MemberCategory.UNIQUE_ID;
scavData.Info.lockedMoveCommands = true;
scavData.RagfairInfo = pmcData.RagfairInfo;
scavData.UnlockedInfo = pmcData.UnlockedInfo;
// Persist previous scav data into new scav
scavData._id = existingScavData._id ?? pmcData.savage;
scavData.sessionId = existingScavData.sessionId ?? pmcData.sessionId;
scavData.Skills = this.getScavSkills(existingScavData);
scavData.Stats = this.getScavStats(existingScavData);
scavData.Info.Level = this.getScavLevel(existingScavData);
scavData.Info.Experience = this.getScavExperience(existingScavData);
scavData.Quests = existingScavData.Quests ?? [];
scavData.TaskConditionCounters = existingScavData.TaskConditionCounters ?? { };
scavData.Notes = existingScavData.Notes ?? { Notes: [] };
scavData.WishList = existingScavData.WishList ?? [];
// Add an extra labs card to pscav backpack based on config chance
if (this.randomUtil.getChance100(playerScavKarmaSettings.labsAccessCardChancePercent))
{
const labsCard = this.itemHelper.getItem("5c94bbff86f7747ee735c08f")[1];
const itemsToAdd: Item[] = [{
_id: this.hashUtil.generate(),
_tpl: labsCard._id,
...this.botGeneratorHelper.generateExtraPropertiesForItem(labsCard),
}];
const result = this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
["TacticalVest", "Pockets", "Backpack"],
itemsToAdd[0]._id,
labsCard._id,
itemsToAdd,
scavData.Inventory,
);
if (result !== ItemAddedResult.SUCCESS)
{
this.logger.debug(`Unable to add keycard to bot. Reason: ${ItemAddedResult[result]}`);
}
}
// Remove secure container
scavData = this.profileHelper.removeSecureContainer(scavData);
// Set cooldown timer
scavData = this.setScavCooldownTimer(scavData, pmcData);
// Add scav to the profile
this.saveServer.getProfile(sessionID).characters.scav = scavData;
return scavData;
}
/**
* Get the scav karama level for a profile
* Is also the fence trader rep level
* @param pmcData pmc profile
* @returns karma level
*/
protected getScavKarmaLevel(pmcData: IPmcData): number
{
const fenceInfo = pmcData.TradersInfo[Traders.FENCE];
// Can be empty during profile creation
if (!fenceInfo)
{
this.logger.warning(this.localisationService.getText("scav-missing_karma_level_getting_default"));
return 0;
}
if (fenceInfo.standing > 6)
{
return 6;
}
// e.g. 2.09 becomes 2
return Math.floor(fenceInfo.standing);
}
/**
* Get a baseBot template
* If the parameter doesnt match "assault", take parts from the loot type and apply to the return bot template
* @param botTypeForLoot bot type to use for inventory/chances
* @returns IBotType object
*/
protected constructBotBaseTemplate(botTypeForLoot: string): IBotType
{
const baseScavType = "assault";
const assaultBase = this.jsonUtil.clone(this.botHelper.getBotTemplate(baseScavType));
// Loot bot is same as base bot, return base with no modification
if (botTypeForLoot === baseScavType)
{
return assaultBase;
}
const lootBase = this.jsonUtil.clone(this.botHelper.getBotTemplate(botTypeForLoot));
assaultBase.inventory = lootBase.inventory;
assaultBase.chances = lootBase.chances;
assaultBase.generation = lootBase.generation;
return assaultBase;
}
/**
* Adjust equipment/mod/item generation values based on scav karma levels
* @param karmaSettings Values to modify the bot template with
* @param baseBotNode bot template to modify according to karama level settings
*/
protected adjustBotTemplateWithKarmaSpecificSettings(karmaSettings: KarmaLevel, baseBotNode: IBotType): void
{
// Adjust equipment chance values
for (const equipmentKey in karmaSettings.modifiers.equipment)
{
if (karmaSettings.modifiers.equipment[equipmentKey] === 0)
{
continue;
}
baseBotNode.chances.equipment[equipmentKey] += karmaSettings.modifiers.equipment[equipmentKey];
}
// Adjust mod chance values
for (const modKey in karmaSettings.modifiers.mod)
{
if (karmaSettings.modifiers.mod[modKey] === 0)
{
continue;
}
baseBotNode.chances.weaponMods[modKey] += karmaSettings.modifiers.mod[modKey];
}
// Adjust item spawn quantity values
for (const itemLimitkey in karmaSettings.itemLimits)
{
baseBotNode.generation.items[itemLimitkey] = karmaSettings.itemLimits[itemLimitkey];
}
// Blacklist equipment
for (const equipmentKey in karmaSettings.equipmentBlacklist)
{
const blacklistedItemTpls = karmaSettings.equipmentBlacklist[equipmentKey];
for (const itemToRemove of blacklistedItemTpls)
{
delete baseBotNode.inventory.equipment[equipmentKey][itemToRemove];
}
}
}
protected getScavSkills(scavProfile: IPmcData): Skills
{
if (scavProfile.Skills)
{
return scavProfile.Skills;
}
return this.getDefaultScavSkills();
}
protected getDefaultScavSkills(): Skills
{
return { Common: [], Mastering: [], Points: 0 };
}
protected getScavStats(scavProfile: IPmcData): Stats
{
if (scavProfile.Stats)
{
return scavProfile.Stats;
}
return this.profileHelper.getDefaultCounters();
}
protected getScavLevel(scavProfile: IPmcData): number
{
// Info can be null on initial account creation
if (!(scavProfile.Info?.Level))
{
return 1;
}
return scavProfile.Info.Level;
}
protected getScavExperience(scavProfile: IPmcData): number
{
// Info can be null on initial account creation
if (!(scavProfile.Info?.Experience))
{
return 0;
}
return scavProfile.Info.Experience;
}
/**
* Set cooldown till pscav is playable
* take into account scav cooldown bonus
* @param scavData scav profile
* @param pmcData pmc profile
* @returns
*/
protected setScavCooldownTimer(scavData: IPmcData, pmcData: IPmcData): IPmcData
{
// Set cooldown time.
// Make sure to apply ScavCooldownTimer bonus from Hideout if the player has it.
let scavLockDuration = this.databaseServer.getTables().globals.config.SavagePlayCooldown;
let modifier = 1;
for (const bonus of pmcData.Bonuses)
{
if (bonus.type === "ScavCooldownTimer")
{
// Value is negative, so add.
// Also note that for scav cooldown, multiple bonuses stack additively.
modifier += bonus.value / 100;
}
}
const fenceInfo = this.fenceService.getFenceInfo(pmcData);
modifier *= fenceInfo.SavageCooldownModifier;
scavLockDuration *= modifier;
const fullProfile = this.profileHelper.getFullProfile(pmcData?.sessionId);
if (fullProfile?.info?.edition?.toLowerCase?.().startsWith?.(AccountTypes.SPT_DEVELOPER))
{
// Set scav cooldown timer to 10 seconds for spt developer account
scavLockDuration = 10;
}
scavData.Info.SavageLockTime = (Date.now() / 1000) + scavLockDuration;
return scavData;
}
}