diff --git a/.gitignore b/.gitignore
index 9491a2f..f112bcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,4 +360,9 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
\ No newline at end of file
+FodyWeavers.xsd
+
+/BackdoorBandit.csproj
+dist/
+build.ps1
+**.7z
diff --git a/BackdoorBandit.csproj b/BackdoorBandit.csproj
deleted file mode 100644
index 200c94e..0000000
--- a/BackdoorBandit.csproj
+++ /dev/null
@@ -1,153 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {30D38A81-B688-474D-A40A-47BFC63BFE69}
- Library
- Properties
- BackdoorBandit
- dvize.BackdoorBandit
- v4.7.1
- 512
- true
-
-
-
- true
- portable
- true
- bin\Debug\
- TRACE;DEBUG
- prompt
- 4
-
-
- none
- true
- bin\Release\
-
-
- prompt
- 4
- false
-
-
- x64
- bin\x64\Debug\
-
-
- x64
- bin\x64\Release\
-
-
-
- F:\SPT-AKI-DEV\BepInEx\core\0Harmony.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Assembly-CSharp.dll
-
-
- F:\SPT-AKI-DEV\BepInEx\core\BepInEx.dll
-
-
-
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Comfort.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Comfort.Unity.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DissonanceVoip.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Newtonsoft.Json.dll
-
-
- F:\SPT-AKI-DEV\BepInEx\plugins\spt\spt-common.dll
-
-
- F:\SPT-AKI-DEV\BepInEx\patchers\spt-prepatch.dll
-
-
- F:\SPT-AKI-DEV\BepInEx\plugins\spt\spt-reflection.dll
-
-
-
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.dll
-
-
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.AudioModule.dll
-
-
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.CoreModule.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.IMGUIModule.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.InputLegacyModule.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.Physics2DModule.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.PhysicsModule.dll
-
-
- False
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.TextRenderingModule.dll
-
-
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll
-
-
- F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.UnityWebRequestModule.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- copy "$(TargetPath)" "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.BackdoorBandit\$(TargetName).dll"
-
-if "$(ConfigurationName)" == "Debug" (
- copy "$(TargetDir)$(TargetName).pdb" "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.BackdoorBandit\$(TargetName).pdb"
-) else (
- del "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.BackdoorBandit\$(TargetName).pdb"
-)
-
-if "$(ConfigurationName)" == "Release" (
- powershell -ExecutionPolicy Bypass -NoProfile -NonInteractive -File "$(ProjectDir)PackageMods.ps1" -ConfigurationName "$(ConfigurationName)" -TargetPath "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.BackdoorBandit\$(TargetName).dll" -TargetName "$(TargetName)" -TargetDir "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.BackdoorBandit"
-)
-
-
\ No newline at end of file
diff --git a/DamageUtility.cs b/DamageUtility.cs
index a40bf83..8173113 100644
--- a/DamageUtility.cs
+++ b/DamageUtility.cs
@@ -12,11 +12,10 @@ namespace BackdoorBandit
{
internal static class DamageUtility
{
- internal static void CheckWeaponAndAmmo(DamageInfo damageInfo, ref bool validDamage, ref HashSet validWeapons, Func isRoundValid, Func isValidLockHit)
+ internal static void CheckWeaponAndAmmo(DamageInfoStruct damageInfo, ref bool validDamage, ref HashSet validWeapons, Func isRoundValid, Func isValidLockHit)
{
- var material = damageInfo.HittedBallisticCollider.TypeOfMaterial;
- var weapon = damageInfo.Weapon.TemplateId;
-
+ MaterialType material = damageInfo.HittedBallisticCollider.TypeOfMaterial;
+ MongoID weaponID = damageInfo.Weapon.TemplateId;
//semi-pleb mode. All regular doors are shootable any weapon except for reinforced doors
if (DoorBreachPlugin.SemiPlebMode.Value && material != MaterialType.MetalThin && material != MaterialType.MetalThick)
@@ -28,7 +27,7 @@ internal static void CheckWeaponAndAmmo(DamageInfo damageInfo, ref bool validDam
//regular valid melee weapon check
if (damageInfo.DamageType != EDamageType.Bullet && damageInfo.DamageType != EDamageType.GrenadeFragment)
{
- if (damageInfo.DamageType == EDamageType.Melee && DoorBreachComponent.MeleeWeapons.Contains(weapon) && material != MaterialType.MetalThin && material != MaterialType.MetalThick)
+ if (damageInfo.DamageType == EDamageType.Melee && DoorBreachComponent.MeleeWeapons.Contains(weaponID) && material != MaterialType.MetalThin && material != MaterialType.MetalThick)
{
validDamage = true;
}
@@ -36,16 +35,16 @@ internal static void CheckWeaponAndAmmo(DamageInfo damageInfo, ref bool validDam
return;
}
- var bulletTemplate = Singleton.Instance.ItemTemplates[damageInfo.SourceId] as AmmoTemplate;
+ AmmoTemplate bulletTemplate = Singleton.Instance.ItemTemplates[damageInfo.SourceId] as AmmoTemplate;
#if DEBUG
- DoorBreachComponent.Logger.LogInfo($"ammoTemplate: {bulletTemplate.Name}");
- DoorBreachComponent.Logger.LogInfo($"BB: Actual DamageType is : {damageInfo.DamageType}");
- DoorBreachComponent.Logger.LogInfo($"isValidLockHit: {isValidLockHit(damageInfo)}");
- DoorBreachComponent.Logger.LogInfo($"isRoundValid: {isRoundValid(bulletTemplate)}");
- DoorBreachComponent.Logger.LogInfo($"weapon used: {damageInfo.Weapon.LocalizedName()}, id: {damageInfo.Weapon.TemplateId}");
- DoorBreachComponent.Logger.LogInfo($"validWeapons Contains weapon tpl id: {validWeapons.Contains(weapon).ToString()}");
+ DoorBreachComponent.Logger.LogDebug($"ammoTemplate: {bulletTemplate.Name}");
+ DoorBreachComponent.Logger.LogDebug($"BB: Actual DamageType is : {damageInfo.DamageType}");
+ DoorBreachComponent.Logger.LogDebug($"isValidLockHit: {isValidLockHit(damageInfo)}");
+ DoorBreachComponent.Logger.LogDebug($"isRoundValid: {isRoundValid(bulletTemplate)}");
+ DoorBreachComponent.Logger.LogDebug($"weapon used: {damageInfo.Weapon.LocalizedName()}, id: {damageInfo.Weapon.TemplateId}");
#endif
+
//check if weapon is a shotgun and material type is metal
if (!DoorBreachPlugin.BreachingRoundsOpenMetalDoors.Value)
{
@@ -57,16 +56,16 @@ internal static void CheckWeaponAndAmmo(DamageInfo damageInfo, ref bool validDam
}
//check if its on the validWeapons hashset and its not a shotgun.. something user added then we need to skip the isRoundValidCheck
- if (validWeapons.Contains(weapon) && !isShotgun(damageInfo) && isValidLockHit(damageInfo))
+ if (validWeapons.Contains(weaponID) && !isShotgun(damageInfo) && isValidLockHit(damageInfo))
{
validDamage = true;
return;
}
//regular valid weapon and round check
- else if (validWeapons.Contains(weapon) && isRoundValid(bulletTemplate) && isValidLockHit(damageInfo))
+ else if (validWeapons.Contains(weaponID) && isRoundValid(bulletTemplate) && isValidLockHit(damageInfo))
{
#if DEBUG
- DoorBreachComponent.Logger.LogInfo($"BB: Valid round detected.");
+ DoorBreachComponent.Logger.LogDebug($"BB: Valid round detected.");
#endif
validDamage = true;
@@ -80,19 +79,19 @@ internal static void CheckWeaponAndAmmo(DamageInfo damageInfo, ref bool validDam
}
}
- internal static void CheckDoorWeaponAndAmmo(DamageInfo damageInfo, ref bool validDamage)
+ internal static void CheckDoorWeaponAndAmmo(DamageInfoStruct damageInfo, ref bool validDamage)
{
CheckWeaponAndAmmo(damageInfo, ref validDamage, ref DoorBreachComponent.ApplicableWeapons,
ammo => isHEGrenade(ammo) || isShrapnel(ammo) || isBreachingSlug(ammo), isValidDoorLockHit);
}
- internal static void CheckCarWeaponAndAmmo(DamageInfo damageInfo, ref bool validDamage)
+ internal static void CheckCarWeaponAndAmmo(DamageInfoStruct damageInfo, ref bool validDamage)
{
CheckWeaponAndAmmo(damageInfo, ref validDamage, ref DoorBreachComponent.ApplicableWeapons,
ammo => isHEGrenade(ammo) || isShrapnel(ammo) || isBreachingSlug(ammo), isValidCarTrunkLockHit);
}
- internal static void CheckLootableContainerWeaponAndAmmo(DamageInfo damageInfo, ref bool validDamage)
+ internal static void CheckLootableContainerWeaponAndAmmo(DamageInfoStruct damageInfo, ref bool validDamage)
{
CheckWeaponAndAmmo(damageInfo, ref validDamage, ref DoorBreachComponent.ApplicableWeapons,
ammo => isHEGrenade(ammo) || isShrapnel(ammo) || isBreachingSlug(ammo), isValidContainerLockHit);
@@ -118,13 +117,13 @@ internal static bool isBreachingSlug(AmmoTemplate bulletTemplate)
return (bulletTemplate._id == "660249a0712c1005a4a3ab41");
}
- internal static bool isShotgun(DamageInfo damageInfo)
+ internal static bool isShotgun(DamageInfoStruct damageInfo)
{
//check if weapon is a shotgun
return ((damageInfo.Weapon as Weapon)?.WeapClass == "shotgun");
}
- internal static bool isValidDoorLockHit(DamageInfo damageInfo)
+ internal static bool isValidDoorLockHit(DamageInfoStruct damageInfo)
{
//check if door handle area was hit
Collider col = damageInfo.HitCollider;
@@ -146,7 +145,7 @@ internal static bool isValidDoorLockHit(DamageInfo damageInfo)
}
- internal static bool isValidCarTrunkLockHit(DamageInfo damageInfo)
+ internal static bool isValidCarTrunkLockHit(DamageInfoStruct damageInfo)
{
//check if door handle area was hit
Collider col = damageInfo.HitCollider;
@@ -154,11 +153,11 @@ internal static bool isValidCarTrunkLockHit(DamageInfo damageInfo)
//if doorhandle exists and is hit
if (col.GetComponentInParent().GetComponentInChildren() != null)
{
- var gameobj = col.GetComponentInParent().gameObject;
+ GameObject gameobj = col.GetComponentInParent().gameObject;
//find child game object Lock from gameobj
- var carLockObj = gameobj.transform.Find("CarLock_Hand").gameObject;
- var lockObj = carLockObj.transform.Find("Lock").gameObject;
+ GameObject carLockObj = gameobj.transform.Find("CarLock_Hand").gameObject;
+ GameObject lockObj = carLockObj.transform.Find("Lock").gameObject;
float distanceToLock = Vector3.Distance(damageInfo.HitPoint, lockObj.transform.position);
@@ -172,7 +171,7 @@ internal static bool isValidCarTrunkLockHit(DamageInfo damageInfo)
}
- internal static bool isValidContainerLockHit(DamageInfo damageInfo)
+ internal static bool isValidContainerLockHit(DamageInfoStruct damageInfo)
{
//check if door handle area was hit
Collider col = damageInfo.HitCollider;
@@ -180,10 +179,10 @@ internal static bool isValidContainerLockHit(DamageInfo damageInfo)
//if doorhandle exists and is hit
if (col.GetComponentInParent().GetComponentInChildren() != null)
{
- var gameobj = col.GetComponentInParent().gameObject;
+ GameObject gameobj = col.GetComponentInParent().gameObject;
//find child game object Lock from gameobj
- var lockObj = gameobj.transform.Find("Lock").gameObject;
+ GameObject lockObj = gameobj.transform.Find("Lock").gameObject;
float distanceToLock = Vector3.Distance(damageInfo.HitPoint, lockObj.transform.position);
return distanceToLock < 0.25f;
diff --git a/DoorBreachComponent.cs b/DoorBreachComponent.cs
index 46526f3..ebc7d54 100644
--- a/DoorBreachComponent.cs
+++ b/DoorBreachComponent.cs
@@ -98,8 +98,8 @@ private void ProcessObjectsOfType(string objectType, int interactiveLayer) wh
if (!IsValidObject(obj, ref invalidCount, ref inoperableCount, ref invalidLayerCount, interactiveLayer))
return;
- var randHitPoints = UnityEngine.Random.Range(DoorBreachPlugin.MinHitPoints.Value, DoorBreachPlugin.MaxHitPoints.Value);
- var hitpoints = obj.gameObject.GetOrAddComponent();
+ int randHitPoints = UnityEngine.Random.Range(DoorBreachPlugin.MinHitPoints.Value, DoorBreachPlugin.MaxHitPoints.Value);
+ Hitpoints hitpoints = obj.gameObject.GetOrAddComponent();
hitpoints.hitpoints = randHitPoints;
if (obj is Door door)
@@ -173,8 +173,13 @@ private bool IsValidObject(T obj, ref int invalidCount, ref int inoperableCou
return true;
}
- private bool IsValidDoorState(Door door) =>
- door.DoorState == EDoorState.Shut || door.DoorState == EDoorState.Locked || door.DoorState == EDoorState.Breaching || door.DoorState == EDoorState.Open;
+ private bool IsValidDoorState(Door door)
+ {
+ if(door.DoorState == EDoorState.Shut || door.DoorState == EDoorState.Locked || door.DoorState == EDoorState.Breaching || door.DoorState == EDoorState.Open)
+ return true;
+
+ return false;
+ }
private bool IsValidContainerState(LootableContainer container) =>
container.DoorState == EDoorState.Shut || container.DoorState == EDoorState.Locked || container.DoorState == EDoorState.Breaching;
@@ -198,7 +203,7 @@ public static void Enable()
{
if (Singleton.Instantiated)
{
- var gameWorld = Singleton.Instance;
+ GameWorld gameWorld = Singleton.Instance;
gameWorld.GetOrAddComponent();
}
}
@@ -226,10 +231,10 @@ internal static void SetupApplicableWeapons()
ApplicableWeapons.UnionWith(DoorBreachComponent.OtherWeapons);
#if DEBUG
//print out applicable weapons hashes to console
- Logger.LogInfo("Applicable Weapons:");
- foreach (var weapon in ApplicableWeapons)
+ Logger.LogDebug("Applicable Weapons:");
+ foreach (string weapon in ApplicableWeapons)
{
- Logger.LogInfo(weapon);
+ Logger.LogDebug(weapon);
}
#endif
}
diff --git a/ExplosiveBreachComponent.cs b/ExplosiveBreachComponent.cs
index 1b519ef..225797f 100644
--- a/ExplosiveBreachComponent.cs
+++ b/ExplosiveBreachComponent.cs
@@ -80,7 +80,7 @@ private IEnumerator LoadAudioClip(string filePath, bool isBeepClip)
internal static bool hasC4Explosives(Player player)
{
// Search playerItems for first c4 explosive
- var foundItem = player.Inventory.GetPlayerItems(EPlayerItems.Equipment).FirstOrDefault(x => x.TemplateId == C4ExplosiveId);
+ Item foundItem = player.Inventory.GetPlayerItems(EPlayerItems.Equipment).FirstOrDefault(x => x.TemplateId == C4ExplosiveId);
if (foundItem != null)
{
@@ -118,14 +118,14 @@ internal static void StartExplosiveBreach(Door door, Player player)
// Start a coroutine for the most recently placed TNT.
if (c4Instances.Any())
{
- var latestC4Instance = c4Instances.Last();
+ C4Instance latestC4Instance = c4Instances.Last();
StartDelayedExplosionCoroutine(door, player, componentInstance, latestC4Instance);
}
}
private static void TryPlaceC4OnDoor(Door door, Player player)
{
- var itemFactory = Singleton.Instance;
- var c4Item = itemFactory.CreateItem(MongoID.Generate(), C4ExplosiveId, null);
+ ItemFactoryClass itemFactory = Singleton.Instance;
+ Item c4Item = itemFactory.CreateItem(MongoID.Generate(), C4ExplosiveId, null);
Transform lockTransform = door.transform.Find("Lock");
Transform doorHandleTransform = null;
@@ -193,11 +193,11 @@ private static void TryPlaceC4OnDoor(Door door, Player player)
private static void RemoveItemFromPlayerInventory(Player player)
{
- var foundItem = player.Inventory.GetPlayerItems(EPlayerItems.Equipment).FirstOrDefault(x => x.TemplateId == C4ExplosiveId);
+ Item foundItem = player.Inventory.GetPlayerItems(EPlayerItems.Equipment).FirstOrDefault(x => x.TemplateId == C4ExplosiveId);
if (foundItem == null) return;
- var traderController = (TraderControllerClass)foundItem.Parent.GetOwner();
- var discardResult = InteractionsHandlerClass.Discard(foundItem, traderController, false, false);
+ TraderControllerClass traderController = (TraderControllerClass)foundItem.Parent.GetOwner();
+ GStruct455 discardResult = InteractionsHandlerClass.Discard(foundItem, traderController, false);
if (discardResult.Error != null)
{
@@ -320,7 +320,7 @@ private static void TriggerExplosion(Door door, Player player, C4Instance c4Inst
float damageMultiplier = Mathf.Clamp01(1 - distance / explosionRadius);
float damageAmount = baseDamage * damageMultiplier;
- DamageInfo damageInfo = new DamageInfo
+ DamageInfoStruct damageInfo = new DamageInfoStruct
{
DamageType = EDamageType.Explosion,
Damage = damageAmount,
@@ -382,7 +382,7 @@ public static void Enable()
{
if (Singleton.Instantiated)
{
- var gameWorld = Singleton.Instance;
+ GameWorld gameWorld = Singleton.Instance;
gameWorld.GetOrAddComponent();
}
diff --git a/PackageMods.ps1 b/PackageMods.ps1
deleted file mode 100644
index 0203c1d..0000000
--- a/PackageMods.ps1
+++ /dev/null
@@ -1,117 +0,0 @@
-param (
- [string]$ConfigurationName,
- [string]$TargetPath,
- [string]$TargetName,
- [string]$TargetDir
-)
-
-# Define the base directory
-$baseDir = "F:\SPT-AKI-DEV\BepInEx\plugins"
-
-# Function to log messages to the console
-function Log {
- param (
- [string]$message
- )
- $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
- Write-Host "$timestamp - $message"
-}
-
-Log "Script started"
-Log "ConfigurationName: $ConfigurationName"
-Log "TargetPath: $TargetPath"
-Log "TargetName: $TargetName"
-Log "TargetDir: $TargetDir"
-
-# Get the assembly version
-$assembly = [System.Reflection.Assembly]::LoadFile($TargetPath)
-$version = $assembly.GetName().Version.ToString()
-Log "Assembly version: $version"
-
-# Determine the directory of the deployed DLL
-$deployDir = Split-Path -Parent $TargetPath
-Log "DeployDir: $deployDir"
-
-# Check if the deploy directory is the base directory or one level further
-if ($deployDir -ne $baseDir) {
- $relativePath = $deployDir.Substring($baseDir.Length + 1) # Get the relative path beyond the base directory
- Log "RelativePath: $relativePath"
- if (($relativePath -split '\\').Count -eq 1) { # Check if it's exactly one directory level further
- $directoryName = (Get-Item $deployDir).Name
- $zipPath = "F:\SPT-AKI-DEV\BepInEx\plugins\$directoryName-v$version.zip"
- Log "DirectoryName: $directoryName"
- Log "ZipPath: $zipPath"
-
- # Remove existing zip file if it exists
- if (Test-Path $zipPath) {
- Log "ZipPath exists, removing"
- Remove-Item $zipPath -Force
- }
-
- # Create the temp directory structure
- $tempZipDir = "F:\SPT-AKI-DEV\tempZip"
- if (Test-Path $tempZipDir) {
- Log "TempZipDir exists, removing"
- Remove-Item $tempZipDir -Recurse -Force
- }
- New-Item -ItemType Directory -Path $tempZipDir
- Log "TempZipDir created: $tempZipDir"
-
- $newZipStructure = Join-Path $tempZipDir "Bepinex\plugins\$directoryName"
- New-Item -ItemType Directory -Path $newZipStructure -Force
- Log "New zip structure directory created: $newZipStructure"
-
- # Copy files to the new zip structure
- Copy-Item -Path "$TargetDir\*" -Destination $newZipStructure -Recurse -Force
- Log "Files copied to new zip structure"
-
- # Create the final zip file
- Add-Type -AssemblyName System.IO.Compression.FileSystem
- [System.IO.Compression.ZipFile]::CreateFromDirectory($tempZipDir, $zipPath)
- Log "Final zip file created: $zipPath"
-
- # Clean up temp directory
- Remove-Item $tempZipDir -Recurse -Force
- Log "TempZipDir removed"
- } else {
- Log "RelativePath is not one directory level further"
- }
-} else {
- $zipPath = "F:\SPT-AKI-DEV\BepInEx\plugins\$TargetName-v$version.zip"
- Log "ZipPath: $zipPath"
-
- # Remove existing zip file if it exists
- if (Test-Path $zipPath) {
- Log "ZipPath exists, removing"
- Remove-Item $zipPath -Force
- }
-
- # Create the temp directory structure
- $tempZipDir = "F:\SPT-AKI-DEV\tempZip"
- if (Test-Path $tempZipDir) {
- Log "TempZipDir exists, removing"
- Remove-Item $tempZipDir -Recurse -Force
- }
- New-Item -ItemType Directory -Path $tempZipDir
- Log "TempZipDir created: $tempZipDir"
-
- # Create the required folder structure within the temp directory
- $bepinexPluginsDir = Join-Path $tempZipDir "Bepinex\plugins"
- New-Item -ItemType Directory -Path $bepinexPluginsDir -Force
- Log "Bepinex\plugins directory created: $bepinexPluginsDir"
-
- # Copy the single DLL to the new structure
- Copy-Item -Path $TargetPath -Destination $bepinexPluginsDir -Force
- Log "DLL copied to Bepinex\plugins directory"
-
- # Create the final zip file
- Add-Type -AssemblyName System.IO.Compression.FileSystem
- [System.IO.Compression.ZipFile]::CreateFromDirectory($tempZipDir, $zipPath)
- Log "Final zip file created: $zipPath"
-
- # Clean up temp directory
- Remove-Item $tempZipDir -Recurse -Force
- Log "TempZipDir removed"
-}
-
-Log "Script finished"
diff --git a/Patches/ActionMenuDoorPatch.cs b/Patches/ActionMenuDoorPatch.cs
index a88f2ff..b5012a0 100644
--- a/Patches/ActionMenuDoorPatch.cs
+++ b/Patches/ActionMenuDoorPatch.cs
@@ -3,33 +3,34 @@
using SPT.Reflection.Patching;
using EFT;
using EFT.Interactive;
+using DoorBreach;
namespace BackdoorBandit.Patches
{
internal class ActionMenuDoorPatch : ModulePatch
{
- protected override MethodBase GetTargetMethod() => typeof(GetActionsClass).GetMethod(nameof(GetActionsClass.smethod_10));
+ protected override MethodBase GetTargetMethod() => typeof(GetActionsClass).GetMethod(nameof(GetActionsClass.smethod_14));
[PatchPostfix]
public static void Postfix(ref ActionsReturnClass __result, GamePlayerOwner owner, Door door)
{
- // Add an additional action after the original method executes
- if (__result != null && __result.Actions != null)
+ if (__result == null || __result.Actions == null) return;
+
+ // Add new action to exisitng actions
+ ActionsTypesClass breachC4 = new ActionsTypesClass
{
- __result.Actions.Add(new ActionsTypesClass
+ Name = "Plant Explosive",
+ Action = new Action(() =>
{
- Name = "Plant Explosive",
- Action = new Action(() =>
- {
- BackdoorBandit.ExplosiveBreachComponent.StartExplosiveBreach(door, owner.Player);
+ BackdoorBandit.ExplosiveBreachComponent.StartExplosiveBreach(door, owner.Player);
+ }),
+ Disabled = (!door.IsBreachAngle(owner.Player.Position) || !BackdoorBandit.ExplosiveBreachComponent.IsValidDoorState(door) ||
+ !BackdoorBandit.ExplosiveBreachComponent.hasC4Explosives(owner.Player))
+ };
- }),
- Disabled = (!door.IsBreachAngle(owner.Player.Position) || !BackdoorBandit.ExplosiveBreachComponent.IsValidDoorState(door) ||
- !BackdoorBandit.ExplosiveBreachComponent.hasC4Explosives(owner.Player))
- });
- }
+ __result.Actions.Add(breachC4);
}
}
}
\ No newline at end of file
diff --git a/Patches/ActionMenuKeyCardPatch.cs b/Patches/ActionMenuKeyCardPatch.cs
index 065d16b..77e1859 100644
--- a/Patches/ActionMenuKeyCardPatch.cs
+++ b/Patches/ActionMenuKeyCardPatch.cs
@@ -10,7 +10,7 @@ namespace BackdoorBandit.Patches
{
internal class ActionMenuKeyCardPatch : ModulePatch
{
- protected override MethodBase GetTargetMethod() => typeof(GetActionsClass).GetMethod(nameof(GetActionsClass.smethod_9));
+ protected override MethodBase GetTargetMethod() => typeof(GetActionsClass).GetMethod(nameof(GetActionsClass.smethod_13));
// Check if an action is already added. Hopefully door's action takes precedence
@@ -20,21 +20,24 @@ public static bool IsActionAdded(List actions, string actionN
}
[PatchPostfix]
- public static void Postfix(ref ActionsReturnClass __result, GamePlayerOwner owner, Door door)
+ public static void Postfix(ref ActionsReturnClass __result, GamePlayerOwner owner, Door door, bool isProxy)
{
- if (__result != null && __result.Actions != null && !IsActionAdded(__result.Actions, "Plant Explosive"))
+ if (__result == null || __result.Actions == null || IsActionAdded(__result.Actions, "Plant Explosive"))
{
- __result.Actions.Add(new ActionsTypesClass
- {
- Name = "Plant Explosive",
- Action = new Action(() =>
- {
- BackdoorBandit.ExplosiveBreachComponent.StartExplosiveBreach(door, owner.Player);
- }),
- Disabled = (!door.IsBreachAngle(owner.Player.Position) || !BackdoorBandit.ExplosiveBreachComponent.IsValidDoorState(door) ||
- !BackdoorBandit.ExplosiveBreachComponent.hasC4Explosives(owner.Player))
- });
+ return;
}
+
+ // Add new action after existing actions
+ __result.Actions.Add(new ActionsTypesClass
+ {
+ Name = "Plant Explosive",
+ Action = new Action(() =>
+ {
+ BackdoorBandit.ExplosiveBreachComponent.StartExplosiveBreach(door, owner.Player);
+ }),
+ Disabled = (!door.IsBreachAngle(owner.Player.Position) || !BackdoorBandit.ExplosiveBreachComponent.IsValidDoorState(door) ||
+ !BackdoorBandit.ExplosiveBreachComponent.hasC4Explosives(owner.Player))
+ });
}
}
}
diff --git a/Patches/ApplyHitPatch.cs b/Patches/ApplyHitPatch.cs
index 08ea209..d04f219 100644
--- a/Patches/ApplyHitPatch.cs
+++ b/Patches/ApplyHitPatch.cs
@@ -30,7 +30,7 @@ internal class ApplyHit : ModulePatch
[PatchPostfix]
- public static void PatchPostFix(DamageInfo damageInfo, GStruct389 shotID)
+ public static void PatchPostFix(DamageInfoStruct damageInfo, ShotIdStruct shotID)
{
//try catch for random things applying damage that we don't want
try
@@ -43,7 +43,7 @@ public static void PatchPostFix(DamageInfo damageInfo, GStruct389 shotID)
catch { }
}
- private static bool ShouldApplyDamage(DamageInfo damageInfo)
+ private static bool ShouldApplyDamage(DamageInfoStruct damageInfo)
{
return damageInfo.Player != null
&& damageInfo.Player.iPlayer.IsYourPlayer
@@ -52,7 +52,7 @@ private static bool ShouldApplyDamage(DamageInfo damageInfo)
&& damageInfo.DamageType != EDamageType.Explosion;
}
- private static void HandleDamageForEntity(DamageInfo damageInfo, BallisticCollider collider)
+ private static void HandleDamageForEntity(DamageInfoStruct damageInfo, BallisticCollider collider)
{
bool isCarTrunk = false;
bool isLootableContainer = false;
@@ -85,7 +85,7 @@ private static void HandleDamageForEntity(DamageInfo damageInfo, BallisticCollid
}
#region DamageApplication
- private static void HandleCarTrunkDamage(DamageInfo damageInfo, BallisticCollider collider, ref bool validDamage)
+ private static void HandleCarTrunkDamage(DamageInfoStruct damageInfo, BallisticCollider collider, ref bool validDamage)
{
if (!DoorBreachPlugin.PlebMode.Value && DoorBreachPlugin.OpenCarDoors.Value)
{
@@ -96,13 +96,13 @@ private static void HandleCarTrunkDamage(DamageInfo damageInfo, BallisticCollide
{
if (hitpoints.hitpoints <= 0)
{
- var carTrunk = entity.GetComponentInParent();
+ Trunk carTrunk = entity.GetComponentInParent();
OpenDoorIfNotAlreadyOpen(carTrunk, damageInfo.Player.AIData.Player, EInteractionType.Open);
}
});
}
- private static void HandleLootableContainerDamage(DamageInfo damageInfo, BallisticCollider collider, ref bool validDamage)
+ private static void HandleLootableContainerDamage(DamageInfoStruct damageInfo, BallisticCollider collider, ref bool validDamage)
{
if (!DoorBreachPlugin.PlebMode.Value && DoorBreachPlugin.OpenLootableContainers.Value)
{
@@ -113,13 +113,13 @@ private static void HandleLootableContainerDamage(DamageInfo damageInfo, Ballist
{
if (hitpoints.hitpoints <= 0)
{
- var lootContainer = entity.GetComponentInParent();
+ LootableContainer lootContainer = entity.GetComponentInParent();
OpenDoorIfNotAlreadyOpen(lootContainer, damageInfo.Player.AIData.Player, EInteractionType.Open);
}
});
}
- internal static void HandleDoorDamage(DamageInfo damageInfo, BallisticCollider collider, ref bool validDamage)
+ internal static void HandleDoorDamage(DamageInfoStruct damageInfo, BallisticCollider collider, ref bool validDamage)
{
if (!DoorBreachPlugin.PlebMode.Value)
{
@@ -128,17 +128,25 @@ internal static void HandleDoorDamage(DamageInfo damageInfo, BallisticCollider c
HandleDamage(damageInfo, collider, ref validDamage, "Door", (hitpoints, entity) =>
{
+ WorldInteractiveObject door = entity.GetComponentInParent();
+
+#if DEBUG
+ DoorBreachComponent.Logger.LogDebug("[Door info]");
+ DoorBreachComponent.Logger.LogDebug($"KeyId: {door.KeyId}");
+ DoorBreachComponent.Logger.LogDebug($"DoorState: {door.DoorState}");
+ DoorBreachComponent.Logger.LogDebug($"InitialDoorState: {door.InitialDoorState}");
+#endif
+
if (hitpoints.hitpoints <= 0)
{
- var door = entity.GetComponentInParent();
OpenDoorIfNotAlreadyOpen(door, damageInfo.Player.AIData.Player, EInteractionType.Breach);
}
});
}
- internal static void HandleDamage(DamageInfo damageInfo, BallisticCollider collider, ref bool validDamage, string entityName, Action onHitpointsZero)
+ internal static void HandleDamage(DamageInfoStruct damageInfo, BallisticCollider collider, ref bool validDamage, string entityName, Action onHitpointsZero)
{
- var hitpoints = collider.GetComponentInParent() as Hitpoints;
+ Hitpoints hitpoints = collider.GetComponentInParent() as Hitpoints;
if (validDamage)
{
@@ -187,7 +195,7 @@ internal static void OpenDoorIfNotAlreadyOpen(T entity, Player player, EInter
}
- #endregion
+#endregion
diff --git a/Patches/PerfectCullingNullRefPatch.cs b/Patches/PerfectCullingNullRefPatch.cs
index 62f0e20..d815e93 100644
--- a/Patches/PerfectCullingNullRefPatch.cs
+++ b/Patches/PerfectCullingNullRefPatch.cs
@@ -18,7 +18,7 @@ static bool Prefix(PerfectCullingBakeGroup __instance, bool rendererEnabled, int
// Safely handle Renderer[] array
if (__instance.runtimeProxies != null)
{
- foreach (var renderer in __instance.runtimeProxies)
+ foreach (Renderer renderer in __instance.runtimeProxies)
{
if (renderer != null)
{
@@ -30,7 +30,7 @@ static bool Prefix(PerfectCullingBakeGroup __instance, bool rendererEnabled, int
// Safely handle CullingObject array
if (__instance.cullingLightObjects != null)
{
- foreach (var cullingObject in __instance.cullingLightObjects)
+ foreach (CullingObject cullingObject in __instance.cullingLightObjects)
{
if (cullingObject != null)
{
@@ -42,7 +42,7 @@ static bool Prefix(PerfectCullingBakeGroup __instance, bool rendererEnabled, int
// Safely handle AnalyticSource array
if (__instance.analyticSources != null)
{
- foreach (var analyticSource in __instance.analyticSources)
+ foreach (AnalyticSource analyticSource in __instance.analyticSources)
{
if (analyticSource != null)
{
diff --git a/Plugin.cs b/Plugin.cs
index d7a2e93..d82748e 100644
--- a/Plugin.cs
+++ b/Plugin.cs
@@ -11,8 +11,7 @@
namespace DoorBreach
{
- [BepInPlugin("com.dvize.BackdoorBandit", "dvize.BackdoorBandit", "1.9.0")]
- //[BepInDependency("com.spt-aki.core", "3.7.6")]
+ [BepInPlugin("com.dvize.BackdoorBandit", "dvize.BackdoorBandit", "1.11.1")]
public class DoorBreachPlugin : BaseUnityPlugin
{
public static ConfigEntry PlebMode;
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
index 3f62ddd..5c2088f 100644
--- a/Properties/AssemblyInfo.cs
+++ b/Properties/AssemblyInfo.cs
@@ -7,7 +7,7 @@
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("dvize.BackdoorBandit")]
-[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -18,6 +18,6 @@
[assembly: Guid("8fdfc616-1704-4056-a87e-2783692cc153")]
-[assembly: AssemblyVersion("1.9.0.0")]
-[assembly: AssemblyFileVersion("1.9.0.0")]
-[assembly: TarkovVersion(29197)]
+[assembly: AssemblyVersion("1.11.2")]
+[assembly: AssemblyFileVersion("1.11.2")]
+[assembly: TarkovVersion(35392)]
diff --git a/server files/BepInEx/plugins/dvize.BackdoorBandit/Beep.mp3 b/server files/BepInEx/plugins/dvize.BackdoorBandit/Beep.mp3
new file mode 100644
index 0000000..07b424d
Binary files /dev/null and b/server files/BepInEx/plugins/dvize.BackdoorBandit/Beep.mp3 differ
diff --git a/server files/BepInEx/plugins/dvize.BackdoorBandit/FinalBeepTone.mp3 b/server files/BepInEx/plugins/dvize.BackdoorBandit/FinalBeepTone.mp3
new file mode 100644
index 0000000..d439391
Binary files /dev/null and b/server files/BepInEx/plugins/dvize.BackdoorBandit/FinalBeepTone.mp3 differ
diff --git a/server files/BepInEx/plugins/dvize.BackdoorBandit/GrenadeLaunchers.json b/server files/BepInEx/plugins/dvize.BackdoorBandit/GrenadeLaunchers.json
new file mode 100644
index 0000000..0ba0dd4
--- /dev/null
+++ b/server files/BepInEx/plugins/dvize.BackdoorBandit/GrenadeLaunchers.json
@@ -0,0 +1,7 @@
+[
+ "62e7e7bbe6da9612f743f1e0",
+ "6357c98711fb55120211f7e1",
+ "5e81ebcd8e146c7080625e15",
+ "5d52cc5ba4b9367408500062",
+ "6275303a9f372d6ea97f9ec7"
+]
diff --git a/server files/BepInEx/plugins/dvize.BackdoorBandit/MeleeWeapons.json b/server files/BepInEx/plugins/dvize.BackdoorBandit/MeleeWeapons.json
new file mode 100644
index 0000000..7fa2958
--- /dev/null
+++ b/server files/BepInEx/plugins/dvize.BackdoorBandit/MeleeWeapons.json
@@ -0,0 +1,7 @@
+[
+ "63a0b208f444d32d6f03ea1e",
+ "6087e570b998180e9f76dc24",
+ "5c07df7f0db834001b73588a",
+ "5bffe7930db834001b734a39",
+ "57cd379a24597778e7682ecf"
+]
diff --git a/server files/BepInEx/plugins/dvize.BackdoorBandit/OtherWeapons.json b/server files/BepInEx/plugins/dvize.BackdoorBandit/OtherWeapons.json
new file mode 100644
index 0000000..e062f20
--- /dev/null
+++ b/server files/BepInEx/plugins/dvize.BackdoorBandit/OtherWeapons.json
@@ -0,0 +1,3 @@
+[
+ ""
+]
diff --git a/server files/BepInEx/plugins/dvize.BackdoorBandit/ShotgunWeapons.json b/server files/BepInEx/plugins/dvize.BackdoorBandit/ShotgunWeapons.json
new file mode 100644
index 0000000..fcce5fb
--- /dev/null
+++ b/server files/BepInEx/plugins/dvize.BackdoorBandit/ShotgunWeapons.json
@@ -0,0 +1,14 @@
+[
+ "5580223e4bdc2d1c128b457f",
+ "54491c4f4bdc2db1078b4568",
+ "6259b864ebedf17603599e88",
+ "5e870397991fd70db46995c8",
+ "576165642459773c7a400233",
+ "5447b6094bdc2dc3278b4567",
+ "60db29ce99594040e04c4a27",
+ "5580223e4bdc2d1c128b457f",
+ "56dee2bdd2720bc8328b4567",
+ "606dae0ab0e443224b421bb7",
+ "64748cb8de82c85eaf0a273a",
+ "5a7828548dc32e5a9c28b516"
+]
diff --git a/server files/user/mods/DoorBreacher/bundles.json b/server files/user/mods/DoorBreacher/bundles.json
new file mode 100644
index 0000000..e7276a2
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/bundles.json
@@ -0,0 +1,28 @@
+{
+ "manifest": [
+ {
+ "key": "DoorBreacher.bundle",
+ "dependencyKeys": [
+ "cubemaps",
+ "shaders",
+ "assets/commonassets/physics/physicsmaterials.bundle"
+ ]
+ },
+ {
+ "key": "DoorBreacherBox.bundle",
+ "dependencyKeys": [
+ "cubemaps",
+ "shaders",
+ "assets/commonassets/physics/physicsmaterials.bundle"
+ ]
+ },
+ {
+ "key": "C4Explosive.bundle",
+ "dependencyKeys": [
+ "cubemaps",
+ "shaders",
+ "assets/commonassets/physics/physicsmaterials.bundle"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/bundles/C4Explosive.bundle b/server files/user/mods/DoorBreacher/bundles/C4Explosive.bundle
new file mode 100644
index 0000000..576bd69
Binary files /dev/null and b/server files/user/mods/DoorBreacher/bundles/C4Explosive.bundle differ
diff --git a/server files/user/mods/DoorBreacher/bundles/DoorBreacher.bundle b/server files/user/mods/DoorBreacher/bundles/DoorBreacher.bundle
new file mode 100644
index 0000000..5dd5dce
Binary files /dev/null and b/server files/user/mods/DoorBreacher/bundles/DoorBreacher.bundle differ
diff --git a/server files/user/mods/DoorBreacher/bundles/DoorBreacherBox.bundle b/server files/user/mods/DoorBreacher/bundles/DoorBreacherBox.bundle
new file mode 100644
index 0000000..fde5c4c
Binary files /dev/null and b/server files/user/mods/DoorBreacher/bundles/DoorBreacherBox.bundle differ
diff --git a/server files/user/mods/DoorBreacher/database/templates/craftingItem.jsonc b/server files/user/mods/DoorBreacher/database/templates/craftingItem.jsonc
new file mode 100644
index 0000000..693e86c
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/database/templates/craftingItem.jsonc
@@ -0,0 +1,54 @@
+[
+ {
+ "_id": "665d4ce7e381d16c8676292b", // Unique MongoID for this recipe
+ "areaType": 10,
+ "requirements": [
+ {
+ "areaType": 10, //Workbench
+ "requiredLevel": 2,
+ "type": "Area"
+ },
+ {
+ "templateId": "60391b0fb847c71012789415", // TNT
+ "count": 2,
+ "isFunctional": false,
+ "isEncoded": false,
+ "type": "Item"
+ },
+ {
+ "templateId": "5c06779c86f77426e00dd782", // Bundle of wires
+ "count": 1,
+ "isFunctional": false,
+ "isEncoded": false,
+ "type": "Item"
+ },
+ {
+ "templateId": "5e2af51086f7746d3f3c3402", // Greade fuze
+ "count": 1,
+ "isFunctional": false,
+ "isEncoded": false,
+ "type": "Item"
+ },
+ {
+ "templateId": "5d0376a486f7747d8050965c", // Military circuit board
+ "count": 1,
+ "isFunctional": false,
+ "isEncoded": false,
+ "type": "Item"
+ },
+ {
+ "templateId": "5af04b6486f774195a3ebb49", // Pliers elite tool
+ "type": "Tool"
+ }
+ ],
+ "productionTime": 3600, // Time to craft in seconds
+ "needFuelForAllProductionTime": false,
+ "locked": false,
+ "endProduct": "6636606320e842b50084e51a", // C4 template_id, don't change
+ "continuous": false,
+ "count": 1, // How many can be produced at once
+ "productionLimitCount": 0,
+ "isEncoded": false,
+ "isCodeProduction": false
+ }
+]
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/database/templates/items.jsonc b/server files/user/mods/DoorBreacher/database/templates/items.jsonc
new file mode 100644
index 0000000..40c88f2
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/database/templates/items.jsonc
@@ -0,0 +1,290 @@
+{
+ "doorbreacher": {
+ "_id": "660249a0712c1005a4a3ab41", //unique mongo id for doorbreacher shell
+ "_name": "doorbreacher",
+ "_parent": "5485a8684bdc2da71d8b4567", //ammo
+ "_type": "Item",
+ "_props": {
+ "Name": "12x70 Breaching Round",
+ "ShortName": "12x70",
+ "Description": "12x70 Breacher",
+ "Weight": 0.048,
+ "BackgroundColor": "yellow",
+ "Width": 1,
+ "Height": 1,
+ "StackMaxSize": 20,
+ "ItemSound": "ammo_shotgun",
+ "Prefab": {
+ "path": "DoorBreacher.bundle",
+ "rcid": ""
+ },
+ "UsePrefab": {
+ "path": "",
+ "rcid": ""
+ },
+ "StackObjectsCount": 1,
+ "NotShownInSlot": false,
+ "ExaminedByDefault": false,
+ "ExamineTime": 1,
+ "IsUndiscardable": false,
+ "IsUnsaleable": false,
+ "IsUnbuyable": false,
+ "IsUngivable": false,
+ "IsLockedafterEquip": false,
+ "QuestItem": false,
+ "LootExperience": 0,
+ "ExamineExperience": 100,
+ "HideEntrails": false,
+ "RepairCost": 0,
+ "RepairSpeed": 0,
+ "ExtraSizeLeft": 0,
+ "ExtraSizeRight": 0,
+ "ExtraSizeUp": 0,
+ "ExtraSizeDown": 0,
+ "ExtraSizeForceAdd": false,
+ "MergesWithChildren": false,
+ "CanSellOnRagfair": true,
+ "CanRequireOnRagfair": true,
+ "ConflictingItems": [],
+ "Unlootable": false,
+ "UnlootableFromSlot": "FirstPrimaryWeapon",
+ "UnlootableFromSide": [],
+ "AnimationVariantsNumber": 0,
+ "DiscardingBlock": false,
+ "RagFairCommissionModifier": 1,
+ "IsAlwaysAvailableForInsurance": false,
+ "DiscardLimit": -1,
+ "DropSoundType": "None",
+ "InsuranceDisabled": false,
+ "QuestStashMaxCount": 0,
+ "IsSpecialSlotOnly": false,
+ "IsUnremovable": false,
+ "StackMinRandom": 1,
+ "StackMaxRandom": 10,
+ "ammoType": "bullet",
+ "InitialSpeed": 370,
+ "BallisticCoeficient": 0.121,
+ "BulletMassGram": 45,
+ "BulletDiameterMilimeters": 13,
+ "Damage": 197,
+ "ammoAccr": 115,
+ "ammoRec": 0,
+ "ammoDist": 0,
+ "buckshotBullets": 8,
+ "PenetrationPower": 26,
+ "PenetrationPowerDiviation": 1,
+ "ammoHear": 0,
+ "ammoSfx": "standart",
+ "MisfireChance": 0.00,
+ "MinFragmentsCount": 1,
+ "MaxFragmentsCount": 1,
+ "ammoShiftChance": 0,
+ "casingName": "12x70 Breacher",
+ "casingEjectPower": 0,
+ "casingMass": 0,
+ "casingSounds": "shotgun_big",
+ "ProjectileCount": 1,
+ "PenetrationChance": 0.28,
+ "RicochetChance": 0.1,
+ "FragmentationChance": 0.05,
+ "Deterioration": 1,
+ "SpeedRetardation": 0.00013,
+ "Tracer": false,
+ "TracerColor": "red",
+ "TracerDistance": 0.5,
+ "ArmorDamage": 57,
+ "Caliber": "Caliber12g",
+ "StaminaBurnPerDamage": 0.21024,
+ "HeavyBleedingDelta": 0.15,
+ "LightBleedingDelta": 0,
+ "ShowBullet": false,
+ "HasGrenaderComponent": false,
+ "FuzeArmTimeSec": 0,
+ "ExplosionStrength": 0,
+ "MinExplosionDistance": 0,
+ "MaxExplosionDistance": 0,
+ "FragmentsCount": 0,
+ "FragmentType": "5996f6d686f77467977ba6cc",
+ "ShowHitEffectOnExplode": false,
+ "ExplosionType": "",
+ "AmmoLifeTimeSec": 5,
+ "Contusion": {
+ "x": 0,
+ "y": 0,
+ "z": 0
+ },
+ "ArmorDistanceDistanceDamage": {
+ "x": 0,
+ "y": 0,
+ "z": 0
+ },
+ "Blindness": {
+ "x": 0,
+ "y": 0,
+ "z": 0
+ },
+ "IsLightAndSoundShot": false,
+ "LightAndSoundShotAngle": 0,
+ "LightAndSoundShotSelfContusionTime": 0,
+ "LightAndSoundShotSelfContusionStrength": 0,
+ "MalfMisfireChance": 0.0,
+ "DurabilityBurnModificator": 1.5,
+ "HeatFactor": 2.51,
+ "MalfFeedChance": 0,
+ "RemoveShellAfterFire": false
+ },
+ "_proto": "560d5e524bdc2d25448b4571" //12/70 buckshot
+ },
+
+ "doorbreacherbox": {
+ "_id": "660ed60d5effc350b86bb289", //unique mongo id for doorbreacher box
+ "_name": "doorbreacherbox",
+ "_parent": "543be5cb4bdc2deb348b4568", // Ammo Box
+ "_type": "Item",
+ "_props": {
+ "Name": "ammo_box_12x70_default",
+ "ShortName": "ammo_box_12x70_default",
+ "Description": "ammo_box_12x70_default",
+ "Weight": 0.35,
+ "BackgroundColor": "yellow",
+ "Width": 1,
+ "Height": 1,
+ "StackMaxSize": 1,
+ "ItemSound": "ammo_pack_generic",
+ "Prefab": {
+ "path": "DoorBreacherBox.bundle",
+ "rcid": ""
+ },
+ "UsePrefab": {
+ "path": "",
+ "rcid": ""
+ },
+ "StackObjectsCount": 1,
+ "NotShownInSlot": false,
+ "ExaminedByDefault": true,
+ "ExamineTime": 1,
+ "IsUndiscardable": false,
+ "IsUnsaleable": false,
+ "IsUnbuyable": false,
+ "IsUngivable": false,
+ "IsLockedafterEquip": false,
+ "QuestItem": false,
+ "LootExperience": 0,
+ "ExamineExperience": 10,
+ "HideEntrails": true,
+ "RepairCost": 0,
+ "RepairSpeed": 0,
+ "ExtraSizeLeft": 0,
+ "ExtraSizeRight": 0,
+ "ExtraSizeUp": 0,
+ "ExtraSizeDown": 0,
+ "ExtraSizeForceAdd": false,
+ "MergesWithChildren": false,
+ "CanSellOnRagfair": true,
+ "CanRequireOnRagfair": true,
+ "ConflictingItems": [],
+ "Unlootable": false,
+ "UnlootableFromSlot": "FirstPrimaryWeapon",
+ "UnlootableFromSide": [],
+ "AnimationVariantsNumber": 0,
+ "DiscardingBlock": false,
+ "RagFairCommissionModifier": 1,
+ "IsAlwaysAvailableForInsurance": false,
+ "DiscardLimit": -1,
+ "DropSoundType": "None",
+ "InsuranceDisabled": false,
+ "QuestStashMaxCount": 0,
+ "IsSpecialSlotOnly": false,
+ "IsUnremovable": false,
+ "StackMinRandom": 5,
+ "StackMaxRandom": 5,
+ "ammoCaliber": "Caliber12g",
+ "StackSlots": [
+ {
+ "_name": "cartridges",
+ "_id": "6627cd20915ff02066761f3f", //? unique id for stack slot
+ "_parent": "660ed60d5effc350b86bb289", //doorbreacher box
+ "_max_count": 5,
+ "_props": {
+ "filters": [
+ {
+ "Filter": [
+ "660249a0712c1005a4a3ab41" //doorbreacher shell
+ ]
+ }
+ ]
+ },
+ "_proto": "5748538b2459770af276a261" //??
+ }
+ ]
+ }
+ },
+
+ "C4Explosive":{
+ "_id": "6636606320e842b50084e51a", //unique mongo id for c4explosive
+ "_name": "C4Explosive",
+ "_parent": "57864e4c24597754843f8723", //lubricant
+ "_type": "Item",
+ "_props": {
+ "Name": "C4 Explosive",
+ "ShortName": "C4 Explosive",
+ "Description": "C4 Explosive",
+ "Weight": 0.6,
+ "BackgroundColor": "blue",
+ "Width": 2,
+ "Height": 1,
+ "StackMaxSize": 1,
+ "ItemSound": "generic",
+ "Prefab": {
+ "path": "C4Explosive.bundle",
+ "rcid": ""
+ },
+ "UsePrefab": {
+ "path": "",
+ "rcid": ""
+ },
+ "StackObjectsCount": 1,
+ "NotShownInSlot": false,
+ "ExaminedByDefault": true,
+ "ExamineTime": 1,
+ "IsUndiscardable": false,
+ "IsUnsaleable": false,
+ "IsUnbuyable": false,
+ "IsUngivable": false,
+ "IsLockedafterEquip": false,
+ "QuestItem": false,
+ "LootExperience": 20,
+ "ExamineExperience": 10,
+ "HideEntrails": false,
+ "RepairCost": 0,
+ "RepairSpeed": 0,
+ "ExtraSizeLeft": 0,
+ "ExtraSizeRight": 0,
+ "ExtraSizeUp": 0,
+ "ExtraSizeDown": 0,
+ "ExtraSizeForceAdd": false,
+ "MergesWithChildren": false,
+ "CanSellOnRagfair": true,
+ "CanRequireOnRagfair": false,
+ "ConflictingItems": [],
+ "Unlootable": false,
+ "UnlootableFromSlot": "FirstPrimaryWeapon",
+ "UnlootableFromSide": [],
+ "AnimationVariantsNumber": 0,
+ "DiscardingBlock": false,
+ "RagFairCommissionModifier": 1,
+ "IsAlwaysAvailableForInsurance": false,
+ "DiscardLimit": 0,
+ "DropSoundType": "None",
+ "InsuranceDisabled": false,
+ "QuestStashMaxCount": 0,
+ "IsSpecialSlotOnly": false,
+ "IsUnremovable": false,
+ "MaxResource": 0,
+ "Resource": 0
+ },
+ "_proto": "590a373286f774287540368b"
+ }
+
+}
+
diff --git a/server files/user/mods/DoorBreacher/package.json b/server files/user/mods/DoorBreacher/package.json
new file mode 100644
index 0000000..5365d20
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "props-doorbreacher",
+ "version": "1.11.0",
+ "main": "src/mod.js",
+ "isBundleMod": true,
+ "license": "MIT",
+ "author": "Props",
+ "sptVersion": "~3.11",
+ "loadBefore": [],
+ "loadAfter": [],
+ "incompatibilities": [],
+ "contributors": ["Tron", "MakerMacher"],
+ "scripts": {
+ "setup": "npm i",
+ "build": "node ./build.mjs",
+ "buildinfo": "node ./build.mjs --verbose"
+ },
+ "devDependencies": {
+ "@types/node": "^22.12.0",
+ "@typescript-eslint/eslint-plugin": "6.7.5",
+ "@typescript-eslint/parser": "6.7.5",
+ "archiver": "^6.0",
+ "eslint": "8.51.0",
+ "fs-extra": "^11.1",
+ "ignore": "^5.2",
+ "os": "^0.1",
+ "tsyringe": "4.8.0",
+ "typescript": "5.8.2",
+ "winston": "3.11.0"
+ }
+}
diff --git a/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.js b/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.js
new file mode 100644
index 0000000..cad5657
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.js
@@ -0,0 +1,102 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FluentAssortConstructor = void 0;
+class FluentAssortConstructor {
+ itemsToSell = [];
+ barterScheme = {};
+ loyaltyLevels = {};
+ hashUtil;
+ logger;
+ constructor(hashUtil, logger) {
+ this.hashUtil = hashUtil;
+ this.logger = logger;
+ }
+ createSingleAssortItem(itemTpl, itemId = undefined) {
+ const newItem = {
+ _id: itemId || this.hashUtil.generate(),
+ _tpl: itemTpl,
+ parentId: "hideout",
+ slotId: "hideout",
+ upd: { UnlimitedCount: false, StackObjectsCount: 100 }
+ };
+ this.itemsToSell.push(newItem);
+ return this;
+ }
+ addStackCount(stackCount, itemId) {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ item.upd.StackObjectsCount = stackCount;
+ }
+ return this;
+ }
+ addUnlimitedStackCount(itemId) {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ item.upd.StackObjectsCount = 999999;
+ item.upd.UnlimitedCount = true;
+ }
+ return this;
+ }
+ addBuyRestriction(maxBuyLimit, itemId) {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ item.upd.BuyRestrictionMax = maxBuyLimit;
+ item.upd.BuyRestrictionCurrent = 0;
+ }
+ return this;
+ }
+ addLoyaltyLevel(level, itemId) {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ this.loyaltyLevels[item._id] = level;
+ }
+ return this;
+ }
+ addMoneyCost(currencyType, amount, itemId) {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ this.barterScheme[item._id] = [[{ count: amount, _tpl: currencyType }]];
+ }
+ return this;
+ }
+ addBarterCost(itemTpl, count, itemId) {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (!this.barterScheme[item._id]) {
+ this.barterScheme[item._id] = [[{ count, _tpl: itemTpl }]];
+ }
+ else {
+ const scheme = this.barterScheme[item._id][0].find(s => s._tpl === itemTpl);
+ if (scheme) {
+ scheme.count += count;
+ }
+ else {
+ this.barterScheme[item._id][0].push({ count, _tpl: itemTpl });
+ }
+ }
+ return this;
+ }
+ export(data) {
+ for (const item of this.itemsToSell) {
+ if (data.assort.items.some(i => i._id === item._id)) {
+ this.logger.error(`Item with ID ${item._id} already exists in the assortment.`);
+ return;
+ }
+ //this.logger.info(`Adding item with ID ${item._id} to the assortment.`)
+ //this.logger.info(item);
+ data.assort.items.push(item);
+ if (this.barterScheme[item._id]) {
+ data.assort.barter_scheme[item._id] = this.barterScheme[item._id];
+ }
+ if (this.loyaltyLevels[item._id]) {
+ data.assort.loyal_level_items[item._id] = this.loyaltyLevels[item._id];
+ }
+ }
+ // Reset internal state
+ this.itemsToSell = [];
+ this.barterScheme = {};
+ this.loyaltyLevels = {};
+ return this;
+ }
+}
+exports.FluentAssortConstructor = FluentAssortConstructor;
+//# sourceMappingURL=fluentTraderAssortCreator.js.map
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.js.map b/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.js.map
new file mode 100644
index 0000000..349e6e8
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "fluentTraderAssortCreator.js",
+ "sourceRoot": "",
+ "sources": [
+ "fluentTraderAssortCreator.ts"
+ ],
+ "names": [],
+ "mappings": "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"
+}
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.ts b/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.ts
new file mode 100644
index 0000000..2455461
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/fluentTraderAssortCreator.ts
@@ -0,0 +1,113 @@
+import { IItem } from "@spt/models/eft/common/tables/IItem";
+import { IBarterScheme, ITrader } from "@spt/models/eft/common/tables/ITrader";
+import { Money } from "@spt/models/enums/Money";
+import { ILogger } from "@spt/models/spt/utils/ILogger";
+import { HashUtil } from "@spt/utils/HashUtil";
+
+export class FluentAssortConstructor {
+ protected itemsToSell: IItem[] = [];
+ protected barterScheme: Record = {};
+ protected loyaltyLevels: Record = {};
+ protected hashUtil: HashUtil;
+ protected logger: ILogger;
+
+ constructor(hashUtil: HashUtil, logger: ILogger) {
+ this.hashUtil = hashUtil;
+ this.logger = logger;
+ }
+
+ public createSingleAssortItem(itemTpl: string, itemId = undefined): FluentAssortConstructor {
+ const newItem: IItem = {
+ _id: itemId || this.hashUtil.generate(),
+ _tpl: itemTpl,
+ parentId: "hideout",
+ slotId: "hideout",
+ upd: { UnlimitedCount: false, StackObjectsCount: 100 }
+ };
+ this.itemsToSell.push(newItem);
+ return this;
+ }
+
+ public addStackCount(stackCount: number, itemId?: string): FluentAssortConstructor {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ item.upd.StackObjectsCount = stackCount;
+ }
+ return this;
+ }
+
+ public addUnlimitedStackCount(itemId?: string): FluentAssortConstructor {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ item.upd.StackObjectsCount = 999999;
+ item.upd.UnlimitedCount = true;
+ }
+ return this;
+ }
+
+ public addBuyRestriction(maxBuyLimit: number, itemId?: string): FluentAssortConstructor {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ item.upd.BuyRestrictionMax = maxBuyLimit;
+ item.upd.BuyRestrictionCurrent = 0;
+ }
+ return this;
+ }
+
+ public addLoyaltyLevel(level: number, itemId?: string): FluentAssortConstructor {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ this.loyaltyLevels[item._id] = level;
+ }
+ return this;
+ }
+
+ public addMoneyCost(currencyType: Money, amount: number, itemId?: string): FluentAssortConstructor {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (item) {
+ this.barterScheme[item._id] = [[{ count: amount, _tpl: currencyType }]];
+ }
+ return this;
+ }
+
+ public addBarterCost(itemTpl: string, count: number, itemId?: string): FluentAssortConstructor {
+ const item = itemId ? this.itemsToSell.find(i => i._id === itemId) : this.itemsToSell[0];
+ if (!this.barterScheme[item._id]) {
+ this.barterScheme[item._id] = [[{ count, _tpl: itemTpl }]];
+ } else {
+ const scheme = this.barterScheme[item._id][0].find(s => s._tpl === itemTpl);
+ if (scheme) {
+ scheme.count += count;
+ } else {
+ this.barterScheme[item._id][0].push({ count, _tpl: itemTpl });
+ }
+ }
+ return this;
+ }
+
+ public export(data: ITrader): FluentAssortConstructor {
+ for (const item of this.itemsToSell) {
+ if (data.assort.items.some(i => i._id === item._id)) {
+ this.logger.error(`Item with ID ${item._id} already exists in the assortment.`);
+ return;
+ }
+ //this.logger.info(`Adding item with ID ${item._id} to the assortment.`)
+ //this.logger.info(item);
+
+ data.assort.items.push(item);
+ if (this.barterScheme[item._id]) {
+ data.assort.barter_scheme[item._id] = this.barterScheme[item._id];
+ }
+ if (this.loyaltyLevels[item._id]) {
+ data.assort.loyal_level_items[item._id] = this.loyaltyLevels[item._id];
+ }
+ }
+
+ // Reset internal state
+ this.itemsToSell = [];
+ this.barterScheme = {};
+ this.loyaltyLevels = {};
+
+ return this;
+ }
+}
diff --git a/server files/user/mods/DoorBreacher/src/items.type.js b/server files/user/mods/DoorBreacher/src/items.type.js
new file mode 100644
index 0000000..900e554
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/items.type.js
@@ -0,0 +1,3 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+//# sourceMappingURL=items.type.js.map
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/items.type.js.map b/server files/user/mods/DoorBreacher/src/items.type.js.map
new file mode 100644
index 0000000..517158d
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/items.type.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "items.type.js",
+ "sourceRoot": "",
+ "sources": [
+ "items.type.ts"
+ ],
+ "names": [],
+ "mappings": ""
+}
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/items.type.ts b/server files/user/mods/DoorBreacher/src/items.type.ts
new file mode 100644
index 0000000..83588a8
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/items.type.ts
@@ -0,0 +1,8 @@
+import type { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
+
+
+export interface ItemsJson {
+ doorbreacher: ITemplateItem;
+ doorbreacherbox: ITemplateItem;
+ C4Explosive: ITemplateItem;
+}
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/mod.js b/server files/user/mods/DoorBreacher/src/mod.js
new file mode 100644
index 0000000..ebaa8db
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/mod.js
@@ -0,0 +1,194 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const Money_1 = require("C:/snapshot/project/obj/models/enums/Money");
+const fluentTraderAssortCreator_1 = require("./fluentTraderAssortCreator");
+const node_path_1 = __importDefault(require("node:path"));
+class Mod {
+ // Declare private variable db of DatabaseServer type
+ dbServer;
+ db;
+ fluentTraderAssortHelper;
+ traderID;
+ itemsJson;
+ logger;
+ jsonUtil;
+ postDBLoad(container) {
+ // Resolve containers
+ this.logger = container.resolve("WinstonLogger");
+ const customItem = container.resolve("CustomItemService");
+ const hashUtil = container.resolve("HashUtil");
+ this.dbServer = container.resolve("DatabaseServer");
+ this.db = this.dbServer.getTables();
+ this.fluentTraderAssortHelper = new fluentTraderAssortCreator_1.FluentAssortConstructor(hashUtil, this.logger);
+ this.jsonUtil = container.resolve("JsonUtil");
+ // Get fileSystem to read in configs
+ const fileSystem = container.resolve("FileSystemSync");
+ const itemsJsonPath = node_path_1.default.resolve(__dirname, "../database/templates/items.jsonc");
+ // Read the items.json file with type ItemsJson
+ this.itemsJson = this.jsonUtil.deserializeJsonC(fileSystem.read(itemsJsonPath));
+ // Set trader id we want to add assort items to
+ this.traderID = "5a7c2eca46aef81a7ca2145d";
+ // Load hideoutrecipes and our custom recipes
+ const hideoutRecipes = this.db.hideout.production.recipes;
+ const customRecipes = this.jsonUtil.deserializeJsonC(fileSystem.read(node_path_1.default.resolve(__dirname, "../database/templates/craftingItem.jsonc")));
+ setupItems(this.itemsJson, customItem);
+ handleAssorts(this.db, this.fluentTraderAssortHelper, this.traderID, this.itemsJson);
+ modifyAmmoPropForWeapons(this.db, this.itemsJson);
+ this.logger.info("DoorBreacher: Finished Modifying Ammo Properties for Weapons");
+ this.logger.info(`Before ${hideoutRecipes.length}`);
+ // Adds custom recipe(s) to the workbench. Currently only the C4
+ customRecipes.forEach(customRecipe => {
+ hideoutRecipes.push(customRecipe);
+ });
+ this.logger.info(`After ${hideoutRecipes.length}`);
+ this.logger.info(`Is recipe loaded? ${hideoutRecipes.filter(r => r._id === "665d4ce7e381d16c8676292b").length >= 1}`);
+ this.logger.info("Added custom recipes to hideout recipes");
+ }
+}
+function setupItems(itemsjson, customItem) {
+ //Make locale for DoorBreacher
+ const doorBreacherLocale = {
+ en: {
+ name: "12/70 Door-Breaching Round",
+ shortName: "Breach",
+ description: "The door-breaching round is designed to destroy deadbolts, locks, and hinges without risking lives by ricocheting or penetrating through doors. These frangible rounds are made of a dense sintered material which can destroy a lock or hinge and then immediately disperse."
+ }
+ };
+ // Add new custom item
+ const doorBreacher = {
+ newItem: itemsjson.doorbreacher,
+ fleaPriceRoubles: 8000,
+ handbookPriceRoubles: 10000,
+ handbookParentId: "5b47574386f77428ca22b33b",
+ locales: doorBreacherLocale
+ };
+ // Make locale for DoorBreacherBox
+ const doorBreacherBoxLocale = {
+ en: {
+ name: "12/70 Door-Breaching 5-Round Box",
+ shortName: "Breach",
+ description: "A 5-round box of 12ga door breaching shells. The door-breaching round is designed to destroy deadbolts, locks, and hinges without risking lives by ricocheting or penetrating through doors. These frangible rounds are made of a dense sintered material which can destroy a lock or hinge and then immediately disperse."
+ }
+ };
+ // Add new custom item
+ const doorBreacherBox = {
+ newItem: itemsjson.doorbreacherbox,
+ fleaPriceRoubles: 40000,
+ handbookPriceRoubles: 50000,
+ handbookParentId: "5b47574386f77428ca22b33c",
+ locales: doorBreacherBoxLocale
+ };
+ // Make locale for DoorBreacher
+ const c4ExplosiveLocale = {
+ en: {
+ name: "C4 Explosive",
+ shortName: "C4",
+ description: "This C4 Explosive is used for breaching reinforced doors. It is a powerful explosive that is used in the military and law enforcement. It is a plastic explosive that is stable and safe to handle and triggered after a set timer."
+ }
+ };
+ // Add new custom item
+ const c4Explosive = {
+ newItem: itemsjson.C4Explosive,
+ fleaPriceRoubles: 45000,
+ handbookPriceRoubles: 40000,
+ handbookParentId: "5b47574386f77428ca22b2f2",
+ locales: c4ExplosiveLocale
+ };
+ // Create the items
+ customItem.createItem(doorBreacher);
+ customItem.createItem(doorBreacherBox);
+ customItem.createItem(c4Explosive);
+}
+function modifyAmmoPropForWeapons(db, itemsJson) {
+ const weaponProperties = [
+ { name: "Chambers", index: 0 },
+ { name: "Cartridges", index: 1 },
+ { name: "camora_000", index: 2 },
+ { name: "camora_001", index: 3 },
+ { name: "camora_002", index: 4 },
+ { name: "camora_003", index: 5 },
+ { name: "camora_004", index: 6 }
+ ];
+ const is12GaugeAmmo = (filters) => {
+ return filters ? filters.some(filter => filter.Filter?.includes("560d5e524bdc2d25448b4571")) : false;
+ };
+ const addDoorBreacher = (item, filters, weaponPropName) => {
+ console.info(`DoorBreacher added to: ${item._name} in weaponPropName: ${weaponPropName}`);
+ filters[0].Filter.push(itemsJson.doorbreacher._id.toString());
+ };
+ const processWeaponProperty = (item, weaponPropName) => {
+ const property = item._props[weaponPropName];
+ if (!property) {
+ return;
+ }
+ if (Array.isArray(property)) {
+ // For properties like "Chambers"
+ for (const subProperty of property) {
+ if (subProperty._props.filters && is12GaugeAmmo(subProperty._props.filters)) {
+ addDoorBreacher(item, subProperty._props.filters, weaponPropName);
+ }
+ }
+ }
+ else {
+ // For properties directly under _props like "Cartridges"
+ if (property.filters && is12GaugeAmmo(property.filters)) {
+ addDoorBreacher(item, property.filters, weaponPropName);
+ }
+ }
+ };
+ const processSlots = (slots) => {
+ if (!slots || slots.length === 0) {
+ return;
+ }
+ for (const slot of slots) {
+ if (slot._props.filters && is12GaugeAmmo(slot._props.filters)) {
+ addDoorBreacher(slot, slot._props.filters, slot._name);
+ }
+ }
+ };
+ // Iterate over all items
+ for (const item of Object.values(db.templates.items)) {
+ for (const prop of weaponProperties) {
+ if (item._props[prop.name]) {
+ processWeaponProperty(item, prop.name);
+ }
+ }
+ // Process slots for "camora"
+ if (item._props.Slots) {
+ processSlots(item._props.Slots);
+ }
+ }
+}
+function handleAssorts(db, assortHelper, traderID, itemsjson) {
+ const targetTrader = db.traders[traderID];
+ // Create assort for doorbreacher. No money, add barter only later
+ assortHelper
+ .createSingleAssortItem(itemsjson.doorbreacher._id)
+ .addStackCount(100)
+ .addUnlimitedStackCount()
+ .addLoyaltyLevel(1)
+ .addMoneyCost(Money_1.Money.ROUBLES, 10000)
+ .export(targetTrader);
+ // Create assort for doorbreacherbox - no assort since no other trader sells a packl
+ // assortHelper
+ // .createSingleAssortItem(itemsjson.doorbreacherbox._id)
+ // .addStackCount(100)
+ // .addUnlimitedStackCount()
+ // .addLoyaltyLevel(1)
+ // .addMoneyCost(Money.ROUBLES, 50000)
+ // .export(targetTrader);
+ // Create barter item for doorbreacher
+ const electricWire = "5c06779c86f77426e00dd782";
+ assortHelper
+ .createSingleAssortItem(itemsjson.doorbreacher._id)
+ .addStackCount(100)
+ .addUnlimitedStackCount()
+ .addBarterCost(electricWire, 1)
+ .addLoyaltyLevel(1)
+ .export(targetTrader);
+}
+module.exports = { mod: new Mod() };
+//# sourceMappingURL=mod.js.map
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/mod.js.map b/server files/user/mods/DoorBreacher/src/mod.js.map
new file mode 100644
index 0000000..dba7202
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/mod.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "mod.js",
+ "sourceRoot": "",
+ "sources": [
+ "mod.ts"
+ ],
+ "names": [],
+ "mappings": "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"
+}
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/mod.ts b/server files/user/mods/DoorBreacher/src/mod.ts
new file mode 100644
index 0000000..dded29c
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/mod.ts
@@ -0,0 +1,252 @@
+import { DependencyContainer } from "tsyringe";
+import { IPostDBLoadMod } from "@spt/models/external/IPostDBLoadMod";
+import { CustomItemService } from "@spt/services/mod/CustomItemService";
+import { LocaleDetails, NewItemDetails } from "@spt/models/spt/mod/NewItemDetails";
+import { ILogger } from "@spt/models/spt/utils/ILogger";
+import { DatabaseServer } from "@spt/servers/DatabaseServer";
+import { HashUtil } from "@spt/utils/HashUtil";
+import { Money } from "@spt/models/enums/Money";
+import { FluentAssortConstructor } from "./fluentTraderAssortCreator";
+import { ItemsJson } from "./items.type";
+import { FileSystemSync } from "@spt/utils/FileSystemSync";
+import path from "node:path";
+import { JsonUtil } from "@spt/utils/JsonUtil";
+import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
+import { IDatabaseTables } from "@spt/models/spt/server/IDatabaseTables";
+
+
+class Mod implements IPostDBLoadMod {
+ // Declare private variable db of DatabaseServer type
+ private dbServer: DatabaseServer;
+ private db: IDatabaseTables;
+ private fluentTraderAssortHelper: FluentAssortConstructor;
+ private traderID: string;
+ private itemsJson: ItemsJson;
+ private logger: ILogger;
+ private jsonUtil: JsonUtil;
+
+
+ public postDBLoad(container: DependencyContainer): void {
+ // Resolve containers
+ this.logger = container.resolve("WinstonLogger");
+ const customItem = container.resolve("CustomItemService");
+ const hashUtil: HashUtil = container.resolve("HashUtil");
+
+ this.dbServer = container.resolve("DatabaseServer");
+ this.db = this.dbServer.getTables();
+ this.fluentTraderAssortHelper = new FluentAssortConstructor(
+ hashUtil,
+ this.logger
+ );
+ this.jsonUtil = container.resolve("JsonUtil");
+
+ // Get fileSystem to read in configs
+ const fileSystem = container.resolve("FileSystemSync");
+ const itemsJsonPath = path.resolve(__dirname, "../database/templates/items.jsonc");
+
+ // Read the items.json file with type ItemsJson
+ this.itemsJson = this.jsonUtil.deserializeJsonC(fileSystem.read(itemsJsonPath));
+
+ // Set trader id we want to add assort items to
+ this.traderID = "5a7c2eca46aef81a7ca2145d";
+
+ // Load hideoutrecipes and our custom recipes
+ const hideoutRecipes: IHideoutProduction[] = this.db.hideout.production.recipes;
+ const customRecipes = this.jsonUtil.deserializeJsonC(fileSystem.read(path.resolve(__dirname, "../database/templates/craftingItem.jsonc")));
+
+
+ setupItems(this.itemsJson, customItem);
+ handleAssorts(
+ this.db,
+ this.fluentTraderAssortHelper,
+ this.traderID,
+ this.itemsJson
+ );
+
+ modifyAmmoPropForWeapons(this.db, this.itemsJson);
+ this.logger.info("DoorBreacher: Finished Modifying Ammo Properties for Weapons");
+
+ this.logger.info(`Before ${hideoutRecipes.length}`);
+ // Adds custom recipe(s) to the workbench. Currently only the C4
+ customRecipes.forEach(customRecipe => {
+ hideoutRecipes.push(customRecipe);
+ });
+ this.logger.info(`After ${hideoutRecipes.length}`);
+ this.logger.info(`Is recipe loaded? ${hideoutRecipes.filter(r => r._id === "665d4ce7e381d16c8676292b").length >= 1}`);
+ this.logger.info("Added custom recipes to hideout recipes");
+ }
+}
+
+function setupItems(itemsjson: ItemsJson, customItem: CustomItemService) {
+ //Make locale for DoorBreacher
+ const doorBreacherLocale: Record = {
+ en: {
+ name: "12/70 Door-Breaching Round",
+ shortName: "Breach",
+ description:
+ "The door-breaching round is designed to destroy deadbolts, locks, and hinges without risking lives by ricocheting or penetrating through doors. These frangible rounds are made of a dense sintered material which can destroy a lock or hinge and then immediately disperse."
+ }
+ };
+
+ // Add new custom item
+ const doorBreacher: NewItemDetails = {
+ newItem: itemsjson.doorbreacher,
+ fleaPriceRoubles: 8000,
+ handbookPriceRoubles: 10000,
+ handbookParentId: "5b47574386f77428ca22b33b",
+ locales: doorBreacherLocale
+ };
+
+ // Make locale for DoorBreacherBox
+ const doorBreacherBoxLocale: Record = {
+ en: {
+ name: "12/70 Door-Breaching 5-Round Box",
+ shortName: "Breach",
+ description:
+ "A 5-round box of 12ga door breaching shells. The door-breaching round is designed to destroy deadbolts, locks, and hinges without risking lives by ricocheting or penetrating through doors. These frangible rounds are made of a dense sintered material which can destroy a lock or hinge and then immediately disperse."
+ }
+ };
+
+ // Add new custom item
+ const doorBreacherBox: NewItemDetails = {
+ newItem: itemsjson.doorbreacherbox,
+ fleaPriceRoubles: 40000,
+ handbookPriceRoubles: 50000,
+ handbookParentId: "5b47574386f77428ca22b33c",
+ locales: doorBreacherBoxLocale
+ };
+
+ // Make locale for DoorBreacher
+ const c4ExplosiveLocale: Record = {
+ en: {
+ name: "C4 Explosive",
+ shortName: "C4",
+ description:
+ "This C4 Explosive is used for breaching reinforced doors. It is a powerful explosive that is used in the military and law enforcement. It is a plastic explosive that is stable and safe to handle and triggered after a set timer."
+ }
+ };
+
+ // Add new custom item
+ const c4Explosive: NewItemDetails = {
+ newItem: itemsjson.C4Explosive,
+ fleaPriceRoubles: 45000,
+ handbookPriceRoubles: 40000,
+ handbookParentId: "5b47574386f77428ca22b2f2",
+ locales: c4ExplosiveLocale
+ };
+
+
+ // Create the items
+ customItem.createItem(doorBreacher);
+ customItem.createItem(doorBreacherBox);
+ customItem.createItem(c4Explosive);
+}
+
+function modifyAmmoPropForWeapons(db: IDatabaseTables, itemsJson: ItemsJson) {
+ const weaponProperties = [
+ { name: "Chambers", index: 0 },
+ { name: "Cartridges", index: 1 },
+ { name: "camora_000", index: 2 },
+ { name: "camora_001", index: 3 },
+ { name: "camora_002", index: 4 },
+ { name: "camora_003", index: 5 },
+ { name: "camora_004", index: 6 }
+ ];
+
+ const is12GaugeAmmo = (filters) => {
+ return filters ? filters.some(filter => filter.Filter?.includes("560d5e524bdc2d25448b4571")) : false;
+ };
+
+ const addDoorBreacher = (item, filters, weaponPropName) => {
+ console.info(`DoorBreacher added to: ${item._name} in weaponPropName: ${weaponPropName}`);
+ filters[0].Filter.push(itemsJson.doorbreacher._id.toString());
+ };
+
+ const processWeaponProperty = (item, weaponPropName) => {
+ const property = item._props[weaponPropName];
+ if (!property) {
+ return;
+ }
+
+ if (Array.isArray(property)) {
+ // For properties like "Chambers"
+ for (const subProperty of property) {
+ if (subProperty._props.filters && is12GaugeAmmo(subProperty._props.filters)) {
+ addDoorBreacher(item, subProperty._props.filters, weaponPropName);
+ }
+ }
+ } else {
+ // For properties directly under _props like "Cartridges"
+ if (property.filters && is12GaugeAmmo(property.filters)) {
+ addDoorBreacher(item, property.filters, weaponPropName);
+ }
+ }
+ };
+
+ const processSlots = (slots) => {
+ if (!slots || slots.length === 0) {
+ return;
+ }
+
+ for (const slot of slots) {
+ if (slot._props.filters && is12GaugeAmmo(slot._props.filters)) {
+ addDoorBreacher(slot, slot._props.filters, slot._name);
+ }
+ }
+ };
+
+ // Iterate over all items
+ for (const item of Object.values(db.templates.items)) {
+ for (const prop of weaponProperties) {
+ if (item._props[prop.name]) {
+ processWeaponProperty(item, prop.name);
+ }
+ }
+
+ // Process slots for "camora"
+ if (item._props.Slots) {
+ processSlots(item._props.Slots);
+ }
+ }
+}
+
+function handleAssorts(
+ db: IDatabaseTables,
+ assortHelper: FluentAssortConstructor,
+ traderID: string,
+ itemsjson: ItemsJson
+) {
+ const targetTrader = db.traders[traderID];
+
+ // Create assort for doorbreacher. No money, add barter only later
+ assortHelper
+ .createSingleAssortItem(itemsjson.doorbreacher._id)
+ .addStackCount(100)
+ .addUnlimitedStackCount()
+ .addLoyaltyLevel(1)
+ .addMoneyCost(Money.ROUBLES, 10000)
+ .export(targetTrader);
+
+ // Create assort for doorbreacherbox - no assort since no other trader sells a packl
+ // assortHelper
+ // .createSingleAssortItem(itemsjson.doorbreacherbox._id)
+ // .addStackCount(100)
+ // .addUnlimitedStackCount()
+ // .addLoyaltyLevel(1)
+ // .addMoneyCost(Money.ROUBLES, 50000)
+ // .export(targetTrader);
+
+ // Create barter item for doorbreacher
+ const electricWire = "5c06779c86f77426e00dd782";
+ assortHelper
+ .createSingleAssortItem(itemsjson.doorbreacher._id)
+ .addStackCount(100)
+ .addUnlimitedStackCount()
+ .addBarterCost(electricWire, 1)
+ .addLoyaltyLevel(1)
+ .export(targetTrader);
+
+}
+
+
+module.exports = { mod: new Mod() };
diff --git a/server files/user/mods/DoorBreacher/src/traderHelpers.js b/server files/user/mods/DoorBreacher/src/traderHelpers.js
new file mode 100644
index 0000000..8e4f9ac
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/traderHelpers.js
@@ -0,0 +1,160 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.TraderHelper = void 0;
+class TraderHelper {
+ /**
+ * Add profile picture to our trader
+ * @param baseJson json file for trader (db/base.json)
+ * @param preAkiModLoader mod loader class - used to get the mods file path
+ * @param imageRouter image router class - used to register the trader image path so we see their image on trader page
+ * @param traderImageName Filename of the trader icon to use
+ */
+ //biome-ignore lint/suspicious/noExplicitAny: baseJson comes from base.json, so no type
+ registerProfileImage(baseJson, modName, preAkiModLoader, imageRouter, traderImageName) {
+ // Reference the mod "res" folder
+ const imageFilepath = `./${preAkiModLoader.getModPath(modName)}res`;
+ // Register a route to point to the profile picture - remember to remove the .jpg from it
+ imageRouter.addRoute(baseJson.avatar.replace(".jpg", ""), `${imageFilepath}/${traderImageName}`);
+ }
+ /**
+ * Add record to trader config to set the refresh time of trader in seconds (default is 60 minutes)
+ * @param traderConfig trader config to add our trader to
+ * @param baseJson json file for trader (db/base.json)
+ * @param refreshTimeSecondsMin How many seconds between trader stock refresh min time
+ * @param refreshTimeSecondsMax How many seconds between trader stock refresh max time
+ */
+ //biome-ignore lint/suspicious/noExplicitAny: baseJson comes from base.json, so no type
+ setTraderUpdateTime(traderConfig, baseJson, refreshTimeSecondsMin, refreshTimeSecondsMax) {
+ // Add refresh time in seconds to config
+ const traderRefreshRecord = {
+ traderId: baseJson._id,
+ seconds: {
+ min: refreshTimeSecondsMin,
+ max: refreshTimeSecondsMax
+ }
+ };
+ traderConfig.updateTime.push(traderRefreshRecord);
+ }
+ /**
+ * Add our new trader to the database
+ * @param traderDetailsToAdd trader details
+ * @param tables database
+ * @param jsonUtil json utility class
+ */
+ //biome-ignore lint/suspicious/noExplicitAny: traderDetailsToAdd comes from base.json, so no type
+ addTraderToDb(traderDetailsToAdd, tables, jsonUtil) {
+ // Add trader to trader table, key is the traders id
+ tables.traders[traderDetailsToAdd._id] = {
+ assort: this.createAssortTable(), // assorts are the 'offers' trader sells, can be a single item (e.g. carton of milk) or multiple items as a collection (e.g. a gun)
+ base: jsonUtil.deserialize(jsonUtil.serialize(traderDetailsToAdd)), // Deserialise/serialise creates a copy of the json and allows us to cast it as an ITraderBase
+ questassort: {
+ started: {},
+ success: {},
+ fail: {}
+ } // questassort is empty as trader has no assorts unlocked by quests
+ };
+ }
+ /**
+ * Create basic data for trader + add empty assorts table for trader
+ * @param tables SPT db
+ * @param jsonUtil SPT JSON utility class
+ * @returns ITraderAssort
+ */
+ createAssortTable() {
+ // Create a blank assort object, ready to have items added
+ const assortTable = {
+ nextResupply: 0,
+ items: [],
+ barter_scheme: {},
+ loyal_level_items: {}
+ };
+ return assortTable;
+ }
+ /**
+ * Create a weapon from scratch, ready to be added to trader
+ * @returns Item[]
+ */
+ createGlock() {
+ // Create an array ready to hold weapon + all mods
+ const glock = [];
+ // Add the base first
+ glock.push({
+ _id: "glockBase", // Ids dont matter, as long as they are unique (can use hashUtil.generate() if you dont want to type every id by hand)
+ _tpl: "5a7ae0c351dfba0017554310" // This is the weapons tpl, found on: https://db.sp-tarkov.com/search
+ });
+ // Add barrel
+ glock.push({
+ _id: "glockbarrel",
+ _tpl: "5a6b60158dc32e000a31138b",
+ parentId: "glockBase", // This is a sub item, you need to define its parent its attached to / inserted into
+ slotId: "mod_barrel" // Required for mods, you need to define what 'role' they have
+ });
+ // Add reciever
+ glock.push({
+ _id: "glockReciever",
+ _tpl: "5a9685b1a2750c0032157104",
+ parentId: "glockBase",
+ slotId: "mod_reciever"
+ });
+ // Add compensator
+ glock.push({
+ _id: "glockCompensator",
+ _tpl: "5a7b32a2e899ef00135e345a",
+ parentId: "glockReciever", // The parent of this mod is the reciever NOT weapon, be careful to get the correct parent
+ slotId: "mod_muzzle"
+ });
+ // Add Pistol grip
+ glock.push({
+ _id: "glockPistolGrip",
+ _tpl: "5a7b4960e899ef197b331a2d",
+ parentId: "glockBase",
+ slotId: "mod_pistol_grip"
+ });
+ // Add front sight
+ glock.push({
+ _id: "glockRearSight",
+ _tpl: "5a6f5d528dc32e00094b97d9",
+ parentId: "glockReciever",
+ slotId: "mod_sight_rear"
+ });
+ // Add rear sight
+ glock.push({
+ _id: "glockFrontSight",
+ _tpl: "5a6f58f68dc32e000a311390",
+ parentId: "glockReciever",
+ slotId: "mod_sight_front"
+ });
+ // Add magazine
+ glock.push({
+ _id: "glockMagazine",
+ _tpl: "630769c4962d0247b029dc60",
+ parentId: "glockBase",
+ slotId: "mod_magazine"
+ });
+ return glock;
+ }
+ /**
+ * Add traders name/location/description to the locale table
+ * @param baseJson json file for trader (db/base.json)
+ * @param tables database tables
+ * @param fullName Complete name of trader
+ * @param firstName First name of trader
+ * @param nickName Nickname of trader
+ * @param location Location of trader (e.g. "Here in the cat shop")
+ * @param description Description of trader
+ */
+ //biome-ignore lint/suspicious/noExplicitAny: baseJson comes from base.json, so no type
+ addTraderToLocales(baseJson, tables, fullName, firstName, nickName, location, description) {
+ // For each language, add locale for the new trader
+ const locales = Object.values(tables.locales.global);
+ for (const locale of locales) {
+ locale[`${baseJson._id} FullName`] = fullName;
+ locale[`${baseJson._id} FirstName`] = firstName;
+ locale[`${baseJson._id} Nickname`] = nickName;
+ locale[`${baseJson._id} Location`] = location;
+ locale[`${baseJson._id} Description`] = description;
+ }
+ }
+}
+exports.TraderHelper = TraderHelper;
+//# sourceMappingURL=traderHelpers.js.map
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/traderHelpers.js.map b/server files/user/mods/DoorBreacher/src/traderHelpers.js.map
new file mode 100644
index 0000000..0ee089c
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/traderHelpers.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "traderHelpers.js",
+ "sourceRoot": "",
+ "sources": [
+ "traderHelpers.ts"
+ ],
+ "names": [],
+ "mappings": "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"
+}
\ No newline at end of file
diff --git a/server files/user/mods/DoorBreacher/src/traderHelpers.ts b/server files/user/mods/DoorBreacher/src/traderHelpers.ts
new file mode 100644
index 0000000..e8d0602
--- /dev/null
+++ b/server files/user/mods/DoorBreacher/src/traderHelpers.ts
@@ -0,0 +1,190 @@
+import type { PreAkiModLoader } from "@spt/loaders/PreAkiModLoader";
+import type { Item } from "@spt/models/eft/common/tables/IItem";
+import type { ITraderBase, ITraderAssort } from "@spt/models/eft/common/tables/ITrader";
+import type { ITraderConfig, UpdateTime } from "@spt/models/spt/config/ITraderConfig";
+import type { IDatabaseTables } from "@spt/models/spt/server/IDatabaseTables";
+import type { ImageRouter } from "@spt/routers/ImageRouter";
+import type { JsonUtil } from "@spt/utils/JsonUtil";
+
+export class TraderHelper
+{
+ /**
+ * Add profile picture to our trader
+ * @param baseJson json file for trader (db/base.json)
+ * @param preAkiModLoader mod loader class - used to get the mods file path
+ * @param imageRouter image router class - used to register the trader image path so we see their image on trader page
+ * @param traderImageName Filename of the trader icon to use
+ */
+
+ //biome-ignore lint/suspicious/noExplicitAny: baseJson comes from base.json, so no type
+ public registerProfileImage(baseJson: any, modName: string, preAkiModLoader: PreAkiModLoader, imageRouter: ImageRouter, traderImageName: string): void
+ {
+ // Reference the mod "res" folder
+ const imageFilepath = `./${preAkiModLoader.getModPath(modName)}res`;
+
+ // Register a route to point to the profile picture - remember to remove the .jpg from it
+ imageRouter.addRoute(baseJson.avatar.replace(".jpg", ""), `${imageFilepath}/${traderImageName}`);
+ }
+
+ /**
+ * Add record to trader config to set the refresh time of trader in seconds (default is 60 minutes)
+ * @param traderConfig trader config to add our trader to
+ * @param baseJson json file for trader (db/base.json)
+ * @param refreshTimeSecondsMin How many seconds between trader stock refresh min time
+ * @param refreshTimeSecondsMax How many seconds between trader stock refresh max time
+ */
+
+ //biome-ignore lint/suspicious/noExplicitAny: baseJson comes from base.json, so no type
+ public setTraderUpdateTime(traderConfig: ITraderConfig, baseJson: any, refreshTimeSecondsMin: number, refreshTimeSecondsMax: number): void
+ {
+ // Add refresh time in seconds to config
+ const traderRefreshRecord: UpdateTime = {
+ traderId: baseJson._id,
+ seconds: {
+ min: refreshTimeSecondsMin,
+ max: refreshTimeSecondsMax
+ } };
+
+ traderConfig.updateTime.push(traderRefreshRecord);
+ }
+
+ /**
+ * Add our new trader to the database
+ * @param traderDetailsToAdd trader details
+ * @param tables database
+ * @param jsonUtil json utility class
+ */
+
+ //biome-ignore lint/suspicious/noExplicitAny: traderDetailsToAdd comes from base.json, so no type
+ public addTraderToDb(traderDetailsToAdd: any, tables: IDatabaseTables, jsonUtil: JsonUtil): void
+ {
+ // Add trader to trader table, key is the traders id
+ tables.traders[traderDetailsToAdd._id] = {
+ assort: this.createAssortTable(), // assorts are the 'offers' trader sells, can be a single item (e.g. carton of milk) or multiple items as a collection (e.g. a gun)
+ base: jsonUtil.deserialize(jsonUtil.serialize(traderDetailsToAdd)) as ITraderBase, // Deserialise/serialise creates a copy of the json and allows us to cast it as an ITraderBase
+ questassort: {
+ started: {},
+ success: {},
+ fail: {}
+ } // questassort is empty as trader has no assorts unlocked by quests
+ };
+ }
+
+ /**
+ * Create basic data for trader + add empty assorts table for trader
+ * @param tables SPT db
+ * @param jsonUtil SPT JSON utility class
+ * @returns ITraderAssort
+ */
+ private createAssortTable(): ITraderAssort
+ {
+ // Create a blank assort object, ready to have items added
+ const assortTable: ITraderAssort = {
+ nextResupply: 0,
+ items: [],
+ barter_scheme: {},
+ loyal_level_items: {}
+ }
+
+ return assortTable;
+ }
+
+ /**
+ * Create a weapon from scratch, ready to be added to trader
+ * @returns Item[]
+ */
+ public createGlock(): Item[]
+ {
+ // Create an array ready to hold weapon + all mods
+ const glock: Item[] = [];
+
+ // Add the base first
+ glock.push({ // Add the base weapon first
+ _id: "glockBase", // Ids dont matter, as long as they are unique (can use hashUtil.generate() if you dont want to type every id by hand)
+ _tpl: "5a7ae0c351dfba0017554310" // This is the weapons tpl, found on: https://db.sp-tarkov.com/search
+ });
+
+ // Add barrel
+ glock.push({
+ _id: "glockbarrel",
+ _tpl: "5a6b60158dc32e000a31138b",
+ parentId: "glockBase", // This is a sub item, you need to define its parent its attached to / inserted into
+ slotId: "mod_barrel" // Required for mods, you need to define what 'role' they have
+ });
+
+ // Add reciever
+ glock.push({
+ _id: "glockReciever",
+ _tpl:"5a9685b1a2750c0032157104",
+ parentId: "glockBase",
+ slotId: "mod_reciever"
+ });
+
+ // Add compensator
+ glock.push({
+ _id: "glockCompensator",
+ _tpl:"5a7b32a2e899ef00135e345a",
+ parentId: "glockReciever", // The parent of this mod is the reciever NOT weapon, be careful to get the correct parent
+ slotId: "mod_muzzle"
+ });
+
+ // Add Pistol grip
+ glock.push({
+ _id: "glockPistolGrip",
+ _tpl:"5a7b4960e899ef197b331a2d",
+ parentId: "glockBase",
+ slotId: "mod_pistol_grip"
+ });
+
+ // Add front sight
+ glock.push({
+ _id: "glockRearSight",
+ _tpl: "5a6f5d528dc32e00094b97d9",
+ parentId: "glockReciever",
+ slotId: "mod_sight_rear"
+ });
+
+ // Add rear sight
+ glock.push({
+ _id: "glockFrontSight",
+ _tpl: "5a6f58f68dc32e000a311390",
+ parentId: "glockReciever",
+ slotId: "mod_sight_front"
+ });
+
+ // Add magazine
+ glock.push({
+ _id: "glockMagazine",
+ _tpl: "630769c4962d0247b029dc60",
+ parentId: "glockBase",
+ slotId: "mod_magazine"
+ });
+
+ return glock;
+ }
+
+ /**
+ * Add traders name/location/description to the locale table
+ * @param baseJson json file for trader (db/base.json)
+ * @param tables database tables
+ * @param fullName Complete name of trader
+ * @param firstName First name of trader
+ * @param nickName Nickname of trader
+ * @param location Location of trader (e.g. "Here in the cat shop")
+ * @param description Description of trader
+ */
+
+ //biome-ignore lint/suspicious/noExplicitAny: baseJson comes from base.json, so no type
+ public addTraderToLocales(baseJson: any, tables: IDatabaseTables, fullName: string, firstName: string, nickName: string, location: string, description: string)
+ {
+ // For each language, add locale for the new trader
+ const locales = Object.values(tables.locales.global) as Record[];
+ for (const locale of locales) {
+ locale[`${baseJson._id} FullName`] = fullName;
+ locale[`${baseJson._id} FirstName`] = firstName;
+ locale[`${baseJson._id} Nickname`] = nickName;
+ locale[`${baseJson._id} Location`] = location;
+ locale[`${baseJson._id} Description`] = description;
+ }
+ }
+}
\ No newline at end of file