diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot.meta
new file mode 100644
index 000000000..3bb118d07
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f11bacc0ef1a67244944dcffc1fbbb9e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallBoost.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallBoost.asset
new file mode 100644
index 000000000..bf0911415
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallBoost.asset
@@ -0,0 +1,67 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escBallBoost
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.BallBoost, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: baseDamageCoefficient
+ fieldValue:
+ stringValue: 4
+ objectValue: {fileID: 0}
+ - fieldName: awayForceMagnitude
+ fieldValue:
+ stringValue: 1000
+ objectValue: {fileID: 0}
+ - fieldName: upwardForceMagnitude
+ fieldValue:
+ stringValue: 500
+ objectValue: {fileID: 0}
+ - fieldName: hitPauseDuration
+ fieldValue:
+ stringValue: 0.1
+ objectValue: {fileID: 0}
+ - fieldName: recoilAmplitude
+ fieldValue:
+ stringValue: 1
+ objectValue: {fileID: 0}
+ - fieldName: massThresholdForKnockback
+ fieldValue:
+ stringValue: 250
+ objectValue: {fileID: 0}
+ - fieldName: knockbackForce
+ fieldValue:
+ stringValue: 3000
+ objectValue: {fileID: 0}
+ - fieldName: startSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: endSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: impactSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: startEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: impactEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallBoost.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallBoost.asset.meta
new file mode 100644
index 000000000..d7f4b9e75
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallBoost.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b08200c6c3da88f45a29d00654286425
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallMainState.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallMainState.asset
new file mode 100644
index 000000000..d9cdac999
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallMainState.asset
@@ -0,0 +1,47 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escBallMainState
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.BallMainState, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: moveSpeedMultiplier
+ fieldValue:
+ stringValue: 1.6
+ objectValue: {fileID: 0}
+ - fieldName: ballAirControl
+ fieldValue:
+ stringValue: 0.15
+ objectValue: {fileID: 0}
+ - fieldName: accelerationMultiplier
+ fieldValue:
+ stringValue: 0.6
+ objectValue: {fileID: 0}
+ - fieldName: momentumDamping
+ fieldValue:
+ stringValue: 0.98
+ objectValue: {fileID: 0}
+ - fieldName: enterSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: loopSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: exitSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallMainState.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallMainState.asset.meta
new file mode 100644
index 000000000..ec8b6eac5
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallMainState.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 39f7b37f3bd74544494d7465ae397e90
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallSlam.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallSlam.asset
new file mode 100644
index 000000000..5d8f0bd9e
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallSlam.asset
@@ -0,0 +1,79 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escBallSlam
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.BallSlam, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: airControl
+ fieldValue:
+ stringValue: 0.15
+ objectValue: {fileID: 0}
+ - fieldName: minimumDuration
+ fieldValue:
+ stringValue: 0.15
+ objectValue: {fileID: 0}
+ - fieldName: verticalAcceleration
+ fieldValue:
+ stringValue: -80
+ objectValue: {fileID: 0}
+ - fieldName: exitSlowdownCoefficient
+ fieldValue:
+ stringValue: 0.3
+ objectValue: {fileID: 0}
+ - fieldName: exitVerticalVelocity
+ fieldValue:
+ stringValue: 8
+ objectValue: {fileID: 0}
+ - fieldName: blastRadius
+ fieldValue:
+ stringValue: 12
+ objectValue: {fileID: 0}
+ - fieldName: blastDamageCoefficient
+ fieldValue:
+ stringValue: 5
+ objectValue: {fileID: 0}
+ - fieldName: blastProcCoefficient
+ fieldValue:
+ stringValue: 1
+ objectValue: {fileID: 0}
+ - fieldName: blastForce
+ fieldValue:
+ stringValue: 2000
+ objectValue: {fileID: 0}
+ - fieldName: maxDuration
+ fieldValue:
+ stringValue: 10
+ objectValue: {fileID: 0}
+ - fieldName: blastBonusForce
+ fieldValue:
+ stringValue: (0.0, 0.0, 0.0)
+ objectValue: {fileID: 0}
+ - fieldName: enterSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: blastEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: blastImpactEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: slamEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallSlam.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallSlam.asset.meta
new file mode 100644
index 000000000..0ff8ca64c
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escBallSlam.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9ef4aa2f76bfec846a904e387dcf0f0c
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDeployCharges.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDeployCharges.asset
new file mode 100644
index 000000000..7ce8f0b74
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDeployCharges.asset
@@ -0,0 +1,47 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escDeployCharges
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.DeployCharges, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: damageCoefficient
+ fieldValue:
+ stringValue: 4
+ objectValue: {fileID: 0}
+ - fieldName: baseDuration
+ fieldValue:
+ stringValue: 0.5
+ objectValue: {fileID: 0}
+ - fieldName: chargeSpawnRadius
+ fieldValue:
+ stringValue: 2
+ objectValue: {fileID: 0}
+ - fieldName: force
+ fieldValue:
+ stringValue: 500
+ objectValue: {fileID: 0}
+ - fieldName: soundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleFlashPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: chargeProjectilePrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDeployCharges.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDeployCharges.asset.meta
new file mode 100644
index 000000000..cd401d250
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDeployCharges.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 20e7eb8c3c7bc2340bd89f722b762314
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDispatchWeapon.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDispatchWeapon.asset
new file mode 100644
index 000000000..9e93844eb
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDispatchWeapon.asset
@@ -0,0 +1,19 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escDispatchWeapon
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.DispatchWeapon, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields: []
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDispatchWeapon.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDispatchWeapon.asset.meta
new file mode 100644
index 000000000..5766b1cb3
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escDispatchWeapon.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 30ee2fded33a907469f5f2315b763b65
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrapplingHook.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrapplingHook.asset
new file mode 100644
index 000000000..d6ce3c392
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrapplingHook.asset
@@ -0,0 +1,43 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escFireGrapplingHook
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.FireGrapplingHook, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: damageCoefficient
+ fieldValue:
+ stringValue: 1
+ objectValue: {fileID: 0}
+ - fieldName: fireSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: disconnectSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleflashEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleString
+ fieldValue:
+ stringValue: Muzzle
+ objectValue: {fileID: 0}
+ - fieldName: maxDuration
+ fieldValue:
+ stringValue: 8
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrapplingHook.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrapplingHook.asset.meta
new file mode 100644
index 000000000..38d7d28d4
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrapplingHook.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c4cd3a69c494a954595004d00416116b
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrenadeLauncher.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrenadeLauncher.asset
new file mode 100644
index 000000000..b4bd07142
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrenadeLauncher.asset
@@ -0,0 +1,55 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escFireGrenadeLauncher
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.FireGrenadeLauncher, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: damageCoefficient
+ fieldValue:
+ stringValue: 3
+ objectValue: {fileID: 0}
+ - fieldName: force
+ fieldValue:
+ stringValue: 1500
+ objectValue: {fileID: 0}
+ - fieldName: baseDuration
+ fieldValue:
+ stringValue: 0.6
+ objectValue: {fileID: 0}
+ - fieldName: recoilAmplitude
+ fieldValue:
+ stringValue: 1.5
+ objectValue: {fileID: 0}
+ - fieldName: spreadBloomValue
+ fieldValue:
+ stringValue: 1
+ objectValue: {fileID: 0}
+ - fieldName: muzzleString
+ fieldValue:
+ stringValue: Muzzle
+ objectValue: {fileID: 0}
+ - fieldName: soundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleFlashPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: projectilePrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrenadeLauncher.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrenadeLauncher.asset.meta
new file mode 100644
index 000000000..a9dad56df
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireGrenadeLauncher.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 80366aacdd64bc445ac58ca9e00d011a
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireRapidLaser.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireRapidLaser.asset
new file mode 100644
index 000000000..be82ffc94
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireRapidLaser.asset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escFireRapidLaser
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.FireRapidLaser, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: damageCoefficient
+ fieldValue:
+ stringValue: 0.8
+ objectValue: {fileID: 0}
+ - fieldName: procCoefficient
+ fieldValue:
+ stringValue: 0.7
+ objectValue: {fileID: 0}
+ - fieldName: baseFireInterval
+ fieldValue:
+ stringValue: 0.12
+ objectValue: {fileID: 0}
+ - fieldName: force
+ fieldValue:
+ stringValue: 100
+ objectValue: {fileID: 0}
+ - fieldName: recoilAmplitude
+ fieldValue:
+ stringValue: 0.4
+ objectValue: {fileID: 0}
+ - fieldName: spreadBloomValue
+ fieldValue:
+ stringValue: 0.3
+ objectValue: {fileID: 0}
+ - fieldName: maxDistance
+ fieldValue:
+ stringValue: 60
+ objectValue: {fileID: 0}
+ - fieldName: bulletRadius
+ fieldValue:
+ stringValue: 0.4
+ objectValue: {fileID: 0}
+ - fieldName: baseDuration
+ fieldValue:
+ stringValue: 0.1
+ objectValue: {fileID: 0}
+ - fieldName: muzzleString
+ fieldValue:
+ stringValue: Muzzle
+ objectValue: {fileID: 0}
+ - fieldName: soundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleFlashPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: tracerEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: hitEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireRapidLaser.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireRapidLaser.asset.meta
new file mode 100644
index 000000000..c983e821b
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireRapidLaser.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3eaacfd2c8bfb0d4d93acf715f43469b
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireShotgun.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireShotgun.asset
new file mode 100644
index 000000000..20a2a71fc
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireShotgun.asset
@@ -0,0 +1,79 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escFireShotgun
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.FireShotgun, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: damageCoefficient
+ fieldValue:
+ stringValue: 1.2
+ objectValue: {fileID: 0}
+ - fieldName: procCoefficient
+ fieldValue:
+ stringValue: 0.5
+ objectValue: {fileID: 0}
+ - fieldName: baseDuration
+ fieldValue:
+ stringValue: 0.6
+ objectValue: {fileID: 0}
+ - fieldName: force
+ fieldValue:
+ stringValue: 300
+ objectValue: {fileID: 0}
+ - fieldName: recoilAmplitude
+ fieldValue:
+ stringValue: 3
+ objectValue: {fileID: 0}
+ - fieldName: spreadBloomValue
+ fieldValue:
+ stringValue: 1.5
+ objectValue: {fileID: 0}
+ - fieldName: pelletCount
+ fieldValue:
+ stringValue: 6
+ objectValue: {fileID: 0}
+ - fieldName: spreadAngle
+ fieldValue:
+ stringValue: 10
+ objectValue: {fileID: 0}
+ - fieldName: maxDistance
+ fieldValue:
+ stringValue: 12
+ objectValue: {fileID: 0}
+ - fieldName: bulletRadius
+ fieldValue:
+ stringValue: 0.3
+ objectValue: {fileID: 0}
+ - fieldName: muzzleString
+ fieldValue:
+ stringValue: Muzzle
+ objectValue: {fileID: 0}
+ - fieldName: soundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleFlashPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: tracerEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: hitEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireShotgun.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireShotgun.asset.meta
new file mode 100644
index 000000000..fd89a7c1d
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireShotgun.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2087d7396b7403046829339e9f7c9d8b
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireSniperLaser.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireSniperLaser.asset
new file mode 100644
index 000000000..466a404b1
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireSniperLaser.asset
@@ -0,0 +1,71 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escFireSniperLaser
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.FireSniperLaser, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: damageCoefficient
+ fieldValue:
+ stringValue: 6
+ objectValue: {fileID: 0}
+ - fieldName: procCoefficient
+ fieldValue:
+ stringValue: 1
+ objectValue: {fileID: 0}
+ - fieldName: baseDuration
+ fieldValue:
+ stringValue: 0.8
+ objectValue: {fileID: 0}
+ - fieldName: force
+ fieldValue:
+ stringValue: 2500
+ objectValue: {fileID: 0}
+ - fieldName: recoilAmplitude
+ fieldValue:
+ stringValue: 4
+ objectValue: {fileID: 0}
+ - fieldName: spreadBloomValue
+ fieldValue:
+ stringValue: 2
+ objectValue: {fileID: 0}
+ - fieldName: maxDistance
+ fieldValue:
+ stringValue: 1000
+ objectValue: {fileID: 0}
+ - fieldName: bulletRadius
+ fieldValue:
+ stringValue: 0.5
+ objectValue: {fileID: 0}
+ - fieldName: muzzleString
+ fieldValue:
+ stringValue: Muzzle
+ objectValue: {fileID: 0}
+ - fieldName: soundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: muzzleFlashPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: tracerEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: hitEffectPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireSniperLaser.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireSniperLaser.asset.meta
new file mode 100644
index 000000000..9baddb054
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escFireSniperLaser.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2fafdf1caab072549bc916215a343ca2
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escRangeFinder.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escRangeFinder.asset
new file mode 100644
index 000000000..b7a88307a
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escRangeFinder.asset
@@ -0,0 +1,39 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escRangeFinder
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.RangeFinder, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: maxRange
+ fieldValue:
+ stringValue: 200
+ objectValue: {fileID: 0}
+ - fieldName: muzzleString
+ fieldValue:
+ stringValue: Muzzle
+ objectValue: {fileID: 0}
+ - fieldName: enterSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: exitSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
+ - fieldName: laserPrefab
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escRangeFinder.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escRangeFinder.asset.meta
new file mode 100644
index 000000000..737fcc329
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escRangeFinder.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: be09324759323af40a755dd7240b9b13
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToBall.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToBall.asset
new file mode 100644
index 000000000..c36b0cd34
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToBall.asset
@@ -0,0 +1,23 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escStanceSwapToBall
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.StanceSwapToBall, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: enterSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToBall.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToBall.asset.meta
new file mode 100644
index 000000000..51a97f1df
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToBall.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ba8942b7aa0849646befbcd9b6a36b45
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToDeployed.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToDeployed.asset
new file mode 100644
index 000000000..01d4b4274
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToDeployed.asset
@@ -0,0 +1,23 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3}
+ m_Name: escStanceSwapToDeployed
+ m_EditorClassIdentifier:
+ targetType:
+ assemblyQualifiedName: EntityStates.NemToolbot.StanceSwapToDeployed, Starstorm2, Version=1.0.0.0,
+ Culture=neutral, PublicKeyToken=null
+ serializedFieldsCollection:
+ serializedFields:
+ - fieldName: enterSoundString
+ fieldValue:
+ stringValue:
+ objectValue: {fileID: 0}
diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToDeployed.asset.meta b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToDeployed.asset.meta
new file mode 100644
index 000000000..57af220f9
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/NemToolbot/escStanceSwapToDeployed.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 642c5b79f5002d441b5d3187d481da02
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Components/NemToolbot.meta b/SS2-Project/Assets/Starstorm2/Components/NemToolbot.meta
new file mode 100644
index 000000000..eec4242e7
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Components/NemToolbot.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e2564ce86268ba3419ed1efbda687d4c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotController.cs b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotController.cs
new file mode 100644
index 000000000..f92dd5efb
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotController.cs
@@ -0,0 +1,257 @@
+using RoR2;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace SS2.Components
+{
+ public class NemToolbotController : NetworkBehaviour
+ {
+ public enum WeaponType : byte
+ {
+ Shotgun = 0,
+ RapidLaser = 1,
+ GrenadeLauncher = 2,
+ SniperLaser = 3
+ }
+
+ // Range thresholds(meters) for weapon selection
+ public static float shotgunMaxRange = 3f;
+ public static float rapidLaserMaxRange = 6f;
+ public static float grenadeLauncherMaxRange = 9f;
+
+ // Ammo pool configuration
+ public static int shotgunMaxAmmo = 10;
+ public static int rapidLaserMaxAmmo = 50;
+ public static int grenadeLauncherMaxAmmo = 10;
+ public static int sniperLaserMaxAmmo = 10;
+
+ // Regen rates (ammo per second, only while in ball form)
+ public static float shotgunRegenRate = 1f;
+ public static float rapidLaserRegenRate = 5f;
+ public static float grenadeLauncherRegenRate = 1f;
+ public static float sniperLaserRegenRate = 1f;
+
+ [SyncVar(hook = nameof(OnWeaponChanged))]
+ private WeaponType _currentWeapon = WeaponType.RapidLaser;
+
+ [SyncVar(hook = nameof(OnFormChanged))]
+ private bool _isBallForm;
+
+ [SyncVar]
+ private int _shotgunAmmo;
+ [SyncVar]
+ private int _rapidLaserAmmo;
+ [SyncVar]
+ private int _grenadeLauncherAmmo;
+ [SyncVar]
+ private int _sniperLaserAmmo;
+
+ // Sub-frame regen accumulators (server-only, not synced)
+ private float shotgunRegenAccumulator;
+ private float rapidLaserRegenAccumulator;
+ private float grenadeLauncherRegenAccumulator;
+ private float sniperLaserRegenAccumulator;
+
+ private CharacterBody characterBody;
+ private CharacterMotor characterMotor;
+
+ public WeaponType currentWeapon => _currentWeapon;
+ public bool isBallForm => _isBallForm;
+
+ private void Awake()
+ {
+ if (!TryGetComponent(out characterBody))
+ {
+ Debug.LogError("NemToolbotController: Failed to get CharacterBody on " + gameObject.name);
+ }
+ if (!TryGetComponent(out characterMotor))
+ {
+ Debug.LogError("NemToolbotController: Failed to get CharacterMotor on " + gameObject.name);
+ }
+
+ // Initialize ammo to max
+ _shotgunAmmo = shotgunMaxAmmo;
+ _rapidLaserAmmo = rapidLaserMaxAmmo;
+ _grenadeLauncherAmmo = grenadeLauncherMaxAmmo;
+ _sniperLaserAmmo = sniperLaserMaxAmmo;
+ }
+
+ ///
+ /// Maps a raycast distance to the appropriate weapon type.
+ ///
+ public static WeaponType GetWeaponFromRange(float distance)
+ {
+ if (distance < shotgunMaxRange)
+ return WeaponType.Shotgun;
+ if (distance < rapidLaserMaxRange)
+ return WeaponType.RapidLaser;
+ if (distance < grenadeLauncherMaxRange)
+ return WeaponType.GrenadeLauncher;
+ return WeaponType.SniperLaser;
+ }
+
+ ///
+ /// Sets the current weapon. Server-only.
+ ///
+ [Server]
+ public void SetWeapon(WeaponType weapon)
+ {
+ Debug.Log($"[NemToolbot] SetWeapon: {_currentWeapon} -> {weapon}");
+ OnWeaponChanged(weapon);
+ _currentWeapon = weapon;
+ }
+
+ ///
+ /// Sets ball form state. Server-only.
+ ///
+ [Server]
+ public void SetBallForm(bool ballForm)
+ {
+ Debug.Log($"[NemToolbot] SetBallForm: {_isBallForm} -> {ballForm}");
+ OnFormChanged(ballForm);
+ _isBallForm = ballForm;
+ }
+
+ #region Ammo System
+
+ ///
+ /// Returns the max ammo for a given weapon type.
+ ///
+ public static int GetMaxAmmo(WeaponType weapon)
+ {
+ switch (weapon)
+ {
+ case WeaponType.Shotgun: return shotgunMaxAmmo;
+ case WeaponType.RapidLaser: return rapidLaserMaxAmmo;
+ case WeaponType.GrenadeLauncher: return grenadeLauncherMaxAmmo;
+ case WeaponType.SniperLaser: return sniperLaserMaxAmmo;
+ default: return 0;
+ }
+ }
+
+ ///
+ /// Returns the current ammo for a given weapon type. Client-readable.
+ ///
+ public int GetAmmo(WeaponType weapon)
+ {
+ switch (weapon)
+ {
+ case WeaponType.Shotgun: return _shotgunAmmo;
+ case WeaponType.RapidLaser: return _rapidLaserAmmo;
+ case WeaponType.GrenadeLauncher: return _grenadeLauncherAmmo;
+ case WeaponType.SniperLaser: return _sniperLaserAmmo;
+ default: return 0;
+ }
+ }
+
+ ///
+ /// Returns true if the given weapon has at least 1 ammo. Client-readable.
+ ///
+ public bool HasAmmo(WeaponType weapon)
+ {
+ return GetAmmo(weapon) > 0;
+ }
+
+ ///
+ /// Attempts to consume 1 ammo from the given weapon's pool. Server-only.
+ /// Returns true if ammo was consumed, false if the pool was empty.
+ ///
+ [Server]
+ public bool TryConsumeAmmo(WeaponType weapon)
+ {
+ switch (weapon)
+ {
+ case WeaponType.Shotgun:
+ if (_shotgunAmmo <= 0) return false;
+ _shotgunAmmo--;
+ return true;
+ case WeaponType.RapidLaser:
+ if (_rapidLaserAmmo <= 0) return false;
+ _rapidLaserAmmo--;
+ return true;
+ case WeaponType.GrenadeLauncher:
+ if (_grenadeLauncherAmmo <= 0) return false;
+ _grenadeLauncherAmmo--;
+ return true;
+ case WeaponType.SniperLaser:
+ if (_sniperLaserAmmo <= 0) return false;
+ _sniperLaserAmmo--;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Regenerates ammo for all weapons. Called by BallMainState each tick.
+ /// Server-only. Uses float accumulators for sub-frame precision.
+ ///
+ public void RegenAllAmmo(float deltaTime)
+ {
+ if (!NetworkServer.active)
+ return;
+
+ RegenWeaponAmmo(ref _shotgunAmmo, shotgunMaxAmmo, shotgunRegenRate, ref shotgunRegenAccumulator, deltaTime);
+ RegenWeaponAmmo(ref _rapidLaserAmmo, rapidLaserMaxAmmo, rapidLaserRegenRate, ref rapidLaserRegenAccumulator, deltaTime);
+ RegenWeaponAmmo(ref _grenadeLauncherAmmo, grenadeLauncherMaxAmmo, grenadeLauncherRegenRate, ref grenadeLauncherRegenAccumulator, deltaTime);
+ RegenWeaponAmmo(ref _sniperLaserAmmo, sniperLaserMaxAmmo, sniperLaserRegenRate, ref sniperLaserRegenAccumulator, deltaTime);
+ }
+
+ private static void RegenWeaponAmmo(ref int currentAmmo, int maxAmmo, float regenRate, ref float accumulator, float deltaTime)
+ {
+ if (currentAmmo >= maxAmmo)
+ {
+ accumulator = 0f;
+ return;
+ }
+
+ accumulator += regenRate * deltaTime;
+ if (accumulator >= 1f)
+ {
+ int toAdd = Mathf.FloorToInt(accumulator);
+ accumulator -= toAdd;
+ currentAmmo = Mathf.Min(currentAmmo + toAdd, maxAmmo);
+ }
+ }
+
+ ///
+ /// Resets all regen accumulators. Call when exiting ball form.
+ ///
+ public void ResetRegenAccumulators()
+ {
+ shotgunRegenAccumulator = 0f;
+ rapidLaserRegenAccumulator = 0f;
+ grenadeLauncherRegenAccumulator = 0f;
+ sniperLaserRegenAccumulator = 0f;
+ }
+
+ #endregion
+
+ ///
+ /// Returns a damage multiplier based on current velocity relative to base move speed.
+ /// Used by ball form attacks to scale damage with momentum.
+ ///
+ public float GetDamageMultiplierFromSpeed()
+ {
+ if (characterBody == null || characterMotor == null)
+ return 1f;
+
+ float currentSpeed = characterMotor.velocity.magnitude;
+ float baseSpeed = characterBody.baseMoveSpeed;
+ if (baseSpeed <= 0f)
+ return 1f;
+
+ return Mathf.Max(1f, currentSpeed / baseSpeed);
+ }
+
+ private void OnWeaponChanged(WeaponType newWeapon)
+ {
+ _currentWeapon = newWeapon;
+ }
+
+ private void OnFormChanged(bool newBallForm)
+ {
+ _isBallForm = newBallForm;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotController.cs.meta b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotController.cs.meta
new file mode 100644
index 000000000..8392388b7
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 96211b05081c54e49b5e6d88ee802112
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotGrappleController.cs b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotGrappleController.cs
new file mode 100644
index 000000000..15fad0257
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotGrappleController.cs
@@ -0,0 +1,640 @@
+using System;
+using EntityStates;
+using EntityStates.NemToolbot;
+using UnityEngine;
+using UnityEngine.Networking;
+using RoR2;
+using RoR2.Projectile;
+
+namespace SS2.Components
+{
+ ///
+ /// Projectile component that drives NemToolbot's grappling hook lifecycle.
+ /// Based on Loader's ProjectileGrappleController with identical feel.
+ ///
+ /// State pipeline:
+ /// FlyState --> GripState (heavy target or world) --> ReturnState
+ /// |-> YankState (light enemy) --> ReturnState
+ /// |-> ReturnState (missed / max distance)
+ ///
+ /// Changes from Loader's version:
+ /// - Casts to FireGrapplingHook instead of FireHook
+ /// - Configurable ESM customName (hookStateMachineName)
+ /// - TryGetComponent everywhere with Debug.LogError on failure
+ /// - Configurable stock deduction slot
+ ///
+ [RequireComponent(typeof(EntityStateMachine))]
+ [RequireComponent(typeof(ProjectileController))]
+ [RequireComponent(typeof(ProjectileSimple))]
+ [RequireComponent(typeof(ProjectileStickOnImpact))]
+ public class NemToolbotGrappleController : MonoBehaviour
+ {
+ private struct OwnerInfo
+ {
+ public readonly GameObject gameObject;
+ public readonly CharacterBody characterBody;
+ public readonly CharacterMotor characterMotor;
+ public readonly EntityStateMachine stateMachine;
+ public readonly bool hasEffectiveAuthority;
+
+ public OwnerInfo(GameObject ownerGameObject, string hookESMName)
+ {
+ this = default(OwnerInfo);
+ gameObject = ownerGameObject;
+ if (!gameObject)
+ {
+ return;
+ }
+
+ if (!gameObject.TryGetComponent(out CharacterBody body))
+ {
+ Debug.LogError("NemToolbotGrappleController: Owner missing CharacterBody on " + gameObject.name);
+ }
+ characterBody = body;
+
+ if (!gameObject.TryGetComponent(out CharacterMotor motor))
+ {
+ Debug.LogError("NemToolbotGrappleController: Owner missing CharacterMotor on " + gameObject.name);
+ }
+ characterMotor = motor;
+
+ hasEffectiveAuthority= Util.HasEffectiveAuthority(gameObject);
+
+ EntityStateMachine[] esms = gameObject.GetComponents();
+ for (int i = 0; i < esms.Length; i++)
+ {
+ if (esms[i].customName == hookESMName)
+ {
+ stateMachine = esms[i];
+ break;
+ }
+ }
+
+ if (!stateMachine)
+ {
+ Debug.LogError("NemToolbotGrappleController: Owner missing ESM with customName '" + hookESMName + "' on " + gameObject.name);
+ }
+ }
+ }
+
+ #region Inner States
+
+ ///
+ /// Shared foundation for all grapple hook states. Validates owner each tick,
+ /// updates spatial data, and auto-retracts when owner exits firing state.
+ ///
+ private class BaseState : EntityStates.BaseState
+ {
+ protected NemToolbotGrappleController grappleController;
+ protected Vector3 aimOrigin;
+ protected Vector3 position;
+
+ protected bool ownerValid { get; private set; }
+ protected ref OwnerInfo owner => ref grappleController.owner;
+
+ private void UpdatePositions()
+ {
+ aimOrigin = grappleController.owner.characterBody.aimOrigin;
+ position = base.transform.position + base.transform.up * grappleController.normalOffset;
+ }
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ if (!base.gameObject.TryGetComponent(out NemToolbotGrappleController controller))
+ {
+ Debug.LogError("NemToolbotGrappleController.BaseState: Missing NemToolbotGrappleController on " + base.gameObject.name);
+ return;
+ }
+ grappleController = controller;
+ ownerValid = (bool)grappleController && (bool)grappleController.owner.gameObject;
+ if (ownerValid)
+ {
+ UpdatePositions();
+ }
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (ownerValid)
+ {
+ ownerValid &= grappleController.owner.gameObject;
+ if (ownerValid)
+ {
+ UpdatePositions();
+ FixedUpdateBehavior();
+ }
+ }
+ if (NetworkServer.active && !ownerValid)
+ {
+ ownerValid = false;
+ EntityState.Destroy(base.gameObject);
+ }
+ }
+
+ protected virtual void FixedUpdateBehavior()
+ {
+ if (base.isAuthority && !grappleController.OwnerIsInFiringState())
+ {
+ outer.SetNextState(new ReturnState());
+ }
+ }
+
+ protected Ray GetOwnerAimRay()
+ {
+ if (!owner.characterBody)
+ {
+ return default(Ray);
+ }
+ return owner.characterBody.inputBank.GetAimRay();
+ }
+ }
+
+ ///
+ /// Hook is traveling outward. On impact, transitions to GripState (heavy/world)
+ /// or YankState (light enemy). Auto-retracts if max distance reached.
+ ///
+ private class FlyState : BaseState
+ {
+ private float duration;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ if (grappleController != null)
+ {
+ duration = grappleController.maxTravelDistance / grappleController.projectileSimple.velocity;
+ }
+ }
+
+ protected override void FixedUpdateBehavior()
+ {
+ base.FixedUpdateBehavior();
+ if (!base.isAuthority)
+ {
+ return;
+ }
+
+ if (grappleController.projectileStickOnImpactController.stuck)
+ {
+ EntityState nextState = null;
+
+ if ((bool)grappleController.projectileStickOnImpactController.stuckBody)
+ {
+ if (grappleController.projectileStickOnImpactController.stuckBody.TryGetComponent(out Rigidbody stuckRb))
+ {
+ if (stuckRb.mass < grappleController.yankMassLimit)
+ {
+ stuckRb.TryGetComponent(out CharacterBody stuckCharBody);
+ if (!stuckCharBody
+ || !stuckCharBody.isPlayerControlled
+ || stuckCharBody.teamComponent.teamIndex != base.projectileController.teamFilter.teamIndex
+ || FriendlyFireManager.ShouldDirectHitProceed(stuckCharBody.healthComponent, base.projectileController.teamFilter.teamIndex))
+ {
+ nextState = new YankState();
+ }
+ }
+ }
+ }
+
+ if (nextState == null)
+ {
+ nextState = new GripState();
+ }
+
+ DeductOwnerStock();
+ outer.SetNextState(nextState);
+ }
+ else if (duration <= base.fixedAge)
+ {
+ outer.SetNextState(new ReturnState());
+ }
+ }
+
+ private void DeductOwnerStock()
+ {
+ if (!base.ownerValid || !base.owner.hasEffectiveAuthority)
+ {
+ return;
+ }
+
+ if (base.owner.gameObject.TryGetComponent(out SkillLocator skillLocator))
+ {
+ GenericSkill skill = grappleController.GetDeductionSkill(skillLocator);
+ if ((bool)skill)
+ {
+ skill.DeductStock(1);
+ }
+ }
+ else
+ {
+ Debug.LogError("NemToolbotGrappleController.FlyState: Owner missing SkillLocator on " + base.owner.gameObject.name);
+ }
+ }
+ }
+
+ ///
+ /// Shared base for GripState and YankState. Tracks distance and handles
+ /// release conditions: unstuck, near-break, button release, state exit.
+ ///
+ private class BaseGripState : BaseState
+ {
+ protected float currentDistance;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ currentDistance = Vector3.Distance(aimOrigin, position);
+ }
+
+ protected override void FixedUpdateBehavior()
+ {
+ base.FixedUpdateBehavior();
+ currentDistance = Vector3.Distance(aimOrigin, position);
+ if (base.isAuthority)
+ {
+ bool hookDetached = !grappleController.projectileStickOnImpactController.stuck;
+ bool arrivedAtHook = currentDistance < grappleController.nearBreakDistance;
+ bool ownerLeftFiringState = !grappleController.OwnerIsInFiringState();
+
+ if (!base.owner.stateMachine
+ || !((base.owner.stateMachine.state as BaseSkillState)?.IsKeyDownAuthority() ?? false)
+ || ownerLeftFiringState
+ || arrivedAtHook
+ || hookDetached)
+ {
+ outer.SetNextState(new ReturnState());
+ }
+ }
+ }
+ }
+
+ ///
+ /// Pulls the PLAYER toward the hook point (grapple swing). Identical force model
+ /// to Loader's GripState: base acceleration + look steering + move input + escape force.
+ ///
+ private class GripState : BaseGripState
+ {
+ private float lastDistance;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ lastDistance = Vector3.Distance(aimOrigin, position);
+ if (base.ownerValid)
+ {
+ if ((bool)base.owner.characterMotor)
+ {
+ Vector3 direction = GetOwnerAimRay().direction;
+ Vector3 velocity = base.owner.characterMotor.velocity;
+ velocity = ((Vector3.Dot(velocity, direction) < 0f) ? Vector3.zero : Vector3.Project(velocity, direction));
+ velocity += direction * grappleController.initialLookImpulse;
+ velocity += base.owner.characterMotor.moveDirection * grappleController.initiallMoveImpulse;
+ base.owner.characterMotor.velocity = velocity;
+ }
+ }
+ }
+
+ protected override void FixedUpdateBehavior()
+ {
+ base.FixedUpdateBehavior();
+ float accel = grappleController.acceleration;
+
+ if (currentDistance > lastDistance)
+ {
+ accel *= grappleController.escapeForceMultiplier;
+ }
+ lastDistance = currentDistance;
+
+ if (base.owner.hasEffectiveAuthority && (bool)base.owner.characterMotor && (bool)base.owner.characterBody)
+ {
+ Ray ownerAimRay = GetOwnerAimRay();
+ Vector3 hookDirection = (base.transform.position - base.owner.characterBody.aimOrigin).normalized;
+ Vector3 force = hookDirection * accel;
+
+ float time = Mathf.Clamp01(base.fixedAge / grappleController.lookAccelerationRampUpDuration);
+ float rampUpFactor = grappleController.lookAccelerationRampUpCurve.Evaluate(time);
+ float alignmentFactor = Util.Remap(Vector3.Dot(ownerAimRay.direction, hookDirection), -1f, 1f, 1f, 0f);
+ force += ownerAimRay.direction * (grappleController.lookAcceleration * rampUpFactor * alignmentFactor);
+
+ force += base.owner.characterMotor.moveDirection * grappleController.moveAcceleration;
+
+ base.owner.characterMotor.ApplyForce(force * (base.owner.characterMotor.mass * GetDeltaTime()), alwaysApply: true, disableAirControlUntilCollision: true);
+ }
+ }
+ }
+
+ ///
+ /// Pulls a LIGHT ENEMY toward the player. Identical to Loader's YankState:
+ /// IDisplacementReceiver pull with delay, brief owner hover.
+ ///
+ private class YankState : BaseGripState
+ {
+ public static float yankSpeed;
+ public static float delayBeforeYanking;
+ public static float hoverTimeLimit = 0.5f;
+
+ private CharacterBody stuckBody;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ stuckBody = grappleController.projectileStickOnImpactController.stuckBody;
+ }
+
+ protected override void FixedUpdateBehavior()
+ {
+ base.FixedUpdateBehavior();
+ if (!stuckBody)
+ {
+ return;
+ }
+
+ if (Util.HasEffectiveAuthority(stuckBody.gameObject))
+ {
+ Vector3 pullDirection = aimOrigin - position;
+ if (stuckBody.TryGetComponent(out IDisplacementReceiver displacementReceiver) && base.fixedAge >= delayBeforeYanking)
+ {
+ displacementReceiver.AddDisplacement(pullDirection * (yankSpeed * GetDeltaTime()));
+ }
+ }
+
+ if (base.owner.hasEffectiveAuthority && (bool)base.owner.characterMotor && base.fixedAge < hoverTimeLimit)
+ {
+ Vector3 velocity = base.owner.characterMotor.velocity;
+ if (velocity.y < 0f)
+ {
+ velocity.y = 0f;
+ base.owner.characterMotor.velocity = velocity;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Hook retracts back to the player. Detaches, disables collider,
+ /// accelerates toward aimOrigin, and self-destructs on overshoot.
+ ///
+ private class ReturnState : BaseState
+ {
+ private float returnSpeedAcceleration = 240f;
+ private float returnSpeed;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ if (base.ownerValid)
+ {
+ returnSpeed = grappleController.projectileSimple.velocity;
+ returnSpeedAcceleration = returnSpeed * 2f;
+ }
+
+ if (NetworkServer.active && (bool)grappleController)
+ {
+ grappleController.projectileStickOnImpactController.Detach();
+ grappleController.projectileStickOnImpactController.ignoreCharacters = true;
+ grappleController.projectileStickOnImpactController.ignoreWorld = true;
+ }
+
+ if (base.gameObject.TryGetComponent(out Collider collider))
+ {
+ collider.enabled = false;
+ }
+ }
+
+ protected override void FixedUpdateBehavior()
+ {
+ base.FixedUpdateBehavior();
+ if (!base.rigidbody)
+ {
+ return;
+ }
+
+ returnSpeed += returnSpeedAcceleration * GetDeltaTime();
+ base.rigidbody.velocity = (aimOrigin - position).normalized * returnSpeed;
+
+ if (NetworkServer.active)
+ {
+ Vector3 endPosition = position + base.rigidbody.velocity * GetDeltaTime();
+ if (HGMath.Overshoots(position, endPosition, aimOrigin))
+ {
+ EntityState.Destroy(base.gameObject);
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region Controller Fields
+
+ private ProjectileController projectileController;
+ private ProjectileStickOnImpact projectileStickOnImpactController;
+ private ProjectileSimple projectileSimple;
+
+ ///
+ /// The EntityState type the owner must be in for the hook to remain active.
+ /// Set to FireGrapplingHook in the inspector. If the owner exits this state, the hook retracts.
+ ///
+ public SerializableEntityStateType ownerHookStateType;
+
+ ///
+ /// Name of the EntityStateMachine on the owner that runs the hook firing state.
+ /// Default "Hook". Must match the ESM customName on the NemToolbot character body.
+ ///
+ [Tooltip("ESM customName on the owner body that runs the hook firing state.")]
+ public string hookStateMachineName = "Hook";
+
+ ///
+ /// Which skill slot to deduct stock from on successful hook impact.
+ ///
+ public enum DeductSlot
+ {
+ Primary,
+ Secondary,
+ Utility,
+ Special,
+ None
+ }
+
+ [Tooltip("Which skill slot to deduct stock from when the hook sticks.")]
+ public DeductSlot deductStockSlot = DeductSlot.Secondary;
+
+ public float acceleration;
+ public float lookAcceleration = 4f;
+ public float lookAccelerationRampUpDuration = 0.25f;
+ public float initialLookImpulse = 5f;
+ public float initiallMoveImpulse = 5f;
+ public float moveAcceleration = 4f;
+
+ public string enterSoundString;
+ public string exitSoundString;
+ public string hookDistanceRTPCstring;
+ public float minHookDistancePitchModifier;
+ public float maxHookDistancePitchModifier;
+
+ public AnimationCurve lookAccelerationRampUpCurve;
+ public Transform ropeEndTransform;
+ public string muzzleStringOnBody = "MuzzleLeft";
+
+ [Tooltip("Minimum distance before the hook auto-detaches (arrival).")]
+ public float nearBreakDistance;
+
+ [Tooltip("Maximum travel distance for the hook.")]
+ public float maxTravelDistance;
+
+ public float escapeForceMultiplier = 2f;
+ public float normalOffset = 1f;
+ public float yankMassLimit;
+
+ private Type resolvedOwnerHookStateType;
+ private OwnerInfo owner;
+ private uint soundID;
+
+ #endregion
+
+ #region Lifecycle
+
+ private void Awake()
+ {
+ if (!TryGetComponent(out projectileStickOnImpactController))
+ {
+ Debug.LogError("NemToolbotGrappleController: Missing ProjectileStickOnImpact on " + gameObject.name);
+ }
+ if (!TryGetComponent(out projectileController))
+ {
+ Debug.LogError("NemToolbotGrappleController: Missing ProjectileController on " + gameObject.name);
+ }
+ if (!TryGetComponent(out projectileSimple))
+ {
+ Debug.LogError("NemToolbotGrappleController: Missing ProjectileSimple on " + gameObject.name);
+ }
+
+ resolvedOwnerHookStateType = ownerHookStateType.stateType;
+
+ if ((bool)ropeEndTransform)
+ {
+ soundID = Util.PlaySound(enterSoundString, ropeEndTransform.gameObject);
+ }
+ }
+
+ private void Start()
+ {
+ if (projectileController != null && projectileController.owner != null)
+ {
+ owner = new OwnerInfo(projectileController.owner, hookStateMachineName);
+ AssignHookReferenceToBodyStateMachine();
+ }
+ else
+ {
+ Debug.LogError("NemToolbotGrappleController: projectileController or owner is null on Start for " + gameObject.name);
+ }
+ }
+
+ private void FixedUpdate()
+ {
+ if ((bool)ropeEndTransform)
+ {
+ float in_value = Util.Remap(
+ (ropeEndTransform.transform.position - base.transform.position).magnitude,
+ minHookDistancePitchModifier,
+ maxHookDistancePitchModifier,
+ 0f, 100f);
+ AkSoundEngine.SetRTPCValueByPlayingID(hookDistanceRTPCstring, in_value, soundID);
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if ((bool)ropeEndTransform)
+ {
+ Util.PlaySound(exitSoundString, ropeEndTransform.gameObject);
+ UnityEngine.Object.Destroy(ropeEndTransform.gameObject);
+ }
+ else
+ {
+ AkSoundEngine.StopPlayingID(soundID);
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ ///
+ /// Links this hook projectile back to the owner's FireGrapplingHook state
+ /// and parents the rope end to the owner's muzzle bone.
+ ///
+ private void AssignHookReferenceToBodyStateMachine()
+ {
+ if ((bool)owner.stateMachine && owner.stateMachine.state is FireGrapplingHook fireGrapplingHook)
+ {
+ fireGrapplingHook.SetHookReference(base.gameObject);
+ }
+
+ if (!owner.gameObject.TryGetComponent(out ModelLocator modelLocator))
+ {
+ Debug.LogError("NemToolbotGrappleController: Owner missing ModelLocator on " + owner.gameObject.name);
+ return;
+ }
+
+ Transform modelTransform = modelLocator.modelTransform;
+ if (!modelTransform)
+ {
+ return;
+ }
+
+ if (modelTransform.TryGetComponent(out ChildLocator childLocator))
+ {
+ Transform muzzle = childLocator.FindChild(muzzleStringOnBody);
+ if ((bool)muzzle && (bool)ropeEndTransform)
+ {
+ ropeEndTransform.SetParent(muzzle, worldPositionStays: false);
+ }
+ }
+ else
+ {
+ Debug.LogError("NemToolbotGrappleController: Owner model missing ChildLocator on " + modelTransform.name);
+ }
+ }
+
+ private bool OwnerIsInFiringState()
+ {
+ if ((bool)owner.stateMachine)
+ {
+ return owner.stateMachine.state.GetType() == resolvedOwnerHookStateType;
+ }
+ return false;
+ }
+
+ private GenericSkill GetDeductionSkill(SkillLocator skillLocator)
+ {
+ switch (deductStockSlot)
+ {
+ case DeductSlot.Primary:
+ return skillLocator.primary;
+ case DeductSlot.Secondary:
+ return skillLocator.secondary;
+ case DeductSlot.Utility:
+ return skillLocator.utility;
+ case DeductSlot.Special:
+ return skillLocator.special;
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Configures a projectile's EntityStateMachine to use this controller's inner FlyState.
+ /// Call this when setting up the grapple hook projectile prefab in code.
+ ///
+ public static void ConfigureProjectileESM(EntityStateMachine esm)
+ {
+ esm.initialStateType = new SerializableEntityStateType(typeof(FlyState));
+ esm.mainStateType = new SerializableEntityStateType(typeof(FlyState));
+ }
+
+ #endregion
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotGrappleController.cs.meta b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotGrappleController.cs.meta
new file mode 100644
index 000000000..55b28debf
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Components/NemToolbot/NemToolbotGrappleController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 10f3ec3e4a77aa44d94cefee11c06a5a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/NemToolbot.cs b/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/NemToolbot.cs
new file mode 100644
index 000000000..88fa3d687
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/NemToolbot.cs
@@ -0,0 +1,251 @@
+using MSU;
+using RoR2;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using UnityEngine.Networking;
+using RoR2.ContentManagement;
+using RoR2.Projectile;
+using SS2.Components;
+using R2API;
+using EntityStates.NemToolbot;
+
+#if DEBUG
+namespace SS2.Survivors
+{
+ public sealed class NemToolbot : SS2Survivor
+ {
+ public override SS2AssetRequest AssetRequest => SS2Assets.LoadAssetAsync("acNemToolbot", SS2Bundle.Indev);
+
+ private GameObject grappleHookProjectile;
+ private GameObject grenadeProjectile;
+ private GameObject deployChargeProjectile;
+
+ public override void Initialize()
+ {
+ ModifyPrefab();
+ CreateProjectiles();
+ AssignEffects();
+ }
+
+ private void ModifyPrefab()
+ {
+ if (!CharacterPrefab.TryGetComponent(out CharacterBody cb))
+ {
+ Debug.LogError("NemToolbot.ModifyPrefab: Failed to get CharacterBody on " + CharacterPrefab.name);
+ return;
+ }
+
+ cb._defaultCrosshairPrefab = Addressables.LoadAssetAsync("RoR2/Base/UI/StandardCrosshair.prefab").WaitForCompletion();
+ cb.preferredPodPrefab = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/RoboCratePod.prefab").WaitForCompletion();
+
+ if (cb.TryGetComponent(out ModelLocator modelLocator) && modelLocator.modelTransform != null)
+ {
+ if (modelLocator.modelTransform.TryGetComponent(out FootstepHandler footstepHandler))
+ {
+ footstepHandler.footstepDustPrefab = Addressables.LoadAssetAsync("RoR2/Base/Common/VFX/GenericFootstepDust.prefab").WaitForCompletion();
+ }
+ }
+ }
+
+ private static void EnsureNetworkIdentity(GameObject prefab)
+ {
+ if (!prefab.GetComponent())
+ {
+ prefab.AddComponent();
+ }
+ }
+
+ private void CreateProjectiles()
+ {
+ CreateGrappleHookProjectile();
+ CreateGrenadeProjectile();
+ CreateDeployChargeProjectile();
+ }
+
+ private void CreateGrappleHookProjectile()
+ {
+ GameObject loaderHook = Addressables.LoadAssetAsync("RoR2/Base/Loader/LoaderHook.prefab").WaitForCompletion();
+ grappleHookProjectile = loaderHook.InstantiateClone("NemToolbotGrappleHook", true);
+ EnsureNetworkIdentity(grappleHookProjectile);
+
+ // Null out ropeEndTransform before destroy to prevent vanilla OnDestroy
+ // from deleting the RopeEnd child we need for our controller
+ ProjectileGrappleController loaderGrapple = grappleHookProjectile.GetComponent();
+ if (loaderGrapple != null)
+ {
+ loaderGrapple.ropeEndTransform = null;
+ Object.DestroyImmediate(loaderGrapple);
+ }
+
+ // Reconfigure the projectile's ESM before adding our controller
+ // (RequireComponent won't duplicate it since one already exists from the clone)
+ if (grappleHookProjectile.TryGetComponent(out EntityStateMachine esm))
+ {
+ NemToolbotGrappleController.ConfigureProjectileESM(esm);
+ }
+ else
+ {
+ Debug.LogError("NemToolbot: Cloned LoaderHook missing EntityStateMachine.");
+ }
+
+ // Add our grapple controller and configure
+ NemToolbotGrappleController grappleController = grappleHookProjectile.AddComponent();
+ grappleController.ownerHookStateType = new EntityStates.SerializableEntityStateType(typeof(FireGrapplingHook));
+ grappleController.hookStateMachineName = "Hook";
+ grappleController.acceleration = 45f;
+ grappleController.lookAcceleration = 35f;
+ grappleController.lookAccelerationRampUpDuration = 0.5f;
+ grappleController.initialLookImpulse = 0f;
+ grappleController.initiallMoveImpulse = 15f;
+ grappleController.moveAcceleration = 5f;
+ grappleController.nearBreakDistance = 1f;
+ grappleController.maxTravelDistance = 80f;
+ grappleController.escapeForceMultiplier = 4f;
+ grappleController.normalOffset = 1f;
+ grappleController.yankMassLimit = 0f;
+ grappleController.muzzleStringOnBody = "MuzzleLeft";
+ grappleController.deductStockSlot = NemToolbotGrappleController.DeductSlot.Secondary;
+ grappleController.lookAccelerationRampUpCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
+ grappleController.enterSoundString = "Play_loader_m2_travel_loop";
+ grappleController.exitSoundString = "Stop_loader_m2_travel_loop";
+ grappleController.hookDistanceRTPCstring = "loaderM2_grappleRemain";
+ grappleController.minHookDistancePitchModifier = 3f;
+ grappleController.maxHookDistancePitchModifier = 80f;
+
+ // Find and assign the rope end transform from the cloned prefab
+ Transform ropeEnd = grappleHookProjectile.transform.Find("RopeEnd");
+ if (ropeEnd != null)
+ {
+ grappleController.ropeEndTransform = ropeEnd;
+ }
+ else
+ {
+ Debug.LogError("NemToolbot: Cloned LoaderHook missing RopeEnd child transform.");
+ }
+ }
+
+ private void CreateGrenadeProjectile()
+ {
+ GameObject toolbotGrenade = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/ToolbotGrenadeLauncherProjectile.prefab").WaitForCompletion();
+ grenadeProjectile = toolbotGrenade.InstantiateClone("NemToolbotGrenadeProjectile", true);
+ EnsureNetworkIdentity(grenadeProjectile);
+
+ // Enable gravity for arc trajectory
+ if (grenadeProjectile.TryGetComponent(out Rigidbody rb))
+ {
+ rb.useGravity = true;
+ }
+ else
+ {
+ Debug.LogError("NemToolbot: Cloned grenade missing Rigidbody.");
+ }
+
+ // Set explosion VFX
+ if (grenadeProjectile.TryGetComponent(out ProjectileImpactExplosion pie))
+ {
+ pie.impactEffect = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/OmniExplosionVFXToolbotQuick.prefab").WaitForCompletion();
+ }
+ }
+
+ private void CreateDeployChargeProjectile()
+ {
+ GameObject toolbotGrenade = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/ToolbotGrenadeLauncherProjectile.prefab").WaitForCompletion();
+ deployChargeProjectile = toolbotGrenade.InstantiateClone("NemToolbotDeployCharge", true);
+ EnsureNetworkIdentity(deployChargeProjectile);
+
+ // Stationary: no forward movement, no gravity
+ if (deployChargeProjectile.TryGetComponent(out ProjectileSimple ps))
+ {
+ ps.desiredForwardSpeed = 0f;
+ ps.lifetime = 4f;
+ }
+ else
+ {
+ Debug.LogError("NemToolbot: Cloned deploy charge missing ProjectileSimple.");
+ }
+
+ if (deployChargeProjectile.TryGetComponent(out Rigidbody rb))
+ {
+ rb.useGravity = false;
+ rb.isKinematic = true;
+ }
+
+ // Timed detonation: explode after lifetime, not on contact
+ if (deployChargeProjectile.TryGetComponent(out ProjectileImpactExplosion pie))
+ {
+ pie.destroyOnWorld = false;
+ pie.destroyOnEnemy = false;
+ pie.timerAfterImpact = false;
+ pie.lifetime = 4f;
+ pie.impactEffect = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/OmniExplosionVFXToolbotQuick.prefab").WaitForCompletion();
+ }
+ else
+ {
+ Debug.LogError("NemToolbot: Cloned deploy charge missing ProjectileImpactExplosion.");
+ }
+ }
+
+ private void AssignEffects()
+ {
+ // Cache shared loads
+ GameObject muzzleflashFMJ = Addressables.LoadAssetAsync("RoR2/Base/Commando/MuzzleflashFMJ.prefab").WaitForCompletion();
+ GameObject hitsparkBandit = Addressables.LoadAssetAsync("RoR2/Base/Bandit2/HitsparkBandit.prefab").WaitForCompletion();
+ GameObject loaderGroundSlam = Addressables.LoadAssetAsync("RoR2/Base/Loader/LoaderGroundSlam.prefab").WaitForCompletion();
+
+ // Shotgun
+ FireShotgun.muzzleFlashPrefab = muzzleflashFMJ;
+ FireShotgun.tracerEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Bandit2/TracerBandit2Shotgun.prefab").WaitForCompletion();
+ FireShotgun.hitEffectPrefab = hitsparkBandit;
+
+ // Rapid laser
+ FireRapidLaser.muzzleFlashPrefab = muzzleflashFMJ;
+ FireRapidLaser.tracerEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/CaptainDefenseMatrix/TracerCaptainDefenseMatrix.prefab").WaitForCompletion();
+ FireRapidLaser.hitEffectPrefab = hitsparkBandit;
+
+ // Sniper laser
+ FireSniperLaser.muzzleFlashPrefab = Addressables.LoadAssetAsync("RoR2/DLC1/Railgunner/MuzzleflashRailgun.prefab").WaitForCompletion();
+ FireSniperLaser.tracerEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/TracerToolbotRebar.prefab").WaitForCompletion();
+ FireSniperLaser.hitEffectPrefab = Addressables.LoadAssetAsync("RoR2/DLC1/Railgunner/ImpactRailgun.prefab").WaitForCompletion();
+
+ // Grenade launcher
+ FireGrenadeLauncher.muzzleFlashPrefab = muzzleflashFMJ;
+ FireGrenadeLauncher.projectilePrefab = grenadeProjectile;
+
+ // Deploy charges
+ DeployCharges.muzzleFlashPrefab = muzzleflashFMJ;
+ DeployCharges.chargeProjectilePrefab = deployChargeProjectile;
+
+ // Grappling hook
+ FireGrapplingHook.muzzleflashEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Loader/MuzzleflashLoader.prefab").WaitForCompletion();
+ FireGrapplingHook.projectilePrefab = grappleHookProjectile;
+
+ // Ball boost
+ BallBoost.startEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/ImpactToolbotDash.prefab").WaitForCompletion();
+ BallBoost.impactEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/ImpactToolbotDashLarge.prefab").WaitForCompletion();
+
+ // Ball slam
+ BallSlam.blastEffectPrefab = loaderGroundSlam;
+ BallSlam.blastImpactEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Loader/OmniImpactVFXLoader.prefab").WaitForCompletion();
+ BallSlam.slamEffectPrefab = loaderGroundSlam;
+ }
+
+ public override void ModifyContentPack(ContentPack contentPack)
+ {
+ base.ModifyContentPack(contentPack);
+ contentPack.projectilePrefabs.Add(new GameObject[]
+ {
+ grappleHookProjectile,
+ grenadeProjectile,
+ deployChargeProjectile,
+ });
+ }
+
+ // NOTE: Must return true (or use SS2Config beta gate) for Initialize()
+ // and ModifyContentPack() to run. MSU skips both when IsAvailable=false.
+ public override bool IsAvailable(ContentPack contentPack)
+ {
+ return false;
+ }
+ }
+}
+#endif
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot.meta
new file mode 100644
index 000000000..5be4d49b9
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 949db2a536025984db3b04a98fe40183
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallBoost.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallBoost.cs
new file mode 100644
index 000000000..d86bbee2f
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallBoost.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Ball form primary (grounded). Boosts forward at high speed with an OverlapAttack,
+ /// dealing speed-scaled damage to enemies in the path. Similar to Mul-T's ToolbotDash.
+ /// If used while airborne, immediately dispatches to BallSlam instead.
+ ///
+ public class BallBoost : BaseSkillState
+ {
+ [SerializeField]
+ public float baseDuration = 1.5f;
+
+ [SerializeField]
+ public float speedMultiplier = 2.5f;
+
+ public static float baseDamageCoefficient = 4f;
+ public static float awayForceMagnitude = 1000f;
+ public static float upwardForceMagnitude = 500f;
+ public static float hitPauseDuration = 0.1f;
+ public static float recoilAmplitude = 1f;
+ public static float massThresholdForKnockback = 250f;
+ public static float knockbackForce = 3000f;
+
+ public static string startSoundString = "";
+ public static string endSoundString = "";
+ public static string impactSoundString = "";
+ public static GameObject startEffectPrefab;
+ public static GameObject impactEffectPrefab;
+
+ private NemToolbotController controller;
+ private float duration;
+ private float hitPauseTimer;
+ private Vector3 idealDirection;
+ private OverlapAttack attack;
+ private bool inHitPause;
+ private List victimsStruck = new List();
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+
+ // If airborne, dispatch to BallSlam instead
+ if (isAuthority && characterMotor != null && !characterMotor.isGrounded)
+ {
+ outer.SetNextState(new BallSlam());
+ return;
+ }
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot BallBoost: Failed to get NemToolbotController on " + gameObject.name);
+ }
+
+ duration = baseDuration;
+
+ if (isAuthority && inputBank != null)
+ {
+ idealDirection = inputBank.aimDirection;
+ idealDirection.y = 0f;
+ idealDirection.Normalize();
+ }
+
+ if (characterDirection != null)
+ {
+ characterDirection.forward = idealDirection;
+ }
+
+ if (startEffectPrefab != null && characterBody != null)
+ {
+ EffectManager.SpawnEffect(startEffectPrefab, new EffectData
+ {
+ origin = characterBody.corePosition
+ }, transmit: false);
+ }
+
+ Util.PlaySound(startSoundString, gameObject);
+
+ if (characterBody != null)
+ {
+ characterBody.isSprinting = true;
+ }
+
+ // Build overlap attack
+ HitBoxGroup hitBoxGroup = null;
+ Transform modelTransform = GetModelTransform();
+ if (modelTransform != null)
+ {
+ hitBoxGroup = Array.Find(modelTransform.GetComponents(), (HitBoxGroup element) => element.groupName == "Charge");
+ }
+
+ attack = new OverlapAttack();
+ attack.attacker = gameObject;
+ attack.inflictor = gameObject;
+ attack.teamIndex = GetTeam();
+ attack.damage = baseDamageCoefficient * damageStat;
+ attack.hitEffectPrefab = impactEffectPrefab;
+ attack.forceVector = Vector3.up * upwardForceMagnitude;
+ attack.pushAwayForce = awayForceMagnitude;
+ attack.hitBoxGroup = hitBoxGroup;
+ attack.damageType.damageSource = DamageSource.Utility;
+ attack.isCrit = RollCrit();
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ if (fixedAge >= duration)
+ {
+ if (isAuthority)
+ {
+ outer.SetNextStateToMain();
+ }
+ return;
+ }
+
+ if (!isAuthority)
+ return;
+
+ if (!inHitPause)
+ {
+ UpdateDirection();
+
+ if (characterDirection != null)
+ {
+ characterDirection.moveVector = idealDirection;
+ }
+
+ if (characterMotor != null && !characterMotor.disableAirControlUntilCollision)
+ {
+ characterMotor.rootMotion += GetIdealVelocity() * GetDeltaTime();
+ }
+
+ // Scale damage by speed
+ float speedMultFromVelocity = controller != null ? controller.GetDamageMultiplierFromSpeed() : 1f;
+ attack.damage = damageStat * (baseDamageCoefficient * speedMultFromVelocity);
+
+ if (attack.Fire(victimsStruck))
+ {
+ Util.PlaySound(impactSoundString, gameObject);
+ inHitPause = true;
+ hitPauseTimer = hitPauseDuration;
+ AddRecoil(-0.5f * recoilAmplitude, -0.5f * recoilAmplitude, -0.5f * recoilAmplitude, 0.5f * recoilAmplitude);
+ }
+ }
+ else
+ {
+ if (characterMotor != null)
+ {
+ characterMotor.velocity = Vector3.zero;
+ }
+ hitPauseTimer -= GetDeltaTime();
+ if (hitPauseTimer < 0f)
+ {
+ inHitPause = false;
+ }
+ }
+ }
+
+ public override void OnExit()
+ {
+ Util.PlaySound(endSoundString, gameObject);
+
+ if (characterMotor != null && !characterMotor.disableAirControlUntilCollision)
+ {
+ characterMotor.velocity += GetIdealVelocity();
+ }
+
+ if (characterBody != null)
+ {
+ characterBody.isSprinting = false;
+ }
+
+ base.OnExit();
+ }
+
+ private void UpdateDirection()
+ {
+ if (inputBank != null)
+ {
+ Vector2 moveInput = Util.Vector3XZToVector2XY(inputBank.moveVector);
+ if (moveInput != Vector2.zero)
+ {
+ moveInput.Normalize();
+ idealDirection = new Vector3(moveInput.x, 0f, moveInput.y).normalized;
+ }
+ }
+ }
+
+ private Vector3 GetIdealVelocity()
+ {
+ if (characterDirection == null || characterBody == null)
+ return Vector3.zero;
+
+ return characterDirection.forward * characterBody.moveSpeed * speedMultiplier;
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Frozen;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallBoost.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallBoost.cs.meta
new file mode 100644
index 000000000..b12197928
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallBoost.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aa129d756bacbcf4c911777337d1580d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallMainState.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallMainState.cs
new file mode 100644
index 000000000..b01a1c56e
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallMainState.cs
@@ -0,0 +1,155 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Body main state for ball form. Replaces GenericCharacterMain while NemToolbot
+ /// is rolled up. Provides momentum-based movement with higher top speed, modified
+ /// air control, and velocity tracking for speed-based damage scaling.
+ ///
+ public class BallMainState : BaseCharacterMain
+ {
+ public static float moveSpeedMultiplier = 1.6f;
+ public static float ballAirControl = 0.15f;
+ public static float accelerationMultiplier = 0.6f;
+ public static float momentumDamping = 0.98f;
+
+ public static string enterSoundString = "";
+ public static string loopSoundString = "";
+ public static string exitSoundString = "";
+
+ private NemToolbotController controller;
+ private float originalMoveSpeed;
+ private float originalAcceleration;
+ private float originalAirControl;
+ private uint loopSoundID;
+
+ private static readonly int ballModeHash = Animator.StringToHash("BallMode");
+ private static readonly int aimWeightHash = Animator.StringToHash("aimWeight");
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot BallMainState: Failed to get NemToolbotController on " + gameObject.name);
+ }
+
+ // Store original motor parameters for restoration on exit
+ if (characterBody != null)
+ {
+ originalMoveSpeed = characterBody.baseMoveSpeed;
+ originalAcceleration = characterBody.baseAcceleration;
+ characterBody.baseMoveSpeed *= moveSpeedMultiplier;
+ characterBody.baseAcceleration *= accelerationMultiplier;
+ characterBody.statsDirty = true;
+ }
+
+ if (characterMotor != null)
+ {
+ originalAirControl = characterMotor.airControl;
+ characterMotor.airControl = ballAirControl;
+ }
+
+ if (NetworkServer.active && characterBody != null)
+ {
+ characterBody.AddBuff(RoR2Content.Buffs.ArmorBoost);
+ }
+
+ Debug.Log($"[NemToolbot] BallMainState: OnEnter (moveSpeed={characterBody?.baseMoveSpeed:F1}, accel={characterBody?.baseAcceleration:F1}, airControl={characterMotor?.airControl:F2})");
+
+ Util.PlaySound(enterSoundString, gameObject);
+ loopSoundID = Util.PlaySound(loopSoundString, gameObject);
+
+ PlayCrossfade("Body", "BallModeEnter", 0.1f);
+ if (modelAnimator != null)
+ {
+ modelAnimator.SetFloat(aimWeightHash, 0f);
+ }
+
+ if (modelLocator != null)
+ {
+ modelLocator.normalizeToFloor = true;
+ }
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ if (isAuthority && characterBody != null)
+ {
+ characterBody.isSprinting = true;
+ }
+
+ // Apply momentum damping for ball-like deceleration
+ if (isAuthority && characterMotor != null && characterMotor.isGrounded)
+ {
+ Vector3 velocity = characterMotor.velocity;
+ velocity.x *= momentumDamping;
+ velocity.z *= momentumDamping;
+ characterMotor.velocity = velocity;
+ }
+
+ // Regenerate ammo for all weapons while in ball form
+ if (NetworkServer.active && controller != null)
+ {
+ controller.RegenAllAmmo(GetDeltaTime());
+ }
+ }
+
+ public override void OnExit()
+ {
+ AkSoundEngine.StopPlayingID(loopSoundID);
+ Util.PlaySound(exitSoundString, gameObject);
+
+ if (characterBody != null)
+ {
+ characterBody.baseMoveSpeed = originalMoveSpeed;
+ characterBody.baseAcceleration = originalAcceleration;
+ characterBody.statsDirty = true;
+ characterBody.isSprinting = false;
+ }
+
+ if (characterMotor != null)
+ {
+ characterMotor.airControl = originalAirControl;
+ }
+
+ if (NetworkServer.active && characterBody != null)
+ {
+ characterBody.RemoveBuff(RoR2Content.Buffs.ArmorBoost);
+ }
+
+ if (NetworkServer.active && controller != null)
+ {
+ controller.ResetRegenAccumulators();
+ }
+
+ if (modelAnimator != null)
+ {
+ modelAnimator.SetFloat(aimWeightHash, 1f);
+ }
+
+ if (modelLocator != null)
+ {
+ modelLocator.normalizeToFloor = false;
+ }
+
+ Debug.Log($"[NemToolbot] BallMainState: OnExit (restoring moveSpeed={originalMoveSpeed:F1}, accel={originalAcceleration:F1}, airControl={originalAirControl:F2})");
+
+ PlayAnimation("Body", "BallModeExit");
+
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Skill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallMainState.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallMainState.cs.meta
new file mode 100644
index 000000000..8a359dc13
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallMainState.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1da9d000512fc894bbabdd7bf5bb9dd9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallSlam.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallSlam.cs
new file mode 100644
index 000000000..6230656b0
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallSlam.cs
@@ -0,0 +1,176 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Ball form primary (airborne). Slams downward similar to Loader's GroundSlam /
+ /// Overwatch Wrecking Ball's slam. Accelerates downward, detonates a BlastAttack
+ /// on ground contact with speed-scaled damage.
+ ///
+ public class BallSlam : BaseCharacterMain
+ {
+ public static float airControl = 0.15f;
+ public static float minimumDuration = 0.15f;
+ public static float verticalAcceleration = -80f;
+ public static float exitSlowdownCoefficient = 0.3f;
+ public static float exitVerticalVelocity = 8f;
+ public static float blastRadius = 12f;
+ public static float blastDamageCoefficient = 5f;
+ public static float blastProcCoefficient = 1f;
+ public static float blastForce = 2000f;
+ public static float maxDuration = 10f;
+ public static Vector3 blastBonusForce = Vector3.zero;
+
+ public static string enterSoundString = "";
+ public static GameObject blastEffectPrefab;
+ public static GameObject blastImpactEffectPrefab;
+ public static GameObject slamEffectPrefab;
+
+ private NemToolbotController controller;
+ private float previousAirControl;
+ private bool detonateNextFrame;
+ private GameObject slamEffectInstance;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot BallSlam: Failed to get NemToolbotController on " + gameObject.name);
+ }
+
+ PlayCrossfade("Body", "BallSlam", 0.1f);
+
+ if (isAuthority && characterMotor != null)
+ {
+ characterMotor.onMovementHit += OnMovementHit;
+ }
+
+ Util.PlaySound(enterSoundString, gameObject);
+
+ if (characterMotor != null)
+ {
+ previousAirControl = characterMotor.airControl;
+ characterMotor.airControl = airControl;
+ }
+
+ if (slamEffectPrefab != null)
+ {
+ Transform modelChild = FindModelChild("Root");
+ if (modelChild != null)
+ {
+ slamEffectInstance = Object.Instantiate(slamEffectPrefab, modelChild);
+ }
+ }
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ if (isAuthority && characterMotor != null)
+ {
+ // Allow limited steering during descent
+ characterMotor.moveDirection = inputBank.moveVector;
+ if (characterDirection != null)
+ {
+ characterDirection.moveVector = characterMotor.moveDirection;
+ }
+
+ // Accelerate downward faster than normal gravity
+ characterMotor.velocity.y += verticalAcceleration * GetDeltaTime();
+
+ // Safety timeout if we never hit the ground
+ if (fixedAge >= maxDuration)
+ {
+ DetonateAuthority();
+ outer.SetNextStateToMain();
+ return;
+ }
+
+ // Detonate when hitting the ground after minimum duration
+ if (fixedAge >= minimumDuration && (detonateNextFrame || characterMotor.Motor.GroundingStatus.IsStableOnGround))
+ {
+ DetonateAuthority();
+ outer.SetNextStateToMain();
+ }
+ }
+ }
+
+ public override void OnExit()
+ {
+ if (isAuthority && characterMotor != null)
+ {
+ characterMotor.onMovementHit -= OnMovementHit;
+ characterMotor.Motor.ForceUnground();
+ characterMotor.velocity *= exitSlowdownCoefficient;
+ characterMotor.velocity.y = exitVerticalVelocity;
+ }
+
+ if (characterMotor != null)
+ {
+ characterMotor.airControl = previousAirControl;
+ }
+
+ if (slamEffectInstance != null)
+ {
+ EntityState.Destroy(slamEffectInstance);
+ }
+
+ base.OnExit();
+ }
+
+ private void OnMovementHit(ref CharacterMotor.MovementHitInfo movementHitInfo)
+ {
+ detonateNextFrame = true;
+ }
+
+ private BlastAttack.Result DetonateAuthority()
+ {
+ if (!isAuthority)
+ return default;
+
+ Vector3 footPosition = characterBody.footPosition;
+
+ float speedMultiplier = controller != null ? controller.GetDamageMultiplierFromSpeed() : 1f;
+
+ if (blastEffectPrefab != null)
+ {
+ EffectManager.SpawnEffect(blastEffectPrefab, new EffectData
+ {
+ origin = footPosition,
+ scale = blastRadius
+ }, transmit: true);
+ }
+
+ BlastAttack blastAttack = new BlastAttack();
+ blastAttack.attacker = gameObject;
+ blastAttack.baseDamage = damageStat * blastDamageCoefficient * speedMultiplier;
+ blastAttack.baseForce = blastForce;
+ blastAttack.bonusForce = blastBonusForce;
+ blastAttack.crit = RollCrit();
+ blastAttack.damageType = DamageType.Stun1s;
+ blastAttack.falloffModel = BlastAttack.FalloffModel.None;
+ blastAttack.procCoefficient = blastProcCoefficient;
+ blastAttack.radius = blastRadius;
+ blastAttack.position = footPosition;
+ blastAttack.attackerFiltering = AttackerFiltering.NeverHitSelf;
+ if (blastImpactEffectPrefab != null)
+ {
+ blastAttack.impactEffect = EffectCatalog.FindEffectIndexFromPrefab(blastImpactEffectPrefab);
+ }
+ blastAttack.teamIndex = teamComponent.teamIndex;
+ blastAttack.damageType.damageSource = DamageSource.Special;
+
+ return blastAttack.Fire();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Frozen;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallSlam.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallSlam.cs.meta
new file mode 100644
index 000000000..771442fa0
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/BallSlam.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 01b19ffad6eb5ee4eaf69e96d597f97c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DeployCharges.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DeployCharges.cs
new file mode 100644
index 000000000..f7213c785
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DeployCharges.cs
@@ -0,0 +1,106 @@
+using RoR2;
+using RoR2.Projectile;
+using UnityEngine;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Utility skill shared between deployed and ball forms.
+ /// Drops 4 charge projectiles at cardinal directions around the character.
+ /// Each charge detonates after a delay (handled by the projectile prefab's
+ /// ProjectileImpactExplosion with lifetime).
+ ///
+ public class DeployCharges : BaseSkillState
+ {
+ public static float damageCoefficient = 4f;
+ public static float baseDuration = 0.5f;
+ public static float chargeSpawnRadius = 2f;
+ public static float force = 500f;
+
+ public static string soundString = "";
+ public static GameObject muzzleFlashPrefab;
+ public static GameObject chargeProjectilePrefab;
+
+ private float duration;
+
+ // Cardinal offsets relative to the character's forward direction
+ private static readonly Vector3[] cardinalOffsets = new Vector3[]
+ {
+ Vector3.forward,
+ Vector3.back,
+ Vector3.left,
+ Vector3.right
+ };
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+
+ Util.PlaySound(soundString, gameObject);
+ if (muzzleFlashPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleFlashPrefab, gameObject, "Muzzle", transmit: false);
+ }
+
+ PlayCrossfade("Gesture, Override", "DeployCharges", "DeployCharges.playbackRate", duration, 0.05f);
+
+ if (isAuthority)
+ {
+ FireCharges();
+ }
+ }
+
+ private void FireCharges()
+ {
+ if (chargeProjectilePrefab == null)
+ {
+ Debug.LogError("NemToolbot DeployCharges: chargeProjectilePrefab is null.");
+ return;
+ }
+
+ Vector3 footPosition = characterBody.footPosition;
+ Quaternion characterRotation = Quaternion.LookRotation(characterDirection != null ? characterDirection.forward : transform.forward, Vector3.up);
+
+ DamageTypeCombo damageType = DamageType.Generic;
+ damageType.damageSource = DamageSource.Utility;
+
+ for (int i = 0; i < cardinalOffsets.Length; i++)
+ {
+ Vector3 worldOffset = characterRotation * (cardinalOffsets[i] * chargeSpawnRadius);
+ Vector3 spawnPosition = footPosition + worldOffset;
+
+ FireProjectileInfo fireProjectileInfo = default(FireProjectileInfo);
+ fireProjectileInfo.projectilePrefab = chargeProjectilePrefab;
+ fireProjectileInfo.position = spawnPosition;
+ fireProjectileInfo.rotation = Quaternion.identity;
+ fireProjectileInfo.owner = gameObject;
+ fireProjectileInfo.damage = damageStat * damageCoefficient;
+ fireProjectileInfo.force = force;
+ fireProjectileInfo.crit = RollCrit();
+ fireProjectileInfo.damageTypeOverride = damageType;
+
+ ProjectileManager.instance.FireProjectile(fireProjectileInfo);
+ }
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority && fixedAge >= duration)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.PrioritySkill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DeployCharges.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DeployCharges.cs.meta
new file mode 100644
index 000000000..1d244d0ed
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DeployCharges.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3ae1d33864b399748b15e536cbe55183
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DispatchWeapon.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DispatchWeapon.cs
new file mode 100644
index 000000000..266863481
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DispatchWeapon.cs
@@ -0,0 +1,78 @@
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Primary skill dispatcher for deployed form.
+ /// Reads the currently selected weapon from NemToolbotController
+ /// and immediately transitions to the appropriate weapon entity state.
+ ///
+ public class DispatchWeapon : BaseState
+ {
+ private NemToolbotController controller;
+ private NemToolbotController.WeaponType cachedWeapon = NemToolbotController.WeaponType.RapidLaser;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+
+ if (isAuthority)
+ {
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot DispatchWeapon: Failed to get NemToolbotController on " + gameObject.name);
+ outer.SetNextStateToMain();
+ return;
+ }
+
+ cachedWeapon = controller.currentWeapon;
+
+ if (!controller.HasAmmo(cachedWeapon))
+ {
+ Debug.Log($"[NemToolbot] DispatchWeapon: No ammo for {cachedWeapon}, returning to main");
+ outer.SetNextStateToMain();
+ return;
+ }
+
+ Debug.Log($"[NemToolbot] DispatchWeapon: Firing {cachedWeapon} (ammo: {controller.GetAmmo(cachedWeapon)})");
+ EntityState nextState = cachedWeapon switch
+ {
+ NemToolbotController.WeaponType.Shotgun => new FireShotgun(),
+ NemToolbotController.WeaponType.RapidLaser => new FireRapidLaser(),
+ NemToolbotController.WeaponType.GrenadeLauncher => new FireGrenadeLauncher(),
+ NemToolbotController.WeaponType.SniperLaser => new FireSniperLaser(),
+ _ => new FireRapidLaser()
+ };
+ outer.SetNextState(nextState);
+ }
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnSerialize(NetworkWriter writer)
+ {
+ base.OnSerialize(writer);
+ writer.Write((byte)cachedWeapon);
+ }
+
+ public override void OnDeserialize(NetworkReader reader)
+ {
+ base.OnDeserialize(reader);
+ cachedWeapon = (NemToolbotController.WeaponType)reader.ReadByte();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Any;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DispatchWeapon.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DispatchWeapon.cs.meta
new file mode 100644
index 000000000..b04d3a3bc
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/DispatchWeapon.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f48ea39468614b04aa0d2dad625b3bbd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrapplingHook.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrapplingHook.cs
new file mode 100644
index 000000000..70e7d7e62
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrapplingHook.cs
@@ -0,0 +1,171 @@
+using RoR2;
+using RoR2.Projectile;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Ball form secondary. Fires a grappling hook projectile that pulls NemToolbot
+ /// toward the impact point, following Loader's FireHook pattern. The hook projectile
+ /// handles the actual pull physics; this state manages animation and lifetime.
+ ///
+ /// Key difference from Loader: pressing the skill button again disconnects the hook.
+ ///
+ public class FireGrapplingHook : BaseSkillState
+ {
+ public static GameObject projectilePrefab;
+
+ public static float damageCoefficient = 1f;
+ public static string fireSoundString = "";
+ public static string disconnectSoundString = "";
+ public static GameObject muzzleflashEffectPrefab;
+ public static string muzzleString = "Muzzle";
+ public static float maxDuration = 8f;
+
+ ///
+ /// Reference to the live hook projectile. Set by the projectile via SetHookReference().
+ /// When this becomes null (projectile destroyed), the state exits.
+ ///
+ public GameObject hookInstance;
+
+ protected ProjectileStickOnImpact hookStickOnImpact;
+ private bool isStuck;
+ private bool hadHookInstance;
+ private bool hasReleasedButton;
+
+ private static readonly int FireHookIntroHash = Animator.StringToHash("FireHookIntro");
+ private static readonly int FireHookLoopHash = Animator.StringToHash("FireHookLoop");
+ private static readonly int FireHookExitHash = Animator.StringToHash("FireHookExit");
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+
+ if (isAuthority)
+ {
+ Ray ray = GetAimRay();
+ if (projectilePrefab != null)
+ {
+ TrajectoryAimAssist.ApplyTrajectoryAimAssist(ref ray, projectilePrefab, gameObject);
+
+ FireProjectileInfo fireProjectileInfo = default(FireProjectileInfo);
+ fireProjectileInfo.position = ray.origin;
+ fireProjectileInfo.rotation = Quaternion.LookRotation(ray.direction);
+ fireProjectileInfo.crit = characterBody.RollCrit();
+ fireProjectileInfo.damage = damageStat * damageCoefficient;
+ fireProjectileInfo.force = 0f;
+ fireProjectileInfo.damageColorIndex = DamageColorIndex.Default;
+ fireProjectileInfo.damageTypeOverride = DamageTypeCombo.GenericSecondary;
+ fireProjectileInfo.procChainMask = default(ProcChainMask);
+ fireProjectileInfo.projectilePrefab = projectilePrefab;
+ fireProjectileInfo.owner = gameObject;
+
+ ProjectileManager.instance.FireProjectile(fireProjectileInfo);
+ }
+ else
+ {
+ Debug.LogError("NemToolbot FireGrapplingHook: projectilePrefab is null.");
+ outer.SetNextStateToMain();
+ return;
+ }
+ }
+
+ if (muzzleflashEffectPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleflashEffectPrefab, gameObject, muzzleString, transmit: false);
+ }
+ Util.PlaySound(fireSoundString, gameObject);
+
+ PlayAnimation("Grapple", FireHookIntroHash);
+ }
+
+ ///
+ /// Called by the hook projectile to register itself with this state.
+ /// The projectile finds its owner's FireGrapplingHook state and hands back a reference.
+ ///
+ public void SetHookReference(GameObject hook)
+ {
+ hookInstance = hook;
+ if (hook.TryGetComponent(out ProjectileStickOnImpact stickComponent))
+ {
+ hookStickOnImpact = stickComponent;
+ }
+ else
+ {
+ Debug.LogError("NemToolbot FireGrapplingHook: Hook projectile missing ProjectileStickOnImpact on " + hook.name);
+ }
+ hadHookInstance = true;
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ // Transition animation when hook sticks
+ if (hookStickOnImpact != null)
+ {
+ if (hookStickOnImpact.stuck && !isStuck)
+ {
+ PlayAnimation("Grapple", FireHookLoopHash);
+ }
+ isStuck = hookStickOnImpact.stuck;
+ }
+
+ if (!isAuthority)
+ return;
+
+ // Track button release so we can detect a second press to disconnect
+ if (!hasReleasedButton && !IsKeyDownAuthority())
+ {
+ hasReleasedButton = true;
+ }
+
+ // Disconnect on second press (button was released then pressed again)
+ if (hasReleasedButton && IsKeyDownAuthority() && hookInstance != null)
+ {
+ Util.PlaySound(disconnectSoundString, gameObject);
+ EntityState.Destroy(hookInstance);
+ hookInstance = null;
+ }
+
+ // Failsafe: exit if hook was never linked (e.g. missing ModelLocator/ChildLocator)
+ if (fixedAge >= maxDuration)
+ {
+ outer.SetNextStateToMain();
+ return;
+ }
+
+ // Exit when hook projectile existed and is now destroyed
+ if (!hookInstance && hadHookInstance)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ PlayAnimation("Grapple", FireHookExitHash);
+ if (muzzleflashEffectPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleflashEffectPrefab, gameObject, muzzleString, transmit: false);
+ }
+
+ // Clean up hook if still alive when state is forcibly exited
+ if (hookInstance != null)
+ {
+ if (NetworkServer.active)
+ {
+ EntityState.Destroy(hookInstance);
+ }
+ }
+
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Pain;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrapplingHook.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrapplingHook.cs.meta
new file mode 100644
index 000000000..2386c0505
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrapplingHook.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ba489bb3e86677a4788911256ead21ef
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrenadeLauncher.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrenadeLauncher.cs
new file mode 100644
index 000000000..e45fa708c
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrenadeLauncher.cs
@@ -0,0 +1,101 @@
+using RoR2;
+using RoR2.Projectile;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ public class FireGrenadeLauncher : BaseSkillState
+ {
+ public static float damageCoefficient = 3f;
+ public static float force = 1500f;
+ public static float baseDuration = 0.6f;
+ public static float recoilAmplitude = 1.5f;
+ public static float spreadBloomValue = 1f;
+
+ public static string muzzleString = "Muzzle";
+ public static string soundString = "";
+ public static GameObject muzzleFlashPrefab;
+ public static GameObject projectilePrefab;
+
+ private float duration;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+ characterBody.SetAimTimer(duration + 1f);
+
+ if (isAuthority)
+ {
+ if (!gameObject.TryGetComponent(out NemToolbotController controller))
+ {
+ Debug.LogError("FireGrenadeLauncher: NemToolbotController not found.");
+ }
+ else if (NetworkServer.active && !controller.TryConsumeAmmo(NemToolbotController.WeaponType.GrenadeLauncher))
+ {
+ outer.SetNextStateToMain();
+ return;
+ }
+ }
+
+ Util.PlaySound(soundString, gameObject);
+ if (muzzleFlashPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleFlashPrefab, gameObject, muzzleString, transmit: false);
+ }
+
+ PlayCrossfade("Gesture, Override", "FireGrenade", "FireGrenade.playbackRate", duration, 0.05f);
+ AddRecoil(-1f * recoilAmplitude, -1.5f * recoilAmplitude, -0.25f * recoilAmplitude, 0.25f * recoilAmplitude);
+
+ if (isAuthority)
+ {
+ Ray aimRay = GetAimRay();
+
+ if (projectilePrefab != null)
+ {
+ DamageTypeCombo damageType = DamageType.Generic;
+ damageType.damageSource = DamageSource.Primary;
+
+ FireProjectileInfo fireProjectileInfo = default(FireProjectileInfo);
+ fireProjectileInfo.projectilePrefab = projectilePrefab;
+ fireProjectileInfo.position = aimRay.origin;
+ fireProjectileInfo.rotation = Util.QuaternionSafeLookRotation(aimRay.direction);
+ fireProjectileInfo.owner = gameObject;
+ fireProjectileInfo.damage = damageStat * damageCoefficient;
+ fireProjectileInfo.force = force;
+ fireProjectileInfo.crit = RollCrit();
+ fireProjectileInfo.damageTypeOverride = damageType;
+
+ ProjectileManager.instance.FireProjectile(fireProjectileInfo);
+ }
+ else
+ {
+ Debug.LogError("NemToolbot FireGrenadeLauncher: projectilePrefab is null.");
+ }
+ }
+
+ characterBody.AddSpreadBloom(spreadBloomValue);
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority && fixedAge >= duration)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Skill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrenadeLauncher.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrenadeLauncher.cs.meta
new file mode 100644
index 000000000..2ea678972
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireGrenadeLauncher.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bce995f90d3664e4fa59c2420f76ec67
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireRapidLaser.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireRapidLaser.cs
new file mode 100644
index 000000000..2349b6810
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireRapidLaser.cs
@@ -0,0 +1,130 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ public class FireRapidLaser : BaseSkillState
+ {
+ public static float damageCoefficient = 0.8f;
+ public static float procCoefficient = 0.7f;
+ public static float baseFireInterval = 0.12f;
+ public static float force = 100f;
+ public static float recoilAmplitude = 0.4f;
+ public static float spreadBloomValue = 0.3f;
+ public static float maxDistance = 60f;
+ public static float bulletRadius = 0.4f;
+ public static float baseDuration = 0.1f;
+
+ public static string muzzleString = "Muzzle";
+ public static string soundString = "";
+ public static GameObject muzzleFlashPrefab;
+ public static GameObject tracerEffectPrefab;
+ public static GameObject hitEffectPrefab;
+
+ private NemToolbotController controller;
+ private float fireTimer;
+ private float duration;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+ characterBody.SetAimTimer(2f);
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("FireRapidLaser: NemToolbotController not found.");
+ }
+
+ PlayCrossfade("Gesture, Override", "FireRapidLaser", "FireRapidLaser.playbackRate", duration, 0.05f);
+ FireBullet();
+ }
+
+ private void FireBullet()
+ {
+ fireTimer = baseFireInterval / attackSpeedStat;
+
+ if (isAuthority && controller != null && NetworkServer.active && !controller.TryConsumeAmmo(NemToolbotController.WeaponType.RapidLaser))
+ {
+ outer.SetNextStateToMain();
+ return;
+ }
+
+ Util.PlaySound(soundString, gameObject);
+ if (muzzleFlashPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleFlashPrefab, gameObject, muzzleString, transmit: false);
+ }
+
+ AddRecoil(-0.5f * recoilAmplitude, -0.8f * recoilAmplitude, -0.3f * recoilAmplitude, 0.3f * recoilAmplitude);
+
+ if (isAuthority)
+ {
+ Ray aimRay = GetAimRay();
+ DamageTypeCombo damageType = DamageType.Generic;
+ damageType.damageSource = DamageSource.Primary;
+
+ BulletAttack bulletAttack = new BulletAttack
+ {
+ bulletCount = 1,
+ aimVector = aimRay.direction,
+ origin = aimRay.origin,
+ damage = damageCoefficient * damageStat,
+ damageColorIndex = DamageColorIndex.Default,
+ damageType = damageType,
+ falloffModel = BulletAttack.FalloffModel.DefaultBullet,
+ maxDistance = maxDistance,
+ force = force,
+ hitMask = LayerIndex.CommonMasks.bullet,
+ minSpread = 0f,
+ maxSpread = characterBody.spreadBloomAngle,
+ isCrit = RollCrit(),
+ owner = gameObject,
+ muzzleName = muzzleString,
+ smartCollision = true,
+ procChainMask = default(ProcChainMask),
+ procCoefficient = procCoefficient,
+ radius = bulletRadius,
+ sniper = false,
+ stopperMask = LayerIndex.CommonMasks.bullet,
+ weapon = gameObject,
+ tracerEffectPrefab = tracerEffectPrefab,
+ spreadPitchScale = 1f,
+ spreadYawScale = 1f,
+ hitEffectPrefab = hitEffectPrefab
+ };
+ bulletAttack.Fire();
+ }
+
+ characterBody.AddSpreadBloom(spreadBloomValue);
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ fireTimer -= GetDeltaTime();
+ if (fireTimer <= 0f)
+ {
+ FireBullet();
+ }
+
+ if (isAuthority && !IsKeyDownAuthority())
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Skill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireRapidLaser.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireRapidLaser.cs.meta
new file mode 100644
index 000000000..542a440b1
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireRapidLaser.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f59aef8b8afae8a40a0d11288538ecd3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireShotgun.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireShotgun.cs
new file mode 100644
index 000000000..4ab1edd70
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireShotgun.cs
@@ -0,0 +1,117 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ public class FireShotgun : BaseSkillState
+ {
+ public static float damageCoefficient = 1.2f;
+ public static float procCoefficient = 0.5f;
+ public static float baseDuration = 0.6f;
+ public static float force = 300f;
+ public static float recoilAmplitude = 3f;
+ public static float spreadBloomValue = 1.5f;
+ public static int pelletCount = 6;
+ public static float spreadAngle = 10f;
+ public static float maxDistance = 12f;
+ public static float bulletRadius = 0.3f;
+
+ public static string muzzleString = "Muzzle";
+ public static string soundString = "";
+ public static GameObject muzzleFlashPrefab;
+ public static GameObject tracerEffectPrefab;
+ public static GameObject hitEffectPrefab;
+
+ private float duration;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+ characterBody.SetAimTimer(duration + 1f);
+
+ if (isAuthority)
+ {
+ if (!gameObject.TryGetComponent(out NemToolbotController controller))
+ {
+ Debug.LogError("FireShotgun: NemToolbotController not found.");
+ }
+ else if (NetworkServer.active && !controller.TryConsumeAmmo(NemToolbotController.WeaponType.Shotgun))
+ {
+ outer.SetNextStateToMain();
+ return;
+ }
+ }
+
+ Util.PlaySound(soundString, gameObject);
+ if (muzzleFlashPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleFlashPrefab, gameObject, muzzleString, transmit: false);
+ }
+
+ PlayCrossfade("Gesture, Override", "FireShotgun", "FireShotgun.playbackRate", duration, 0.05f);
+ AddRecoil(-2f * recoilAmplitude, -3f * recoilAmplitude, -0.5f * recoilAmplitude, 0.5f * recoilAmplitude);
+
+ if (isAuthority)
+ {
+ Ray aimRay = GetAimRay();
+ DamageTypeCombo damageType = DamageType.Generic;
+ damageType.damageSource = DamageSource.Primary;
+
+ BulletAttack bulletAttack = new BulletAttack
+ {
+ bulletCount = (uint)pelletCount,
+ aimVector = aimRay.direction,
+ origin = aimRay.origin,
+ damage = damageCoefficient * damageStat,
+ damageColorIndex = DamageColorIndex.Default,
+ damageType = damageType,
+ falloffModel = BulletAttack.FalloffModel.Buckshot,
+ maxDistance = maxDistance,
+ force = force,
+ hitMask = LayerIndex.CommonMasks.bullet,
+ minSpread = 0f,
+ maxSpread = spreadAngle,
+ isCrit = RollCrit(),
+ owner = gameObject,
+ muzzleName = muzzleString,
+ smartCollision = true,
+ procChainMask = default(ProcChainMask),
+ procCoefficient = procCoefficient,
+ radius = bulletRadius,
+ sniper = false,
+ stopperMask = LayerIndex.CommonMasks.bullet,
+ weapon = gameObject,
+ tracerEffectPrefab = tracerEffectPrefab,
+ spreadPitchScale = 1f,
+ spreadYawScale = 1f,
+ hitEffectPrefab = hitEffectPrefab
+ };
+ bulletAttack.Fire();
+ }
+
+ characterBody.AddSpreadBloom(spreadBloomValue);
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority && fixedAge >= duration)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Skill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireShotgun.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireShotgun.cs.meta
new file mode 100644
index 000000000..e6ca7b3a6
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireShotgun.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 17b0c7bc00382784fbd9b8a56970264a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireSniperLaser.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireSniperLaser.cs
new file mode 100644
index 000000000..336c99740
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireSniperLaser.cs
@@ -0,0 +1,115 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ public class FireSniperLaser : BaseSkillState
+ {
+ public static float damageCoefficient = 6f;
+ public static float procCoefficient = 1f;
+ public static float baseDuration = 0.8f;
+ public static float force = 2500f;
+ public static float recoilAmplitude = 4f;
+ public static float spreadBloomValue = 2f;
+ public static float maxDistance = 1000f;
+ public static float bulletRadius = 0.5f;
+
+ public static string muzzleString = "Muzzle";
+ public static string soundString = "";
+ public static GameObject muzzleFlashPrefab;
+ public static GameObject tracerEffectPrefab;
+ public static GameObject hitEffectPrefab;
+
+ private float duration;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+ characterBody.SetAimTimer(duration + 2f);
+
+ if (isAuthority)
+ {
+ if (!gameObject.TryGetComponent(out NemToolbotController controller))
+ {
+ Debug.LogError("FireSniperLaser: NemToolbotController not found.");
+ }
+ else if (NetworkServer.active && !controller.TryConsumeAmmo(NemToolbotController.WeaponType.SniperLaser))
+ {
+ outer.SetNextStateToMain();
+ return;
+ }
+ }
+
+ Util.PlaySound(soundString, gameObject);
+ if (muzzleFlashPrefab != null)
+ {
+ EffectManager.SimpleMuzzleFlash(muzzleFlashPrefab, gameObject, muzzleString, transmit: false);
+ }
+
+ PlayCrossfade("Gesture, Override", "FireSniper", "FireSniper.playbackRate", duration, 0.05f);
+ AddRecoil(-2f * recoilAmplitude, -3f * recoilAmplitude, -0.5f * recoilAmplitude, 0.5f * recoilAmplitude);
+
+ if (isAuthority)
+ {
+ Ray aimRay = GetAimRay();
+ DamageTypeCombo damageType = DamageType.Generic;
+ damageType.damageSource = DamageSource.Primary;
+
+ BulletAttack bulletAttack = new BulletAttack
+ {
+ bulletCount = 1,
+ aimVector = aimRay.direction,
+ origin = aimRay.origin,
+ damage = damageCoefficient * damageStat,
+ damageColorIndex = DamageColorIndex.Default,
+ damageType = damageType,
+ falloffModel = BulletAttack.FalloffModel.None,
+ maxDistance = maxDistance,
+ force = force,
+ hitMask = LayerIndex.CommonMasks.bullet,
+ minSpread = 0f,
+ maxSpread = 0f,
+ isCrit = RollCrit(),
+ owner = gameObject,
+ muzzleName = muzzleString,
+ smartCollision = true,
+ procChainMask = default(ProcChainMask),
+ procCoefficient = procCoefficient,
+ radius = bulletRadius,
+ sniper = true,
+ stopperMask = LayerIndex.world.mask,
+ weapon = gameObject,
+ tracerEffectPrefab = tracerEffectPrefab,
+ spreadPitchScale = 0f,
+ spreadYawScale = 0f,
+ hitEffectPrefab = hitEffectPrefab
+ };
+ bulletAttack.Fire();
+ }
+
+ characterBody.AddSpreadBloom(spreadBloomValue);
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority && fixedAge >= duration)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.PrioritySkill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireSniperLaser.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireSniperLaser.cs.meta
new file mode 100644
index 000000000..7d70d2b30
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/FireSniperLaser.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d02824ba7a23f504189609f26705a297
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/RangeFinder.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/RangeFinder.cs
new file mode 100644
index 000000000..d2bfcf818
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/RangeFinder.cs
@@ -0,0 +1,130 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Secondary skill for deployed form.
+ /// Hold to project a laser from the muzzle. The laser raycasts and shows the
+ /// range to the first surface/enemy hit, along with which weapon that range
+ /// would select. On release, sets the weapon on NemToolbotController.
+ ///
+ public class RangeFinder : BaseSkillState
+ {
+ public static float maxRange = 200f;
+ public static string muzzleString = "Muzzle";
+ public static string enterSoundString = "";
+ public static string exitSoundString = "";
+
+ private static Material laserMaterial;
+
+ private NemToolbotController controller;
+ private GameObject laserObject;
+ private LineRenderer lineRenderer;
+ private Transform muzzleTransform;
+ private float hitDistance;
+ private NemToolbotController.WeaponType pendingWeapon;
+ private Vector3 lastLaserOrigin;
+ private Vector3 lastLaserEndPoint;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot RangeFinder: Failed to get NemToolbotController on " + gameObject.name);
+ }
+
+ Util.PlaySound(enterSoundString, gameObject);
+ characterBody.SetAimTimer(3f);
+
+ muzzleTransform = FindModelChild(muzzleString);
+
+ if (laserMaterial == null)
+ {
+ laserMaterial = Addressables.LoadAssetAsync("RoR2/Base/Common/VFX/matTracerBright.mat").WaitForCompletion();
+ }
+
+ laserObject = new GameObject("NemToolbotRangeFinderLaser");
+ lineRenderer = laserObject.AddComponent();
+ lineRenderer.positionCount = 2;
+ lineRenderer.startWidth = 0.05f;
+ lineRenderer.endWidth = 0.05f;
+ lineRenderer.material = laserMaterial;
+ lineRenderer.startColor = Color.red;
+ lineRenderer.endColor = Color.red;
+ lineRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
+ lineRenderer.receiveShadows = false;
+ lineRenderer.textureMode = LineTextureMode.Tile;
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ Ray aimRay = GetAimRay();
+ Vector3 endPoint;
+
+ if (Physics.Raycast(aimRay, out RaycastHit hit, maxRange, LayerIndex.CommonMasks.bullet, QueryTriggerInteraction.Ignore))
+ {
+ hitDistance = hit.distance;
+ endPoint = hit.point;
+ }
+ else
+ {
+ hitDistance = maxRange;
+ endPoint = aimRay.GetPoint(maxRange);
+ }
+
+ pendingWeapon = NemToolbotController.GetWeaponFromRange(hitDistance);
+
+ lastLaserOrigin = aimRay.origin;
+ lastLaserEndPoint = endPoint;
+
+ if (isAuthority && !IsKeyDownAuthority())
+ {
+ Debug.Log($"[NemToolbot] RangeFinder: Released at {hitDistance:F1}m -> selecting {pendingWeapon}");
+ if (NetworkServer.active && controller != null)
+ {
+ controller.SetWeapon(pendingWeapon);
+ }
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void Update()
+ {
+ base.Update();
+ UpdateLaserVisuals(lastLaserOrigin, lastLaserEndPoint);
+ }
+
+ private void UpdateLaserVisuals(Vector3 origin, Vector3 endPoint)
+ {
+ if (lineRenderer != null)
+ {
+ Vector3 start = muzzleTransform != null ? muzzleTransform.position : origin;
+ lineRenderer.SetPosition(0, start);
+ lineRenderer.SetPosition(1, endPoint);
+ }
+ }
+
+ public override void OnExit()
+ {
+ Util.PlaySound(exitSoundString, gameObject);
+ if (laserObject != null)
+ {
+ EntityState.Destroy(laserObject);
+ }
+ base.OnExit();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.PrioritySkill;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/RangeFinder.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/RangeFinder.cs.meta
new file mode 100644
index 000000000..80e5d6de0
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/RangeFinder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9a51897a3bd884c45bfef777150fcb53
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToBall.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToBall.cs
new file mode 100644
index 000000000..20b9353ab
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToBall.cs
@@ -0,0 +1,73 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Deployed form special. Plays a transition animation then sets the body state
+ /// to BallMainState. Follows the ToolbotStanceSwap timing pattern.
+ ///
+ public class StanceSwapToBall : BaseState
+ {
+ [SerializeField]
+ public float baseDuration = 0.5f;
+
+ public static string enterSoundString = "";
+
+ private float duration;
+ private NemToolbotController controller;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot StanceSwapToBall: Failed to get NemToolbotController on " + gameObject.name);
+ }
+
+ Debug.Log($"[NemToolbot] StanceSwapToBall: Entering ball form (duration={duration:F2}s)");
+ if (NetworkServer.active && controller != null)
+ {
+ controller.SetBallForm(true);
+ }
+
+ Util.PlaySound(enterSoundString, gameObject);
+ PlayCrossfade("Body", "TransformToBall", "StanceSwap.playbackRate", duration, 0.1f);
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority && fixedAge >= duration)
+ {
+ outer.SetNextState(new BallMainState());
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override void OnSerialize(NetworkWriter writer)
+ {
+ base.OnSerialize(writer);
+ writer.Write(fixedAge);
+ }
+
+ public override void OnDeserialize(NetworkReader reader)
+ {
+ base.OnDeserialize(reader);
+ fixedAge = reader.ReadSingle();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Frozen;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToBall.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToBall.cs.meta
new file mode 100644
index 000000000..682875488
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToBall.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a19ad93d926de2b46bac1330060ebc1d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToDeployed.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToDeployed.cs
new file mode 100644
index 000000000..f3668dffc
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToDeployed.cs
@@ -0,0 +1,74 @@
+using RoR2;
+using SS2.Components;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace EntityStates.NemToolbot
+{
+ ///
+ /// Ball form special. Plays a transition animation then returns the body state
+ /// to main (GenericCharacterMain / deployed main state). Follows the ToolbotStanceSwap
+ /// timing pattern.
+ ///
+ public class StanceSwapToDeployed : BaseState
+ {
+ [SerializeField]
+ public float baseDuration = 0.5f;
+
+ public static string enterSoundString = "";
+
+ private float duration;
+ private NemToolbotController controller;
+
+ public override void OnEnter()
+ {
+ base.OnEnter();
+ duration = baseDuration / attackSpeedStat;
+
+ if (!gameObject.TryGetComponent(out controller))
+ {
+ Debug.LogError("NemToolbot StanceSwapToDeployed: Failed to get NemToolbotController on " + gameObject.name);
+ }
+
+ Debug.Log($"[NemToolbot] StanceSwapToDeployed: Exiting ball form (duration={duration:F2}s)");
+ if (NetworkServer.active && controller != null)
+ {
+ controller.SetBallForm(false);
+ }
+
+ Util.PlaySound(enterSoundString, gameObject);
+ PlayCrossfade("Body", "TransformToDeployed", "StanceSwap.playbackRate", duration, 0.1f);
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+ if (isAuthority && fixedAge >= duration)
+ {
+ outer.SetNextStateToMain();
+ }
+ }
+
+ public override void OnExit()
+ {
+ base.OnExit();
+ }
+
+ public override void OnSerialize(NetworkWriter writer)
+ {
+ base.OnSerialize(writer);
+ writer.Write(fixedAge);
+ }
+
+ public override void OnDeserialize(NetworkReader reader)
+ {
+ base.OnDeserialize(reader);
+ fixedAge = reader.ReadSingle();
+ }
+
+ public override InterruptPriority GetMinimumInterruptPriority()
+ {
+ return InterruptPriority.Frozen;
+ }
+ }
+}
diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToDeployed.cs.meta b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToDeployed.cs.meta
new file mode 100644
index 000000000..82f0a8dfc
--- /dev/null
+++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/NemToolbot/StanceSwapToDeployed.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9dedcb401bb1acb43911cd6d8359937d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/SS2-Project/Assets/ThunderKitSettings/ThunderKitSettings.asset b/SS2-Project/Assets/ThunderKitSettings/ThunderKitSettings.asset
index 09659ba2e..96f677294 100644
--- a/SS2-Project/Assets/ThunderKitSettings/ThunderKitSettings.asset
+++ b/SS2-Project/Assets/ThunderKitSettings/ThunderKitSettings.asset
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f9057f7f601990a4883b2cb295c4f43e, type: 3}
m_Name: ThunderKitSettings
m_EditorClassIdentifier:
- FirstLoad: 0
+ FirstLoad: 1
ShowOnStartup: 1
GameExecutable:
GamePath: