feat(moar)

This commit is contained in:
GetParanoid 2025-07-18 15:51:23 -05:00
parent e334736730
commit ff3ab9a974
32 changed files with 6960 additions and 0 deletions

BIN
BepInEx/plugins/MOAR.dll Normal file

Binary file not shown.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Dushaoan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,19 @@
{
"live-like": 45,
"quiet-raids": 8,
"more-scavs": 5,
"more-pmcs": 4,
"more-scavs-and-pmcs": 3,
"delayed-scavs": 2,
"delayed-pmcs": 4,
"arrived-early": 4,
"rogue-invasion": 1,
"raider-invasion": 1,
"insanity": 1,
"minor-boss-invasion": 4,
"less-pmcs-less-scavs-boss-invasion": 3,
"low-pmcs-low-scavs-major-boss-invasion": 1,
"main-boss-guaranteed": 5,
"main-boss-guaranteed-roaming": 3,
"sniper-buddies": 5
}

View file

@ -0,0 +1,107 @@
{
"live-like": {},
"quiet-raids": {
"spawnSmoothing": false,
"randomSpawns": true,
"scavGroupChance": 0.1,
"scavMaxGroupSize": 2,
"pmcGroupChance": 0.1,
"pmcMaxGroupSize": 2,
"scavWaveQuantity": 0.7,
"pmcWaveQuantity": 0.5,
"mainBossChanceBuff": 0
},
"more-scavs": {
"scavGroupChance": 0.5,
"scavMaxGroupSize": 5,
"scavWaveQuantity": 1.5,
"pmcWaveQuantity": 0.7
},
"more-pmcs": {
"pmcGroupChance": 0.5,
"pmcWaveQuantity": 1.3,
"scavWaveQuantity": 0.7
},
"more-scavs-and-pmcs": {
"scavGroupChance": 0.3,
"scavMaxGroupSize": 5,
"pmcGroupChance": 0.3,
"pmcMaxGroupSize": 5,
"scavWaveQuantity": 1.4,
"pmcWaveQuantity": 1.2
},
"delayed-scavs": {
"scavWaveDistribution": 1.5
},
"delayed-pmcs": {
"pmcWaveDistribution": 1.2
},
"arrived-early": {
"spawnSmoothing": false,
"scavWaveDistribution": 1.2,
"pmcWaveDistribution": 1.2
},
"rogue-invasion": {
"randomRaiderGroup": true,
"randomRaiderGroupChance": 100
},
"raider-invasion": {
"randomRaiderGroup": true,
"randomRaiderGroupChance": 100
},
"insanity": {
"scavWaveQuantity": 1.3,
"pmcWaveQuantity": 1.3,
"scavGroupChance": 0.7,
"pmcGroupChance": 0.8,
"pmcMaxGroupSize": 6,
"scavMaxGroupSize": 6,
"sniperGroupChance": 0.5,
"bossOpenZones": true,
"randomRaiderGroup": true,
"randomRaiderGroupChance": 50,
"randomRogueGroup": true,
"randomRogueGroupChance": 50,
"mainBossChanceBuff": 50,
"bossInvasion": true,
"bossInvasionSpawnChance": 10
},
"minor-boss-invasion": {
"bossOpenZones": true,
"bossInvasion": true,
"bossInvasionSpawnChance": 10,
"mainBossChanceBuff": 25,
"gradualBossInvasion": true
},
"less-pmcs-less-scavs-boss-invasion": {
"maxBotCap": 20,
"scavWaveQuantity": 0.7,
"pmcWaveQuantity": 0.7,
"bossOpenZones": true,
"bossInvasion": true,
"bossInvasionSpawnChance": 25,
"mainBossChanceBuff": 35,
"gradualBossInvasion": true
},
"low-pmcs-low-scavs-major-boss-invasion": {
"maxBotCap": 12,
"scavWaveQuantity": 0.3,
"pmcWaveQuantity": 0.1,
"bossOpenZones": true,
"bossInvasion": true,
"bossInvasionSpawnChance": 90,
"mainBossChanceBuff": 80,
"gradualBossInvasion": true
},
"main-boss-guaranteed": {
"mainBossChanceBuff": 100
},
"main-boss-guaranteed-roaming": {
"bossOpenZones": true,
"mainBossChanceBuff": 100
},
"sniper-buddies": {
"sniperMaxGroupSize": 4,
"sniperGroupChance": 1
}
}

View file

@ -0,0 +1,262 @@
{
"bigmap": [
{
"x": -237.231491,
"y": 1.75956392,
"z": -120.84491
},
{
"x": -333.538757,
"y": 1.2834585,
"z": -193.426743
},
{
"x": -330.967346,
"y": 0.7311232,
"z": -148.138031
},
{
"x": -320.449768,
"y": -0.1416501,
"z": -67.51158
},
{
"x": 494.7378,
"y": 2.0099400000000003,
"z": 214.363235
},
{
"x": 406.431763,
"y": 13.6794415,
"z": 205.624054
},
{
"x": 565.3794,
"y": 13.6622076,
"z": 176.73848
},
{
"x": 670.381958,
"y": 4.553419,
"z": 116.672173
},
{
"x": 648.4502,
"y": 0.884521127,
"z": 110.214828
},
{
"x": 648.0046,
"y": 1.60148263,
"z": -144.927017
},
{
"x": 607.7813,
"y": 1.9404974,
"z": -131.76033
},
{
"x": 353.7437,
"y": 1.67306828,
"z": -182.096039
}
],
"factory4_day": [],
"factory4_night": [],
"interchange": [
{
"x": 281.41922,
"y": 21.82544,
"z": 350.742
},
{
"x": 485.793152,
"y": 19.02228,
"z": -376.709045
},
{
"x": -244.620956,
"y": 21.9201584,
"z": -249.0072
}
],
"laboratory": [],
"lighthouse": [
{
"x": -6.39022541,
"y": 0.612077177,
"z": 582.3435
},
{
"x": 151.168228,
"y": 1.59125161,
"z": 332.439056
},
{
"x": 146.495163,
"y": 0.8794734479999999,
"z": -160.836212
}
],
"rezervbase": [
{
"x": 233.31601,
"y": -10.2675419,
"z": -4.18630457
},
{
"x": 139.089874,
"y": -10.305027,
"z": 23.3623848
},
{
"x": 56.89732,
"y": -6.44232559,
"z": 137.955032
}
],
"shoreline": [
{
"x": 257.83,
"y": -53.64759,
"z": -104.635
},
{
"x": 155.2863,
"y": -33.8709831,
"z": -282.204285
},
{
"x": 249.29483,
"y": -64.13083,
"z": 450.0022
},
{
"x": -529.4264,
"y": -18.0687866,
"z": -338.916351
}
],
"tarkovstreets": [],
"woods": [
{
"x": -255.085876,
"y": 18.7673836,
"z": -657.3163
},
{
"x": 216.3761,
"y": 11.7787333,
"z": -831.962036
},
{
"x": -629.5677,
"y": 13.3888264,
"z": -257.016571
},
{
"x": -602.1799,
"y": 24.6817856,
"z": -154.038223
},
{
"x": 253.6307,
"y": -10.6299782,
"z": 331.7935
}
],
"sandbox": [
{
"x": 12.19389,
"y": 23.9751911,
"z": 157.1094
},
{
"x": 109.844994,
"y": 23.96955,
"z": 304.835266
},
{
"x": 139.231842,
"y": 23.92805,
"z": 275.822052
},
{
"x": 120.96067,
"y": 28.6914253,
"z": 285.82135
},
{
"x": 147.699371,
"y": 23.2307129,
"z": 255.266312
},
{
"x": 5.82757664,
"y": 23.2639179,
"z": 330.428345
},
{
"x": -11.9459629,
"y": 24.33598,
"z": -57.9175034
},
{
"x": 207.106964,
"y": 16.67053,
"z": 37.7381
},
{
"x": 181.100342,
"y": 16.74511,
"z": 125.971664
}
],
"sandbox_high": [
{
"x": 12.19389,
"y": 23.9751911,
"z": 157.1094
},
{
"x": 109.844994,
"y": 23.96955,
"z": 304.835266
},
{
"x": 139.231842,
"y": 23.92805,
"z": 275.822052
},
{
"x": 120.96067,
"y": 28.6914253,
"z": 285.82135
},
{
"x": 147.699371,
"y": 23.2307129,
"z": 255.266312
},
{
"x": 5.82757664,
"y": 23.2639179,
"z": 330.428345
},
{
"x": -11.9459629,
"y": 24.33598,
"z": -57.9175034
},
{
"x": 207.106964,
"y": 16.67053,
"z": 37.7381
},
{
"x": 181.100342,
"y": 16.74511,
"z": 125.971664
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
{
"bigmap": [],
"factory4_day": [],
"factory4_night": [],
"interchange": [],
"laboratory": [],
"lighthouse": [],
"rezervbase": [],
"shoreline": [],
"tarkovstreets": [],
"woods": [],
"sandbox": [],
"sandbox_high": []
}

View file

@ -0,0 +1,8 @@
{
"-------------JUST LEAVE THESE ALONE---------------": "",
"ActivateSpawnCullingOnServerStart": false,
"MarksmanDifficultyChanges": true,
"EnableBossPerformanceImprovements": true,
"SpawnpointAreaTarget": 1,
"showMapCullingDebug": false
}

View file

@ -0,0 +1,63 @@
{
"ADD_THESE_TO_A_MAP_TO_OVERRIDE_OR_ADD_A_BOSS_TO_A_MAP": {
"BOSS_NAME_EXAMPLE": "CHANCE_OF_SPAWNING_PERCENT",
"sectantPriest": 0,
"arenaFighterEvent": 0,
"bossBoarSniper": 0,
"pmcBot": 0,
"bossZryachiy": 0,
"exUsec": 0,
"crazyAssaultEvent": 0,
"peacemaker": 0,
"bossKojaniy": 0,
"bossGluhar": 0,
"bossSanitar": 0,
"bossKilla": 0,
"bossTagilla": 0,
"bossKnight": 0,
"bossBoar": 0,
"bossKolontay": 0,
"bossPartisan": 0,
"bossBully": 0
},
"customs": {
"bossKnight": 18,
"bossPartisan": 8,
"bossBully": 30
},
"factoryDay": {
"bossTagilla": 30
},
"factoryNight": {
"bossTagilla": 30
},
"interchange": {
"bossKilla": 30
},
"laboratory": {},
"lighthouse": {
"bossKnight": 18,
"bossPartisan": 8
},
"rezervbase": {
"bossGluhar": 30
},
"shoreline": {
"bossKnight": 18,
"bossPartisan": 8,
"bossSanitar": 30
},
"tarkovstreets": {
"bossBoar": 30,
"bossKolontay": 30
},
"woods": {
"bossKojaniy": 30,
"bossKnight": 18,
"bossPartisan": 8
},
"gzLow": {},
"gzHigh": {
"bossKolontay": 30
}
}

View file

@ -0,0 +1,49 @@
{
"enableBotSpawning": true,
"spawnSmoothing": true,
"pmcDifficulty": 0.6,
"scavDifficulty": 0.4,
"scavWaveDistribution": 0.6,
"scavWaveQuantity": 1,
"startingPmcs": false,
"pmcWaveDistribution": 0.3,
"pmcWaveQuantity": 1,
"randomSpawns": false,
"zombiesEnabled": false,
"zombieWaveDistribution": 0.8,
"zombieWaveQuantity": 1,
"zombieHealth": 1,
"maxBotCap": 28,
"maxBotPerZone": 7,
"sniperGroupChance": 0.2,
"scavGroupChance": 0.2,
"pmcGroupChance": 0.2,
"pmcMaxGroupSize": 4,
"scavMaxGroupSize": 3,
"sniperMaxGroupSize": 1,
"bossOpenZones": false,
"randomRaiderGroup": false,
"randomRaiderGroupChance": 10,
"randomRogueGroup": false,
"randomRogueGroupChance": 10,
"disableBosses": false,
"mainBossChanceBuff": 0,
"bossInvasion": false,
"bossInvasionSpawnChance": 5,
"gradualBossInvasion": true,
"debug": false
}

View file

@ -0,0 +1,185 @@
{
"customs": {
"sniperQuantity": 3,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 12,
"mapCullingNearPointValuePmc": 12,
"mapCullingNearPointValueScav": 22,
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 24,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneDormitory"
],
"pmcHotZones": [
"ZoneDormitory"
]
},
"factoryDay": {
"sniperQuantity": 0,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.6,
"mapCullingNearPointValuePlayer": 2,
"mapCullingNearPointValuePmc": 2,
"mapCullingNearPointValueScav": 1,
"spawnMinDistance": 15,
"maxBotCapOverride": 12,
"maxBotPerZoneOverride": 12,
"pmcWaveCount": 8,
"scavWaveCount": 12,
"zombieWaveCount": 6
},
"factoryNight": {
"sniperQuantity": 0,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.6,
"mapCullingNearPointValuePlayer": 2,
"mapCullingNearPointValuePmc": 2,
"mapCullingNearPointValueScav": 1,
"spawnMinDistance": 15,
"maxBotCapOverride": 12,
"maxBotPerZoneOverride": 12,
"pmcWaveCount": 8,
"scavWaveCount": 12,
"zombieWaveCount": 6
},
"interchange": {
"sniperQuantity": 0,
"initialSpawnDelay": 20,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 12,
"mapCullingNearPointValuePmc": 4,
"mapCullingNearPointValueScav": 1,
"spawnMinDistance": 30,
"pmcWaveCount": 14,
"scavWaveCount": 36,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneCenterBot",
"ZoneCenter"
]
},
"laboratory": {
"sniperQuantity": 0,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 3,
"mapCullingNearPointValuePmc": 3,
"mapCullingNearPointValueScav": 2,
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 0,
"zombieWaveCount": 12
},
"lighthouse": {
"sniperQuantity": 1,
"initialSpawnDelay": 20,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 6,
"mapCullingNearPointValuePmc": 5,
"mapCullingNearPointValueScav": 8,
"maxBotCapOverride": 22,
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 24,
"zombieWaveCount": 10,
"scavHotZones": [
"Zone_LongRoad"
]
},
"rezervbase": {
"sniperQuantity": 0,
"initialSpawnDelay": 20,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 12,
"mapCullingNearPointValuePmc": 6,
"mapCullingNearPointValueScav": 1,
"spawnMinDistance": 30,
"pmcWaveCount": 11,
"scavWaveCount": 28,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneRailStrorage"
],
"pmcHotZones": [
"ZoneBarrack"
]
},
"shoreline": {
"sniperQuantity": 3,
"initialSpawnDelay": 20,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 12,
"mapCullingNearPointValuePmc": 6,
"mapCullingNearPointValueScav": 15,
"spawnMinDistance": 30,
"pmcWaveCount": 14,
"scavWaveCount": 36,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneSanatorium1"
],
"pmcHotZones": [
"ZoneSanatorium2"
]
},
"tarkovstreets": {
"sniperQuantity": 5,
"initialSpawnDelay": 20,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 12,
"mapCullingNearPointValuePmc": 6,
"mapCullingNearPointValueScav": 6,
"maxBotCapOverride": 18,
"spawnMinDistance": 30,
"pmcWaveCount": 10,
"scavWaveCount": 20,
"zombieWaveCount": 13
},
"woods": {
"sniperQuantity": 1,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.9,
"mapCullingNearPointValuePlayer": 12,
"mapCullingNearPointValuePmc": 30,
"mapCullingNearPointValueScav": 36,
"spawnMinDistance": 30,
"pmcWaveCount": 14,
"scavWaveCount": 36,
"zombieWaveCount": 10,
"scavHotZones": [
"ZoneWoodCutter"
],
"pmcHotZones": [
"ZoneWoodCutter"
]
},
"gzLow": {
"sniperQuantity": 1,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.7,
"mapCullingNearPointValuePlayer": 6,
"mapCullingNearPointValuePmc": 5,
"mapCullingNearPointValueScav": 1,
"maxBotCapOverride": 15,
"spawnMinDistance": 15,
"pmcWaveCount": 10,
"scavWaveCount": 18,
"zombieWaveCount": 9
},
"gzHigh": {
"sniperQuantity": 1,
"initialSpawnDelay": 15,
"smoothingDistribution": 0.7,
"mapCullingNearPointValuePlayer": 6,
"mapCullingNearPointValuePmc": 5,
"mapCullingNearPointValueScav": 1,
"maxBotCapOverride": 15,
"spawnMinDistance": 15,
"pmcWaveCount": 12,
"scavWaveCount": 18,
"zombieWaveCount": 9
}
}

