2023-03-03 16:23:46 +01:00
|
|
|
import { inject, injectable } from "tsyringe";
|
|
|
|
|
2023-10-19 19:21:17 +02:00
|
|
|
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";
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
@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);
|
2023-10-14 14:18:07 +02:00
|
|
|
const pmcData = this.jsonUtil.clone(profile.characters.pmc);
|
|
|
|
const existingScavData = this.jsonUtil.clone(profile.characters.scav);
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// 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}`);
|
|
|
|
|
2023-10-21 19:23:58 +02:00
|
|
|
// Edit baseBotNode values
|
2023-03-03 16:23:46 +01:00
|
|
|
const baseBotNode: IBotType = this.constructBotBaseTemplate(playerScavKarmaSettings.botTypeForLoot);
|
|
|
|
this.adjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode);
|
|
|
|
|
|
|
|
let scavData = this.botGenerator.generatePlayerScav(sessionID, playerScavKarmaSettings.botTypeForLoot.toLowerCase(), "easy", baseBotNode);
|
2023-10-21 19:23:58 +02:00
|
|
|
|
|
|
|
// Remove cached bot data after scav was generated
|
2023-03-03 16:23:46 +01:00
|
|
|
this.botLootCacheService.clearCache();
|
|
|
|
|
2023-10-21 19:23:58 +02:00
|
|
|
// Add scav metadata
|
2023-10-10 13:03:20 +02:00
|
|
|
scavData.savage = null;
|
|
|
|
scavData.aid = pmcData.aid;
|
2023-10-14 14:18:07 +02:00
|
|
|
scavData.TradersInfo = pmcData.TradersInfo;
|
|
|
|
scavData.Info.Settings = {} as Settings;
|
|
|
|
scavData.Info.Bans = [];
|
|
|
|
scavData.Info.RegistrationDate = pmcData.Info.RegistrationDate;
|
|
|
|
scavData.Info.GameVersion = pmcData.Info.GameVersion;
|
2023-10-14 14:29:27 +02:00
|
|
|
scavData.Info.MemberCategory = MemberCategory.UNIQUE_ID;
|
2023-10-21 19:23:58 +02:00
|
|
|
scavData.Info.lockedMoveCommands = true;
|
|
|
|
scavData.RagfairInfo = pmcData.RagfairInfo;
|
|
|
|
scavData.UnlockedInfo = pmcData.UnlockedInfo;
|
|
|
|
|
|
|
|
// Persist previous scav data into new scav
|
2023-10-21 21:13:29 +02:00
|
|
|
scavData._id = existingScavData._id ?? pmcData.savage;
|
|
|
|
scavData.sessionId = existingScavData.sessionId ?? pmcData.sessionId;
|
2023-10-21 19:23:58 +02:00
|
|
|
scavData.Skills = this.getScavSkills(existingScavData);
|
|
|
|
scavData.Stats = this.getScavStats(existingScavData);
|
2023-03-03 16:23:46 +01:00
|
|
|
scavData.Info.Level = this.getScavLevel(existingScavData);
|
|
|
|
scavData.Info.Experience = this.getScavExperience(existingScavData);
|
2023-10-21 21:13:29 +02:00
|
|
|
scavData.Quests = existingScavData.Quests ?? [];
|
|
|
|
scavData.ConditionCounters = existingScavData.ConditionCounters ?? {Counters: []};
|
|
|
|
scavData.Notes = existingScavData.Notes ?? {Notes: []};
|
|
|
|
scavData.WishList = existingScavData.WishList ?? [];
|
2023-10-21 19:23:58 +02:00
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2023-10-21 19:23:58 +02:00
|
|
|
// Remove secure container
|
2023-03-03 16:23:46 +01:00
|
|
|
scavData = this.profileHelper.removeSecureContainer(scavData);
|
|
|
|
|
2023-10-21 19:23:58 +02:00
|
|
|
// Set cooldown timer
|
2023-03-03 16:23:46 +01:00
|
|
|
scavData = this.setScavCooldownTimer(scavData, pmcData);
|
|
|
|
|
2023-10-21 19:23:58 +02:00
|
|
|
// Add scav to the profile
|
2023-03-03 16:23:46 +01:00
|
|
|
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)
|
|
|
|
{
|
2023-10-10 13:03:20 +02:00
|
|
|
baseBotNode.generation.items[itemLimitkey] = karmaSettings.itemLimits[itemLimitkey];
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2023-10-10 13:03:20 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2023-03-03 16:23:46 +01:00
|
|
|
|
2023-10-10 13:03:20 +02:00
|
|
|
scavData.Info.SavageLockTime = (Date.now() / 1000) + scavLockDuration;
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
return scavData;
|
|
|
|
}
|
|
|
|
}
|