Server/project/src/generators/PlayerScavGenerator.ts
2023-10-21 20:13:29 +01:00

329 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 { 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.ConditionCounters = existingScavData.ConditionCounters ?? {Counters: []};
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)
}];
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(["TacticalVest", "Pockets", "Backpack"], itemsToAdd[0]._id, labsCard._id, itemsToAdd, scavData.Inventory);
}
// 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.mods[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;
}
}