diff --git a/project/assets/configs/pmcchatresponse.json b/project/assets/configs/pmcchatresponse.json new file mode 100644 index 00000000..08ba7ab5 --- /dev/null +++ b/project/assets/configs/pmcchatresponse.json @@ -0,0 +1,24 @@ +{ + "victim": { + "responseChancePercent": 25, + "responseTypeWeights": { + "positive": 8, + "negative": 2, + "plead": 2, + }, + "stripCapitalisationChancePercent": 25, + "allCapsChancePercent": 25, + "appendBroToMessageEndChancePercent": 35 + }, + "killer": { + "responseChancePercent": 20, + "responseTypeWeights": { + "positive": 5, + "negative": 2, + "plead": 2, + }, + "stripCapitalisationChancePercent": 25, + "allCapsChancePercent": 25, + "appendBroToMessageEndChancePercent": 35 + } +} diff --git a/project/assets/locales/en.json b/project/assets/locales/en.json index 7c01bb7f..5de3f4b5 100644 --- a/project/assets/locales/en.json +++ b/project/assets/locales/en.json @@ -147,8 +147,8 @@ "ragfair-unable_to_find_item_in_inventory": "Unable to find item with id: {{id}} in inventory", "ragfair-unable_to_find_requested_items_in_inventory": "Unable to find any requested items in the inventory", "ragfair-unable_to_pay_commission_fee": "Unable to pay commission fee", - "ragfair-offer_no_longer_exists": "Offer no longer exists", - "ragfair-unable_to_purchase_0_count_item": "Unable to purchase item: %s with a count of 0", + "ragfair-offer_no_longer_exists": "Offer no longer exists", + "ragfair-unable_to_purchase_0_count_item": "Unable to purchase item: %s with a count of 0", "ragfair-unable_to_place_offer_with_no_requirements": "Unable to place offer with no requirements", "repeatable-accepted_repeatable_quest_not_found_in_active_quests": "Accepted a repeatable quest: %s which could not be found in the activeQuests array. Please report this bug", "repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive": "Generate Completion Quest: No items remain. Either Whitelist is too small or Blacklist too restrictive", @@ -215,5 +215,30 @@ "websocket-player_connected": "[WS] Player: %s has connected", "websocket-received_message": "[WS] Received message from user %s ", "websocket-socket_lost_deleting_handle": "[WS] Socket lost, deleting handle", - "websocket-started": "Started websocket at %s" -} + "websocket-started": "Started websocket at %s", + "pmcresponse-victim_positive_1": "Nice shot", + "pmcresponse-victim_positive_2": "Great shot", + "pmcresponse-victim_positive_3": "Good kill man", + "pmcresponse-victim_positive_4": "Deserved kill, good one", + "pmcresponse-victim_positive_5": "Lucky kill", + "pmcresponse-victim_positive_6": "Good fight", + "pmcresponse-victim_positive_7": "That was fair, nice kill", + "pmcresponse-victim_positive_8": "You're a good shot, that's for sure", + "pmcresponse-victim_negative_1": "Nice aimbot", + "pmcresponse-victim_negative_2": "Cheap shot", + "pmcresponse-victim_negative_3": "Wow esp much", + "pmcresponse-victim_negative_4": "Cheap kill", + "pmcresponse-victim_negative_5": "Nice cheese strats", + "pmcresponse-victim_negative_6": "How much did your hacks cost", + "pmcresponse-victim_negative_7": ":(", + "pmcresponse-victim_negative_8": "I am malding so hard right now", + "pmcresponse-victim_negative_9": "Good job sweatlord", + "pmcresponse-victim_negative_10": "I was AFK!!", + "pmcresponse-victim_negative_11": "Reported you for cheating :)", + "pmcresponse-victim_plead_1": "I was questing", + "pmcresponse-victim_plead_2": "I just wanted to do a quest, why'd you kill me :(", + "pmcresponse-victim_plead_3": "Hope ur happy i cant even afford a new kit", + "pmcresponse-victim_plead_4": "Bro i'm new to the game why you kill me", + "pmcresponse-victim_plead_5": "I am never gonna get this stupid quest done", + "pmcresponse-victim_plead_6": "Did you at least stash my gear?!" +} \ No newline at end of file diff --git a/project/src/controllers/InraidController.ts b/project/src/controllers/InraidController.ts index 64579048..39e6572b 100644 --- a/project/src/controllers/InraidController.ts +++ b/project/src/controllers/InraidController.ts @@ -29,6 +29,7 @@ import { DatabaseServer } from "../servers/DatabaseServer"; import { SaveServer } from "../servers/SaveServer"; import { InsuranceService } from "../services/InsuranceService"; import { LocaleService } from "../services/LocaleService"; +import { PmcChatResponseService } from "../services/PmcChatResponseService"; import { JsonUtil } from "../utils/JsonUtil"; import { TimeUtil } from "../utils/TimeUtil"; @@ -48,6 +49,7 @@ export class InraidController @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("LocaleService") protected localeService: LocaleService, + @inject("PmcChatResponseService") protected pmcChatResponseService: PmcChatResponseService, @inject("QuestHelper") protected questHelper: QuestHelper, @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper, @@ -137,28 +139,22 @@ export class InraidController { if (locationName.toLowerCase() === "laboratory") { - const localeDb = this.localeService.getLocaleDb(); - - const failedText = localeDb["5a8fd75188a45036844e0b0c"]; - const senderDetails: IUserDialogInfo = { - _id: Traders.PRAPOR, - info: { - Nickname: "Prapor", - Level: 1, - Side: "Bear", - MemberCategory: MemberCategory.TRADER - } - }; - - this.notificationSendHelper.sendMessageToPlayer(sessionID, senderDetails, failedText, MessageType.NPC_TRADER); + this.sendLostInsuranceMessage(sessionID); } } if (isDead) { + //TODO - find way to get killer name //this.pmcChatResponseService.sendKillerResponse(sessionID, pmcData); pmcData = this.performPostRaidActionsWhenDead(offraidData, pmcData, insuranceEnabled, preRaidGear, sessionID); } + const victims = offraidData.profile.Stats.Victims.filter(x => x.Role === "sptBear" || x.Role === "sptUsec"); + if (victims?.length > 0) + { + this.pmcChatResponseService.sendVictimResponse(sessionID, victims); + } + if (insuranceEnabled) { this.insuranceService.sendInsuredItems(pmcData, sessionID, map.Id); @@ -260,6 +256,24 @@ export class InraidController this.handlePostRaidPlayerScavProcess(scavData, sessionID, offraidData, pmcData, isDead); } + protected sendLostInsuranceMessage(sessionID: string): void + { + const localeDb = this.localeService.getLocaleDb(); + + const failedText = localeDb["5a8fd75188a45036844e0b0c"]; + const senderDetails: IUserDialogInfo = { + _id: Traders.PRAPOR, + info: { + Nickname: "Prapor", + Level: 1, + Side: "Bear", + MemberCategory: MemberCategory.TRADER + } + }; + + this.notificationSendHelper.sendMessageToPlayer(sessionID, senderDetails, failedText, MessageType.NPC_TRADER); + } + /** * Is the player dead after a raid - dead is anything other than "survived" / "runner" * @param statusOnExit exit value from offraidData object diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index d3fac019..7d79524b 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -1,4 +1,5 @@ import { DependencyContainer, Lifecycle } from "tsyringe"; + import { BotCallbacks } from "../callbacks/BotCallbacks"; import { BundleCallbacks } from "../callbacks/BundleCallbacks"; import { CustomizationCallbacks } from "../callbacks/CustomizationCallbacks"; @@ -212,6 +213,7 @@ import { NotificationService } from "../services/NotificationService"; import { OpenZoneService } from "../services/OpenZoneService"; import { PaymentService } from "../services/PaymentService"; import { PlayerService } from "../services/PlayerService"; +import { PmcChatResponseService } from "../services/PmcChatResponseService"; import { ProfileFixerService } from "../services/ProfileFixerService"; import { ProfileSnapshotService } from "../services/ProfileSnapshotService"; import { RagfairCategoriesService } from "../services/RagfairCategoriesService"; @@ -603,6 +605,7 @@ export class Container depContainer.register("BotWeaponModLimitService", BotWeaponModLimitService, { lifecycle: Lifecycle.Singleton }); depContainer.register("SeasonalEventService", SeasonalEventService, { lifecycle: Lifecycle.Singleton }); depContainer.register("TraderPurchasePersisterService", TraderPurchasePersisterService); + depContainer.register("PmcChatResponseService", PmcChatResponseService); } private static registerServers(depContainer: DependencyContainer): void diff --git a/project/src/models/enums/ConfigTypes.ts b/project/src/models/enums/ConfigTypes.ts index f7b99be7..23252e33 100644 --- a/project/src/models/enums/ConfigTypes.ts +++ b/project/src/models/enums/ConfigTypes.ts @@ -14,6 +14,7 @@ export enum ConfigTypes LOCATION = "aki-location", MATCH = "aki-match", PLAYERSCAV = "aki-playerscav", + PMC_CHAT_RESPONSE = "aki-pmcchatresponse", QUEST = "aki-quest", RAGFAIR = "aki-ragfair", REPAIR = "aki-repair", diff --git a/project/src/models/spt/config/IPmChatResponse.ts b/project/src/models/spt/config/IPmChatResponse.ts new file mode 100644 index 00000000..886d41bc --- /dev/null +++ b/project/src/models/spt/config/IPmChatResponse.ts @@ -0,0 +1,17 @@ +import { IBaseConfig } from "./IBaseConfig"; + +export interface IPmcChatResponse extends IBaseConfig +{ + kind: "aki-pmcchatresponse" + victim: IResponseSettings + killer: IResponseSettings +} + +export interface IResponseSettings +{ + responseChancePercent: number + responseTypeWeights: Record + stripCapitalisationChancePercent: number + allCapsChancePercent: number; + appendBroToMessageEndChancePercent: number; +} \ No newline at end of file diff --git a/project/src/services/LocalisationService.ts b/project/src/services/LocalisationService.ts index 4d56fbf9..1782e7ab 100644 --- a/project/src/services/LocalisationService.ts +++ b/project/src/services/LocalisationService.ts @@ -42,4 +42,13 @@ export class LocalisationService { return this.i18n.__(key.toLowerCase(), args); } + + /** + * Get all locale keys + * @returns string array of keys + */ + public getKeys(): string[] + { + return Object.keys(this.i18n.getCatalog("en")); + } } \ No newline at end of file diff --git a/project/src/services/PmcChatResponseService.ts b/project/src/services/PmcChatResponseService.ts new file mode 100644 index 00000000..235d0f1d --- /dev/null +++ b/project/src/services/PmcChatResponseService.ts @@ -0,0 +1,179 @@ +import { inject, injectable } from "tsyringe"; + +import { NotificationSendHelper } from "../helpers/NotificationSendHelper"; +import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper"; +import { IPmcData } from "../models/eft/common/IPmcData"; +import { Victim } from "../models/eft/common/tables/IBotBase"; +import { IUserDialogInfo } from "../models/eft/profile/IAkiProfile"; +import { ConfigTypes } from "../models/enums/ConfigTypes"; +import { MemberCategory } from "../models/enums/MemberCategory"; +import { MessageType } from "../models/enums/MessageType"; +import { IPmcChatResponse } from "../models/spt/config/IPmChatResponse"; +import { ConfigServer } from "../servers/ConfigServer"; +import { RandomUtil } from "../utils/RandomUtil"; +import { LocalisationService } from "./LocalisationService"; + +@injectable() +export class PmcChatResponseService +{ + protected pmcResponsesConfig: IPmcChatResponse; + + constructor( + @inject("RandomUtil") protected randomUtil: RandomUtil, + @inject("NotificationSendHelper") protected notificationSendHelper: NotificationSendHelper, + @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, + @inject("ConfigServer") protected configServer: ConfigServer + ) + { + this.pmcResponsesConfig = this.configServer.getConfig(ConfigTypes.PMC_CHAT_RESPONSE); + } + + /** + * Chooses a random victim from those provided and sends a message to the player, can be positive or negative + * @param sessionId Session id + * @param pmcVictims Array of bots killed by player + */ + public sendVictimResponse(sessionId: string, pmcVictims: Victim[]): void + { + const victim = this.chooseRandomVictim(pmcVictims); + + const message = this.chooseMessage(true); + + this.notificationSendHelper.sendMessageToPlayer(sessionId, victim, message, MessageType.USER_MESSAGE); + } + + + /** + * Not fully implemented yet, needs method of acquiring killers details after raid + * @param sessionId Session id + * @param pmcData Players profile + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public sendKillerResponse(sessionId: string, pmcData: IPmcData): void + { + const killer: IUserDialogInfo = { + _id: "", + info: undefined + }; + + const message = this.chooseMessage(false); + + this.notificationSendHelper.sendMessageToPlayer(sessionId, killer, message, MessageType.USER_MESSAGE); + } + + /** + * Choose a localised message to send the player (different if sender was killed or killed player) + * @param isVictim + * @returns + */ + protected chooseMessage(isVictim: boolean): string + { + // Positive/negative etc + const responseType = this.chooseResponseType(isVictim); + + // Get all locale keys + const possibleResponseLocaleKeys = this.getResponseLocaleKeys(responseType, isVictim); + + // Choose random response from above list and request it from localisation service + let responseText = this.localisationService.getText(this.randomUtil.getArrayValue(possibleResponseLocaleKeys)); + if (this.appendBroToMessageEnd(isVictim)) + { + responseText += " bro"; + } + + if (this.stripCapitalistion(isVictim)) + { + responseText = responseText.toLowerCase(); + } + + if (this.allCaps(isVictim)) + { + responseText = responseText.toUpperCase(); + } + + return responseText; + } + + /** + * Should capitalisation be stripped from the message response before sending + * @param isVictim Was responder a victim of player + * @returns true = should be stripped + */ + protected stripCapitalistion(isVictim: boolean): boolean + { + const chance = isVictim + ? this.pmcResponsesConfig.victim.stripCapitalisationChancePercent + : this.pmcResponsesConfig.killer.stripCapitalisationChancePercent; + + return this.randomUtil.getChance100(chance); + } + + /** + * Should capitalisation be stripped from the message response before sending + * @param isVictim Was responder a victim of player + * @returns true = should be stripped + */ + protected allCaps(isVictim: boolean): boolean + { + const chance = isVictim + ? this.pmcResponsesConfig.victim.allCapsChancePercent + : this.pmcResponsesConfig.killer.allCapsChancePercent; + + return this.randomUtil.getChance100(chance); + } + + /** + * Should the word 'bro' be appended to the message being sent to player + * @param isVictim Was responder a victim of player + * @returns true = should be stripped + */ + appendBroToMessageEnd(isVictim: boolean): boolean + { + const chance = isVictim + ? this.pmcResponsesConfig.victim.appendBroToMessageEndChancePercent + : this.pmcResponsesConfig.killer.appendBroToMessageEndChancePercent; + + return this.randomUtil.getChance100(chance); + } + + /** + * Choose a type of response based on the weightings in pmc response config + * @param isVictim Was responder killed by player + * @returns Response type (positive/negative) + */ + protected chooseResponseType(isVictim = true): string + { + const responseWeights = isVictim + ? this.pmcResponsesConfig.victim.responseTypeWeights + : this.pmcResponsesConfig.killer.responseTypeWeights; + + return this.weightedRandomHelper.getWeightedInventoryItem(responseWeights); + } + + /** + * Get locale keys related to the type of response to send (victim/killer) + * @param keyType Positive/negative + * @param isVictim Was responder killed by player + * @returns + */ + protected getResponseLocaleKeys(keyType: string, isVictim = true): string[] + { + const keyBase = isVictim ? "pmcresponse-victim_" : "pmcresponse-killer_"; + const keys = this.localisationService.getKeys(); + + return keys.filter(x => x.startsWith(`${keyBase}${keyType}`)); + } + + /** + * Randomly draw a victim of the the array and return thier details + * @param pmcVictims Possible victims to choose from + * @returns IUserDialogInfo + */ + protected chooseRandomVictim(pmcVictims: Victim[]): IUserDialogInfo + { + const randomVictim = this.randomUtil.getArrayValue(pmcVictims); + + return {_id: randomVictim.Name, info:{Nickname: randomVictim.Name, Level: randomVictim.Level, Side: randomVictim.Side, MemberCategory: MemberCategory.UNIQUE_ID}}; + } +} \ No newline at end of file