View file

@ -0,0 +1,25 @@
{
"name": "MOAR",
"version": "3.1.6",
"main": "src/mod.js",
"license": "MIT",
"author": "DewardianDev",
"sptVersion": "^3.11.x",
"scripts": {
"setup": "npm i",
"build": "node ./packageBuild.ts"
},
"devDependencies": {
"@semantic-release/git": "^10.0.1",
"@types/node": "16.18.10",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"bestzip": "2.2.1",
"eslint": "8.30.0",
"fs-extra": "11.1.0",
"glob": "8.0.3",
"semantic-release": "^24.2.0",
"tsyringe": "4.7.0",
"typescript": "4.9.4"
}
}

View file

@ -0,0 +1,17 @@
import { Ixyz } from "@spt/models/eft/common/Ixyz";
import config from "../config/config.json";
import {
ILocationBase,
ISpawnPointParam,
} from "@spt/models/eft/common/ILocationBase";
export class globalValues {
public static baseConfig: typeof config = undefined;
public static overrideConfig: Partial<typeof config> = undefined;
public static locationsBase: ILocationBase[] = undefined;
public static currentPreset: string = "";
public static forcedPreset: string = "random";
public static addedMapZones: Record<number, string[]> = {};
public static indexedMapSpawns: Record<number, ISpawnPointParam[]> = {};
public static playerSpawn: ISpawnPointParam;
}

View file

@ -0,0 +1,213 @@
import { DependencyContainer } from "tsyringe";
import { buildWaves } from "../Spawning/Spawning";
import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { globalValues } from "../GlobalValues";
import { kebabToTitle } from "../utils";
import PresetWeightingsConfig from "../../config/PresetWeightings.json";
import { Ixyz } from "@spt/models/eft/common/Ixyz";
import {
deleteBotSpawn,
updateBotSpawn,
} from "../SpawnZoneChanges/updateUtils";
export const setupRoutes = (container: DependencyContainer) => {
const staticRouterModService = container.resolve<StaticRouterModService>(
"StaticRouterModService"
);
interface AddSpawnRequest {
map: string;
position: Ixyz;
type: "player" | "pmc" | "scav" | "sniper";
}
staticRouterModService.registerStaticRouter(
`moarAddBotSpawn`,
[
{
url: "/moar/addBotSpawn",
action: async (
url: string,
req: AddSpawnRequest,
sessionID,
output
) => {
updateBotSpawn(req.map, req.position, req.type);
return "success";
},
},
],
"moarAddBotSpawn"
);
staticRouterModService.registerStaticRouter(
`moarDeleteBotSpawn`,
[
{
url: "/moar/deleteBotSpawn",
action: async (
url: string,
req: AddSpawnRequest,
sessionID,
output
) => {
// console.log(req);
deleteBotSpawn(req.map, req.position, req.type);
return "success";
},
},
],
"moarDeleteBotSpawn"
);
// Make buildwaves run on game end
staticRouterModService.registerStaticRouter(
`moarUpdater`,
[
{
url: "/client/match/local/end",
action: async (_url, info, sessionId, output) => {
buildWaves(container);
return output;
},
},
],
"moarUpdater"
);
staticRouterModService.registerStaticRouter(
`moarGetCurrentPreset`,
[
{
url: "/moar/currentPreset",
action: async () => {
return globalValues.forcedPreset || "random";
},
},
],
"moarGetCurrentPreset"
);
staticRouterModService.registerStaticRouter(
`moarGetAnnouncePreset`,
[
{
url: "/moar/announcePreset",
action: async () => {
if (globalValues.forcedPreset?.toLowerCase() === "random") {
return globalValues.currentPreset;
}
return globalValues.forcedPreset || globalValues.currentPreset;
},
},
],
"moarGetAnnouncePreset"
);
staticRouterModService.registerStaticRouter(
`getDefaultConfig`,
[
{
url: "/moar/getDefaultConfig",
action: async () => {
return JSON.stringify(globalValues.baseConfig);
},
},
],
"getDefaultConfig"
);
staticRouterModService.registerStaticRouter(
`getServerConfigWithOverrides`,
[
{
url: "/moar/getServerConfigWithOverrides",
action: async () => {
return JSON.stringify({
...(globalValues.baseConfig || {}),
...(globalValues.overrideConfig || {}),
});
},
},
],
"getServerConfigWithOverrides"
);
staticRouterModService.registerStaticRouter(
`getServerConfigWithOverrides`,
[
{
url: "/moar/getServerConfigWithOverrides",
action: async () => {
return JSON.stringify({
...globalValues.baseConfig,
...globalValues.overrideConfig,
});
},
},
],
"getServerConfigWithOverrides"
);
staticRouterModService.registerStaticRouter(
`moarGetPresetsList`,
[
{
url: "/moar/getPresets",
action: async () => {
let result = [
...Object.keys(PresetWeightingsConfig).map((preset) => ({
Name: kebabToTitle(preset),
Label: preset,
})),
{ Name: "Random", Label: "random" },
{ Name: "Custom", Label: "custom" },
];
return JSON.stringify({ data: result });
},
},
],
"moarGetPresetsList"
);
staticRouterModService.registerStaticRouter(
"setOverrideConfig",
[
{
url: "/moar/setOverrideConfig",
action: async (
url: string,
overrideConfig: typeof globalValues.overrideConfig = {},
sessionID,
output
) => {
globalValues.overrideConfig = overrideConfig;
buildWaves(container);
return "Success";
},
},
],
"setOverrideConfig"
);
staticRouterModService.registerStaticRouter(
"moarSetPreset",
[
{
url: "/moar/setPreset",
action: async (url: string, { Preset }, sessionID, output) => {
globalValues.forcedPreset = Preset;
buildWaves(container);
return `Current Preset: ${kebabToTitle(
globalValues.forcedPreset || "Random"
)}`;
},
},
],
"moarSetPreset"
);
};

View file

@ -0,0 +1,5 @@
// context/index.js
export { default as PlayerSpawns } from "../../config/Spawns/playerSpawns.json";
export { default as ScavSpawns } from "../../config/Spawns/scavSpawns.json";
export { default as SniperSpawns } from "../../config/Spawns/sniperSpawns.json";
export { default as PmcSpawns } from "../../config/Spawns/pmcSpawns.json";

View file

