Updated MOAR & Tweaked Config

This commit is contained in:
Rage 2025-01-13 18:17:36 -05:00
parent c99136d826
commit 6eecae59f6
41 changed files with 2649 additions and 199 deletions

View File

@ -1,8 +1,23 @@
## Settings file was created by plugin MOAR v2.5.6
## Settings file was created by plugin MOAR v2.6.7
## Plugin GUID: MOAR.settings
[1. Main Settings]
## All Bots/Players (excluding bosses) can spawn anywhere (overrides PMC/Player openzone options)
# Setting type: Boolean
# Default value: false
PMC/Scav/Player OpenZones On/Off = false
## Adds a large number of zones (including all scav zones) to pmc bots spawn pool
# Setting type: Boolean
# Default value: true
PMC OpenZones On/Off = true
## Adds a large number of zones to the Player's (you) spawn pool
# Setting type: Boolean
# Default value: false
Player OpenZones On/Off = false
## Causes all PMCs to spawn in the first few minutes of the game (performance intensive)
# Setting type: Boolean
# Default value: false
@ -16,9 +31,9 @@ Pmc difficulty = 0.6
## Works with SAIN or SPT to decide the bot's 'difficulty' preset (EASY: 0, easy-MEDIUM: 0.4, easy-MEDIUM-hard: 0.6, medium-hard: 0.85, HARD-impossible: 1, etc..)
# Setting type: Double
# Default value: 0.3
# Default value: 0.4
# Acceptable value range: From 0 to 1.5
Scav difficulty = 0.3
Scav difficulty = 0.4
## Preset to be used, random pulls a random weighted preset from the PresetWeights.json every time a raid ends
# Setting type: String
@ -36,12 +51,6 @@ Preset Announce On/Off = true
# Default value:
FIKA DETECTED: ALWAYS PRESS THIS FIRST BEFORE MAKING CHANGES!! =
PMC/Scav/Player OpenZones On/Off = false
PMC OpenZones On/Off = true
Player OpenZones On/Off = true
[2. Custom game Settings]
## Pushes settings to server
@ -63,7 +72,7 @@ gradualBossInvasion On/Off = true
# Setting type: Int32
# Default value: 5
# Acceptable value range: From 0 to 100
bossInvasionSpawnChance = 5
bossInvasionSpawnChance = 0
## Allows the main bosses (not knight,rogues,raiders) to invade other maps with a reduced retinue, by default they will spawn in native boss locations
# Setting type: Boolean
@ -123,23 +132,23 @@ scavMaxGroupSize = 4
# Setting type: Int32
# Default value: 4
# Acceptable value range: From 0 to 10
pmcMaxGroupSize = 4
pmcMaxGroupSize = 5
## Increases chances of pmc groups spawning, doesn't dramatically increase quantity.
# Setting type: Boolean
# Default value: false
morePmcGroups On/Off = false
morePmcGroups On/Off = true
## Increases chances of scav groups spawning, doesn't dramatically increase quantity.
# Setting type: Boolean
# Default value: false
moreScavGroups On/Off = false
moreScavGroups On/Off = true
## Max bots permitted in any particular spawn zone, recommend not to touch this.
# Setting type: Int32
# Default value: 7
# Default value: 5
# Acceptable value range: From 0 to 15
MaxBotPerZone = 7
MaxBotPerZone = 5
## Max bots alive at one time
# Setting type: Int32
@ -157,34 +166,34 @@ ZombieHealth = 1
# Setting type: Double
# Default value: 1
# Acceptable value range: From 0 to 10
ZombieWaveQuantity = 1
ZombieWaveQuantity = 1.5
## Determines the weighting of spawns at the beginning (1) spread evenly throughout (0.5) or at the end(0) of the raid
# Setting type: Double
# Default value: 0.5
# Acceptable value range: From 0 to 1
ZombieWaveDistribution = 0.5
ZombieWaveDistribution = 0.2
## Enables zombies to spawn
# Setting type: Boolean
# Default value: false
zombiesEnabled On/Off = false
zombiesEnabled On/Off = true
## Determines the weighting of spawns at the beginning (1) spread evenly throughout (0.5) or at the end(0) of the raid
# Setting type: Double
# Default value: 0.3
# Default value: 0.5
# Acceptable value range: From 0 to 1
ScavWaveDistribution = 0.3
ScavWaveDistribution = 0.5
## Determines the weighting of spawns at the beginning (1) spread evenly throughout (0.5) or at the end(0) of the raid
# Setting type: Double
# Default value: 0.8
# Default value: 0.7
# Acceptable value range: From 0 to 1
PmcWaveDistribution = 0.8
PmcWaveDistribution = 0.7
## Multiplies wave counts seen in the server's mapConfig.json by this number
# Setting type: Double
# Default value: 0.5
# Default value: 1
# Acceptable value range: From 0 to 10
ScavWaveQuantity = 1
@ -192,7 +201,7 @@ ScavWaveQuantity = 1
# Setting type: Double
# Default value: 1
# Acceptable value range: From 0 to 10
PmcWaveQuantity = 1.2
PmcWaveQuantity = 1.5
[3.Debug]

View File

@ -1,6 +1,6 @@
{
"nextUpdateTime": 1736141944920,
"selectedMap": "woods",
"nextUpdateTime": 1736820722143,
"selectedMap": "lighthouse",
"lastRotationInterval": 180,
"lastUpdateTime": 1736131144920
"lastUpdateTime": 1736809922143
}

View File

@ -1,11 +1,11 @@
[General]
gameName=spt
modid=0
version=d2024.12.31.0
version=d2025.1.13.0
newestVersion=
category="1,"
nexusFileStatus=1
installationFile=DewardianDev-MOAR-2.6.1.zip
installationFile=DewardianDev-MOAR-2.6.7.zip
repository=Nexus
ignoredVersion=
comments=

View File

