mirror of
https://github.com/sp-tarkov/server.git
synced 2025-02-13 09:50:43 -05:00

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
573 lines
21 KiB
TypeScript
573 lines
21 KiB
TypeScript
import { inject, injectable } from "tsyringe";
|
|
|
|
import { BotInventoryGenerator } from "@spt-aki/generators/BotInventoryGenerator";
|
|
import { BotLevelGenerator } from "@spt-aki/generators/BotLevelGenerator";
|
|
import { BotDifficultyHelper } from "@spt-aki/helpers/BotDifficultyHelper";
|
|
import { BotHelper } from "@spt-aki/helpers/BotHelper";
|
|
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
|
|
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
|
|
import {
|
|
Common,
|
|
Health as PmcHealth,
|
|
IBaseJsonSkills,
|
|
IBaseSkill,
|
|
IBotBase,
|
|
Info,
|
|
Skills as botSkills,
|
|
} from "@spt-aki/models/eft/common/tables/IBotBase";
|
|
import { Appearance, Health, IBotType } from "@spt-aki/models/eft/common/tables/IBotType";
|
|
import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem";
|
|
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
|
|
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
|
|
import { MemberCategory } from "@spt-aki/models/enums/MemberCategory";
|
|
import { BotGenerationDetails } from "@spt-aki/models/spt/bots/BotGenerationDetails";
|
|
import { IBotConfig } from "@spt-aki/models/spt/config/IBotConfig";
|
|
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
|
|
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
|
|
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
|
|
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
|
|
import { BotEquipmentFilterService } from "@spt-aki/services/BotEquipmentFilterService";
|
|
import { LocalisationService } from "@spt-aki/services/LocalisationService";
|
|
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
|
|
import { HashUtil } from "@spt-aki/utils/HashUtil";
|
|
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
|
|
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
|
|
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
|
|
|
|
@injectable()
|
|
export class BotGenerator
|
|
{
|
|
protected botConfig: IBotConfig;
|
|
protected pmcConfig: IPmcConfig;
|
|
|
|
constructor(
|
|
@inject("WinstonLogger") protected logger: ILogger,
|
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
|
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
|
@inject("JsonUtil") protected jsonUtil: JsonUtil,
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
|
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
|
@inject("BotInventoryGenerator") protected botInventoryGenerator: BotInventoryGenerator,
|
|
@inject("BotLevelGenerator") protected botLevelGenerator: BotLevelGenerator,
|
|
@inject("BotEquipmentFilterService") protected botEquipmentFilterService: BotEquipmentFilterService,
|
|
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
|
|
@inject("BotHelper") protected botHelper: BotHelper,
|
|
@inject("BotDifficultyHelper") protected botDifficultyHelper: BotDifficultyHelper,
|
|
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
|
)
|
|
{
|
|
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
|
|
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
|
|
}
|
|
|
|
/**
|
|
* Generate a player scav bot object
|
|
* @param role e.g. assault / pmcbot
|
|
* @param difficulty easy/normal/hard/impossible
|
|
* @param botTemplate base bot template to use (e.g. assault/pmcbot)
|
|
* @returns
|
|
*/
|
|
public generatePlayerScav(sessionId: string, role: string, difficulty: string, botTemplate: IBotType): IBotBase
|
|
{
|
|
let bot = this.getCloneOfBotBase();
|
|
bot.Info.Settings.BotDifficulty = difficulty;
|
|
bot.Info.Settings.Role = role;
|
|
bot.Info.Side = "Savage";
|
|
|
|
const botGenDetails: BotGenerationDetails = {
|
|
isPmc: false,
|
|
side: "Savage",
|
|
role: role,
|
|
playerLevel: 0,
|
|
botRelativeLevelDeltaMax: 0,
|
|
botRelativeLevelDeltaMin: 0,
|
|
botCountToGenerate: 1,
|
|
botDifficulty: difficulty,
|
|
isPlayerScav: true,
|
|
};
|
|
|
|
bot = this.generateBot(sessionId, bot, botTemplate, botGenDetails);
|
|
|
|
return bot;
|
|
}
|
|
|
|
/**
|
|
* Create x number of bots of the type/side/difficulty defined in botGenerationDetails
|
|
* @param sessionId Session id
|
|
* @param botGenerationDetails details on how to generate bots
|
|
* @returns array of bots
|
|
*/
|
|
public prepareAndGenerateBots(sessionId: string, botGenerationDetails: BotGenerationDetails): IBotBase[]
|
|
{
|
|
const output: IBotBase[] = [];
|
|
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
|
|
{
|
|
let bot = this.getCloneOfBotBase();
|
|
|
|
bot.Info.Settings.Role = botGenerationDetails.role;
|
|
bot.Info.Side = botGenerationDetails.side;
|
|
bot.Info.Settings.BotDifficulty = botGenerationDetails.botDifficulty;
|
|
|
|
// Get raw json data for bot (Cloned)
|
|
const botJsonTemplate = this.jsonUtil.clone(
|
|
this.botHelper.getBotTemplate((botGenerationDetails.isPmc)
|
|
? bot.Info.Side
|
|
: botGenerationDetails.role),
|
|
);
|
|
|
|
bot = this.generateBot(sessionId, bot, botJsonTemplate, botGenerationDetails);
|
|
|
|
output.push(bot);
|
|
}
|
|
|
|
this.logger.debug(
|
|
`Generated ${botGenerationDetails.botCountToGenerate} ${output[0].Info.Settings.Role} (${
|
|
botGenerationDetails.eventRole ?? ""
|
|
}) bots`,
|
|
);
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Get a clone of the database\bots\base.json file
|
|
* @returns IBotBase object
|
|
*/
|
|
protected getCloneOfBotBase(): IBotBase
|
|
{
|
|
return this.jsonUtil.clone(this.databaseServer.getTables().bots.base);
|
|
}
|
|
|
|
/**
|
|
* Create a IBotBase object with equipment/loot/exp etc
|
|
* @param sessionId Session id
|
|
* @param bot bots base file
|
|
* @param botJsonTemplate Bot template from db/bots/x.json
|
|
* @param botGenerationDetails details on how to generate the bot
|
|
* @returns IBotBase object
|
|
*/
|
|
protected generateBot(
|
|
sessionId: string,
|
|
bot: IBotBase,
|
|
botJsonTemplate: IBotType,
|
|
botGenerationDetails: BotGenerationDetails,
|
|
): IBotBase
|
|
{
|
|
const botRole = botGenerationDetails.role.toLowerCase();
|
|
const botLevel = this.botLevelGenerator.generateBotLevel(
|
|
botJsonTemplate.experience.level,
|
|
botGenerationDetails,
|
|
bot,
|
|
);
|
|
|
|
if (!botGenerationDetails.isPlayerScav)
|
|
{
|
|
this.botEquipmentFilterService.filterBotEquipment(
|
|
sessionId,
|
|
botJsonTemplate,
|
|
botLevel.level,
|
|
botGenerationDetails,
|
|
);
|
|
}
|
|
|
|
bot.Info.Nickname = this.generateBotNickname(
|
|
botJsonTemplate,
|
|
botGenerationDetails.isPlayerScav,
|
|
botRole,
|
|
sessionId,
|
|
);
|
|
|
|
if (!this.seasonalEventService.christmasEventEnabled())
|
|
{
|
|
this.seasonalEventService.removeChristmasItemsFromBotInventory(
|
|
botJsonTemplate.inventory,
|
|
botGenerationDetails.role,
|
|
);
|
|
}
|
|
|
|
// Remove hideout data if bot is not a PMC or pscav
|
|
if (!(botGenerationDetails.isPmc || botGenerationDetails.isPlayerScav))
|
|
{
|
|
bot.Hideout = null;
|
|
}
|
|
|
|
bot.Info.Experience = botLevel.exp;
|
|
bot.Info.Level = botLevel.level;
|
|
bot.Info.Settings.Experience = this.randomUtil.getInt(
|
|
botJsonTemplate.experience.reward.min,
|
|
botJsonTemplate.experience.reward.max,
|
|
);
|
|
bot.Info.Settings.StandingForKill = botJsonTemplate.experience.standingForKill;
|
|
bot.Info.Voice = this.randomUtil.getArrayValue(botJsonTemplate.appearance.voice);
|
|
bot.Health = this.generateHealth(botJsonTemplate.health, bot.Info.Side === "Savage");
|
|
bot.Skills = this.generateSkills(<any>botJsonTemplate.skills); // TODO: fix bad type, bot jsons store skills in dict, output needs to be array
|
|
|
|
this.setBotAppearance(bot, botJsonTemplate.appearance, botGenerationDetails);
|
|
|
|
bot.Inventory = this.botInventoryGenerator.generateInventory(
|
|
sessionId,
|
|
botJsonTemplate,
|
|
botRole,
|
|
botGenerationDetails.isPmc,
|
|
botLevel.level,
|
|
);
|
|
|
|
if (this.botHelper.isBotPmc(botRole))
|
|
{
|
|
this.getRandomisedGameVersionAndCategory(bot.Info);
|
|
bot = this.generateDogtag(bot);
|
|
bot.Info.IsStreamerModeAvailable = true; // Set to true so client patches can pick it up later - client sometimes alters botrole to assaultGroup
|
|
}
|
|
|
|
// generate new bot ID
|
|
bot = this.generateId(bot);
|
|
|
|
// generate new inventory ID
|
|
bot = this.generateInventoryID(bot);
|
|
|
|
// Set role back to originally requested now its been generated
|
|
if (botGenerationDetails.eventRole)
|
|
{
|
|
bot.Info.Settings.Role = botGenerationDetails.eventRole;
|
|
}
|
|
|
|
return bot;
|
|
}
|
|
|
|
/**
|
|
* Choose various appearance settings for a bot using weights
|
|
* @param bot Bot to adjust
|
|
* @param appearance Appearance settings to choose from
|
|
* @param botGenerationDetails Generation details
|
|
*/
|
|
protected setBotAppearance(bot: IBotBase, appearance: Appearance, botGenerationDetails: BotGenerationDetails): void
|
|
{
|
|
bot.Customization.Head = this.randomUtil.getArrayValue(appearance.head);
|
|
bot.Customization.Body = this.weightedRandomHelper.getWeightedValue<string>(appearance.body);
|
|
bot.Customization.Feet = this.weightedRandomHelper.getWeightedValue<string>(appearance.feet);
|
|
bot.Customization.Hands = this.randomUtil.getArrayValue(appearance.hands);
|
|
}
|
|
|
|
/**
|
|
* Create a bot nickname
|
|
* @param botJsonTemplate x.json from database
|
|
* @param isPlayerScav Will bot be player scav
|
|
* @param botRole role of bot e.g. assault
|
|
* @returns Nickname for bot
|
|
*/
|
|
protected generateBotNickname(
|
|
botJsonTemplate: IBotType,
|
|
isPlayerScav: boolean,
|
|
botRole: string,
|
|
sessionId: string,
|
|
): string
|
|
{
|
|
let name = `${this.randomUtil.getArrayValue(botJsonTemplate.firstName)} ${
|
|
this.randomUtil.getArrayValue(botJsonTemplate.lastName) || ""
|
|
}`;
|
|
name = name.trim();
|
|
const playerProfile = this.profileHelper.getPmcProfile(sessionId);
|
|
|
|
// Simulate bot looking like a Player scav with the pmc name in brackets
|
|
if (botRole === "assault" && this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName))
|
|
{
|
|
if (isPlayerScav)
|
|
{
|
|
return name;
|
|
}
|
|
|
|
const pmcNames = [
|
|
...this.databaseServer.getTables().bots.types.usec.firstName,
|
|
...this.databaseServer.getTables().bots.types.bear.firstName,
|
|
];
|
|
|
|
return `${name} (${this.randomUtil.getArrayValue(pmcNames)})`;
|
|
}
|
|
|
|
if (this.botConfig.showTypeInNickname && !isPlayerScav)
|
|
{
|
|
name += ` ${botRole}`;
|
|
}
|
|
|
|
// If bot name matches current players name, chance to add localised prefix to name
|
|
if (name.toLowerCase() === playerProfile.Info.Nickname.toLowerCase())
|
|
{
|
|
if (this.randomUtil.getChance100(this.pmcConfig.addPrefixToSameNamePMCAsPlayerChance))
|
|
{
|
|
const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_");
|
|
name = `${prefix} ${name}`;
|
|
}
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Log the number of PMCs generated to the debug console
|
|
* @param output Generated bot array, ready to send to client
|
|
*/
|
|
protected logPmcGeneratedCount(output: IBotBase[]): void
|
|
{
|
|
const pmcCount = output.reduce(
|
|
(acc, cur) => cur.Info.Side === "Bear" || cur.Info.Side === "Usec" ? ++acc : acc,
|
|
0,
|
|
);
|
|
this.logger.debug(`Generated ${output.length} total bots. Replaced ${pmcCount} with PMCs`);
|
|
}
|
|
|
|
/**
|
|
* Converts health object to the required format
|
|
* @param healthObj health object from bot json
|
|
* @param playerScav Is a pscav bot being generated
|
|
* @returns PmcHealth object
|
|
*/
|
|
protected generateHealth(healthObj: Health, playerScav = false): PmcHealth
|
|
{
|
|
const bodyParts = playerScav ? healthObj.BodyParts[0] : this.randomUtil.getArrayValue(healthObj.BodyParts);
|
|
|
|
const newHealth: PmcHealth = {
|
|
Hydration: {
|
|
Current: this.randomUtil.getInt(healthObj.Hydration.min, healthObj.Hydration.max),
|
|
Maximum: healthObj.Hydration.max,
|
|
},
|
|
Energy: {
|
|
Current: this.randomUtil.getInt(healthObj.Energy.min, healthObj.Energy.max),
|
|
Maximum: healthObj.Energy.max,
|
|
},
|
|
Temperature: {
|
|
Current: this.randomUtil.getInt(healthObj.Temperature.min, healthObj.Temperature.max),
|
|
Maximum: healthObj.Temperature.max,
|
|
},
|
|
BodyParts: {
|
|
Head: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.Head.min, bodyParts.Head.max),
|
|
Maximum: Math.round(bodyParts.Head.max),
|
|
},
|
|
},
|
|
Chest: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.Chest.min, bodyParts.Chest.max),
|
|
Maximum: Math.round(bodyParts.Chest.max),
|
|
},
|
|
},
|
|
Stomach: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.Stomach.min, bodyParts.Stomach.max),
|
|
Maximum: Math.round(bodyParts.Stomach.max),
|
|
},
|
|
},
|
|
LeftArm: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.LeftArm.min, bodyParts.LeftArm.max),
|
|
Maximum: Math.round(bodyParts.LeftArm.max),
|
|
},
|
|
},
|
|
RightArm: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.RightArm.min, bodyParts.RightArm.max),
|
|
Maximum: Math.round(bodyParts.RightArm.max),
|
|
},
|
|
},
|
|
LeftLeg: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.LeftLeg.min, bodyParts.LeftLeg.max),
|
|
Maximum: Math.round(bodyParts.LeftLeg.max),
|
|
},
|
|
},
|
|
RightLeg: {
|
|
Health: {
|
|
Current: this.randomUtil.getInt(bodyParts.RightLeg.min, bodyParts.RightLeg.max),
|
|
Maximum: Math.round(bodyParts.RightLeg.max),
|
|
},
|
|
},
|
|
},
|
|
UpdateTime: this.timeUtil.getTimestamp(),
|
|
};
|
|
|
|
return newHealth;
|
|
}
|
|
|
|
/**
|
|
* Get a bots skills with randomsied progress value between the min and max values
|
|
* @param botSkills Skills that should have their progress value randomised
|
|
* @returns
|
|
*/
|
|
protected generateSkills(botSkills: IBaseJsonSkills): botSkills
|
|
{
|
|
const skillsToReturn: botSkills = {
|
|
Common: this.getSkillsWithRandomisedProgressValue(botSkills.Common, true),
|
|
Mastering: this.getSkillsWithRandomisedProgressValue(botSkills.Mastering, false),
|
|
Points: 0,
|
|
};
|
|
|
|
return skillsToReturn;
|
|
}
|
|
|
|
/**
|
|
* Randomise the progress value of passed in skills based on the min/max value
|
|
* @param skills Skills to randomise
|
|
* @param isCommonSkills Are the skills 'common' skills
|
|
* @returns Skills with randomised progress values as an array
|
|
*/
|
|
protected getSkillsWithRandomisedProgressValue(
|
|
skills: Record<string, IBaseSkill>,
|
|
isCommonSkills: boolean,
|
|
): IBaseSkill[]
|
|
{
|
|
if (Object.keys(skills ?? []).length === 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return Object.keys(skills).map((skillKey): IBaseSkill =>
|
|
{
|
|
// Get skill from dict, skip if not found
|
|
const skill = skills[skillKey];
|
|
if (!skill)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// All skills have id and progress props
|
|
const skillToAdd: IBaseSkill = { Id: skillKey, Progress: this.randomUtil.getInt(skill.min, skill.max) };
|
|
|
|
// Common skills have additional props
|
|
if (isCommonSkills)
|
|
{
|
|
(skillToAdd as Common).PointsEarnedDuringSession = 0;
|
|
(skillToAdd as Common).LastAccess = 0;
|
|
}
|
|
|
|
return skillToAdd;
|
|
}).filter((x) => x !== null);
|
|
}
|
|
|
|
/**
|
|
* Generate a random Id for a bot and apply to bots _id and aid value
|
|
* @param bot bot to update
|
|
* @returns updated IBotBase object
|
|
*/
|
|
protected generateId(bot: IBotBase): IBotBase
|
|
{
|
|
const botId = this.hashUtil.generate();
|
|
|
|
bot._id = botId;
|
|
bot.aid = this.hashUtil.generateAccountId();
|
|
|
|
return bot;
|
|
}
|
|
|
|
protected generateInventoryID(profile: IBotBase): IBotBase
|
|
{
|
|
const defaultInventory = "55d7217a4bdc2d86028b456d";
|
|
const itemsByParentHash: Record<string, Item[]> = {};
|
|
const inventoryItemHash: Record<string, Item> = {};
|
|
|
|
// Generate inventoryItem list
|
|
let inventoryId = "";
|
|
for (const item of profile.Inventory.items)
|
|
{
|
|
inventoryItemHash[item._id] = item;
|
|
|
|
if (item._tpl === defaultInventory)
|
|
{
|
|
inventoryId = item._id;
|
|
continue;
|
|
}
|
|
|
|
if (!("parentId" in item))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!(item.parentId in itemsByParentHash))
|
|
{
|
|
itemsByParentHash[item.parentId] = [];
|
|
}
|
|
|
|
itemsByParentHash[item.parentId].push(item);
|
|
}
|
|
|
|
// update inventoryId
|
|
const newInventoryId = this.hashUtil.generate();
|
|
inventoryItemHash[inventoryId]._id = newInventoryId;
|
|
profile.Inventory.equipment = newInventoryId;
|
|
|
|
// update inventoryItem id
|
|
if (inventoryId in itemsByParentHash)
|
|
{
|
|
for (const item of itemsByParentHash[inventoryId])
|
|
{
|
|
item.parentId = newInventoryId;
|
|
}
|
|
}
|
|
|
|
return profile;
|
|
}
|
|
|
|
/**
|
|
* Randomise a bots game version and account category
|
|
* Chooses from all the game versions (standard, eod etc)
|
|
* Chooses account type (default, Sherpa, etc)
|
|
* @param botInfo bot info object to update
|
|
*/
|
|
protected getRandomisedGameVersionAndCategory(botInfo: Info): void
|
|
{
|
|
if (botInfo.Nickname.toLowerCase() === "nikita")
|
|
{
|
|
botInfo.GameVersion = "edge_of_darkness";
|
|
botInfo.MemberCategory = MemberCategory.DEVELOPER;
|
|
|
|
return;
|
|
}
|
|
|
|
// more color = more op
|
|
botInfo.GameVersion = this.weightedRandomHelper.getWeightedValue(this.pmcConfig.gameVersionWeight);
|
|
botInfo.MemberCategory = Number.parseInt(
|
|
this.weightedRandomHelper.getWeightedValue(this.pmcConfig.accountTypeWeight),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add a side-specific (usec/bear) dogtag item to a bots inventory
|
|
* @param bot bot to add dogtag to
|
|
* @returns Bot with dogtag added
|
|
*/
|
|
protected generateDogtag(bot: IBotBase): IBotBase
|
|
{
|
|
const upd: Upd = {
|
|
SpawnedInSession: true,
|
|
Dogtag: {
|
|
AccountId: bot.sessionId,
|
|
ProfileId: bot._id,
|
|
Nickname: bot.Info.Nickname,
|
|
Side: bot.Info.Side,
|
|
Level: bot.Info.Level,
|
|
Time: (new Date().toISOString()),
|
|
Status: "Killed by ",
|
|
KillerAccountId: "Unknown",
|
|
KillerProfileId: "Unknown",
|
|
KillerName: "Unknown",
|
|
WeaponName: "Unknown",
|
|
},
|
|
};
|
|
|
|
const inventoryItem: Item = {
|
|
_id: this.hashUtil.generate(),
|
|
_tpl: ((bot.Info.Side === "Usec") ? BaseClasses.DOG_TAG_USEC : BaseClasses.DOG_TAG_BEAR),
|
|
parentId: bot.Inventory.equipment,
|
|
slotId: "Dogtag",
|
|
location: undefined,
|
|
upd: upd,
|
|
};
|
|
|
|
bot.Inventory.items.push(inventoryItem);
|
|
|
|
return bot;
|
|
}
|
|
}
|