@ -0,0 +1,243 @@
import { DatabaseServer } from "@spt/servers/DatabaseServer";
import { configLocations, originalMapList } from "../Spawning/constants";
import { DependencyContainer } from "tsyringe";
import mapConfig from "../../config/mapConfig.json";
import advancedConfig from "../../config/advancedConfig.json";
import { ISpawnPointParam } from "@spt/models/eft/common/ILocationBase";
import { globalValues } from "../GlobalValues";
import {
AddCustomBotSpawnPoints,
BuildCustomPlayerSpawnPoints,
AddCustomPmcSpawnPoints,
AddCustomSniperSpawnPoints,
cleanClosest,
getClosestZone,
removeClosestSpawnsFromCustomBots,
} from "../Spawning/spawnZoneUtils";
import { shuffle } from "../Spawning/utils";
import { PlayerSpawns, PmcSpawns, ScavSpawns, SniperSpawns } from ".";
import { updateAllBotSpawns } from "./updateUtils";
import { showMapCullingDebug } from "../../config/advancedConfig.json"
export const setupSpawns = (container: DependencyContainer) => {
const databaseServer = container.resolve<DatabaseServer>("DatabaseServer");
const { locations } = databaseServer.getTables();
const indexedMapSpawns: Record<number, ISpawnPointParam[]> = {};
const mapsToExcludeFromPlayerCulling = new Set([
"factory4_day",
"factory4_night",
"laboratory",
]);
originalMapList.forEach((map, mapIndex) => {
const configMap = configLocations[mapIndex];
const allZones = [
...new Set(
locations[map].base.SpawnPointParams.filter(
({ BotZoneName }: ISpawnPointParam) => !!BotZoneName
).map(({ BotZoneName }: ISpawnPointParam) => BotZoneName)
),
];
locations[map].base.OpenZones = allZones.join(",");
let bossSpawns: ISpawnPointParam[] = [];
let scavSpawns: ISpawnPointParam[] = [];
let sniperSpawns: ISpawnPointParam[] = [];
let pmcSpawns: ISpawnPointParam[] = [];
const bossZoneList = new Set([
"Zone_Blockpost",
"Zone_RoofRocks",
"Zone_RoofContainers",
"Zone_RoofBeach",
"Zone_TreatmentRocks",
"Zone_TreatmentBeach",
"Zone_Hellicopter",
"Zone_Island",
"BotZoneGate1",
"BotZoneGate2",
"BotZoneBasement",
]);
const isGZ = map.includes("sandbox");
shuffle<ISpawnPointParam[]>(locations[map].base.SpawnPointParams).forEach(
(point) => {
switch (true) {
case point.Categories.includes("Boss") ||
bossZoneList.has(point.BotZoneName):
bossSpawns.push(point);
break;
case point.BotZoneName?.toLowerCase().includes("snipe") ||
(map !== "lighthouse" && point.DelayToCanSpawnSec > 40):
sniperSpawns.push(point);
break;
case !!point.Infiltration || point.Categories.includes("Coop"):
pmcSpawns.push(point);
break;
default:
scavSpawns.push(point);
break;
}
}
);
// fix GZ
if (isGZ) {
sniperSpawns.map((point, index) => {
if (index < 2) {
point.BotZoneName = Math.random()
? "ZoneSandSnipeCenter"
: "ZoneSandSnipeCenter2";
} else {
point.BotZoneName = ["ZoneSandSnipeCenter", "ZoneSandSnipeCenter2"][
index
];
}
return point;
});
}
if (advancedConfig.ActivateSpawnCullingOnServerStart) {
ScavSpawns[map] =
removeClosestSpawnsFromCustomBots(
ScavSpawns,
scavSpawns,
map,
configLocations[mapIndex]
) || [];
PmcSpawns[map] =
removeClosestSpawnsFromCustomBots(
PmcSpawns,
pmcSpawns,
map,
configLocations[mapIndex]
) || [];
PlayerSpawns[map] =
removeClosestSpawnsFromCustomBots(
PlayerSpawns,
pmcSpawns,
map,
configLocations[mapIndex]
) || [];
SniperSpawns[map] =
removeClosestSpawnsFromCustomBots(
SniperSpawns,
sniperSpawns,
map,
configLocations[mapIndex]
) || [];
}
const { spawnMinDistance: limit } = mapConfig[configLocations[mapIndex]];
let playerSpawns = BuildCustomPlayerSpawnPoints(
map,
locations[map].base.SpawnPointParams
);
playerSpawns = cleanClosest(
playerSpawns,
mapIndex,
mapConfig[configMap].mapCullingNearPointValuePlayer
);
scavSpawns = cleanClosest(
AddCustomBotSpawnPoints(scavSpawns, map),
mapIndex,
mapConfig[configMap].mapCullingNearPointValueScav
).map((point, botIndex) => {
if (point.ColliderParams?._props?.Radius < limit) {
point.ColliderParams._props.Radius = limit;
}
return !!point.Categories.length
? {
...point,
BotZoneName: isGZ ? "ZoneSandbox" : point?.BotZoneName,
Categories: ["Bot"],
Sides: ["Savage"],
CorePointId: 1,
}
: point;
});
pmcSpawns = cleanClosest(
AddCustomPmcSpawnPoints(pmcSpawns, map),
mapIndex,
mapConfig[configMap].mapCullingNearPointValuePmc
).map((point, pmcIndex) => {
if (point.ColliderParams?._props?.Radius < limit) {
point.ColliderParams._props.Radius = limit;
}
return !!point.Categories.length
? {
...point,
BotZoneName: isGZ
? "ZoneSandbox"
: getClosestZone(
scavSpawns,
point.Position.x,
point.Position.y,
point.Position.z
),
Categories: ["Coop", Math.random() ? "Group" : "Opposite"],
Sides: ["Pmc"],
CorePointId: 0,
}
: point;
});
sniperSpawns = AddCustomSniperSpawnPoints(sniperSpawns, map);
indexedMapSpawns[mapIndex] = [
...sniperSpawns.map((point) => ({ ...point, type: "sniper" })),
...bossSpawns.map((point) => ({ ...point, type: "boss" })),
...scavSpawns.map((point) => ({ ...point, type: "scav" })),
...pmcSpawns.map((point) => ({ ...point, type: "pmc" })),
...playerSpawns.map((point) => ({ ...point, type: "player" })),
];
showMapCullingDebug &&
console.log(
"sniperSpawns",
sniperSpawns.length,
"bossSpawns",
bossSpawns.length,
"scavSpawns",
scavSpawns.length,
"pmcSpawns",
pmcSpawns.length,
"playerSpawns",
playerSpawns.length,
map
);
locations[map].base.SpawnPointParams = indexedMapSpawns[mapIndex];
const listToAddToOpenZones = [
...new Set(
locations[map].base.SpawnPointParams.map(
({ BotZoneName }) => BotZoneName
).filter((zone) => !!zone)
),
];
locations[map].base.OpenZones = listToAddToOpenZones.join(",");
});
// PlayerSpawns, PmcSpawns, ScavSpawns, SniperSpawns
if (advancedConfig.ActivateSpawnCullingOnServerStart) {
updateAllBotSpawns(PlayerSpawns, "playerSpawns");
updateAllBotSpawns(PmcSpawns, "pmcSpawns");
updateAllBotSpawns(ScavSpawns, "scavSpawns");
updateAllBotSpawns(SniperSpawns, "sniperSpawns");
}
globalValues.indexedMapSpawns = indexedMapSpawns;
};

View file

@ -0,0 +1,112 @@
import { Ixyz } from "@spt/models/eft/common/Ixyz";
import { getDistance } from "../Spawning/spawnZoneUtils";
const fs = require("fs");
const path = require("path");
const currentDirectory = process.cwd();
// Function to update JSON file
export const updateJsonFile = <T>(
filePath: string,
callback: (jsonData) => void,
successMessage: string
) => {
// Read the JSON file
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
console.error("Error reading the file:", err);
return;
}
// Parse the JSON data
let jsonData;
try {
jsonData = JSON.parse(data);
} catch (parseError) {
console.error("Error parsing JSON data:", parseError);
return;
}
callback(jsonData);
// Update the JSON object
// Write the updated JSON object back to the file
fs.writeFile(
filePath,
JSON.stringify(jsonData, null, 2),
"utf8",
(writeError) => {
if (writeError) {
console.error("Error writing the file:", writeError);
return;
}
console.log(successMessage);
}
);
});
};
export const updateBotSpawn = (
map: string,
value: Ixyz,
type: "player" | "pmc" | "scav" | "sniper"
) => {
map = map.toLowerCase();
updateJsonFile<Ixyz>(
`${currentDirectory}/user/mods/DewardianDev-MOAR/config/Spawns/${type}Spawns.json`,
(jsonData) => {
value.y = value.y + 0.5;
if (jsonData[map]) {
jsonData[map].push(value);
} else {
jsonData[map] = [value];
}
},
"Successfully added one bot spawn to " + map
);
};
export const deleteBotSpawn = (
map: string,
value: Ixyz,
type: "player" | "pmc" | "scav" | "sniper"
) => {
map = map.toLowerCase();
updateJsonFile<Ixyz>(
`${currentDirectory}/user/mods/DewardianDev-MOAR/config/Spawns/${type}Spawns.json`,
(jsonData) => {
if (jsonData[map]) {
const { x: X, y: Y, z: Z } = value;
let nearest = undefined;
let nearDist = Infinity;
jsonData[map].forEach(({ x, y, z }, index) => {
const dist = getDistance(x, y, z, X, Y, Z);
if (dist < nearDist) {
nearest = index;
nearDist = dist;
}
});
if (nearest) {
(jsonData[map] as Ixyz[]).splice(nearest, 1);
} else {
console.log("No nearest spawn on " + map);
}
}
},
"Successfully removed one bot spawn from "
);
};
export const updateAllBotSpawns = (
values: Record<string, Ixyz[]>,
targetType: string
) =>
updateJsonFile<Ixyz>(
`${currentDirectory}/user/mods/DewardianDev-MOAR/config/Spawns/${targetType}.json`,
(jsonData) => {
Object.keys(jsonData).forEach((map) => (jsonData[map] = values[map]));
},
"Successfully updated all Spawns"
);

View file

@ -0,0 +1,173 @@
import { IBotConfig } from "@spt/models/spt/config/IBotConfig.d";
import { IPmcConfig } from "@spt/models/spt/config/IPmcConfig.d";
import { DatabaseServer } from "@spt/servers/DatabaseServer";
import _config from "../../config/config.json";
import _mapConfig from "../../config/mapConfig.json";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { DependencyContainer } from "tsyringe";
import { globalValues } from "../GlobalValues";
import {
cloneDeep,
getRandomPresetOrCurrentlySelectedPreset,
saveToFile,
} from "../utils";
import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig.d";
import { originalMapList } from "./constants";
import { buildBossWaves } from "./buildBossWaves";
import buildZombieWaves from "./buildZombieWaves";
import buildScavMarksmanWaves from "./buildScavMarksmanWaves";
import buildPmcs from "./buildPmcs";
import { enforceSmoothing, setEscapeTimeOverrides } from "./utils";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import updateSpawnLocations from "./updateSpawnLocations";
import marksmanChanges from "./marksmanChanges";
import advancedConfig from "../../config/advancedConfig.json";
export const buildWaves = (container: DependencyContainer) => {
const configServer = container.resolve<ConfigServer>("ConfigServer");
const Logger = container.resolve<ILogger>("WinstonLogger");
const pmcConfig = configServer.getConfig<IPmcConfig>(ConfigTypes.PMC);
const botConfig = configServer.getConfig<IBotConfig>(ConfigTypes.BOT);
const locationConfig = configServer.getConfig<ILocationConfig>(
ConfigTypes.LOCATION
);
locationConfig.rogueLighthouseSpawnTimeSettings.waitTimeSeconds = 60;
locationConfig.enableBotTypeLimits = false;
locationConfig.fitLootIntoContainerAttempts = 1; // Move to ALP
locationConfig.addCustomBotWavesToMaps = false;
locationConfig.customWaves = { boss: {}, normal: {} };
const databaseServer = container.resolve<DatabaseServer>("DatabaseServer");
const { locations, bots } = databaseServer.getTables();
let config = cloneDeep(globalValues.baseConfig) as typeof _config;
const preset = getRandomPresetOrCurrentlySelectedPreset();
Object.keys(globalValues.overrideConfig).forEach((key) => {
if (config[key] !== globalValues.overrideConfig[key]) {
config.debug &&
console.log(
`[MOAR] overrideConfig ${key} changed from ${config[key]} to ${globalValues.overrideConfig[key]}`
);
config[key] = globalValues.overrideConfig[key];
}
});
// Set from preset if preset above is not empty
Object.keys(preset).forEach((key) => {
if (config[key] !== preset[key]) {
config.debug &&
console.log(
`[MOAR] preset ${globalValues.currentPreset}: ${key} changed from ${config[key]} to ${preset[key]}`
);
config[key] = preset[key];
}
});
// config.debug &&
console.log(
globalValues.forcedPreset === "custom"
? "custom"
: globalValues.forcedPreset
? globalValues.forcedPreset
: globalValues.currentPreset
);
const {
bigmap: customs,
factory4_day: factoryDay,
factory4_night: factoryNight,
interchange,
laboratory,
lighthouse,
rezervbase,
shoreline,
tarkovstreets,
woods,
sandbox: gzLow,
sandbox_high: gzHigh,
} = locations;
let locationList = [
customs,
factoryDay,
factoryNight,
interchange,
laboratory,
lighthouse,
rezervbase,
shoreline,
tarkovstreets,
woods,
gzLow,
gzHigh,
];
// This resets all locations to original state
if (!globalValues.locationsBase) {
globalValues.locationsBase = locationList.map(({ base }) =>
cloneDeep(base)
);
} else {
locationList = locationList.map((item, key) => ({
...item,
base: cloneDeep(globalValues.locationsBase[key]),
}));
}
pmcConfig.removeExistingPmcWaves = true;
Object.keys(pmcConfig.customPmcWaves).forEach(key => {
pmcConfig.customPmcWaves[key] = []
})
if (config.startingPmcs && (!config.randomSpawns || config.spawnSmoothing)) {
Logger.warning(
`[MOAR] Starting pmcs turned on, turning off cascade system and smoothing.\n`
);
config.spawnSmoothing = false;
config.randomSpawns = true;
}
if (advancedConfig.MarksmanDifficultyChanges) {
marksmanChanges(bots);
}
updateSpawnLocations(locationList, config);
setEscapeTimeOverrides(locationList, _mapConfig, Logger, config);
// BOSS RELATED STUFF!
buildBossWaves(config, locationList);
//Zombies
if (config.zombiesEnabled) {
buildZombieWaves(config, locationList, bots);
}
buildPmcs(config, locationList);
// Make main waves
buildScavMarksmanWaves(config, locationList, botConfig);
// enableSmoothing
if (config.spawnSmoothing) {
enforceSmoothing(locationList);
}
// saveToFile(locations.bigmap.base.SpawnPointParams, "spawns.json");
originalMapList.forEach((name, index) => {
if (!locations[name]) {
console.log("[MOAR] OH CRAP we have a problem!", name);
} else {
locations[name] = locationList[index];
}
});
};

View file

