Compare commits

...

13 Commits

Author SHA1 Message Date
HOME-DESKTOP\kkilb fa01a0fb3e Removed Redundant Stylesheets 2025-01-20 23:57:09 -05:00
HOME-DESKTOP\kkilb 6e51b30da0 Revert "Test Commit"
This reverts commit 06b52867de.
2025-01-20 23:34:35 -05:00
HOME-DESKTOP\kkilb 06b52867de Test Commit 2025-01-20 23:34:01 -05:00
Rage d212aba253 Updated Waypoints 2025-01-20 18:13:20 -05:00
Rage 753c068fb2 Updated Some Mod Configs 2025-01-19 01:43:06 -05:00
Rage 6361dc03d1 Updated Interactable Exfils API 2025-01-17 21:57:14 -05:00
Rage 7c93b1b84f Corrected Version Number 2025-01-17 19:35:50 -05:00
Rage a5ec6f3db1 Disabled, Updated & Removed Some Mods
Added
- Questing Bots (Testing for now, not officially released, so it may be buggy somewhat)

Updated
- Waypoints
- Load Order

Removed
- Headshot Darkness
   - Seems that this just doesn't function w/Fika, which makes sense as it force ends a raid when you die to a headshot.
- Mod Backups

Disabled
- Performance Improvements
   - Seems this is funky still and it's safer to just have it disabled, doesn't really offer that much of a performance increase either.
2025-01-17 19:35:05 -05:00
Rage 24d268c53f Corrected Version Number 2025-01-17 06:28:24 -05:00
Rage cebddea8fc Updated Version Number 2025-01-17 06:26:11 -05:00
Rage fd42f32c6a Adjusted Personality Chances for SAIN PMCs 2025-01-17 06:20:01 -05:00
Rage 3567f74bcd Tweaked Mod Configs 2025-01-16 21:55:51 -05:00
Rage 9ca4e261ab Updated Performance Improvements & Visceral Combat
Both mods have been updated for the latest Fika, hopefully removing some of the jank present from the previous builds.
2025-01-16 19:11:13 -05:00
435 changed files with 43637 additions and 32600 deletions

View File

@ -1,4 +1,4 @@
## Settings file was created by plugin InteractableExfilsAPI v1.5.1 ## Settings file was created by plugin InteractableExfilsAPI v2.0.0
## Plugin GUID: Jehree.InteractableExfilsAPI ## Plugin GUID: Jehree.InteractableExfilsAPI
[1: Settings] [1: Settings]

View File

@ -193,15 +193,15 @@ PmcWaveDistribution = 0.7
## Multiplies wave counts seen in the server's mapConfig.json by this number ## Multiplies wave counts seen in the server's mapConfig.json by this number
# Setting type: Double # Setting type: Double
# Default value: 1 # Default value: 0.8
# Acceptable value range: From 0 to 10 # Acceptable value range: From 0 to 10
ScavWaveQuantity = 1 ScavWaveQuantity = 0.8
## Multiplies wave counts seen in the server's mapConfig.json by this number ## Multiplies wave counts seen in the server's mapConfig.json by this number
# Setting type: Double # Setting type: Double
# Default value: 1 # Default value: 0.8
# Acceptable value range: From 0 to 10 # Acceptable value range: From 0 to 10
PmcWaveQuantity = 1 PmcWaveQuantity = 0.8
[3.Debug] [3.Debug]

View File

