Archived
Adding Mods w/Symlink
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
[General]
|
||||
gameName=spt
|
||||
modid=0
|
||||
version=d2025.1.2.0
|
||||
newestVersion=
|
||||
category="1,"
|
||||
nexusFileStatus=1
|
||||
installationFile=inory-dynamicgoons-1.1.0.rar
|
||||
repository=Nexus
|
||||
ignoredVersion=
|
||||
comments=
|
||||
notes=
|
||||
nexusDescription=
|
||||
url=
|
||||
hasCustomURL=true
|
||||
lastNexusQuery=
|
||||
lastNexusUpdate=
|
||||
nexusLastModified=2024-12-16T05:56:04Z
|
||||
nexusCategory=0
|
||||
converted=false
|
||||
validated=false
|
||||
color=@Variant(\0\0\0\x43\0\xff\xff\0\0\0\0\0\0\0\0)
|
||||
tracked=0
|
||||
|
||||
[installedFiles]
|
||||
1\modid=0
|
||||
1\fileid=0
|
||||
size=1
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"debugLogs": false,
|
||||
"preventSameMapRotation": true,
|
||||
"goonsSpawnChance": 30,
|
||||
"rotationInterval": 180
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"enabledMaps": {
|
||||
"bigmap": true,
|
||||
"shoreline": true,
|
||||
"lighthouse": true,
|
||||
"woods": true,
|
||||
"rezervbase": false,
|
||||
"laboratory": false,
|
||||
"tarkovstreets": false,
|
||||
"factory4_day": false,
|
||||
"factory4_night": false,
|
||||
"sandbox_high": false,
|
||||
"interchange": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "Dynamic Goons",
|
||||
"version": "1.1.0",
|
||||
"sptVersion": "~3.10",
|
||||
"loadBefore": [],
|
||||
"loadAfter": [],
|
||||
"incompatibilities": [],
|
||||
"isBundleMod": false,
|
||||
"main": "src/mod.js",
|
||||
"scripts": {
|
||||
"setup": "npm i",
|
||||
"build": "node ./build.mjs",
|
||||
"buildinfo": "node ./build.mjs --verbose"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.11",
|
||||
"@typescript-eslint/eslint-plugin": "7.2",
|
||||
"@typescript-eslint/parser": "7.2",
|
||||
"archiver": "^6.0",
|
||||
"eslint": "8.57",
|
||||
"fs-extra": "11.2",
|
||||
"ignore": "^5.2",
|
||||
"tsyringe": "4.8.0",
|
||||
"typescript": "5.4",
|
||||
"winston": "3.12"
|
||||
},
|
||||
"author": "inory",
|
||||
"contributors": [],
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { inject, injectAll, injectable } from "tsyringe";
|
||||
|
||||
import { AbstractDialogueChatBot } from "@spt/helpers/Dialogue/AbstractDialogueChatBot";
|
||||
import { IChatCommand } from "@spt/helpers/Dialogue/Commando/IChatCommand";
|
||||
import { IUserDialogInfo } from "@spt/models/eft/profile/ISptProfile";
|
||||
import { MemberCategory } from "@spt/models/enums/MemberCategory";
|
||||
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||
import { MailSendService } from "@spt/services/MailSendService";
|
||||
|
||||
@injectable()
|
||||
export class GoonsTracker extends AbstractDialogueChatBot {
|
||||
constructor(
|
||||
@inject("WinstonLogger") logger: ILogger,
|
||||
@inject("MailSendService") mailSendService: MailSendService,
|
||||
@injectAll("TrackerCommands") chatCommands: IChatCommand[]
|
||||
) {
|
||||
super(logger, mailSendService, chatCommands);
|
||||
}
|
||||
|
||||
public getChatBot(): IUserDialogInfo {
|
||||
return {
|
||||
_id: "674d96b02225f02fff47b3be",
|
||||
aid: 777,
|
||||
Info: {
|
||||
Level: 1,
|
||||
MemberCategory: MemberCategory.SHERPA,
|
||||
SelectedMemberCategory: MemberCategory.SHERPA,
|
||||
Nickname: "Goons Tracker",
|
||||
Side: "Usec",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected getUnrecognizedCommandMessage(): string {
|
||||
return (
|
||||
"Unrecognized command, please type goons track to receive details of The Goon's location. " +
|
||||
"Or type goons rotation if you need an explanation for their rotation mechanic."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { inject, injectable } from "tsyringe";
|
||||
import { IChatCommand } from "@spt/helpers/Dialogue/Commando/IChatCommand";
|
||||
import { ISendMessageRequest } from "@spt/models/eft/dialog/ISendMessageRequest";
|
||||
import { IUserDialogInfo } from "@spt/models/eft/profile/ISptProfile";
|
||||
import { MailSendService } from "@spt/services/MailSendService";
|
||||
import { ChatLocationService } from "../services/ChatLocationService";
|
||||
|
||||
@injectable()
|
||||
export class TrackerCommands implements IChatCommand {
|
||||
constructor(
|
||||
@inject("MailSendService") protected mailSendService: MailSendService,
|
||||
@inject("ChatLocationService") private locationService: ChatLocationService
|
||||
) {}
|
||||
|
||||
public getCommandPrefix(): string {
|
||||
return "goons";
|
||||
}
|
||||
|
||||
public getCommandHelp(command: string): string {
|
||||
if (command === "track") {
|
||||
return "Usage: goons track - Get current rotation information.";
|
||||
} else if (command === "rotation") {
|
||||
return "Usage: goons rotation - Learn about the rotation mechanics.";
|
||||
}
|
||||
}
|
||||
|
||||
public getCommands(): Set<string> {
|
||||
return new Set<string>(["track", "rotation"]);
|
||||
}
|
||||
|
||||
public handle(
|
||||
command: string,
|
||||
commandHandler: IUserDialogInfo,
|
||||
sessionId: string,
|
||||
request: ISendMessageRequest
|
||||
): string {
|
||||
if (command === "track") {
|
||||
try {
|
||||
const locationData = this.locationService.getLocationData();
|
||||
|
||||
const responseMessage =
|
||||
`Location: ${locationData.location}\n` +
|
||||
`Last Seen: ${locationData.timeSinceLastSeen} minutes ago\n` +
|
||||
`Rotation: ${locationData.rotationChance.toFixed(2)}%\n` +
|
||||
`${locationData.dateLastSeen}`;
|
||||
|
||||
this.mailSendService.sendUserMessageToPlayer(
|
||||
sessionId,
|
||||
commandHandler,
|
||||
responseMessage
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in handle:", error.message);
|
||||
this.mailSendService.sendUserMessageToPlayer(
|
||||
sessionId,
|
||||
commandHandler,
|
||||
"Error retrieving location data. Please try again later."
|
||||
);
|
||||
}
|
||||
} else if (command === "rotation") {
|
||||
const rotationExplanation =
|
||||
"The Goons stay on a map for a variable amount of time. " +
|
||||
"As time passes, the chance of them switching to a new " +
|
||||
"map increases. They will rotate to a new map at the end of a raid based on this chance. " +
|
||||
"Use goons track to see where they are currently and their rotation chance.";
|
||||
|
||||
this.mailSendService.sendUserMessageToPlayer(
|
||||
sessionId,
|
||||
commandHandler,
|
||||
rotationExplanation
|
||||
);
|
||||
}
|
||||
|
||||
return request.dialogId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"factory4_day": ["BotZone"],
|
||||
"factory4_night": ["BotZone"],
|
||||
"tarkovstreets": ["ZoneCinema", "ZoneConcordiaParking"],
|
||||
"laboratory": ["BotZoneFloor1", "BotZoneFloor2"],
|
||||
"rezervbase": ["ZoneRailStrorage", "ZoneBarrack"],
|
||||
"sandbox_high": ["ZoneSandbox"],
|
||||
"interchange": ["ZonePowerStation", "ZoneCenterBot"]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"nextUpdateTime": 0,
|
||||
"selectedMap": "0",
|
||||
"lastRotationInterval": 180,
|
||||
"lastUpdateTime": 0
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import * as path from "path";
|
||||
import type { DependencyContainer } from "tsyringe";
|
||||
import type { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||
import type { IPostDBLoadMod } from "@spt/models/external/IPostDBLoadMod";
|
||||
import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
|
||||
import type { DatabaseServer } from "@spt/servers/DatabaseServer";
|
||||
import type { IDatabaseTables } from "@spt/models/spt/server/IDatabaseTables";
|
||||
import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
|
||||
import type { LocationCallbacks } from "@spt/callbacks/LocationCallback";
|
||||
import { ILocations } from "@spt/models/spt/server/ILocations";
|
||||
import { DialogueController } from "@spt/controllers/DialogueController";
|
||||
|
||||
import { TrackerCommands } from "./chatbot/TrackerCommands";
|
||||
import { GoonsTracker } from "./chatbot/GoonsTracker";
|
||||
import { ChatLocationService } from "./services/ChatLocationService";
|
||||
import { RotationService } from "./services/RotationService";
|
||||
import { AddBossToMaps } from "./services/AddGoonsToMaps";
|
||||
|
||||
class Mod implements IPostDBLoadMod, IPreSptLoadMod {
|
||||
private logger: ILogger;
|
||||
private databaseServer: DatabaseServer;
|
||||
private tables: IDatabaseTables;
|
||||
private maps: ILocations;
|
||||
private locationCallbacks: LocationCallbacks;
|
||||
private rotationService: RotationService;
|
||||
private addBossToAllMaps: AddBossToMaps;
|
||||
private rotationData = path.resolve(__dirname, "db/rotationData.json");
|
||||
private modConfig = require("../config/config.json");
|
||||
private mapConfig = path.resolve(__dirname, "../config/mapConfig.json");
|
||||
private zonesConfigPath = path.resolve(__dirname, "../src/db/mapZones.json");
|
||||
private async postDBLoad(container: DependencyContainer): Promise<void> {
|
||||
this.databaseServer = container.resolve<DatabaseServer>("DatabaseServer");
|
||||
this.tables = this.databaseServer.getTables();
|
||||
this.maps = this.tables.locations;
|
||||
this.locationCallbacks =
|
||||
container.resolve<LocationCallbacks>("LocationCallbacks");
|
||||
|
||||
container.register("ChatLocationService", {
|
||||
useClass: ChatLocationService,
|
||||
});
|
||||
|
||||
container.register<TrackerCommands>("TrackerCommands", TrackerCommands);
|
||||
container.register<GoonsTracker>("GoonsTracker", GoonsTracker);
|
||||
|
||||
container
|
||||
.resolve<DialogueController>("DialogueController")
|
||||
.registerChatBot(container.resolve<GoonsTracker>("GoonsTracker"));
|
||||
|
||||
this.addBossToAllMaps = new AddBossToMaps(
|
||||
this.logger,
|
||||
this.zonesConfigPath,
|
||||
this.modConfig
|
||||
);
|
||||
this.addBossToAllMaps.addBossToMaps(this.maps);
|
||||
|
||||
this.rotationService = new RotationService(
|
||||
this.logger,
|
||||
this.modConfig,
|
||||
this.rotationData,
|
||||
this.mapConfig
|
||||
);
|
||||
|
||||
const rotationData = await this.rotationService.readRotationData();
|
||||
const currentTime = Date.now();
|
||||
const rotationInterval = this.modConfig.rotationInterval;
|
||||
|
||||
await this.rotationService.handleRotationChance(
|
||||
rotationData,
|
||||
currentTime,
|
||||
rotationInterval
|
||||
);
|
||||
}
|
||||
|
||||
public async preSptLoad(container: DependencyContainer): Promise<void> {
|
||||
this.logger = container.resolve<ILogger>("WinstonLogger");
|
||||
|
||||
const staticRouterModService = container.resolve<StaticRouterModService>(
|
||||
"StaticRouterModService"
|
||||
);
|
||||
|
||||
staticRouterModService.registerStaticRouter(
|
||||
"RotationUpdate",
|
||||
[
|
||||
{
|
||||
url: "/client/locations",
|
||||
action: async (
|
||||
url: string,
|
||||
info: any,
|
||||
sessionId: string,
|
||||
output: string
|
||||
) => {
|
||||
await this.updateBossSpawnChances();
|
||||
return this.locationCallbacks.getLocationData(url, info, sessionId);
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "/client/match/local/end",
|
||||
action: async (url, info, sessionId, output) => {
|
||||
const rotationData = await this.rotationService.readRotationData();
|
||||
const currentTime = Date.now();
|
||||
const rotationInterval = this.modConfig.rotationInterval || 180;
|
||||
|
||||
await this.rotationService.handleRotationChance(
|
||||
rotationData,
|
||||
currentTime,
|
||||
rotationInterval
|
||||
);
|
||||
|
||||
return output;
|
||||
},
|
||||
},
|
||||
],
|
||||
"spt"
|
||||
);
|
||||
}
|
||||
|
||||
private async updateBossSpawnChances(): Promise<void> {
|
||||
const { selectedMap } =
|
||||
await this.rotationService.getNextUpdateTimeAndMapData();
|
||||
const bossName = "bossKnight";
|
||||
const spawnChance = this.modConfig.goonsSpawnChance;
|
||||
|
||||
for (const mapName in this.maps) {
|
||||
const mapBosses = this.maps[mapName]?.base?.BossLocationSpawn || [];
|
||||
|
||||
for (const mapBoss of mapBosses) {
|
||||
if (mapBoss.BossName !== bossName) continue;
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] ${mapName}: Before Chance: ${mapBoss.BossChance}`
|
||||
);
|
||||
}
|
||||
|
||||
mapBoss.BossChance = mapName === selectedMap ? spawnChance : 0;
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] ${mapName}: After Chance: ${mapBoss.BossChance}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mod = new Mod();
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as fs from "fs";
|
||||
import { inject, injectable } from "tsyringe";
|
||||
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||
import { ILocation } from "@spt/models/eft/common/ILocation";
|
||||
import { IBossLocationSpawn } from "@spt/models/eft/common/ILocationBase";
|
||||
|
||||
@injectable()
|
||||
export class AddBossToMaps {
|
||||
private zonesConfig: Record<string, string[]>;
|
||||
private bossData: IBossLocationSpawn;
|
||||
private modConfig: any;
|
||||
constructor(
|
||||
@inject("Logger") private logger: ILogger,
|
||||
private zonesConfigPath: string,
|
||||
modConfig: any
|
||||
) {
|
||||
this.zonesConfig = this.loadZonesConfig();
|
||||
this.bossData = this.defineBossData();
|
||||
this.modConfig = modConfig;
|
||||
}
|
||||
|
||||
public addBossToMaps(locationList: Record<string, ILocation>): void {
|
||||
const bossName = this.bossData.BossName;
|
||||
|
||||
for (const mapName in this.zonesConfig) {
|
||||
const location = locationList[mapName];
|
||||
if (!location) {
|
||||
this.logger.warning(
|
||||
`[Dynamic Goons] Skipping map '${mapName}' as it is not present in the locationList.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const zonesForMap = this.zonesConfig[mapName];
|
||||
const combinedZones = zonesForMap.join(",");
|
||||
|
||||
const mapBosses = location.base?.BossLocationSpawn || [];
|
||||
|
||||
mapBosses.push({
|
||||
...this.bossData,
|
||||
BossZone: combinedZones, // Combine all zones into a single entry
|
||||
});
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] Added boss '${bossName}' to map '${mapName}' with zones '${combinedZones}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadZonesConfig(): Record<string, string[]> {
|
||||
try {
|
||||
const data = fs.readFileSync(this.zonesConfigPath, "utf8");
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Dynamic Goons] Error loading zones config: ${error.message}`
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private defineBossData(): IBossLocationSpawn {
|
||||
return {
|
||||
BossChance: 30,
|
||||
BossDifficult: "normal",
|
||||
BossEscortAmount: "0",
|
||||
BossEscortDifficult: "normal",
|
||||
BossEscortType: "exUsec",
|
||||
BossName: "bossKnight",
|
||||
BossPlayer: false,
|
||||
BossZone: "",
|
||||
Delay: 0,
|
||||
DependKarma: false,
|
||||
DependKarmaPVE: false,
|
||||
ForceSpawn: false,
|
||||
IgnoreMaxBots: true,
|
||||
RandomTimeSpawn: true,
|
||||
SpawnMode: ["pve", "regular"],
|
||||
Supports: [
|
||||
{
|
||||
BossEscortAmount: "1",
|
||||
BossEscortDifficult: ["normal"],
|
||||
BossEscortType: "followerBigPipe",
|
||||
},
|
||||
{
|
||||
BossEscortAmount: "1",
|
||||
BossEscortDifficult: ["normal"],
|
||||
BossEscortType: "followerBirdEye",
|
||||
},
|
||||
],
|
||||
Time: -1,
|
||||
TriggerId: "",
|
||||
TriggerName: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { injectable } from "tsyringe";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { rotationChanceCalculator } from "./RotationChanceCalculator";
|
||||
|
||||
@injectable()
|
||||
export class ChatLocationService {
|
||||
private dataFilePath = path.resolve(__dirname, "../db/rotationData.json");
|
||||
private modConfig = require("../../config/config.json");
|
||||
|
||||
public getLocationData(): {
|
||||
location: string;
|
||||
timeSinceLastSeen: number;
|
||||
rotationChance: number;
|
||||
dateLastSeen: string;
|
||||
} {
|
||||
try {
|
||||
const data = fs.readFileSync(this.dataFilePath, "utf-8");
|
||||
const parsedData = JSON.parse(data);
|
||||
|
||||
if (
|
||||
parsedData &&
|
||||
parsedData.selectedMap &&
|
||||
typeof parsedData.lastUpdateTime === "number"
|
||||
) {
|
||||
const locationMap: { [key: string]: string } = {
|
||||
bigmap: "Customs",
|
||||
woods: "Woods",
|
||||
shoreline: "Shoreline",
|
||||
lighthouse: "Lighthouse",
|
||||
tarkovstreets: "Streets of Tarkov",
|
||||
interchange: "Interchange",
|
||||
sandbox_high: "Ground Zero",
|
||||
factory4_day: "Factory Day",
|
||||
factory4_night: "Factory Night",
|
||||
laboratory: "The Lab",
|
||||
rezervbase: "Reserve",
|
||||
};
|
||||
|
||||
const location = locationMap[parsedData.selectedMap.toLowerCase()];
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeSinceLastSeen = Math.max(
|
||||
0,
|
||||
Math.floor((currentTime - parsedData.lastUpdateTime) / 1000 / 60)
|
||||
);
|
||||
|
||||
const remainingTime = parsedData.nextUpdateTime - currentTime;
|
||||
const rotationChance = this.calculateRotationChance(remainingTime);
|
||||
|
||||
const dateLastSeen = new Date(parsedData.lastUpdateTime).toLocaleString(
|
||||
"en-US",
|
||||
{
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
location,
|
||||
timeSinceLastSeen,
|
||||
rotationChance,
|
||||
dateLastSeen,
|
||||
};
|
||||
} else {
|
||||
throw new Error("Invalid data format in the JSON file.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reading or parsing the JSON file:", error.message);
|
||||
throw new Error(
|
||||
"Error retrieving location data. Please try again later."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public calculateRotationChance(remainingTime: number): number {
|
||||
return rotationChanceCalculator(
|
||||
remainingTime,
|
||||
this.modConfig.rotationInterval
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||
|
||||
export function rotationChanceCalculator(
|
||||
remainingTime: number,
|
||||
rotationInterval: number,
|
||||
logger: ILogger | null = null,
|
||||
debugLogs: boolean = false
|
||||
): number {
|
||||
const maxTime = rotationInterval * 60 * 1000;
|
||||
const maxChance = 100;
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
return maxChance;
|
||||
}
|
||||
|
||||
const factor = remainingTime / maxTime;
|
||||
|
||||
const steepFactor = Math.pow(factor, 0.3); // Adjust the rotation chance curve, smaller exponent, slower the initial rise
|
||||
|
||||
const chance = maxChance * (1 - Math.pow(steepFactor, 1));
|
||||
|
||||
if (debugLogs && logger) {
|
||||
logger.info(
|
||||
`[Dynamic Goons] Remaining time: ${remainingTime}ms, Rotation chance: ${chance}%`
|
||||
);
|
||||
}
|
||||
|
||||
return chance;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import * as fs from "fs";
|
||||
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||
import { inject, injectable } from "tsyringe";
|
||||
import { rotationChanceCalculator } from "../services/RotationChanceCalculator";
|
||||
|
||||
@injectable()
|
||||
export class RotationService {
|
||||
private modConfig: any;
|
||||
private rotationData: string;
|
||||
private mapConfig: any;
|
||||
constructor(
|
||||
@inject("Logger") private logger: ILogger,
|
||||
modConfig: any,
|
||||
rotationDataFilePath: string,
|
||||
mapConfig: any
|
||||
) {
|
||||
this.modConfig = modConfig;
|
||||
this.rotationData = rotationDataFilePath;
|
||||
this.mapConfig = mapConfig;
|
||||
}
|
||||
|
||||
public async getNextUpdateTimeAndMapData(): Promise<{
|
||||
nextUpdateTime: number;
|
||||
selectedMap: string;
|
||||
}> {
|
||||
try {
|
||||
const rotationData = await this.readRotationData();
|
||||
return {
|
||||
nextUpdateTime: rotationData.nextUpdateTime,
|
||||
selectedMap: rotationData.selectedMap,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Dynamic Goons] Error reading rotation data: ${error.message}`
|
||||
);
|
||||
return { nextUpdateTime: 0, selectedMap: "bigmap" };
|
||||
}
|
||||
}
|
||||
|
||||
public async handleRotationChance(
|
||||
rotationData: any,
|
||||
currentTime: number,
|
||||
rotationInterval: number
|
||||
): Promise<void> {
|
||||
const remainingTime = rotationData.nextUpdateTime - currentTime;
|
||||
const rotationChance = rotationChanceCalculator(
|
||||
remainingTime,
|
||||
rotationInterval,
|
||||
this.logger,
|
||||
this.modConfig.debugLogs
|
||||
);
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] Remaining time: ${remainingTime}ms, Rotation chance: ${rotationChance}%`
|
||||
);
|
||||
}
|
||||
|
||||
const randomRoll = Math.random() * 100;
|
||||
|
||||
if (rotationData.lastRotationInterval !== this.modConfig.rotationInterval) {
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] Rotation interval changed. Rotating now.`
|
||||
);
|
||||
}
|
||||
await this.selectRandomMapAndSave(rotationInterval);
|
||||
}
|
||||
|
||||
if (randomRoll <= rotationChance) {
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(`[Dynamic Goons] Rotation triggered. Rotating now.`);
|
||||
}
|
||||
await this.selectRandomMapAndSave(rotationInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public calculateRotationChance(remainingTime: number): number {
|
||||
const maxTime = this.modConfig.rotationInterval * 60 * 1000;
|
||||
const maxChance = 100;
|
||||
|
||||
if (remainingTime <= 0) return maxChance;
|
||||
|
||||
const factor = Math.min(Math.max(remainingTime / maxTime, 0), 1);
|
||||
const chance = maxChance * (1 - Math.pow(factor, 2));
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] Remaining time: ${remainingTime}ms, Rotation chance: ${chance}%`
|
||||
);
|
||||
}
|
||||
|
||||
return chance;
|
||||
}
|
||||
|
||||
private async selectRandomMapAndSave(
|
||||
rotationInterval: number
|
||||
): Promise<void> {
|
||||
const rotationData = await this.readRotationData();
|
||||
const chosenMap = await this.getRandomMap(rotationData.selectedMap);
|
||||
|
||||
const nextUpdateTime = Date.now() + rotationInterval * 60 * 1000;
|
||||
const lastUpdateTime = Date.now();
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
const updateTimeString = new Date(nextUpdateTime).toLocaleTimeString(
|
||||
"en-GB",
|
||||
{
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}
|
||||
);
|
||||
const remainingTimeFormatted = this.formatTime(
|
||||
nextUpdateTime - Date.now()
|
||||
);
|
||||
|
||||
this.logger.info(
|
||||
`[Dynamic Goons] Selected Map: ${chosenMap}, Update Scheduled in: ${updateTimeString}, Remaining Time: ${remainingTimeFormatted}, Last Rotation: ${new Date(
|
||||
lastUpdateTime
|
||||
).toLocaleString()}`
|
||||
);
|
||||
}
|
||||
|
||||
await this.saveNextUpdateTimeAndMapData(
|
||||
nextUpdateTime,
|
||||
chosenMap,
|
||||
rotationInterval,
|
||||
lastUpdateTime
|
||||
);
|
||||
}
|
||||
|
||||
private async saveNextUpdateTimeAndMapData(
|
||||
nextUpdateTime: number,
|
||||
selectedMap: string,
|
||||
lastRotationInterval: number,
|
||||
lastUpdateTime: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = {
|
||||
nextUpdateTime,
|
||||
selectedMap,
|
||||
lastRotationInterval,
|
||||
lastUpdateTime,
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
this.rotationData,
|
||||
JSON.stringify(data, null, 4),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
if (this.modConfig.debugLogs) {
|
||||
this.logger.info(`[Dynamic Goons] Rotation data saved successfully.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Dynamic Goons] Error saving rotation data: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async readRotationData(): Promise<any> {
|
||||
try {
|
||||
const data = await fs.promises.readFile(this.rotationData, "utf8");
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Dynamic Goons] Error reading rotation data: ${error.message}`
|
||||
);
|
||||
return {
|
||||
lastRotationInterval: 180,
|
||||
nextUpdateTime: 0,
|
||||
selectedMap: "bigmap",
|
||||
lastUpdateTime: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getEnabledMaps(): Promise<string[]> {
|
||||
try {
|
||||
const data = await fs.promises.readFile(this.mapConfig, "utf8");
|
||||
const json = JSON.parse(data);
|
||||
|
||||
if (!json || !json.enabledMaps || typeof json.enabledMaps !== "object") {
|
||||
throw new Error("Invalid JSON structure for enabled maps.");
|
||||
}
|
||||
|
||||
// Filter only the maps that are set to true
|
||||
return Object.entries(json.enabledMaps)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([mapName, _]) => mapName);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Dynamic Goons] Error reading enabled maps file: ${error.message}`
|
||||
);
|
||||
// Fallback to default maps if reading fails
|
||||
return ["bigmap", "shoreline", "lighthouse", "woods"];
|
||||
}
|
||||
}
|
||||
|
||||
private async getRandomMap(
|
||||
excludeMap: string | null = null
|
||||
): Promise<string> {
|
||||
const enabledMaps = await this.getEnabledMaps();
|
||||
|
||||
// Filter out the excluded map
|
||||
const availableMaps = excludeMap
|
||||
? enabledMaps.filter((map) => map !== excludeMap)
|
||||
: enabledMaps;
|
||||
|
||||
// Fallback to default maps if no available maps
|
||||
const defaultFallbackMaps = ["bigmap", "shoreline", "lighthouse", "woods"];
|
||||
const finalMaps =
|
||||
availableMaps.length > 0 ? availableMaps : defaultFallbackMaps;
|
||||
|
||||
// Randomly select a map
|
||||
const selectedMap = finalMaps[Math.floor(Math.random() * finalMaps.length)];
|
||||
return selectedMap;
|
||||
}
|
||||
|
||||
// This is just for making debugging logs easier to read for me :v
|
||||
private formatTime(ms: number): string {
|
||||
const hours = Math.floor(ms / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((ms % (1000 * 60)) / 1000);
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user