@ -0,0 +1,341 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import bossConfig from "../../config/bossConfig.json";
import advancedConfig from "../../config/advancedConfig.json";
import mapConfig from "../../config/mapConfig.json";
import {
bossesToRemoveFromPool,
bossPerformanceHash,
configLocations,
mainBossNameList,
originalMapList,
} from "./constants";
import { buildBossBasedWave, shuffle } from "./utils";
import { IBossLocationSpawn } from "@spt/models/eft/common/ILocationBase";
import { cloneDeep } from "../utils";
export function buildBossWaves(
config: typeof _config,
locationList: ILocation[]
) {
let {
randomRaiderGroup,
randomRaiderGroupChance,
randomRogueGroup,
randomRogueGroupChance,
mainBossChanceBuff,
bossInvasion,
bossInvasionSpawnChance,
disableBosses,
bossOpenZones,
gradualBossInvasion,
} = config;
const bossList = mainBossNameList.filter(
(bossName) => !["bossKnight"].includes(bossName)
);
const allBosses: Record<string, IBossLocationSpawn> = {};
for (const key in locationList) {
locationList[key].base.BossLocationSpawn.forEach((boss) => {
if (!allBosses[boss.BossName]) {
allBosses[boss.BossName] = boss;
}
});
}
// CreateBossList
const bosses: Record<string, IBossLocationSpawn> = {};
for (let indx = 0; indx < locationList.length; indx++) {
// Disable Bosses
if (disableBosses && !!locationList[indx].base?.BossLocationSpawn) {
locationList[indx].base.BossLocationSpawn = [];
} else {
//Remove all other spawns from pool now that we have the spawns zone list
locationList[indx].base.BossLocationSpawn = locationList[
indx
].base.BossLocationSpawn.filter(
(boss) => !bossesToRemoveFromPool.has(boss.BossName)
);
// Performance changes
if (advancedConfig.EnableBossPerformanceImprovements) {
locationList[indx].base.BossLocationSpawn.forEach((Boss, bIndex) => {
if (Boss.BossChance < 1) return;
if (!!bossPerformanceHash[Boss.BossName || ""]) {
const varsToUpdate: Record<string, any> =
bossPerformanceHash[Boss.BossName];
// make it so bossPartisan has a random spawn time
if (Boss.BossName === "bossPartisan") {
const max = locationList[
indx
].base.EscapeTimeLimit
varsToUpdate.Time = Math.floor(Math.random() * 50 * max)
// console.log(varsToUpdate, max * 60)
}
locationList[indx].base.BossLocationSpawn[bIndex] = {
...Boss,
...varsToUpdate,
};
}
});
}
const location = locationList[indx];
const defaultBossSettings =
mapConfig?.[configLocations[indx]]?.defaultBossSettings;
// Sets bosses spawn chance from settings
if (
location?.base?.BossLocationSpawn &&
defaultBossSettings &&
Object.keys(defaultBossSettings)?.length
) {
const filteredBossList = Object.keys(defaultBossSettings).filter(
(name) => defaultBossSettings[name]?.BossChance !== undefined
);
if (filteredBossList?.length) {
filteredBossList.forEach((bossName) => {
location.base.BossLocationSpawn =
location.base.BossLocationSpawn.map((boss) => ({
...boss,
...(boss.BossName === bossName
? { BossChance: defaultBossSettings[bossName].BossChance }
: {}),
}));
});
}
}
if (randomRaiderGroup) {
const raiderWave = buildBossBasedWave(
randomRaiderGroupChance,
"1,2,2,2,3",
"pmcBot",
"pmcBot",
"",
locationList[indx].base.EscapeTimeLimit
);
location.base.BossLocationSpawn.push(raiderWave);
}
if (randomRogueGroup) {
const rogueWave = buildBossBasedWave(
randomRogueGroupChance,
"1,2,2,2,3",
"exUsec",
"exUsec",
"",
locationList[indx].base.EscapeTimeLimit
);
location.base.BossLocationSpawn.push(rogueWave);
}
//Add each boss from each map to bosses object
const filteredBosses = location.base.BossLocationSpawn?.filter(
({ BossName }) => mainBossNameList.includes(BossName)
);
if (filteredBosses.length) {
for (let index = 0; index < filteredBosses.length; index++) {
const boss = filteredBosses[index];
if (
!bosses[boss.BossName] ||
(bosses[boss.BossName] &&
bosses[boss.BossName].BossChance < boss.BossChance)
) {
bosses[boss.BossName] = { ...boss };
}
}
}
}
}
if (!disableBosses) {
// Make boss Invasion
if (bossInvasion) {
if (bossInvasionSpawnChance) {
bossList.forEach((bossName) => {
if (bosses[bossName])
bosses[bossName].BossChance = bossInvasionSpawnChance;
});
}
for (let key = 0; key < locationList.length; key++) {
//Gather bosses to avoid duplicating.
const duplicateBosses = [
...locationList[key].base.BossLocationSpawn.filter(
({ BossName, BossZone }) => bossList.includes(BossName)
).map(({ BossName }) => BossName),
"bossKnight", // So knight doesn't invade
];
//Build bosses to add
const bossesToAdd = shuffle<IBossLocationSpawn[]>(Object.values(bosses))
.filter(({ BossName }) => !duplicateBosses.includes(BossName))
.map((boss, j) => ({
...boss,
BossZone: "",
BossEscortAmount:
boss.BossEscortAmount === "0" ? boss.BossEscortAmount : "1",
...(gradualBossInvasion ? { Time: j * 20 + 1 } : {}),
}));
// UpdateBosses
locationList[key].base.BossLocationSpawn = [
...locationList[key].base.BossLocationSpawn,
...bossesToAdd,
];
}
}
let hasChangedBossSpawns = false;
// console.log(Object.keys(allBosses));
configLocations.forEach((mapName, index) => {
const bossLocationSpawn = locationList[index].base.BossLocationSpawn;
const mapBossConfig: Record<string, number> = cloneDeep(
bossConfig[mapName] || {}
);
// if (Object.keys(mapBossConfig).length === 0) console.log(name, "empty");
const adjusted = new Set<string>([]);
bossLocationSpawn.forEach(({ BossName, BossChance }, bossIndex) => {
if (typeof mapBossConfig[BossName] === "number") {
if (BossChance !== mapBossConfig[BossName]) {
if (!hasChangedBossSpawns) {
console.log(
`\n[MOAR]: --- Adjusting default boss spawn rates from bossConfig.json --- `
);
hasChangedBossSpawns = true;
}
console.log(
`[MOAR]: ${mapName} ${BossName}: ${locationList[index].base.BossLocationSpawn[bossIndex].BossChance} => ${mapBossConfig[BossName]}`
);
locationList[index].base.BossLocationSpawn[bossIndex].BossChance =
mapBossConfig[BossName];
}
adjusted.add(BossName);
}
});
const bossesToAdd = Object.keys(mapBossConfig)
.filter(
(adjustName) => !adjusted.has(adjustName) && !!allBosses[adjustName]
)
.map((bossName) => {
`[MOAR]: Adding non-default boss ${bossName} to ${originalMapList[index]}`;
const newBoss: IBossLocationSpawn = cloneDeep(
allBosses[bossName] || {}
);
newBoss.BossChance = mapBossConfig[bossName];
// console.log(
// "Adding boss",
// bossName,
// "to ",
// originalMapList[index],
// "spawn chance =>",
// mapBossConfig[bossName]
// );
return newBoss;
});
// console.log(bossesToAdd);
if (bossOpenZones || mainBossChanceBuff) {
locationList[index].base?.BossLocationSpawn?.forEach((boss, key) => {
if (bossList.includes(boss.BossName)) {
if (bossOpenZones) {
locationList[index].base.BossLocationSpawn[key] = {
...locationList[index].base.BossLocationSpawn[key],
BossZone: "",
};
}
if (!!boss.BossChance && mainBossChanceBuff > 0) {
locationList[index].base.BossLocationSpawn[key] = {
...locationList[index].base.BossLocationSpawn[key],
BossChance:
boss.BossChance + mainBossChanceBuff > 100
? 100
: Math.round(boss.BossChance + mainBossChanceBuff),
};
}
}
});
}
locationList[index].base.BossLocationSpawn = [
...locationList[index].base.BossLocationSpawn,
...bossesToAdd,
];
bossesToAdd.length &&
console.log(
`[MOAR] Adding the following bosses to map ${configLocations[index]
}: ${bossesToAdd.map(({ BossName }) => BossName)}`
);
// console.log(locationList[index].base.BossLocationSpawn.length);
const bossesToSkip = new Set(["sectantPriest", "pmcBot"]);
// Apply the percentages on all bosses, cull those that won't spawn, make all bosses 100 chance that remain.
locationList[index].base.BossLocationSpawn = locationList[
index
].base.BossLocationSpawn.map(
({ BossChance, BossName, TriggerId }, bossIndex) => {
if (BossChance < 1) {
return locationList[index].base.BossLocationSpawn[bossIndex];
}
if (
!TriggerId &&
!bossesToSkip.has(BossName) &&
BossChance < 100
) {
if (
BossChance / 100 < Math.random()) {
locationList[index].base.BossLocationSpawn[
bossIndex
].BossChance = 0;
locationList[index].base.BossLocationSpawn[bossIndex].ForceSpawn =
false;
locationList[index].base.BossLocationSpawn[
bossIndex
].IgnoreMaxBots = false;
} else {
locationList[index].base.BossLocationSpawn[
bossIndex
].BossChance = 100;
}
}
return locationList[index].base.BossLocationSpawn[bossIndex];
}
).filter(({ BossChance, BossName, ...rest }) => {
if (BossChance < 1) {
return false;
}
return true
});
// if (mapName === "lighthouse") {
// console.log(
// locationList[index].base.BossLocationSpawn.map(
// ({ BossName, BossChance }) => ({ BossName, BossChance })
// )
// );
// }
});
if (hasChangedBossSpawns) {
console.log(
`[MOAR]: --- Adjusting default boss spawn rates complete --- \n`
);
}
}
}

View file