@ -6,12 +6,13 @@
"scavWaveQuantity": 1.2
},
"more-pmcs": {
"scavWaveDistribution": 0.4,
"morePmcGroups": true,
"pmcMaxGroupSize": 5,
"pmcWaveQuantity": 1.2
},
"more-scavs-and-pmcs": {
"maxBotCap": 30,
"scavWaveDistribution": 0.4,
"moreScavGroups": true,
"scavMaxGroupSize": 5,
"morePmcGroups": true,
@ -39,8 +40,6 @@
"scavWaveDistribution": 0.4,
"scavWaveQuantity": 1.3,
"pmcWaveQuantity": 1.3,
"maxBotCap": 30,
"maxBotPerZone": 9,
"moreScavGroups": true,
"morePmcGroups": true,
"pmcMaxGroupSize": 6,

View File

@ -2,14 +2,18 @@
"enableBotSpawning": true,
"pmcDifficulty": 0.6,
"scavDifficulty": 0.3,
"scavDifficulty": 0.4,
"scavWaveDistribution": 0.3,
"scavWaveQuantity": 0.5,
"scavWaveDistribution": 0.5,
"scavWaveQuantity": 1,
"startingPmcs": false,
"pmcWaveDistribution": 0.8,
"playerOpenZones": false,
"pmcOpenZones": true,
"allOpenZones": false,
"pmcWaveDistribution": 0.7,
"pmcWaveQuantity": 1,
"zombiesEnabled": false,
@ -18,7 +22,7 @@
"zombieHealth": 1,
"maxBotCap": 25,
"maxBotPerZone": 7,
"maxBotPerZone": 5,
"moreScavGroups": false,
"morePmcGroups": false,

View File

@ -5,14 +5,10 @@
"scavWaveCount": 21,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneDormitory",
"ZoneCrossRoad",
"ZoneGasStation"
"ZoneDormitory"
],
"pmcHotZones": [
"ZoneDormitory",
"ZoneGasStation",
"ZoneCustoms"
"ZoneDormitory"
]
},
"factoryDay": {
@ -39,11 +35,6 @@
"scavHotZones": [
"ZoneCenterBot",
"ZoneCenter"
],
"pmcHotZones": [
"ZoneIDEA",
"ZoneOLI",
"ZoneCenter"
]
},
"laboratory": {
@ -59,12 +50,7 @@
"zombieWaveCount": 10,
"scavHotZones": [
"Zone_LongRoad",
"Zone_Village"
],
"pmcHotZones": [
"Zone_DestroyedHouse",
"Zone_Chalet",
"Zone_Village"
"Zone_LongRoad"
]
},
"rezervbase": {
@ -73,13 +59,10 @@
"scavWaveCount": 24,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneRailStrorage",
"ZoneBunkerStorage",
"ZoneBarrack"
"ZoneRailStrorage"
],
"pmcHotZones": [
"ZoneBarrack",
"ZoneBunkerStorage"
"ZoneBarrack"
]
},
"shoreline": {
@ -88,34 +71,17 @@
"scavWaveCount": 32,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneSanatorium1",
"ZoneGasStation",
"ZonePowerStation",
"ZoneBusStation",
"ZoneStartVillage"
"ZoneSanatorium1"
],
"pmcHotZones": [
"ZoneSanatorium2",
"ZoneGasStation",
"ZonePowerStation"
"ZoneSanatorium2"
]
},
"tarkovstreets": {
"spawnMinDistance": 40,
"pmcWaveCount": 16,
"scavWaveCount": 28,
"zombieWaveCount": 13,
"scavHotZones": [
"ZoneHotel_2",
"ZoneHotel_1",
"ZoneConstruction",
"ZoneCarShowroom"
],
"pmcHotZones": [
"ZoneSanatorium2",
"ZoneCinema",
"ZoneConcordiaParking"
]
"zombieWaveCount": 13
},
"woods": {
"spawnMinDistance": 40,
@ -123,15 +89,10 @@
"scavWaveCount": 28,
"zombieWaveCount": 10,
"scavHotZones": [
"ZoneWoodCutter",
"ZoneClearVill",
"ZoneScavBase2",
"ZoneRedHouse"
"ZoneWoodCutter"
],
"pmcHotZones": [
"ZoneWoodCutter",
"ZoneBigRocks",
"ZoneHighRocks"
"ZoneWoodCutter"
]
},
"gzLow": {

View File

@ -1,6 +1,6 @@
{
"name": "MOAR",
"version": "2.6.1",
"version": "2.6.7",
"main": "src/mod.js",
"license": "MIT",
"author": "DewardianDev",

View File

@ -1,7 +1,7 @@
import { DependencyContainer } from "tsyringe";
import { buildWaves } from "../Spawning/Spawning";
import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { DynamicRouterModService } from "@spt/services/mod/dynamicRouter/DynamicRouterModService";
// import { DynamicRouterModService } from "@spt/services/mod/dynamicRouter/DynamicRouterModService";
import { globalValues } from "../GlobalValues";
import { kebabToTitle } from "../utils";
import PresetWeightingsConfig from "../../config/PresetWeightings.json";
@ -11,9 +11,9 @@ export const setupRoutes = (container: DependencyContainer) => {
"StaticRouterModService"
);
const dynamicRouterModService = container.resolve<DynamicRouterModService>(
"DynamicRouterModService"
);
// const dynamicRouterModService = container.resolve<DynamicRouterModService>(
// "DynamicRouterModService"
// );
// Make buildwaves run on game end
staticRouterModService.registerStaticRouter(

View File

@ -7,7 +7,11 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { DependencyContainer } from "tsyringe";
import { globalValues } from "../GlobalValues";
import { cloneDeep, getRandomPresetOrCurrentlySelectedPreset } from "../utils";
import {
cloneDeep,
getRandomPresetOrCurrentlySelectedPreset,
saveToFile,
} from "../utils";
import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig.d";
import { originalMapList } from "./constants";
import { buildBossWaves } from "./buildBossWaves";
@ -63,12 +67,12 @@ export const buildWaves = (container: DependencyContainer) => {
}
});
config.debug &&
console.log(
globalValues.forcedPreset === "custom"
? "custom"
: globalValues.currentPreset
);
// config.debug &&
console.log(
globalValues.forcedPreset === "custom"
? "custom"
: globalValues.currentPreset
);
const {
bigmap: customs,
@ -127,7 +131,7 @@ export const buildWaves = (container: DependencyContainer) => {
rezervbase: { pmcbot: { min: 0, max: 0 } },
};
updateSpawnLocations(locationList);
updateSpawnLocations(locationList, config);
setEscapeTimeOverrides(locationList, _mapConfig, Logger, config);

View File

@ -139,36 +139,20 @@ export function buildBossWaves(
for (let key = 0; key < locationList.length; key++) {
//Gather bosses to avoid duplicating.
let bossLocations = "";
const duplicateBosses = [
...locationList[key].base.BossLocationSpawn.filter(
({ BossName, BossZone }) => {
bossLocations += BossZone + ",";
return bossList.includes(BossName);
}
({ BossName, BossZone }) => bossList.includes(BossName)
).map(({ BossName }) => BossName),
"bossKnight", // So knight doesn't invade
];
const uniqueBossZones = bossOpenZones
? ""
: [
...new Set(
bossLocations
.split(",")
.filter(
(zone) => !!zone && !zone.toLowerCase().includes("snipe")
)
),
].join(",");
//Build bosses to add
const bossesToAdd = shuffle<IBossLocationSpawn[]>(Object.values(bosses))
.filter(({ BossName }) => !duplicateBosses.includes(BossName))
.map((boss, j) => ({
...boss,
BossZone: uniqueBossZones,
BossZone: "",
BossEscortAmount:
boss.BossEscortAmount === "0" ? boss.BossEscortAmount : "1",
...(gradualBossInvasion ? { Time: j * 20 + 1 } : {}),
@ -268,7 +252,22 @@ export function buildBossWaves(
configLocations[index]
}: ${bossesToAdd.map(({ BossName }) => BossName)}`
);
// console.log(locationList[index].base.BossLocationSpawn.length);
// 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.filter(({ BossChance, BossName }, bossIndex) => {
if (BossChance < 100 && BossChance / 100 < Math.random()) {
return false;
}
return true;
}).map((boss) => ({ ...boss, ...{ BossChance: 100 } }));
// if (mapName === "customs")
// console.log(mapName, locationList[index].base.BossLocationSpawn);
});
if (hasChangedBossSpawns) {
console.log(
`[MOAR]: --- Adjusting default boss spawn rates complete --- \n`

View File

@ -30,25 +30,21 @@ export default function buildPmcs(
.filter(
({ Categories, BotZoneName }) =>
!!BotZoneName &&
(Categories.includes("Player") ||
(map === "laboratory" &&
!BotZoneName.includes("BotZoneGate"))) &&
!BotZoneName.includes("snipe")
!BotZoneName.includes("snipe") &&
(Categories.includes("Player") || Categories.includes("All")) &&
!BotZoneName.includes("BotZoneGate")
)
.map(({ BotZoneName, ...rest }) => {
return BotZoneName;
})
),
...pmcHotZones,
]);
// Make labs have only named zones
if (map === "laboratory") {
pmcZones = new Array(10).fill(pmcZones).flat(1);
// console.log(pmcZones);
}
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
const { pmcWaveCount } = mapConfig[map];
const escapeTimeLimitRatio = Math.round(
@ -58,13 +54,12 @@ export default function buildPmcs(
const totalWaves = Math.round(
pmcWaveCount * config.pmcWaveQuantity * escapeTimeLimitRatio
);
// console.log(pmcZones.length, totalWaves);
const numberOfZoneless = totalWaves - pmcZones.length;
if (numberOfZoneless > 0) {
const addEmpty = new Array(numberOfZoneless).fill("");
pmcZones = shuffle<string[]>([...pmcZones, ...addEmpty]);
}
// if (map === "laboratory") console.log(numberOfZoneless, pmcZones);
if (config.debug) {
console.log(`${map} PMC count ${totalWaves} \n`);
@ -75,10 +70,16 @@ export default function buildPmcs(
);
}
const waves = buildPmcWaves(pmcWaveCount, timeLimit, config, pmcZones);
// if (map === "laboratory")
// console.log(waves.map(({ BossZone }) => BossZone));
// apply our new waves
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
const waves = buildPmcWaves(
totalWaves,
timeLimit,
config,
pmcZones,
pmcHotZones
);
locationList[index].base.BossLocationSpawn = [
...waves,
...locationList[index].base.BossLocationSpawn,

View File

@ -89,19 +89,16 @@ export default function buildScavMarksmanWaves(
const sniperLocations = new Set(
[...locationList[index].base.SpawnPointParams]
.filter(
({ Categories, Sides, BotZoneName }) =>
!!BotZoneName &&
Sides.includes("Savage") &&
!Categories.includes("Boss")
({ Categories, DelayToCanSpawnSec, BotZoneName, Sides }) =>
!Categories.includes("Boss") &&
Sides[0] === "Savage" &&
(BotZoneName?.toLowerCase().includes("snipe") ||
DelayToCanSpawnSec > 40)
)
.filter(
({ BotZoneName, DelayToCanSpawnSec }) =>
BotZoneName?.toLowerCase().includes("snipe") ||
DelayToCanSpawnSec > 300
)
.map(({ BotZoneName }) => BotZoneName)
.map(({ BotZoneName }) => BotZoneName || "")
);
if (sniperLocations.size) {
locationList[index].base.MinMaxBots = [
{
@ -112,32 +109,21 @@ export default function buildScavMarksmanWaves(
];
}
const scavZones = shuffle<string[]>([
let scavZones = shuffle<string[]>([
...new Set(
[...locationList[index].base.SpawnPointParams]
.filter(
({ Categories, Sides, BotZoneName }) =>
!!BotZoneName &&
Sides.includes("Savage") &&
!Categories.includes("Boss")
Categories.includes("Bot") &&
(Sides.includes("Savage") || Sides.includes("All"))
)
.map(({ BotZoneName }) => BotZoneName)
.filter((name) => !sniperLocations.has(name))
),
]);
// Reduced Zone Delay
locationList[index].base.SpawnPointParams = locationList[
index
].base.SpawnPointParams.map((spawn) => ({
...spawn,
DelayToCanSpawnSec:
spawn.DelayToCanSpawnSec > 20
? Math.round(spawn.DelayToCanSpawnSec / 10)
: spawn.DelayToCanSpawnSec,
}));
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
const { scavWaveCount } = mapConfig[map];
const escapeTimeLimitRatio = Math.round(
@ -149,12 +135,19 @@ export default function buildScavMarksmanWaves(
scavWaveCount * scavWaveQuantity * escapeTimeLimitRatio
);
const numberOfZoneless = scavTotalWaveCount - scavZones.length;
// console.log(numberOfZoneless);
if (numberOfZoneless > 0) {
const addEmpty = new Array(numberOfZoneless).fill("");
scavZones = shuffle<string[]>([...scavZones, ...addEmpty]);
}
// console.log(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;
let snipers = waveBuilder(
sniperLocations.size,
Math.round(timeLimit / 4),
@ -166,14 +159,13 @@ export default function buildScavMarksmanWaves(
[],
shuffle([...sniperLocations]),
80,
false,
true,
true
);
if (snipersHaveFriends)
snipers = snipers.map((wave) => ({
...wave,
slots_min: 0,
...(snipersHaveFriends && wave.slots_max < 2
? { slots_min: 1, slots_max: 2 }
: {}),

View File

@ -1,36 +1,137 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import { configLocations } from "./constants";
import mapConfig from "../../config/mapConfig.json";
import _config from "../../config/config.json";
export default function updateSpawnLocations(locationList: ILocation[]) {
export default function updateSpawnLocations(
locationList: ILocation[],
config: typeof _config
) {
for (let index = 0; index < locationList.length; index++) {
const map = configLocations[index];
// console.log(map);
const limit = mapConfig[map].spawnMinDistance;
const InfiltrationList = [
...new Set(
locationList[index].base.SpawnPointParams.filter(
({ Infiltration }) => Infiltration
).map(({ Infiltration }) => Infiltration)
),
];
// console.log(map, InfiltrationList);
const getRandomInfil = (): string =>
InfiltrationList[Math.floor(Math.random() * InfiltrationList.length)];
// console.log(InfiltrationList);
// console.log("\n" + map);
locationList[index].base.SpawnPointParams.forEach(
(
{ ColliderParams, BotZoneName, DelayToCanSpawnSec, Categories, Sides },
{
ColliderParams,
BotZoneName,
DelayToCanSpawnSec,
Categories,
Sides,
Infiltration,
},
innerIndex
) => {
if (
ColliderParams?._props?.Radius !== undefined &&
ColliderParams?._props?.Radius < limit &&
!Categories.includes("Boss") &&
!BotZoneName?.toLowerCase().includes("snipe") &&
DelayToCanSpawnSec < 300
DelayToCanSpawnSec < 41
) {
// console.log(
// "----",
// ColliderParams._props.Radius,
// "=>",
// limit,
// BotZoneName
// );
// Make it so players/pmcs can spawn anywhere.
if (
config.playerOpenZones &&
!!Infiltration &&
(Sides.includes("Pmc") || Sides.includes("All"))
) {
locationList[index].base.SpawnPointParams[innerIndex].Categories = [
"Player",
"Coop",
innerIndex % 2 === 0 ? "Group" : "Opposite",
];
locationList[index].base.SpawnPointParams[
innerIndex
].ColliderParams._props.Radius = limit;
locationList[index].base.SpawnPointParams[innerIndex].Sides = [
"Pmc",
"All",
];
// console.log(
// BotZoneName || "none",
// locationList[index].base.SpawnPointParams[innerIndex].Categories,
// locationList[index].base.SpawnPointParams[innerIndex].Sides
// );
}
if (!Infiltration) {
if (
!config.allOpenZones &&
config.pmcOpenZones &&
Categories.includes("Bot") &&
Sides[0] === "Savage"
) {
// if (BotZoneName === "Zone_LongRoad") console.log("yes");
locationList[index].base.SpawnPointParams[innerIndex].Categories =
["Player", "Bot"];
locationList[index].base.SpawnPointParams[
innerIndex
].Infiltration = getRandomInfil();
}
if (config.allOpenZones) {
locationList[index].base.SpawnPointParams[innerIndex].Categories =
[
"Bot",
"Player",
"Coop",
innerIndex % 2 === 0 ? "Group" : "Opposite",
];
locationList[index].base.SpawnPointParams[
innerIndex
].Infiltration = getRandomInfil();
// console.log(
// locationList[index].base.SpawnPointParams[innerIndex].Infiltration
// );
locationList[index].base.SpawnPointParams[innerIndex].Sides = [
"Pmc",
"Savage",
"All",
];
}
if (config.bossOpenZones && Categories.includes("Bot")) {
locationList[index].base.SpawnPointParams[
innerIndex
].Categories.push("Boss");
}
}
if (
ColliderParams?._props?.Radius !== undefined &&
ColliderParams?._props?.Radius < limit
) {
locationList[index].base.SpawnPointParams[
innerIndex
].ColliderParams._props.Radius = limit;
}
} else {
if (!Categories.includes("Boss") && DelayToCanSpawnSec > 40) {
locationList[index].base.SpawnPointParams[
innerIndex
].DelayToCanSpawnSec = Math.round(
DelayToCanSpawnSec * Math.random() * Math.random() * 0.5
);
// console.log(
// BotZoneName,
// DelayToCanSpawnSec,
// locationList[index].base.SpawnPointParams[innerIndex]
// .DelayToCanSpawnSec
// );
}
}
}
);

View File

@ -44,7 +44,7 @@ export const waveBuilder = (
);
const min = !offset && waves.length < 1 ? 0 : timeStart;
const max = !offset && waves.length < 1 ? 0 : timeStart + 10;
const max = !offset && waves.length < 1 ? 0 : timeStart + 60;
if (waves.length >= 1 || offset) timeStart = timeStart + stage;
const BotPreset = getDifficulty(difficulty);
@ -55,8 +55,9 @@ export const waveBuilder = (
);
if (slotMax < 1) slotMax = 1;
const slotMin = (Math.round(Math.random() * slotMax) || 1) - 1;
let slotMin = (Math.round(Math.random() * slotMax) || 1) - 1;
if (wildSpawnType === "marksman" && slotMin < 1) slotMin = 1;
waves.push({
BotPreset,
BotSide: getBotSide(wildSpawnType),
@ -189,11 +190,26 @@ export const getRandomZombieType = () =>
zombieTypesCaps[Math.round((zombieTypesCaps.length - 1) * Math.random())];
export const buildPmcWaves = (
totalWaves: number,
pmcTotal: number,
escapeTimeLimit: number,
config: typeof _config,
bossZones: string[]
bossZones: string[],
hotZones: string[]
): IBossLocationSpawn[] => {
// console.log(pmcTotal)
if (!pmcTotal) return [];
const halfIndex = Math.round(bossZones.length * 0.75); //Put hotzones in the 2 - 4 spawns
// console.log(bossZones.length);
bossZones = [
...bossZones.slice(0, halfIndex),
...hotZones,
...bossZones.slice(halfIndex),
];
// console.log(bossZones.length, hotZones.length);
// console.log(bossZones);
pmcTotal = pmcTotal + hotZones.length;
let {
pmcMaxGroupSize,
pmcDifficulty,
@ -202,14 +218,12 @@ export const buildPmcWaves = (
pmcWaveDistribution,
} = config;
const averageTime = escapeTimeLimit / totalWaves;
const firstHalf = Math.round(averageTime * (1 - pmcWaveDistribution));
const secondHalf = Math.round(averageTime * (1 + pmcWaveDistribution));
let timeStart = -1;
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = totalWaves;
const averageTime = (escapeTimeLimit * 0.8) / pmcTotal;
while (totalWaves > 0) {
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = pmcTotal;
while (pmcTotal > 0) {
let bossEscortAmount = Math.round(
(morePmcGroups ? 1 : Math.random()) *
Math.random() *
@ -217,20 +231,24 @@ export const buildPmcWaves = (
);
if (bossEscortAmount < 0) bossEscortAmount = 0;
const accelerate = totalWaves > 5 && waves.length < totalWaves / 3;
const stage = startingPmcs
? 10
: Math.round(
waves.length < Math.round(totalWaves * 0.5)
? accelerate
? firstHalf / 3
: firstHalf
: secondHalf
);
if (waves.length >= 1) timeStart = timeStart + stage;
// const totalCountThisWave = bossEscortAmount + 1;
const totalCountThusFar = pmcTotal - maxSlotsReached;
const timeToUse =
totalCountThusFar < pmcTotal * pmcWaveDistribution
? Math.round(
averageTime * (1 - pmcWaveDistribution) * totalCountThusFar
)
: Math.round(
escapeTimeLimit * (1 - pmcWaveDistribution) +
(1 - pmcWaveDistribution) * totalCountThusFar * averageTime
);
let timeStart =
(startingPmcs ? totalCountThusFar * totalCountThusFar * 3 : timeToUse) ||
-1;
// console.log(timeStart, BossEscortAmount);
const side = Math.random() > 0.5 ? "pmcBEAR" : "pmcUSEC";
const BossDifficult = getDifficulty(pmcDifficulty);
@ -260,7 +278,10 @@ export const buildPmcWaves = (
maxSlotsReached -= 1 + bossEscortAmount;
if (maxSlotsReached <= 0) break;
}
// console.log(
// escapeTimeLimit,
// waves.map(({ Time }) => Time)
// );
return waves;
};
@ -270,6 +291,7 @@ export const buildZombie = (
waveDistribution: number,
BossChance: number = 100
): IBossLocationSpawn[] => {
if (!totalWaves) return [];
const averageTime = (escapeTimeLimit * 60) / totalWaves;
const firstHalf = Math.round(averageTime * (1 - waveDistribution));
const secondHalf = Math.round(averageTime * (1 + waveDistribution));

View File

@ -0,0 +1,28 @@
[General]
gameName=spt
modid=0
version=d2024.12.31.0
newestVersion=
category="1,"
nexusFileStatus=1
installationFile=DewardianDev-MOAR-2.6.1.zip
repository=Nexus
ignoredVersion=
comments=
notes=
nexusDescription=
url=
hasCustomURL=false
lastNexusQuery=
lastNexusUpdate=
nexusLastModified=2024-12-16T06:46:30Z
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

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,12 @@
{
"live-like": 25,
"more-scavs": 8,
"more-pmcs": 8,
"more-scavs-and-pmcs": 5,
"main-boss-roaming": 5,
"sniper-buddies": 4,
"boss-invasion": 2,
"rogue-invasion": 0,
"raider-invasion": 0,
"insanity": 0
}

View File

@ -0,0 +1,65 @@
{
"live-like": {},
"more-scavs": {
"moreScavGroups": true,
"scavMaxGroupSize": 5,
"scavWaveQuantity": 1.2
},
"more-pmcs": {
"morePmcGroups": true,
"pmcMaxGroupSize": 5,
"pmcWaveQuantity": 1.2
},
"more-scavs-and-pmcs": {
"maxBotCap": 30,
"moreScavGroups": true,
"scavMaxGroupSize": 5,
"morePmcGroups": true,
"pmcMaxGroupSize": 5,
"scavWaveQuantity": 1.2,
"pmcWaveQuantity": 1.2,
"mainBossChanceBuff": 25
},
"boss-invasion": {
"bossOpenZones": true,
"bossInvasion": true,
"bossInvasionSpawnChance": 10,
"mainBossChanceBuff": 25,
"gradualBossInvasion": true
},
"rogue-invasion": {
"randomRaiderGroup": true,
"randomRaiderGroupChance": 50
},
"raider-invasion": {
"randomRaiderGroup": true,
"randomRaiderGroupChance": 50
},
"insanity": {
"scavWaveDistribution": 0.4,
"scavWaveQuantity": 1.3,
"pmcWaveQuantity": 1.3,
"maxBotCap": 30,
"maxBotPerZone": 9,
"moreScavGroups": true,
"morePmcGroups": true,
"pmcMaxGroupSize": 6,
"scavMaxGroupSize": 6,
"snipersHaveFriends": true,
"bossOpenZones": true,
"randomRaiderGroup": true,
"randomRaiderGroupChance": 50,
"randomRogueGroup": true,
"randomRogueGroupChance": 50,
"mainBossChanceBuff": 50,
"bossInvasion": true,
"bossInvasionSpawnChance": 10
},
"main-boss-roaming": {
"bossOpenZones": true,
"mainBossChanceBuff": 35
},
"sniper-buddies": {
"snipersHaveFriends": true
}
}

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": 30,
"bossPartisan": 30,
"bossBully": 30
},
"factoryDay": {
"bossTagilla": 30
},
"factoryNight": {
"bossTagilla": 30
},
"interchange": {
"bossKilla": 30
},
"laboratory": {},
"lighthouse": {
"bossKnight": 30,
"bossPartisan": 30
},
"rezervbase": {
"bossGluhar": 30
},
"shoreline": {
"bossKnight": 30,
"bossPartisan": 30,
"bossSanitar": 30
},
"tarkovstreets": {
"bossBoar": 30,
"bossKolontay": 30
},
"woods": {
"bossKojaniy": 30,
"bossKnight": 30,
"bossPartisan": 30
},
"gzLow": {},
"gzHigh": {
"bossKolontay": 30
}
}

View File

@ -0,0 +1,46 @@
{
"enableBotSpawning": true,
"pmcDifficulty": 0.6,
"scavDifficulty": 0.3,
"scavWaveDistribution": 0.3,
"scavWaveQuantity": 0.5,
"startingPmcs": false,
"pmcWaveDistribution": 0.8,
"pmcWaveQuantity": 1.6,
"zombiesEnabled": false,
"zombieWaveDistribution": 0.5,
"zombieWaveQuantity": 1,
"zombieHealth": 1,
"maxBotCap": 25,
"maxBotPerZone": 7,
"moreScavGroups": false,
"morePmcGroups": false,
"pmcMaxGroupSize": 4,
"scavMaxGroupSize": 4,
"snipersHaveFriends": false,
"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,149 @@
{
"customs": {
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 21,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneDormitory",
"ZoneCrossRoad",
"ZoneGasStation"
],
"pmcHotZones": [
"ZoneDormitory",
"ZoneGasStation",
"ZoneCustoms"
]
},
"factoryDay": {
"spawnMinDistance": 20,
"maxBotCapOverride": 12,
"maxBotPerZoneOverride": 10,
"pmcWaveCount": 8,
"scavWaveCount": 9,
"zombieWaveCount": 6
},
"factoryNight": {
"spawnMinDistance": 20,
"maxBotCapOverride": 12,
"maxBotPerZoneOverride": 10,
"pmcWaveCount": 8,
"scavWaveCount": 9,
"zombieWaveCount": 6
},
"interchange": {
"spawnMinDistance": 40,
"pmcWaveCount": 14,
"scavWaveCount": 32,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneCenterBot",
"ZoneCenter"
],
"pmcHotZones": [
"ZoneIDEA",
"ZoneOLI",
"ZoneCenter"
]
},
"laboratory": {
"spawnMinDistance": 20,
"pmcWaveCount": 10,
"scavWaveCount": 0,
"zombieWaveCount": 12
},
"lighthouse": {
"spawnMinDistance": 40,
"pmcWaveCount": 12,
"scavWaveCount": 20,
"zombieWaveCount": 10,
"scavHotZones": [
"Zone_LongRoad",
"Zone_Village"
],
"pmcHotZones": [
"Zone_DestroyedHouse",
"Zone_Chalet",
"Zone_Village"
]
},
"rezervbase": {
"spawnMinDistance": 40,
"pmcWaveCount": 11,
"scavWaveCount": 24,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneRailStrorage",
"ZoneBunkerStorage",
"ZoneBarrack"
],
"pmcHotZones": [
"ZoneBarrack",
"ZoneBunkerStorage"
]
},
"shoreline": {
"spawnMinDistance": 40,
"pmcWaveCount": 14,
"scavWaveCount": 32,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneSanatorium1",
"ZoneGasStation",
"ZonePowerStation",
"ZoneBusStation",
"ZoneStartVillage"
],
"pmcHotZones": [
"ZoneSanatorium2",
"ZoneGasStation",
"ZonePowerStation"
]
},
"tarkovstreets": {
"spawnMinDistance": 40,
"pmcWaveCount": 16,
"scavWaveCount": 28,
"zombieWaveCount": 13,
"scavHotZones": [
"ZoneHotel_2",
"ZoneHotel_1",
"ZoneConstruction",
"ZoneCarShowroom"
],
"pmcHotZones": [
"ZoneSanatorium2",
"ZoneCinema",
"ZoneConcordiaParking"
]
},
"woods": {
"spawnMinDistance": 40,
"pmcWaveCount": 14,
"scavWaveCount": 28,
"zombieWaveCount": 10,
"scavHotZones": [
"ZoneWoodCutter",
"ZoneClearVill",
"ZoneScavBase2",
"ZoneRedHouse"
],
"pmcHotZones": [
"ZoneWoodCutter",
"ZoneBigRocks",
"ZoneHighRocks"
]
},
"gzLow": {
"spawnMinDistance": 30,
"pmcWaveCount": 10,
"scavWaveCount": 18,
"zombieWaveCount": 9
},
"gzHigh": {
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 18,
"zombieWaveCount": 9
}
}

View File

@ -0,0 +1,25 @@
{
"name": "MOAR",
"version": "2.6.1",
"main": "src/mod.js",
"license": "MIT",
"author": "DewardianDev",
"sptVersion": "^3.10.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,11 @@
import config from "../config/config.json";
import { ILocationBase } 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 = "custom";
public static addedMapZones: Record<string, string[]> = {};
}

View File

@ -0,0 +1,168 @@
import { DependencyContainer } from "tsyringe";
import { buildWaves } from "../Spawning/Spawning";
import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { DynamicRouterModService } from "@spt/services/mod/dynamicRouter/DynamicRouterModService";
import { globalValues } from "../GlobalValues";
import { kebabToTitle } from "../utils";
import PresetWeightingsConfig from "../../config/PresetWeightings.json";
export const setupRoutes = (container: DependencyContainer) => {
const staticRouterModService = container.resolve<StaticRouterModService>(
"StaticRouterModService"
);
const dynamicRouterModService = container.resolve<DynamicRouterModService>(
"DynamicRouterModService"
);
// 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,154 @@
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 } 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 { setEscapeTimeOverrides } from "./utils";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import updateSpawnLocations from "./updateSpawnLocations";
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, globals } = 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.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.convertIntoPmcChance = {
default: {
assault: { min: 0, max: 0 },
cursedassault: { min: 0, max: 0 },
pmcbot: { min: 0, max: 0 },
exusec: { min: 0, max: 0 },
arenafighter: { min: 0, max: 0 },
arenafighterevent: { min: 0, max: 0 },
crazyassaultevent: { min: 0, max: 0 },
},
factory4_day: { assault: { min: 0, max: 0 } },
laboratory: { pmcbot: { min: 0, max: 0 } },
rezervbase: { pmcbot: { min: 0, max: 0 } },
};
updateSpawnLocations(locationList);
setEscapeTimeOverrides(locationList, _mapConfig, Logger, config);
// Make main waves
buildScavMarksmanWaves(config, locationList, botConfig);
// BOSS RELATED STUFF!
buildBossWaves(config, locationList);
//Zombies
if (config.zombiesEnabled) {
buildZombieWaves(config, locationList, bots);
}
buildPmcs(config, locationList);
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,278 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import bossConfig from "../../config/bossConfig.json";
import mapConfig from "../../config/mapConfig.json";
import {
bossesToRemoveFromPool,
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)
);
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.
let bossLocations = "";
const duplicateBosses = [
...locationList[key].base.BossLocationSpawn.filter(
({ BossName, BossZone }) => {
bossLocations += BossZone + ",";
return bossList.includes(BossName);
}
).map(({ BossName }) => BossName),
"bossKnight", // So knight doesn't invade
];
const uniqueBossZones = bossOpenZones
? ""
: [
...new Set(
bossLocations
.split(",")
.filter(
(zone) => !!zone && !zone.toLowerCase().includes("snipe")
)
),
].join(",");
//Build bosses to add
const bossesToAdd = shuffle<IBossLocationSpawn[]>(Object.values(bosses))
.filter(({ BossName }) => !duplicateBosses.includes(BossName))
.map((boss, j) => ({
...boss,
BossZone: uniqueBossZones,
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 --- `
);
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)}`
);
});
if (hasChangedBossSpawns) {
console.log(
`[MOAR]: --- Adjusting default boss spawn rates complete --- \n`
);
}
}
}

View File

@ -0,0 +1,87 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import mapConfig from "../../config/mapConfig.json";
import {
bossesToRemoveFromPool,
defaultEscapeTimes,
defaultHostility,
} from "./constants";
import { buildPmcWaves, MapSettings, shuffle } from "./utils";
import { saveToFile } from "../utils";
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];
locationList[index].base.BotLocationModifier.AdditionalHostilitySettings =
defaultHostility;
const { pmcHotZones = [] } = (mapConfig?.[map] as MapSettings) || {};
let pmcZones = shuffle<string[]>([
...new Set(
[...locationList[index].base.SpawnPointParams]
.filter(
({ Categories, BotZoneName }) =>
!!BotZoneName &&
(Categories.includes("Player") ||
(map === "laboratory" &&
!BotZoneName.includes("BotZoneGate"))) &&
!BotZoneName.includes("snipe")
)
.map(({ BotZoneName, ...rest }) => {
return BotZoneName;
})
),
...pmcHotZones,
]);
// Make labs have only named zones
if (map === "laboratory") {
pmcZones = new Array(10).fill(pmcZones).flat(1);
// console.log(pmcZones);
}
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
const { pmcWaveCount } = mapConfig[map];
const escapeTimeLimitRatio = Math.round(
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
const totalWaves = Math.round(
pmcWaveCount * config.pmcWaveQuantity * escapeTimeLimitRatio
);
// console.log(pmcZones.length, totalWaves);
const numberOfZoneless = totalWaves - pmcZones.length;
if (numberOfZoneless > 0) {
const addEmpty = new Array(numberOfZoneless).fill("");
pmcZones = shuffle<string[]>([...pmcZones, ...addEmpty]);
}
// if (map === "laboratory") console.log(numberOfZoneless, 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 waves = buildPmcWaves(pmcWaveCount, timeLimit, config, pmcZones);
// if (map === "laboratory")
// console.log(waves.map(({ BossZone }) => BossZone));
// apply our new waves
locationList[index].base.BossLocationSpawn = [
...waves,
...locationList[index].base.BossLocationSpawn,
];
}
}

View File

@ -0,0 +1,226 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import _config from "../../config/config.json";
import mapConfig from "../../config/mapConfig.json";
import {
configLocations,
defaultEscapeTimes,
defaultHostility,
originalMapList,
} from "./constants";
import { MapSettings, shuffle, waveBuilder } from "./utils";
import { IWave, WildSpawnType } from "@spt/models/eft/common/ILocationBase";
import { IBotConfig } from "@spt/models/spt/config/IBotConfig";
import { saveToFile } from "../utils";
export default function buildScavMarksmanWaves(
config: typeof _config,
locationList: ILocation[],
botConfig: IBotConfig
) {
let {
debug,
maxBotCap,
scavWaveQuantity,
scavWaveDistribution,
snipersHaveFriends,
maxBotPerZone,
scavMaxGroupSize,
scavDifficulty,
moreScavGroups,
} = 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 = {
...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,
} = (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;
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, Sides, BotZoneName }) =>
!!BotZoneName &&
Sides.includes("Savage") &&
!Categories.includes("Boss")
)
.filter(
({ BotZoneName, DelayToCanSpawnSec }) =>
BotZoneName?.toLowerCase().includes("snipe") ||
DelayToCanSpawnSec > 300
)
.map(({ BotZoneName }) => BotZoneName)
);
if (sniperLocations.size) {
locationList[index].base.MinMaxBots = [
{
WildSpawnType: "marksman",
max: sniperLocations.size * 5,
min: sniperLocations.size,
},
];
}
const scavZones = shuffle<string[]>([
...new Set(
[...locationList[index].base.SpawnPointParams]
.filter(
({ Categories, Sides, BotZoneName }) =>
!!BotZoneName &&
Sides.includes("Savage") &&
!Categories.includes("Boss")
)
.map(({ BotZoneName }) => BotZoneName)
.filter((name) => !sniperLocations.has(name))
),
]);
// Reduced Zone Delay
locationList[index].base.SpawnPointParams = locationList[
index
].base.SpawnPointParams.map((spawn) => ({
...spawn,
DelayToCanSpawnSec:
spawn.DelayToCanSpawnSec > 20
? Math.round(spawn.DelayToCanSpawnSec / 10)
: spawn.DelayToCanSpawnSec,
}));
const timeLimit = locationList[index].base.EscapeTimeLimit * 60;
const { scavWaveCount } = mapConfig[map];
const escapeTimeLimitRatio = Math.round(
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
// Scavs
const scavTotalWaveCount = Math.round(
scavWaveCount * scavWaveQuantity * escapeTimeLimitRatio
);
config.debug &&
escapeTimeLimitRatio !== 1 &&
console.log(
`${map} Scav wave count changed from ${scavWaveCount} to ${scavTotalWaveCount} due to escapeTimeLimit adjustment`
);
let snipers = waveBuilder(
sniperLocations.size,
Math.round(timeLimit / 4),
0.5,
WildSpawnType.MARKSMAN,
0.7,
false,
2,
[],
shuffle([...sniperLocations]),
80,
false,
true
);
if (snipersHaveFriends)
snipers = snipers.map((wave) => ({
...wave,
slots_min: 0,
...(snipersHaveFriends && wave.slots_max < 2
? { slots_min: 1, slots_max: 2 }
: {}),
}));
const scavWaves = waveBuilder(
scavTotalWaveCount,
timeLimit,
scavWaveDistribution,
WildSpawnType.ASSAULT,
scavDifficulty,
false,
scavMaxGroupSize,
map === "gzHigh" ? [] : scavZones,
scavHotZones,
0,
false,
!!moreScavGroups
);
if (debug) {
let totalscav = 0;
scavWaves.forEach(({ slots_max }) => (totalscav += slots_max));
console.log(configLocations[index]);
console.log(
"Scavs:",
totalscav,
"configVal",
Math.round((totalscav / scavWaveCount) * 100) / 100,
"configWaveCount",
scavWaveCount,
"waveCount",
scavWaves.length,
"\n"
);
}
// const finalSniperWaves = snipers?.map(({ ...rest }, snipKey) => ({
// ...rest,
// number: snipKey,
// time_min: snipKey * 120,
// time_max: snipKey * 120 + 120,
// }));
// if (map === "customs") saveToFile({ scavWaves }, "scavWaves.json");
locationList[index].base.waves = [...snipers, ...scavWaves]
.sort(({ time_min: a }, { time_min: b }) => a - b)
.map((wave, i) => ({ ...wave, number: i + 1 }));
}
}

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,
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,204 @@
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",
"shooterBTR",
"bossPartisan",
"spiritWinter",
"spiritSpring",
"peacemaker",
"skier",
"assault",
"marksman",
"pmcUSEC",
"pmcBEAR",
"exUsec",
"pmcBot",
"bossBully",
],
AlwaysFriends: [
"bossZryachiy",
"followerZryachiy",
"peacefullZryachiyEvent",
"ravangeZryachiyEvent",
"gifter",
],
BearEnemyChance: 100,
BearPlayerBehaviour: "AlwaysEnemies",
BotRole: "pmcBEAR",
ChancedEnemies: [],
Neutral: [],
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",
"shooterBTR",
"bossPartisan",
"spiritWinter",
"spiritSpring",
"peacemaker",
"skier",
"assault",
"marksman",
"pmcUSEC",
"pmcBEAR",
"exUsec",
"pmcBot",
"bossBully",
],
AlwaysFriends: [
"bossZryachiy",
"followerZryachiy",
"peacefullZryachiyEvent",
"ravangeZryachiyEvent",
"gifter",
],
BearEnemyChance: 100,
BearPlayerBehaviour: "AlwaysEnemies",
BotRole: "pmcUSEC",
ChancedEnemies: [],
Neutral: [],
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,
};

View File

@ -0,0 +1,38 @@
import { ILocation } from "@spt/models/eft/common/ILocation";
import { configLocations } from "./constants";
import mapConfig from "../../config/mapConfig.json";
export default function updateSpawnLocations(locationList: ILocation[]) {
for (let index = 0; index < locationList.length; index++) {
const map = configLocations[index];
const limit = mapConfig[map].spawnMinDistance;
// console.log("\n" + map);
locationList[index].base.SpawnPointParams.forEach(
(
{ ColliderParams, BotZoneName, DelayToCanSpawnSec, Categories, Sides },
innerIndex
) => {
if (
ColliderParams?._props?.Radius !== undefined &&
ColliderParams?._props?.Radius < limit &&
!BotZoneName?.toLowerCase().includes("snipe") &&
DelayToCanSpawnSec < 300
) {
// console.log(
// "----",
// ColliderParams._props.Radius,
// "=>",
// limit,
// BotZoneName
// );
locationList[index].base.SpawnPointParams[
innerIndex
].ColliderParams._props.Radius = limit;
}
}
);
}
}

View File

@ -0,0 +1,430 @@
import {
IBossLocationSpawn,
IWave,
WildSpawnType,
} from "@spt/models/eft/common/ILocationBase";
import _config from "../../config/config.json";
import { ILocation } from "@spt/models/eft/common/ILocation";
import { 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 + 10;
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;
const slotMin = (Math.round(Math.random() * slotMax) || 1) - 1;
waves.push({
BotPreset,
BotSide: getBotSide(wildSpawnType),
SpawnPoints: getZone(
specialZones,
combinedZones,
waves.length >= totalWaves
),
isPlayers: isPlayer,
slots_max: slotMax,
slots_min: slotMin,
time_min: starting || !max ? -1 : min,
time_max: starting || !max ? -1 : 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 buildPmcWaves = (
totalWaves: number,
escapeTimeLimit: number,
config: typeof _config,
bossZones: string[]
): IBossLocationSpawn[] => {
let {
pmcMaxGroupSize,
pmcDifficulty,
startingPmcs,
morePmcGroups,
pmcWaveDistribution,
} = config;
const averageTime = escapeTimeLimit / totalWaves;
const firstHalf = Math.round(averageTime * (1 - pmcWaveDistribution));
const secondHalf = Math.round(averageTime * (1 + pmcWaveDistribution));
let timeStart = -1;
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = totalWaves;
while (totalWaves > 0) {
let bossEscortAmount = Math.round(
(morePmcGroups ? 1 : Math.random()) *
Math.random() *
(pmcMaxGroupSize - 1)
);
if (bossEscortAmount < 0) bossEscortAmount = 0;
const accelerate = totalWaves > 5 && waves.length < totalWaves / 3;
const stage = startingPmcs
? 10
: Math.round(
waves.length < Math.round(totalWaves * 0.5)
? accelerate
? firstHalf / 3
: firstHalf
: secondHalf
);
if (waves.length >= 1) timeStart = timeStart + stage;
// console.log(timeStart, BossEscortAmount);
const side = Math.random() > 0.5 ? "pmcBEAR" : "pmcUSEC";
const BossDifficult = getDifficulty(pmcDifficulty);
waves.push({
BossChance: 9999,
BossDifficult,
BossEscortAmount: bossEscortAmount.toString(),
BossEscortDifficult: "normal",
BossEscortType: side,
BossName: side,
BossPlayer: false,
BossZone: bossZones.pop() || "",
Delay: 0,
DependKarma: false,
DependKarmaPVE: false,
ForceSpawn: true,
IgnoreMaxBots: true,
RandomTimeSpawn: false,
Time: timeStart,
Supports: null,
TriggerId: "",
TriggerName: "",
spawnMode: ["regular", "pve"],
});
maxSlotsReached -= 1 + bossEscortAmount;
if (maxSlotsReached <= 0) break;
}
return waves;
};
export const buildZombie = (
totalWaves: number,
escapeTimeLimit: number,
waveDistribution: number,
BossChance: number = 100
): IBossLocationSpawn[] => {
const averageTime = (escapeTimeLimit * 60) / totalWaves;
const firstHalf = Math.round(averageTime * (1 - waveDistribution));
const secondHalf = Math.round(averageTime * (1 + waveDistribution));
let timeStart = 90;
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = Math.round(1.3 * totalWaves);
while (totalWaves > 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
);
if (waves.length >= 1) timeStart = timeStart + stage;
const main = getRandomZombieType();
waves.push({
BossChance,
BossDifficult: "normal",
BossEscortAmount: "0",
BossEscortDifficult: "normal",
BossEscortType: main,
BossName: main,
BossPlayer: false,
BossZone: "",
Delay: 0,
IgnoreMaxBots: true,
RandomTimeSpawn: false,
Time: timeStart,
Supports: new Array(
Math.round(Math.random() * 4) /* <= 4 AddthistoConfig */
)
.fill("")
.map(() => ({
BossEscortType: getRandomZombieType(),
BossEscortDifficult: ["normal"],
BossEscortAmount: "1",
})),
TriggerId: "",
TriggerName: "",
spawnMode: ["regular", "pve"],
});
maxSlotsReached -= 1 + waves[waves.length - 1].Supports.length;
if (maxSlotsReached <= 0) break;
}
return waves;
};
export interface MapSettings {
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;
}
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;
}
}
};

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,29 @@
import { DependencyContainer } from "tsyringe";
import { IPostSptLoadMod } from "@spt/models/external/IPostSptLoadMod";
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";
class Moar implements IPostSptLoadMod, IPreSptLoadMod {
preSptLoad(container: DependencyContainer): void {
if (enableBotSpawning) setupRoutes(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,57 @@
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(" ");

View File

@ -1,4 +1,3 @@
# This file was automatically generated by Mod Organizer.
+Unsorted_separator
-Version 1.28.6_separator
-SWAG + DONUTS