@ -0,0 +1,186 @@
## Settings file was created by plugin DanW-QuestingBots v0.9.0
## Plugin GUID: com.DanW.QuestingBots
[AI Limiter]
## Improve FPS by minimizing CPU load for AI out of certain ranges
# Setting type: Boolean
# Default value: false
Enable AI Limiting = false
## Allow AI to be disabled for bots that are questing
# Setting type: Boolean
# Default value: true
Enable AI Limiting for Bots That Are Questing = true
## Only allow AI to be disabled for bots that are questing on the selected maps
# Setting type: TarkovMaps
# Default value: Streets
# Acceptable values: Customs, Factory, Interchange, Labs, Lighthouse, Reserve, Shoreline, Streets, Woods, GroundZero, All
# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)
Maps to Allow AI Limiting for Bots That Are Questing = Streets
## These bot types will never be disabled by the AI limiter
# Setting type: BotTypeException
# Default value: SniperScavs, Rogues
# Acceptable values: SniperScavs, Rogues, Raiders, BossesAndFollowers, All
# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)
Bot Types that Cannot be Disabled = SniperScavs, Rogues
## AI will only be disabled if there are at least this number of bots on the map
# Setting type: Int32
# Default value: 15
# Acceptable value range: From 1 to 30
Min Bots to Enable AI Limiting = 15
## AI will only be disabled if it's more than this distance from other questing bots (typically PMC's and player Scavs)
# Setting type: Int32
# Default value: 75
# Acceptable value range: From 25 to 1000
Distance from Bots That Are Questing (m) = 75
## AI will only be disabled if it's more than this distance from a human player. This takes priority over the map-specific advanced settings.
# Setting type: Int32
# Default value: 200
# Acceptable value range: From 50 to 1000
Distance from Human Players (m) = 200
## AI will only be disabled if it's more than this distance from a human player on Customs
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Customs (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Factory
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Factory (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Interchange
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Interchange (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Labs
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Labs (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Lighthouse
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Lighthouse (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Reserve
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Reserve (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Shoreline
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Shoreline (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Streets
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Streets (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on Woods
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on Woods (m) = 1000
## AI will only be disabled if it's more than this distance from a human player on GroundZero
# Setting type: Int32
# Default value: 1000
# Acceptable value range: From 50 to 1000
Distance from Human Players on GroundZero (m) = 1000
[Custom Quest Locations]
## Allow custom quest locations to be saved
# Setting type: Boolean
# Default value: false
Enable Quest Location Saving = false
## Display your current (x,y,z) coordinates on the screen
# Setting type: Boolean
# Default value: false
Display Current Location = false
## Name of the next quest location that will be stored
# Setting type: String
# Default value: Custom Quest Location
Quest Location Name = Custom Quest Location
## Store your current location as a quest location
# Setting type: KeyboardShortcut
# Default value: KeypadEnter
Store New Quest Location = KeypadEnter
[Debug]
## Show information about what each bot is doing
# Setting type: Boolean
# Default value: false
Show Bot Info Overlays = false
## Show the target position for each bot that is questing
# Setting type: Boolean
# Default value: false
Show Bot Path Overlays = false
## Show information about every nearby quest objective location
# Setting type: Boolean
# Default value: false
Show Quest Info Overlays = false
## Include quest markers and information for spawn-search quests like 'Spawn Point Wander' and 'Boss Hunter' quests
# Setting type: Boolean
# Default value: false
Show Quest Info for Spawn-Search Quests = false
## Quest markers and info overlays will only be shown if the objective location is within this distance from you
# Setting type: Int32
# Default value: 100
# Acceptable value range: From 10 to 300
Max Distance (m) to Show Quest Info = 100
## Font Size for Quest Overlays
# Setting type: Int32
# Default value: 16
# Acceptable value range: From 12 to 36
Font Size for Quest Info = 16
[Main]
## Allow bots to quest
# Setting type: Boolean
# Default value: true
Enable Questing = true
## Show additional debug messages to troubleshoot spawning issues
# Setting type: Boolean
# Default value: false
Show Debug Messages for Spawning = false
## Allow bots to sprint while questing. This does not affect their ability to sprint when they're not questing.
# Setting type: Boolean
# Default value: true
Allow Bots to Sprint while Questing = true
## Bots will not be allowed to sprint if they are within this distance from their objective
# Setting type: Int32
# Default value: 3
# Acceptable value range: From 0 to 75
Sprinting Distance Limit from Objectives (m) = 3

View File

@ -69,7 +69,7 @@ Gain multiplier = 1
# Setting type: Single # Setting type: Single
# Default value: 1.07 # Default value: 1.07
# Acceptable value range: From 0 to 2 # Acceptable value range: From 0 to 2
Mask size multiplier = 1.304742 Mask size multiplier = 1.07
## Applies gain multiplier to all NVGs ## Applies gain multiplier to all NVGs
# Setting type: Single # Setting type: Single

View File

@ -16,5 +16,5 @@ Enable keybind = true
## Keybind to quick throw grenades - One of these keybinds must be same as BSG's grenade keybind ## Keybind to quick throw grenades - One of these keybinds must be same as BSG's grenade keybind
# Setting type: KeyboardShortcut # Setting type: KeyboardShortcut
# Default value: G + LeftShift # Default value: G + LeftShift
Quick throw keybind = G + LeftShift Quick throw keybind = G + LeftControl

View File

@ -1,4 +1,4 @@
## Settings file was created by plugin Performance Improvements v0.2.0 ## Settings file was created by plugin Performance Improvements v0.2.3
## Plugin GUID: com.dirtbikercj.performanceImprovements ## Plugin GUID: com.dirtbikercj.performanceImprovements
[Bot Limiter] [Bot Limiter]
@ -40,6 +40,9 @@ Use Experimental Patches(Requires Restart) = true
[Graphics] [Graphics]
## Enables experimental graphic settings in the EFT graphics settings page. (REQUIRES RESTART)
# Setting type: Boolean
# Default value: true
Enable Experimental Graphic Settings = true Enable Experimental Graphic Settings = true
[Scope Resolution] [Scope Resolution]

View File

@ -97,7 +97,7 @@ Ammo Type HUD display = false
# Setting type: Boolean # Setting type: Boolean
# Default value: true # Default value: true
Fire Mode display = false Fire Mode display = true
# Setting type: Boolean # Setting type: Boolean
# Default value: false # Default value: false

View File

@ -38,7 +38,7 @@ Recording Notification = true
## Enable if you are using an SSL Certificate infront of your http server. ## Enable if you are using an SSL Certificate infront of your http server.
# Setting type: Boolean # Setting type: Boolean
# Default value: false # Default value: false
4. TLS = false 4. TLS = true
## [WARNING] Only disable if you want to stop all data from being sent to raid review, requires restart. ## [WARNING] Only disable if you want to stop all data from being sent to raid review, requires restart.
# Setting type: Boolean # Setting type: Boolean

View File

@ -1,4 +1,4 @@
## Settings file was created by plugin DrakiaXYZ-Waypoints v1.6.0 ## Settings file was created by plugin DrakiaXYZ-Waypoints v1.6.2
## Plugin GUID: xyz.drakia.waypoints ## Plugin GUID: xyz.drakia.waypoints
[] []

View File

@ -4,7 +4,7 @@
"Assignment": { "Assignment": {
"Enabled": true, "Enabled": true,
"CanBeRandomlyAssigned": true, "CanBeRandomlyAssigned": true,
"RandomlyAssignedChance": 8.0, "RandomlyAssignedChance": 20.0,
"MinLevel": 0.0, "MinLevel": 0.0,
"MaxLevel": 100.0, "MaxLevel": 100.0,
"PowerLevelScaleStart": 0.0, "PowerLevelScaleStart": 0.0,

View File

@ -4,7 +4,7 @@
"Assignment": { "Assignment": {
"Enabled": true, "Enabled": true,
"CanBeRandomlyAssigned": true, "CanBeRandomlyAssigned": true,
"RandomlyAssignedChance": 10.0, "RandomlyAssignedChance": 25.0,
"MinLevel": 0.0, "MinLevel": 0.0,
"MaxLevel": 100.0, "MaxLevel": 100.0,
"PowerLevelScaleStart": 0.0, "PowerLevelScaleStart": 0.0,

View File

@ -4,7 +4,7 @@
"Assignment": { "Assignment": {
"Enabled": true, "Enabled": true,
"CanBeRandomlyAssigned": true, "CanBeRandomlyAssigned": true,
"RandomlyAssignedChance": 10.0, "RandomlyAssignedChance": 15.0,
"MinLevel": 15.0, "MinLevel": 15.0,
"MaxLevel": 100.0, "MaxLevel": 100.0,
"PowerLevelScaleStart": 150.0, "PowerLevelScaleStart": 150.0,

View File

@ -4,7 +4,7 @@
"Assignment": { "Assignment": {
"Enabled": true, "Enabled": true,
"CanBeRandomlyAssigned": true, "CanBeRandomlyAssigned": true,
"RandomlyAssignedChance": 5.0, "RandomlyAssignedChance": 10.0,
"MinLevel": 0.0, "MinLevel": 0.0,
"MaxLevel": 15.0, "MaxLevel": 15.0,
"PowerLevelScaleStart": 0.0, "PowerLevelScaleStart": 0.0,

View File

@ -4,7 +4,7 @@
"Assignment": { "Assignment": {
"Enabled": true, "Enabled": true,
"CanBeRandomlyAssigned": true, "CanBeRandomlyAssigned": true,
"RandomlyAssignedChance": 4.0, "RandomlyAssignedChance": 10.0,
"MinLevel": 0.0, "MinLevel": 0.0,
"MaxLevel": 100.0, "MaxLevel": 100.0,
"PowerLevelScaleStart": 250.0, "PowerLevelScaleStart": 250.0,

View File

@ -39,6 +39,7 @@
"shibdib-shibsexpandedcrafts", "shibdib-shibsexpandedcrafts",
"zSolarint-SAIN-ServerMod", "zSolarint-SAIN-ServerMod",
"Skwizzy-LootingBots-ServerMod", "Skwizzy-LootingBots-ServerMod",
"DanW-SPTQuestingBots",
"acidphantasm-progressivebotsystem", "acidphantasm-progressivebotsystem",
"DewardianDev-MOAR", "DewardianDev-MOAR",
"inory-dynamicgoons", "inory-dynamicgoons",

View File

@ -1,11 +0,0 @@
{
"genericDeathStrings": [
"You are dead."
],
"headshotDeathStrings": [
"You have been shot in the head."
],
"explosionDeathStrings": [
"You have been blown up."
]
}

View File

@ -1,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2025.1.13.0 version=d2025.1.17.0
newestVersion= newestVersion=
category="1," category="1,"
nexusFileStatus=1 nexusFileStatus=1
installationFile=Jehree-InteractableExfilsAPI-1.5.1.zip installationFile=Jehree-InteractableExfilsAPI-2.0.0.zip
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=

View File

@ -5,7 +5,7 @@
"scavDifficulty": 0.4, "scavDifficulty": 0.4,
"scavWaveDistribution": 0.5, "scavWaveDistribution": 0.5,
"scavWaveQuantity": 1, "scavWaveQuantity": 0.8,
"startingPmcs": false, "startingPmcs": false,
@ -14,7 +14,7 @@
"allOpenZones": false, "allOpenZones": false,
"pmcWaveDistribution": 0.7, "pmcWaveDistribution": 0.7,
"pmcWaveQuantity": 1, "pmcWaveQuantity": 0.8,
"zombiesEnabled": false, "zombiesEnabled": false,
"zombieWaveDistribution": 0.5, "zombieWaveDistribution": 0.5,

View File

@ -1,12 +0,0 @@
{
"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

@ -1,64 +0,0 @@
{
"live-like": {},
"more-scavs": {
"moreScavGroups": true,
"scavMaxGroupSize": 5,
"scavWaveQuantity": 1.2
},
"more-pmcs": {
"scavWaveDistribution": 0.4,
"morePmcGroups": true,
"pmcMaxGroupSize": 5,
"pmcWaveQuantity": 1.2
},
"more-scavs-and-pmcs": {
"scavWaveDistribution": 0.4,
"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,
"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

@ -1,63 +0,0 @@
{
"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

@ -1,50 +0,0 @@
{
"enableBotSpawning": true,
"pmcDifficulty": 0.6,
"scavDifficulty": 0.4,
"scavWaveDistribution": 0.5,
"scavWaveQuantity": 1.0,
"startingPmcs": false,
"playerOpenZones": false,
"pmcOpenZones": true,
"allOpenZones": false,
"pmcWaveDistribution": 0.8,
"pmcWaveQuantity": 1.0,
"zombiesEnabled": true,
"zombieWaveDistribution": 0.2,
"zombieWaveQuantity": 1,
"zombieHealth": 0.5,
"maxBotCap": 25,
"maxBotPerZone": 5,
"moreScavGroups": false,
"morePmcGroups": false,
"pmcMaxGroupSize": 5,
"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

@ -1,110 +0,0 @@
{
"customs": {
"spawnMinDistance": 30,
"pmcWaveCount": 14,
"scavWaveCount": 21,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneDormitory"
],
"pmcHotZones": [
"ZoneDormitory"
]
},
"factoryDay": {
"spawnMinDistance": 20,
"maxBotCapOverride": 12,
"maxBotPerZoneOverride": 10,
"pmcWaveCount": 10,
"scavWaveCount": 10,
"zombieWaveCount": 6
},
"factoryNight": {
"spawnMinDistance": 20,
"maxBotCapOverride": 12,
"maxBotPerZoneOverride": 10,
"pmcWaveCount": 10,
"scavWaveCount": 10,
"zombieWaveCount": 6
},
"interchange": {
"spawnMinDistance": 40,
"pmcWaveCount": 16,
"scavWaveCount": 32,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneCenterBot",
"ZoneCenter"
]
},
"laboratory": {
"spawnMinDistance": 20,
"pmcWaveCount": 12,
"scavWaveCount": 0,
"zombieWaveCount": 12
},
"lighthouse": {
"spawnMinDistance": 40,
"pmcWaveCount": 14,
"scavWaveCount": 22,
"zombieWaveCount": 10,
"scavHotZones": [
"Zone_LongRoad",
"Zone_LongRoad"
]
},
"rezervbase": {
"spawnMinDistance": 40,
"pmcWaveCount": 14,
"scavWaveCount": 26,
"zombieWaveCount": 9,
"scavHotZones": [
"ZoneRailStrorage"
],
"pmcHotZones": [
"ZoneBarrack"
]
},
"shoreline": {
"spawnMinDistance": 40,
"pmcWaveCount": 16,
"scavWaveCount": 32,
"zombieWaveCount": 12,
"scavHotZones": [
"ZoneSanatorium1"
],
"pmcHotZones": [
"ZoneSanatorium2"
]
},
"tarkovstreets": {
"spawnMinDistance": 40,
"pmcWaveCount": 16,
"scavWaveCount": 28,
"zombieWaveCount": 13
},
"woods": {
"spawnMinDistance": 40,
"pmcWaveCount": 16,
"scavWaveCount": 28,
"zombieWaveCount": 10,
"scavHotZones": [
"ZoneWoodCutter"
],
"pmcHotZones": [
"ZoneWoodCutter"
]
},
"gzLow": {
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 18,
"zombieWaveCount": 9
},
"gzHigh": {
"spawnMinDistance": 30,
"pmcWaveCount": 12,
"scavWaveCount": 18,
"zombieWaveCount": 9
}
}

View File

@ -1,25 +0,0 @@
{
"name": "MOAR",
"version": "2.6.7",
"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

@ -1,11 +0,0 @@
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

@ -1,168 +0,0 @@
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

@ -1,158 +0,0 @@
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 { 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, config);
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

@ -1,277 +0,0 @@
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.
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 --- `
);
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);
// 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

@ -1,88 +0,0 @@
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 &&
!BotZoneName.includes("snipe") &&
(Categories.includes("Player") || Categories.includes("All")) &&
!BotZoneName.includes("BotZoneGate")
)
.map(({ BotZoneName, ...rest }) => {
return BotZoneName;
})
),
]);
// Make labs have only named zones
if (map === "laboratory") {
pmcZones = new Array(10).fill(pmcZones).flat(1);
}
const { pmcWaveCount } = mapConfig[map];
const escapeTimeLimitRatio = Math.round(
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
const totalWaves = Math.round(
pmcWaveCount * config.pmcWaveQuantity * escapeTimeLimitRatio
);
const numberOfZoneless = totalWaves - pmcZones.length;
if (numberOfZoneless > 0) {
const addEmpty = new Array(numberOfZoneless).fill("");
pmcZones = shuffle<string[]>([...pmcZones, ...addEmpty]);
}
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 waves = buildPmcWaves(
totalWaves,
timeLimit,
config,
pmcZones,
pmcHotZones
);
locationList[index].base.BossLocationSpawn = [
...waves,
...locationList[index].base.BossLocationSpawn,
];
}
}

View File

@ -1,218 +0,0 @@
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, DelayToCanSpawnSec, BotZoneName, Sides }) =>
!Categories.includes("Boss") &&
Sides[0] === "Savage" &&
(BotZoneName?.toLowerCase().includes("snipe") ||
DelayToCanSpawnSec > 40)
)
.map(({ BotZoneName }) => BotZoneName || "")
);
if (sniperLocations.size) {
locationList[index].base.MinMaxBots = [
{
WildSpawnType: "marksman",
max: sniperLocations.size * 5,
min: sniperLocations.size,
},
];
}
let scavZones = shuffle<string[]>([
...new Set(
[...locationList[index].base.SpawnPointParams]
.filter(
({ Categories, Sides, BotZoneName }) =>
!!BotZoneName &&
Categories.includes("Bot") &&
(Sides.includes("Savage") || Sides.includes("All"))
)
.map(({ BotZoneName }) => BotZoneName)
.filter((name) => !sniperLocations.has(name))
),
]);
const { scavWaveCount } = mapConfig[map];
const escapeTimeLimitRatio = Math.round(
locationList[index].base.EscapeTimeLimit / defaultEscapeTimes[map]
);
// Scavs
const scavTotalWaveCount = Math.round(
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),
0.5,
WildSpawnType.MARKSMAN,
0.7,
false,
2,
[],
shuffle([...sniperLocations]),
80,
true,
true
);
if (snipersHaveFriends)
snipers = snipers.map((wave) => ({
...wave,
...(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

@ -1,80 +0,0 @@
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

@ -1,204 +0,0 @@
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

@ -1,139 +0,0 @@
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[],
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,
Infiltration,
},
innerIndex
) => {
if (
!Categories.includes("Boss") &&
!BotZoneName?.toLowerCase().includes("snipe") &&
DelayToCanSpawnSec < 41
) {
// 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].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

@ -1,452 +0,0 @@
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 + 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: 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 = (
pmcTotal: number,
escapeTimeLimit: number,
config: typeof _config,
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,
startingPmcs,
morePmcGroups,
pmcWaveDistribution,
} = config;
const averageTime = (escapeTimeLimit * 0.8) / pmcTotal;
const waves: IBossLocationSpawn[] = [];
let maxSlotsReached = pmcTotal;
while (pmcTotal > 0) {
let bossEscortAmount = Math.round(
(morePmcGroups ? 1 : Math.random()) *
Math.random() *
(pmcMaxGroupSize - 1)
);
if (bossEscortAmount < 0) bossEscortAmount = 0;
// 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;
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;
}
// console.log(
// escapeTimeLimit,
// waves.map(({ Time }) => Time)
// );
return waves;
};
export const buildZombie = (
totalWaves: number,
escapeTimeLimit: number,
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));
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

@ -1,28 +0,0 @@
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

@ -1,160 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,57 +0,0 @@
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,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2024.12.18.0 version=d2025.1.16.0
newestVersion= newestVersion=
category="2," category="2,"
nexusFileStatus=1 nexusFileStatus=1
installationFile=PerformanceImprovements.7z installationFile=PerformanceImprovements-Release-0.2.3-e8fd7fc3.zip
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=

View File

@ -1,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2025.1.13.0 version=d2025.1.16.0
newestVersion= newestVersion=
category="1," category="1,"
nexusFileStatus=1 nexusFileStatus=1
installationFile=DewardianDev-MOAR-2.6.7.zip installationFile=DanW-SPTQuestingBots.zip
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=
@ -15,7 +15,7 @@ url=
hasCustomURL=false hasCustomURL=false
lastNexusQuery= lastNexusQuery=
lastNexusUpdate= lastNexusUpdate=
nexusLastModified=2024-12-16T06:46:30Z nexusLastModified=2025-01-18T00:31:47Z
nexusCategory=0 nexusCategory=0
converted=false converted=false
validated=false validated=false

View File

@ -0,0 +1,390 @@
You're no longer the only PMC running around placing markers and collecting quest items. The bots have transcended and are coming for you...
**This mod may have a performance impact**, but it should be minimal starting with the 0.5.0 release. If you notice performance problems, please try using the built-in AI limiter.
**---------- Mod Compatibility ----------**
**REQUIRES:**
* [BigBrain](https://hub.sp-tarkov.com/files/file/1219-bigbrain/)
* [Waypoints](https://hub.sp-tarkov.com/files/file/1119-waypoints-expanded-bot-patrols-and-navmesh/)
**Highly Recommended:**
* [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) (3.2.0 or later recommended)
* [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/) (1.4.0 or later recommended)
**NOT compatible with:**
* [AI Limit](https://hub.sp-tarkov.com/files/file/793-ai-limit/) or any other mods that disable AI in a similar manner. This mod relies on the AI being active throughout the entire map. **Starting with 0.2.10, Questing Bots has its own AI Limiter feature.** Please see the tab below for more information.
* [Traveler](https://hub.sp-tarkov.com/files/file/1212-traveler/) (You MUST use another mod like [SWAG + DONUTS](https://hub.sp-tarkov.com/files/file/878-swag-donuts-dynamic-spawn-waves-and-custom-spawn-points/) to manage bot spawning when using this mod. Otherwise, bots will spawn right in front of you.)
**Compatible with:**
* [SWAG + DONUTS](https://hub.sp-tarkov.com/files/file/878-swag-donuts-dynamic-spawn-waves-and-custom-spawn-points/)
* [Late to the Party](https://hub.sp-tarkov.com/files/file/1099-late-to-the-party/)
* **Fika** (Requires client version 0.9.8962.33287 or later)
**NOTE: Please disable the bot-spawning system in this mod if you're using other mods that manage spawning! Otherwise, there will be too many bots on the map. The bot-spawning system in this mod will be automatically disabled** if any of the following mods are detected:
* [SWAG + DONUTS](https://hub.sp-tarkov.com/files/file/878-swag-donuts-dynamic-spawn-waves-and-custom-spawn-points/)
* [MOAR](https://hub.sp-tarkov.com/files/file/1059-moar-bots-spawning-difficulty/)
* [Better Spawns Plus](https://hub.sp-tarkov.com/files/file/1002-better-spawns-plus/)
**---------- Overview ----------**
There are two main components of this mod: adding an objective system to the AI and directly controlling PMC and player-Scav spawning to mimic live Tarkov.
**Objective System:**
Instead of simply patrolling their spawn areas, bots will now move around the map to perform randomly-selected quest objectives. By default this system is only active for PMC's and player Scavs, but it can be enabled for normal Scavs and bosses if you want an extra challenge.
After spawning (regardless of when this occurs during the raid), bots will immediately begin questing, and there are only a few conditions that will cause them to stop questing:
* They got stuck too many times
* Their health is too low and they're unable to heal
* They're over-encumbered
* They're trying to extract (using [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/))
Otherwise, they will only temporarily stop questing for the following reasons:
* They're currently or were just recently in combat
* They heard a suspicious noise
* They recently completed an objective
* They're checking for or have found loot
* Their health is too low or they have blacked limbs (besides arms)
* Their energy or hydration is too low
* They have followers that are too far from them
There are several types of quests available to each bot:
* **EFT Quests:** Bots will go to locations specified in EFT quests for placing markers, collecting/placing items, killing other bots, etc. Bots can also use quests added by other mods.
* **Spawn Rush:** At the beginning of the raid, bots that are within a certain distance of you will run to your spawn point. Only a certain number of bots are allowed to perform this quest, and they won't always select it. This makes PVP-focused maps like Factory even more challenging.
* **Boss Hunter:** Bots will search zones in which bosses are known to spawn. They will only be allowed to select this quest at the beginning of the raid (within the first 5 minutes by default) and if they're a high enough level.
* **Airdrop Chaser:** Bots will run to the most recent airdrop if it's close to them (within 500m by default). They will be allowed to select this quest within **questing.bot_quests.airdrop_bot_interest_time** seconds (420s by default) of the airdrop crate landing.
* **Spawn Point Wandering:** Bots will wander to different spawn points around the map. This is used as a last resort in case the bot doesn't select any other quests. This quest is currently disabled by default because it should no longer be needed with the quest variety offered in the 0.4.0 and later releases.
* **"Standard" Quests:** Bots will go to specified locations around the map. They will prioritize more desirable locations for loot and locations that are closer to them. These also include some sniping and camping quests on all maps, so be careful!
* **"Custom" Quests:** You can create your own quests for bots using the templates for "standard" quests. None are provided by default.
**PMC and Player-Scav Spawning Systems:**
At the beginning of the raid, PMC's will spawn around the map at actual EFT PMC spawn points. The spawning system will try to separate spawn points as much as possible, but spawn killing is still entirely possible just like it is in live Tarkov. The total number of PMC's that can spawn is a random number between the minimum and maximum player count for the map (other mods can change these values). However, you count as one of those PMC's for PMC raids. That number will be reduced if you spawn into the map late for a Scav run. The PMC difficulty is set by your raid settings in EFT.
Starting with the 0.4.0 release, player Scavs will also spawn throughout the raid. Each group of player Scavs will be assigned a minimum spawn time that is generated using SPT's raid-time-reduction settings for Scav raids. This mod will use SPT's weighting settings for choosing when player Scavs will spawn into each location, it will add some randomness, and then it will generate a spawn schedule for all player-Scav spawns. Effectively, this means that player Scavs are most likely to spawn toward the middle and last third of raids. They're unlikely to spawn toward the very beginning or very end of them. Player Scavs can spawn at any EFT PMC or player-Scav spawn point on the map, and player-Scav bot difficulty is set by your raid settings in EFT.
Only a certain (configurable) number of initial PMC's will spawn at the beginning of the raid, and the rest will spawn as the existing ones die. PMC's that spawn after the initial wave can spawn anywhere that is far enough from you and other bots (at any EFT spawn point for PMC's or player Scavs). After all PMC's have spawned, player Scavs will be allowed to spawn. The maximum total number of PMC's and player Scavs on the map cannot exceed the number of initial PMC's (determined by **bot_spawns.max_alive_bots**). For example, Customs allows 10-12 players, but Questing Bots only allows 7 to be on the map at the same time by default. That means 7 PMC's will spawn immediately as the raid starts, and as some of them die others will spawn to replace them. After all PMC's have spawned and less than 7 are remaining, player Scavs will be allowed to spawn. If there are 5 PMC's left on the map, 2 player Scavs will be allowed to spawn. If there are 3 PMC's left on the map, 4 player Scavs will be allowed to spawn, and so on. Even if most total PMC's have died, player Scavs will not be allowed to spawn earlier than their scheduled spawn times.
A new feature of the 0.4.0 and later releases is an advanced spawning system that tricks EFT into thinking that PMC's and player Scavs are human players. This makes PMC's and player Scavs not count toward the bot cap for the map, so they shouldn't impede normal EFT bot spawns for normal Scavs and bosses. It also prevents PMC's and player Scavs from counting toward the maximum bot counts for each zone on the map, and this allows normal Scavs to spawn like they would in live EFT. Without this system, all initial bosses must be configured to spawn first (which is a config option in this mod) or EFT may suppress them due to the high number of bots on the map. To accomodate the large initial PMC wave and still allow Scavs and bosses to spawn, the bot cap can be optionally increased (which is also a config option in this mod) if you're not using the advanced spawning system.
**---------- Bot Quest-Selection Algorithm Overview ----------**
When each bot spawns, this mod finds the furthest extract from them and references it when selecting new quests for the bot. If the bot ever comes close enough to that extract while traversing the map, this happens again; a new extract is selected for it that is the furthest one from its current location. This continues until the bot extracts or dies. This extract is NOT used when bots extract via [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/); it is only used when this mod selects new quests for the bot.
Before selecting a quest for a bot, all quests are first filtered to ensure they have at least one valid location on the map and the bot is able to accept the quest (it's not blocked by player level, etc.). Then, the following metrics are generated for every valid quest:
1) The distance between the bot and each objective for the quest with some randomness applied (by **questing.bot_quests.distance_randomness**). This value is then normalized based on the furthest objective from the bot (for any valid quest), and finally it's multiplied by a weighting factor defined by **questing.bot_quests.distance_weighting** (1 by default).
2) A "desirability" rating for each quest, which is the desirability rating assigned to the quest but with some randomness applied (by **questing.bot_quests.desirability_randomness**). This value is divided by 100 and then multiplied by a weighting factor defined by **questing.bot_quests.desirability_weighting** (1 by default). There are modifiers that can be applied to the desirability ratings of quests including **questing.bot_quests.desirability_camping_multiplier**, **questing.bot_quests.desirability_sniping_multiplier**, and **questing.bot_quests.desirability_active_quest_multiplier**. More information about these settings can be found in the README or GitHub repo for this mod.
3) The angle between two vectors: the vector between the bot and its selected extract (described above), and the vector between the bot and each objective for the quest. If the quest objective is in the same direction as the bot's selected extract, this angle will be small. If the bot has to move further from its selected extract, this angle will be large. Angles that are below a certain threshold (90 deg by default) are reduced down to 0 deg. This value is divided by 180 deg minus the threshold just described (90 deg by default), and finally it's multiplied by a weighting factor defined by **questing.bot_quests.exfil_direction_weighting**, which is different for every map.
These three metrics are then added together, and the result is the overall score for the corresponding quest. The quest with the highest score is assigned to the bot. If for some reason the bot is unable to perform that quest, it selects the one with the next-highest score, and so on. If no quests are available for the bot to select, this mod will first try allowing the bot to perform repeatable quests early (before the **questing.bot_questing_requirements.repeat_quest_delay** delay expires). If there are no available repeatable quests, this mod will then attempt to make the bot extract via [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/). Finally, this mod will stop assigning new quests to the bot.
**---------- How to Add Custom Quests ----------**
To add custom quests to a map, first create a *user\mods\DanW-SPTQuestingBots-#.#.#\quests\custom* directory if it doesn't already exist. Then, create a file for each map for which you want to add custom quests. The file name should exactly match the corresponding file in the *user\mods\DanW-SPTQuestingBots-#.#.#\quests\standard* directory (case sensitive).
The three major data structures are:
* **Quests**: A quest is a collection of at least one quest objective, and objectives can be placed anywhere on the map. Objectives can be completed in any order.
Quests have the following properties:
* **repeatable**: Boolean value indicating if the bot can repeat the quest later in the raid. This is typically used for quests that are PvP or PvE focused, where a bot might want to check an area again later in the raid for more enemies.
* **isCamping**: If the quest should be considered to be a camping quest
* **isSniping**: If the quest should be considered to be a sniping quest
* **pmcsOnly**: Only PMC's will be allowed to select the quest
* **minLevel**: Only bots that are at least this player level will be allowed to select the quest
* **maxLevel**: Only bots that are at most this player level will be allowed to select the quest
* **maxBots**: The maximum number of bots that can be performing the quest at the same time.
* **maxBotsInGroup**: If the number of bots in a group exceeds this value, the boss of the group will not be allowed to select this quest.
* **desirability**: A rating roughly equivalent to a percentage that indicates "how much" bots want to select this quest. Quests with high desirability ratings (50+) are very likely to be selected, and quests with low desirability ratings (<20) are unlikely to be selected unless the bot is close to them.
* **minRaidET**: The quest can only be selected if at least this many seconds have elapsed in the raid. This is based on the overall raid time, not the time after you spawn. For example, if you set **maxRaidET=60** for a quest and you spawn into a Factory raid with 15 minutes remaining, this quest will never be used because 300 seconds has already elapsed in the overall raid. This property is typically used to make bots rush to locations like Dorms when the raid begins.
* **maxRaidET**: The quest can only be selected if more more than this many seconds have elapsed in the raid. See **minRaidET** for more information.
* **maxTimeOnQuest**: The maximum time (in seconds) that a bot is allowed to continue doing the quest after it completes at least one of its objectives. This is intended to add more variety to bot questing instead of having them stay in one area for a long period of time. By default, this is 300 seconds.
* **canRunBetweenObjectives**: Boolean indicating if bots are allowed to sprint to the next objective in the quest after it completes at least one objective. This is intended to be used in areas where stealth is more important (typically in buildings). This is **true** by default.
* **requiredSwitches**: A dictionary of the switches that must be in a specific position bot bots to perform the quest. The dictionary key is the ID of the switch, and the value is a boolean indicating if the switch must be open (actuated). If the dictionary is empty, no switches will be checked.
* **forbiddenWeapons**: An array of weapon classes that cannot be used to perform this quest. In order for the bot to perform the quest, it must have at least one weapon that is not in the weapon classes listed in the array. The only available options for the array elements are (case-sensitive):
* assaultCarbine
* assaultRifle
* grenadeLauncher
* machinegun
* marksmanRifle
* pistol
* shotgun
* smg
* sniperRifle
* specialWeapon
* **name**: The name of the quest. This doesn't have to be unique, but it's best if it is to avoid confusion when troubleshooting.
* **waypoints**: An array of waypoints that can be used to assist bots with finding paths to the quest's objectives. Each waypoint is an (x, y, z) coordinate.
* **objectives**: An array of the objectives in the quest. Bots can complete objectives in any order.
* **Objectives**: An objective is a collection of at least one step. An objective represents a list of actions that the bot must complete in the order you specify.
Quest objectives have the following properties:
* **repeatable**: Boolean value indicating if the bot can repeat the quest objective later in the raid. This is typically used for quests are are PvP or PvE focused, where a bot might want to check an area again later in the raid for more enemies.
* **minDistanceFromBot**: The objective will only be selected if the bot is at least this many meters away from it.
* **maxDistanceFromBot**: The objective will only be selected if the bot is no more than this many meters away from it.
* **maxRunDistance**: If bots get within this radius (in meters) of the position for the first step in the objective, they will no longer be allowed to sprint. This is intended to be used in areas where stealth is more important (typically in buildings). This is **0** by default.
* **lootAfterCompleting**: The only valid options for this are "Default", "Force", and "Inhibit" (case-sensitive). If "Force" is used, Questing Bots will try invoking [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/) to make the bot scan for loot immediately after completing each step in the objective. However, bots will not be able to loot if they're in combat or have no available space. If "Inhibit" is used, this mod will try invoking [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/) to prevent the bot from looting until after it selects another quest objective. [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/) version 1.2.1 or later is required for either option to work. [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) 2.1.9 or later is required for Questing Bots to properly force bots to loot.
* **doorIDToUnlock**: If specified, the door with this ID must be unlocked before bots are allowed to proceed with any steps in the objective. The door's state will be checked when the bot is within **questing.unlocking_doors.search_radius** meters of the objective's first step position.
* **fixedPositionToUnlockDoor**: If **doorIDToUnlock** is specified, this field can optionally be added to specify an exact position where bots will stand to open the door. If this field is omitted, the interaction position will be determined programmatically.
* **steps**: An array of the steps in the objective. Bots will complete the steps exactly in the order you specify.
* **Steps**: A step is an individual component of an objective.
Quest objective steps have the following properties:
* **position**: The position on the map that the bot will try to reach
* **lookToPosition**: The position on the map that bots will look toward after they reach **position**. This is only used for the **Ambush** and **Snipe** step types described below.
* **waitTimeAfterCompleting**: The time the bot must wait after completing the step before it will be allowed to quest again. The default value for this field is defined by **questing.default_wait_time_after_objective_completion**.
* **stepType**: The only valid options for this are (case-sensitive):
* **MoveToPosition**: Bots are instructed to go to **position**. If possible, they're allowed to unlock doors that block their path.
* **HoldAtPosition**: Bots are instructed to remain within **maxDistance** meters of **position** and stay alert for a random time between the min and max values of **minElapsedTime**.
* **Ambush**: Bots are instructed to go to **position**, stand still, and look at **lookToPosition** for a random time between the min and max values of **minElapsedTime**. This is used to simulate camping quests.
* **Snipe**: The same as **Ambush**, but bots can be interrupted if they hear suspicious noises
* **ToggleSwitch**: Bots are instructed to go to **position** and toggle the switch defined by **switchID**.
* **RequestExtract**: This mod will try to instruct bots to extract via [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/).
* **CloseNearbyDoors**: Bots are instructed to close all doors within **maxDistance** meters of **position**
If **stepType** is omitted, **MoveToPosition** is used by default.
* **minElapsedTime**: The range of minimum and maximum time that a bot will perform certain types of objective steps (namely **HoldAtPosition**, **Ambush**, and **Snipe**).
* **switchID**: If **stepType="ToggleSwitch"**, this is the ID of the switch the bot should open. It needs to exactly match one of the results in the "Found switches" debug message shown in the bepinex console when loading into the map.
* **maxDistance**: If **stepType="HoldAtPosition"**, this is the maximum distance (in meters) bots will be allowed to wander from **position** for the objective step. If **stepType="CloseNearbyDoors"**, bots will close all doors within this radius of **position** (in meters).
* **chanceOfHavingKey**: The chance that bots will have keys to unlock any doors that are blocking their paths to this objective step. This overrides the default chance of having keys defined by **questing.unlocking_doors.default_chance_of_bots_having_keys** or **questing.bot_quests.eft_quests.chance_of_having_keys**.
**Tips and Tricks**
* Objectives should be sparsely placed on the map. Since bots take a break from questing after each objective is completed, they will wander around the area (for an unknown distance) before continuing the quest. If you place objective positions too close to each other, the bot will unnecessarily run back and forth around the area. As a rule of thumb, place objectives at least 20m from each other.
* If you want a bot to go to several specific positions that are close to each other (i.e. small, adjacent rooms), use multiple steps in a single objective instead of using multiple objectives each with a single step.
* Bots will use the NavMesh to calculate the more efficient path to their objective (using an internal Unity algorithm). They cannot perform complex actions to reach objective locations, so avoid placing objective steps on top of objects (i.e. inside truck beds) or in areas that are difficult to reach. Bots will not know to crouch or jump to get around obstacles.
* Quest waypoints can help bots find paths to objectives in labyrinthic areas, but adding too many to a quest may impact performance.
**---------- Bot Group Spawning System ----------**
* Spawn chances for various group sizes are configurable. By default, solo spawns are most likely, but 2-man and 3-man groups will be commonly seen. 4-man and 5-man groups are rare but possible.
* EFT will assign one bot in the group to be a "boss", and the boss will select the quest to perform. All other bots in the group will follow the boss.
* If any group members stray too far from the boss, the boss will stop questing and attempt to regroup
* If any member of the group engages in combat or hears a suspicious noise, all other members will stop questing (or following) and engage in combat too.
* If the boss is allowed to sprint, so are its followers and vice versa.
* If the boss of a bot group dies, EFT will automatically assign a new one from the remaining members
* Followers are only allowed to loot if they remain within a certain distance from the boss
**---------- AI Limiter System ----------**
Since normal AI Limit mods will disable bots that are questing (which will prevent them from exploring the map), this mod has its own AI Limiter with the following features:
* AI Limiting must be explicitly enabled in the F12 menu.
* AI Limiting must be explicitly enabled for bots that are questing for each map. By default, questing bots will only be disabled on Streets.
* Bots will only be allowed to be disabled if they are beyond a certain distance (200m by default) from human players. There are individual map-specific distances that can be adjusted by enabling advanced settings in the F12 menu, but the global setting will take priority. In other words, the actual limiting distance is the minimum of the two (the map-specific value and the global value). By default, all map-specific distances are set to 1000m to avoid confusion when only the global setting is adjusted.
* Bots will only be allowed to be disabled if they are beyond a certain distance (75m by default) from other bots that are questing (and not disabled)
**---------- Configuration Options in *config.json* ----------**
**Main Options:**
* **enabled**: Completely enable or disable all featues of this mod.
* **debug.enabled**: Enable debug mode.
* **debug.always_spawn_pmcs**: If **true**, PMC's will spawn even when you select "None" for the amount of bots when starting a raid.
* **debug.always_spawn_pscavs**: If **true**, player Scavs will spawn even when you select "None" for the amount of bots when starting a raid.
* **debug.show_zone_outlines**: If **true**, EFT quest zones will be outlined in light blue. Target locations for each zone will have light-blue spherical outlines.
* **debug.show_failed_paths**: If **true**, whenever a bot gets stuck its target path will be drawn in red.
* **debug.show_door_interaction_test_points**: If **true**, the positions tested when determining where bots should travel to unlock doors will have spherical outlines. If the a valid NavMesh position cannot be found for the test point, the outline color will be white. If a valid NavMesh position is found but the bot cannot access that point, the outline color will be yellow. If a valid NavMesh position is found and the bot can access that point, the outline color will be magenta. The position selected for the bot will be shown with a green outline.
* **max_calc_time_per_frame_ms**: The maximum amount of time (in milliseconds) the mod is allowed to run quest-generation and PMC-spawning procedures per frame. By default this is set to **5** ms, and delays of <15 ms are basically imperceptible.
* **chance_of_being_hostile_toward_bosses.scav**: The chance that Scavs will be hostile toward all bosses on the map. This is **0%** by default.
* **chance_of_being_hostile_toward_bosses.pscav**: The chance that player Scavs will be hostile toward all bosses on the map even if the bosses aren't hostile toward them (i.e. Rogues are not initially hostile toward player Scavs). This is **20%** by default.
* **chance_of_being_hostile_toward_bosses.pmc**: The chance that PMC's will be hostile toward all bosses on the map even if the bosses aren't hostile toward them (i.e. Rogues are not initially hostile toward USEC PMC's). This is **80%** by default.
* **chance_of_being_hostile_toward_bosses.boss**: The chance that bosses will be hostile toward all other bosses on the map. This is **0%** by default.
**Questing Options:**
* **questing.enabled**: Completely enable or disable questing.
* **questing.bot_pathing_update_interval_ms**: The interval (in milliseconds) at which each bot will recalculate its path to its current objective. If this value is very low, performance will be impacted. If this value is very high, the bot will not react to obstacles changing as quickly (i.e. doors being unlocked). By default, this is **100** ms.
* **questing.brain_layer_priorities.xxx**: The priority numbers assigned to the "brain" layers for this mod. **Do not change these unless you know what you're doing!**
* **questing.quest_selection_timeout**: If a quest cannot be selected for a bot after trying for this amount of time (in seconds), the mod will give up and write an error message.
* **questing.btr_run_distance**: Override value (in meters) for the EFT setting that makes bots "avoid danger" when they're near the BTR. The default EFT value is 40m, and the default value of this setting is **10** m.
* **questing.allowed_bot_types_for_questing.scav**: If Scavs are allowed to quest. This is **false** by default.
* **questing.allowed_bot_types_for_questing.pscav**: If player Scavs are allowed to quest. This is **true** by default.
* **questing.allowed_bot_types_for_questing.pmc**: If PMC's are allowed to quest. This is **true** by default.
* **questing.allowed_bot_types_for_questing.boss**: If bosses are allowed to quest. This is **false** by default. Boss followers will never be allowed to quest.
* **questing.allowed_bot_types_for_questing.min/max**: The minimum and maximum time (in seconds) that a bot will wait after ending combat before it's allowed to quest again. After the bot is no longer actively engaged in combat, it will continue its quest following a random delay between these two values. This is to allow the bot to search for threats before blindly running toward its objective.
* **questing.stuck_bot_detection.distance**: The minimum distance (in meters) the bot must travel over a period of **questing.stuck_bot_detection.time** seconds while questing or the mod will assume it's stuck. This is **2** m by default.
* **questing.stuck_bot_detection.time**: The maximum time (in seconds) the bot is allowed to move less than **questing.stuck_bot_detection.distance** meters while questing or the mod will assume it's stuck. This is **20** s by default.
* **questing.stuck_bot_detection.stuck_bot_remedies.enabled**: If bots will try jumping or vaulting if they get stuck while questing (until they get stuck for **questing.stuck_bot_detection.time** or more seconds). This is **true** by default.
* **questing.stuck_bot_detection.stuck_bot_remedies.min_time_before_jumping**: The minimum time (in seconds) the bot is allowed to move less than **questing.stuck_bot_detection.distance** meters while questing before it will try jumping to prevent itself from being stuck. This is **6** s by default.
* **questing.stuck_bot_detection.stuck_bot_remedies.jump_debounce_time**: The minimum time (in seconds) between a bot's jump attempts to prevent itself from being stuck. This is **4** s by default.
* **questing.stuck_bot_detection.stuck_bot_remedies.min_time_before_vaulting**: The minimum time (in seconds) the bot is allowed to move less than **questing.stuck_bot_detection.distance** meters while questing before it will try vaulting to prevent itself from being stuck. This is **8** s by default.
* **questing.stuck_bot_detection.stuck_bot_remedies.vault_debounce_time**: The minimum time (in seconds) between a bot's vault attempts to prevent itself from being stuck. This is **4** s by default.
* **questing.stuck_bot_detection.max_count**: The maximum number of times the bot can be stuck before questing is completely disabled for it. This counter is reset whenever the bot completes an objective. Whenever the bot is assumed to be stuck, a new objective will be selected for it to force it to generate a different path. This is **8** by default.
* **questing.stuck_bot_detection.follower_break_time**: If a boss follower is stuck while trying to follow it, it will take a break for this many seconds (**10** by default).
* **questing.stuck_bot_detection.max_not_able_bodied_time**: If a bot is continuously not able-bodied (typically due to injuries) for this amount of time (in seconds), it will be separated from its group. If it's the boss of its group, a new boss will be selected for that group. This timer is paused when a bot is either in combat or hears suspicious noises. This is **120** s by default.
* **questing.unlocking_doors.enabled.scav**: If questing Scavs are allowed to open locked doors. This is **false** by default.
* **questing.unlocking_doors.enabled.pscav**: If questing player Scavs are allowed to open locked doors. This is **false** by default.
* **questing.unlocking_doors.enabled.pmc**: If questing PMC's are allowed to open locked doors. This is **true** by default.
* **questing.unlocking_doors.enabled.boss**: If questing bosses are allowed to open locked doors. This is **false** by default.
* **questing.unlocking_doors.search_radius**: The distance (in meters) to search around the bot for locked doors. This is **25**m by default.
* **questing.unlocking_doors.max_distance_to_unlock**: The maximum distance (in meters) that a bot is allowed to be from a door in order to unlock it. This is **0.5**m by default. **Do not change this unless you know what you're doing!**
* **questing.unlocking_doors.door_approach_position_search_radius**: The distance (in meters) to search around doors for positions that are on the NavMesh and have complete paths to the bot's current location. This is **0.75**m by default. **Do not change this unless you know what you're doing!**
* **questing.unlocking_doors.door_approach_position_search_offset**: The distance (in meters) to offset the search positions around doors determined by **questing.unlocking_doors.door_approach_position_search_radius**. This is **-0.75**m by default. **Do not change this unless you know what you're doing!**
* **questing.unlocking_doors.pause_time_after_unlocking**: The time (in seconds) bots must wait after unlocking doors before they're allowed to continue with their quests. If this is too low, their pathing will not be updated and they may fail the quest they're currently doing. **Do not change this unless you know what you're doing!**
* **questing.unlocking_doors.debounce_time**: The time (in seconds) bots must wait after selecting a door to unlock before they're allowed to select another one to unlock. This is to prevent bots from rapidly selecting doors instead of allowing them to change objectives. This is **1**s by default.
* **questing.unlocking_doors.default_chance_of_bots_having_keys**: The default chance (in percentage) that bots will have keys for quest locations.
* **questing.min_time_between_switching_objectives**: The minimum amount of time (in seconds) the bot must wait after completing an objective before a new objective is selected for it. This is to allow it to check its surroundings, search for loot, etc. This is **5** s by default.
* **questing.default_wait_time_after_objective_completion**: The default time (in seconds) a bot will wait after completing a quest objective step before it will select a new one. This is **5** s by default.
* **questing.wait_time_before_planting**: If the bot needs to plant an item at a quest location, this is the time (in seconds) it will wait between reaching its target location and beginning to "plant" the required item. This is **1** s by default. If this is much lower than **1** s, there may be strange behavior when the bot transitions into planting its item.
* **questing.quest_generation.navmesh_search_distance_item**: The radius (in meters) around quest items (i.e. the bronze pocket watch) to seach for a valid NavMesh position to use for a target location for creating a quest objective for it. If this value is too low, bots may not be able to generate a complete path to the item. If this value is too high, bots may generate paths into adjacent rooms or to vertical positions on different floors. This is **1.5** m by default.
* **questing.quest_generation.navmesh_search_distance_zone**: The radius (in meters) around target positions in zones (i.e. trigger areas for placing markers) to seach for a valid NavMesh position to use for a target location for creating a quest objective for it. If this value is too low, bots may not be able to generate a complete path to the zone. If this value is too high, bots may generate paths into adjacent rooms or to vertical positions on different floors. This is **1.5** m by default. The target position for a zone is the center-most valid NavMesh position in it. If the zone surrounds multiple floors in a building, the lowest floor is typically used.
* **questing.quest_generation.navmesh_search_distance_spawn**: The radius (in meters) around spawn points to seach for a valid NavMesh position to use for a target location for creating a quest objective for it. If this value is too low, bots may not be able to generate a complete path to the spawn point. If this value is too high, bots may generate paths into adjacent rooms or to vertical positions on different floors. This is **2** m by default.
* **questing.quest_generation.navmesh_search_distance_doors**: The radius (in meters) to search for a valid NavMesh position around the test points used for determining if a bot can unlock a door. If this value is too low, bots may not be able to unlock the door. If this value is too high, bots may generate paths into adjacent rooms or to vertical positions on different floors. This is **0.75** m by default.
* **questing.bot_search_distances.objective_reached_ideal**: Bots must travel within this distance (in meters) of their target objective positions for the objective to be considered successfully completed. This is **0.25** m by default.
* **questing.bot_search_distances.objective_reached_navmesh_path_error**: The maximum distance (in meters) that the end of a bot's calculated path can be from its target objective position before the objective is considerd unreachable. This is **2** m by default.
* **questing.bot_search_distances.max_navmesh_path_error**: If a complete path cannot be generated to a bot's target objective position, it will try to get within this radius (in meters) of it anyway. This is to simulate situations like bots checking if a door is unlocked when it doesn't have the key. This is **10** m by default.
* **questing.bot_pathing.max_start_position_discrepancy**: The minimum distance (in meters) between the bot's position and the start of its path above which its path will be recalculated if there is a difference between its current target position and the target position for its quest. **Do not change this unless you know what you're doing!**
* **questing.bot_pathing.incomplete_path_retry_interval**: If a bot's path to its objective is incomplete, the path will be recalculated at this interval (in seconds) until a complete path is found. This is **5** s by default.
* **questing.bot_questing_requirements.exclude_bots_by_level**: Each quest has a minimum and maximum player level assigned to it. If this option is **true** (which is the default setting), bots will only be allowed to select a quest if its player level is within this range. This prevents low-level bots from selecting end-game quests and vice versa.
* **questing.bot_questing_requirements.repeat_quest_delay**: The minimum delay (in seconds) after a bot stops performing objectives for a repeatable quest before it's allowed to repeat the quest. This is **360** s by default.
* **questing.bot_questing_requirements.max_time_per_quest**: The maximum amount of time (in seconds) that bots are allowed to perform objectives for the same quest. This is to encourage questing diversity for bots and to deter them from remaining in the same area for a long time. This is **300** s by default.
* **questing.bot_questing_requirements.min_hydration**: The minimum hydration level permitted for bots or they will not be allowed to quest. This is **20** by default.
* **questing.bot_questing_requirements.min_energy**: The minimum energy level permitted for bots or they will not be allowed to quest. This is **20** by default.
* **questing.bot_questing_requirements.min_health_head**: The minimum permitted health percentage of a bot's head or it will not be allowed to quest. This is **50%** by default.
* **questing.bot_questing_requirements.min_health_chest**: The minimum permitted health percentage of a bot's chest or it will not be allowed to quest. This is **50%** by default.
* **questing.bot_questing_requirements.min_health_stomach**: The minimum permitted health percentage of a bot's stomach or it will not be allowed to quest. This is **50%** by default.
* **questing.bot_questing_requirements.min_health_legs**: The minimum permitted health percentage of either of a bot's legs or it will not be allowed to quest. This is **50%** by default.
* **questing.bot_questing_requirements.max_overweight_percentage**: The maximum total weight permitted for bots (as a percentage of their overweight threshold) or they will not be allowed to quest. This is **100%** by default.
* **questing.bot_questing_requirements.search_time_after_combat.xxx.min/max:** Bots will not be allowed to quest until a random amount of time (in seconds) in this range has passed after combat most recently ended for them. If SAIN's lowest brain-layer priority is greater than **questing.brain_layer_priorities.questing**, the *prioritized_sain* settings will be used (for **xxx**). Otherwise, *prioritized_questing* settings will be used.
* **questing.bot_questing_requirements.hearing_sensor.enabled**: If bots are allowed to stop questing due to suspicious noises. This is **true** by default.
* **questing.bot_questing_requirements.hearing_sensor.min_corrected_sound_power**: If the "loudness" of a sound is less than this value, bots will ignore it. Currently, this results in all bots (even those wearing a headset) ignoring you if you crouch-walk at the slowest speed. This is **17** by default, and the units are unknown.
* **questing.bot_questing_requirements.hearing_sensor.max_distance_footsteps**: If bots hear footsteps within this distance (in meters), they will become suspicious and stop questing. This is **20** m by default.
* **questing.bot_questing_requirements.hearing_sensor.max_distance_gunfire**: If bots hear gunfire within this distance (in meters), they will become suspicious and stop questing. This is **50** m by default.
* **questing.bot_questing_requirements.hearing_sensor.max_distance_gunfire_suppressed**: If bots hear suppressed gunfire within this distance (in meters), they will become suspicious and stop questing. This is **50** m by default.
* **questing.bot_questing_requirements.hearing_sensor.loudness_multiplier_footsteps**: A scaling factor to adjust the bot's hearing sensitivity to footsteps. This is **1** by default.
* **questing.bot_questing_requirements.hearing_sensor.loudness_multiplier_headset**: A scaling factor to apply to the "loudness" of sounds if a bot is wearing any type of headset. This is **1.3** by default.
* **questing.bot_questing_requirements.hearing_sensor.loudness_multiplier_helmet_low_deaf**: A scaling factor to apply to the "loudness" of sounds if a bot is wearing a helmet that has a "low deafness" rating for reducing the volume of sounds it perceives. This is **0.8** by default.
* **questing.bot_questing_requirements.hearing_sensor.loudness_multiplier_helmet_high_deaf**: A scaling factor to apply to the "loudness" of sounds if a bot is wearing a helmet that has a "high deafness" rating for reducing the volume of sounds it perceives. This is **0.6** by default.
* **questing.bot_questing_requirements.hearing_sensor.suspicious_time.min/max**: If a bot becomes suspicious of a noise, they will stop questing until a random amount of time (in seconds) in this range has passed after the last suspicious noise they heard.
* **questing.bot_questing_requirements.hearing_sensor.max_suspicious_time**: The maximum time (in seconds) that a bot can remain suspicious of noises it hears before it will be forced to ignore them for at least **questing.bot_questing_requirements.hearing_sensor.suspicion_cooldown_time** seconds. This is to prevent bots for remaining in one spot for a long time, especially for PVP-focused maps like Factory. The value of this is map-specific.
* **questing.bot_questing_requirements.hearing_sensor.suspicion_cooldown_time**: If a bot is suspicious for at least **questing.bot_questing_requirements.hearing_sensor.max_suspicious_time** seconds, it will be forced to ignore suspicious noises for this amount of time (in seconds) before it will be allowed to be suspicious of noises again. This is **7** s by default.
* **questing.bot_questing_requirements.break_for_looting.enabled**: If **true** (the default setting), bots will temporarily stop questing at certain intervals to check for loot (or whatever).
* **questing.bot_questing_requirements.break_for_looting.min_time_between_looting_checks**: The minimum delay (in seconds) after a bot takes a break to check for loot before it will be allowed to take a break again. If this value is very low, bots may frequently back-track and may never reach their objectives. If this value is high, bots will rarely loot. This is **50** s by default.
* **questing.bot_questing_requirements.break_for_looting.min_time_between_follower_looting_checks**: The minimum delay (in seconds) after any of a bot's followers take a break to check for loot before they will be allowed to take a break again. If this value is very low, bot groups may frequently back-track and may never reach their objectives. If this value is high, followers will rarely loot. This is **30** s by default.
* **questing.bot_questing_requirements.break_for_looting.min_time_between_looting_events**: The minimum delay (in seconds) after a bot successfully finds loot before it will be allowed to take a break again. If this value is very low, bots may frequently back-track and may never reach their objectives. If this value is high, bots will rarely loot. This supersedes **bot_questing_requirements.break_for_looting.min_time_between_looting_checks**, and it requires [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/) (or it will be ignored). This is **80** s by default.
* **questing.bot_questing_requirements.break_for_looting.max_time_to_start_looting**: The duration of each break (in seconds). If one of the [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/) brain layers is not active after this time, the bot will resume questing. This is **2** s by default.
* **questing.bot_questing_requirements.break_for_looting.max_loot_scan_time**: The maximum time that bots will be allowed to search for loot via [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/). If the bot hasn't found any loot within this time, it will continue questing. If it has found loot, it will not continue questing until it's completely finished with looting. This is **4** s by default.
* **questing.bot_questing_requirements.break_for_looting.max_distance_from_boss**: The maximum distance (in meters) that a follower will be allowed to travel from its boss while looting. If the follower exceeds this distance, it will be forced to stop looting and regroup. This is **75** m by default.
* **questing.bot_questing_requirements.break_for_looting.max_sain_version_for_resetting_decisions**: A string defining the maximum version of [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) that still requires bots' decisions to be reset when instructing them to loot. Otherwise, they will get stuck in SAIN's combat layers instead of looting for an extended period of time. This is **"2.2.1.99"** by default. **Do not change this unless you know what you're doing!**
* **questing.bot_questing_requirements.max_follower_distance.max_wait_time**: The maximum time (in seconds) that a bot's followers are allowed to be too far from it before it will stop questing and regroup. This is **5** s by default.
* **questing.bot_questing_requirements.max_follower_distance.min_regroup_time**: The minimum time (in seconds) that a bot will be forced to regroup with its followers if it's too far from them. After this time, the bot will be allowed to patrol its area instead. This is **1** s by default.
* **questing.bot_questing_requirements.max_follower_distance.regroup_pause_time**: When a boss reaches its nearest follower while regrouping, it will stop regrouping for this amount of time (in seconds). After that delay, it will continue regrouping if required, or it will continue questing. This delay is to prevent bosses from standing completely still while waiting for the rest of their followers to regroup. This is **2** s by default.
* **questing.bot_questing_requirements.max_follower_distance.target_range_questing.min/max**: The allowed range of distances (in meters) that followers will try to be from their boss while questing. If a follower needs to get closer to its boss, it will try to get within the **min** distance (**7** m by default) of it. After that, it will be allowed to wander up to the **max** distance (**12** m by default) from it.
* **questing.bot_questing_requirements.max_follower_distance.target_range_combat.min/max**: The same as **questing.bot_questing_requirements.max_follower_distance.target_range_questing.min/max** but for when the bot's group is in combat. The default **min** distance is **20** m, and the default **max** distance is **30** m.
* **questing.bot_questing_requirements.max_follower_distance.nearest**: If the bot has any followers, it will not be allowed to quest if its nearest follower is more than this distance (in meters) from it. This is **25** m by default.
* **questing.bot_questing_requirements.max_follower_distance.furthest**: If the bot has any followers, it will not be allowed to quest if its furthest follower is more than this distance (in meters) from it. This is **40** m by default.
* **questing.extraction_requirements.min_alive_time**: The minimum time (in seconds) a bot must wait after spawning before it will be allowed to extract. This is **60** s by default.
* **questing.extraction_requirements.must_extract_time_remaining**: The time remaining in the raid (in seconds) after which bots will be unable to select new quest objectives and must extract instead. Requires [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) 2.1.7 or later. By default, this is **300** s.
* **questing.extraction_requirements.total_quests.min/max**: The minimum and maximum quests that a bot must complete before being instructed to extract. The actual number is randomly selected between this range. Requires [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) 2.1.7 or later. Bots can still be instructed to extract if they satisfy their **questing.extraction_requirements.EFT_quests.min/max** requirement.
* **questing.extraction_requirements.EFT_quests.min/max**: The minimum and maximum EFT quests that a bot must complete before being instructed to extract. The actual number is randomly selected between this range. Requires [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) 2.1.7 or later. Bots can still be instructed to extract if they satisfy their **questing.extraction_requirements.total_quests.min/max** requirement.
* **questing.sprinting_limitations.stamina.min**: The lower stamina threshold (as a fraction of max stamina) below which bots will not be allowed to sprint. This is **0.1** by default.
* **questing.sprinting_limitations.stamina.max**: The upper stamina threshold (as a fraction of max stamina) above which bots will always be allowed to sprint. If a bot's stamina drops below **questing.sprinting_limitations.stamina.min**, it won't be allowed to sprint again until its stamina raises back to this level or higher. This is **0.5** by default.
* **questing.sprinting_limitations.sharp_path_corners.distance**: If a bot is within this distance (in meters) of a sharp corner in its path, it will not be allowed to sprint. This was mainly implemented to prevent bots from shuffle-running around stairwells. This is **2** m by default.
* **questing.sprinting_limitations.sharp_path_corners.angle**: The angle (in degrees) between segments in a bot's path above which the corner is considered a sharp corner. This was mainly implemented to prevent bots from shuffle-running around stairwells. This is **45** deg by default.
* **questing.sprinting_limitations.approaching_closed_doors.distance**: If a bot is within this distance (in meters) of a closed door and is heading toward it, it will not be allowed to sprint. This was implemented to prevent bots from sliding into closed doors before opening them. this is **3** m by default.
* **questing.sprinting_limitations.approaching_closed_doors.angle**: If a bot is within **questing.sprinting_limitations.approaching_closed_doors.distance** meters of a closed door, it will not be allowed to sprint if the angle between the bot's heading and the vector from the bot to the door is less than this value (in degrees). This was implemented to prevent bots from sliding into closed doors before opening them. This is **60** deg by default.
* **questing.bot_quests.distance_randomness**: One of the sources of "randomness" to apply when selecting a new quest for a bot. This is defined as a percentage of the total range of distances between the bot and every quest objective available to it. By default, this is **30%**.
* **questing.bot_quests.desirability_randomness**: The maximum amount that desirability ratings of quests can be randomly changed when bots select new quests. By default, this is **20%**.
* **questing.bot_quests.distance_weighting**: A factor to change how much the distances between bots and possible quest objectives for them are weighted when selecting new quests. Higher numbers mean that bots will tend to select quests that are closer, but not necessarily more desirable. This is **1** by default.
* **questing.bot_quests.desirability_weighting**: A factor to change how much the desirability of quests are weighted when selecting new quests for bots. Higher numbers mean that bots will tend to select quests that are more desirable even if they're further away. This is **1** by default.
* **questing.bot_quests.desirability_camping_multiplier**: The desirability of all camping quests (determined by **isCamping=true** in their settings) will be multiplied by this factor. This is **1** by default.
* **questing.bot_quests.desirability_sniping_multiplier**: The desirability of all sniping quests (determined by **isSniping=true** in their settings) will be multiplied by this factor. This is **1** by default.
* **questing.bot_quests.desirability_active_quest_multiplier**: The desirability of all EFT quests will be multiplied by this factor if it's an active quest for you. This is **1.2** by default.
* **questing.bot_quests.exfil_direction_weighting.xxx**: A factor to change how likely bots are to select new quests that are in the direction of their selected exfil point. Higher numbers mean that bots will tend to select quests that are on the way to their selected exfil even if they're undesirable. This factor is different for every map.
* **questing.bot_quests.exfil_direction_max_angle**: If the angle between the vector from a bot to its selected exfil and the vector from the bot to a quest objective is below this value (in degrees), the angle will be ignored (treated as 0 deg) for that objective when selecting new quests for bots. This is to allow bots to meander toward their selected exfil instead of having them tend to follow a straight path toward it. This is **90** deg by default.
* **questing.bot_quests.exfil_reached_min_fraction**: This value is multiplied by the maximum distance between all exfils on the map to determine the distance threshold below which bots will change their selected exfils. If a bot travels within that threshold of its selected exfil, it will choose a new exfil. This is to allow bots to travel around the map instead of gravitating toward their initially selected exfils even after they reach them. This is **0.2** by default.
* **questing.bot_quests.blacklisted_boss_hunter_bosses**: An array containing the names of bosses that bots doing the "Boss Hunter" quest will not be allowed to hunt.
* **questing.bot_quests.airdrop_bot_interest_time**: The time (in seconds) after an airdop lands during which bots can go to it via an "Airdrop Chaser" quest. This is **420** s by default.
* **questing.bot_quests.elimination_quest_search_time**: The time (in seconds) a bot will wait before selecting another quest after reaching each objective in an elimination EFT quest. This is **60** s by default.
* **questing.bot_quests.lightkeeper_island_quests.enabled**: If bots are able to perform quests on Lightkeeper Island. This is **true** by default.
* **questing.bot_quests.lightkeeper_island_quests.min_sain_version**: If [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) is loaded, it must be at least this version for bots to be able to perform Lightkeeper quests. If [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) is below this version, bots may not respect alliance changes correct when they enter/exit the bridge. This is **"3.1.0.99"** by default. **Do not change this unless you know what you're doing!**
* **questing.bot_quests.eft_quests.xxx**: The settings to apply to all quests based on EFT's quests.
* **questing.bot_quests.spawn_rush.xxx**: The settings to apply to the "Spawn Rush" quest.
* **questing.bot_quests.spawn_point_wander.xxx**: The settings to apply to the "Spawn Point Wandering" quest.
* **questing.bot_quests.boss_hunter.xxx**: The settings to apply to the "Boss Hunter" quest.
* **questing.bot_quests.airdrop_chaser.xxx**: The settings to apply to the "Airdrop Chaser" quest.
**Options for Each Section in *bot_quests*:**
* **desirability**: The desirability rating (in percent) of the quest. Bots will be more likely to select quests with higher desirability ratings.
* **max_bots_per_quest**: The maximum number of bots that can actively be performing each quest of that type.
* **min_distance**: Each objective in the quest will only be selected if the bot is at least this many meters away from it.
* **max_distance**: Each objective in the quest will only be selected if the bot is at most this many meters away from it.
* **max_raid_ET**: The quest can only be selected if this many seconds (or less) have elapsed in the raid. This is based on the overall raid time, not the time after you spawn. For example, if you set **maxRaidET=60** for a quest and you spawn into a Factory raid with 15 minutes remaining, this quest will never be used because 300 seconds has already elapsed in the overall raid.
* **chance_of_having_keys**: The chance that bots will have keys for the locations specified in the quests.
* **match_looting_behavior_distance**: If there are any EFT quest objectives within this distance of non-EFT quest objectives, the EFT quest-objective looting behavior ("Force" or "Inhibit") will be changed to match the nearby non-EFT quest-objective looting behavior
* **min_level**: The absolute minimum player level allowed for bots to select the quest.
* **max_level**: The absolute maximum player level allowed for bots to select the quest.
* **level_range**: An array of [minimum player level for the quest, level range] pairs to determine the maximum player level for each quest of that type. This value is added to the minimum player level for the quest. For example, if a quest is only available at level 15, the level range for it will be 20 (as determined via interpolation of this array using its default values). As a result, only bots between levels 15 and 35 will be allowed select that quest.
**PMC and Player-Scav Spawning Options:**
* **bot_spawns.enabled**: Allow this mod to spawn PMC's and player Scavs (**true** by default).
* **bot_spawns.blacklisted_pmc_bot_brains**: An array of the bot "brain" types that SPT will not be able to use when generating initial PMC's. These "brain" types have behaviors that inhibit their ability to quest, and this causes them to get stuck in areas for a long time (including their spawn locations). **Do not change this unless you know what you're doing!**
* **bot_spawns.spawn_retry_time**: If any bots fail to spawn, no other attempts will be made to spawn more of them for this amount of time (in seconds). By default, this is **10** s.
* **bot_spawns.delay_game_start_until_bot_gen_finishes**: After the final loading screen shows "0:00.000" for a few seconds, the game will be further delayed from starting if not all bots have been generated. Without doing this, PMC's may not spawn immediately when the raid starts, and the remaining bots will take much longer to generate. This is **true** by default.
* **bot_spawns.spawn_initial_bosses_first**: If initial bosses must spawn before PMC's are allowed to spawn. This does not apply to Factory (Day or Night). If this is **false** and **bot_spawns.advanced_eft_bot_count_management.enabled=false**, initial PMC spawns may prevent some bosses (i.e. Rogues on Lighthouse) from spawning at the beginning of the raid. This is **false** by default and assumes **bot_spawns.advanced_eft_bot_count_management.enabled=true**.
* **bot_spawns.non_wave_bot_spawn_period_factor**: The value of BotSpawnPeriodCheck for each map will be adjusted by a factor of this setting. Bots will spawn less frequently when this is >1 and more frequently when it's <1. This is **3** by default to reduce the "swarming" feeling of the "new" EFT spawning system used in EFT 0.14.x.
* **bot_spawns.advanced_eft_bot_count_management.enabled**: If **true**, this enables code that tricks EFT into thinking that bots generated by this mod are human players. This makes EFT ignore bot caps (both total and zone-specific) for PMC's and player Scavs generated by this mod. This is **true** by default.
* **bot_spawns.advanced_eft_bot_count_management.use_EFT_bot_caps**: If **bot_spawns.advanced_eft_bot_count_management.enabled=true**, SPT's bot caps will be changed to match EFT's bot caps. This is **true** by default.
* **bot_spawns.advanced_eft_bot_count_management.only_decrease_bot_caps**: If **bot_spawns.advanced_eft_bot_count_management.enabled=true** and **bot_spawns.advanced_eft_bot_count_management.use_EFT_bot_caps=true**, SPT's bot caps will be changed to match EFT's bot caps only if EFT's bot caps are lower. This is **true** by default.
* **bot_spawns.advanced_eft_bot_count_management.bot_cap_adjustments**: If **bot_spawns.advanced_eft_bot_count_management.enabled=true** and **bot_spawns.advanced_eft_bot_count_management.use_EFT_bot_caps=true**, these additional adjustments will be made to SPT's bot caps after changing them to EFT's. This is used to balance bot spawns and performance.
* **bot_spawns.bot_cap_adjustments.enabled**: If bot caps should be modified (**false** by default assuming **bot_spawns.advanced_eft_bot_count_management.enabled=true**).
* **bot_spawns.bot_cap_adjustments.min_other_bots_allowed_to_spawn**: PMC's and player Scavs will not be allowed to spawn unless there are fewer than this value below the bot count for the map. For example, if this value is 4 and the maximum bot cap is 20, PMC's and player Scavs will not be allowed to spawn if there are 17 or more alive bots in the map. This is to retain a "buffer" below the maximum bot cap so that Scavs are able to continue spawning throughout the raid. This is **2** by default.
* **bot_spawns.bot_cap_adjustments.add_max_players_to_bot_cap**: If this is **true** (which is the default setting), the bot cap for the map will be increased by the maximum number of players for the map. This is to better emulate live Tarkov where there can still be many Scavs around the map even with a full lobby.
* **bot_spawns.bot_cap_adjustments.max_additional_bots**: The bot cap for the map will not be allowed to be increased by more than this value. If this value is too high, performance may be impacted. This is **5** by default.
* **bot_spawns.bot_cap_adjustments.max_total_bots**: The highest allowed bot cap for any map. If this value is too high, performance may be impacted. This is **26** by default.
* **bot_spawns.limit_initial_boss_spawns.enabled**: If initial boss spawns should be limited (**true** by default).
* **bot_spawns.limit_initial_boss_spawns.disable_rogue_delay**: If the 180-second delay SPT adds to Rogue spawns on Lighthouse should be removed. This is **true** by default.
* **bot_spawns.limit_initial_boss_spawns.max_initial_bosses**: The maximum number of bosses that are allowed to spawn at the beginning of the raid (including Raiders and Rogues). After this number is reached, all remaining initial boss spawns will be canceled. If this number is too high, few Scavs will be able to spawn after the initial PMC spawns. This is **14** by default.
* **bot_spawns.limit_initial_boss_spawns.max_initial_rogues**: The maximum number of Rogues that are allowed to spawn at the beginning of the raid. After this number is reached, all remaining initial Rogue spawns will be canceled. If this number is too high, few Scavs will be able to spawn after the initial PMC spawns. This is **10** by default.
* **bot_spawns.max_alive_bots**: The maximum number of PMC's and player Scavs (combined) that can be alive at the same time on each map. This only applies to PMC's and player Scavs generated by this mod; it doesn't apply to bots spawned by other mods or for Scavs converted to PMC's or player Scavs automatically by SPT.
* **bot_spawns.pmc_hostility_adjustments.enabled**: If this mod should override EFT's hostility chances for PMC bots. This is **true** by default.
* **bot_spawns.pmc_hostility_adjustments.pmcs_always_hostile_against_pmcs**: Makes PMC's always hostile against other PMC's. This is **true** by default.
* **bot_spawns.pmc_hostility_adjustments.global_scav_enemy_chance**: Sets the global chance that PMC's will be hostile toward Scavs. However, EFT does not use this setting in many maps. This is **100** by default.
* **bot_spawns.pmc_hostility_adjustments.pmc_enemy_roles**: Makes PMC's always hostile toward bots with these roles.
* **bot_spawns.pmcs.xxx**: The settings to apply to PMC spawns (see below for details).
* **bot_spawns.player_scavs.xxx**: The settings to apply to player Scav spawns (see below for details).
* **adjust_pscav_chance.enabled**: If the chances that Scavs are converted to player Scavs should be adjusted throughout the raid. This is only used if **bot_spawns.enabled=false** or **bot_spawns.player_scavs.enabled=false**, and it is **true** by default.
* **adjust_pscav_chance.chance_vs_time_remaining_fraction**: An array describing how likely Scavs are to be converted to player Scavs as a function of the fraction of time remaining in the raid. This is based on the overall raid time, not the time after you spawn. The array contains [fraction of raid time remaining, conversion chance] pairs, and there is no limit to the number of pairs.
**Options for *bot_spawns.pmcs* and *bot_spawns.player_scavs*:**
* **enabled**: If the corresponding bot type will be allowed to spawn. This is **true** by default for both bot types.
* **min_raid_time_remaining**: The minimum time (in seconds) that must be remaining in the raid for bots of the corresponding bot type to spawn. This is **180** s by default for both PMC's and PScavs.
* **min_distance_from_players_initial**: The minimum distance (in meters) that a bot must be from you and other bots when selecting its spawn point. This is used during the first wave of spawns and is **25** m by default.
* **min_distance_from_players_during_raid**: The minimum distance (in meters) that a bot must be from you and other bots when selecting its spawn point. This is used after the first wave of spawns.
* **min_distance_from_players_during_raid_factory**: The minimum distance (in meters) that a bot must be from you and other bots when selecting its spawn point. This is used after the first wave of spawns. However, this is only used for Factory raids instead of **min_distance_from_players_during_raid**.
* **fraction_of_max_players**: When determining how many total bots of this type will spawn throughout the raid, the maximum player count for the map is multiplied by this value. This is **1** by default for PMC's and **1.5** by default for player Scavs.
* **fraction_of_max_players_vs_raidET**: If you spawn late into the raid as a Scav, the minimum and maximum initial PMC's will be reduced by a factor determined by this array. The array contains [fraction of raid time remaining, fraction of max players] pairs, and there is no limit to the number of pairs.
* **time_randomness**: The maximum percentage of total raid time (before reducing it for Scav raids) that player-Scav spawns can be randomly adjusted when generating a spawn schedule for them. However, player Scavs will never be allowed to spawn earlier than the minimum reduced raid time in the SPT configuration for the map, and they will never be allowed to spawn later than the maximum reduced raid time for the map. This is **10%** by default.
* **bots_per_group_distribution**: An array describing how likely bot groups of various sizes are allowed to spawn. When generating bot groups, this mod will select a random number for each group between 0 and 1. It will then use interpolation to determine how many bots to add to the group using this array. The first column is the look-up value for the random number selected for the group, and the second column is the number of bots to add to the group. The interpolated value for number of bots is rounded to the nearest integer.
* **bot_difficulty_as_online**: An array describing the chances that members of a new bot group will be of a certain difficulty. When generating bot groups, this mod will select a random number for each group between 0 and 1. It will then use interpolation to determine the difficulty of all bots in the group using this array. The first column is the look-up value for the random number selected for the group, and the second column is a number corresponding to the difficulty that will be used (0 = easy, 1 = normal, 2 = hard, 3 = impossible). The interpolated value for number of bots is rounded to the nearest integer.
**---------- Known Issues ----------**
**Objective System:**
* Mods that add a lot of new quests may cause latency issues that may result in game stability problems and stuttering
* Bots tend to get trapped in certain areas. Known areas:
* Customs between Warehouse 4 and New Gas
* Customs in some Dorms rooms (i.e. 214 and 220 in 3 story)
* Lighthouse in the mountains near the Resort spawn
* Lighthouse on the rocks near the helicopter crash
* Bots blindly run to their objective (unless they're in combat) even if it's certain death (i.e. running into the Sawmill when Shturman is there).
* Bots take the most direct path to their objectives, which may involve running in the middle of an open area without any cover.
* Certain bot "brains" stay in a combat state for a long time, during which they're unable to continue their quests.
* Certain bot "brains" are blacklisted because they cause the bot to always be in a combat state and therefore never quest (i.e. exUSEC's when they're near a stationary weapon)
* Bots sometimes unlock doors for no reason if they can't properly resolve their quest locations.
* A *"Destroying GameObjects immediately is not permitted during physics trigger/contact, animation event callbacks or OnValidate. You must use Destroy instead."* error will sometimes appear in the game console after a bot unlocks a door. This can be ignored.
* Player-level ranges for some quests are not reasonable, so bots may do late-game quests at low player levels and vice versa. This is because EFT has no minimum level defined for several quest lines.
**PMC and Player-Scav Spawning System:**
* If there is a lot of PMC action at the beginning of the raid, the rest of the raid will feel dead. However, this isn't so different from live Tarkov.
* If **advanced_eft_bot_count_management.enabled=false**, not all PMC's or player Scavs spawn into Streets because too many Scavs spawn into the map first
* In maps with a high number of max players, Scavs don't always spawn when the game generates them if your **max_alive_bots** setting is high and **advanced_eft_bot_count_management.enabled=false**
* In maps with a high number of max players, performance can be pretty bad if your **max_alive_bots** setting is high
* Noticeable stuttering for (possibly) several seconds when the initial PMC wave spawns if your **max_alive_bots** setting is high
* Performance may be worse if **advanced_eft_bot_count_management.enabled=true** because EFT may be allowed to spawn more Scavs than with previous versions of this mod.
**---------- Credits ----------**
* Thanks to [Props](https://hub.sp-tarkov.com/user/18746-props/) for sharing the code [DONUTS](https://hub.sp-tarkov.com/files/file/878-swag-donuts-dynamic-spawn-waves-and-custom-spawn-points/) uses to spawn bots. This was the inspiration to create this mod.
* Thanks to [DrakiaXYZ](https://hub.sp-tarkov.com/user/30839-drakiaxyz/) for creating [BigBrain](https://hub.sp-tarkov.com/files/file/1219-bigbrain/) and [Waypoints](https://hub.sp-tarkov.com/files/file/1119-waypoints-expanded-bot-patrols-and-navmesh/) and for all of your help with developing this mod. Also, thanks for your help with adding interop capability to [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/).
* Thanks to [nooky](https://hub.sp-tarkov.com/user/29062-nooky/) for lots of help with testing and ensuring this mod remains compatible with [SWAG + DONUTS](https://hub.sp-tarkov.com/files/file/878-swag-donuts-dynamic-spawn-waves-and-custom-spawn-points/).
* Thanks to [Skwizzy](https://hub.sp-tarkov.com/user/31303-skwizzy/) for help with adding interop capability to [Looting Bots](https://hub.sp-tarkov.com/files/file/1096-looting-bots/).
* Thanks to [Solarint](https://hub.sp-tarkov.com/user/30697-solarint/) for help with improving interop capability to [SAIN](https://hub.sp-tarkov.com/files/file/1062-sain-2-0-solarint-s-ai-modifications-full-ai-combat-system-replacement/) and working with me to balance bot questing vs. combat behavior.
* Thanks to everyone else on Discord who helped to test the many alpha releases of this mod and provided feedback to make it better. There are too many people to name, but you're all awesome.
* Of course, thanks to the SPT development team who made this possible in the first place.

View File

@ -0,0 +1,381 @@
{
"enabled": true,
"debug": {
"enabled": true,
"always_spawn_pmcs": false,
"always_spawn_pscavs": false,
"show_zone_outlines": false,
"show_failed_paths": false,
"show_door_interaction_test_points": false
},
"max_calc_time_per_frame_ms": 5,
"chance_of_being_hostile_toward_bosses": {
"scav": 0,
"pscav": 20,
"pmc": 80,
"boss": 0
},
"questing": {
"enabled": true,
"bot_pathing_update_interval_ms": 100,
"brain_layer_priorities": {
"questing": 18,
"following": 19,
"regrouping": 26,
"sleeping": 99
},
"quest_selection_timeout": 250,
"btr_run_distance": 10,
"allowed_bot_types_for_questing": {
"scav": false,
"pscav": true,
"pmc": true,
"boss": false
},
"stuck_bot_detection": {
"distance": 2,
"time": 20,
"max_count": 8,
"follower_break_time": 10,
"max_not_able_bodied_time": 120,
"stuck_bot_remedies": {
"enabled" : true,
"min_time_before_jumping": 6,
"jump_debounce_time": 4,
"min_time_before_vaulting": 8,
"vault_debounce_time": 4
}
},
"unlocking_doors" : {
"enabled": {
"scav": false,
"pscav": false,
"pmc": true,
"boss": false
},
"search_radius": 25,
"max_distance_to_unlock": 0.5,
"door_approach_position_search_radius": 0.75,
"door_approach_position_search_offset": -0.75,
"pause_time_after_unlocking": 5,
"debounce_time": 1,
"default_chance_of_bots_having_keys": 25
},
"min_time_between_switching_objectives": 5,
"default_wait_time_after_objective_completion": 5,
"wait_time_before_planting": 1,
"quest_generation": {
"navmesh_search_distance_item": 1.5,
"navmesh_search_distance_zone": 1.5,
"navmesh_search_distance_spawn": 2,
"navmesh_search_distance_doors": 0.75
},
"bot_search_distances": {
"objective_reached_ideal": 0.5,
"objective_reached_navmesh_path_error": 2,
"max_navmesh_path_error": 10
},
"bot_pathing": {
"max_start_position_discrepancy": 0.5,
"incomplete_path_retry_interval": 5
},
"bot_questing_requirements": {
"exclude_bots_by_level": true,
"repeat_quest_delay": 360,
"max_time_per_quest": 300,
"min_hydration": 20,
"min_energy": 20,
"min_health_head": 50,
"min_health_chest": 50,
"min_health_stomach": 50,
"min_health_legs": 50,
"max_overweight_percentage": 100,
"search_time_after_combat": {
"prioritized_sain" : {
"min": 5,
"max": 20
},
"prioritized_questing" : {
"min": 20,
"max": 45
}
},
"hearing_sensor": {
"enabled": true,
"min_corrected_sound_power": 17,
"max_distance_footsteps": 20,
"max_distance_gunfire": 50,
"max_distance_gunfire_suppressed": 50,
"loudness_multiplier_footsteps": 1,
"loudness_multiplier_headset": 1.3,
"loudness_multiplier_helmet_low_deaf": 0.8,
"loudness_multiplier_helmet_high_deaf": 0.6,
"suspicious_time": {
"min": 5,
"max": 20
},
"max_suspicious_time": {
"default": 60,
"factory4_day": 30,
"factory4_night": 45,
"bigmap": 120,
"woods": 120,
"shoreline": 120,
"lighthouse": 120,
"rezervbase": 120,
"interchange": 120,
"laboratory": 60,
"tarkovstreets": 120,
"sandbox": 120,
"sandbox_high": 120
},
"suspicion_cooldown_time": 7
},
"break_for_looting": {
"enabled": true,
"min_time_between_looting_checks": 50,
"min_time_between_follower_looting_checks": 30,
"min_time_between_looting_events": 80,
"max_time_to_start_looting": 2,
"max_loot_scan_time": 4,
"max_distance_from_boss": 50,
"max_sain_version_for_resetting_decisions": "2.2.1.99"
},
"max_follower_distance": {
"max_wait_time": 5,
"min_regroup_time": 1,
"regroup_pause_time": 2,
"target_range_questing": {
"min": 7,
"max": 12
},
"target_range_combat": {
"min": 15,
"max": 35
},
"nearest": 15,
"furthest": 25
}
},
"extraction_requirements": {
"min_alive_time": 60,
"must_extract_time_remaining": 300,
"total_quests": {
"min": 3,
"max": 8
},
"EFT_quests": {
"min": 2,
"max": 4
}
},
"sprinting_limitations": {
"stamina": {
"min": 0.1,
"max": 0.5
},
"sharp_path_corners" : {
"distance": 2,
"angle": 45
},
"approaching_closed_doors" : {
"distance": 3,
"angle": 60
}
},
"bot_quests": {
"distance_randomness": 30,
"desirability_randomness": 20,
"distance_weighting": 1,
"desirability_weighting": 1,
"desirability_camping_multiplier": 1,
"desirability_sniping_multiplier": 1,
"desirability_active_quest_multiplier": 1.2,
"exfil_direction_weighting": {
"default": 0,
"factory4_day": 0.2,
"factory4_night": 0.2,
"bigmap": 0.7,
"woods": 0.7,
"shoreline": 0.7,
"lighthouse": 0.5,
"rezervbase": 0.4,
"interchange": 0.7,
"laboratory": 0.3,
"tarkovstreets": 0.7,
"sandbox": 0.5,
"sandbox_high": 0.5
},
"exfil_direction_max_angle": 90,
"exfil_reached_min_fraction": 0.2,
"blacklisted_boss_hunter_bosses": [ "pmcBEAR", "pmcUSEC", "gifter", "arenaFighterEvent", "shooterBTR", "bossZryachiy", "followerZryachiy", "skier", "peacemaker" ],
"airdrop_bot_interest_time": 420,
"elimination_quest_search_time": 60,
"eft_quests": {
"desirability": 60,
"max_bots_per_quest": 3,
"chance_of_having_keys": 50,
"match_looting_behavior_distance": 5,
"level_range": [
[0, 99],
[1, 8],
[10, 15],
[20, 25],
[30, 30],
[40, 40]
]
},
"lightkeeper_island_quests" : {
"enabled": true,
"min_sain_version": "3.1.1.99"
},
"spawn_rush": {
"desirability": 100,
"max_bots_per_quest": 1,
"max_distance": 75,
"max_raid_ET": 30
},
"spawn_point_wander": {
"desirability": 0,
"min_distance": 75,
"max_bots_per_quest": 30
},
"boss_hunter": {
"desirability": 40,
"min_level": 15,
"max_raid_ET": 300,
"min_distance": 50,
"max_bots_per_quest": 2
},
"airdrop_chaser": {
"desirability": 70,
"max_bots_per_quest": 3,
"max_distance": 400
}
}
},
"bot_spawns": {
"enabled": true,
"blacklisted_pmc_bot_brains": [ "bossKilla", "bossTagilla", "exUsec", "followerGluharAssault", "followerGluharProtect", "crazyAssaultEvent", "bossKnight" ],
"spawn_retry_time": 10,
"delay_game_start_until_bot_gen_finishes": true,
"spawn_initial_bosses_first": false,
"non_wave_bot_spawn_period_factor": 3,
"advanced_eft_bot_count_management": {
"enabled": true,
"use_EFT_bot_caps": true,
"only_decrease_bot_caps": true,
"bot_cap_adjustments": {
"default": 0,
"factory4_day": 0,
"factory4_night": 0,
"bigmap": 0,
"woods": 0,
"shoreline": 0,
"lighthouse": 4,
"rezervbase": 0,
"interchange": 0,
"laboratory": 0,
"tarkovstreets": -4,
"sandbox": 0,
"sandbox_high": 0
}
},
"bot_cap_adjustments" : {
"enabled": false,
"min_other_bots_allowed_to_spawn": 2,
"add_max_players_to_bot_cap": true,
"max_additional_bots": 5,
"max_total_bots": 26
},
"limit_initial_boss_spawns" : {
"enabled": true,
"disable_rogue_delay": true,
"max_initial_bosses": 14,
"max_initial_rogues": 10
},
"max_alive_bots": {
"default": 6,
"factory4_day": 7,
"factory4_night": 7,
"bigmap": 7,
"woods": 8,
"shoreline": 7,
"lighthouse": 7,
"rezervbase": 7,
"interchange": 8,
"laboratory": 9,
"tarkovstreets": 8,
"sandbox": 7,
"sandbox_high": 7
},
"pmc_hostility_adjustments": {
"enabled": true,
"pmcs_always_hostile_against_pmcs": true,
"global_scav_enemy_chance": 100,
"pmc_enemy_roles": ["pmcBEAR", "pmcUSEC", "assault", "marksman"]
},
"pmcs" : {
"enabled": true,
"min_raid_time_remaining": 180,
"min_distance_from_players_initial": 25,
"min_distance_from_players_during_raid": 75,
"min_distance_from_players_during_raid_factory": 50,
"fraction_of_max_players_vs_raidET": [
[0, 0.2],
[0.2, 0.2],
[0.6, 0.5],
[0.8, 0.7],
[0.9, 0.9],
[0.95, 1],
[1, 1]
],
"bots_per_group_distribution" : [
[0.40, 1],
[0.80, 2],
[0.92, 3],
[0.97, 4],
[1.00, 5]
],
"bot_difficulty_as_online" : [
[0.00, 0],
[0.50, 1],
[0.90, 2],
[1.00, 3]
]
},
"player_scavs": {
"enabled": true,
"min_raid_time_remaining": 180,
"min_distance_from_players_initial": 25,
"min_distance_from_players_during_raid": 75,
"min_distance_from_players_during_raid_factory": 35,
"fraction_of_max_players": 1.5,
"time_randomness": 10,
"bots_per_group_distribution" : [
[0.80, 1],
[0.90, 2],
[0.95, 3],
[0.98, 4],
[1.00, 5]
],
"bot_difficulty_as_online" : [
[0.00, 0],
[0.60, 1],
[0.95, 2],
[1.00, 3]
]
}
},
"adjust_pscav_chance" : {
"enabled": true,
"chance_vs_time_remaining_fraction" : [
[0, 50],
[0.3, 50],
[0.5, 20],
[0.8, 10],
[0.9, 0],
[1, 0]
]
}
}

View File

@ -0,0 +1,62 @@
{
"60896b7bfa70fc097863b8f5" : {
"waypoints" : [
{
"x": -95.56027,
"y": -14.5272923,
"z": 37.5281944
}
]
},
"5ede55112c95834b583f052a" : {
"waypoints" : [
{
"x": -95.56027,
"y": -14.5272923,
"z": 37.5281944
}
]
},
"60896888e4a85c72ef3fa300" : {
"waypoints" : [
{
"x": -80.53533,
"y": -15.8884859,
"z": 144.298065
},
{
"x": -27.8268414,
"y": 12.5911255,
"z": 180.3212
},
{
"x": -73.2474442,
"y": -11.7335672,
"z": 67.4957
}
]
},
"6089736efa70fc097863b8f6" : {
"requiredSwitches": {
"autoId_00000_D2_LEVER": true,
"00453": true
},
"waypoints" : [
{
"x": -80.53533,
"y": -15.8884859,
"z": 144.298065
},
{
"x": -27.8268414,
"y": 12.5911255,
"z": 180.3212
},
{
"x": -73.2474442,
"y": -11.7335672,
"z": 67.4957
}
]
}
}

View File

@ -0,0 +1,58 @@
{
"5937fd0086f7742bf33fc198" : {
"position" : {
"x": 100.578163,
"y": 1.16586256,
"z": -6.816542
},
"mustUnlockNearbyDoor": true,
"nearbyDoorSearchRadius": 5,
"nearbyDoorInteractionPosition": {
"x": 100.578163,
"y": 1.16586256,
"z": -6.816542
}
},
"619252352be33f26043400a7" : {
"position" : {
"x": 141.1595,
"y": 3.32964182,
"z": -130.344269
}
},
"prapor_hq_area_check_1" : {
"position" : {
"x": -83.9231,
"y": -14.4244938,
"z": 19.2263775
}
},
"case_extraction" : {
"position" : {
"x": 42.1924667,
"y": 4.38859224,
"z": 40.55366
}
},
"Check_mine_zone_factory" : {
"position" : {
"x": 24.2944946,
"y": 8.121774,
"z": 38.7408066
}
},
"NosQuests_8_factory_place" : {
"position" : {
"x": 36.0028954,
"y": 8.146189,
"z": 36.1306229
}
},
"zone_terminator" : {
"position" : {
"x": -47.66114,
"y": 1.15424979,
"z": 61.0818634
}
}
}

View File

@ -0,0 +1,28 @@
{
"name": "SPTQuestingBots",
"version": "0.9.0",
"main": "src/mod.js",
"license": "MIT",
"author": "DanW",
"sptVersion": ">=3.10.0 <3.11.0",
"loadBefore": [],
"loadAfter": [],
"incompatibilities": ["Andrudis-QuestManiac"],
"isBundleMod": false,
"scripts": {
"setup": "npm i",
"build": "node ./packageBuild.ts"
},
"devDependencies": {
"@types/node": "20.11",
"@typescript-eslint/eslint-plugin": "7.2",
"@typescript-eslint/parser": "7.2",
"archiver": "^6.0",
"eslint": "8.57",
"fs-extra": "11.2",
"ignore": "^5.2",
"tsyringe": "4.8.0",
"typescript": "5.4",
"winston": "3.12"
}
}

View File

@ -0,0 +1,212 @@
import modConfig from "../config/config.json";
import type { CommonUtils } from "./CommonUtils";
import type { IDatabaseTables } from "@spt/models/spt/server/IDatabaseTables";
import type { ILocationConfig } from "@spt/models/spt/config/ILocationConfig";
import type { IBotConfig } from "@spt/models/spt/config/IBotConfig";
import type { ILocation } from "@spt/models/eft/common/ILocation";
import type { IAdditionalHostilitySettings, IBossLocationSpawn } from "@spt/models/eft/common/ILocationBase";
export class BotUtil
{
private static readonly pmcRoles = ["pmcBEAR", "pmcUSEC"];
constructor(private commonUtils: CommonUtils, private databaseTables: IDatabaseTables, private iLocationConfig: ILocationConfig, private iBotConfig: IBotConfig)
{
}
public adjustAllBotHostilityChances(): void
{
if (!modConfig.bot_spawns.pmc_hostility_adjustments.enabled)
{
return;
}
this.commonUtils.logInfo("Adjusting bot hostility chances...");
for (const location in this.databaseTables.locations)
{
this.adjustAllBotHostilityChancesForLocation(this.databaseTables.locations[location]);
}
}
private adjustAllBotHostilityChancesForLocation(location : ILocation): void
{
if ((location.base === undefined) || (location.base.BotLocationModifier === undefined))
{
return;
}
const settings = location.base.BotLocationModifier.AdditionalHostilitySettings;
if (settings === undefined)
{
return;
}
for (const botType in settings)
{
if (!BotUtil.pmcRoles.includes(settings[botType].BotRole))
{
this.commonUtils.logWarning(`Did not adjust ${settings[botType].BotRole} hostility settings on ${location.base.Name}`);
continue;
}
this.adjustBotHostilityChances(settings[botType]);
}
}
private adjustBotHostilityChances(settings: IAdditionalHostilitySettings): void
{
if (modConfig.bot_spawns.pmc_hostility_adjustments.pmcs_always_hostile_against_pmcs)
{
settings.BearEnemyChance = 100;
settings.UsecEnemyChance = 100;
}
// This seems to be undefined for most maps
if (settings.SavageEnemyChance !== undefined)
{
settings.SavageEnemyChance = modConfig.bot_spawns.pmc_hostility_adjustments.global_scav_enemy_chance;
}
for (const chancedEnemy in settings.ChancedEnemies)
{
if (modConfig.bot_spawns.pmc_hostility_adjustments.pmc_enemy_roles.includes(settings.ChancedEnemies[chancedEnemy].Role))
{
settings.ChancedEnemies[chancedEnemy].EnemyChance = 100;
continue;
}
// This allows Questing Bots to set boss hostilities when the bot spawns
settings.ChancedEnemies[chancedEnemy].EnemyChance = 0;
}
}
public disablePvEBossWaves(): void
{
this.commonUtils.logInfo("Disabling PvE boss waves...");
let removedWaves = 0;
for (const location in this.databaseTables.locations)
{
removedWaves += this.removePvEBossWavesFromLocation(this.databaseTables.locations[location]);
}
this.commonUtils.logInfo(`Disabled ${removedWaves} PvE boss waves`);
}
private removePvEBossWavesFromLocation(location : ILocation): number
{
let removedWaves = 0;
if ((location.base === undefined) || (location.base.BossLocationSpawn === undefined))
{
return removedWaves;
}
const modifiedBossLocationSpawn : IBossLocationSpawn[] = [];
for (const bossLocationSpawnId in location.base.BossLocationSpawn)
{
const bossLocationSpawn = location.base.BossLocationSpawn[bossLocationSpawnId];
if (BotUtil.pmcRoles.includes(bossLocationSpawn.BossName))
{
removedWaves++;
continue;
}
modifiedBossLocationSpawn.push(bossLocationSpawn);
}
location.base.BossLocationSpawn = modifiedBossLocationSpawn;
return removedWaves;
}
public disableCustomBossWaves(): void
{
this.commonUtils.logInfo("Disabling custom boss waves...");
this.iLocationConfig.customWaves.boss = {};
}
public disableCustomScavWaves(): void
{
this.commonUtils.logInfo("Disabling custom Scav waves...");
this.iLocationConfig.customWaves.normal = {};
}
public increaseBotCaps(): void
{
if (!modConfig.bot_spawns.bot_cap_adjustments.add_max_players_to_bot_cap)
{
return;
}
const maxAddtlBots = modConfig.bot_spawns.bot_cap_adjustments.max_additional_bots;
const maxTotalBots = modConfig.bot_spawns.bot_cap_adjustments.max_total_bots;
for (const location in this.iBotConfig.maxBotCap)
{
if (this.databaseTables.locations[location].base === undefined)
{
continue;
}
const maxPlayers = this.databaseTables.locations[location].base.MaxPlayers;
this.iBotConfig.maxBotCap[location] = Math.min(this.iBotConfig.maxBotCap[location] + Math.min(maxPlayers, maxAddtlBots), maxTotalBots);
this.commonUtils.logInfo(`Changed bot cap for ${location} to: ${this.iBotConfig.maxBotCap[location]}`);
}
this.iBotConfig.maxBotCap.default = Math.min(this.iBotConfig.maxBotCap.default + maxAddtlBots, maxTotalBots);
this.commonUtils.logInfo(`Changed default bot cap to: ${this.iBotConfig.maxBotCap.default}`);
}
public useEFTBotCaps(): void
{
if (!modConfig.bot_spawns.advanced_eft_bot_count_management.use_EFT_bot_caps)
{
return;
}
for (const location in this.iBotConfig.maxBotCap)
{
if ((this.databaseTables.locations[location] === undefined) || (this.databaseTables.locations[location].base === undefined))
{
continue;
}
const originalSPTCap = this.iBotConfig.maxBotCap[location];
const eftCap = this.databaseTables.locations[location].base.BotMax;
if (!modConfig.bot_spawns.advanced_eft_bot_count_management.only_decrease_bot_caps || (originalSPTCap > eftCap))
{
this.iBotConfig.maxBotCap[location] = eftCap;
}
const fixedAdjustment = modConfig.bot_spawns.advanced_eft_bot_count_management.bot_cap_adjustments[location];
this.iBotConfig.maxBotCap[location] += fixedAdjustment;
const newCap = this.iBotConfig.maxBotCap[location];
this.commonUtils.logInfo(`Updated bot cap for ${location} to ${newCap} (Original SPT: ${originalSPTCap}, EFT: ${eftCap}, fixed adjustment: ${fixedAdjustment})`);
}
}
public modifyNonWaveBotSpawnSettings(): void
{
this.commonUtils.logInfo("Updating BotSpawnPeriodCheck for all maps...");
for (const location in this.iBotConfig.maxBotCap)
{
if ((this.databaseTables.locations[location] === undefined) || (this.databaseTables.locations[location].base === undefined))
{
continue;
}
this.databaseTables.locations[location].base.BotSpawnPeriodCheck *= modConfig.bot_spawns.non_wave_bot_spawn_period_factor;
}
}
}

View File

@ -0,0 +1,49 @@
import modConfig from "../config/config.json";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { IDatabaseTables } from "@spt/models/spt/server/IDatabaseTables";
import type { LocaleService } from "@spt/services/LocaleService";
export class CommonUtils
{
private debugMessagePrefix = "[Questing Bots] ";
private translations: Record<string, string>;
constructor (private logger: ILogger, private databaseTables: IDatabaseTables, private localeService: LocaleService)
{
// Get all translations for the current locale
this.translations = this.localeService.getLocaleDb();
}
public logInfo(message: string, alwaysShow = false): void
{
if (modConfig.enabled || alwaysShow)
this.logger.info(this.debugMessagePrefix + message);
}
public logWarning(message: string): void
{
this.logger.warning(this.debugMessagePrefix + message);
}
public logError(message: string): void
{
this.logger.error(this.debugMessagePrefix + message);
}
public getItemName(itemID: string): string
{
const translationKey = `${itemID} Name`;
if (translationKey in this.translations)
return this.translations[translationKey];
// If a key can't be found in the translations dictionary, fall back to the template data if possible
if (!(itemID in this.databaseTables.templates.items))
{
return undefined;
}
const item = this.databaseTables.templates.items[itemID];
return item._name;
}
}

View File

@ -0,0 +1,148 @@
import modConfig from "../config/config.json";
import type { CommonUtils } from "./CommonUtils";
import type { MinMax } from "@spt/models/common/MinMax";
import type { IPmcConfig } from "@spt/models/spt/config/IPmcConfig";
export class PMCConversionUtil
{
private convertIntoPmcChanceOrig: Record<string, Record<string, MinMax>> = {};
constructor(private commonUtils: CommonUtils, private iPmcConfig: IPmcConfig)
{
}
public setAllOriginalPMCConversionChances(): void
{
// Store the default PMC-conversion chances for each bot type defined in SPT's configuration file
let logMessage = "";
for (const map in this.iPmcConfig.convertIntoPmcChance)
{
logMessage += `${map} = [`;
for (const pmcType in this.iPmcConfig.convertIntoPmcChance[map])
{
if ((this.convertIntoPmcChanceOrig[map] !== undefined) && (this.convertIntoPmcChanceOrig[map][pmcType] !== undefined))
{
logMessage += `${pmcType}: already buffered, `;
continue;
}
this.setOriginalPMCConversionChances(map, pmcType);
const chances = this.convertIntoPmcChanceOrig[map][pmcType];
logMessage += `${pmcType}: ${chances.min}-${chances.max}%, `;
}
logMessage += "], ";
}
this.commonUtils.logInfo(`Reading default PMC spawn chances: ${logMessage}`);
}
private setOriginalPMCConversionChances(map: string, pmcType: string): void
{
const chances: MinMax = {
min: this.iPmcConfig.convertIntoPmcChance[map][pmcType].min,
max: this.iPmcConfig.convertIntoPmcChance[map][pmcType].max
}
if (this.convertIntoPmcChanceOrig[map] === undefined)
{
this.convertIntoPmcChanceOrig[map] = {};
}
this.convertIntoPmcChanceOrig[map][pmcType] = chances;
}
public adjustAllPmcConversionChances(scalingFactor: number, verify: boolean): void
{
// Adjust the chances for each applicable bot type
let logMessage = "";
let verified = true;
for (const map in this.iPmcConfig.convertIntoPmcChance)
{
logMessage += `${map} = [`;
for (const pmcType in this.iPmcConfig.convertIntoPmcChance[map])
{
verified = verified && this.adjustAndVerifyPmcConversionChances(map, pmcType, scalingFactor, verify);
const chances = this.iPmcConfig.convertIntoPmcChance[map][pmcType];
logMessage += `${pmcType}: ${chances.min}-${chances.max}%, `;
}
logMessage += "], ";
if (!verified)
{
break;
}
}
if (!verify)
{
this.commonUtils.logInfo(`Adjusting PMC spawn chances (${scalingFactor}): ${logMessage}`);
}
if (!verified)
{
this.commonUtils.logError("Another mod has changed the PMC conversion chances. This mod may not work properly!");
}
}
public adjustAndVerifyPmcConversionChances(map: string, pmcType: string, scalingFactor: number, verify: boolean): boolean
{
// Do not allow the chances to exceed 100%. Who knows what might happen...
const min = Math.round(Math.min(100, this.convertIntoPmcChanceOrig[map][pmcType].min * scalingFactor));
const max = Math.round(Math.min(100, this.convertIntoPmcChanceOrig[map][pmcType].max * scalingFactor));
if (verify)
{
if (this.iPmcConfig.convertIntoPmcChance[map][pmcType].min !== min)
{
return false;
}
if (this.iPmcConfig.convertIntoPmcChance[map][pmcType].max !== max)
{
return false;
}
}
this.iPmcConfig.convertIntoPmcChance[map][pmcType].min = min;
this.iPmcConfig.convertIntoPmcChance[map][pmcType].max = max;
return true;
}
public removeBlacklistedBrainTypes(): void
{
const badBrains = modConfig.bot_spawns.blacklisted_pmc_bot_brains;
this.commonUtils.logInfo("Removing blacklisted brain types from being used for PMC's...");
let removedBrains = 0;
for (const pmcType in this.iPmcConfig.pmcType)
{
for (const map in this.iPmcConfig.pmcType[pmcType])
{
const mapBrains = this.iPmcConfig.pmcType[pmcType][map];
for (const i in badBrains)
{
if (mapBrains[badBrains[i]] === undefined)
{
continue;
}
//this.commonUtils.logInfo(`Removing ${badBrains[i]} from ${pmcType} in ${map}...`);
delete mapBrains[badBrains[i]];
removedBrains++;
}
}
}
this.commonUtils.logInfo(`Removing blacklisted brain types from being used for PMC's...done. Removed entries: ${removedBrains}`);
}
}

View File

@ -0,0 +1,365 @@
import modConfig from "../config/config.json";
import eftQuestSettings from "../config/eftQuestSettings.json";
import eftZoneAndItemPositions from "../config/zoneAndItemQuestPositions.json";
import { CommonUtils } from "./CommonUtils";
import { BotUtil } from "./BotLocationUtil";
import { PMCConversionUtil } from "./PMCConversionUtil";
import type { DependencyContainer } from "tsyringe";
import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
import type { IPostDBLoadMod } from "@spt/models/external/IPostDBLoadMod";
import type { IPostSptLoadMod } from "@spt/models/external/IPostSptLoadMod";
import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import type { DynamicRouterModService } from "@spt/services/mod/dynamicRouter/DynamicRouterModService";
import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import type { ConfigServer } from "@spt/servers/ConfigServer";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { DatabaseServer } from "@spt/servers/DatabaseServer";
import type { IDatabaseTables } from "@spt/models/spt/server/IDatabaseTables";
import type { LocaleService } from "@spt/services/LocaleService";
import type { QuestHelper } from "@spt/helpers/QuestHelper";
import type { VFS } from "@spt/utils/VFS";
import type { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import type { RandomUtil } from "@spt/utils/RandomUtil";
import type { BotController } from "@spt/controllers/BotController";
import type { BotCallbacks } from "@spt/callbacks/BotCallbacks";
import type { IGenerateBotsRequestData, ICondition } from "@spt/models/eft/bot/IGenerateBotsRequestData";
import type { IBotBase } from "@spt/models/eft/common/tables/IBotBase";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import type { IBotConfig } from "@spt/models/spt/config/IBotConfig";
import type { IPmcConfig } from "@spt/models/spt/config/IPmcConfig";
import type { ILocationConfig } from "@spt/models/spt/config/ILocationConfig";
const modName = "SPTQuestingBots";
class QuestingBots implements IPreSptLoadMod, IPostSptLoadMod, IPostDBLoadMod
{
private commonUtils: CommonUtils
private botUtil: BotUtil
private pmcConversionUtil : PMCConversionUtil
private logger: ILogger;
private configServer: ConfigServer;
private databaseServer: DatabaseServer;
private databaseTables: IDatabaseTables;
private localeService: LocaleService;
private questHelper: QuestHelper;
private vfs: VFS;
private httpResponseUtil: HttpResponseUtil;
private randomUtil: RandomUtil;
private botController: BotController;
private iBotConfig: IBotConfig;
private iPmcConfig: IPmcConfig;
private iLocationConfig: ILocationConfig;
private basePScavConversionChance: number;
public preSptLoad(container: DependencyContainer): void
{
this.logger = container.resolve<ILogger>("WinstonLogger");
const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService");
const dynamicRouterModService = container.resolve<DynamicRouterModService>("DynamicRouterModService");
// Get config.json settings for the bepinex plugin
staticRouterModService.registerStaticRouter(`StaticGetConfig${modName}`,
[{
url: "/QuestingBots/GetConfig",
action: async () =>
{
return JSON.stringify(modConfig);
}
}], "GetConfig"
);
if (!modConfig.enabled)
{
return;
}
// Apply a scalar factor to the SPT-AKI PMC conversion chances
dynamicRouterModService.registerDynamicRouter(`DynamicAdjustPMCConversionChances${modName}`,
[{
url: "/QuestingBots/AdjustPMCConversionChances/",
action: async (url: string) =>
{
const urlParts = url.split("/");
const factor: number = Number(urlParts[urlParts.length - 2]);
const verify: boolean = JSON.parse(urlParts[urlParts.length - 1].toLowerCase());
this.pmcConversionUtil.adjustAllPmcConversionChances(factor, verify);
return JSON.stringify({ resp: "OK" });
}
}], "AdjustPMCConversionChances"
);
// Apply a scalar factor to the SPT-AKI PScav conversion chance
dynamicRouterModService.registerDynamicRouter(`DynamicAdjustPScavChance${modName}`,
[{
url: "/QuestingBots/AdjustPScavChance/",
action: async (url: string) =>
{
const urlParts = url.split("/");
const factor: number = Number(urlParts[urlParts.length - 1]);
this.iBotConfig.chanceAssaultScavHasPlayerScavName = Math.round(this.basePScavConversionChance * factor);
this.commonUtils.logInfo(`Adjusted PScav spawn chance to ${this.iBotConfig.chanceAssaultScavHasPlayerScavName}%`);
return JSON.stringify({ resp: "OK" });
}
}], "AdjustPScavChance"
);
// Get all EFT quest templates
// NOTE: This includes custom quests added by mods
staticRouterModService.registerStaticRouter(`GetAllQuestTemplates${modName}`,
[{
url: "/QuestingBots/GetAllQuestTemplates",
action: async () =>
{
return JSON.stringify({ templates: this.questHelper.getQuestsFromDb() });
}
}], "GetAllQuestTemplates"
);
// Get override settings for EFT quests
staticRouterModService.registerStaticRouter(`GetEFTQuestSettings${modName}`,
[{
url: "/QuestingBots/GetEFTQuestSettings",
action: async () =>
{
return JSON.stringify({ settings: eftQuestSettings });
}
}], "GetEFTQuestSettings"
);
// Get override settings for quest zones and items
staticRouterModService.registerStaticRouter(`GetZoneAndItemQuestPositions${modName}`,
[{
url: "/QuestingBots/GetZoneAndItemQuestPositions",
action: async () =>
{
return JSON.stringify({ zoneAndItemPositions: eftZoneAndItemPositions });
}
}], "GetZoneAndItemQuestPositions"
);
// Get Scav-raid settings to determine PScav conversion chances
staticRouterModService.registerStaticRouter(`GetScavRaidSettings${modName}`,
[{
url: "/QuestingBots/GetScavRaidSettings",
action: async () =>
{
return JSON.stringify({ maps: this.iLocationConfig.scavRaidTimeSettings.maps });
}
}], "GetScavRaidSettings"
);
// Get the chance that a PMC will be a USEC
staticRouterModService.registerStaticRouter(`GetUSECChance${modName}`,
[{
url: "/QuestingBots/GetUSECChance",
action: async () =>
{
return JSON.stringify({ usecChance: this.iPmcConfig.isUsec });
}
}], "GetUSECChance"
);
// Intercept the EFT bot-generation request to include a PScav conversion chance
container.afterResolution("BotCallbacks", (_t, result: BotCallbacks) =>
{
result.generateBots = async (url: string, info: IGenerateBotsRequestDataWithPScavChance, sessionID: string) =>
{
const bots = await this.generateBots({ conditions: info.conditions }, sessionID, this.randomUtil.getChance100(info.PScavChance));
return this.httpResponseUtil.getBody(bots);
}
}, {frequency: "Always"});
}
public postDBLoad(container: DependencyContainer): void
{
this.configServer = container.resolve<ConfigServer>("ConfigServer");
this.databaseServer = container.resolve<DatabaseServer>("DatabaseServer");
this.localeService = container.resolve<LocaleService>("LocaleService");
this.questHelper = container.resolve<QuestHelper>("QuestHelper");
this.vfs = container.resolve<VFS>("VFS");
this.httpResponseUtil = container.resolve<HttpResponseUtil>("HttpResponseUtil");
this.randomUtil = container.resolve<RandomUtil>("RandomUtil");
this.botController = container.resolve<BotController>("BotController");
this.iBotConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.iPmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
this.iLocationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
this.databaseTables = this.databaseServer.getTables();
this.commonUtils = new CommonUtils(this.logger, this.databaseTables, this.localeService);
this.botUtil = new BotUtil(this.commonUtils, this.databaseTables, this.iLocationConfig, this.iBotConfig);
this.pmcConversionUtil = new PMCConversionUtil(this.commonUtils, this.iPmcConfig);
if (!modConfig.enabled)
{
return;
}
if (!this.doesFileIntegrityCheckPass())
{
modConfig.enabled = false;
return;
}
}
public postSptLoad(container: DependencyContainer): void
{
if (!modConfig.enabled)
{
this.commonUtils.logInfo("Mod disabled in config.json", true);
return;
}
const presptModLoader = container.resolve<PreSptModLoader>("PreSptModLoader");
this.pmcConversionUtil.removeBlacklistedBrainTypes();
// Disable the Questing Bots spawning system if another spawning mod has been loaded
if (this.shouldDisableSpawningSystem(presptModLoader.getImportedModsNames()))
{
modConfig.bot_spawns.enabled = false;
}
// Make Questing Bots control PScav spawning
this.basePScavConversionChance = this.iBotConfig.chanceAssaultScavHasPlayerScavName;
if (modConfig.adjust_pscav_chance.enabled || (modConfig.bot_spawns.enabled && modConfig.bot_spawns.player_scavs.enabled))
{
this.iBotConfig.chanceAssaultScavHasPlayerScavName = 0;
}
this.configureSpawningSystem();
}
private configureSpawningSystem(): void
{
if (!modConfig.bot_spawns.enabled)
{
return;
}
this.commonUtils.logInfo("Configuring game for bot spawning...");
// Store the current PMC-conversion chances in case they need to be restored later
this.pmcConversionUtil.setAllOriginalPMCConversionChances();
// Overwrite BSG's chances of bots being friendly toward each other
this.botUtil.adjustAllBotHostilityChances();
// Remove all of BSG's PvE-only boss waves
this.botUtil.disablePvEBossWaves();
// Currently these are all PMC waves, which are unnecessary with PMC spawns in this mod
this.botUtil.disableCustomBossWaves();
// Disable all of the extra Scavs that spawn into Factory
this.botUtil.disableCustomScavWaves();
// If Rogues don't spawn immediately, PMC spawns will be significantly delayed
if (modConfig.bot_spawns.limit_initial_boss_spawns.disable_rogue_delay)
{
this.commonUtils.logInfo("Removing SPT Rogue spawn delay...");
this.iLocationConfig.rogueLighthouseSpawnTimeSettings.waitTimeSeconds = -1;
}
if (modConfig.bot_spawns.advanced_eft_bot_count_management.enabled)
{
this.commonUtils.logInfo("Enabling advanced_eft_bot_count_management will instruct EFT to ignore this mod's PMC's and PScavs when spawning more bots.");
this.botUtil.useEFTBotCaps();
this.botUtil.modifyNonWaveBotSpawnSettings();
}
if (modConfig.bot_spawns.bot_cap_adjustments.enabled)
{
this.botUtil.increaseBotCaps();
}
this.commonUtils.logInfo("Configuring game for bot spawning...done.");
}
private async generateBots(info: IGenerateBotsRequestData, sessionID: string, shouldBePScavGroup: boolean) : Promise<IBotBase[]>
{
const bots = await this.botController.generate(sessionID, info);
if (!shouldBePScavGroup)
{
return bots;
}
const pmcNames = [
...this.databaseTables.bots.types.usec.firstName,
...this.databaseTables.bots.types.bear.firstName
];
for (const bot in bots)
{
if (info.conditions[0].Role !== "assault")
{
continue;
}
bots[bot].Info.Nickname = `${bots[bot].Info.Nickname} (${this.randomUtil.getArrayValue(pmcNames)})`
}
return bots;
}
private doesFileIntegrityCheckPass(): boolean
{
const path = `${__dirname}/..`;
if (this.vfs.exists(`${path}/quests/`))
{
this.commonUtils.logWarning("Found obsolete quests folder 'user\\mods\\DanW-SPTQuestingBots\\quests'. Only quest files in 'BepInEx\\plugins\\DanW-SPTQuestingBots\\quests' will be used.");
}
if (this.vfs.exists(`${path}/log/`))
{
this.commonUtils.logWarning("Found obsolete log folder 'user\\mods\\DanW-SPTQuestingBots\\log'. Logs are now saved in 'BepInEx\\plugins\\DanW-SPTQuestingBots\\log'.");
}
if (this.vfs.exists(`${path}/../../../BepInEx/plugins/SPTQuestingBots.dll`))
{
this.commonUtils.logError("Please remove BepInEx/plugins/SPTQuestingBots.dll from the previous version of this mod and restart the server, or it will NOT work correctly.");
return false;
}
return true;
}
private shouldDisableSpawningSystem(importedModNames: string[]): boolean
{
if (!modConfig.bot_spawns.enabled)
{
return false;
}
const spawningModNames = ["SWAG", "DewardianDev-MOAR", "PreyToLive-BetterSpawnsPlus"];
for (const spawningModName of spawningModNames)
{
if (importedModNames.includes(spawningModName))
{
this.commonUtils.logWarning(`${spawningModName} detected. Disabling the Questing Bots spawning system.`);
return true;
}
}
return false;
}
}
export interface IGenerateBotsRequestDataWithPScavChance
{
conditions: ICondition[];
PScavChance: number;
}
module.exports = { mod: new QuestingBots() }

View File

@ -6,7 +6,7 @@
## Maximum of BSG's Blood Decals that can be placed on the floor. Changes upon next Raid. Be careful with this value!! ## Maximum of BSG's Blood Decals that can be placed on the floor. Changes upon next Raid. Be careful with this value!!
# Setting type: Int32 # Setting type: Int32
# Default value: 1024 # Default value: 1024
Maximum Ground Decals = 2048 Maximum Ground Decals = 1024
[Blood | Splatters] [Blood | Splatters]
@ -56,22 +56,22 @@ Infinite Shell Casing Lifetime = true
## Turns off Used Shell Casing Deletion ## Turns off Used Shell Casing Deletion
# Setting type: Boolean # Setting type: Boolean
# Default value: false # Default value: false
Infinite Shell Casing Lifetime = false Infinite Shell Casing Lifetime = true
## Multiplier For Muzzle Smoke Size ## Multiplier For Muzzle Smoke Size
# Setting type: Single # Setting type: Single
# Default value: 1 # Default value: 1
Muzzle Fume Size Multiplier = 1.5 Muzzle Fume Size Multiplier = 1
## Multiplier For Muzzle Jet (Muzzle Fire) Size ## Multiplier For Muzzle Jet (Muzzle Fire) Size
# Setting type: Single # Setting type: Single
# Default value: 1 # Default value: 1
Muzzle Jet Size Multiplier = 3 Muzzle Jet Size Multiplier = 1
## Multiplier For The Chance that a Muzzle Jet Happens ## Multiplier For The Chance that a Muzzle Jet Happens
# Setting type: Single # Setting type: Single
# Default value: 1 # Default value: 1
Muzzle Jet Chance Multiplier = 10 Muzzle Jet Chance Multiplier = 1
## Multiplier For The Amount of Muzzle Sparks ## Multiplier For The Amount of Muzzle Sparks
# Setting type: Int32 # Setting type: Int32
@ -108,11 +108,11 @@ Helmet Knock Off Chance = 15
# Setting type: Single # Setting type: Single
# Default value: 1 # Default value: 1
Duration for anim swap = 0.5 Duration for anim swap = 1
# Setting type: Single # Setting type: Single
# Default value: 1 # Default value: 1
Duration for Mapping Weight swap = 0 Duration for Mapping Weight swap = 1
# Setting type: Boolean # Setting type: Boolean
# Default value: true # Default value: true
@ -128,14 +128,14 @@ Item Physics = false
## Multiplier that determines the amount of force applied to physics objects. ## Multiplier that determines the amount of force applied to physics objects.
# Setting type: Single # Setting type: Single
# Default value: 1 # Default value: 1
Item Force Intensity = 0.4 Item Force Intensity = 1
[Ragdolls | Ragdoll Phsyical Properties] [Ragdolls | Ragdoll Phsyical Properties]
## How much force is applied to a shot. This is also dependent on caliber. Default is 85 ## How much force is applied to a shot. This is also dependent on caliber. Default is 85
# Setting type: Single # Setting type: Single
# Default value: 85 # Default value: 85
Bullet Intensity = 45 Bullet Intensity = 85
## How much force is applied to a grenade explosion. This is also dependent on caliber. Default is 190 ## How much force is applied to a grenade explosion. This is also dependent on caliber. Default is 190
# Setting type: Single # Setting type: Single
@ -145,7 +145,7 @@ Grenade Intensity = 190
## Allows you to step on bodies. You can potentially get stuck on them once in awhile for brief moments. Turn this off if you do not like it. ## Allows you to step on bodies. You can potentially get stuck on them once in awhile for brief moments. Turn this off if you do not like it.
# Setting type: Boolean # Setting type: Boolean
# Default value: true # Default value: true
Player Body Collision = false Player Body Collision = true
[Splatters] [Splatters]

Binary file not shown.

View File

@ -1,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2025.1.15.0 version=d2025.1.16.0
newestVersion= newestVersion=
category="1,2" category="1,2"
nexusFileStatus=1 nexusFileStatus=1
installationFile=VC_PRERELEASE_7.zip installationFile=VC_PRERELEASE_8.zip
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2024.12.16.0 version=d2025.1.20.0
newestVersion= newestVersion=
category="1,2" category="1,2"
nexusFileStatus=1 nexusFileStatus=1
installationFile=DrakiaXYZ-Waypoints-1.6.0.7z installationFile=DrakiaXYZ-Waypoints-1.6.2.7z
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2023 Dushaoan Copyright (c) 2023 DrakiaXYZ
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

Binary file not shown.

View File

@ -1,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2024.12.20.0 version=d2025.1.17.0
newestVersion= newestVersion=
category="2," category="1,2"
nexusFileStatus=1 nexusFileStatus=1
installationFile=HeadshotDarkness.zip installationFile=DrakiaXYZ-Waypoints-1.6.1.7z
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=
@ -15,7 +15,7 @@ url=
hasCustomURL=false hasCustomURL=false
lastNexusQuery= lastNexusQuery=
lastNexusUpdate= lastNexusUpdate=
nexusLastModified=2024-12-21T03:13:57Z nexusLastModified=2024-12-16T06:41:17Z
nexusCategory=0 nexusCategory=0
converted=false converted=false
validated=false validated=false

View File

@ -1,11 +1,10 @@
# This file was automatically generated by Mod Organizer. # This file was automatically generated by Mod Organizer.
-Unsorted_separator -Unsorted_separator
-Version 1.37.0_separator -Version 1.38.0_separator
-Performance Improvements
-Third Person -Third Person
-WTT - Menu Overhaul -WTT - Menu Overhaul
-Performance Improvements
-Custom Asset Importer -Custom Asset Importer
-Visceral Combat
-SWAG + DONUTS -SWAG + DONUTS
-Backburner_separator -Backburner_separator
+Config Files +Config Files
@ -16,10 +15,12 @@
-Tarky Menu -Tarky Menu
-Bot Debug -Bot Debug
-Tools & Debugging_separator -Tools & Debugging_separator
+Visceral Combat
+Nerf Bot Grenades +Nerf Bot Grenades
+Progressive Bot System +Progressive Bot System
+Dynamic Goons +Dynamic Goons
+MOAR - Ultra Lite Spawn Mod +MOAR - Ultra Lite Spawn Mod
+Questing Bots
+Looting Bots +Looting Bots
+That's Lit - Sync +That's Lit - Sync
+That's Lit +That's Lit
@ -78,7 +79,6 @@
+Better Keys NG +Better Keys NG
+More Tag Colors +More Tag Colors
+Magazine Inspector +Magazine Inspector
+Headshot Darkness
+Player Encumbrance Bar +Player Encumbrance Bar
-More Checkmarks - Server -More Checkmarks - Server
+More Checkmarks +More Checkmarks

View File

@ -1,11 +1,9 @@
# This file was automatically generated by Mod Organizer.
+Unsorted_separator +Unsorted_separator
-Version 1.37.0_separator -Version 1.38.0_separator
-Performance Improvements
-Third Person -Third Person
-WTT - Menu Overhaul -WTT - Menu Overhaul
-Performance Improvements
-Custom Asset Importer -Custom Asset Importer
-Visceral Combat
-SWAG + DONUTS -SWAG + DONUTS
+Backburner_separator +Backburner_separator
+Config Files +Config Files
@ -16,10 +14,12 @@
-Tarky Menu -Tarky Menu
-Bot Debug -Bot Debug
+Tools & Debugging_separator +Tools & Debugging_separator
+Visceral Combat
+Nerf Bot Grenades +Nerf Bot Grenades
+Progressive Bot System +Progressive Bot System
+Dynamic Goons +Dynamic Goons
+MOAR - Ultra Lite Spawn Mod +MOAR - Ultra Lite Spawn Mod
+Questing Bots
+Looting Bots +Looting Bots
+That's Lit - Sync +That's Lit - Sync
+That's Lit +That's Lit
@ -78,7 +78,6 @@
+Better Keys NG +Better Keys NG
-More Tag Colors -More Tag Colors
-Magazine Inspector -Magazine Inspector
-Headshot Darkness
-Player Encumbrance Bar -Player Encumbrance Bar
+More Checkmarks - Server +More Checkmarks - Server
-More Checkmarks -More Checkmarks

View File

@ -1,285 +0,0 @@
* {
background: transparent;
color: #DEDEDE;
font-size: 12px;
border: 0; }
*:disabled {
color: #636363; }
QAbstractScrollArea {
background: #202020;
border: 1px solid #2B2B2B;
alternate-background-color: #202020; }
QAbstractScrollArea::item {
min-height: 22px; }
QAbstractScrollArea::item:hover {
background: #4D4D4D;
color: #FFF; }
QAbstractScrollArea::item:selected {
background: #777; }
QScrollBar:horizontal {
margin: 0 17px; }
QScrollBar:vertical {
margin: 17px 0; }
QScrollBar::add-line {
background: #171717;
subcontrol-origin: margin; }
QScrollBar::add-line:horizontal {
width: 17px;
image: url("./1809 Dark Mode/Arrows/Right.svg");
subcontrol-position: right; }
QScrollBar::add-line:vertical {
height: 17px;
image: url("./1809 Dark Mode/Arrows/Down.svg");
subcontrol-position: bottom; }
QScrollBar::add-line:hover {
background: #373737; }
QScrollBar::sub-line {
background: #171717;
subcontrol-origin: margin; }
QScrollBar::sub-line:horizontal {
width: 17px;
image: url("./1809 Dark Mode/Arrows/Left.svg");
subcontrol-position: left; }
QScrollBar::sub-line:vertical {
height: 17px;
image: url("./1809 Dark Mode/Arrows/Up.svg");
subcontrol-position: top; }
QScrollBar::sub-line:hover {
background: #373737; }
QScrollBar::add-page, QScrollBar::sub-page {
background: #171717; }
QScrollBar::handle {
background: #4D4D4D;
margin: 1px; }
QScrollBar::handle:hover {
background: #7A7A7A; }
QTreeView::branch:open:has-children {
image: url("./1809 Dark Mode/Arrows/Down.svg"); }
QTreeView::branch:closed:has-children {
image: url("./1809 Dark Mode/Arrows/Right.svg"); }
QCheckBox::indicator, QRadioButton::indicator, QTreeView::indicator {
background: #000;
border: 1px solid #898989; }
QCheckBox::indicator:checked, QRadioButton::indicator:checked, QTreeView::indicator:checked {
image: url("./1809 Dark Mode/check.svg"); }
QCheckBox::indicator:hover, QRadioButton::indicator:hover, QTreeView::indicator:hover {
border: 1px solid #797979; }
QToolBar {
qproperty-movable: true; }
QToolBar::handle, QToolBar::separator {
width: 2px;
height: 2px;
background: #2B2B2B; }
QToolBar QToolButton {
margin: 4px; }
QToolBar QToolButton:hover {
background: #4D4D4D; }
QHeaderView {
margin: -2px; }
QHeaderView::section {
height: 22px;
background: #202020;
color: #FFF;
padding: 0 4px;
border: 0;
border-right: 1px solid #636363; }
QHeaderView::section:last {
margin-right: -2px; }
QHeaderView::section:hover {
background: #434343; }
QHeaderView::down-arrow {
padding-right: 10px;
image: url("./1809 Dark Mode/Arrows/Down.svg");
subcontrol-position: center right; }
QHeaderView::up-arrow {
padding-right: 10px;
image: url("./1809 Dark Mode/Arrows/Up.svg");
subcontrol-position: center right; }
QComboBox, QLineEdit {
min-height: 20px;
background: #191919;
color: #FFF;
padding-left: 5px;
border: 1px solid #535353;
margin: 6px 0; }
QComboBox::drop-down, QLineEdit::drop-down {
width: 20px;
subcontrol-origin: padding;
subcontrol-position: top right;
border: 0; }
QComboBox::down-arrow, QLineEdit::down-arrow {
image: url("./1809 Dark Mode/Arrows/Down.svg"); }
QComboBox:disabled, QLineEdit:disabled {
color: #636363; }
QTabWidget::pane {
background: #202020;
border: 1px solid #2B2B2B; }
QTabWidget QAbstractItemView {
background: #2B2B2B;
alternate-background-color: #2B2B2B;
border: 1px solid #222121; }
QTabWidget QHeaderView::section {
background: #2B2B2B; }
QTabBar::tab {
height: 24px;
background: #191919;
color: #FFF;
padding: 0 10px;
border: 1px solid transparent;
margin-bottom: -1px; }
QTabBar::tab:disabled {
color: #636363; }
QTabBar::tab:hover {
background: #4D4D4D; }
QTabBar::tab:selected {
background: #0078D7;
color: #FFF;
padding: 0 10px; }
QMenu {
background: #2B2B2B;
color: #FFF;
padding: 2px;
border: 1px solid #A0A0A0; }
QMenu:separator {
height: 1px;
background: #808080;
margin: 0 10px; }
QMenu::item {
min-height: 22px;
padding: 2px 20px; }
QMenu::item:selected {
background: #414141; }
QMenu::icon {
padding-right: 20px; }
QMenu::right-arrow {
image: url("./1809 Dark Mode/Arrows/Right.svg");
padding-right: 10px; }
QMenu QPushButton {
height: 24px;
background: #2B2B2B;
color: #FFF;
text-align: left;
padding-left: 20px; }
QMenu QPushButton::menu-indicator {
padding-right: 10px;
subcontrol-origin: padding;
subcontrol-position: center right; }
QMenu QCheckBox {
height: 24px;
background: #2B2B2B;
color: #FFF; }
QMenu QCheckBox:hover {
background: #4D4D4D;
color: #FFF; }
QMenuBar {
min-height: 24px;
background: #000; }
QMenuBar::item {
padding: 0 10px; }
QMenuBar::item:selected {
background: #4D4D4D;
color: #FFF; }
QStatusBar {
background: #333; }
QStatusBar::item {
padding: 0 15px; }
QGroupBox {
padding-top: 21px; }
QGroupBox::title {
padding-top: 10px; }
QProgressBar {
background: transparent;
color: #FFF;
text-align: center;
border: 1px solid #535353; }
QProgressBar::chunk {
background: #06B025; }
QPushButton {
color: #FFF;
min-height: 22px;
padding: 0 10px; }
QPushButton:hover {
background: #4D4D4D;
color: #FFF;
border: 0; }
QPushButton::menu-indicator {
image: url("./1809 Dark Mode/Arrows/Down.svg"); }
QToolButton {
padding: 4px; }
QToolButton:hover {
background: #4D4D4D;
color: #FFF;
border: 0; }
QToolButton::menu-indicator {
image: url("./1809 Dark Mode/Arrows/Down.svg"); }
QToolTip {
background: #202020;
border: 1px solid #2B2B2B; }
QTableCornerButton::section {
background: #202020;
border: 0;
border-right: 1px solid #535353; }
QSpinBox, QDoubleSpinBox {
background: #191919;
border: 1px solid #535353; }
QMainWindow, QDialog {
background: #191919; }
QSplitter {
min-width: 8px; }
LinkLabel {
qproperty-linkColor: #0078D7; }
#startButton {
background: #2B2B2B;
padding: 4px 0; }
#startButton:hover {
background: #4D4D4D; }
#linkButton {
background: #2B2B2B; }
#linkButton:hover {
background: #4D4D4D; }
#executablesListBox {
margin-right: 8px; }
#notes, #comments {
background: #2B2B2B;
border: 1px solid #222121; }
#iconLabel {
image: url(./Paper/mo2.svg);
qproperty-pixmap: none; }
#displayCategoriesBtn {
min-width: 12px; }

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9"
height="9"
viewBox="0 0 9 9"
version="1.1"
id="svg8"
inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
inkscape:export-xdpi="307.20001"
inkscape:export-ydpi="307.20001"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="down.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#505050"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="74.259817"
inkscape:cx="4.6347389"
inkscape:cy="3.1044614"
inkscape:document-units="px"
inkscape:current-layer="layer25"
showgrid="false"
units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:snap-page="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:measure-start="18.8975,12.498"
inkscape:measure-end="20,12.498"
inkscape:snap-intersection-paths="true"
inkscape:object-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="true">
<sodipodi:guide
position="4.5,4.5"
orientation="1,0"
id="guide4039"
inkscape:locked="false" />
<sodipodi:guide
position="4.5,2"
orientation="0,1"
id="guide3935"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="4.5,7"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer25"
inkscape:label="Content"
transform="translate(0,-11)">
<path
style="fill:none;stroke:#696969;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 0.75862799,13.041402 4.5,17.147396 8.241372,13.041402"
id="path817"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9"
height="9"
viewBox="0 0 9 9"
version="1.1"
id="svg8"
inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
inkscape:export-xdpi="307.20001"
inkscape:export-ydpi="307.20001"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="left.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#505050"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="52.50962"
inkscape:cx="7.7244116"
inkscape:cy="2.1053499"
inkscape:document-units="px"
inkscape:current-layer="layer25"
showgrid="false"
units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:snap-page="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:measure-start="18.8975,12.498"
inkscape:measure-end="20,12.498"
inkscape:snap-intersection-paths="true"
inkscape:object-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="true">
<sodipodi:guide
position="4.5,4.5"
orientation="1,0"
id="guide4039"
inkscape:locked="false" />
<sodipodi:guide
position="4.5,2"
orientation="0,1"
id="guide3935"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="4.5,7"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer25"
inkscape:label="Content"
transform="translate(0,-11)">
<path
style="fill:none;stroke:#696969;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.9585976,11.758628 2.8526036,15.5 l 4.105994,3.741372"
id="path817"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9"
height="9"
viewBox="0 0 9 9"
version="1.1"
id="svg8"
inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
inkscape:export-xdpi="307.20001"
inkscape:export-ydpi="307.20001"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="right.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#505050"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="52.50962"
inkscape:cx="7.7244116"
inkscape:cy="2.1053499"
inkscape:document-units="px"
inkscape:current-layer="layer25"
showgrid="false"
units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:snap-page="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:measure-start="18.8975,12.498"
inkscape:measure-end="20,12.498"
inkscape:snap-intersection-paths="true"
inkscape:object-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="true">
<sodipodi:guide
position="4.5,4.5"
orientation="1,0"
id="guide4039"
inkscape:locked="false" />
<sodipodi:guide
position="4.5,2"
orientation="0,1"
id="guide3935"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="4.5,7"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer25"
inkscape:label="Content"
transform="translate(0,-11)">
<path
style="fill:none;stroke:#696969;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2.0414025,19.241372 6.1473965,15.5 2.0414025,11.758628"
id="path817"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9"
height="9"
viewBox="0 0 9 9"
version="1.1"
id="svg8"
inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
inkscape:export-xdpi="307.20001"
inkscape:export-ydpi="307.20001"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="up.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#505050"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="52.50962"
inkscape:cx="7.7244116"
inkscape:cy="2.1053499"
inkscape:document-units="px"
inkscape:current-layer="layer25"
showgrid="false"
units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:snap-page="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:measure-start="18.8975,12.498"
inkscape:measure-end="20,12.498"
inkscape:snap-intersection-paths="true"
inkscape:object-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="true">
<sodipodi:guide
position="4.5,4.5"
orientation="1,0"
id="guide4039"
inkscape:locked="false" />
<sodipodi:guide
position="4.5,2"
orientation="0,1"
id="guide3935"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="4.5,7"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer25"
inkscape:label="Content"
transform="translate(0,-11)">
<path
style="fill:none;stroke:#696969;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 8.241372,17.958598 4.5,13.852604 0.75862804,17.958598"
id="path817"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,141 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="11"
height="11"
viewBox="0 0 11 11"
version="1.1"
id="svg8"
inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
inkscape:export-xdpi="307.20001"
inkscape:export-ydpi="307.20001"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="check.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#505050"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="52.50962"
inkscape:cx="6.2250839"
inkscape:cy="5.2460693"
inkscape:document-units="px"
inkscape:current-layer="layer25"
showgrid="false"
units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:snap-page="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:measure-start="18.8975,12.498"
inkscape:measure-end="20,12.498"
inkscape:snap-intersection-paths="true"
inkscape:object-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="true">
<sodipodi:guide
position="5.5,5.5"
orientation="1,0"
id="guide4039"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="4.5,2"
orientation="0,1"
id="guide3935"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="5.5,9"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="2,5.5"
orientation="1,0"
id="guide819"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="10,5.5"
orientation="1,0"
id="guide821"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="5.5,6"
orientation="0,1"
id="guide823"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer25"
inkscape:label="Content"
transform="translate(0,-9)">
<image
y="9"
x="0"
id="image835"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAABHNCSVQICAgIfAhkiAAAALRJREFU
GJWVUbGRg0AMXPt/hkwZEVSgdrgKaICM2LFqIbwKqOAiwo2ogAbWkRmwx/+2ZhRIWkmr1QWA8KFd
/wO4O9x9j/XOzUzzPIukUkr6c3JEoG3bU04ANAyDzGyf2ve9SIqkIuKRhyJCJJVzlpnJ3XdgzvlI
DUopnYqlFJFUKeW07QfAbVkWNE0Dd0dd16iqCgDQdR3WdX3lfKTzuP5Znd9j1ziO2LYNADBN04s6
F3zxwTvvAn9IVHkBGwAAAABJRU5ErkJggg==
"
preserveAspectRatio="none"
height="11"
width="11" />
<path
style="fill:none;stroke:#dedede;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 2,14.5 2.908096,2.908096 4.3566724,-5.635964"
id="path838"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,702 +0,0 @@
/* Night Eyes theme v1.2.0 for Mod Organizer 2 by Ciathyza */
/* https://github.com/ciathyza/mo2-themes */
/* Main Window ---------------------------------------------------------------- */
QWidget
{
background: #181818;
color: #AAAAAA;
}
QWidget:disabled
{
background: #181818;
color: #808080;
}
QMainWindow::separator
{
border: 0px;
}
QAbstractItemView
{
background: #141414;
alternate-background-color: #141414;
show-decoration-selected: 1;
selection-background-color: #001133;
selection-color: #0099EE;
}
QAbstractItemView::item:hover
{
color: #FFFFFF;
}
QAbstractItemView::item:selected
{
background: #001133;
color: #0099EE;
}
QAbstractScrollArea::corner
{
background: #141414;
border: 2px solid #181818;
border-bottom-right-radius: 6px;
margin: 0px -2px -2px 0px;
}
LinkLabel
{
qproperty-linkColor: #3399FF;
}
/* Toolbar -------------------------------------------------------------------- */
QToolBar
{
background: #181818;
border: 1px solid #181818;
}
QToolBar::separator
{
background: #181818;
}
QToolButton
{
padding: 4px 1px;
border-radius: 2px;
margin: 4px 1px 0px 1px;
}
QToolButton:hover
{
background: #282828;
}
QToolButton:pressed
{
background: #181818;
}
QToolButton::menu-indicator
{
width: 8px;
}
/* Left Pane & File Trees ----------------------------------------------------- */
QTreeView
{
border-radius: 6px;
}
QTreeView::branch:hover
{
background: #181818;
color: #FFFFFF;
}
QTreeView::branch:selected
{
background: #001133;
color: #0099EE;
}
QTreeView::item:selected
{
background: #001133;
color: #0099EE;
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:closed:has-children:has-siblings
{
image: url(:/stylesheet/branch-closed.png);
border: 0px;
}
QTreeView::branch:open:has-children:!has-siblings,
QTreeView::branch:open:has-children:has-siblings
{
image: url(:/stylesheet/branch-open.png);
border: 0px;
}
QListView
{
border-radius: 6px;
}
QListView::item:hover
{
background: #242424;
color: #FFCC88;
}
QListView::item:selected
{
background: #001133;
color: #0099EE;
}
QTextEdit
{
background: #141414;
border-radius: 6px;
}
QWebView
{
background: #141414;
border-radius: 6px;
}
/* Group Boxes ---------------------------------------------------------------- */
QGroupBox
{
padding: 24px 4px;
border: 2px solid #141414;
border-radius: 10px;
}
QGroupBox::title
{
subcontrol-origin: padding;
subcontrol-position: top left;
padding: 8px;
}
/* Search Boxes --------------------------------------------------------------- */
QLineEdit
{
background: #141414;
min-height: 14px;
padding: 2px;
border: 2px solid #141414;
border-radius: 6px;
margin-top: 3px;
}
QLineEdit:hover
{
border: 2px solid #242424;
}
/* Most Dropdowns ------------------------------------------------------------- */
QComboBox
{
background: #141414;
min-height: 20px;
padding-left: 5px;
border: 2px solid #141414;
border-radius: 6px;
margin: 3px 0px 1px 0px;
}
QComboBox:hover
{
border: 2px solid #242424;
}
QComboBox:on
{
background: #181818;
color: #FFCC88;
border: 2px solid #181818;
}
QComboBox::drop-down
{
width: 20px;
subcontrol-origin: padding;
subcontrol-position: top right;
border: none;
}
QComboBox QAbstractItemView
{
border: 0px;
}
QComboBox::down-arrow
{
image: url(:/stylesheet/combobox-down.png);
}
/* Most Buttons --------------------------------------------------------------- */
QPushButton
{
background: #141414;
color: #FFCC88;
min-height: 18px;
padding: 2px 12px;
border-radius: 6px;
}
QPushButton:hover
{
background: #242424;
color: #FFCC88;
}
QPushButton:pressed
{
background: #181818;
color: #FFCC88;
}
QPushButton:checked
{
background: #181818;
color: #FFCC88;
margin: 4px;
}
/* Scroll Bars ---------------------------------------------------------------- */
/* Horizontal */
QScrollBar:horizontal
{
background: #141414;
height: 16px;
border: 2px solid #181818;
margin: 0px 23px -2px 23px;
}
QScrollBar::handle:horizontal
{
background: #222222;
min-width: 32px;
border-radius: 6px;
margin: 2px;
}
QScrollBar::add-line:horizontal
{
background: #141414;
width: 23px;
subcontrol-position: right;
subcontrol-origin: margin;
border: 2px solid #181818;
margin: 0px -2px -2px 0px;
}
QScrollBar::sub-line:horizontal
{
background: #141414;
width: 23px;
subcontrol-position: left;
subcontrol-origin: margin;
border: 2px solid #181818;
border-bottom-left-radius: 6px;
margin: 0px 0px -2px -2px;
}
/* Vertical */
QScrollBar:vertical
{
background: #141414;
width: 16px;
border: 2px solid #181818;
margin: 23px -2px 23px 0px;
}
QScrollBar::handle:vertical
{
background: #222222;
min-height: 32px;
border-radius: 6px;
margin: 2px;
}
QScrollBar::add-line:vertical
{
background: #141414;
height: 23px;
subcontrol-position: bottom;
subcontrol-origin: margin;
border: 2px solid #181818;
border-bottom-right-radius: 6px;
margin: 0px -2px -2px 0px;
}
QScrollBar::sub-line:vertical
{
background: #141414;
height: 23px;
subcontrol-position: top;
subcontrol-origin: margin;
border: 2px solid #181818;
border-top-right-radius: 6px;
margin: -2px -2px 0px 0px;
}
/* Combined */
QScrollBar::handle:horizontal:hover,
QScrollBar::handle:vertical:hover,
QScrollBar::add-line:horizontal:hover,
QScrollBar::sub-line:horizontal:hover,
QScrollBar::add-line:vertical:hover,
QScrollBar::sub-line:vertical:hover
{
background: #242424;
}
QScrollBar::handle:horizontal:pressed,
QScrollBar::handle:vertical:pressed,
QScrollBar::add-line:horizontal:pressed,
QScrollBar::sub-line:horizontal:pressed,
QScrollBar::add-line:vertical:pressed,
QScrollBar::sub-line:vertical:pressed
{
background: #181818;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal,
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical
{
background: transparent;
}
QScrollBar::up-arrow:vertical,
QScrollBar::right-arrow:horizontal,
QScrollBar::down-arrow:vertical,
QScrollBar::left-arrow:horizontal
{
height: 1px;
width: 1px;
border: 1px solid #181818;
}
/* Header Rows ---------------------------------------------------------------- */
QHeaderView
{
background: #181818;
}
/* Table View Tab Headers */
QHeaderView::section
{
background: #141414;
color: #D3D3D3;
height: 22px;
padding: 0px 5px;
border: 0px;
border-bottom: 2px solid #181818;
border-right: 2px solid #181818;
}
QHeaderView::section:first
{
border-top-left-radius: 6px;
}
QHeaderView::section:last
{
border-right: 0px;
border-top-right-radius: 6px;
}
QHeaderView::section:hover
{
background: #242424;
color: #FFCC88;
}
QHeaderView::down-arrow
{
padding-right: 4px;
height: 10px;
width: 10px;
}
/* Context Menus, Toolbar Dropdowns, & Tooltips ------------------------------- */
QMenuBar
{
background: #181818;
border: 1px solid #181818;
}
QMenuBar::item:selected
{
background: #242424;
color: #FFCC88;
}
QMenu
{
background: #141414;
selection-color: #FFCC88;
border: 0px;
}
QMenu::item
{
background: #141414;
selection-background-color: #242424;
padding: 4px 20px;
}
QMenu::item:selected
{
background: #242424;
color: #FFCC88;
}
QMenu::item:disabled
{
background: #242424;
color: #808080;
}
QMenu::separator
{
background: #181818;
height: 2px;
}
QMenu::icon
{
margin: 1px;
}
QToolTip
{
background: #181818;
color: #FFCC88;
padding: 1px;
border: 0px;
}
QStatusBar::item {border: None;}
/* Progress Bars (Downloads) -------------------------------------------------- */
QProgressBar
{
background: #141414;
text-align: center;
border: 0px;
border-radius: 6px;
margin: 0px 10px;
}
QProgressBar::chunk
{
background: #242424;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
/* Right Pane and Tab Bars ---------------------------------------------------- */
QTabWidget::pane
{
top: 1px;
padding: 2px 2px 10px 2px;
border: 2px solid #141414;
border-radius: 10px;
}
QTabWidget::tab-bar
{
alignment: center;
}
QTabBar::tab
{
background: #141414;
color: #141414;
padding: 4px 1em;
border: 1px solid #181818;
border-top: 0px;
border-bottom: 0px;
}
QTabBar::tab:!selected
{
background: #141414;
color: #D3D3D3;
}
QTabBar::tab:disabled
{
background: #181818;
color: #808080;
}
QTabBar::tab:selected
{
background: #181818;
color: #FFCC88;
}
QTabBar::tab:!selected:hover
{
background: #242424;
color: #FFCC88;
}
QTabBar::tab:first
{
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
QTabBar::tab:last
{
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
QTabBar QToolButton
{
background: #242424;
padding: 1px;
border-radius: 6px;
margin: 1px;
}
QTabBar QToolButton:disabled
{
background: transparent;
}
/* Sliders (Configurator) ----------------------------------------------------- */
/* QSlider::groove:horizontal
{
background: #FFCC88;
height: 1px;
border: 1px solid #FFCC88;
}
QSlider::handle:horizontal
{
background: #242424;
width: 10px;
border: 2px solid #242424;
border-radius: 6px;
margin: -10px 0px;
}
QSlider::handle:horizontal:hover
{
background: #181818;
border: 2px solid #181818;
} */
/* Tables (Configure Mod Categories) ------------------------------------------ */
QTableView
{
gridline-color: #181818;
border: 0px;
}
QListWidget::item#executablesListBox
{
/* fixes the black text problem on the Modify Executables window */
color: #D3D3D3;
}
/* Downloads tab -------------------------------------------------------------- */
QWidget#downloadTab QAbstractScrollArea
{
/* background of the entire downloads tab */
background: #141414;
}
DownloadListView QFrame
{
/* an entry on the Downloads tab */
background: #181818;
}
DownloadListView QFrame#frame
{
/* outer box of an entry on the Downloads tab */
border: 2px solid #141414;
}
DownloadListView QLabel#installLabel
{
color: none;
}
DownloadListView QFrame:clicked
{
background: #242424;
}
/* compact downloads view */
DownloadListView[downloadView=standard]::item
{
padding: 16px;
}
DownloadListView[downloadView=compact]::item
{
padding: 4px;
}
DownloadListView::item:hover
{
padding: 0px;
}
DownloadListView::item:selected
{
padding: 0px;
}
QProgressBar
{
border: 2px solid grey;
border-radius: 5px;
text-align: center;
margin: 0px;
}
QAbstractItemView[filtered=true]
{
border: 2px solid #f00 !important;
}
QLineEdit[valid-filter=false]
{
background-color: #661111 !important;
}

Some files were not shown because too many files have changed in this diff Show More