@ -0,0 +1,142 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import mapConfig from "../../config/mapConfig.json";
import { defaultEscapeTimes, defaultHostility } from "./constants";
import { buildBotWaves, looselyShuffle, MapSettings, shuffle } from "./utils";
import { saveToFile } from "../utils";
import getSortedSpawnPointList from "./spawnZoneUtils";
import { globalValues } from "../GlobalValues";
export default function buildPmcs(
config: typeof _config,
locationList: ILocation[]
) {
for (let index = 0; index < locationList.length; index++) {
const mapSettingsList = Object.keys(mapConfig) as Array<
keyof typeof mapConfig
>;
const map = mapSettingsList[index];
// Set pmcs hostile to everything
locationList[index].base.BotLocationModifier.AdditionalHostilitySettings =
defaultHostility;
const {
pmcHotZones = [],
pmcWaveCount,
initialSpawnDelay,
} = (mapConfig?.[map] as MapSettings) || {};
const {
Position: { x, y, z },
} = globalValues.playerSpawn;
let pmcZones = getSortedSpawnPointList(
locationList[index].base.SpawnPointParams.filter(
(point) => point["type"] === "pmc"
),
x,
y,
z,
0.05
).map(({ BotZoneName }) => BotZoneName);
looselyShuffle(pmcZones, 3);
// console.log(pmcZones);
if (map === "laboratory") {
pmcZones = new Array(10).fill(pmcZones).flat(1);
}
if (config.randomSpawns) pmcZones = shuffle<string[]>(pmcZones);
const escapeTimeLimitRatio = Math.round(
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
let totalWaves = Math.round(
pmcWaveCount * config.pmcWaveQuantity * escapeTimeLimitRatio
);
if (!!pmcHotZones.length && totalWaves > 0) {
totalWaves = totalWaves + pmcHotZones.length;
}
while (totalWaves - pmcZones.length > 0) {
console.log(
`${map} ran out of appropriate zones for pmcs, duplicating zones`
);
// const addEmpty = new Array(numberOfZoneless).fill("");
pmcZones = [...pmcZones, ...pmcZones];
if (pmcZones.length === 0) {
pmcZones = [""];
}
}
if (config.debug) {
console.log(`${map} PMC count ${totalWaves} \n`);
escapeTimeLimitRatio !== 1 &&
console.log(
`${map} PMC wave count changed from ${pmcWaveCount} to ${totalWaves} due to escapeTimeLimit adjustment`
);
}
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
const half = Math.round(
totalWaves % 2 === 0 ? totalWaves / 2 : (totalWaves + 1) / 2
);
const usecSpawns = pmcZones.filter((_, i) => i % 2 === 0);
const bearSpawns = pmcZones.filter((_, i) => i % 2 !== 0);
const pmcUSEC = buildBotWaves(
half,
config.startingPmcs ? Math.round(0.2 * timeLimit) : timeLimit,
config.pmcMaxGroupSize - 1,
config.pmcGroupChance,
usecSpawns,
config.pmcDifficulty,
"pmcUSEC",
false,
config.pmcWaveDistribution,
initialSpawnDelay + Math.round(10 * Math.random())
);
const pmcBEAR = buildBotWaves(
half,
config.startingPmcs ? Math.round(0.1 * timeLimit) : timeLimit,
config.pmcMaxGroupSize - 1,
config.pmcGroupChance,
bearSpawns,
config.pmcDifficulty,
"pmcBEAR",
false,
config.pmcWaveDistribution,
initialSpawnDelay + Math.round(10 * Math.random())
);
const pmcs = [...pmcUSEC, ...pmcBEAR];
// console.log(pmcs.map(({ Time }) => Time));
if (pmcs.length) {
// Add hotzones if exist
pmcHotZones.forEach((hotzone) => {
const index = Math.floor(pmcs.length * Math.random());
pmcs[index].BossZone = hotzone;
// console.log(pmcs[index]);
});
}
// console.log(
// map,
// pmcs.map(({ BossZone }) => BossZone)
// );
locationList[index].base.BossLocationSpawn = [
...pmcs,
...locationList[index].base.BossLocationSpawn,
];
}
}

View file

@ -0,0 +1,231 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import mapConfig from "../../config/mapConfig.json";
import { defaultEscapeTimes, originalMapList } from "./constants";
import { buildBotWaves, looselyShuffle, MapSettings, shuffle } from "./utils";
import { WildSpawnType } from "@spt/models/eft/common/ILocationBase";
import { IBotConfig } from "@spt/models/spt/config/IBotConfig";
import { saveToFile } from "../utils";
import getSortedSpawnPointList from "./spawnZoneUtils";
import { globalValues } from "../GlobalValues";
export default function buildScavMarksmanWaves(
config: typeof _config,
locationList: ILocation[],
botConfig: IBotConfig
) {
let {
maxBotCap,
scavWaveQuantity,
scavWaveDistribution,
sniperMaxGroupSize,
maxBotPerZone,
scavMaxGroupSize,
scavDifficulty,
sniperGroupChance,
scavGroupChance,
} = config;
for (let index = 0; index < locationList.length; index++) {
const mapSettingsList = Object.keys(mapConfig) as Array<
keyof typeof mapConfig
>;
const map = mapSettingsList[index];
locationList[index].base.waves = [];
locationList[index].base = {
...locationList[index].base,
...{
NewSpawn: false,
OcculsionCullingEnabled: true,
OfflineNewSpawn: false,
OfflineOldSpawn: true,
OldSpawn: true,
BotSpawnCountStep: 0,
},
};
locationList[index].base.NonWaveGroupScenario.Enabled = false;
locationList[index].base["BotStartPlayer"] = 0;
if (
locationList[index].base.BotStop <
locationList[index].base.EscapeTimeLimit * 60
) {
locationList[index].base.BotStop =
locationList[index].base.EscapeTimeLimit * 60;
}
const {
maxBotPerZoneOverride,
maxBotCapOverride,
EscapeTimeLimit,
scavHotZones = [],
sniperQuantity = 1,
scavWaveCount,
initialSpawnDelay,
} = (mapConfig?.[map] as MapSettings) || {};
// Set per map EscapeTimeLimit
if (EscapeTimeLimit) {
locationList[index].base.EscapeTimeLimit = EscapeTimeLimit;
locationList[index].base.exit_access_time = EscapeTimeLimit + 1;
}
// Set default or per map maxBotCap
if (maxBotCapOverride || maxBotCap) {
const capToSet = maxBotCapOverride || maxBotCap;
// console.log(map, capToSet, maxBotCapOverride, maxBotCap);
locationList[index].base.BotMax = capToSet;
locationList[index].base.BotMaxPvE = capToSet;
locationList[index].base.BotMaxPlayer = capToSet;
botConfig.maxBotCap[originalMapList[index]] = capToSet;
}
// Adjust botZone quantity
if (maxBotPerZoneOverride || maxBotPerZone) {
const BotPerZone = maxBotPerZoneOverride || maxBotPerZone;
// console.log(map, BotPerZone, maxBotPerZoneOverride, maxBotPerZone);
locationList[index].base.MaxBotPerZone = BotPerZone;
}
// const sniperLocations = new Set(
// [...locationList[index].base.SpawnPointParams]
// .filter(
// ({ Categories, DelayToCanSpawnSec, BotZoneName, Sides }) =>
// !Categories.includes("Boss") &&
// Sides[0] === "Savage" &&
// (BotZoneName?.toLowerCase().includes("snipe") ||
// DelayToCanSpawnSec > 40)
// )
// .map(({ BotZoneName }) => BotZoneName || "")
// );
const {
Position: { x, y, z },
} = globalValues.playerSpawn;
const sniperSpawns = getSortedSpawnPointList(
locationList[index].base.SpawnPointParams.filter(
(point) => point["type"] === "sniper"
),
x,
y,
z
);
let sniperLocations = sniperSpawns.map(({ BotZoneName }) => BotZoneName);
// console.log(sniperLocations);
const sniperDelay = 25;
// Make sure that the sniper spawns permit snipers to actually spawn early.
const sniperIds = new Set(sniperSpawns.map(({ Id }) => Id));
locationList[index].base.SpawnPointParams.forEach((point, snipeIndex) => {
if (sniperIds.has(point.Id)) {
locationList[index].base.SpawnPointParams[
snipeIndex
].DelayToCanSpawnSec = 20;
}
});
if (sniperLocations.length) {
locationList[index].base.MinMaxBots = [
{
WildSpawnType: "marksman",
max: sniperLocations.length * 5,
min: sniperLocations.length,
},
];
}
let scavZones = getSortedSpawnPointList(
locationList[index].base.SpawnPointParams.filter(
(point) => point["type"] === "scav"
),
x,
y,
z,
0.05
).map(({ BotZoneName }) => BotZoneName);
looselyShuffle(scavZones, 3);
const escapeTimeLimitRatio = Math.round(
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
// Scavs
let scavTotalWaveCount = Math.round(
scavWaveCount * scavWaveQuantity * escapeTimeLimitRatio
);
if (scavHotZones.length && scavTotalWaveCount > 0) {
scavTotalWaveCount = scavTotalWaveCount + scavHotZones.length;
}
while (scavTotalWaveCount - scavZones.length > 0) {
console.log(
`${map} ran out of appropriate zones for scavs, duplicating zones`
);
// const addEmpty = new Array(numberOfZoneless).fill("");
scavZones = [...scavZones, ...scavZones];
if (scavZones.length === 0) {
scavZones = [""];
}
}
config.debug &&
escapeTimeLimitRatio !== 1 &&
console.log(
`${map} Scav wave count changed from ${scavWaveCount} to ${scavTotalWaveCount} due to escapeTimeLimit adjustment`
);
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
// if (config.randomSpawns)
// sniperLocations = shuffle<string[]>(sniperLocations);
// console.log(map);
const snipers = buildBotWaves(
Math.min(sniperQuantity, sniperLocations.length),
timeLimit, ///30,
sniperMaxGroupSize,
sniperGroupChance,
sniperLocations,
0.8,
WildSpawnType.MARKSMAN,
true,
0.3,
sniperDelay
);
if (config.randomSpawns) scavZones = shuffle<string[]>(scavZones);
const scavWaves = buildBotWaves(
scavTotalWaveCount,
timeLimit,
scavMaxGroupSize,
scavGroupChance,
scavZones,
scavDifficulty,
WildSpawnType.ASSAULT,
false,
scavWaveDistribution,
initialSpawnDelay + Math.round(10 * Math.random())
);
// Add hotzones if exist
if (scavWaves.length) {
scavHotZones.forEach((hotzone) => {
const index = Math.floor(scavWaves.length * Math.random());
scavWaves[index].BossZone = hotzone;
// console.log(scavWaves[index].BossZone);
});
}
// if (map === "shoreline") console.log(scavWaves.map(({ Time }) => Time));
// console.log(snipers, scavWaves)
locationList[index].base.BossLocationSpawn = [
...snipers,
...scavWaves,
...locationList[index].base.BossLocationSpawn,
];
}
}

View file

@ -0,0 +1,80 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import mapConfig from "../../config/mapConfig.json";
import { configLocations, defaultEscapeTimes } from "./constants";
import {
buildZombie,
getHealthBodyPartsByPercentage,
zombieTypes,
} from "./utils";
import { IBots } from "@spt/models/spt/bots/IBots";
export default function buildZombieWaves(
config: typeof _config,
locationList: ILocation[],
bots: IBots
) {
let { debug, zombieWaveDistribution, zombieWaveQuantity, zombieHealth } =
config;
const zombieBodyParts = getHealthBodyPartsByPercentage(zombieHealth);
zombieTypes.forEach((type) => {
bots.types?.[type]?.health?.BodyParts?.forEach((_, index) => {
bots.types[type].health.BodyParts[index] = zombieBodyParts;
});
});
for (let indx = 0; indx < locationList.length; indx++) {
const location = locationList[indx].base;
const mapSettingsList = Object.keys(mapConfig) as Array<
keyof typeof mapConfig
>;
const map = mapSettingsList[indx];
const { zombieWaveCount } = mapConfig?.[configLocations[indx]];
// if (location.Events?.Halloween2024?.MaxCrowdAttackSpawnLimit)
// location.Events.Halloween2024.MaxCrowdAttackSpawnLimit = 100;
// if (location.Events?.Halloween2024?.CrowdCooldownPerPlayerSec)
// location.Events.Halloween2024.CrowdCooldownPerPlayerSec = 60;
// if (location.Events?.Halloween2024?.CrowdCooldownPerPlayerSec)
// location.Events.Halloween2024.CrowdsLimit = 10;
// if (location.Events?.Halloween2024?.CrowdAttackSpawnParams)
// location.Events.Halloween2024.CrowdAttackSpawnParams = [];
if (!zombieWaveCount) return;
const escapeTimeLimitRatio = Math.round(
locationList[indx].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
const zombieTotalWaveCount = Math.round(
zombieWaveCount * zombieWaveQuantity * escapeTimeLimitRatio
);
config.debug &&
escapeTimeLimitRatio !== 1 &&
console.log(
`${map} Zombie wave count changed from ${zombieWaveCount} to ${zombieTotalWaveCount} due to escapeTimeLimit adjustment`
);
const zombieWaves = buildZombie(
zombieTotalWaveCount,
location.EscapeTimeLimit * 60,
zombieWaveDistribution,
9999
);
debug &&
console.log(
configLocations[indx],
" generated ",
zombieWaves.length,
"Zombies"
);
location.BossLocationSpawn.push(...zombieWaves);
// console.log(zombieWaves[0], zombieWaves[7]);
}
}

View file

@ -0,0 +1,232 @@
export const defaultHostility = [
{
AlwaysEnemies: [
"bossTest",
"followerTest",
"bossKilla",
"bossKojaniy",
"followerKojaniy",
"cursedAssault",
"bossGluhar",
"followerGluharAssault",
"followerGluharSecurity",
"followerGluharScout",
"followerGluharSnipe",
"followerSanitar",
"bossSanitar",
"test",
"assaultGroup",
"sectantWarrior",
"sectantPriest",
"bossTagilla",
"followerTagilla",
"bossKnight",
"followerBigPipe",
"followerBirdEye",
"bossBoar",
"followerBoar",
"arenaFighter",
"arenaFighterEvent",
"bossBoarSniper",
"crazyAssaultEvent",
"sectactPriestEvent",
"followerBoarClose1",
"followerBoarClose2",
"bossKolontay",
"followerKolontayAssault",
"followerKolontaySecurity",
"bossPartisan",
"spiritWinter",
"spiritSpring",
"peacemaker",
"skier",
"assault",
"marksman",
"pmcUSEC",
"exUsec",
"pmcBot",
"bossBully",
],
AlwaysFriends: [
"bossZryachiy",
"followerZryachiy",
"peacefullZryachiyEvent",
"ravangeZryachiyEvent",
"gifter",
],
BearEnemyChance: 100,
BearPlayerBehaviour: "AlwaysEnemies",
BotRole: "pmcBEAR",
ChancedEnemies: [],
Neutral: ["shooterBTR"],
SavagePlayerBehaviour: "AlwaysEnemies",
UsecEnemyChance: 100,
UsecPlayerBehaviour: "AlwaysEnemies",
Warn: ["sectactPriestEvent"],
},
{
AlwaysEnemies: [
"bossTest",
"followerTest",
"bossKilla",
"bossKojaniy",
"followerKojaniy",
"cursedAssault",
"bossGluhar",
"followerGluharAssault",
"followerGluharSecurity",
"followerGluharScout",
"followerGluharSnipe",
"followerSanitar",
"bossSanitar",
"test",
"assaultGroup",
"sectantWarrior",
"sectantPriest",
"bossTagilla",
"followerTagilla",
"bossKnight",
"followerBigPipe",
"followerBirdEye",
"bossBoar",
"followerBoar",
"arenaFighter",
"arenaFighterEvent",
"bossBoarSniper",
"crazyAssaultEvent",
"sectactPriestEvent",
"followerBoarClose1",
"followerBoarClose2",
"bossKolontay",
"followerKolontayAssault",
"followerKolontaySecurity",
"bossPartisan",
"spiritWinter",
"spiritSpring",
"peacemaker",
"skier",
"assault",
"marksman",
"pmcBEAR",
"exUsec",
"pmcBot",
"bossBully",
],
AlwaysFriends: [
"bossZryachiy",
"followerZryachiy",
"peacefullZryachiyEvent",
"ravangeZryachiyEvent",
"gifter",
],
BearEnemyChance: 100,
BearPlayerBehaviour: "AlwaysEnemies",
BotRole: "pmcUSEC",
ChancedEnemies: [],
Neutral: ["shooterBTR"],
SavagePlayerBehaviour: "AlwaysEnemies",
UsecEnemyChance: 100,
UsecPlayerBehaviour: "AlwaysEnemies",
Warn: ["sectactPriestEvent"],
},
];
export const configLocations = [
"customs",
"factoryDay",
"factoryNight",
"interchange",
"laboratory",
"lighthouse",
"rezervbase",
"shoreline",
"tarkovstreets",
"woods",
"gzLow",
"gzHigh",
];
export const originalMapList = [
"bigmap",
"factory4_day",
"factory4_night",
"interchange",
"laboratory",
"lighthouse",
"rezervbase",
"shoreline",
"tarkovstreets",
"woods",
"sandbox",
"sandbox_high",
];
export const bossesToRemoveFromPool = new Set([
"assault",
"pmcBEAR",
"pmcUSEC",
"infectedAssault",
"infectedTagilla",
"infectedLaborant",
"infectedCivil",
]);
export const mainBossNameList = [
"bossKojaniy",
"bossGluhar",
"bossSanitar",
"bossKilla",
"bossTagilla",
"bossKnight",
"bossBoar",
"bossKolontay",
"bossPartisan",
"bossBully",
];
export const defaultEscapeTimes = {
customs: 40,
factoryDay: 20,
factoryNight: 25,
interchange: 40,
laboratory: 35,
lighthouse: 40,
rezervbase: 40,
shoreline: 45,
tarkovstreets: 50,
woods: 40,
gzLow: 35,
gzHigh: 35,
};
export const bossPerformanceHash = {
bossZryachiy: {
BossChance: 50,
BossEscortAmount: "0",
},
exUsec: {
BossEscortAmount: "1",
BossChance: 40,
},
bossBully: {
BossEscortAmount: "2,3",
},
bossBoar: {
BossEscortAmount: "1,2,2,2",
},
bossBoarSniper: {
BossEscortAmount: "1",
},
bossKojaniy: {
BossEscortAmount: "1,2,2",
},
bossPartisan: {
TriggerId: "",
TriggerName: "",
RandomTimeSpawn: false,
Time:120,
},
// bossSanitar: {
// BossEscortAmount: "1,2,3",
// },
};

View file

@ -0,0 +1,31 @@
import { IDifficultyCategories } from "@spt/models/eft/common/tables/IBotType";
import { IBots } from "@spt/models/spt/bots/IBots";
import { saveToFile } from "../utils";
export default function marksmanChanges(bots: IBots) {
// saveToFile(bots.types.marksman.difficulty, "marksmanDifficulty.json");
for (const diff in bots.types.marksman.difficulty) {
(bots.types.marksman.difficulty[diff] as IDifficultyCategories).Core = {
...bots.types.marksman.difficulty[diff].Core,
VisibleAngle: 300,
VisibleDistance: 245,
ScatteringPerMeter: 0.1,
HearingSense: 2.85,
};
(bots.types.marksman.difficulty[diff] as IDifficultyCategories).Mind = {
...bots.types.marksman.difficulty[diff].Mind,
BULLET_FEEL_DIST: 360,
CHANCE_FUCK_YOU_ON_CONTACT_100: 10,
};
(bots.types.marksman.difficulty[diff] as IDifficultyCategories).Hearing = {
...bots.types.marksman.difficulty[diff].Hearing,
CHANCE_TO_HEAR_SIMPLE_SOUND_0_1: 0.7,
DISPERSION_COEF: 3.6,
CLOSE_DIST: 10,
FAR_DIST: 30,
};
}
// saveToFile(bots.types.marksman.difficulty, "marksmanDifficulty2.json");
}

View file

@ -0,0 +1,410 @@
import _config from "../../config/config.json";
import { ISpawnPointParam } from "@spt/models/eft/common/ILocationBase";
import mapConfig from "../../config/mapConfig.json";
import { Ixyz } from "@spt/models/eft/common/Ixyz";
import { configLocations } from "./constants";
import {
ScavSpawns,
PlayerSpawns,
SniperSpawns,
PmcSpawns,
} from "../SpawnZoneChanges";
import { MapConfigType } from "./utils";
function sq(n: number) {
return n * n;
}
function pt(a: number, b: number) {
return Math.sqrt(sq(a) + sq(b));
}
export const getDistance = (
x: number,
y: number,
z: number,
mX: number,
mY: number,
mZ: number
) => {
(x = Math.abs(x - mX)), (y = Math.abs(y - mY)), (z = Math.abs(z - mZ));
return pt(pt(x, z), y);
};
export default function getSortedSpawnPointList(
SpawnPointParams: ISpawnPointParam[],
mX: number,
my: number,
mZ: number,
cull?: number
): ISpawnPointParam[] {
let culledAmount = 0;
const sorted = SpawnPointParams.sort((a, b) => {
const a1 = getDistance(
a.Position.x,
a.Position.y,
a.Position.z,
mX,
my,
mZ
);
const b1 = getDistance(
b.Position.x,
b.Position.y,
b.Position.z,
mX,
my,
mZ
);
return a1 - b1;
}).filter((_, index) => {
if (!cull) return true;
const result = index > SpawnPointParams.length * cull;
if (!result) culledAmount++;
return result;
});
if (_config.debug && culledAmount > 0) {
console.log(
"Reduced to " +
Math.round((sorted.length / SpawnPointParams.length) * 100) +
"% of original spawns",
SpawnPointParams.length,
">",
sorted.length,
"\n"
);
}
return sorted;
}
export function cleanClosest(
SpawnPointParams: ISpawnPointParam[],
mapIndex: number,
mapCullingNearPointValue: number
): ISpawnPointParam[] {
const map = configLocations[mapIndex];
const okayList = new Set();
const filteredParams = SpawnPointParams.map((point) => {
const {
Position: { x: X, y: Y, z: Z },
} = point;
const result = !SpawnPointParams.some(({ Position: { z, x, y }, Id }) => {
const dist = getDistance(X, Y, Z, x, y, z);
return mapCullingNearPointValue > dist && dist !== 0 && !okayList.has(Id);
});
if (!result) {
okayList.add(point.Id);
}
return result
? point
: {
...point,
DelayToCanSpawnSec: 9999999,
CorePointId: 99999,
Categories: [],
Sides: [],
};
});
if (_config.debug) {
const actualCulled = filteredParams.filter(
({ Categories }) => !!Categories.length
);
console.log(
map,
filteredParams.length,
">",
actualCulled.length,
"Reduced to " +
Math.round((actualCulled.length / filteredParams.length) * 100) +
"% of original spawns",
// player ? "player" : "bot"
); // high, low}
}
return filteredParams.filter((point) => !!point.Categories.length);
// if (!_config.debug) {
// const actualCulled = culled.filter(({ Categories }) => !!Categories.length);
// console.log(
// map,
// "Reduced to " +
// Math.round((actualCulled.length / culled.length) * 100) +
// "% of original spawns",
// culled.length,
// ">",
// actualCulled.length
// // "\n"
// ); // high, low}
// }
}
export function uuidv4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
(
+c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
).toString(16)
);
}
export const AddCustomPmcSpawnPoints = (
SpawnPointParams: ISpawnPointParam[],
map: string
) => {
if (!PmcSpawns[map] || !PmcSpawns[map].length) {
_config.debug && console.log("no custom Bot spawns for " + map);
return SpawnPointParams;
}
const playerSpawns = PmcSpawns[map].map((coords: Ixyz, index: number) => ({
BotZoneName: getClosestZone(SpawnPointParams, coords.x, coords.y, coords.z),
Categories: ["Coop", Math.random() ? "Group" : "Opposite"],
Sides: ["Pmc"],
CorePointId: 0,
ColliderParams: {
_parent: "SpawnSphereParams",
_props: {
Center: {
x: 0,
y: 0,
z: 0,
},
Radius: 20,
},
},
DelayToCanSpawnSec: 4,
Id: uuidv4(),
Infiltration: "",
Position: coords,
Rotation: random360(),
}));
return [...SpawnPointParams, ...playerSpawns];
};
export const AddCustomBotSpawnPoints = (
SpawnPointParams: ISpawnPointParam[],
map: string
) => {
if (!ScavSpawns[map] || !ScavSpawns[map].length) {
_config.debug && console.log("no custom Bot spawns for " + map);
return SpawnPointParams;
}
const scavSpawns = ScavSpawns[map].map((coords: Ixyz) => ({
BotZoneName: getClosestZone(SpawnPointParams, coords.x, coords.y, coords.z),
Categories: ["Bot"],
ColliderParams: {
_parent: "SpawnSphereParams",
_props: {
Center: {
x: 0,
y: 0,
z: 0,
},
Radius: 20,
},
},
CorePointId: 1,
DelayToCanSpawnSec: 4,
Id: uuidv4(),
Infiltration: "",
Position: coords,
Rotation: random360(),
Sides: ["Savage"],
}));
return [...SpawnPointParams, ...scavSpawns];
};
export const AddCustomSniperSpawnPoints = (
SpawnPointParams: ISpawnPointParam[],
map: string
) => {
if (!SniperSpawns[map] || !SniperSpawns[map].length) {
_config.debug && console.log("no custom Player spawns for " + map);
return SpawnPointParams;
}
const sniperSpawns = SniperSpawns[map].map((coords: Ixyz, index: number) => ({
BotZoneName:
getClosestZone(SpawnPointParams, coords.x, coords.y, coords.z) ||
"custom_snipe_" + index,
Categories: ["Bot"],
ColliderParams: {
_parent: "SpawnSphereParams",
_props: {
Center: {
x: 0,
y: 0,
z: 0,
},
Radius: 20,
},
},
CorePointId: 1,
DelayToCanSpawnSec: 4,
Id: uuidv4(),
Infiltration: "",
Position: coords,
Rotation: random360(),
Sides: ["Savage"],
}));
return [...SpawnPointParams, ...sniperSpawns];
};
export const random360 = () => Math.random() * 360;
export const BuildCustomPlayerSpawnPoints = (
map: string,
refSpawns: ISpawnPointParam[]
) => {
const playerOnlySpawns = refSpawns
.filter((item) => !!item.Infiltration && item.Categories[0] === "Player")
.map((point) => {
point.ColliderParams._props.Radius = 1;
point.Position.y = point.Position.y + 0.5;
return {
...point,
BotZoneName: "",
isCustom: true,
Id: uuidv4(),
Sides: ["Pmc"],
};
});
// console.log(map, playerOnlySpawns.length);
if (!PlayerSpawns[map] || !PlayerSpawns[map].length) {
_config.debug && console.log("no custom Player spawns for " + map);
return playerOnlySpawns;
}
const getClosestInfil = (X: number, Y: number, Z: number) => {
let closest = Infinity;
let selectedInfil = "";
playerOnlySpawns.forEach(({ Infiltration, Position: { x, y, z } }) => {
const dist = getDistance(X, Y, Z, x, y, z);
if (!!Infiltration && dist < closest) {
closest = dist;
selectedInfil = Infiltration;
}
});
return selectedInfil;
};
const playerSpawns = PlayerSpawns[map].map((coords: Ixyz, index) => ({
BotZoneName: "",
Categories: ["Player"],
ColliderParams: {
_parent: "SpawnSphereParams",
_props: {
Center: {
x: 0,
y: 0,
z: 0,
},
Radius: 1,
},
},
isCustom: true,
CorePointId: 0,
DelayToCanSpawnSec: 4,
Id: uuidv4(),
Infiltration: getClosestInfil(coords.x, coords.y, coords.z),
Position: coords,
Rotation: random360(),
Sides: ["Pmc"],
}));
// TODO: Check infils
// console.log(map);
// console.log(playerOnlySpawns[0], playerSpawns[0]);
return [...playerOnlySpawns, ...playerSpawns];
};
export const getClosestZone = (
params: ISpawnPointParam[],
x: number,
y: number,
z: number
) => {
if (
Array.isArray(params) &&
!params.filter(({ BotZoneName }) => BotZoneName).length
)
return "";
return (
getSortedSpawnPointList(params, x, y, z).find(
({ BotZoneName }) => !!BotZoneName
)?.BotZoneName || ""
);
};
export const removeClosestSpawnsFromCustomBots = (
CustomBots: Record<string, Ixyz[]>,
SpawnPointParams: ISpawnPointParam[],
map: string,
mapConfigMap: string
) => {
if (!CustomBots[map] || !CustomBots[map].length) {
console.log(map, "Is empty");
return;
}
const coords: Ixyz[] = CustomBots[map];
const { mapCullingNearPointValuePlayer,
mapCullingNearPointValuePmc,
mapCullingNearPointValueScav } = (mapConfig[mapConfigMap] as MapConfigType)
const mapCullingNearPointValue = (mapCullingNearPointValuePlayer +
mapCullingNearPointValuePmc +
mapCullingNearPointValueScav) / 3
let filteredCoords = coords.filter(
({ x: X, y: Y, z: Z }) =>
!SpawnPointParams.some(({ Position: { z, x, y } }) => {
return mapCullingNearPointValue > getDistance(X, Y, Z, x, y, z);
})
);
const okayList = new Set();
filteredCoords = [...coords].filter(({ x: X, y: Y, z: Z }, index) => {
const result = !coords.some(({ z, x, y }) => {
const dist = getDistance(X, Y, Z, x, y, z);
return (
mapCullingNearPointValue * 1.3 > dist &&
dist !== 0 &&
!okayList.has("" + x + y + z)
);
});
if (!result) okayList.add("" + X + Y + Z);
return result;
});
console.log(
map,
coords.length,
">",
filteredCoords.length,
"culled",
coords.length - filteredCoords.length,
"spawns"
);
return filteredCoords;
};

View file

@ -0,0 +1,71 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import { configLocations } from "./constants";
import _config from "../../config/config.json";
import { getRandomInArray, shuffle } from "./utils";
import advancedConfig from "../../config/advancedConfig.json";
import { ISpawnPointParam } from "@spt/models/eft/common/ILocationBase";
import { globalValues } from "../GlobalValues";
import getSortedSpawnPointList, {
getClosestZone,
getDistance,
uuidv4,
} from "./spawnZoneUtils";
export default function updateSpawnLocations(
locationList: ILocation[],
config: typeof _config
) {
for (let index = 0; index < locationList.length; index++) {
const map = configLocations[index];
const mapSpawns = [...globalValues.indexedMapSpawns[index]];
const playerSpawns = mapSpawns.filter(
(point) => point?.["type"] === "player"
);
const playerSpawn: ISpawnPointParam = getRandomInArray(playerSpawns);
globalValues.playerSpawn = playerSpawn;
const { x, y, z } = playerSpawn.Position;
const sortedSpawnPointList = getSortedSpawnPointList(mapSpawns, x, y, z);
const possibleSpawnList: ISpawnPointParam[] = [];
sortedSpawnPointList.forEach((point) => {
if (
possibleSpawnList.length < advancedConfig.SpawnpointAreaTarget &&
point?.["type"] === "player"
) {
point.ColliderParams._props.Radius = 1
possibleSpawnList.push(point);
}
});
// const possibleSpawnListSet = new Set(possibleSpawnList.map(({ Id }) => Id));
locationList[index].base.SpawnPointParams = [
...possibleSpawnList,
...sortedSpawnPointList.filter((point) => point["type"] !== "player"),
];
// {
// if (point["type"] === "player" && !possibleSpawnListSet.has(point.Id)) {
// point.Categories = [];
// point.Sides = [];
// }
// return point;
// }
// console.log(
// map,
// locationList[index].base.SpawnPointParams.filter(
// (point) => point?.["type"] === "player"
// ).length,
// locationList[index].base.SpawnPointParams.filter(
// (point) => point?.Categories[0] === "Player"
// ).length
// );
}
}

View file

@ -0,0 +1,538 @@
import {
IBossLocationSpawn,
IWave,
WildSpawnType,
} from "@spt/models/eft/common/ILocationBase";
import _config from "../../config/config.json";
import mapConfig from "../../config/mapConfig.json";
import { ILocation } from "@spt/models/eft/common/ILocation";
import { configLocations, defaultEscapeTimes } from "./constants";
import { ILogger } from "@spt/models/spt/utils/ILogger";
export const waveBuilder = (
totalWaves: number,
timeLimit: number,
waveDistribution: number,
wildSpawnType: "marksman" | "assault",
difficulty: number,
isPlayer: boolean,
maxSlots: number,
combinedZones: string[] = [],
specialZones: string[] = [],
offset?: number,
starting?: boolean,
moreGroups?: boolean
): IWave[] => {
if (totalWaves === 0) return [];
const averageTime = timeLimit / totalWaves;
const firstHalf = Math.round(averageTime * (1 - waveDistribution));
const secondHalf = Math.round(averageTime * (1 + waveDistribution));
let timeStart = offset || 0;
const waves: IWave[] = [];
let maxSlotsReached = Math.round(1.3 * totalWaves);
while (
totalWaves > 0 &&
(waves.length < totalWaves || specialZones.length > 0)
) {
const accelerate = totalWaves > 5 && waves.length < totalWaves / 3;
const stage = Math.round(
waves.length < Math.round(totalWaves * 0.5)
? accelerate
? firstHalf / 3
: firstHalf
: secondHalf
);
const min = !offset && waves.length < 1 ? 0 : timeStart;
const max = !offset && waves.length < 1 ? 0 : timeStart + 60;
if (waves.length >= 1 || offset) timeStart = timeStart + stage;
const BotPreset = getDifficulty(difficulty);
// console.log(wildSpawnType, BotPreset);
// Math.round((1 - waves.length / totalWaves) * maxSlots) || 1;
let slotMax = Math.round(
(moreGroups ? Math.random() : Math.random() * Math.random()) * maxSlots
);
if (slotMax < 1) slotMax = 1;
let slotMin = (Math.round(Math.random() * slotMax) || 1) - 1;
if (wildSpawnType === "marksman" && slotMin < 1) slotMin = 1;
waves.push({
BotPreset,
BotSide: getBotSide(wildSpawnType),
SpawnPoints: getZone(
specialZones,
combinedZones,
waves.length >= totalWaves
),
isPlayers: isPlayer,
slots_max: slotMax,
slots_min: slotMin,
time_min: min,
time_max: max,
WildSpawnType: wildSpawnType as WildSpawnType,
number: waves.length,
sptId: wildSpawnType + waves.length,
SpawnMode: ["regular", "pve"],
});
maxSlotsReached -= slotMax;
// if (wildSpawnType === "assault") console.log(slotMax, maxSlotsReached);
if (maxSlotsReached <= 0) break;
}
// console.log(waves.map(({ slots_min }) => slots_min));
return waves;
};
const getZone = (specialZones, combinedZones, specialOnly) => {
if (!specialOnly && combinedZones.length)
return combinedZones[
Math.round((combinedZones.length - 1) * Math.random())
];
if (specialZones.length) return specialZones.pop();
return "";
};
export const getDifficulty = (diff: number) => {
const randomNumb = Math.random() + diff;
switch (true) {
case randomNumb < 0.55:
return "easy";
case randomNumb < 1.4:
return "normal";
case randomNumb < 1.85:
return "hard";
default:
return "impossible";
}
};
export const shuffle = <n>(array: any): n => {
let currentIndex = array.length,
randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
};
const getBotSide = (
spawnType: "marksman" | "assault" | "pmcBEAR" | "pmcUSEC"
) => {
switch (spawnType) {
case "pmcBEAR":
return "Bear";
case "pmcUSEC":
return "Usec";
default:
return "Savage";
}
};
export const buildBossBasedWave = (
BossChance: number,
BossEscortAmount: string,
BossEscortType: string,
BossName: string,
BossZone: string,
raidTime?: number
): IBossLocationSpawn => {
return {
BossChance,
BossDifficult: "normal",
BossEscortAmount,
BossEscortDifficult: "normal",
BossEscortType,
BossName,
BossPlayer: false,
BossZone,
Delay: 0,
ForceSpawn: false,
IgnoreMaxBots: true,
RandomTimeSpawn: false,
Time: raidTime ? Math.round(Math.random() * (raidTime * 5)) : -1,
Supports: null,
TriggerId: "",
TriggerName: "",
SpawnMode: ["regular", "pve"],
};
};
export const zombieTypes = [
"infectedassault",
"infectedpmc",
"infectedlaborant",
"infectedcivil",
];
export const zombieTypesCaps = [
"infectedAssault",
"infectedPmc",
"infectedLaborant",
"infectedCivil",
];
export const getRandomDifficulty = (num: number = 1.5) =>
getDifficulty(Math.round(Math.random() * num * 10) / 10);
export const getRandomZombieType = () =>
zombieTypesCaps[Math.round((zombieTypesCaps.length - 1) * Math.random())];
export const buildBotWaves = (
botTotal: number,
escapeTimeLimit: number,
maxGroup: number,
groupChance: number,
bossZones: string[],
difficulty: number,
botType: string,
ForceSpawn: boolean,
botDistribution: number,
spawnDelay = 0
): IBossLocationSpawn[] => {
if (!botTotal) return [];
const pushToEnd = botDistribution > 1;
const pullFromEnd = botDistribution < 1;
const botToZoneTotal = bossZones.length / botTotal;
const isMarksman = botType === "marksman";
const isPMC = botType === "pmcUSEC" || botType === "pmcBEAR";
let startTime = pushToEnd
? Math.round((botDistribution - 1) * escapeTimeLimit)
: spawnDelay;
escapeTimeLimit = pullFromEnd
? Math.round(escapeTimeLimit * botDistribution)
: Math.round(escapeTimeLimit - startTime);
const averageTime = Math.round(escapeTimeLimit / botTotal);
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = botTotal;
if (maxGroup < 1) maxGroup = 1;
while (botTotal > 0) {
const allowGroup = groupChance > Math.random();
let bossEscortAmount = allowGroup
? Math.round(maxGroup * Math.random())
: 0;
if (
bossEscortAmount < 0 ||
(bossEscortAmount > 0 && bossEscortAmount + 1 > maxSlotsReached)
) {
bossEscortAmount = 0;
}
const totalCountThisWave = isMarksman ? 1 : bossEscortAmount + 1;
const totalCountThusFar = botTotal - maxSlotsReached;
const BossDifficult = getDifficulty(difficulty);
waves.push({
BossChance: 100,
BossDifficult,
BossEscortAmount: bossEscortAmount.toString(),
BossEscortDifficult: BossDifficult,
BossEscortType: botType,
BossName: botType,
BossPlayer: false,
BossZone: bossZones[Math.floor(totalCountThusFar * botToZoneTotal)] || "",
ForceSpawn,
IgnoreMaxBots: ForceSpawn,
RandomTimeSpawn: false,
Time: startTime,
Supports: null,
TriggerId: "",
TriggerName: "",
SpawnMode: isPMC ? ["pve"] : ["regular", "pve"],
});
startTime += Math.round(totalCountThisWave * averageTime);
maxSlotsReached -= 1 + (isMarksman ? 0 : bossEscortAmount);
if (maxSlotsReached <= 0) break;
}
// isMarksman &&
// console.log(
// // bossZones,
// botType,
// bossZones.length,
// waves.map(({ Time, BossZone }) => ({ Time, BossZone }))
// );
return waves;
};
export const buildZombie = (
botTotal: number,
escapeTimeLimit: number,
botDistribution: number,
BossChance: number = 100
): IBossLocationSpawn[] => {
if (!botTotal) return [];
const pushToEnd = botDistribution > 1;
const pullFromEnd = botDistribution < 1;
let startTime = pushToEnd
? Math.round((botDistribution - 1) * escapeTimeLimit)
: 0;
escapeTimeLimit = pullFromEnd
? Math.round(escapeTimeLimit * botDistribution)
: Math.round(escapeTimeLimit - startTime);
const averageTime = Math.round(escapeTimeLimit / botTotal);
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = botTotal;
while (botTotal > 0) {
const allowGroup = 0.2 > Math.random();
let bossEscortAmount = allowGroup ? Math.round(4 * Math.random()) : 0;
if (bossEscortAmount < 0) bossEscortAmount = 0;
const totalCountThisWave = bossEscortAmount + 1;
const main = getRandomZombieType();
waves.push({
BossChance,
BossDifficult: "normal",
BossEscortAmount: "0",
BossEscortDifficult: "normal",
BossEscortType: main,
BossName: main,
BossPlayer: false,
BossZone: "",
Delay: 0,
IgnoreMaxBots: false,
RandomTimeSpawn: false,
Time: startTime,
Supports: new Array(bossEscortAmount).fill("").map(() => ({
BossEscortType: getRandomZombieType(),
BossEscortDifficult: ["normal"],
BossEscortAmount: "1",
})),
TriggerId: "",
TriggerName: "",
SpawnMode: ["regular", "pve"],
});
startTime += Math.round(totalCountThisWave * averageTime);
maxSlotsReached -= 1 + bossEscortAmount;
if (maxSlotsReached <= 0) break;
}
// console.log(waves)
return waves;
};
export interface MapSettings {
sniperQuantity?: number;
initialSpawnDelay: number;
smoothingDistribution: number;
mapCullingNearPointValuePlayer: number;
mapCullingNearPointValuePmc: number;
mapCullingNearPointValueScav: number;
spawnMinDistance: number;
EscapeTimeLimit?: number;
maxBotPerZoneOverride?: number;
maxBotCapOverride?: number;
pmcHotZones?: string[];
scavHotZones?: string[];
pmcWaveCount: number;
scavWaveCount: number;
zombieWaveCount: number;
}
export const getHealthBodyPartsByPercentage = (num: number) => {
const num35 = Math.round(35 * num);
const num100 = Math.round(100 * num);
const num70 = Math.round(70 * num);
const num80 = Math.round(80 * num);
return {
Head: {
min: num35,
max: num35,
},
Chest: {
min: num100,
max: num100,
},
Stomach: {
min: num100,
max: num100,
},
LeftArm: {
min: num70,
max: num70,
},
RightArm: {
min: num70,
max: num70,
},
LeftLeg: {
min: num80,
max: num80,
},
RightLeg: {
min: num80,
max: num80,
},
};
};
export interface MapConfigType {
spawnMinDistance: number;
pmcWaveCount: number;
scavWaveCount: number;
zombieWaveCount?: number;
scavHotZones?: string[];
pmcHotZones?: string[];
EscapeTimeLimitOverride?: number;
mapCullingNearPointValuePlayer: number;
mapCullingNearPointValuePmc: number;
mapCullingNearPointValueScav: number;
}
export const setEscapeTimeOverrides = (
locationList: ILocation[],
mapConfig: Record<string, MapConfigType>,
logger: ILogger,
config: typeof _config
) => {
for (let index = 0; index < locationList.length; index++) {
const mapSettingsList = Object.keys(mapConfig) as Array<
keyof typeof mapConfig
>;
const map = mapSettingsList[index];
const override = mapConfig[map].EscapeTimeLimitOverride;
const hardcodedEscapeLimitMax = 5;
if (
!override &&
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map] >
hardcodedEscapeLimitMax
) {
const maxLimit = defaultEscapeTimes[map] * hardcodedEscapeLimitMax;
logger.warning(
`[MOAR] EscapeTimeLimit set too high on ${map}\nEscapeTimeLimit changed from ${locationList[index].base.EscapeTimeLimit} => ${maxLimit}\n`
);
locationList[index].base.EscapeTimeLimit = maxLimit;
}
if (override && locationList[index].base.EscapeTimeLimit !== override) {
console.log(
`[Moar] Set ${map}'s Escape time limit to ${override} from ${locationList[index].base.EscapeTimeLimit}\n`
);
locationList[index].base.EscapeTimeLimit = override;
locationList[index].base.EscapeTimeLimitCoop = override;
locationList[index].base.EscapeTimeLimitPVE = override;
}
if (
config.startingPmcs &&
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map] > 2
) {
logger.warning(
`[MOAR] Average EscapeTimeLimit is too high (greater than 2x) to enable starting PMCS\nStarting PMCS has been turned off to prevent performance issues.\n`
);
config.startingPmcs = false;
}
}
};
export const getRandomInArray = <T>(arr: T[]): T =>
arr[Math.floor(Math.random() * arr.length)];
export const enforceSmoothing = (locationList: ILocation[]) => {
for (let index = 0; index < locationList.length; index++) {
const waves = locationList[index].base.BossLocationSpawn;
const Bosses: IBossLocationSpawn[] = [];
let notBosses: IBossLocationSpawn[] = [];
const notBossesSet = new Set([
"infectedLaborant",
"infectedAssault",
"infectedCivil",
WildSpawnType.ASSAULT,
WildSpawnType.MARKSMAN,
"pmcBEAR",
"pmcUSEC",
]);
for (const wave of waves) {
if (notBossesSet.has(wave.BossName)) {
notBosses.push(wave);
} else {
Bosses.push(wave);
}
}
let first = Infinity,
last = -Infinity;
notBosses.forEach((notBoss) => {
first = Math.min(notBoss.Time, first);
last = Math.max(notBoss.Time, last);
});
if (first < 15) first = 15;
notBosses = notBosses.sort((a, b) => a.Time - b.Time);
// console.log(notBosses.map(({ Time }) => Time))
let start = first;
const smoothingDistribution = (mapConfig[configLocations[index]] as any)
.smoothingDistribution as number;
const increment =
(Math.round((last - first) / notBosses.length) * 2) * smoothingDistribution;
for (let index = 0; index < notBosses.length; index++) {
const ratio = (index + 1) / notBosses.length;
// console.log(ratio);
notBosses[index].Time = start;
let inc = Math.round(increment * ratio);
if (inc < 10) inc = 5;
start += inc;
}
// console.log(
// configLocations[index],
// last,
// notBosses.map(({ Time, BossName }) => ({ BossName, Time }))
// );
locationList[index].base.BossLocationSpawn = [...Bosses, ...notBosses];
}
};
export const looselyShuffle = <T>(arr: T[], shuffleStep: number = 3): T[] => {
const n = arr.length;
const halfN = Math.floor(n / 2);
for (let i = shuffleStep - 1; i < halfN; i += shuffleStep) {
// Pick a random index from the second half of the array to swap with the current index
const randomIndex = halfN + Math.floor(Math.random() * (n - halfN));
// Swap the elements at the current index and the random index
const temp = arr[i];
arr[i] = arr[randomIndex];
arr[randomIndex] = temp;
}
return arr;
};

View file

@ -0,0 +1,28 @@
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { DependencyContainer } from "tsyringe";
import config from "../../config/config.json";
import presets from "../../config/Presets.json";
import presetWeightings from "../../config/PresetWeightings.json";
export default function checkPresetLogic(container: DependencyContainer) {
const Logger = container.resolve<ILogger>("WinstonLogger");
for (const key in presetWeightings) {
if (presets[key] === undefined) {
Logger.error(
`\n[MOAR]: No preset found in PresetWeightings.json for preset "${key}" in Presets.json`
);
}
}
for (const key in presets) {
const preset = presets[key];
for (const id in preset) {
if (config[id] === undefined) {
Logger.error(
`\n[MOAR]: No associated key found in config.json called "${id}" for preset "${key}" in Presets.json`
);
}
}
}
}

View file

@ -0,0 +1,160 @@
import { DependencyContainer } from "tsyringe";
import {
ISeasonalEventConfig,
ISeasonalEvent,
} from "@spt/models/spt/config/ISeasonalEventConfig.d";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { zombieTypesCaps } from "../Spawning/utils";
export const baseZombieSettings = (enabled: boolean, count: number) =>
({
enabled,
name: "zombies",
type: "Zombies",
startDay: "1",
startMonth: "1",
endDay: "31",
endMonth: "12",
settings: {
enableSummoning: false,
removeEntryRequirement: [],
replaceBotHostility: true,
zombieSettings: {
enabled: true,
mapInfectionAmount: {
Interchange: count === -1 ? randomNumber100() : count,
Lighthouse: count === -1 ? randomNumber100() : count,
RezervBase: count === -1 ? randomNumber100() : count,
Sandbox: count === -1 ? randomNumber100() : count,
Shoreline: count === -1 ? randomNumber100() : count,
TarkovStreets: count === -1 ? randomNumber100() : count,
Woods: count === -1 ? randomNumber100() : count,
bigmap: count === -1 ? randomNumber100() : count,
factory4: count === -1 ? randomNumber100() : count,
laboratory: count === -1 ? randomNumber100() : count,
},
disableBosses: [],
disableWaves: [],
},
},
} as unknown as ISeasonalEvent);
const randomNumber100 = () => Math.round(Math.random() * 100);
export const resetCurrentEvents = (
container: DependencyContainer,
enabled: boolean,
zombieWaveQuantity: number,
random: boolean = false
) => {
const configServer = container.resolve<ConfigServer>("ConfigServer");
const eventConfig = configServer.getConfig<ISeasonalEventConfig>(
ConfigTypes.SEASONAL_EVENT
);
let percentToShow = random ? -1 : Math.round(zombieWaveQuantity * 100);
if (percentToShow > 100) percentToShow = 100;
eventConfig.events = [baseZombieSettings(enabled, percentToShow)];
const seasonalEventService = container.resolve<SeasonalEventService>(
"SeasonalEventService"
) as any;
// First we need to clear any existing data
seasonalEventService.currentlyActiveEvents = [];
seasonalEventService.christmasEventActive = false;
seasonalEventService.halloweenEventActive = false;
// Then re-calculate the cached data
seasonalEventService.cacheActiveEvents();
// seasonalEventService.addEventBossesToMaps("halloweenzombies");
};
export const setUpZombies = (container: DependencyContainer) => {
const configServer = container.resolve<ConfigServer>("ConfigServer");
const eventConfig = configServer.getConfig<ISeasonalEventConfig>(
ConfigTypes.SEASONAL_EVENT
);
eventConfig.events = [baseZombieSettings(false, 100)];
// eventConfig.eventBossSpawns = {
// zombies: eventConfig.eventBossSpawns.halloweenzombies,
// };
eventConfig.eventGear[eventConfig.events[0].name] = {};
eventConfig.hostilitySettingsForEvent.zombies.default =
eventConfig.hostilitySettingsForEvent.zombies.default
.filter(({ BotRole }) => !["pmcBEAR", "pmcUSEC"].includes(BotRole))
.map((host) => ({
...host,
AlwaysEnemies: [
"infectedAssault",
"infectedPmc",
"infectedCivil",
"infectedLaborant",
"infectedTagilla",
"pmcBEAR",
"pmcUSEC",
],
AlwaysNeutral: [
"marksman",
"assault",
"bossTest",
"bossBully",
"followerTest",
"bossKilla",
"bossKojaniy",
"followerKojaniy",
"pmcBot",
"cursedAssault",
"bossGluhar",
"followerGluharAssault",
"followerGluharSecurity",
"followerGluharScout",
"followerGluharSnipe",
"followerSanitar",
"bossSanitar",
"test",
"assaultGroup",
"sectantWarrior",
"sectantPriest",
"bossTagilla",
"followerTagilla",
"exUsec",
"gifter",
"bossKnight",
"followerBigPipe",
"followerBirdEye",
"bossZryachiy",
"followerZryachiy",
"bossBoar",
"followerBoar",
"arenaFighter",
"arenaFighterEvent",
"bossBoarSniper",
"crazyAssaultEvent",
"peacefullZryachiyEvent",
"sectactPriestEvent",
"ravangeZryachiyEvent",
"followerBoarClose1",
"followerBoarClose2",
"bossKolontay",
"followerKolontayAssault",
"followerKolontaySecurity",
"shooterBTR",
"bossPartisan",
"spiritWinter",
"spiritSpring",
"peacemaker",
"skier",
],
SavagePlayerBehaviour: "Neutral",
BearPlayerBehaviour: "AlwaysEnemies",
UsecPlayerBehaviour: "AlwaysEnemies",
}));
// console.log(eventConfig.hostilitySettingsForEvent.zombies.default);
};

View file

@ -0,0 +1,41 @@
import { DependencyContainer } from "tsyringe";
import { IPostSptLoadMod } from "@spt/models/external/IPostSptLoadMod";
import { IPostDBLoadMod } from "@spt/models/external/IPostDBLoadMod";
import { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
import { enableBotSpawning } from "../config/config.json";
import { buildWaves } from "./Spawning/Spawning";
import config from "../config/config.json";
import { globalValues } from "./GlobalValues";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { setupRoutes } from "./Routes/routes";
import checkPresetLogic from "./Tests/checkPresets";
import { setupSpawns } from "./SpawnZoneChanges/setupSpawn";
class Moar implements IPostSptLoadMod, IPreSptLoadMod, IPostDBLoadMod {
preSptLoad(container: DependencyContainer): void {
if (enableBotSpawning) {
setupRoutes(container);
}
}
postDBLoad(container: DependencyContainer): void {
if (enableBotSpawning) {
setupSpawns(container);
}
}
postSptLoad(container: DependencyContainer): void {
if (enableBotSpawning) {
checkPresetLogic(container);
globalValues.baseConfig = config;
globalValues.overrideConfig = {};
const logger = container.resolve<ILogger>("WinstonLogger");
logger.info(
"\n[MOAR]: Starting up, may the bots ever be in your favour!"
);
buildWaves(container);
}
}
}
module.exports = { mod: new Moar() };

View file

@ -0,0 +1,56 @@
import PresetWeightings from "../config/PresetWeightings.json";
import Presets from "../config/Presets.json";
import { globalValues } from "./GlobalValues";
export const saveToFile = (data, filePath) => {
var fs = require("fs");
let dir = __dirname;
let dirArray = dir.split("\\");
const directory = `${dirArray[dirArray.length - 4]}/${dirArray[dirArray.length - 3]
}/${dirArray[dirArray.length - 2]}/`;
fs.writeFile(
directory + filePath,
JSON.stringify(data, null, 4),
function (err) {
if (err) throw err;
}
);
};
export const cloneDeep = (objectToClone: any) =>
JSON.parse(JSON.stringify(objectToClone));
export const getRandomPresetOrCurrentlySelectedPreset = () => {
switch (true) {
case globalValues.forcedPreset.toLowerCase() === "custom":
return {};
case !globalValues.forcedPreset:
globalValues.forcedPreset = "random";
break;
case globalValues.forcedPreset === "random":
break;
default:
return Presets[globalValues.forcedPreset];
}
const all = [];
const itemKeys = Object.keys(PresetWeightings);
for (const key of itemKeys) {
for (let i = 0; i < PresetWeightings[key]; i++) {
all.push(key);
}
}
const preset: string = all[Math.round(Math.random() * (all.length - 1))];
globalValues.currentPreset = preset;
return Presets[preset];
};
export const kebabToTitle = (str: string): string =>
str
.split("-")
.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
.join(" ");